platform/README.md
nholtware 4b6f898089 chore: FORGEJO_URL and clone URL on https://git.lab.holtlabs.xyz (Plan 12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 12:05:46 -06:00

101 lines
5 KiB
Markdown

# 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`).
```sh
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
```sh
git clone https://git.lab.holtlabs.xyz/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.