feat: enhance canary mode to support domain-aware upsert behavior for site blocks

This commit is contained in:
S
2026-03-02 22:23:52 -06:00
parent b52d3187d9
commit 9224b91374
2 changed files with 186 additions and 37 deletions

View File

@@ -112,6 +112,7 @@ Phase 7.5 is done only when all are true:
3. Script does not change Cloudflare DNS records automatically. 3. Script does not change Cloudflare DNS records automatically.
- DNS updates are intentional/manual to keep blast radius controlled. - DNS updates are intentional/manual to keep blast radius controlled.
4. Do not set public Cloudflare proxied records to private `192.168.x.x` addresses. 4. Do not set public Cloudflare proxied records to private `192.168.x.x` addresses.
5. Canary updates are enclosed between markers: 5. Canary upsert behavior is domain-aware:
- `# BEGIN_PHASE7_5_CANARY` - if site block for the canary domain does not exist, it is added
- `# END_PHASE7_5_CANARY` - if site block exists, it is replaced in-place
- previous block content is printed in logs before replacement

View File

@@ -104,8 +104,6 @@ CANARY_HOST_MAP=(
GITEA_ENTRY="${GITEA_DOMAIN}|http://${UNRAID_GITEA_IP}:3000|false||false" GITEA_ENTRY="${GITEA_DOMAIN}|http://${UNRAID_GITEA_IP}:3000|false||false"
CADDY_COMPOSE_DIR="${UNRAID_COMPOSE_DIR}/caddy" CADDY_COMPOSE_DIR="${UNRAID_COMPOSE_DIR}/caddy"
CANARY_BEGIN_MARKER="# BEGIN_PHASE7_5_CANARY"
CANARY_END_MARKER="# END_PHASE7_5_CANARY"
SELECTED_HOST_MAP=() SELECTED_HOST_MAP=()
if [[ "$MODE" == "canary" ]]; then if [[ "$MODE" == "canary" ]]; then
@@ -212,6 +210,174 @@ emit_site_block_standalone() {
} >> "$outfile" } >> "$outfile"
} }
caddy_block_extract_for_host() {
local infile="$1" host="$2" outfile="$3"
awk -v host="$host" '
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
function brace_delta(s, tmp, opens, closes) {
tmp = s
opens = gsub(/\{/, "{", tmp)
closes = gsub(/\}/, "}", tmp)
return opens - closes
}
function has_host(labels, i, n, parts, token) {
labels = trim(labels)
gsub(/[[:space:]]+/, "", labels)
n = split(labels, parts, ",")
for (i = 1; i <= n; i++) {
token = parts[i]
if (token == host) {
return 1
}
}
return 0
}
BEGIN {
depth = 0
in_target = 0
target_depth = 0
found = 0
}
{
line = $0
if (!in_target) {
if (depth == 0) {
pos = index(line, "{")
if (pos > 0) {
labels = substr(line, 1, pos - 1)
if (trim(labels) != "" && labels !~ /^[[:space:]]*\(/ && has_host(labels)) {
in_target = 1
target_depth = brace_delta(line)
found = 1
print line
next
}
}
}
depth += brace_delta(line)
} else {
target_depth += brace_delta(line)
print line
if (target_depth <= 0) {
in_target = 0
}
}
}
END {
if (!found) {
exit 1
}
}
' "$infile" > "$outfile"
}
caddy_block_remove_for_host() {
local infile="$1" host="$2" outfile="$3"
awk -v host="$host" '
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
function brace_delta(s, tmp, opens, closes) {
tmp = s
opens = gsub(/\{/, "{", tmp)
closes = gsub(/\}/, "}", tmp)
return opens - closes
}
function has_host(labels, i, n, parts, token) {
labels = trim(labels)
gsub(/[[:space:]]+/, "", labels)
n = split(labels, parts, ",")
for (i = 1; i <= n; i++) {
token = parts[i]
if (token == host) {
return 1
}
}
return 0
}
BEGIN {
depth = 0
in_target = 0
target_depth = 0
removed = 0
}
{
line = $0
if (in_target) {
target_depth += brace_delta(line)
if (target_depth <= 0) {
in_target = 0
}
next
}
if (depth == 0) {
pos = index(line, "{")
if (pos > 0) {
labels = substr(line, 1, pos - 1)
if (trim(labels) != "" && labels !~ /^[[:space:]]*\(/ && has_host(labels)) {
in_target = 1
target_depth = brace_delta(line)
removed = 1
next
}
}
}
print line
depth += brace_delta(line)
}
END {
if (!removed) {
exit 1
}
}
' "$infile" > "$outfile"
}
upsert_site_block_by_host() {
local infile="$1" entry="$2" outfile="$3"
local host upstream streaming body_limit skip_verify
IFS='|' read -r host upstream streaming body_limit skip_verify <<< "$entry"
local tmp_new_block tmp_old_block tmp_without_old tmp_combined
tmp_new_block=$(mktemp)
tmp_old_block=$(mktemp)
tmp_without_old=$(mktemp)
tmp_combined=$(mktemp)
: > "$tmp_new_block"
emit_site_block_standalone "$tmp_new_block" "$host" "$upstream" "$streaming" "$body_limit" "$skip_verify"
if caddy_block_extract_for_host "$infile" "$host" "$tmp_old_block"; then
log_info "Domain '${host}' already exists; replacing existing site block"
log_info "Previous block for '${host}':"
sed 's/^/ | /' "$tmp_old_block" >&2
caddy_block_remove_for_host "$infile" "$host" "$tmp_without_old"
cat "$tmp_without_old" "$tmp_new_block" > "$tmp_combined"
else
log_info "Domain '${host}' not present; adding new site block"
cat "$infile" "$tmp_new_block" > "$tmp_combined"
fi
mv "$tmp_combined" "$outfile"
rm -f "$tmp_new_block" "$tmp_old_block" "$tmp_without_old"
}
build_caddyfile() { build_caddyfile() {
local outfile="$1" local outfile="$1"
local entry host upstream streaming body_limit skip_verify local entry host upstream streaming body_limit skip_verify
@@ -260,29 +426,6 @@ build_caddyfile() {
done done
} }
build_canary_fragment() {
local outfile="$1"
local entry host upstream streaming body_limit skip_verify
: > "$outfile"
{
echo
echo "${CANARY_BEGIN_MARKER}"
echo "# Managed by phase7_5_nginx_to_caddy.sh (canary additive mode)"
echo "# Existing routes above are preserved."
echo
} >> "$outfile"
for entry in "${CANARY_HOST_MAP[@]}"; do
IFS='|' read -r host upstream streaming body_limit skip_verify <<< "$entry"
emit_site_block_standalone "$outfile" "$host" "$upstream" "$streaming" "$body_limit" "$skip_verify"
done
{
echo "${CANARY_END_MARKER}"
echo
} >> "$outfile"
}
if ! validate_backend_tls_policy; then if ! validate_backend_tls_policy; then
exit 1 exit 1
fi fi
@@ -340,15 +483,20 @@ if ssh_exec UNRAID "test -f '${CADDY_DATA_PATH}/Caddyfile'" 2>/dev/null; then
fi fi
if [[ "$MODE" == "canary" && "$HAS_EXISTING_CADDYFILE" == "true" ]]; then if [[ "$MODE" == "canary" && "$HAS_EXISTING_CADDYFILE" == "true" ]]; then
TMP_EXISTING=$(mktemp) TMP_WORK=$(mktemp)
TMP_CLEAN=$(mktemp) TMP_NEXT=$(mktemp)
TMP_FRAGMENT=$(mktemp) cp /dev/null "$TMP_NEXT"
ssh_exec UNRAID "cat '${CADDY_DATA_PATH}/Caddyfile'" > "$TMP_EXISTING" ssh_exec UNRAID "cat '${CADDY_DATA_PATH}/Caddyfile'" > "$TMP_WORK"
sed "/^${CANARY_BEGIN_MARKER}\$/,/^${CANARY_END_MARKER}\$/d" "$TMP_EXISTING" > "$TMP_CLEAN"
build_canary_fragment "$TMP_FRAGMENT" for entry in "${CANARY_HOST_MAP[@]}"; do
cat "$TMP_CLEAN" "$TMP_FRAGMENT" > "$TMP_CADDYFILE" upsert_site_block_by_host "$TMP_WORK" "$entry" "$TMP_NEXT"
rm -f "$TMP_EXISTING" "$TMP_CLEAN" "$TMP_FRAGMENT" mv "$TMP_NEXT" "$TMP_WORK"
log_info "Canary mode: preserved existing routes and updated canary block only" TMP_NEXT=$(mktemp)
done
cp "$TMP_WORK" "$TMP_CADDYFILE"
rm -f "$TMP_WORK" "$TMP_NEXT"
log_info "Canary mode: existing routes preserved; canary domains upserted"
else else
build_caddyfile "$TMP_CADDYFILE" build_caddyfile "$TMP_CADDYFILE"
fi fi