fix: enforce D601 native k3s guards
This commit is contained in:
@@ -1211,15 +1211,37 @@ async function resolveKubectlContext(args, env, runCommand) {
|
||||
const selection = resolveDevKubeconfigSelection({ flagValue: args.kubeconfig, env });
|
||||
const which = await runCommand("which", ["kubectl"], { timeoutMs: 5000 });
|
||||
const executor = which.code === 0 ? which.stdout.trim() : "kubectl";
|
||||
const kubectlEnv = {
|
||||
...env,
|
||||
KUBECONFIG: selection.kubeconfig
|
||||
};
|
||||
if (which.code === 0 && args.apply) {
|
||||
const context = await runCommand(executor, ["config", "current-context"], { env: kubectlEnv, timeoutMs: 10000 });
|
||||
const nodes = await runCommand(executor, ["get", "nodes", "-o", "jsonpath={.items[*].metadata.name}"], { env: kubectlEnv, timeoutMs: 15000 });
|
||||
const contextName = String(context.stdout ?? "").trim();
|
||||
const nodeNames = String(nodes.stdout ?? "").trim().split(/\s+/u).filter(Boolean);
|
||||
const forbiddenContext = context.code === 0 && /docker-desktop|desktop-control-plane/iu.test(contextName);
|
||||
const forbiddenNode = nodes.code === 0 && (
|
||||
nodeNames.includes("desktop-control-plane") || (nodeNames.length > 0 && !nodeNames.includes("d601"))
|
||||
);
|
||||
if (forbiddenContext || forbiddenNode) {
|
||||
throw new DevCdApplyError("DEV CD kubeconfig does not target D601 native k3s", {
|
||||
code: "wrong-kubernetes-control-plane",
|
||||
blockers: [{
|
||||
type: "environment_blocker",
|
||||
scope: "d601-native-k3s",
|
||||
status: "open",
|
||||
summary: "D601 DEV CD must use native k3s via /etc/rancher/k3s/k3s.yaml; default docker-desktop kubeconfig is forbidden."
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
executor,
|
||||
kubeconfig: selection.kubeconfig,
|
||||
kubeconfigSource: selection.source,
|
||||
commandPrefix: buildKubectlCommandPrefix(selection.kubeconfig),
|
||||
env: {
|
||||
...env,
|
||||
KUBECONFIG: selection.kubeconfig
|
||||
}
|
||||
env: kubectlEnv
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,32 @@ export function formatKubeconfigAccessFailure(selection, unreadablePath) {
|
||||
return `DEV kubeconfig from ${selection.source} is not readable at ${unreadablePath}; set --kubeconfig, ${devKubeconfigEnvName}, or ${kubeconfigEnvName} to a readable DEV kubeconfig path. Kubeconfig contents and Secret values are never printed.`;
|
||||
}
|
||||
|
||||
async function validateD601NativeKubeconfig(kubectlPath, env) {
|
||||
const context = await commandResult(kubectlPath, ["config", "current-context"], 10_000, { env });
|
||||
if (!context.ok) {
|
||||
return `DEV kubeconfig could not report a kubectl context: ${redactSensitiveText(context.stderr || context.stdout)}`;
|
||||
}
|
||||
const contextName = context.stdout.trim();
|
||||
if (/docker-desktop|desktop-control-plane/iu.test(contextName)) {
|
||||
return `DEV kubeconfig resolved to forbidden context ${contextName}; D601 DEV must use native k3s at ${defaultD601KubeconfigPath}.`;
|
||||
}
|
||||
|
||||
const server = await commandResult(kubectlPath, ["config", "view", "--minify", "-o", "jsonpath={.clusters[0].cluster.server}"], 10_000, { env });
|
||||
if (server.ok && /127\.0\.0\.1:11700|docker-desktop/iu.test(server.stdout)) {
|
||||
return "DEV kubeconfig resolved to Docker Desktop Kubernetes server 127.0.0.1:11700; use D601 native k3s kubeconfig instead.";
|
||||
}
|
||||
|
||||
const nodes = await commandResult(kubectlPath, ["get", "nodes", "-o", "jsonpath={.items[*].metadata.name}"], 15_000, { env });
|
||||
if (!nodes.ok) {
|
||||
return `DEV kubeconfig could not read native k3s nodes: ${redactSensitiveText(nodes.stderr || nodes.stdout)}`;
|
||||
}
|
||||
const nodeNames = nodes.stdout.trim().split(/\s+/u).filter(Boolean);
|
||||
if (!nodeNames.includes("d601")) {
|
||||
return `DEV kubeconfig did not resolve to D601 native k3s; observed nodes=${nodeNames.join(",") || "none"}.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function redactSensitiveText(value) {
|
||||
return String(value)
|
||||
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/giu, "$1<redacted>")
|
||||
@@ -300,6 +326,15 @@ async function resolveD601Kubectl(args = {}, env = process.env) {
|
||||
};
|
||||
}
|
||||
|
||||
const nativeK3sFailure = await validateD601NativeKubeconfig(kubectlPath, base.env);
|
||||
if (nativeK3sFailure !== null) {
|
||||
return {
|
||||
...base,
|
||||
status: "blocked",
|
||||
reason: nativeK3sFailure
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
status: "ready",
|
||||
|
||||
@@ -21,6 +21,7 @@ const defaultReportPath = path.join(repoRoot, "reports/dev-gate/dev-edge-health.
|
||||
const publicHost = "74.48.78.17";
|
||||
const publicPort = 16667;
|
||||
const namespace = "hwlab-dev";
|
||||
const d601KubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
|
||||
const tcpPorts = [16667, 7000, 7402];
|
||||
const CODE_AGENT_PROVIDER_SECRET_CONTRACT = Object.freeze({
|
||||
sourceIssue: "pikasTech/HWLAB#143",
|
||||
@@ -35,6 +36,18 @@ const runtimeServices = [
|
||||
{ serviceId: "hwlab-edge-proxy", port: 6667 }
|
||||
];
|
||||
|
||||
function nativeKubectlArgs(args) {
|
||||
return [`KUBECONFIG=${d601KubeconfigPath}`, "kubectl", ...args];
|
||||
}
|
||||
|
||||
function nativeKubectlCommand(args) {
|
||||
return nativeKubectlArgs(args).join(" ");
|
||||
}
|
||||
|
||||
function runKubectl(args, options) {
|
||||
return runCommand("env", nativeKubectlArgs(args), options);
|
||||
}
|
||||
|
||||
export async function runDevEdgeHealthSmoke(argv) {
|
||||
const args = parseArgs(argv);
|
||||
requireDevEndpoint();
|
||||
@@ -636,7 +649,9 @@ async function inspectKubernetes() {
|
||||
const result = {
|
||||
observable: hasKubectl,
|
||||
namespace,
|
||||
kubeconfig: d601KubeconfigPath,
|
||||
context: null,
|
||||
nodeNames: [],
|
||||
services: [],
|
||||
endpoints: [],
|
||||
pods: [],
|
||||
@@ -650,6 +665,7 @@ async function inspectKubernetes() {
|
||||
}
|
||||
|
||||
await collectKubectlContext(result);
|
||||
await collectKubectlNodeIdentity(result);
|
||||
await collectKubectlJson(result, "services", [
|
||||
"-n", namespace, "get", "svc", "hwlab-cloud-api", "hwlab-edge-proxy", "hwlab-tunnel-client", "hwlab-router", "-o", "json"
|
||||
]);
|
||||
@@ -665,16 +681,30 @@ async function inspectKubernetes() {
|
||||
}
|
||||
|
||||
async function collectKubectlContext(result) {
|
||||
const context = await runCommand("kubectl", ["config", "current-context"], { timeoutMs: 5000 });
|
||||
const context = await runKubectl(["config", "current-context"], { timeoutMs: 5000 });
|
||||
result.context = {
|
||||
exitCode: context.exitCode,
|
||||
value: context.exitCode === 0 ? context.stdout.trim() : null,
|
||||
command: nativeKubectlCommand(["config", "current-context"]),
|
||||
stderr: context.stderr.trim()
|
||||
};
|
||||
}
|
||||
|
||||
async function collectKubectlNodeIdentity(result) {
|
||||
const args = ["get", "nodes", "-o", "jsonpath={.items[*].metadata.name}"];
|
||||
const nodes = await runKubectl(args, { timeoutMs: 5000 });
|
||||
if (nodes.exitCode !== 0) {
|
||||
result.notes.push(`kubectl node identity probe failed: ${nodes.stderr.trim()}`);
|
||||
return;
|
||||
}
|
||||
result.nodeNames = nodes.stdout.trim().split(/\s+/u).filter(Boolean);
|
||||
if (!result.nodeNames.includes("d601")) {
|
||||
result.notes.push(`kubectl node identity did not include d601: ${result.nodeNames.join(",") || "none"}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectKubectlJson(result, kind, args) {
|
||||
const probe = await runCommand("kubectl", args, { timeoutMs: 10000 });
|
||||
const probe = await runKubectl(args, { timeoutMs: 10000 });
|
||||
if (probe.exitCode !== 0) {
|
||||
result.notes.push(`kubectl ${kind} probe failed: ${probe.stderr.trim()}`);
|
||||
return;
|
||||
@@ -719,20 +749,20 @@ async function collectDbSecretPresence(result) {
|
||||
secretValueRead: false,
|
||||
redacted: true
|
||||
};
|
||||
const exists = await runCommand("kubectl", ["-n", namespace, "get", "secret", ref.secretName, "-o", "name"], {
|
||||
const exists = await runKubectl(["-n", namespace, "get", "secret", ref.secretName, "-o", "name"], {
|
||||
timeoutMs: 5000
|
||||
});
|
||||
if (exists.exitCode !== 0) {
|
||||
result.dbSecret = {
|
||||
...base,
|
||||
command: "kubectl get secret -o name",
|
||||
command: nativeKubectlCommand(["-n", namespace, "get", "secret", ref.secretName, "-o", "name"]),
|
||||
error: exists.stderr.trim() || "secret not observed"
|
||||
};
|
||||
result.notes.push(`kubectl DB Secret presence probe failed: ${exists.stderr.trim()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyPresence = await runCommand("kubectl", ["-n", namespace, "describe", "secret", ref.secretName], {
|
||||
const keyPresence = await runKubectl(["-n", namespace, "describe", "secret", ref.secretName], {
|
||||
timeoutMs: 5000
|
||||
});
|
||||
const keyLinePattern = new RegExp(`^\\s*${escapeRegExp(ref.secretKey)}:\\s+\\d+\\s+bytes\\s*$`, "mu");
|
||||
@@ -740,7 +770,7 @@ async function collectDbSecretPresence(result) {
|
||||
...base,
|
||||
secretPresent: true,
|
||||
secretKeyPresent: keyPresence.exitCode === 0 && keyLinePattern.test(keyPresence.stdout),
|
||||
command: "kubectl describe secret <name> (key-presence-only)",
|
||||
command: `${nativeKubectlCommand(["-n", namespace, "describe", "secret", ref.secretName])} (key-presence-only)`,
|
||||
error: keyPresence.exitCode === 0 ? null : keyPresence.stderr.trim()
|
||||
};
|
||||
if (keyPresence.exitCode !== 0) {
|
||||
@@ -761,20 +791,20 @@ async function collectCodeAgentProviderSecretPresence(result) {
|
||||
secretValueRead: false,
|
||||
redacted: true
|
||||
};
|
||||
const exists = await runCommand("kubectl", ["-n", namespace, "get", "secret", ref.secretName, "-o", "name"], {
|
||||
const exists = await runKubectl(["-n", namespace, "get", "secret", ref.secretName, "-o", "name"], {
|
||||
timeoutMs: 5000
|
||||
});
|
||||
if (exists.exitCode !== 0) {
|
||||
result.codeAgentProviderSecret = {
|
||||
...base,
|
||||
command: "kubectl get secret -o name",
|
||||
command: nativeKubectlCommand(["-n", namespace, "get", "secret", ref.secretName, "-o", "name"]),
|
||||
error: exists.stderr.trim() || "secret not observed"
|
||||
};
|
||||
result.notes.push(`kubectl Code Agent provider Secret presence probe failed: ${exists.stderr.trim()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const keyPresence = await runCommand("kubectl", ["-n", namespace, "describe", "secret", ref.secretName], {
|
||||
const keyPresence = await runKubectl(["-n", namespace, "describe", "secret", ref.secretName], {
|
||||
timeoutMs: 5000
|
||||
});
|
||||
const keyLinePattern = new RegExp(`^\\s*${escapeRegExp(ref.secretKey)}:\\s+\\d+\\s+bytes\\s*$`, "mu");
|
||||
@@ -782,7 +812,7 @@ async function collectCodeAgentProviderSecretPresence(result) {
|
||||
...base,
|
||||
secretPresent: true,
|
||||
secretKeyPresent: keyPresence.exitCode === 0 && keyLinePattern.test(keyPresence.stdout),
|
||||
command: "kubectl describe secret <name> (key-presence-only)",
|
||||
command: `${nativeKubectlCommand(["-n", namespace, "describe", "secret", ref.secretName])} (key-presence-only)`,
|
||||
error: keyPresence.exitCode === 0 ? null : keyPresence.stderr.trim()
|
||||
};
|
||||
if (keyPresence.exitCode !== 0) {
|
||||
|
||||
@@ -1042,16 +1042,19 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) {
|
||||
});
|
||||
} else {
|
||||
const k3sEvidence = [];
|
||||
const nativeKubectl = (args) => ["KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", ...args];
|
||||
for (const probe of [
|
||||
["kubectl", ["version", "--client=true"]],
|
||||
["kubectl", ["config", "current-context"]],
|
||||
["kubectl", ["auth", "can-i", "get", "pods", "-n", "hwlab-dev"]],
|
||||
["kubectl", ["get", "namespace", "hwlab-dev", "-o", "json"]],
|
||||
["kubectl", ["-n", "hwlab-dev", "get", "deploy,svc,job,cm", "-o", "name"]]
|
||||
["env", nativeKubectl(["version", "--client=true"])],
|
||||
["env", nativeKubectl(["config", "current-context"])],
|
||||
["env", nativeKubectl(["get", "nodes", "-o", "jsonpath={.items[*].metadata.name}"])],
|
||||
["env", nativeKubectl(["auth", "can-i", "get", "pods", "-n", "hwlab-dev"])],
|
||||
["env", nativeKubectl(["get", "namespace", "hwlab-dev", "-o", "json"])],
|
||||
["env", nativeKubectl(["-n", "hwlab-dev", "get", "deploy,svc,job,cm", "-o", "name"])]
|
||||
]) {
|
||||
k3sEvidence.push(await run(probe[0], probe[1], { timeoutMs }));
|
||||
}
|
||||
const ok = k3sEvidence.every((probe) => probe.ok);
|
||||
const nodeProbe = k3sEvidence[2];
|
||||
const ok = k3sEvidence.every((probe) => probe.ok) && String(nodeProbe.stdout ?? "").split(/\s+/u).includes("d601");
|
||||
reporter.check("d601-k3s-read-access", "k3s", ok ? "pass" : "blocked", ok ? "Read-only kubectl probes reached hwlab-dev." : "At least one read-only kubectl probe failed.", k3sEvidence);
|
||||
if (!ok) {
|
||||
reporter.block({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const defaultRegistryPrefix = "127.0.0.1:5000/hwlab";
|
||||
const d601NativeKubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
|
||||
const imagePullBackoffReasons = new Set(["ErrImagePull", "ImagePullBackOff", "InvalidImageName"]);
|
||||
|
||||
function commandLine(command, args) {
|
||||
@@ -297,10 +298,11 @@ async function probeK3sPullAccess(registryPrefix, timeoutMs) {
|
||||
};
|
||||
}
|
||||
|
||||
const nativeKubectlArgs = (args) => ["KUBECONFIG=" + d601NativeKubeconfigPath, "kubectl", ...args];
|
||||
const [context, canGetPods, podsResult] = await Promise.all([
|
||||
run("kubectl", ["config", "current-context"], { timeoutMs }),
|
||||
run("kubectl", ["auth", "can-i", "get", "pods", "-n", "hwlab-dev"], { timeoutMs }),
|
||||
run("kubectl", ["-n", "hwlab-dev", "get", "pods", "-o", "json"], { timeoutMs })
|
||||
run("env", nativeKubectlArgs(["config", "current-context"]), { timeoutMs }),
|
||||
run("env", nativeKubectlArgs(["auth", "can-i", "get", "pods", "-n", "hwlab-dev"]), { timeoutMs }),
|
||||
run("env", nativeKubectlArgs(["-n", "hwlab-dev", "get", "pods", "-o", "json"]), { timeoutMs })
|
||||
]);
|
||||
const canReadPods = canGetPods.ok && canGetPods.stdout === "yes" && podsResult.ok;
|
||||
if (!canReadPods) {
|
||||
@@ -314,6 +316,7 @@ async function probeK3sPullAccess(registryPrefix, timeoutMs) {
|
||||
summary: "Read-only kubectl probes could not inspect hwlab-dev pods, so k3s pull access is blocked.",
|
||||
evidence: {
|
||||
kubectlAvailable: true,
|
||||
kubeconfig: d601NativeKubeconfigPath,
|
||||
context: summarizeCommand(context),
|
||||
canGetPods: summarizeCommand(canGetPods),
|
||||
pods: summarizeCommand(podsResult),
|
||||
@@ -341,6 +344,7 @@ async function probeK3sPullAccess(registryPrefix, timeoutMs) {
|
||||
: "Read-only kubectl can inspect hwlab-dev, but no current pod proves k3s pulled this registry prefix."),
|
||||
evidence: {
|
||||
kubectlAvailable: true,
|
||||
kubeconfig: d601NativeKubeconfigPath,
|
||||
context: summarizeCommand(context),
|
||||
canGetPods: summarizeCommand(canGetPods),
|
||||
podCount: pods.length,
|
||||
|
||||
Reference in New Issue
Block a user