feat: wire web probe sentinel validation (#912)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -12,6 +12,8 @@ sentinel:
|
||||
hostname: monitor.pikapython.com
|
||||
expectedA: 82.156.23.220
|
||||
frpc:
|
||||
deploymentName: hwlab-web-probe-sentinel-frpc
|
||||
image: 127.0.0.1:5000/hwlab/frpc:v0.68.1
|
||||
serverAddr: 82.156.23.220
|
||||
serverPort: 22000
|
||||
tokenSourceRef: platform-infra/pk01-frp.env
|
||||
|
||||
@@ -58,6 +58,9 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel report --node D601 --lane v03 --view turn-summary",
|
||||
"bun scripts/cli.ts hwlab nodes observability plan --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes observability status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes observability apply --node D601 --lane v03 --dry-run",
|
||||
@@ -70,6 +73,7 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
"`control-plane sync --confirm` syncs the YAML-declared local-k3s postgres bootstrap Secret, terminates a stale running Argo operation, deletes failed Argo hook Jobs, and recreates stale non-ready StatefulSet pods that are still pinned to an old controller revision with pull/backoff errors.",
|
||||
"`--wait` defaults to 120 seconds. If the PipelineRun is still active after 120 seconds, the CLI returns a warning plus env-reuse and git-mirror inspection commands instead of blocking.",
|
||||
"`web-probe sentinel image/control-plane` renders the YAML-first image, GitOps and Argo plan from the owning configRefs; unavailable service validation is a structured failure, not an automatic second execution path.",
|
||||
"`web-probe sentinel validate|maintenance|report` talks to the sentinel through k3s internal Service DNS, records only analyze summaries/views, and never runs a public/fallback validation path.",
|
||||
"Use `--rerun` for a deliberate YAML-first config-only publish when UniDesk node/lane render inputs changed but the HWLAB source commit did not."
|
||||
],
|
||||
};
|
||||
@@ -112,13 +116,19 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane status --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel validate --node D601 --lane v03 --quick-verify --confirm --wait",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance start --node D601 --lane v03 --confirm --wait --release-id <id>",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel maintenance stop --node D601 --lane v03 --confirm --wait --release-id <id>",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel report --node D601 --lane v03 --view summary",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel report --node D601 --lane v03 --view trace-frame",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS'\nexport default async ({ waitWorkbenchReady, fetchJson, fetchApiMatrix, recordStep, collectText, safeEvaluate, screenshot }) => {\n const ready = await waitWorkbenchReady();\n const workspace = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_workbench');\n const apiMatrix = await fetchApiMatrix(['/v1/workbench/workspace?projectId=prj_hwpod_workbench', '/auth/session']);\n const workspaceText = await collectText('#workspace');\n const evaluated = await safeEvaluate(({ a, b }) => ({ sum: a + b }), { a: 1, b: 2 });\n await screenshot('workbench.png');\n recordStep('workbench-summary', { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, apiMatrixOk: apiMatrix.ok });\n return { finalUrl: ready.finalUrl, workspaceOk: workspace.ok, workspaceText, evaluated };\n};\nJS",
|
||||
],
|
||||
actions: {
|
||||
run: "Run the repo-owned scripts/web-live-dom-probe.mjs helper.",
|
||||
script: "Run caller-provided Playwright JS after CLI-managed /auth/login; the script receives authenticated browser/context/page plus gotoStable/reloadStable/gotoCurrentStable/safeReload/fetchJson/safeFetchJson/fetchApiMatrix/recordStep/collectText/safeEvaluate/waitWorkbenchReady/screenshotOnError/summarizeWorkspace/summarizeConversation helpers and must not handle secrets itself.",
|
||||
observe: "Start, inspect, control, stop, collect, and analyze a pure-client long-running Workbench observer on the target host. The observer runs a control page plus a passive observer page in a shared-auth browser context, receives commands through stateDir/commands files, writes JSONL artifacts, and does not expose any inbound service API.",
|
||||
sentinel: "Render the YAML-first service wrapper configRef graph plus image/GitOps/Argo control-plane plan for the production web-probe sentinel. This reads observability.webProbe.sentinel.enabled/configRefs and validates owning YAML presence, shape, redacted hashes, and cross-ref consistency without starting a browser or reading secret values.",
|
||||
sentinel: "Render the YAML-first service wrapper configRef graph plus image/GitOps/Argo control-plane plan for the production web-probe sentinel, then validate/maintenance/report through the k3s internal sentinel Service DNS. Quick verify still uses web-probe observe/analyze artifacts as truth and records only redacted report summaries/views.",
|
||||
},
|
||||
notes: [
|
||||
"The default probe URL, browser proxy mode, observe/analyze alert thresholds, and project-management observe behavior come from config/hwlab-node-lanes.yaml webProbe; pass --url only when intentionally overriding the YAML-selected origin.",
|
||||
@@ -134,7 +144,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"observe analyze scans every sampled DOM point, extracts Workbench timing text such as 总耗时/total and 最近 N 秒/分前, and writes a sample point vs turn timing report: each Markdown table row starts with the timestamp, followed by each turn's 总耗时(s) and 最近更新(s). Timing series are reported for post-processing/manual analysis instead of auto-judged from status tail output.",
|
||||
"observe analyze also reports visible “加载中” count, owner attribution, concurrent loading owners, and continuous visible segments; fixes must reduce real loading latency, not reveal incomplete content early to make this metric disappear.",
|
||||
"script/observe support --browser-proxy-mode auto|direct; use direct for A/B evidence when frontend RUM is slow but OTel server spans are absent or fast, instead of falling back to raw Playwright.",
|
||||
"sentinel plan/status is a configuration visibility command for the service wrapper; sentinel image/control-plane renders image, GitOps and Argo control-plane state from owning YAML and refuses to report deployment mutation until the remote publish job is wired. observe start/status/command/collect/analyze remain the sampling and analysis truth.",
|
||||
"sentinel plan/status is a configuration visibility command for the service wrapper; sentinel image/control-plane renders image, GitOps and Argo control-plane state from owning YAML. sentinel validate checks /api/health, /metrics, recent analyze report and publicExposure; maintenance stop can run the configured quick verify and index the observe/analyze report. observe start/status/command/collect/analyze remain the sampling and analysis truth.",
|
||||
"Use recordStep(name, data) or fetchApiMatrix(paths) to keep structured partial evidence when a later step fails.",
|
||||
"Use reloadStable(), gotoCurrentStable(), or safeReload() for bounded retries around page reload/current-URL navigation jitter such as ERR_NETWORK_CHANGED.",
|
||||
"Playwright page.evaluate accepts one serializable argument; use page.evaluate(({ a, b }) => ..., { a, b }) or safeEvaluate(fn, { a, b }).",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -108,6 +108,8 @@ const REQUIRED_TARGET_SHAPES: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, R
|
||||
"publicBaseUrl",
|
||||
"hostname",
|
||||
"expectedA",
|
||||
"frpc.deploymentName",
|
||||
"frpc.image",
|
||||
"frpc.serverAddr",
|
||||
"frpc.serverPort",
|
||||
"frpc.tokenSourceRef",
|
||||
|
||||
@@ -39,6 +39,7 @@ interface MaintenanceState {
|
||||
readonly startedAt: string | null;
|
||||
readonly stoppedAt: string | null;
|
||||
readonly quickVerifyPlannedAt: string | null;
|
||||
readonly quickVerifyPlannedRunId: string | null;
|
||||
}
|
||||
|
||||
interface CommandPlanStep {
|
||||
@@ -60,6 +61,8 @@ export interface WebProbeSentinelService {
|
||||
maintenance(): MaintenanceState;
|
||||
setMaintenance(active: boolean, input: Record<string, unknown>): MaintenanceState;
|
||||
planScenarioRun(scenarioId: string, reason: string): Record<string, unknown>;
|
||||
recordRun(input: Record<string, unknown>): Record<string, unknown>;
|
||||
report(view: string, runId: string | null): Record<string, unknown>;
|
||||
metrics(): string;
|
||||
dashboardHtml(): string;
|
||||
fetch(request: Request): Promise<Response>;
|
||||
@@ -172,6 +175,7 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
startedAt: nowIso(),
|
||||
stoppedAt: current.stoppedAt,
|
||||
quickVerifyPlannedAt: current.quickVerifyPlannedAt,
|
||||
quickVerifyPlannedRunId: current.quickVerifyPlannedRunId,
|
||||
}
|
||||
: {
|
||||
active: false,
|
||||
@@ -180,11 +184,17 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
startedAt: current.startedAt,
|
||||
stoppedAt: nowIso(),
|
||||
quickVerifyPlannedAt: nowIso(),
|
||||
quickVerifyPlannedRunId: null,
|
||||
};
|
||||
writeMetadata(db, "maintenance", next);
|
||||
if (!active) {
|
||||
const scenarioId = firstEnabledScenarioId(config);
|
||||
if (scenarioId !== null) this.planScenarioRun(scenarioId, "maintenance-stop-quick-verify");
|
||||
if (scenarioId !== null) {
|
||||
const planned = this.planScenarioRun(scenarioId, "maintenance-stop-quick-verify");
|
||||
const withPlan = { ...next, quickVerifyPlannedRunId: stringOrNull(planned.runId) };
|
||||
writeMetadata(db, "maintenance", withPlan);
|
||||
return withPlan;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
},
|
||||
@@ -198,6 +208,12 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
.run(runId, scenarioId, config.node, config.lane, "planned", this.maintenance().active ? 1 : 0, createdAt, createdAt, JSON.stringify({ reason, commandPlan, valuesRedacted: true }));
|
||||
return { ok: true, runId, scenarioId, status: "planned", commandPlanSha256: sha256Json(commandPlan), valuesRedacted: true };
|
||||
},
|
||||
recordRun(input: Record<string, unknown>) {
|
||||
return recordRunResult(config, db, input);
|
||||
},
|
||||
report(view: string, runId: string | null) {
|
||||
return reportRunView(config, db, view, runId);
|
||||
},
|
||||
metrics() {
|
||||
return renderMetrics(config, db, this.health(), this.maintenance());
|
||||
},
|
||||
@@ -239,6 +255,16 @@ async function sentinelFetch(service: WebProbeSentinelService, request: Request)
|
||||
const body = await readJsonBody(request);
|
||||
return jsonResponse(service.planScenarioRun(stringField(body, "scenarioId"), stringOrNull(body.reason) ?? "manual"));
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/api/runs/record") {
|
||||
const body = await readJsonBody(request);
|
||||
return jsonResponse(service.recordRun(body));
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/api/report") {
|
||||
const view = url.searchParams.get("view") ?? stringOrNull(service.config.reportViews.defaultView) ?? "summary";
|
||||
const runId = url.searchParams.get("run") ?? url.searchParams.get("runId");
|
||||
const report = service.report(view, runId);
|
||||
return jsonResponse(report, report.ok === false ? 404 : 200);
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/metrics") {
|
||||
return new Response(service.metrics(), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
|
||||
}
|
||||
@@ -478,7 +504,124 @@ function jsonResponse(value: unknown, status = 200): Response {
|
||||
}
|
||||
|
||||
function emptyMaintenance(): MaintenanceState {
|
||||
return { active: false, reason: null, releaseId: null, startedAt: null, stoppedAt: null, quickVerifyPlannedAt: null };
|
||||
return { active: false, reason: null, releaseId: null, startedAt: null, stoppedAt: null, quickVerifyPlannedAt: null, quickVerifyPlannedRunId: null };
|
||||
}
|
||||
|
||||
function recordRunResult(config: WebProbeSentinelServiceConfig, db: Database, input: Record<string, unknown>): Record<string, unknown> {
|
||||
const now = nowIso();
|
||||
const runId = stringOrNull(input.runId) ?? `sentinel-run-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
||||
const scenarioId = stringOrNull(input.scenarioId) ?? firstEnabledScenarioId(config);
|
||||
if (scenarioId === null) return { ok: false, error: "scenario-missing", valuesRedacted: true };
|
||||
const status = stringOrNull(input.status) ?? "analyzed";
|
||||
const observerId = stringOrNull(input.observerId);
|
||||
const stateDir = stringOrNull(input.stateDir);
|
||||
const reportJsonSha256 = stringOrNull(input.reportJsonSha256);
|
||||
const findings = arrayRecords(input.findings).slice(0, 50);
|
||||
const findingCount = numberOr(input.findingCount, findings.length);
|
||||
const artifactCount = numberOr(input.artifactCount, 0);
|
||||
const createdAt = stringOrNull(input.createdAt) ?? now;
|
||||
db.query(`
|
||||
INSERT INTO runs (id, scenario_id, node, lane, status, observer_id, state_dir, report_json_sha256, finding_count, artifact_count, maintenance, created_at, updated_at, command_plan_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
status = excluded.status,
|
||||
observer_id = excluded.observer_id,
|
||||
state_dir = excluded.state_dir,
|
||||
report_json_sha256 = excluded.report_json_sha256,
|
||||
finding_count = excluded.finding_count,
|
||||
artifact_count = excluded.artifact_count,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(runId, scenarioId, config.node, config.lane, status, observerId, stateDir, reportJsonSha256, findingCount, artifactCount, thisMaintenanceFlag(input), createdAt, now, JSON.stringify({ source: "recorded-analyze-summary", valuesRedacted: true }));
|
||||
db.query("DELETE FROM findings WHERE run_id = ?").run(runId);
|
||||
for (const item of findings) {
|
||||
const findingId = stringOrNull(item.id) ?? stringOrNull(item.kind) ?? stringOrNull(item.code) ?? "finding";
|
||||
const severity = stringOrNull(item.severity) ?? stringOrNull(item.level) ?? "unknown";
|
||||
const summary = stringOrNull(item.summary) ?? stringOrNull(item.message) ?? findingId;
|
||||
db.query("INSERT INTO findings (run_id, finding_id, severity, count, summary, report_json_sha256, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
||||
.run(runId, findingId.slice(0, 160), severity.slice(0, 40), numberOr(item.count, 1), summary.slice(0, 500), reportJsonSha256, now);
|
||||
}
|
||||
writeMetadata(db, `run.report.${runId}`, {
|
||||
runId,
|
||||
scenarioId,
|
||||
observerId,
|
||||
stateDir,
|
||||
reportJsonSha256,
|
||||
summary: record(input.summary),
|
||||
views: record(input.views),
|
||||
publicOrigin: stringOrNull(input.publicOrigin),
|
||||
screenshot: record(input.screenshot),
|
||||
artifactCount,
|
||||
findingCount,
|
||||
valuesRedacted: true,
|
||||
});
|
||||
return { ok: true, runId, scenarioId, status, reportJsonSha256, findingCount, artifactCount, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view: string, runId: string | null): Record<string, unknown> {
|
||||
if (!stringArrayAt(config.reportViews, "views").includes(view)) {
|
||||
return { ok: false, error: "unsupported-report-view", view, valuesRedacted: true };
|
||||
}
|
||||
const row = runId === null
|
||||
? db.query("SELECT * FROM runs WHERE report_json_sha256 IS NOT NULL ORDER BY updated_at DESC LIMIT 1").get() as Record<string, unknown> | null
|
||||
: db.query("SELECT * FROM runs WHERE id = ?").get(runId) as Record<string, unknown> | null;
|
||||
if (row === null) return { ok: false, error: "report-run-missing", runId, view, valuesRedacted: true };
|
||||
const selectedRunId = stringOrNull(row.id);
|
||||
if (selectedRunId === null) return { ok: false, error: "report-run-id-missing", view, valuesRedacted: true };
|
||||
const stored = readMetadata(db, `run.report.${selectedRunId}`) ?? {};
|
||||
const findings = db.query("SELECT finding_id, severity, count, summary, report_json_sha256, created_at FROM findings WHERE run_id = ? ORDER BY created_at DESC LIMIT 50")
|
||||
.all(selectedRunId) as Record<string, unknown>[];
|
||||
const views = record(stored.views);
|
||||
const storedView = record(views[view]);
|
||||
const renderedText = typeof storedView.renderedText === "string" ? storedView.renderedText : view === "summary" ? renderStoredSummary(row, stored, findings) : view === "findings" ? renderStoredFindings(row, findings) : null;
|
||||
if (renderedText === null) {
|
||||
return { ok: false, error: "report-view-not-indexed", runId: selectedRunId, view, availableViews: Object.keys(views), valuesRedacted: true };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
view,
|
||||
run: row,
|
||||
summary: record(stored.summary),
|
||||
findings,
|
||||
renderedText,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function renderStoredSummary(row: Record<string, unknown>, stored: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
|
||||
const summary = record(stored.summary);
|
||||
return [
|
||||
"Web Probe Sentinel Report",
|
||||
"=======================================================",
|
||||
`run=${stringOrNull(row.id) ?? "-"} scenario=${stringOrNull(row.scenario_id) ?? "-"} status=${stringOrNull(row.status) ?? "-"}`,
|
||||
`observer=${stringOrNull(row.observer_id) ?? "-"} stateDir=${stringOrNull(row.state_dir) ?? "-"}`,
|
||||
`report=${stringOrNull(row.report_json_sha256) ?? "-"} artifacts=${String(row.artifact_count ?? 0)} findings=${String(row.finding_count ?? findings.length)}`,
|
||||
`publicOrigin=${stringOrNull(stored.publicOrigin) ?? "-"}`,
|
||||
`analysisWindow=${JSON.stringify(record(summary.analysisWindow))}`,
|
||||
"",
|
||||
"Findings",
|
||||
findings.length === 0 ? "-" : findings.slice(0, 12).map((item) => `${item.severity ?? "-"} ${item.finding_id ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`).join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderStoredFindings(row: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
|
||||
return [
|
||||
"Web Probe Sentinel Findings",
|
||||
"=======================================================",
|
||||
`run=${stringOrNull(row.id) ?? "-"} report=${stringOrNull(row.report_json_sha256) ?? "-"}`,
|
||||
findings.length === 0 ? "-" : findings.map((item) => `${item.severity ?? "-"} ${item.finding_id ?? "-"} count=${item.count ?? "-"} ${item.summary ?? ""}`).join("\n"),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function thisMaintenanceFlag(input: Record<string, unknown>): number {
|
||||
return input.maintenance === true ? 1 : 0;
|
||||
}
|
||||
|
||||
function numberOr(value: unknown, fallback: number): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function arrayRecords(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.map(record) : [];
|
||||
}
|
||||
|
||||
function checkPath(value: unknown, path: string): unknown {
|
||||
@@ -511,6 +654,12 @@ function arrayAt(value: unknown, path: string): Record<string, unknown>[] {
|
||||
return result.filter(record);
|
||||
}
|
||||
|
||||
function stringArrayAt(value: unknown, path: string): string[] {
|
||||
const result = checkPath(value, path);
|
||||
if (!Array.isArray(result)) throw new Error(`${path} must be an array`);
|
||||
return result.filter((item): item is string => typeof item === "string" && item.length > 0);
|
||||
}
|
||||
|
||||
function stringField(value: Record<string, unknown>, key: string): string {
|
||||
const found = value[key];
|
||||
if (typeof found !== "string" || found.length === 0) throw new Error(`${key} must be a non-empty string`);
|
||||
|
||||
@@ -21,7 +21,7 @@ import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectVie
|
||||
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
||||
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
||||
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
||||
import { runWebProbeSentinelCommand, type WebProbeSentinelOptions } from "../hwlab-node-web-sentinel-cicd";
|
||||
import { runWebProbeSentinelCommand, type WebProbeSentinelOptions, type WebProbeSentinelReportView } from "../hwlab-node-web-sentinel-cicd";
|
||||
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help";
|
||||
import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary";
|
||||
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql";
|
||||
@@ -38,10 +38,29 @@ import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "
|
||||
|
||||
export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSentinelOptions {
|
||||
const [sentinelActionRaw] = args;
|
||||
if (sentinelActionRaw !== "plan" && sentinelActionRaw !== "status" && sentinelActionRaw !== "image" && sentinelActionRaw !== "control-plane") {
|
||||
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
if (
|
||||
sentinelActionRaw !== "plan"
|
||||
&& sentinelActionRaw !== "status"
|
||||
&& sentinelActionRaw !== "image"
|
||||
&& sentinelActionRaw !== "control-plane"
|
||||
&& sentinelActionRaw !== "validate"
|
||||
&& sentinelActionRaw !== "maintenance"
|
||||
&& sentinelActionRaw !== "report"
|
||||
) {
|
||||
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|validate|maintenance|report --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
}
|
||||
assertKnownOptions(args, new Set(["--node", "--lane", "--timeout-seconds"]), new Set(["--dry-run", "--confirm", "--wait"]));
|
||||
assertKnownOptions(args, new Set([
|
||||
"--node",
|
||||
"--lane",
|
||||
"--timeout-seconds",
|
||||
"--release-id",
|
||||
"--reason",
|
||||
"--view",
|
||||
"--run",
|
||||
"--run-id",
|
||||
"--trace-id",
|
||||
"--sample-seq",
|
||||
]), new Set(["--dry-run", "--confirm", "--wait", "--quick-verify", "--raw"]));
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
const lane = requiredOption(args, "--lane");
|
||||
@@ -58,12 +77,49 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
const imageAction = args[1];
|
||||
if (imageAction !== "status" && imageAction !== "build") throw new Error("web-probe sentinel image usage: image status|build --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
sentinel = { kind: "image", action: imageAction, node, lane, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
|
||||
} else {
|
||||
} else if (sentinelActionRaw === "control-plane") {
|
||||
const controlPlaneAction = args[1];
|
||||
if (controlPlaneAction !== "plan" && controlPlaneAction !== "apply" && controlPlaneAction !== "status" && controlPlaneAction !== "trigger-current") {
|
||||
throw new Error("web-probe sentinel control-plane usage: control-plane plan|apply|status|trigger-current --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
}
|
||||
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
|
||||
} else if (sentinelActionRaw === "maintenance") {
|
||||
const maintenanceAction = args[1];
|
||||
if (maintenanceAction !== "status" && maintenanceAction !== "start" && maintenanceAction !== "stop") {
|
||||
throw new Error("web-probe sentinel maintenance usage: maintenance status|start|stop --node NODE --lane vNN [--dry-run|--confirm]");
|
||||
}
|
||||
sentinel = {
|
||||
kind: "maintenance",
|
||||
action: maintenanceAction,
|
||||
node,
|
||||
lane,
|
||||
dryRun: maintenanceAction === "status" ? dryRun : dryRun || !confirm,
|
||||
confirm,
|
||||
wait: args.includes("--wait"),
|
||||
timeoutSeconds,
|
||||
releaseId: optionValue(args, "--release-id") ?? null,
|
||||
reason: optionValue(args, "--reason") ?? null,
|
||||
quickVerify: maintenanceAction === "stop" || args.includes("--quick-verify"),
|
||||
};
|
||||
} else if (sentinelActionRaw === "validate") {
|
||||
sentinel = { kind: "validate", action: "validate", node, lane, dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, quickVerify: args.includes("--quick-verify") };
|
||||
} else {
|
||||
const view = parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary");
|
||||
const sampleSeqRaw = optionValue(args, "--sample-seq") ?? null;
|
||||
const sampleSeq = sampleSeqRaw === null ? null : Number(sampleSeqRaw);
|
||||
if (sampleSeq !== null && (!Number.isInteger(sampleSeq) || sampleSeq < 1)) throw new Error("web-probe sentinel report --sample-seq must be a positive integer");
|
||||
sentinel = {
|
||||
kind: "report",
|
||||
action: "report",
|
||||
node,
|
||||
lane,
|
||||
view,
|
||||
runId: optionValue(args, "--run") ?? optionValue(args, "--run-id") ?? null,
|
||||
traceId: optionValue(args, "--trace-id") ?? null,
|
||||
sampleSeq,
|
||||
raw: args.includes("--raw"),
|
||||
timeoutSeconds,
|
||||
};
|
||||
}
|
||||
return {
|
||||
action: "sentinel",
|
||||
@@ -73,6 +129,11 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
};
|
||||
}
|
||||
|
||||
function parseWebProbeSentinelReportView(value: string): WebProbeSentinelReportView {
|
||||
if (value === "summary" || value === "turn-summary" || value === "findings" || value === "trace-frame") return value;
|
||||
throw new Error(`web-probe sentinel report --view must be summary, turn-summary, findings, or trace-frame; got ${value}`);
|
||||
}
|
||||
|
||||
export function normalizeNodeWebProbeObserveArgs(args: string[]): { args: string[]; id: string | null } {
|
||||
const [observeActionRaw, maybeId, ...rest] = args;
|
||||
if (observeActionRaw !== "start" && maybeId !== undefined && !maybeId.startsWith("--")) {
|
||||
|
||||
Reference in New Issue
Block a user