From d7a9d86e72862b11c07126c9dfb0f77200f2a223 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 13:57:43 +0000 Subject: [PATCH] fix: keep sentinel caddy path routes --- scripts/src/hwlab-node-web-sentinel-cicd.ts | 113 ++++++++++++++------ 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index e5431f15..3824ebbe 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2389,19 +2389,12 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe " }", "}", ]; - const block = routePrefix === "/" - ? [ - `${hostname} {`, - ...proxyLines.map((line) => ` ${line}`), - "}", - "", - ].join("\n") - : [ - `handle_path ${routePrefix}* {`, - ...proxyLines.map((line) => ` ${line}`), - "}", - "", - ].join("\n"); + const block = [ + routePrefix === "/" ? "handle {" : `handle_path ${routePrefix}* {`, + ...proxyLines.map((line) => ` ${line}`), + "}", + "", + ].join("\n"); const blockB64 = Buffer.from(block, "utf8").toString("base64"); const script = [ "set +e", @@ -2429,6 +2422,28 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe "begin = f'# BEGIN {marker}'", "end = f'# END {marker}'", "pattern = re.compile(rf'(?ms)^[ \\t]*# BEGIN {re.escape(marker)}\\n.*?^[ \\t]*# END {re.escape(marker)}\\n*')", + "def collect_nested_managed(segment):", + " preserved = []", + " lines = segment.splitlines()", + " index = 0", + " while index < len(lines):", + " stripped = lines[index].strip()", + " if stripped.startswith('# BEGIN ') and stripped != begin:", + " owner_text = stripped[len('# BEGIN '):]", + " end_line = '# END ' + owner_text", + " block_lines = [lines[index]]", + " index += 1", + " while index < len(lines):", + " block_lines.append(lines[index])", + " if lines[index].strip() == end_line:", + " break", + " index += 1", + " preserved.append('\\n'.join(block_lines).rstrip() + '\\n')", + " index += 1", + " return preserved", + "preserved_blocks = []", + "for match in pattern.finditer(text):", + " preserved_blocks.extend(collect_nested_managed(match.group(0)))", "text = pattern.sub('', text)", "def site_span(src, host):", " match = re.search(rf'(?m)^([ \\t]*){re.escape(host)}[ \\t]*\\{{[ \\t]*\\n', src)", @@ -2449,21 +2464,42 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe " return match.start(), end_index, index, match.end()", " index += 1", " raise ValueError(f'unclosed Caddy site block for {host}')", - "if route_prefix == '/':", - " managed = f'{begin}\\n{block.rstrip()}\\n{end}\\n'", - " text = text.rstrip() + '\\n\\n' + managed", + "handler = '\\n'.join((' ' + line) if line else '' for line in block.rstrip().splitlines())", + "managed = f' {begin}\\n{handler}\\n {end}\\n'", + "def fallback_insert_pos(site, relative_open, close_rel):", + " for match in re.finditer(r'(?m)^[ \\t]*handle[ \\t]*\\{[ \\t]*\\n', site[relative_open:close_rel]):", + " pos = relative_open + match.start()", + " prev_end = pos - 1", + " if prev_end >= 0 and site[prev_end] == '\\n':", + " prev_end -= 1", + " if prev_end >= 0:", + " prev_start = site.rfind('\\n', 0, prev_end + 1) + 1", + " if site[prev_start:prev_end + 1].strip().startswith('# BEGIN '):", + " return prev_start", + " return pos", + " return relative_open", + "def append_before_close(site, close_rel, addition):", + " prefix = site[:close_rel]", + " suffix = site[close_rel:]", + " if prefix and not prefix.endswith('\\n'):", + " prefix += '\\n'", + " return prefix + addition + suffix", + "span = site_span(text, hostname)", + "if span is None:", + " body = ''.join(preserved_blocks) + managed", + " text = text.rstrip() + '\\n\\n' + f'{hostname} {{\\n{body}}}\\n'", "else:", - " handler = '\\n'.join((' ' + line) if line else '' for line in block.rstrip().splitlines())", - " managed = f' {begin}\\n{handler}\\n {end}\\n'", - " span = site_span(text, hostname)", - " if span is None:", - " text = text.rstrip() + '\\n\\n' + f'{hostname} {{\\n{managed}}}\\n'", + " start, stop, close_index, open_end = span", + " site = text[start:stop]", + " relative_open = open_end - start", + " close_rel = close_index - start", + " additions = ''.join(preserved_blocks) + managed", + " if route_prefix == '/':", + " replacement = append_before_close(site, close_rel, additions)", " else:", - " start, stop, close_index, open_end = span", - " site = text[start:stop]", - " relative_open = open_end - start", - " replacement = site[:relative_open] + managed + site[relative_open:]", - " text = text[:start] + replacement + text[stop:]", + " insert_at = fallback_insert_pos(site, relative_open, close_rel)", + " replacement = site[:insert_at] + additions + site[insert_at:]", + " text = text[:start] + replacement + text[stop:]", "config.write_text(text, encoding='utf-8')", "PY", "python_rc=$?", @@ -2471,13 +2507,21 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe "reload_rc=", "if [ \"$python_rc\" = 0 ]; then sudo caddy validate --config \"$next\" --adapter caddyfile >/tmp/web-probe-sentinel-caddy-validate.out 2>/tmp/web-probe-sentinel-caddy-validate.err; validate_rc=$?; fi", "if [ \"$validate_rc\" = 0 ]; then sudo install -m 0644 \"$next\" \"$config_path\" >/tmp/web-probe-sentinel-caddy-install.out 2>/tmp/web-probe-sentinel-caddy-install.err && (sudo systemctl reload \"$service\" >/tmp/web-probe-sentinel-caddy-reload.out 2>/tmp/web-probe-sentinel-caddy-reload.err || sudo systemctl restart \"$service\" >>/tmp/web-probe-sentinel-caddy-reload.out 2>>/tmp/web-probe-sentinel-caddy-reload.err); reload_rc=$?; fi", + "probe_rc=1", + "probe_status=", + "if [ \"$reload_rc\" = 0 ]; then", + " probe_path=\"$route_prefix\"", + " if [ \"$probe_path\" = \"/\" ]; then probe_url=\"https://$hostname/\"; else probe_url=\"https://$hostname$probe_path/\"; fi", + " probe_status=$(curl -k -sS -o /tmp/web-probe-sentinel-caddy-probe.out -w '%{http_code}' --max-time 10 --resolve \"$hostname:443:127.0.0.1\" \"$probe_url\" 2>/tmp/web-probe-sentinel-caddy-probe.err)", + " probe_rc=$?", + "fi", "after_present=no", "grep -Fq \"# BEGIN $marker\" \"$config_path\" 2>/dev/null && after_present=yes", "active=$(systemctl is-active \"$service\" 2>/dev/null || true)", - "err=$(cat /tmp/web-probe-sentinel-caddy-python.err /tmp/web-probe-sentinel-caddy-validate.err /tmp/web-probe-sentinel-caddy-install.err /tmp/web-probe-sentinel-caddy-reload.err 2>/dev/null | tr '\\n' ';' | cut -c1-1000 || true)", - "python3 - \"$python_rc\" \"$validate_rc\" \"$reload_rc\" \"$after_present\" \"$active\" \"$hostname\" \"$config_path\" \"$err\" <<'PY'", + "err=$(cat /tmp/web-probe-sentinel-caddy-python.err /tmp/web-probe-sentinel-caddy-validate.err /tmp/web-probe-sentinel-caddy-install.err /tmp/web-probe-sentinel-caddy-reload.err /tmp/web-probe-sentinel-caddy-probe.err 2>/dev/null | tr '\\n' ';' | cut -c1-1000 || true)", + "python3 - \"$python_rc\" \"$validate_rc\" \"$reload_rc\" \"$probe_rc\" \"$probe_status\" \"$after_present\" \"$active\" \"$hostname\" \"$config_path\" \"$err\" <<'PY'", "import json, sys", - "python_rc, validate_rc, reload_rc, after_present, active, hostname, config_path, error_preview = sys.argv[1:9]", + "python_rc, validate_rc, reload_rc, probe_rc, probe_status, after_present, active, hostname, config_path, error_preview = sys.argv[1:11]", "def num(value):", " if value == '':", " return None", @@ -2485,13 +2529,18 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe " return int(value)", " except ValueError:", " return None", + "http_status = num(probe_status)", + "probe_ok = num(probe_rc) == 0 and http_status is not None and 200 <= http_status < 400", "payload = {", - " 'ok': num(python_rc) == 0 and num(validate_rc) == 0 and num(reload_rc) == 0 and after_present == 'yes',", + " 'ok': num(python_rc) == 0 and num(validate_rc) == 0 and num(reload_rc) == 0 and probe_ok and after_present == 'yes',", " 'hostname': hostname,", " 'configPath': config_path,", " 'pythonExitCode': num(python_rc),", " 'validateExitCode': num(validate_rc),", " 'reloadExitCode': num(reload_rc),", + " 'routeProbeExitCode': num(probe_rc),", + " 'routeProbeHttpStatus': http_status,", + " 'routeProbeOk': probe_ok,", " 'afterBlockPresent': after_present == 'yes',", " 'active': active,", " 'errorPreview': error_preview,", @@ -3294,11 +3343,11 @@ function renderControlPlaneResult(result: Record): string { "", Object.keys(runtimeSecretsApply).length === 0 ? "RUNTIME_SECRETS\n-" : table(["OK", "SECRETS", "KEYS", "SKIPPED"], [[runtimeSecretsApply.ok, runtimeSecretsApply.secretCount ?? "-", runtimeSecretsApply.keyCount ?? "-", runtimeSecretsApply.skippedKeyCount ?? "-"]]), "", - Object.keys(publicExposureApply).length === 0 ? "PUBLIC_EXPOSURE_APPLY\n-" : table(["OK", "SECRET", "CADDY", "HOST"], [[publicExposureApply.ok, record(publicExposureApply.secret).ok, record(publicExposureApply.caddy).ok, publicExposureApply.hostname]]), + Object.keys(publicExposureApply).length === 0 ? "PUBLIC_EXPOSURE_APPLY\n-" : table(["OK", "SECRET", "CADDY", "HOST", "ROUTE_HTTP"], [[publicExposureApply.ok, record(publicExposureApply.secret).ok, record(publicExposureApply.caddy).ok, publicExposureApply.hostname, record(publicExposureApply.caddy).routeProbeHttpStatus ?? "-"]]), "", Object.keys(publicExposureCaddy).length === 0 || publicExposureCaddy.ok === true ? "CADDY_APPLY_DETAIL\n-" - : table(["PY", "VALIDATE", "RELOAD", "BLOCK", "ACTIVE", "ERROR", "STDOUT", "STDERR"], [[publicExposureCaddy.pythonExitCode, publicExposureCaddy.validateExitCode, publicExposureCaddy.reloadExitCode, publicExposureCaddy.afterBlockPresent, publicExposureCaddy.active, short(publicExposureCaddy.errorPreview), short(record(publicExposureCaddy.result).stdoutPreview), short(record(publicExposureCaddy.result).stderrPreview)]]), + : table(["PY", "VALIDATE", "RELOAD", "PROBE", "HTTP", "BLOCK", "ACTIVE", "ERROR", "STDOUT", "STDERR"], [[publicExposureCaddy.pythonExitCode, publicExposureCaddy.validateExitCode, publicExposureCaddy.reloadExitCode, publicExposureCaddy.routeProbeExitCode, publicExposureCaddy.routeProbeHttpStatus, publicExposureCaddy.afterBlockPresent, publicExposureCaddy.active, short(publicExposureCaddy.errorPreview), short(record(publicExposureCaddy.result).stdoutPreview), short(record(publicExposureCaddy.result).stderrPreview)]]), "", Object.keys(argoApply).length === 0 ? "ARGO_APPLY\n-" : table(["OK", "EXIT", "PREVIEW"], [[argoApply.ok, record(argoApply.result).exitCode, record(argoApply.result).stdoutPreview]]), "",