fix: harden d601 k3s guards
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type DeployJsonExecutorMirror,
|
||||
type DeployJsonServiceContract,
|
||||
} from "./deploy-json-contract";
|
||||
import { d601K3sGuardShellLines } from "./d601-k3s-guard";
|
||||
|
||||
export type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | "install" | "deploy-backend-core" | "deploy-service";
|
||||
type ArtifactDeployEnvironment = "prod" | "dev";
|
||||
@@ -982,6 +983,10 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function d601K3sGuardScript(): string {
|
||||
return d601K3sGuardShellLines().join("\n");
|
||||
}
|
||||
|
||||
function base64(value: string): string {
|
||||
return Buffer.from(value, "utf8").toString("base64");
|
||||
}
|
||||
@@ -2350,6 +2355,7 @@ function d601DevFrontendAuthPatchScript(config: UniDeskConfig): string {
|
||||
SESSION_TTL_SECONDS: String(config.auth.sessionTtlSeconds),
|
||||
};
|
||||
return [
|
||||
d601K3sGuardScript(),
|
||||
`secret_patch=${shellQuote(JSON.stringify({ data: secretData }))}`,
|
||||
`config_patch=${shellQuote(JSON.stringify({ data: configData }))}`,
|
||||
"kubectl -n unidesk-dev patch secret unidesk-dev-runtime-secrets --type merge -p \"$secret_patch\"",
|
||||
@@ -3095,13 +3101,11 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
" [ -n \"${DOCKER_CONFIG:-}\" ] && rm -rf \"$DOCKER_CONFIG\"",
|
||||
"}",
|
||||
"trap cleanup_artifact_cd EXIT",
|
||||
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml",
|
||||
"command -v docker >/dev/null",
|
||||
"command -v kubectl >/dev/null",
|
||||
"command -v ctr >/dev/null",
|
||||
"test -S /run/k3s/containerd/containerd.sock",
|
||||
"test \"$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}')\" = \"d601\"",
|
||||
"! kubectl config current-context | grep -Eq 'docker-desktop|desktop-control-plane'",
|
||||
d601K3sGuardScript(),
|
||||
`curl -fsSI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' ${shellQuote(`http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`)} >/dev/null`,
|
||||
"docker pull -q \"$registry_image\" >/dev/null",
|
||||
"label_commit=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}')",
|
||||
|
||||
@@ -21,6 +21,7 @@ const syntaxFiles = [
|
||||
"scripts/src/auth-broker.ts",
|
||||
"scripts/src/code-queue.ts",
|
||||
"scripts/src/command.ts",
|
||||
"scripts/src/d601-k3s-guard.ts",
|
||||
"scripts/src/decision-center.ts",
|
||||
"scripts/src/dev-env.ts",
|
||||
"scripts/src/deploy.ts",
|
||||
|
||||
+11
-8
@@ -13,9 +13,10 @@ import {
|
||||
parseArtifactRegistryOptions,
|
||||
type ArtifactRegistryReadonlyProbe,
|
||||
} from "./artifact-registry";
|
||||
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "./d601-k3s-guard";
|
||||
|
||||
const d601ProviderId = "D601";
|
||||
const d601Kubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const d601Kubeconfig = d601NativeKubeconfig;
|
||||
const tektonPipelineVersion = "v1.12.0";
|
||||
const tektonTriggersVersion = "v0.34.0";
|
||||
const tektonPipelineReleaseUrl = `https://infra.tekton.dev/tekton-releases/pipeline/previous/${tektonPipelineVersion}/release.yaml`;
|
||||
@@ -503,7 +504,7 @@ function publishPreflightFailedScopes(preflight: PublishPreflight): string[] {
|
||||
function ciRunnerPreflightScript(sourceHostPath: string): string {
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
...d601K3sGuardShellLines(d601Kubeconfig),
|
||||
"printf 'provider_host_ssh=ok\\n'",
|
||||
"printf 'kubectl='",
|
||||
"command -v kubectl >/dev/null && printf 'ok\\n' || { printf 'missing\\n'; exit 127; }",
|
||||
@@ -530,11 +531,12 @@ function keyValueBool(stdout: string, key: string): boolean {
|
||||
const match = new RegExp(`^${key}=(.*)$`, "mu").exec(stdout);
|
||||
if (match === null) return false;
|
||||
const value = match[1]?.trim().toLowerCase() ?? "";
|
||||
return value === "true" || value === "ok";
|
||||
return value === "true" || value === "ok" || value === "pass" || value.startsWith("pass ");
|
||||
}
|
||||
|
||||
function backendCoreCiRunnerReady(result: DispatchResult): boolean {
|
||||
return result.ok
|
||||
&& keyValueBool(result.stdout, "d601_native_k3s_guard")
|
||||
&& keyValueBool(result.stdout, "kubectl")
|
||||
&& keyValueBool(result.stdout, "docker")
|
||||
&& keyValueBool(result.stdout, "namespace")
|
||||
@@ -751,7 +753,7 @@ async function runRemoteKubectl(script: string, waitMs = 60_000, remoteTimeoutMs
|
||||
async function runRemoteKubectlRaw(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000): Promise<DispatchResult> {
|
||||
const command = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
...d601K3sGuardShellLines(d601Kubeconfig, { passOutput: "stderr" }),
|
||||
script,
|
||||
].join("\n");
|
||||
return dispatchSsh(command, waitMs, remoteTimeoutMs);
|
||||
@@ -857,7 +859,7 @@ async function remoteApplyManifest(path: string): Promise<void> {
|
||||
if (!upload.ok) throw new Error(`failed to upload manifest ${path}: ${upload.stderr || upload.stdout}`);
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
...d601K3sGuardShellLines(d601Kubeconfig),
|
||||
"tmp=$(mktemp /tmp/unidesk-ci-apply.XXXXXX.yaml)",
|
||||
`b64_path=${shellQuote(b64Path)}`,
|
||||
"trap 'rm -f \"$tmp\" \"$b64_path\"' EXIT",
|
||||
@@ -872,7 +874,7 @@ async function prewarmCiRuntimeImages(): Promise<void> {
|
||||
const images = ciRuntimeImages.map(shellQuote).join(" ");
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
...d601K3sGuardShellLines(d601Kubeconfig),
|
||||
"export DOCKER_CONFIG=/tmp/unidesk-ci-docker-config",
|
||||
"mkdir -p \"$DOCKER_CONFIG\"",
|
||||
"printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
|
||||
@@ -946,7 +948,7 @@ async function install(): Promise<Record<string, unknown>> {
|
||||
await prewarmCiRuntimeImages();
|
||||
const installTektonScript = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
...d601K3sGuardShellLines(d601Kubeconfig),
|
||||
`kubectl apply -f ${shellQuote(tektonPipelineReleaseUrl)}`,
|
||||
"kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s",
|
||||
`kubectl apply -f ${shellQuote(tektonTriggersReleaseUrl)}`,
|
||||
@@ -1321,7 +1323,7 @@ async function waitForPipelineRun(name: string, waitMs: number): Promise<Dispatc
|
||||
if (waitMs <= 0) return null;
|
||||
const command = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
|
||||
...d601K3sGuardShellLines(d601Kubeconfig),
|
||||
`printf 'waiting_pipelinerun=%s\\n' ${shellQuote(name)}`,
|
||||
`deadline=$((SECONDS + ${Math.ceil(waitMs / 1000)}))`,
|
||||
"while [ \"$SECONDS\" -lt \"$deadline\" ]; do",
|
||||
@@ -1511,6 +1513,7 @@ async function publishUserServicePreflight(
|
||||
|
||||
const probeScript = [
|
||||
"set -euo pipefail",
|
||||
...d601K3sGuardShellLines(d601Kubeconfig),
|
||||
"printf 'provider_host_ssh=ok\\n'",
|
||||
"command -v bash >/dev/null",
|
||||
"command -v docker >/dev/null",
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
export const d601NativeKubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
export const d601RequiredNodeName = "d601";
|
||||
|
||||
export type D601K3sGuardStatus = "pass" | "refused" | "blocked";
|
||||
|
||||
export interface D601K3sTargetObservation {
|
||||
kubeconfig: string;
|
||||
expectedKubeconfig?: string;
|
||||
currentContext: string | null;
|
||||
apiServer: string | null;
|
||||
nodeNames: string[];
|
||||
commandsOk: boolean;
|
||||
combinedText: string;
|
||||
}
|
||||
|
||||
export interface D601K3sDefaultKubectlDiagnostic {
|
||||
checked: boolean;
|
||||
currentContext: string | null;
|
||||
apiServer: string | null;
|
||||
refusalSignals: string[];
|
||||
status: "clean" | "stale-forbidden-default" | "unavailable";
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface D601K3sGuardClassification {
|
||||
status: D601K3sGuardStatus;
|
||||
refusal: boolean;
|
||||
refusalSignals: string[];
|
||||
kubeconfig: string;
|
||||
expectedKubeconfig: string;
|
||||
currentContext: string | null;
|
||||
apiServer: string | null;
|
||||
nodeNames: string[];
|
||||
nodeCount: number;
|
||||
requiredNodeName: string;
|
||||
requiredNodePresent: boolean;
|
||||
commandsOk: boolean;
|
||||
defaultKubectlDiagnostic?: D601K3sDefaultKubectlDiagnostic;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface D601K3sGuardShellOptions {
|
||||
passOutput?: "stdout" | "stderr" | "quiet";
|
||||
}
|
||||
|
||||
function uniqueSignals(signals: Array<string | null>): string[] {
|
||||
return [...new Set(signals.filter((signal): signal is string => signal !== null))];
|
||||
}
|
||||
|
||||
export function d601ForbiddenKubeSignals(text: string): string[] {
|
||||
return uniqueSignals([
|
||||
/docker-desktop/iu.test(text) ? "docker-desktop" : null,
|
||||
/desktop-control-plane/iu.test(text) ? "desktop-control-plane" : null,
|
||||
/127\.0\.0\.1:11700/u.test(text) ? "127.0.0.1:11700" : null,
|
||||
]);
|
||||
}
|
||||
|
||||
export function classifyD601DefaultKubectlDiagnostic(input: {
|
||||
currentContext: string | null;
|
||||
apiServer: string | null;
|
||||
combinedText: string;
|
||||
commandsOk: boolean;
|
||||
}): D601K3sDefaultKubectlDiagnostic {
|
||||
const refusalSignals = d601ForbiddenKubeSignals(input.combinedText);
|
||||
if (!input.commandsOk) {
|
||||
return {
|
||||
checked: true,
|
||||
currentContext: input.currentContext,
|
||||
apiServer: input.apiServer,
|
||||
refusalSignals,
|
||||
status: "unavailable",
|
||||
summary: "Default kubectl diagnostic could not read context/server; this does not block the explicit D601 target.",
|
||||
};
|
||||
}
|
||||
if (refusalSignals.length > 0) {
|
||||
return {
|
||||
checked: true,
|
||||
currentContext: input.currentContext,
|
||||
apiServer: input.apiServer,
|
||||
refusalSignals,
|
||||
status: "stale-forbidden-default",
|
||||
summary: "Default kubectl resolves to a forbidden local control-plane signal; explicit D601 KUBECONFIG remains the deploy target.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
checked: true,
|
||||
currentContext: input.currentContext,
|
||||
apiServer: input.apiServer,
|
||||
refusalSignals,
|
||||
status: "clean",
|
||||
summary: "Default kubectl diagnostic did not show Docker Desktop control-plane signals.",
|
||||
};
|
||||
}
|
||||
|
||||
export function classifyD601K3sTarget(
|
||||
observation: D601K3sTargetObservation,
|
||||
defaultKubectlDiagnostic?: D601K3sDefaultKubectlDiagnostic,
|
||||
): D601K3sGuardClassification {
|
||||
const expectedKubeconfig = observation.expectedKubeconfig ?? d601NativeKubeconfig;
|
||||
const refusalSignals = d601ForbiddenKubeSignals(observation.combinedText);
|
||||
const requiredNodePresent = observation.nodeNames.includes(d601RequiredNodeName);
|
||||
const wrongKubeconfig = observation.kubeconfig !== expectedKubeconfig;
|
||||
const refusal = refusalSignals.length > 0;
|
||||
const status: D601K3sGuardStatus = refusal
|
||||
? "refused"
|
||||
: !observation.commandsOk || wrongKubeconfig || !requiredNodePresent
|
||||
? "blocked"
|
||||
: "pass";
|
||||
const defaultStale = defaultKubectlDiagnostic?.status === "stale-forbidden-default";
|
||||
const summary = refusal
|
||||
? "Refusing D601 k3s operation because the explicit target kubeconfig resolved to a forbidden Docker Desktop control-plane signal."
|
||||
: wrongKubeconfig
|
||||
? `D601 k3s guard blocked: expected explicit KUBECONFIG=${expectedKubeconfig}.`
|
||||
: !observation.commandsOk
|
||||
? "D601 k3s guard blocked: explicit target kubeconfig could not read context, server, and nodes."
|
||||
: !requiredNodePresent
|
||||
? `D601 k3s guard blocked: explicit target kubeconfig did not report node ${d601RequiredNodeName}.`
|
||||
: defaultStale
|
||||
? "D601 native k3s guard passed with explicit KUBECONFIG; stale default kubectl context was observed only as a diagnostic."
|
||||
: "D601 native k3s guard passed with explicit KUBECONFIG.";
|
||||
return {
|
||||
status,
|
||||
refusal,
|
||||
refusalSignals,
|
||||
kubeconfig: observation.kubeconfig,
|
||||
expectedKubeconfig,
|
||||
currentContext: observation.currentContext,
|
||||
apiServer: observation.apiServer,
|
||||
nodeNames: observation.nodeNames,
|
||||
nodeCount: observation.nodeNames.length,
|
||||
requiredNodeName: d601RequiredNodeName,
|
||||
requiredNodePresent,
|
||||
commandsOk: observation.commandsOk,
|
||||
...(defaultKubectlDiagnostic === undefined ? {} : { defaultKubectlDiagnostic }),
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/gu, "'\\''")}'`;
|
||||
}
|
||||
|
||||
export function d601K3sGuardShellLines(kubeconfig = d601NativeKubeconfig, options: D601K3sGuardShellOptions = {}): string[] {
|
||||
const passOutput = options.passOutput ?? "stdout";
|
||||
const passLine = passOutput === "quiet"
|
||||
? ":"
|
||||
: passOutput === "stderr"
|
||||
? "printf 'd601_native_k3s_guard=pass kubeconfig=%s context=%s server=%s node=%s\\n' \"$required_kubeconfig\" \"$context\" \"$server\" \"$required_node\" >&2"
|
||||
: "printf 'd601_native_k3s_guard=pass kubeconfig=%s context=%s server=%s node=%s\\n' \"$required_kubeconfig\" \"$context\" \"$server\" \"$required_node\"";
|
||||
return [
|
||||
`export KUBECONFIG=${shellQuote(kubeconfig)}`,
|
||||
"d601_k3s_guard() {",
|
||||
` required_kubeconfig=${shellQuote(kubeconfig)}`,
|
||||
` required_node=${shellQuote(d601RequiredNodeName)}`,
|
||||
" if [ \"${KUBECONFIG:-}\" != \"$required_kubeconfig\" ]; then",
|
||||
" printf 'd601_native_k3s_guard=blocked reason=wrong-kubeconfig expected=%s actual=%s\\n' \"$required_kubeconfig\" \"${KUBECONFIG:-<unset>}\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" if ! command -v kubectl >/dev/null 2>&1; then",
|
||||
" echo 'd601_native_k3s_guard=blocked reason=kubectl-missing' >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" if ! context=$(kubectl config current-context 2>&1); then",
|
||||
" printf 'd601_native_k3s_guard=blocked reason=context-read-failed detail=%s\\n' \"$context\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" if ! server=$(kubectl config view --minify -o 'jsonpath={.clusters[0].cluster.server}' 2>&1); then",
|
||||
" printf 'd601_native_k3s_guard=blocked reason=server-read-failed detail=%s\\n' \"$server\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" if ! nodes=$(kubectl get nodes -o 'jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}' 2>&1); then",
|
||||
" printf 'd601_native_k3s_guard=blocked reason=nodes-read-failed detail=%s\\n' \"$nodes\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" combined=$(printf '%s\\n%s\\n%s\\n' \"$context\" \"$server\" \"$nodes\")",
|
||||
" if printf '%s\\n' \"$combined\" | grep -Eiq 'docker-desktop|desktop-control-plane|127\\.0\\.0\\.1:11700'; then",
|
||||
" printf 'd601_native_k3s_guard=refused reason=forbidden-control-plane context=%s server=%s nodes=%s\\n' \"$context\" \"$server\" \"$(printf '%s' \"$nodes\" | tr '\\n' ',')\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" if ! printf '%s\\n' \"$nodes\" | grep -Fx \"$required_node\" >/dev/null; then",
|
||||
" printf 'd601_native_k3s_guard=blocked reason=missing-d601-node context=%s server=%s nodes=%s\\n' \"$context\" \"$server\" \"$(printf '%s' \"$nodes\" | tr '\\n' ',')\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
` ${passLine}`,
|
||||
"}",
|
||||
"d601_k3s_guard",
|
||||
];
|
||||
}
|
||||
+13
-4
@@ -9,6 +9,7 @@ import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText
|
||||
import { startJob } from "./jobs";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "./code-queue-source-guard";
|
||||
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "./d601-k3s-guard";
|
||||
import {
|
||||
compareDeployJsonExecutorMirrors,
|
||||
deployJsonCommitImage,
|
||||
@@ -138,7 +139,7 @@ const providerDispatchCompletionLagMs = 45_000;
|
||||
const pollIntervalMs = 5_000;
|
||||
const remoteDeployRoot = "/home/ubuntu/.unidesk/deploy";
|
||||
const k8sNamespace = "unidesk";
|
||||
const k8sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const k8sKubeconfig = d601NativeKubeconfig;
|
||||
// Production k3s hostPath repo. Code Queue production Pods mount this path as /app and /root/unidesk,
|
||||
// so deploy guards must validate this tree rather than config.json development.worktreePath.
|
||||
const k3sProductionHostPathRepoDir = "/home/ubuntu/cq-deploy";
|
||||
@@ -260,6 +261,10 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/gu, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function d601K3sGuardScript(): string {
|
||||
return d601K3sGuardShellLines(k8sKubeconfig).join("\n");
|
||||
}
|
||||
|
||||
function compactTail(text: string, maxChars = 1600): string {
|
||||
return text.length > maxChars ? text.slice(text.length - maxChars) : text;
|
||||
}
|
||||
@@ -1518,10 +1523,11 @@ function syncDevFrontendAuthScript(config: UniDeskConfig): string {
|
||||
};
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
d601K3sGuardScript(),
|
||||
`secret_patch=${shellQuote(JSON.stringify({ data }))}`,
|
||||
`config_patch=${shellQuote(JSON.stringify({ data: runtimeConfig }))}`,
|
||||
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n unidesk-dev patch secret unidesk-dev-runtime-secrets --type merge -p "$secret_patch"`,
|
||||
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n unidesk-dev patch configmap unidesk-dev-runtime-config --type merge -p "$config_patch"`,
|
||||
`kubectl -n unidesk-dev patch secret unidesk-dev-runtime-secrets --type merge -p "$secret_patch"`,
|
||||
`kubectl -n unidesk-dev patch configmap unidesk-dev-runtime-config --type merge -p "$config_patch"`,
|
||||
"echo dev_frontend_auth_synced=ok",
|
||||
].join("\n");
|
||||
}
|
||||
@@ -2124,6 +2130,7 @@ function ensureNativeK3sScript(): string {
|
||||
` if KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes >/dev/null 2>&1; then break; fi`,
|
||||
" sleep 2",
|
||||
"done",
|
||||
d601K3sGuardScript(),
|
||||
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes -l unidesk.ai/node-id=D601 --no-headers | grep -q .`,
|
||||
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl wait --for=condition=Ready node -l unidesk.ai/node-id=D601 --timeout=180s`,
|
||||
"install_system_images_from_legacy_k3s",
|
||||
@@ -2248,8 +2255,10 @@ function applyK8sScript(service: UniDeskMicroserviceConfig): string {
|
||||
].join("\n")
|
||||
: "";
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
d601K3sGuardScript(),
|
||||
cleanup,
|
||||
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl apply -f ${shellQuote(manifest)}`,
|
||||
`kubectl apply -f ${shellQuote(manifest)}`,
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
|
||||
+23
-5
@@ -1,6 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { runCommand } from "./command";
|
||||
import { repoRoot, rootPath } from "./config";
|
||||
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "./d601-k3s-guard";
|
||||
import { startJob } from "./jobs";
|
||||
|
||||
const defaultManifest = "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml";
|
||||
@@ -216,7 +217,26 @@ function validateDatabaseUrl(url: string): { ok: boolean; url: string; reason: s
|
||||
}
|
||||
|
||||
function kubectlDryRun(manifestPath: string): unknown {
|
||||
const kubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const kubeconfig = d601NativeKubeconfig;
|
||||
const guardScript = d601K3sGuardShellLines(kubeconfig).join("\n");
|
||||
const guard = runCommand(["sh", "-lc", guardScript], repoRoot, {
|
||||
timeoutMs: 60_000,
|
||||
env: { ...process.env, KUBECONFIG: kubeconfig },
|
||||
});
|
||||
const guarded = guard.exitCode === 0;
|
||||
if (!guarded) {
|
||||
return {
|
||||
command: ["sh", "-lc", "d601 native k3s guard"],
|
||||
kubeconfig,
|
||||
exitCode: guard.exitCode,
|
||||
signal: guard.signal,
|
||||
timedOut: guard.timedOut,
|
||||
ok: false,
|
||||
guard: "d601-native-k3s",
|
||||
stdoutTail: guard.stdout.slice(-4000),
|
||||
stderrTail: guard.stderr.slice(-4000),
|
||||
};
|
||||
}
|
||||
const result = runCommand(["kubectl", "apply", "--dry-run=client", "--validate=false", "-f", manifestPath], repoRoot, {
|
||||
timeoutMs: 60_000,
|
||||
env: { ...process.env, KUBECONFIG: kubeconfig },
|
||||
@@ -262,15 +282,13 @@ function prewarmImagesScript(options: PrewarmImagesOptions): string {
|
||||
`proxy_url=${shellQuote(options.proxyUrl)}`,
|
||||
`pull_missing=${options.pullMissing ? "1" : "0"}`,
|
||||
`pull_timeout_seconds=${pullTimeoutSeconds}`,
|
||||
"kubeconfig=/etc/rancher/k3s/k3s.yaml",
|
||||
"ctr_address=/run/k3s/containerd/containerd.sock",
|
||||
...d601K3sGuardShellLines(),
|
||||
"export DOCKER_CONFIG=/tmp/unidesk-dev-env-docker-config",
|
||||
"mkdir -p \"$DOCKER_CONFIG\"",
|
||||
"printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
|
||||
"printf 'dev_env_k3s_context='",
|
||||
"KUBECONFIG=\"$kubeconfig\" kubectl config current-context",
|
||||
"printf 'dev_env_k3s_nodes='",
|
||||
"KUBECONFIG=\"$kubeconfig\" kubectl get nodes -o name | tr '\\n' ' '",
|
||||
"kubectl get nodes -o name | tr '\\n' ' '",
|
||||
"printf '\\n'",
|
||||
"for image in \"${images[@]}\"; do",
|
||||
" if docker image inspect \"$image\" >/dev/null 2>&1; then",
|
||||
|
||||
+25
-22
@@ -4,6 +4,11 @@ import { createWriteStream, existsSync, mkdirSync, openSync, readSync, statSync,
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { repoRoot, rootPath } from "./config";
|
||||
import {
|
||||
classifyD601DefaultKubectlDiagnostic,
|
||||
classifyD601K3sTarget,
|
||||
d601NativeKubeconfig,
|
||||
} from "./d601-k3s-guard";
|
||||
|
||||
type HwlabCdAction = "status" | "apply";
|
||||
type HwlabCdEnvironment = "dev";
|
||||
@@ -59,7 +64,7 @@ interface CommandView {
|
||||
|
||||
const namespace = "hwlab-dev";
|
||||
const lockName = "hwlab-dev-cd-lock";
|
||||
const nativeKubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const nativeKubeconfig = d601NativeKubeconfig;
|
||||
const defaultFrontendLiveUrl = "http://74.48.78.17:16666/health/live";
|
||||
const defaultApiLiveUrl = "http://74.48.78.17:16667/health/live";
|
||||
const parseCaptureLimitBytes = 4 * 1024 * 1024;
|
||||
@@ -347,38 +352,36 @@ async function gitSummary(repoPath: string, dumpDir: string, timeoutMs: number):
|
||||
|
||||
async function nativeK3sGuard(kubeconfig: string, dumpDir: string, timeoutMs: number): Promise<Record<string, unknown>> {
|
||||
const env = { ...process.env, KUBECONFIG: kubeconfig };
|
||||
const [context, server, nodes] = await Promise.all([
|
||||
const [context, server, nodes, defaultContext, defaultServer, defaultNodes] = await Promise.all([
|
||||
runCaptured(["kubectl", "config", "current-context"], repoRoot, dumpDir, "k3s-current-context", { env, timeoutMs }),
|
||||
runCaptured(["kubectl", "config", "view", "--minify", "-o", "jsonpath={.clusters[0].cluster.server}"], repoRoot, dumpDir, "k3s-server", { env, timeoutMs }),
|
||||
runCaptured(["kubectl", "get", "nodes", "-o", "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}"], repoRoot, dumpDir, "k3s-nodes", { env, timeoutMs }),
|
||||
runCaptured(["kubectl", "config", "current-context"], repoRoot, dumpDir, "default-kubectl-current-context", { timeoutMs }),
|
||||
runCaptured(["kubectl", "config", "view", "--minify", "-o", "jsonpath={.clusters[0].cluster.server}"], repoRoot, dumpDir, "default-kubectl-server", { timeoutMs }),
|
||||
runCaptured(["kubectl", "get", "nodes", "-o", "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}"], repoRoot, dumpDir, "default-kubectl-nodes", { timeoutMs }),
|
||||
]);
|
||||
const contextText = context.stdoutText.trim();
|
||||
const serverText = server.stdoutText.trim();
|
||||
const nodeNames = nodes.stdoutText.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
||||
const combined = `${context.stdoutText}\n${context.stderrText}\n${server.stdoutText}\n${server.stderrText}\n${nodes.stdoutText}\n${nodes.stderrText}`;
|
||||
const refusalSignals = [
|
||||
/docker-desktop/iu.test(combined) ? "docker-desktop" : null,
|
||||
/desktop-control-plane/iu.test(combined) ? "desktop-control-plane" : null,
|
||||
/127\.0\.0\.1:11700/u.test(combined) ? "127.0.0.1:11700" : null,
|
||||
].filter((signal): signal is string => signal !== null);
|
||||
const refusal = refusalSignals.length > 0;
|
||||
const readable = context.ok && server.ok && nodes.ok;
|
||||
return {
|
||||
status: refusal ? "refused" : readable ? "pass" : "blocked",
|
||||
refusal,
|
||||
refusalSignals,
|
||||
const defaultDiagnostic = classifyD601DefaultKubectlDiagnostic({
|
||||
currentContext: defaultContext.stdoutText.trim() || null,
|
||||
apiServer: defaultServer.stdoutText.trim() || null,
|
||||
combinedText: `${defaultContext.stdoutText}\n${defaultContext.stderrText}\n${defaultServer.stdoutText}\n${defaultServer.stderrText}\n${defaultNodes.stdoutText}\n${defaultNodes.stderrText}`,
|
||||
commandsOk: defaultContext.ok && defaultServer.ok && defaultNodes.ok,
|
||||
});
|
||||
const guard = classifyD601K3sTarget({
|
||||
kubeconfig,
|
||||
injectedEnv: { KUBECONFIG: kubeconfig },
|
||||
expectedKubeconfig: nativeKubeconfig,
|
||||
currentContext: contextText || null,
|
||||
apiServer: serverText || null,
|
||||
nodeNames,
|
||||
nodeCount: nodeNames.length,
|
||||
summary: refusal
|
||||
? "Refusing HWLAB CD because kubectl resolved to a Docker Desktop control plane signal."
|
||||
: readable
|
||||
? "D601 native k3s guard passed with explicit KUBECONFIG."
|
||||
: "D601 native k3s guard could not fully read context, server, and nodes.",
|
||||
commands: [context, server, nodes].map(commandView),
|
||||
commandsOk: context.ok && server.ok && nodes.ok,
|
||||
combinedText: `${context.stdoutText}\n${context.stderrText}\n${server.stdoutText}\n${server.stderrText}\n${nodes.stdoutText}\n${nodes.stderrText}`,
|
||||
}, defaultDiagnostic);
|
||||
return {
|
||||
...guard,
|
||||
injectedEnv: { KUBECONFIG: kubeconfig },
|
||||
commands: [context, server, nodes, defaultContext, defaultServer, defaultNodes].map(commandView),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user