new-service / delete-service scaffolder
Find a file
nholtware 253b6033c3 feat: new-service and delete-service scaffolder
One command from nothing to a running service: generate repo from template,
Actions secrets, namespace + unleash token, gitops render, kick CI, register
with ArgoCD once the first image exists, optional --wait health check.
delete-service reverses all of it. Idempotent, --dry-run supported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:03:55 -06:00
bin feat: new-service and delete-service scaffolder 2026-11-05 12:03:55 -06:00
lib feat: new-service and delete-service scaffolder 2026-11-05 12:03:55 -06:00
.gitignore feat: new-service and delete-service scaffolder 2026-11-05 12:03:55 -06:00
.shellcheckrc feat: new-service and delete-service scaffolder 2026-11-05 12:03:55 -06:00
platform.env.example feat: new-service and delete-service scaffolder 2026-11-05 12:03:55 -06:00
README.md feat: new-service and delete-service scaffolder 2026-11-05 12:03:55 -06:00

platform

One command from nothing to a running, observed service on the homelab k3s cluster, and one to take it all away again. Bash + curl, jq, git, kubectl (kustomize optional; falls back to kubectl kustomize).

new-service orders --kind backend --with postgres,redis --wait
new-service orders-web --kind frontend --api orders --wait
delete-service orders-web --yes && delete-service orders --yes

Setup

git clone http://192.168.1.230:3000/nholtware/platform.git ~/Code/forgejo.local/nholtware/platform
cd ~/Code/forgejo.local/nholtware/platform
cp platform.env.example platform.env      # gitignored; defaults match the homelab
export PATH="$PWD/bin:$PATH"              # not installed globally

platform.env points at the homelab repo's gitignored .secrets/ token files, the gitops clone, the kubeconfig and the ingress domain. The scripts refuse to run if any of them is missing, the gitops clone is dirty or not on main, the Forgejo token is rejected, or the cluster is unreachable.

new-service

new-service NAME --kind backend|frontend [--with postgres,redis] [--api BACKEND]
            [--wait] [--timeout SECONDS] [--dry-run]
Step What Skipped when
1 validate NAME (^[a-z][a-z0-9-]{1,30}$, not reserved), kind, flags; --api must be an existing backend —
2 generate public repo from service-template-express / service-template-react repo exists
3 set Actions secrets REGISTRY_USER, REGISTRY_TOKEN, GITOPS_TOKEN never (PUT is idempotent)
4 create namespace + Secret unleash-client-token (copied from unleash/unleash-init-tokens) they exist
5 render gitops templates/<kind> (+ postgres, redis) → manifests/NAME, kustomize build lint, commit, push dir exists
6 clone repo to $CODE_DIR/NAME, push empty commit chore: kick ci CI already deployed, or a kick is running/succeeded
7 wait for CI's gitops-bump to replace newTag: latest (fails fast if the CI run fails) tag already bumped
8 write apps/NAME.yaml, commit, push, refresh ArgoCD root file exists
9 --wait: ArgoCD app Synced/Healthy, then /readyz (backend) or /healthz + /api/readyz (frontend) → 200 no --wait

Then it prints the service, ArgoCD, Kibana (pre-filtered on service:"NAME") and Unleash URLs, and the elapsed time.

Why apps/NAME.yaml comes last: ArgoCD only sees the service once CI has pushed a real image. Registering it earlier made the backend's migrate Job sit in ImagePullBackOff on :latest and hold the sync forever (Plan 04 caveat).

Resumable: every step checks before acting. If a run dies (CI failure, timeout, Ctrl-C), fix the cause and re-run the same command; a failed CI run is re-kicked automatically.

Kinds:

  • backend — Express/TS. Runtime config (NAME-runtime-config ConfigMap) is always included. --with postgres adds a CNPG NAME-db cluster + migrate Job, --with redis adds NAME-redis. Without them the env vars and Job are dropped.
  • frontend — Vite/React on nginx. --api BACKEND deploys into the backend's namespace and routes NAME.<domain>/api/* to it (prefix stripped). Without --api it gets its own namespace and no /api route.

delete-service

delete-service NAME [--yes] [--dry-run] [--timeout SECONDS]

Removes apps/NAME.yaml + manifests/NAME from gitops (the Application's resources-finalizer makes ArgoCD delete its workloads when root prunes it), waits for the Application to go, deletes the namespace if it is the service's own (CNPG PVCs go with it), deletes every version of the container package and the Forgejo repo, and removes the local clone. Asks you to type the name unless --yes. Refuses while a frontend still deploys into the service's namespace (delete the frontend first). Leaves a shared namespace and its unleash-client-token Secret alone.

--dry-run

Runs every read-only check for real (config, repo exists?, namespace exists?, render + kustomize build lint into a temp dir) and prints each mutating API call (secret values redacted), git push and kubectl change instead of doing it.

Notes

  • The Forgejo admin token has no package scope; package list/delete uses the registry push token (same user).
  • Git pushes use the admin token through an inline credential helper; it is never written into a remote URL or argv.
  • Platform UIs: ArgoCD, Kibana and Unleash at argocd. / kibana. / unleash.192.168.1.83.nip.io. Credentials and conventions: homelab PLATFORM.md.