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

@ -80,6 +80,9 @@ load_config() {
for v in curl jq git kubectl; do need_cmd "$v"; done
export KUBECONFIG
# 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:-}
FORGEJO_TOKEN=$(tr -d '\n' <"$FORGEJO_ADMIN_TOKEN_FILE")
REGISTRY_TOKEN=$(tr -d '\n' <"$REGISTRY_PUSH_TOKEN_FILE")
GITOPS_TOKEN=$(tr -d '\n' <"$GITOPS_TOKEN_FILE")
@ -126,6 +129,73 @@ api_write() {
api "$method" "$path" "$body" "$as"
}
# ---------------------------------------------------------------- Cloudflare (public DNS)
# cf_require: the Plan 13 settings must be present before touching DNS.
cf_require() {
local v
for v in PUBLIC_DOMAIN CF_ZONE_ID CF_TUNNEL_ID CLOUDFLARE_TOKEN_FILE; do
[[ -n ${!v:-} ]] || die "platform.env: $v is not set (needed for public access, see platform.env.example)"
done
[[ -r $CLOUDFLARE_TOKEN_FILE ]] || die "platform.env: CLOUDFLARE_TOKEN_FILE=$CLOUDFLARE_TOKEN_FILE is not readable"
}
# cf_api METHOD PATH [JSON]: sets CF_BODY; dies if Cloudflare reports failure.
# The token goes through a header file so it never appears in argv.
CF_BODY=''
cf_api() {
local method=$1 path=$2 body=${3:-} hdr args
hdr=$(tmpfile)
chmod 600 "$hdr"
printf 'Authorization: Bearer %s\n' "$(tr -d '\n' <"$CLOUDFLARE_TOKEN_FILE")" >"$hdr"
args=(-sS -X "$method" -H "@$hdr" -H 'Content-Type: application/json')
[[ -n $body ]] && args+=(--data-binary "$body")
CF_BODY=$(curl "${args[@]}" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID$path") || die "curl failed: Cloudflare $method $path"
[[ $(jq -r .success <<<"$CF_BODY") == true ]] || die "Cloudflare $method $path failed: $(jq -c .errors <<<"$CF_BODY")"
}
# cf_record_id NAME: id of the DNS record for NAME.$PUBLIC_DOMAIN, or ''.
cf_record_id() {
cf_api GET "/dns_records?name=$1.$PUBLIC_DOMAIN&type=CNAME"
jq -r '.result[0].id // empty' <<<"$CF_BODY"
}
# cf_cname_ensure NAME: proxied CNAME NAME -> <tunnel>.cfargotunnel.com.
cf_cname_ensure() {
local name=$1 id body
cf_require
body=$(jq -nc --arg n "$name" --arg t "$CF_TUNNEL_ID.cfargotunnel.com" \
'{type:"CNAME", name:$n, content:$t, proxied:true, comment:"platform new-service --public"}')
if is_dry; then
dry "Cloudflare: ensure proxied CNAME $name.$PUBLIC_DOMAIN -> $CF_TUNNEL_ID.cfargotunnel.com ($body)"
return 0
fi
id=$(cf_record_id "$name")
if [[ -n $id ]]; then
ok "DNS $name.$PUBLIC_DOMAIN exists"
else
cf_api POST /dns_records "$body"
ok "DNS $name.$PUBLIC_DOMAIN -> tunnel created"
fi
}
# cf_cname_delete NAME: remove NAME.$PUBLIC_DOMAIN if it is one of ours (points at the tunnel).
cf_cname_delete() {
local name=$1 id
[[ -n $PUBLIC_DOMAIN && -n $CF_ZONE_ID && -n $CF_TUNNEL_ID && -r $CLOUDFLARE_TOKEN_FILE ]] || return 0
if is_dry; then
dry "Cloudflare: delete CNAME $name.$PUBLIC_DOMAIN if it points at $CF_TUNNEL_ID.cfargotunnel.com"
return 0
fi
cf_api GET "/dns_records?name=$name.$PUBLIC_DOMAIN&type=CNAME"
id=$(jq -r --arg t "$CF_TUNNEL_ID.cfargotunnel.com" '.result[] | select(.content == $t) | .id' <<<"$CF_BODY" | head -1)
if [[ -n $id ]]; then
cf_api DELETE "/dns_records/$id"
ok "DNS $name.$PUBLIC_DOMAIN deleted"
else
ok "no tunnel DNS record for $name.$PUBLIC_DOMAIN"
fi
}
repo_exists() {
api GET "/repos/$FORGEJO_OWNER/$1"
[[ $API_CODE == 200 ]]
@ -217,13 +287,14 @@ validate_name() {
# Print the URLs a human wants after a deploy.
print_urls() {
local name=$1 kind=$2
local name=$1 kind=$2 public=${3:-0}
local host="$name.$INGRESS_DOMAIN"
local kql
kql=$(jq -rn --arg s "$name" '"service:\"\($s)\"" | @uri')
cat <<EOF
service : https://$host/
$([[ $public == 1 ]] && echo " public : https://$name.$PUBLIC_DOMAIN/")
health : https://$host/$([[ $kind == backend ]] && echo readyz || echo healthz)
repo : $FORGEJO_URL/$FORGEJO_OWNER/$name
argocd : https://argocd.$INGRESS_DOMAIN/applications/argocd/$name