feat: APPS_DIR environment layer (Plan 14 step 2)

new-service/delete-service read and write $GITOPS_DIR/$APPS_DIR (default apps-homelab)
instead of the literal apps/; FORGEJO_URL moves to git.holtlabs.xyz.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nholtware 2026-11-11 14:01:32 -06:00
parent 5b4987a371
commit 11c68770e1
5 changed files with 23 additions and 19 deletions

View file

@ -13,7 +13,7 @@ delete-service orders-web --yes && delete-service orders --yes
## Setup ## Setup
```sh ```sh
git clone https://git.lab.holtlabs.xyz/nholtware/platform.git ~/Code/forgejo.local/nholtware/platform git clone https://git.holtlabs.xyz/nholtware/platform.git ~/Code/forgejo.local/nholtware/platform
cd ~/Code/forgejo.local/nholtware/platform cd ~/Code/forgejo.local/nholtware/platform
cp platform.env.example platform.env # gitignored; defaults match the homelab cp platform.env.example platform.env # gitignored; defaults match the homelab
export PATH="$PWD/bin:$PATH" # not installed globally export PATH="$PWD/bin:$PATH" # not installed globally
@ -40,13 +40,13 @@ new-service NAME --kind backend|frontend [--with postgres,redis] [--api BACKEND]
| 5 | render gitops `templates/<kind>` (+ `postgres`, `redis`; `__APP__`, `__API__`, `__DOMAIN__`) → `manifests/NAME`, `kustomize build` lint, commit, push | dir exists | | 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 | | 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 | | 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 | | 8 | write `$APPS_DIR/NAME.yaml` (`apps-homelab` by default), 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` | | 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"`) Then it prints the service, ArgoCD, Kibana (pre-filtered on `service:"NAME"`)
and Unleash URLs, and the elapsed time. and Unleash URLs, and the elapsed time.
**Why `apps/NAME.yaml` comes last:** ArgoCD only sees the service once CI has **Why `$APPS_DIR/NAME.yaml` (`apps-homelab` by default) comes last:** ArgoCD only sees the service once CI has
pushed a real image. Registering it earlier made the backend's migrate Job sit 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). in `ImagePullBackOff` on `:latest` and hold the sync forever (Plan 04 caveat).
@ -70,7 +70,7 @@ re-kicked automatically.
delete-service NAME [--yes] [--dry-run] [--timeout SECONDS] delete-service NAME [--yes] [--dry-run] [--timeout SECONDS]
``` ```
Removes `apps/NAME.yaml` + `manifests/NAME` from gitops (the Application's Removes `$APPS_DIR/NAME.yaml` (`apps-homelab` by default) + `manifests/NAME` from gitops (the Application's
`resources-finalizer` makes ArgoCD delete its workloads when `root` prunes it), `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 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 own (CNPG PVCs go with it), deletes every version of the container package and

View file

@ -45,7 +45,7 @@ OWN_NS=0
# Refuse to pull a namespace out from under a frontend co-located with us. # Refuse to pull a namespace out from under a frontend co-located with us.
dependents=() dependents=()
for f in "$GITOPS_DIR"/apps/*.yaml; do for f in "$GITOPS_DIR/$APPS_DIR"/*.yaml; do
other=$(basename "$f" .yaml) other=$(basename "$f" .yaml)
[[ $other != "$NAME" && $(app_namespace "$other") == "$NAME" ]] && dependents+=("$other") [[ $other != "$NAME" && $(app_namespace "$other") == "$NAME" ]] && dependents+=("$other")
done done
@ -54,7 +54,7 @@ done
APP_DIR=$CODE_DIR/$NAME APP_DIR=$CODE_DIR/$NAME
cat >&2 <<EOF cat >&2 <<EOF
This permanently deletes, for service '$NAME': This permanently deletes, for service '$NAME':
- gitops apps/$NAME.yaml and manifests/$NAME/ (ArgoCD prunes the workloads) - gitops $APPS_DIR/$NAME.yaml and manifests/$NAME/ (ArgoCD prunes the workloads)
- $([[ $OWN_NS == 1 ]] && echo "namespace $NAME, including its Postgres data (PVCs)" || echo "nothing else in shared namespace $NS (left in place)") - $([[ $OWN_NS == 1 ]] && echo "namespace $NAME, including its Postgres data (PVCs)" || echo "nothing else in shared namespace $NS (left in place)")
- Forgejo repo $FORGEJO_OWNER/$NAME and its container package versions - Forgejo repo $FORGEJO_OWNER/$NAME and its container package versions
- local clone $APP_DIR - local clone $APP_DIR
@ -72,7 +72,7 @@ if kubectl get application -n argocd "$NAME" >/dev/null 2>&1; then
-p '{"metadata":{"finalizers":["resources-finalizer.argocd.argoproj.io"]}}' >/dev/null -p '{"metadata":{"finalizers":["resources-finalizer.argocd.argoproj.io"]}}' >/dev/null
fi fi
paths=() paths=()
[[ -f $GITOPS_DIR/apps/$NAME.yaml ]] && paths+=("apps/$NAME.yaml") [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml ]] && paths+=("$APPS_DIR/$NAME.yaml")
[[ -d $GITOPS_DIR/manifests/$NAME ]] && paths+=("manifests/$NAME") [[ -d $GITOPS_DIR/manifests/$NAME ]] && paths+=("manifests/$NAME")
if ((${#paths[@]})); then if ((${#paths[@]})); then
if is_dry; then if is_dry; then

View file

@ -88,13 +88,13 @@ gitops_sync
((!PUBLIC)) || cf_require ((!PUBLIC)) || cf_require
if [[ -n $API_APP ]]; then if [[ -n $API_APP ]]; then
[[ -f $GITOPS_DIR/apps/$API_APP.yaml && -d $GITOPS_DIR/manifests/$API_APP ]] || [[ -f $GITOPS_DIR/$APPS_DIR/$API_APP.yaml && -d $GITOPS_DIR/manifests/$API_APP ]] ||
die "--api $API_APP: no such service in gitops (create it first)" die "--api $API_APP: no such service in gitops (create it first)"
[[ $(app_namespace "$API_APP") == "$API_APP" ]] || [[ $(app_namespace "$API_APP") == "$API_APP" ]] ||
die "--api $API_APP: not a backend (its namespace is '$(app_namespace "$API_APP")')" die "--api $API_APP: not a backend (its namespace is '$(app_namespace "$API_APP")')"
fi fi
if [[ -f $GITOPS_DIR/apps/$NAME.yaml && $(app_namespace "$NAME") != "$NS" ]]; then if [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml && $(app_namespace "$NAME") != "$NS" ]]; then
die "gitops apps/$NAME.yaml exists with namespace '$(app_namespace "$NAME")', expected '$NS'" die "gitops $APPS_DIR/$NAME.yaml exists with namespace '$(app_namespace "$NAME")', expected '$NS'"
fi fi
ok "validated $NAME (kind=$KIND ns=$NS postgres=$WITH_PG redis=$WITH_REDIS api=${API_APP:--})" ok "validated $NAME (kind=$KIND ns=$NS postgres=$WITH_PG redis=$WITH_REDIS api=${API_APP:--})"
@ -148,7 +148,7 @@ else
fi fi
# ------------------------------------------------------------ 5. gitops manifests # ------------------------------------------------------------ 5. gitops manifests
# apps/<name>.yaml is written only after CI's first image exists (step 7), so # $APPS_DIR/<name>.yaml is written only after CI's first image exists (step 7), so
# ArgoCD never syncs `newTag: latest` before there is an image to pull (the # ArgoCD never syncs `newTag: latest` before there is an image to pull (the
# migrate-Job ImagePullBackOff caveat from Plan 04). # migrate-Job ImagePullBackOff caveat from Plan 04).
# public_block DIR: keep (markers only) or strip the `# __PUBLIC_BEGIN__..END__` host rule. # public_block DIR: keep (markers only) or strip the `# __PUBLIC_BEGIN__..END__` host rule.
@ -281,17 +281,17 @@ else
fi fi
# ------------------------------------------------------------ 8. register with ArgoCD # ------------------------------------------------------------ 8. register with ArgoCD
if [[ -f $GITOPS_DIR/apps/$NAME.yaml ]]; then if [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml ]]; then
ok "gitops apps/$NAME.yaml exists" ok "gitops $APPS_DIR/$NAME.yaml exists"
else else
is_dry || git_auth -C "$GITOPS_DIR" pull -q --ff-only origin main is_dry || git_auth -C "$GITOPS_DIR" pull -q --ff-only origin main
if is_dry; then if is_dry; then
dry "write $GITOPS_DIR/apps/$NAME.yaml (namespace $NS)" dry "write $GITOPS_DIR/$APPS_DIR/$NAME.yaml (namespace $NS)"
else else
sed "s/__APP__/$NAME/g; s/__NS__/$NS/g" "$GITOPS_DIR/templates/$KIND/application.yaml.tmpl" \ sed "s/__APP__/$NAME/g; s/__NS__/$NS/g" "$GITOPS_DIR/templates/$KIND/application.yaml.tmpl" \
>"$GITOPS_DIR/apps/$NAME.yaml" >"$GITOPS_DIR/$APPS_DIR/$NAME.yaml"
fi fi
gitops_commit_push "feat($NAME): register ArgoCD application" "apps/$NAME.yaml" gitops_commit_push "feat($NAME): register ArgoCD application" "$APPS_DIR/$NAME.yaml"
fi fi
# Don't wait for ArgoCD's 3-min poll. # Don't wait for ArgoCD's 3-min poll.
argocd_refresh root argocd_refresh root

View file

@ -80,6 +80,9 @@ load_config() {
for v in curl jq git kubectl; do need_cmd "$v"; done for v in curl jq git kubectl; do need_cmd "$v"; done
export KUBECONFIG export KUBECONFIG
# Environment layer (Plan 14): the gitops dir this service's Application goes in.
APPS_DIR=${APPS_DIR:-apps-homelab}
[[ -d $GITOPS_DIR/$APPS_DIR ]] || die "platform.env: APPS_DIR=$APPS_DIR does not exist in $GITOPS_DIR"
# Public access is optional config; only --public / delete-service use it. # Public access is optional config; only --public / delete-service use it.
PUBLIC_DOMAIN=${PUBLIC_DOMAIN:-} CF_ZONE_ID=${CF_ZONE_ID:-} CF_TUNNEL_ID=${CF_TUNNEL_ID:-} PUBLIC_DOMAIN=${PUBLIC_DOMAIN:-} CF_ZONE_ID=${CF_ZONE_ID:-} CF_TUNNEL_ID=${CF_TUNNEL_ID:-}
CLOUDFLARE_TOKEN_FILE=${CLOUDFLARE_TOKEN_FILE:-} CLOUDFLARE_TOKEN_FILE=${CLOUDFLARE_TOKEN_FILE:-}
@ -249,9 +252,9 @@ gitops_commit_push() {
die "gitops push failed after 5 attempts" die "gitops push failed after 5 attempts"
} }
# app_namespace NAME: destination namespace from gitops apps/NAME.yaml (empty if absent). # app_namespace NAME: destination namespace from gitops $APPS_DIR/NAME.yaml (empty if absent).
app_namespace() { app_namespace() {
local f=$GITOPS_DIR/apps/$1.yaml local f=$GITOPS_DIR/$APPS_DIR/$1.yaml
[[ -f $f ]] || return 0 [[ -f $f ]] || return 0
awk '/^ destination:/{d=1; next} d && /^ namespace:/{print $2; exit}' "$f" awk '/^ destination:/{d=1; next} d && /^ namespace:/{print $2; exit}' "$f"
} }

View file

@ -2,7 +2,7 @@
# Every file below must exist or the scripts refuse to run. # Every file below must exist or the scripts refuse to run.
HOMELAB_SECRETS=$HOME/Code/github.com/nholtware/homelab/.secrets HOMELAB_SECRETS=$HOME/Code/github.com/nholtware/homelab/.secrets
FORGEJO_URL=https://git.lab.holtlabs.xyz FORGEJO_URL=https://git.holtlabs.xyz
FORGEJO_OWNER=nholtware FORGEJO_OWNER=nholtware
FORGEJO_ADMIN_TOKEN_FILE=$HOMELAB_SECRETS/forgejo-admin-token # repo create/delete, Actions secrets, git push FORGEJO_ADMIN_TOKEN_FILE=$HOMELAB_SECRETS/forgejo-admin-token # repo create/delete, Actions secrets, git push
REGISTRY_PUSH_TOKEN_FILE=$HOMELAB_SECRETS/registry-push-token # becomes REGISTRY_TOKEN; also lists/deletes packages REGISTRY_PUSH_TOKEN_FILE=$HOMELAB_SECRETS/registry-push-token # becomes REGISTRY_TOKEN; also lists/deletes packages
@ -11,6 +11,7 @@ GITOPS_TOKEN_FILE=$HOMELAB_SECRETS/ci-bot-gitops-token # becomes GITOPS
GITOPS_DIR=$HOME/Code/forgejo.local/nholtware/gitops # clean clone of nholtware/gitops on main GITOPS_DIR=$HOME/Code/forgejo.local/nholtware/gitops # clean clone of nholtware/gitops on main
CODE_DIR=$HOME/Code/forgejo.local/nholtware # new service repos are cloned here CODE_DIR=$HOME/Code/forgejo.local/nholtware # new service repos are cloned here
KUBECONFIG=$HOME/.kube/homelab.yaml KUBECONFIG=$HOME/.kube/homelab.yaml
APPS_DIR=apps-homelab # gitops dir for this environment's Applications (Plan 14; apps-cloud for the cloud env)
INGRESS_DOMAIN=lab.holtlabs.xyz # services at https://<name>.$INGRESS_DOMAIN (renders __DOMAIN__) INGRESS_DOMAIN=lab.holtlabs.xyz # services at https://<name>.$INGRESS_DOMAIN (renders __DOMAIN__)
INGRESS_IP=192.168.1.83 # health checks use curl --resolve, no DNS needed (TLS still verified) INGRESS_IP=192.168.1.83 # health checks use curl --resolve, no DNS needed (TLS still verified)