provisioning-catalog/lib/gateway-tls.sh

159 lines
6.7 KiB
Bash

#!/bin/bash
# Shared primitives for HTTPS Gateway listener and cert-manager certificate lifecycle.
#
# Source from component *-lib.sh scripts. Requires _kubectl to be defined
# in the calling context (standard in all catalog component libs).
#
# Functions:
# _lib_gateway_patch_https <gw_name> <gw_ns> <listener_name> <domain> <tls_secret> <cert_ns>
# _lib_gateway_remove_https <gw_name> <gw_ns> <listener_name>
# _lib_ensure_certificate <cert_name> <cert_ns> <domain> <cluster_issuer> [extra_domain...]
# _lib_create_cf_secret <secret_name> <cf_token> <cert_manager_ns>
# Upsert an HTTPS listener on a Gateway.
# If the listener already exists, ensures certificateRefs namespace matches cert_ns.
# Idempotent: safe to call on every update.
_lib_gateway_patch_https() {
local gw_name="$1" gw_ns="$2" listener_name="$3" domain="$4" tls_secret="$5" cert_ns="$6"
# Opt-out for gateways that own their listeners declaratively (adr-007). When a
# consumer sets MANAGE_GATEWAY_LISTENER=false, the listener is a property of the
# gateway's own manifest (e.g. private_gateway.https_listeners) and must NOT be
# imperatively patched here. Default true preserves behaviour for every workspace
# whose gateway does not declare the listener.
if [ "${MANAGE_GATEWAY_LISTENER:-true}" != "true" ]; then
echo " [skip] listener ${listener_name} owned declaratively by the gateway (MANAGE_GATEWAY_LISTENER=false)"
return 0
fi
[ -z "$domain" ] && { echo " [skip] domain not set — skipping gateway HTTPS patch"; return 0; }
# kubectl resolves the lookup server-side (jsonpath filter) — no JSON parser on the node.
local exists=""
[ -n "$(_kubectl get gateway "$gw_name" -n "$gw_ns" \
-o "jsonpath={.spec.listeners[?(@.name=='${listener_name}')].name}" 2>/dev/null)" ] \
&& exists="yes"
if [ "${exists:-no}" = "yes" ]; then
local current_cert_ns
current_cert_ns=$(_kubectl get gateway "$gw_name" -n "$gw_ns" \
-o "jsonpath={.spec.listeners[?(@.name=='${listener_name}')].tls.certificateRefs[0].namespace}" 2>/dev/null)
if [ "${current_cert_ns}" = "${cert_ns}" ]; then
echo " [skip] gateway listener ${listener_name} exists (cert ns: ${cert_ns})"
return 0
fi
# Listener array index for the JSON-patch path: kubectl streams names in order,
# bash counts to the match — no JSON parser.
local idx="" _i=0 _n
while IFS= read -r _n; do
[ "$_n" = "$listener_name" ] && { idx="$_i"; break; }
_i=$((_i + 1))
done < <(_kubectl get gateway "$gw_name" -n "$gw_ns" \
-o 'jsonpath={range .spec.listeners[*]}{.name}{"\n"}{end}' 2>/dev/null)
local cert_patch
cert_patch=$(printf '[{"op":"replace","path":"/spec/listeners/%s/tls/certificateRefs","value":[{"group":"","kind":"Secret","name":"%s","namespace":"%s"}]}]' \
"$idx" "$tls_secret" "$cert_ns")
_kubectl patch gateway "$gw_name" -n "$gw_ns" --type=json -p "$cert_patch"
echo " [ok] gateway ${gw_name}: cert ns updated ${current_cert_ns}${cert_ns} on ${listener_name}"
return 0
fi
local patch
patch=$(printf '[{"op":"add","path":"/spec/listeners/-","value":{"name":"%s","port":443,"protocol":"HTTPS","hostname":"%s","tls":{"mode":"Terminate","certificateRefs":[{"group":"","kind":"Secret","name":"%s","namespace":"%s"}]},"allowedRoutes":{"namespaces":{"from":"All"}}}}]' \
"$listener_name" "$domain" "$tls_secret" "$cert_ns")
_kubectl patch gateway "$gw_name" -n "$gw_ns" --type=json -p "$patch"
echo " [ok] gateway ${gw_name}: HTTPS listener ${listener_name} added for ${domain}"
}
# Remove an HTTPS listener from a Gateway by name.
_lib_gateway_remove_https() {
local gw_name="$1" gw_ns="$2" listener_name="$3"
# Symmetric with _lib_gateway_patch_https: when the gateway owns the listener
# declaratively, deleting the consumer must not strip it from the shared gateway.
if [ "${MANAGE_GATEWAY_LISTENER:-true}" != "true" ]; then
echo " [skip] listener ${listener_name} owned declaratively by the gateway (MANAGE_GATEWAY_LISTENER=false)"
return 0
fi
local idx="" _i=0 _n
while IFS= read -r _n; do
[ "$_n" = "$listener_name" ] && { idx="$_i"; break; }
_i=$((_i + 1))
done < <(_kubectl get gateway "$gw_name" -n "$gw_ns" \
-o 'jsonpath={range .spec.listeners[*]}{.name}{"\n"}{end}' 2>/dev/null)
[ -z "$idx" ] && { echo " [skip] gateway listener ${listener_name} not found"; return 0; }
_kubectl patch gateway "$gw_name" -n "$gw_ns" --type=json \
-p "[{\"op\":\"remove\",\"path\":\"/spec/listeners/${idx}\"}]"
echo " [ok] gateway ${gw_name}: HTTPS listener ${listener_name} removed"
}
# Ensure a cert-manager Certificate resource exists in the given namespace.
# Idempotent: creates only if absent. Does not overwrite existing certificates.
# Extra domains beyond the first are added as additional dnsNames.
_lib_ensure_certificate() {
local cert_name="$1" cert_ns="$2" domain="$3" cluster_issuer="$4"
shift 4
local extra_domains=("$@")
local existing
existing=$(_kubectl get certificate "$cert_name" -n "$cert_ns" --ignore-not-found -o name 2>/dev/null)
if [ -n "$existing" ]; then
echo " [skip] certificate ${cert_name} already exists in ${cert_ns}"
return 0
fi
# Build the dnsNames YAML list (primary domain + any extras) in pure bash —
# one indented ' - "domain"' line per non-empty entry, no trailing newline.
local dns_names="" _d
for _d in "$domain" "${extra_domains[@]}"; do
[ -z "$_d" ] && continue
dns_names="${dns_names} - \"${_d}\""$'\n'
done
dns_names="${dns_names%$'\n'}"
_kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: ${cert_name}
namespace: ${cert_ns}
labels:
app.kubernetes.io/managed-by: provisioning
spec:
secretName: ${cert_name}
issuerRef:
name: ${cluster_issuer}
kind: ClusterIssuer
dnsNames:
${dns_names}
usages:
- server auth
- digital signature
- key encipherment
EOF
echo " [ok] certificate ${cert_name} created in ${cert_ns}"
}
# Create or update the Cloudflare API token secret in the cert-manager namespace.
# Used by ClusterIssuers that require a CF token for DNS-01 challenges.
_lib_create_cf_secret() {
local secret_name="$1" cf_token="$2" cert_manager_ns="${3:-cert-manager}"
[ -z "$cf_token" ] && { echo " [warn] CF token empty — skipping secret ${secret_name}"; return 0; }
if _kubectl get secret "$secret_name" -n "$cert_manager_ns" >/dev/null 2>&1; then
echo " [skip] CF token secret ${secret_name} already exists in ${cert_manager_ns}"
return 0
fi
_kubectl create secret generic "$secret_name" \
--namespace "$cert_manager_ns" \
--from-literal=api-token="$cf_token"
echo " [ok] CF token secret ${secret_name} created in ${cert_manager_ns}"
}