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
```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
cp platform.env.example platform.env # gitignored; defaults match the homelab
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 |
| 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 |
| 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` |
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
**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
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]
```
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),
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

View file

@ -45,7 +45,7 @@ OWN_NS=0
# Refuse to pull a namespace out from under a frontend co-located with us.
dependents=()
for f in "$GITOPS_DIR"/apps/*.yaml; do
for f in "$GITOPS_DIR/$APPS_DIR"/*.yaml; do
other=$(basename "$f" .yaml)
[[ $other != "$NAME" && $(app_namespace "$other") == "$NAME" ]] && dependents+=("$other")
done
@ -54,7 +54,7 @@ done
APP_DIR=$CODE_DIR/$NAME
cat >&2 <<EOF
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)")
- Forgejo repo $FORGEJO_OWNER/$NAME and its container package versions
- 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
fi
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")
if ((${#paths[@]})); then
if is_dry; then

View file

@ -88,13 +88,13 @@ gitops_sync
((!PUBLIC)) || cf_require
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)"
[[ $(app_namespace "$API_APP") == "$API_APP" ]] ||
die "--api $API_APP: not a backend (its namespace is '$(app_namespace "$API_APP")')"
fi
if [[ -f $GITOPS_DIR/apps/$NAME.yaml && $(app_namespace "$NAME") != "$NS" ]]; then
die "gitops apps/$NAME.yaml exists with namespace '$(app_namespace "$NAME")', expected '$NS'"
if [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml && $(app_namespace "$NAME") != "$NS" ]]; then
die "gitops $APPS_DIR/$NAME.yaml exists with namespace '$(app_namespace "$NAME")', expected '$NS'"
fi
ok "validated $NAME (kind=$KIND ns=$NS postgres=$WITH_PG redis=$WITH_REDIS api=${API_APP:--})"
@ -148,7 +148,7 @@ else
fi
# ------------------------------------------------------------ 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
# migrate-Job ImagePullBackOff caveat from Plan 04).
# public_block DIR: keep (markers only) or strip the `# __PUBLIC_BEGIN__..END__` host rule.
@ -281,17 +281,17 @@ else
fi
# ------------------------------------------------------------ 8. register with ArgoCD
if [[ -f $GITOPS_DIR/apps/$NAME.yaml ]]; then
ok "gitops apps/$NAME.yaml exists"
if [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml ]]; then
ok "gitops $APPS_DIR/$NAME.yaml exists"
else
is_dry || git_auth -C "$GITOPS_DIR" pull -q --ff-only origin main
if is_dry; then
dry "write $GITOPS_DIR/apps/$NAME.yaml (namespace $NS)"
dry "write $GITOPS_DIR/$APPS_DIR/$NAME.yaml (namespace $NS)"
else
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
gitops_commit_push "feat($NAME): register ArgoCD application" "apps/$NAME.yaml"
gitops_commit_push "feat($NAME): register ArgoCD application" "$APPS_DIR/$NAME.yaml"
fi
# Don't wait for ArgoCD's 3-min poll.
argocd_refresh root

View file

@ -80,6 +80,9 @@ load_config() {
for v in curl jq git kubectl; do need_cmd "$v"; done
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_DOMAIN=${PUBLIC_DOMAIN:-} CF_ZONE_ID=${CF_ZONE_ID:-} CF_TUNNEL_ID=${CF_TUNNEL_ID:-}
CLOUDFLARE_TOKEN_FILE=${CLOUDFLARE_TOKEN_FILE:-}
@ -249,9 +252,9 @@ gitops_commit_push() {
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() {
local f=$GITOPS_DIR/apps/$1.yaml
local f=$GITOPS_DIR/$APPS_DIR/$1.yaml
[[ -f $f ]] || return 0
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.
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_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
@ -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
CODE_DIR=$HOME/Code/forgejo.local/nholtware # new service repos are cloned here
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_IP=192.168.1.83 # health checks use curl --resolve, no DNS needed (TLS still verified)