From 25c67dcdad5f72a7bfa369fac6c0fa46ed7262fe Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 13:21:46 +0000 Subject: [PATCH] fix(web-probe): merge sentinel caddy path routes --- scripts/src/hwlab-node-web-sentinel-cicd.ts | 73 +++++++++++++++------ 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 5ae007d6..eb6d0a30 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -622,18 +622,21 @@ function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Ext const startedAt = Date.now(); const command = `web-probe sentinel control-plane ${options.action}`; const applyOnly = options.action === "apply"; - const sourceMirrorSync = applyOnly ? null : runSentinelSourceMirrorSyncJob(state, options.timeoutSeconds); + const cicdWaitWarningSeconds = controlPlaneWaitWarningSeconds(state); + const deadline = startedAt + cicdWaitWarningSeconds * 1000; + const remainingCicdWaitSeconds = () => remainingSeconds(deadline, Math.min(options.timeoutSeconds, cicdWaitWarningSeconds)); + const sourceMirrorSync = applyOnly ? null : runSentinelSourceMirrorSyncJob(state, remainingCicdWaitSeconds()); const publish = applyOnly ? null : record(sourceMirrorSync).ok === true - ? runSentinelPublishJob(state, true, options.timeoutSeconds) + ? runSentinelPublishJob(state, true, remainingCicdWaitSeconds()) : sentinelBlockedRemoteResult("source-mirror-sync-blocked", "sentinel source mirror sync failed; publish job was not started"); const flush = !applyOnly && record(publish).ok === true - ? runChildCli(["hwlab", "nodes", "git-mirror", "flush", "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait"], options.timeoutSeconds) + ? runChildCli(["hwlab", "nodes", "git-mirror", "flush", "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait"], remainingCicdWaitSeconds()) : null; - const publicExposureApply = applySentinelPublicExposure(state, options.timeoutSeconds); - const argoApply = applySentinelArgoApplication(state, options.timeoutSeconds); - const observed = waitForSentinelObservedStatus(state, options.timeoutSeconds); + const publicExposureApply = applySentinelPublicExposure(state, remainingCicdWaitSeconds()); + const argoApply = applySentinelArgoApplication(state, remainingCicdWaitSeconds()); + const observed = waitForSentinelObservedStatus(state, remainingCicdWaitSeconds()); const observedReady = sentinelObservedReady(observed); const targetValidation = null; const targetValidationBlocked = false; @@ -646,7 +649,6 @@ function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Ext && record(argoApply).ok === true && observedReady; const elapsedMs = Date.now() - startedAt; - const cicdWaitWarningSeconds = controlPlaneWaitWarningSeconds(state); const blocker = ok ? null : { code: record(sourceMirrorSync).ok === false ? "sentinel-source-mirror-sync-failed" : "sentinel-control-plane-not-ready", reason: record(sourceMirrorSync).ok === false @@ -753,7 +755,7 @@ function collectSentinelObservedStatus(state: SentinelCicdState, timeoutSeconds: function waitForSentinelObservedStatus(state: SentinelCicdState, timeoutSeconds: number, expectation?: SentinelObservedExpectation): SentinelObservedStatus { const startedAt = Date.now(); - const timeoutMs = Math.max(30_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); + const timeoutMs = Math.max(5_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); let observed = collectSentinelObservedStatus(state, timeoutSeconds, expectation); while (!sentinelObservedReady(observed) && Date.now() - startedAt < timeoutMs) { runCommand(["sleep", "5"], repoRoot, { timeoutMs: 6_000 }); @@ -917,7 +919,7 @@ function runSentinelSourceMirrorSyncJob(state: SentinelCicdState, timeoutSeconds return { ok: false, phase: "create-job", jobName, payload: { ok: false, status: "create-failed", valuesRedacted: true }, create: compactCommand(created), valuesRedacted: true }; } const startedAt = Date.now(); - const timeoutMs = Math.max(30_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); + const timeoutMs = Math.max(5_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); const warningBudgetMs = Math.max(1, Math.trunc(controlPlaneWaitWarningSeconds(state))) * 1000; let slowWarningSent = false; let polls = 0; @@ -1160,7 +1162,7 @@ function runSentinelPublishJob(state: SentinelCicdState, publishGitops: boolean, } sentinelProgressEvent("sentinel.publish.progress", { phase: "create-job", status: "succeeded", jobName, publishGitops, node: state.spec.nodeId, lane: state.spec.lane }); const startedAt = Date.now(); - const timeoutMs = Math.max(30_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); + const timeoutMs = Math.max(5_000, Math.min(timeoutSeconds * 1000, controlPlaneWaitWarningSeconds(state) * 1000)); const warningBudgetMs = Math.max(1, Math.trunc(controlPlaneWaitWarningSeconds(state))) * 1000; let slowWarningSent = false; let polls = 0; @@ -2267,10 +2269,8 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe "", ].join("\n") : [ - `${hostname} {`, - ` handle_path ${routePrefix}* {`, - ...proxyLines.map((line) => ` ${line}`), - " }", + `handle_path ${routePrefix}* {`, + ...proxyLines.map((line) => ` ${line}`), "}", "", ].join("\n"); @@ -2281,6 +2281,7 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe `owner=${shellQuote(owner)}`, `config_path=${shellQuote(configPath)}`, `service=${shellQuote(serviceName)}`, + `route_prefix=${shellQuote(routePrefix)}`, `block_b64=${shellQuote(blockB64)}`, "marker=\"unidesk managed $owner\"", "tmp=$(mktemp -d)", @@ -2289,20 +2290,52 @@ function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: numbe "next=\"$tmp/Caddyfile\"", "printf '%s' \"$block_b64\" | base64 -d >\"$block\"", "if [ -f \"$config_path\" ]; then cp \"$config_path\" \"$next\"; else : >\"$next\"; fi", - "python3 - \"$next\" \"$block\" \"$marker\" <<'PY' >/tmp/web-probe-sentinel-caddy-python.out 2>/tmp/web-probe-sentinel-caddy-python.err", + "python3 - \"$next\" \"$block\" \"$marker\" \"$hostname\" \"$route_prefix\" <<'PY' >/tmp/web-probe-sentinel-caddy-python.out 2>/tmp/web-probe-sentinel-caddy-python.err", "import pathlib, re, sys", "config = pathlib.Path(sys.argv[1])", "block = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8')", "marker = sys.argv[3]", + "hostname = sys.argv[4]", + "route_prefix = sys.argv[5]", "text = config.read_text(encoding='utf-8') if config.exists() else ''", "begin = f'# BEGIN {marker}'", "end = f'# END {marker}'", - "managed = f'{begin}\\n{block.rstrip()}\\n# END {marker}\\n'", - "pattern = re.compile(rf'(?ms)^# BEGIN {re.escape(marker)}\\n.*?\\n# END {re.escape(marker)}\\n*')", - "if pattern.search(text):", - " text = pattern.sub(managed, text)", - "else:", + "pattern = re.compile(rf'(?ms)^[ \\t]*# BEGIN {re.escape(marker)}\\n.*?^[ \\t]*# END {re.escape(marker)}\\n*')", + "text = pattern.sub('', text)", + "def site_span(src, host):", + " match = re.search(rf'(?m)^([ \\t]*){re.escape(host)}[ \\t]*\\{{[ \\t]*\\n', src)", + " if not match:", + " return None", + " depth = 1", + " index = match.end()", + " while index < len(src):", + " char = src[index]", + " if char == '{':", + " depth += 1", + " elif char == '}':", + " depth -= 1", + " if depth == 0:", + " end_index = index + 1", + " if end_index < len(src) and src[end_index] == '\\n':", + " end_index += 1", + " 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", + "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'", + " 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:]", "config.write_text(text, encoding='utf-8')", "PY", "python_rc=$?",