feat(new-service): --public opt-in via Cloudflare Tunnel; delete-service removes the CNAME

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nholtware 2026-11-11 12:45:47 -06:00
parent 4b6f898089
commit e094cdbdd9
4 changed files with 110 additions and 6 deletions

View file

@ -17,6 +17,9 @@ usage: new-service NAME --kind backend|frontend [options]
(runtime-config is always included)
--api BACKEND frontend only: route /api to BACKEND and deploy
into its namespace
--public also serve https://NAME.$PUBLIC_DOMAIN through the
Cloudflare Tunnel (adds the DNS CNAME); default is
LAN-only. No Cloudflare Access on generated services.
--wait after deploy, wait until Healthy and the health
endpoint answers; print elapsed time
--timeout SECONDS per-wait timeout (default 900)
@ -26,12 +29,13 @@ usage: new-service NAME --kind backend|frontend [options]
EOF
}
NAME='' KIND='' WITH='' API_APP='' WAIT=0 TIMEOUT=900
NAME='' KIND='' WITH='' API_APP='' WAIT=0 TIMEOUT=900 PUBLIC=0
while (($#)); do
case $1 in
--kind) KIND=${2:?--kind needs a value}; shift 2 ;;
--with) WITH=${2:?--with needs a value}; shift 2 ;;
--api) API_APP=${2:?--api needs a value}; shift 2 ;;
--public) PUBLIC=1; shift ;;
--wait) WAIT=1; shift ;;
--timeout) TIMEOUT=${2:?--timeout needs a value}; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
@ -81,6 +85,7 @@ esac
is_dry && log "DRY RUN: nothing below is executed except read-only checks"
load_config
gitops_sync
((!PUBLIC)) || cf_require
if [[ -n $API_APP ]]; then
[[ -f $GITOPS_DIR/apps/$API_APP.yaml && -d $GITOPS_DIR/manifests/$API_APP ]] ||
@ -146,6 +151,15 @@ fi
# apps/<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.
public_block() {
if ((PUBLIC)); then
sed -i '/# __PUBLIC_\(BEGIN\|END\)__/d' "$1"/*.yaml
else
sed -i '/# __PUBLIC_BEGIN__/,/# __PUBLIC_END__/d' "$1"/*.yaml
fi
}
render_manifests() {
local out=$1 t=$GITOPS_DIR/templates/$KIND
cp -r "$t/." "$out/"
@ -171,15 +185,20 @@ render_manifests() {
for f in "${extra[@]}"; do add+=" - $f\n"; done
sed -i "s|^ # Added by the render recipe.*$|${add%\\n}|" "$out/kustomization.yaml"
sed -i '/^$/d' "$out/kustomization.yaml"
sed -i "s/__APP__/$NAME/g; s/__DOMAIN__/$INGRESS_DOMAIN/g" "$out"/*.yaml
public_block "$out"
sed -i "s/__APP__/$NAME/g; s/__DOMAIN__/$INGRESS_DOMAIN/g; s/__PUBLIC_DOMAIN__/$PUBLIC_DOMAIN/g" "$out"/*.yaml
else
if [[ -z $API_APP ]]; then
rm "$out/ingress-api.yaml" "$out/middleware.yaml"
sed -i '/ingress-api.yaml$/d; /middleware.yaml$/d; /# \/api -> backend/d' "$out/kustomization.yaml"
fi
sed -i "s/__APP__/$NAME/g; s/__API__/$API_APP/g; s/__DOMAIN__/$INGRESS_DOMAIN/g" "$out"/*.yaml
public_block "$out"
# One Unleash URL serves both hosts; public services point browsers at the public name.
local ud=$INGRESS_DOMAIN
((PUBLIC)) && ud=$PUBLIC_DOMAIN
sed -i "s/__APP__/$NAME/g; s/__API__/$API_APP/g; s/__DOMAIN__/$INGRESS_DOMAIN/g; s/__PUBLIC_DOMAIN__/$PUBLIC_DOMAIN/g; s/__UNLEASH_DOMAIN__/$ud/g" "$out"/*.yaml
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"
}
@ -277,6 +296,11 @@ fi
# Don't wait for ArgoCD's 3-min poll.
argocd_refresh root
# ------------------------------------------------------------ 8b. public DNS
if ((PUBLIC)); then
cf_cname_ensure "$NAME"
fi
# ------------------------------------------------------------ 9. wait for healthy
HOST=$NAME.$INGRESS_DOMAIN
if ((WAIT)) && ! is_dry; then
@ -302,7 +326,7 @@ if ((WAIT)) && ! is_dry; then
done
fi
print_urls "$NAME" "$KIND"
print_urls "$NAME" "$KIND" "$PUBLIC"
if is_dry; then
log "dry run complete"
elif ((WAIT)); then