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>
This commit is contained in:
nholtware 2026-11-10 13:15:00 -06:00
parent b133a0d120
commit 2a6971df03
4 changed files with 22 additions and 17 deletions

View file

@ -37,11 +37,11 @@ new-service NAME --kind backend|frontend [--with postgres,redis] [--api BACKEND]
| 2 | generate public repo from `service-template-express` / `service-template-react` | repo exists | | 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) | | 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 | | 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 | | 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/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` | | 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.
@ -91,6 +91,11 @@ call (secret values redacted), git push and kubectl change instead of doing it.
registry push token (same user). registry push token (same user).
- Git pushes use the admin token through an inline credential helper; it is - Git pushes use the admin token through an inline credential helper; it is
never written into a remote URL or `argv`. never written into a remote URL or `argv`.
- Platform UIs: ArgoCD, Kibana and Unleash at `argocd.` / `kibana.` / - Platform UIs: ArgoCD, Kibana and Unleash at `https://argocd.` / `kibana.` /
`unleash.192.168.1.83.nip.io`. Credentials and conventions: homelab `unleash.lab.holtlabs.xyz` (`INGRESS_DOMAIN`). Credentials and conventions:
`PLATFORM.md`. 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.

View file

@ -171,13 +171,13 @@ render_manifests() {
for f in "${extra[@]}"; do add+=" - $f\n"; done for f in "${extra[@]}"; do add+=" - $f\n"; done
sed -i "s|^ # Added by the render recipe.*$|${add%\\n}|" "$out/kustomization.yaml" sed -i "s|^ # Added by the render recipe.*$|${add%\\n}|" "$out/kustomization.yaml"
sed -i '/^$/d' "$out/kustomization.yaml" sed -i '/^$/d' "$out/kustomization.yaml"
sed -i "s/__APP__/$NAME/g" "$out"/*.yaml sed -i "s/__APP__/$NAME/g; s/__DOMAIN__/$INGRESS_DOMAIN/g" "$out"/*.yaml
else else
if [[ -z $API_APP ]]; then if [[ -z $API_APP ]]; then
rm "$out/ingress-api.yaml" "$out/middleware.yaml" rm "$out/ingress-api.yaml" "$out/middleware.yaml"
sed -i '/ingress-api.yaml$/d; /middleware.yaml$/d; /# \/api -> backend/d' "$out/kustomization.yaml" sed -i '/ingress-api.yaml$/d; /middleware.yaml$/d; /# \/api -> backend/d' "$out/kustomization.yaml"
fi fi
sed -i "s/__APP__/$NAME/g; s/__API__/$API_APP/g" "$out"/*.yaml sed -i "s/__APP__/$NAME/g; s/__API__/$API_APP/g; s/__DOMAIN__/$INGRESS_DOMAIN/g" "$out"/*.yaml
fi fi
! grep -rn '__[A-Z]*__' "$out" || die "unrendered placeholder left in manifests" ! grep -rn '__[A-Z]*__' "$out" || die "unrendered placeholder left in manifests"
kustomize_build "$out" >/dev/null || die "kustomize build of rendered manifests failed" kustomize_build "$out" >/dev/null || die "kustomize build of rendered manifests failed"
@ -291,14 +291,14 @@ if ((WAIT)) && ! is_dry; then
local p local p
for p in "$@"; do for p in "$@"; do
[[ $(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \ [[ $(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
--resolve "$HOST:80:$INGRESS_IP" "http://$HOST$p") == 200 ]] || return 1 --resolve "$HOST:443:$INGRESS_IP" "https://$HOST$p") == 200 ]] || return 1
done done
} }
paths=("$HEALTH_PATH") paths=("$HEALTH_PATH")
[[ -n $API_APP ]] && paths+=(/api/readyz) [[ -n $API_APP ]] && paths+=(/api/readyz)
wait_until 180 5 "http://$HOST${paths[*]} -> 200" http_ok "${paths[@]}" wait_until 180 5 "https://$HOST${paths[*]} -> 200" http_ok "${paths[@]}"
for p in "${paths[@]}"; do for p in "${paths[@]}"; do
printf ' GET %s -> %s\n' "$p" "$(curl -s --max-time 5 --resolve "$HOST:80:$INGRESS_IP" "http://$HOST$p")" >&2 printf ' GET %s -> %s\n' "$p" "$(curl -s --max-time 5 --resolve "$HOST:443:$INGRESS_IP" "https://$HOST$p")" >&2
done done
fi fi

View file

@ -223,11 +223,11 @@ print_urls() {
kql=$(jq -rn --arg s "$name" '"service:\"\($s)\"" | @uri') kql=$(jq -rn --arg s "$name" '"service:\"\($s)\"" | @uri')
cat <<EOF cat <<EOF
service : http://$host/ service : https://$host/
health : http://$host/$([[ $kind == backend ]] && echo readyz || echo healthz) health : https://$host/$([[ $kind == backend ]] && echo readyz || echo healthz)
repo : $FORGEJO_URL/$FORGEJO_OWNER/$name repo : $FORGEJO_URL/$FORGEJO_OWNER/$name
argocd : http://argocd.$INGRESS_DOMAIN/applications/argocd/$name argocd : https://argocd.$INGRESS_DOMAIN/applications/argocd/$name
kibana : http://kibana.$INGRESS_DOMAIN/app/discover#/?_g=(time:(from:now-1h,to:now))&_a=(query:(language:kuery,query:'$kql')) kibana : https://kibana.$INGRESS_DOMAIN/app/discover#/?_g=(time:(from:now-1h,to:now))&_a=(query:(language:kuery,query:'$kql'))
unleash : http://unleash.$INGRESS_DOMAIN/projects/default unleash : https://unleash.$INGRESS_DOMAIN/projects/default
EOF EOF
} }

View file

@ -12,5 +12,5 @@ GITOPS_DIR=$HOME/Code/forgejo.local/nholtware/gitops # clean clone of
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
INGRESS_DOMAIN=192.168.1.83.nip.io # services at <name>.$INGRESS_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 INGRESS_IP=192.168.1.83 # health checks use curl --resolve, no DNS needed (TLS still verified)