feat: add monitor evidence inspect cli
This commit is contained in:
@@ -56,8 +56,10 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
&& sentinelActionRaw !== "dashboard"
|
||||
&& sentinelActionRaw !== "error"
|
||||
&& sentinelActionRaw !== "report"
|
||||
&& sentinelActionRaw !== "inspect-url"
|
||||
&& sentinelActionRaw !== "inspect-id"
|
||||
) {
|
||||
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|publish-current|validate|maintenance|dashboard|error|report --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|publish-current|validate|maintenance|dashboard|error|report|inspect-url|inspect-id --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
}
|
||||
assertKnownOptions(args, new Set([
|
||||
"--node",
|
||||
@@ -83,20 +85,47 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
"--source-stage-ref",
|
||||
"--source-mirror-commit",
|
||||
"--source-authority",
|
||||
]), new Set(["--dry-run", "--confirm", "--wait", "--rerun", "--manual-recovery", "--recovery", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page"]));
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
const lane = requiredOption(args, "--lane");
|
||||
assertLane(lane);
|
||||
]), new Set(["--dry-run", "--confirm", "--wait", "--rerun", "--manual-recovery", "--recovery", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page", "--diagnose-monitor-web"]));
|
||||
const nodeOption = optionValue(args, "--node") ?? null;
|
||||
const laneOption = optionValue(args, "--lane") ?? null;
|
||||
const inspectMode = sentinelActionRaw === "inspect-url" || sentinelActionRaw === "inspect-id";
|
||||
let node = nodeOption ?? "";
|
||||
let lane = laneOption ?? "";
|
||||
const sentinelId = optionValue(args, "--sentinel") ?? optionValue(args, "--sentinel-id") ?? null;
|
||||
if (sentinelId !== null && !/^[a-z0-9][a-z0-9-]{1,80}$/u.test(sentinelId)) throw new Error(`--sentinel must be a stable lowercase sentinel id, got ${sentinelId}`);
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
||||
const confirm = args.includes("--confirm");
|
||||
const dryRun = args.includes("--dry-run");
|
||||
if (confirm && dryRun) throw new Error("web-probe sentinel accepts only one of --confirm or --dry-run");
|
||||
const timeoutSeconds = positiveIntegerOption(args, "--timeout-seconds", 900, 3600);
|
||||
const sourceOverride = parseWebProbeSentinelSourceOverride(args);
|
||||
let sentinel: WebProbeSentinelOptions;
|
||||
if (inspectMode) {
|
||||
const input = args[1];
|
||||
if (input === undefined || input.startsWith("--")) throw new Error(`web-probe sentinel ${sentinelActionRaw} requires a URL or id argument`);
|
||||
const resolved = resolveWebProbeSentinelInspectTarget(input, sentinelActionRaw, nodeOption, laneOption, sentinelId);
|
||||
node = resolved.node;
|
||||
lane = resolved.lane;
|
||||
sentinel = {
|
||||
kind: "inspect",
|
||||
action: sentinelActionRaw,
|
||||
node,
|
||||
lane,
|
||||
sentinelId: resolved.sentinelId,
|
||||
input,
|
||||
view: parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary"),
|
||||
raw: args.includes("--raw"),
|
||||
full: args.includes("--full"),
|
||||
diagnoseMonitorWeb: args.includes("--diagnose-monitor-web"),
|
||||
resolutionWarnings: resolved.warnings,
|
||||
timeoutSeconds,
|
||||
};
|
||||
} else {
|
||||
node = requiredOption(args, "--node");
|
||||
lane = requiredOption(args, "--lane");
|
||||
assertNodeId(node);
|
||||
assertLane(lane);
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
||||
}
|
||||
if (sentinelActionRaw === "plan" || sentinelActionRaw === "status") {
|
||||
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, sentinelId, dryRun };
|
||||
} else if (sentinelActionRaw === "image") {
|
||||
@@ -164,7 +193,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
const errorId = requiredOption(args, "--error-id");
|
||||
if (!/^wbe_[a-f0-9]{20}$/u.test(errorId)) throw new Error(`web-probe sentinel error get --error-id must look like wbe_<20 hex>, got ${errorId}`);
|
||||
sentinel = { kind: "error", action: "get", node, lane, sentinelId, errorId, raw: args.includes("--raw"), full: args.includes("--full"), timeoutSeconds };
|
||||
} else {
|
||||
} else if (sentinelActionRaw === "report") {
|
||||
const view = parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary");
|
||||
const latest = args.includes("--latest");
|
||||
const runId = optionValue(args, "--run") ?? optionValue(args, "--run-id") ?? null;
|
||||
@@ -196,6 +225,98 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
};
|
||||
}
|
||||
|
||||
function resolveWebProbeSentinelInspectTarget(
|
||||
input: string,
|
||||
action: "inspect-url" | "inspect-id",
|
||||
nodeOption: string | null,
|
||||
laneOption: string | null,
|
||||
sentinelOption: string | null,
|
||||
): { readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly warnings: readonly string[] } {
|
||||
if ((nodeOption === null) !== (laneOption === null)) throw new Error(`web-probe sentinel ${action} requires both --node and --lane when overriding target`);
|
||||
if (nodeOption !== null && laneOption !== null) {
|
||||
assertNodeId(nodeOption);
|
||||
assertLane(laneOption);
|
||||
if (!isHwlabRuntimeLane(laneOption)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${laneOption}`);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(laneOption, nodeOption);
|
||||
const fallbackSentinel = spec.observability.webProbe?.monitorRoot?.sentinelId ?? null;
|
||||
return { node: nodeOption, lane: laneOption, sentinelId: sentinelOption ?? fallbackSentinel, warnings: [] };
|
||||
}
|
||||
const candidates = webProbeMonitorRootCandidates();
|
||||
const url = parseMonitorInspectUrl(input);
|
||||
const urlSentinel = url === null ? null : sentinelIdFromMonitorUrl(url);
|
||||
const desiredSentinel = sentinelOption ?? urlSentinel;
|
||||
const matched = candidates.filter((candidate) => {
|
||||
if (desiredSentinel !== null && candidate.sentinelId !== desiredSentinel) return false;
|
||||
if (url === null) return true;
|
||||
return monitorUrlMatchesRoot(url, candidate.publicBaseUrl);
|
||||
});
|
||||
if (matched.length === 0) {
|
||||
const hint = candidates.slice(0, 8).map((candidate) => `${candidate.node}/${candidate.lane}/${candidate.sentinelId}`).join(", ");
|
||||
throw new Error(`web-probe sentinel ${action} could not map input to a YAML monitorRoot; pass --node and --lane. candidates: ${hint}`);
|
||||
}
|
||||
const selected = matched.length === 1 ? matched[0] : preferredMonitorRootCandidate(matched);
|
||||
if (selected === null) {
|
||||
const hint = matched.slice(0, 8).map((candidate) => `--node ${candidate.node} --lane ${candidate.lane} --sentinel ${candidate.sentinelId}`).join(" | ");
|
||||
throw new Error(`web-probe sentinel ${action} target is ambiguous; pass one target explicitly. candidates: ${hint}`);
|
||||
}
|
||||
const warnings = matched.length > 1
|
||||
? [`multiple YAML monitorRoot entries matched; selected ${selected.node}/${selected.lane}/${selected.sentinelId} by source-of-truth preference`]
|
||||
: [];
|
||||
return { node: selected.node, lane: selected.lane, sentinelId: selected.sentinelId, warnings };
|
||||
}
|
||||
|
||||
function webProbeMonitorRootCandidates(): readonly { readonly node: string; readonly lane: string; readonly sentinelId: string; readonly publicBaseUrl: string }[] {
|
||||
const candidates: { node: string; lane: string; sentinelId: string; publicBaseUrl: string }[] = [];
|
||||
for (const lane of hwlabRuntimeLaneIds()) {
|
||||
for (const node of hwlabRuntimeNodeIds()) {
|
||||
let spec: HwlabRuntimeLaneSpec;
|
||||
try {
|
||||
spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const root = spec.observability.webProbe?.monitorRoot;
|
||||
if (root?.enabled !== true) continue;
|
||||
candidates.push({ node: spec.nodeId, lane: spec.lane, sentinelId: root.sentinelId, publicBaseUrl: root.publicBaseUrl });
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function parseMonitorInspectUrl(input: string): URL | null {
|
||||
try {
|
||||
return new URL(input);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sentinelIdFromMonitorUrl(url: URL): string | null {
|
||||
const query = url.searchParams.get("sentinel") ?? url.searchParams.get("sentinelId");
|
||||
if (query !== null && /^[a-z0-9][a-z0-9-]{1,80}$/u.test(query)) return query;
|
||||
const match = /\/sentinels\/([a-z0-9][a-z0-9-]{1,80})(?:\/|$)/u.exec(url.pathname);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function monitorUrlMatchesRoot(url: URL, publicBaseUrl: string): boolean {
|
||||
let base: URL;
|
||||
try {
|
||||
base = new URL(publicBaseUrl);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (url.origin !== base.origin) return false;
|
||||
const basePath = base.pathname.replace(/\/+$/u, "");
|
||||
if (basePath.length === 0) return true;
|
||||
return url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
|
||||
}
|
||||
|
||||
function preferredMonitorRootCandidate(candidates: readonly { readonly node: string; readonly lane: string; readonly sentinelId: string }[]): { readonly node: string; readonly lane: string; readonly sentinelId: string } | null {
|
||||
return candidates.find((candidate) => candidate.node === "JD01" && candidate.lane === "v03" && candidate.sentinelId === "jd01-web-probe-sentinel")
|
||||
?? candidates.find((candidate) => candidate.node === "JD01" && candidate.lane === "v03")
|
||||
?? null;
|
||||
}
|
||||
|
||||
function parseWebProbeSentinelSourceOverride(args: string[]): WebProbeSentinelSourceOverrideOptions {
|
||||
const sourceCommit = optionValue(args, "--source-commit") ?? null;
|
||||
const sourceStageRef = optionValue(args, "--source-stage-ref") ?? null;
|
||||
|
||||
Reference in New Issue
Block a user