fix: separate runner kubeconfig gap from D601 status (#144)

Commander review: direction is correct. This PR separates runner kubeconfig/read-only observability gaps from D601 public endpoint/k3s availability and keeps M3 blocked until real operation/trace/audit/evidence are observed. It does not touch PROD or restart services.
This commit is contained in:
Lyon
2026-05-22 20:44:30 +08:00
committed by GitHub
parent bf47a95a97
commit 85da2dcb19
8 changed files with 669 additions and 141 deletions
+76 -8
View File
@@ -2,6 +2,7 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { accessSync, constants as fsConstants } from "node:fs";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
@@ -9,7 +10,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultReportPath = "reports/dev-gate/dev-m3-hardware-loop.json";
@@ -19,6 +20,7 @@ const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml";
const requiredManifestCardinalityCommand = "node scripts/validate-dev-m3-cardinality.mjs";
const requiredM3BoxIds = Object.freeze(["boxsimu_1", "boxsimu_2"]);
const requiredM3BoxResources = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]);
const runnerK3sKubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
const serviceTargets = Object.freeze([
{
@@ -156,12 +158,57 @@ function commandResult(command, args, { timeoutMs = 5000, maxChars = 3000 } = {}
}
}
function runnerKubeconfigCommandText(args) {
return [`KUBECONFIG=${runnerK3sKubeconfigPath}`, "kubectl", ...args].join(" ");
}
function commandExists(command) {
const result = commandResult("bash", ["-lc", `command -v ${command}`], { timeoutMs: 2000 });
return result.exitCode === 0 && result.stdout.trim().length > 0;
}
function collectLocalRuntimeObservations() {
function fileReadable(filePath) {
try {
accessSync(filePath, fsConstants.R_OK);
return true;
} catch {
return false;
}
}
function collectD601RunnerObservability(ingress) {
const args = ["-n", namespace, "get", "pods", "-o", "name"];
const probe = commandResult("env", [`KUBECONFIG=${runnerK3sKubeconfigPath}`, "kubectl", ...args], {
timeoutMs: 5000,
maxChars: 2000
});
const stderr = oneLine(probe.stderr || "") || "empty";
const stdoutLines = String(probe.stdout ?? "")
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const d601PublicEndpointsReachable = ingress.probes.some((item) => item.ok && isHwlabDevIdentity(item));
const runnerKubeconfigReadable = fileReadable(runnerK3sKubeconfigPath);
return {
runnerKubeconfigReadable,
runnerKubeconfigProbeOk: probe.exitCode === 0,
runnerKubeconfigProbeExitCode: probe.exitCode,
runnerKubeconfigProbeStderr: stderr,
d601PublicEndpointsReachable,
d601K3sUnavailable: false,
classification: "runner_permission_mount_gap",
sourceIssue: "pikasTech/HWLAB#46",
command: runnerKubeconfigCommandText(args),
stdoutSummary: probe.exitCode === 0 ? `${stdoutLines.length} pod name(s) observed` : undefined,
summary: "Runner /etc/rancher/k3s/k3s.yaml is not treated as readable from this container; a kubectl success may come from alternate in-cluster config, so file readability and command exit code stay separate.",
inferenceRule: "runnerKubeconfigReadable=false must not imply d601K3sUnavailable=true; public 16666/16667 endpoint reachability is tracked separately.",
secretValuesRead: false,
secretResourcesRead: false
};
}
function collectLocalRuntimeObservations(d601Observability) {
const observations = [];
if (commandExists("docker")) {
@@ -206,6 +253,13 @@ function collectLocalRuntimeObservations() {
});
}
observations.push({
id: "runner-k3s-kubeconfig-readonly",
status: d601Observability.runnerKubeconfigProbeOk ? "observed" : "blocked",
command: d601Observability.command,
summary: `${d601Observability.summary} runnerKubeconfigReadable=${d601Observability.runnerKubeconfigReadable}; exitCode=${d601Observability.runnerKubeconfigProbeExitCode}; stderr=${d601Observability.runnerKubeconfigProbeStderr}.`
});
return observations;
}
@@ -587,6 +641,7 @@ function baseReport({ commitId, observedAt }) {
],
runtimeTarget: {
endpoint: DEV_ENDPOINT,
frontendEndpoint: DEV_FRONTEND_ENDPOINT,
namespace,
environment: ENVIRONMENT_DEV,
requiredBoxSimulators: 2,
@@ -623,6 +678,17 @@ function baseReport({ commitId, observedAt }) {
],
readOnlySupplementalEvidence: [],
localRuntimeObservations: [],
d601Observability: {
runnerKubeconfigReadable: false,
runnerKubeconfigProbeOk: false,
runnerKubeconfigProbeExitCode: null,
runnerKubeconfigProbeStderr: "not_observed",
d601PublicEndpointsReachable: false,
d601K3sUnavailable: false,
classification: "not_observed",
sourceIssue: "pikasTech/HWLAB#46",
inferenceRule: "runnerKubeconfigReadable=false must not imply d601K3sUnavailable=true; public 16666/16667 endpoint reachability is tracked separately."
},
liveOperation: {
status: "not_run",
operationId: "not_observed",
@@ -683,7 +749,8 @@ async function main() {
report.readOnlySupplementalEvidence = await collectReadOnlySupplementalEvidence();
const ingress = await observeDevIngress();
report.localRuntimeObservations = collectLocalRuntimeObservations();
report.d601Observability = collectD601RunnerObservability(ingress);
report.localRuntimeObservations = collectLocalRuntimeObservations(report.d601Observability);
report.liveChecks.push({
id: "dev-ingress-health",
status: ingress.accepted ? "pass" : "blocked",
@@ -727,21 +794,22 @@ async function main() {
if (missing.length > 0) {
addNotRunM3Checks(report, "Stopped before M3 direct checks because service target URLs were not provided.");
report.blockers.push({
type: "runtime_blocker",
type: "observability_blocker",
scope: "m3-service-discovery",
status: "open",
classification: "hardware loop",
summary: `DEV ingress is reachable, but M3 smoke lacks direct DEV service URLs: ${missing.join(", ")}.`
classification: "runner permission/mount gap",
sourceIssue: "pikasTech/HWLAB#46",
summary: `DEV ingress is reachable on frozen :16667, but this runner cannot use ${runnerK3sKubeconfigPath} as readable kubeconfig to discover direct M3 service URLs (${missing.join(", ")}); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline.`
});
report.summary = {
status: "blocked",
classification: "hardware loop",
classification: "runner permission/mount gap",
observedAt,
result: "DEV ingress was reachable, but live M3 simulator and patch-panel targets were not discoverable."
};
await writeReport(report, reportPath);
console.log(`[dev-m3-smoke] status=blocked report=${relativePath(reportPath)}`);
console.log("[dev-m3-smoke] blocker=runtime_blocker scope=m3-service-discovery");
console.log("[dev-m3-smoke] blocker=observability_blocker scope=m3-service-discovery");
return;
}