platform/README.md
nholtware 2a6971df03 feat(new-service): render __DOMAIN__ from INGRESS_DOMAIN; HTTPS URLs and health checks
Plan 11 step 7. INGRESS_DOMAIN=lab.holtlabs.xyz; health checks pin the host
to INGRESS_IP on 443 and verify the wildcard cert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-10 13:15:00 -06:00

5 KiB

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; __APP__, __API__, __DOMAIN__) → 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 https://NAME.$INGRESS_DOMAIN/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 https://argocd. / kibana. / unleash.lab.holtlabs.xyz (INGRESS_DOMAIN). Credentials and conventions: homelab PLATFORM.md.
  • INGRESS_DOMAIN (lab.holtlabs.xyz) fills __DOMAIN__ in the gitops templates, so services get https://NAME.$INGRESS_DOMAIN/. The wildcard DNS record and the wildcard certificate are cluster-level (Plan 11); a new service creates no DNS record and requests no certificate. Health checks pin the host to INGRESS_IP with curl --resolve but still verify TLS.