From f28019bd9ba68edb183e6b27bdc1f0fdf805ac6a Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 12:07:22 +0000 Subject: [PATCH] fix: add yaml ssh secret for d518 git mirror --- config/hwlab-node-control-plane.yaml | 16 ++ scripts/src/hwlab-node-control-plane.ts | 216 ++++++++++++++++++++++-- scripts/src/hwlab-node/entry.ts | 12 +- scripts/src/hwlab-node/render.ts | 16 +- scripts/src/hwlab-node/status.ts | 47 ++++-- scripts/src/hwlab-node/web-probe.ts | 28 ++- 6 files changed, 300 insertions(+), 35 deletions(-) diff --git a/config/hwlab-node-control-plane.yaml b/config/hwlab-node-control-plane.yaml index 52179dbd..1bcb6f08 100644 --- a/config/hwlab-node-control-plane.yaml +++ b/config/hwlab-node-control-plane.yaml @@ -186,6 +186,14 @@ targets: required: true githubTransport: mode: ssh + privateKeySecretKey: ssh-privatekey + privateKeySourceRef: github/hwlab-git-mirror-ssh.env + privateKeySourceKey: GITHUB_SSH_PRIVATE_KEY_B64 + privateKeySourceEncoding: base64 + knownHostsSecretKey: known_hosts + knownHostsSourceRef: github/hwlab-git-mirror-ssh.env + knownHostsSourceKey: GITHUB_KNOWN_HOSTS_B64 + knownHostsSourceEncoding: base64 tekton: pipelineName: hwlab-d601-v03-ci-image-publish serviceAccountName: hwlab-d601-v03-tekton-runner @@ -329,6 +337,14 @@ targets: required: true githubTransport: mode: ssh + privateKeySecretKey: ssh-privatekey + privateKeySourceRef: github/hwlab-git-mirror-ssh.env + privateKeySourceKey: GITHUB_SSH_PRIVATE_KEY_B64 + privateKeySourceEncoding: base64 + knownHostsSecretKey: known_hosts + knownHostsSourceRef: github/hwlab-git-mirror-ssh.env + knownHostsSourceKey: GITHUB_KNOWN_HOSTS_B64 + knownHostsSourceEncoding: base64 tekton: pipelineName: hwlab-d518-v03-ci-image-publish serviceAccountName: hwlab-d518-v03-tekton-runner diff --git a/scripts/src/hwlab-node-control-plane.ts b/scripts/src/hwlab-node-control-plane.ts index b9c27a2c..37642d16 100644 --- a/scripts/src/hwlab-node-control-plane.ts +++ b/scripts/src/hwlab-node-control-plane.ts @@ -112,7 +112,17 @@ interface ControlPlaneGitMirrorEgressProxySpec { } type ControlPlaneGitMirrorGithubTransportSpec = - | { mode: "ssh" } + | { + mode: "ssh"; + privateKeySecretKey: string; + privateKeySourceRef: string; + privateKeySourceKey: string; + privateKeySourceEncoding: "plain" | "base64"; + knownHostsSecretKey: string | null; + knownHostsSourceRef: string | null; + knownHostsSourceKey: string | null; + knownHostsSourceEncoding: "plain" | "base64" | null; + } | { mode: "https"; username: string; @@ -1451,7 +1461,38 @@ function gitMirrorEgressProxySpec(raw: Record, path: string): C function gitMirrorGithubTransportSpec(raw: Record, path: string): ControlPlaneGitMirrorGithubTransportSpec { const mode = stringField(raw, "mode", path); - if (mode === "ssh") return { mode }; + if (mode === "ssh") { + const privateKeySecretKey = stringField(raw, "privateKeySecretKey", path); + const privateKeySourceRef = stringField(raw, "privateKeySourceRef", path); + const privateKeySourceKey = stringField(raw, "privateKeySourceKey", path); + const privateKeySourceEncoding = secretSourceEncodingField(raw, "privateKeySourceEncoding", path); + const knownHostsSecretKey = optionalStringField(raw, "knownHostsSecretKey", path) ?? null; + const knownHostsSourceRef = optionalStringField(raw, "knownHostsSourceRef", path) ?? null; + const knownHostsSourceKey = optionalStringField(raw, "knownHostsSourceKey", path) ?? null; + const knownHostsSourceEncoding = raw.knownHostsSourceEncoding === undefined ? null : secretSourceEncodingField(raw, "knownHostsSourceEncoding", path); + const knownHostsFields = [knownHostsSecretKey, knownHostsSourceRef, knownHostsSourceKey, knownHostsSourceEncoding]; + if (knownHostsFields.some((value) => value !== null) && knownHostsFields.some((value) => value === null)) { + throw new Error(`${path}.knownHostsSecretKey/sourceRef/sourceKey/sourceEncoding must be declared together`); + } + validateSecretKey(privateKeySecretKey, `${path}.privateKeySecretKey`); + if (privateKeySecretKey !== "ssh-privatekey") throw new Error(`${path}.privateKeySecretKey must be ssh-privatekey for kubernetes.io/ssh-auth`); + validateSourceRef(privateKeySourceRef, `${path}.privateKeySourceRef`); + validateEnvKey(privateKeySourceKey, `${path}.privateKeySourceKey`); + if (knownHostsSecretKey !== null) validateSecretKey(knownHostsSecretKey, `${path}.knownHostsSecretKey`); + if (knownHostsSourceRef !== null) validateSourceRef(knownHostsSourceRef, `${path}.knownHostsSourceRef`); + if (knownHostsSourceKey !== null) validateEnvKey(knownHostsSourceKey, `${path}.knownHostsSourceKey`); + return { + mode, + privateKeySecretKey, + privateKeySourceRef, + privateKeySourceKey, + privateKeySourceEncoding, + knownHostsSecretKey, + knownHostsSourceRef, + knownHostsSourceKey, + knownHostsSourceEncoding, + }; + } if (mode !== "https") throw new Error(`${path}.mode must be ssh or https`); const tokenSecretName = stringField(raw, "tokenSecretName", path); const tokenSecretKey = stringField(raw, "tokenSecretKey", path); @@ -1471,6 +1512,12 @@ function gitMirrorGithubTransportSpec(raw: Record, path: string }; } +function secretSourceEncodingField(raw: Record, key: string, path: string): "plain" | "base64" { + const value = stringField(raw, key, path); + if (value !== "plain" && value !== "base64") throw new Error(`${path}.${key} must be plain or base64`); + return value; +} + function targetSpec(raw: Record, index: number): ControlPlaneTargetSpec { const path = `targets[${index}]`; const source = asRecord(raw.source, `${path}.source`); @@ -1560,6 +1607,8 @@ function renderInfraManifest(_node: ControlPlaneNodeSpec, target: ControlPlaneTa ); const githubTokenSecret = gitMirrorGithubTokenSecret(target, labels); if (githubTokenSecret !== null) manifests.push(githubTokenSecret); + const githubSshSecret = gitMirrorGithubSshSecret(target, labels); + if (githubSshSecret !== null) manifests.push(githubSshSecret); if (target.gitMirror.cacheHostPath === null) { manifests.push({ apiVersion: "v1", @@ -1601,6 +1650,81 @@ function renderInfraManifest(_node: ControlPlaneNodeSpec, target: ControlPlaneTa return manifests; } +function gitMirrorGithubSshSecret(target: ControlPlaneTargetSpec, labels: Record): Record | null { + const transport = target.gitMirror.githubTransport; + if (transport.mode !== "ssh") return null; + const material = gitMirrorGithubSshMaterial(transport); + return { + apiVersion: "v1", + kind: "Secret", + metadata: { + name: target.gitMirror.secretName, + namespace: target.gitMirror.namespace, + labels: { ...labels, "app.kubernetes.io/name": "git-mirror" }, + annotations: { + "unidesk.ai/private-key-source-ref": transport.privateKeySourceRef, + "unidesk.ai/private-key-source-key": transport.privateKeySourceKey, + "unidesk.ai/private-key-target-key": transport.privateKeySecretKey, + "unidesk.ai/private-key-fingerprint": material.privateKeyFingerprint, + ...(transport.knownHostsSecretKey === null ? {} : { + "unidesk.ai/known-hosts-source-ref": transport.knownHostsSourceRef ?? "", + "unidesk.ai/known-hosts-source-key": transport.knownHostsSourceKey ?? "", + "unidesk.ai/known-hosts-target-key": transport.knownHostsSecretKey, + "unidesk.ai/known-hosts-fingerprint": material.knownHostsFingerprint ?? "", + }), + }, + }, + type: "kubernetes.io/ssh-auth", + stringData: { + [transport.privateKeySecretKey]: material.privateKey, + ...(transport.knownHostsSecretKey === null || material.knownHosts === null ? {} : { [transport.knownHostsSecretKey]: material.knownHosts }), + }, + }; +} + +function gitMirrorGithubSshMaterial(transport: Extract): { privateKey: string; knownHosts: string | null; privateKeyFingerprint: string; knownHostsFingerprint: string | null } { + const privateSource = readControlPlaneSecretSource(transport.privateKeySourceRef, `gitMirror.githubTransport private key source ${transport.privateKeySourceRef} is missing; create the YAML-declared sourceRef with ${transport.privateKeySourceKey} before applying the control plane`); + const privateValue = requiredEnvValue(privateSource.values, transport.privateKeySourceKey, transport.privateKeySourceRef); + const privateKey = decodeSecretSourceValue(privateValue, transport.privateKeySourceEncoding, "gitMirror.githubTransport.privateKeySourceKey"); + if (!/-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(privateKey)) throw new Error(`${transport.privateKeySourceRef}.${transport.privateKeySourceKey} does not contain private key material`); + let knownHosts: string | null = null; + let knownHostsFingerprint: string | null = null; + if (transport.knownHostsSourceRef !== null && transport.knownHostsSourceKey !== null && transport.knownHostsSourceEncoding !== null) { + const knownHostsSource = readControlPlaneSecretSource(transport.knownHostsSourceRef, `gitMirror.githubTransport known_hosts source ${transport.knownHostsSourceRef} is missing; create the YAML-declared sourceRef with ${transport.knownHostsSourceKey} before applying the control plane`); + const knownHostsValue = requiredEnvValue(knownHostsSource.values, transport.knownHostsSourceKey, transport.knownHostsSourceRef); + knownHosts = decodeSecretSourceValue(knownHostsValue, transport.knownHostsSourceEncoding, "gitMirror.githubTransport.knownHostsSourceKey"); + if (!/(^|\n)(github\.com|\[github\.com\]:22)\s+(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256)\s+/u.test(knownHosts)) { + throw new Error(`${transport.knownHostsSourceRef}.${transport.knownHostsSourceKey} must contain github.com known_hosts rows`); + } + knownHostsFingerprint = fingerprintSecretValues({ knownHosts }, ["knownHosts"]); + } + return { + privateKey: privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, + knownHosts: knownHosts === null ? null : (knownHosts.endsWith("\n") ? knownHosts : `${knownHosts}\n`), + privateKeyFingerprint: fingerprintSecretValues({ privateKey }, ["privateKey"]), + knownHostsFingerprint, + }; +} + +function readControlPlaneSecretSource(sourceRef: string, missingMessage: string): ReturnType { + return readEnvSourceFile({ + root: rootPath(".state", "secrets"), + sourceRef, + missingMessage: () => missingMessage, + }); +} + +function decodeSecretSourceValue(value: string, encoding: "plain" | "base64", path: string): string { + if (encoding === "plain") return value; + try { + const compact = value.replace(/\s+/gu, ""); + if (compact.length === 0) throw new Error("empty base64 value"); + return Buffer.from(compact, "base64").toString("utf8"); + } catch (error) { + throw new Error(`${path} must be valid base64: ${error instanceof Error ? error.message : String(error)}`); + } +} + function gitMirrorGithubTokenSecret(target: ControlPlaneTargetSpec, labels: Record): Record | null { const transport = target.gitMirror.githubTransport; if (transport.mode !== "https") return null; @@ -1975,10 +2099,15 @@ function gitMirrorProxyPrelude(node: ControlPlaneNodeSpec, target: ControlPlaneT "remote=\"https://github.com/${repository}.git\"", ].join("\n"); } + const privateKeyPath = transport.mode === "ssh" ? `/git-ssh/${transport.privateKeySecretKey}` : "/git-ssh/ssh-privatekey"; + const knownHostsCopy = transport.mode === "ssh" && transport.knownHostsSecretKey !== null + ? [`cp ${shQuote(`/git-ssh/${transport.knownHostsSecretKey}`)} /root/.ssh/known_hosts`, "chmod 0600 /root/.ssh/known_hosts"] + : []; return [ "mkdir -p /root/.ssh", - "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa", + `cp ${shQuote(privateKeyPath)} /root/.ssh/id_rsa`, "chmod 0400 /root/.ssh/id_rsa", + ...knownHostsCopy, ...common, ...proxyConnectBlock, "cat > /tmp/hwlab-git-ssh-proxy.sh <<'SH_PROXY'", @@ -2294,7 +2423,20 @@ function gitMirrorEffectiveEgressProxySummary(node: ControlPlaneNodeSpec, target } function gitMirrorGithubTransportSummary(transport: ControlPlaneGitMirrorGithubTransportSpec): Record { - if (transport.mode === "ssh") return { mode: "ssh", valuesPrinted: false }; + if (transport.mode === "ssh") { + return { + mode: "ssh", + privateKeySecretKey: transport.privateKeySecretKey, + privateKeySourceRef: transport.privateKeySourceRef, + privateKeySourceKey: transport.privateKeySourceKey, + privateKeySourceEncoding: transport.privateKeySourceEncoding, + knownHostsSecretKey: transport.knownHostsSecretKey, + knownHostsSourceRef: transport.knownHostsSourceRef, + knownHostsSourceKey: transport.knownHostsSourceKey, + knownHostsSourceEncoding: transport.knownHostsSourceEncoding, + valuesPrinted: false, + }; + } return { mode: "https", username: transport.username, @@ -2332,6 +2474,13 @@ write_svc=${shQuote(target.gitMirror.serviceWriteName)} cache_pvc=${shQuote(target.gitMirror.cachePvcName)} cache_host_path=${shQuote(target.gitMirror.cacheHostPath ?? "")} github_transport_mode=${shQuote(target.gitMirror.githubTransport.mode)} +github_ssh_secret=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.secretName : "")} +github_ssh_private_key=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.githubTransport.privateKeySecretKey : "")} +github_ssh_private_source_ref=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.githubTransport.privateKeySourceRef : "")} +github_ssh_private_source_key=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.githubTransport.privateKeySourceKey : "")} +github_ssh_known_hosts_key=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.githubTransport.knownHostsSecretKey ?? "" : "")} +github_ssh_known_hosts_source_ref=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.githubTransport.knownHostsSourceRef ?? "" : "")} +github_ssh_known_hosts_source_key=${shQuote(target.gitMirror.githubTransport.mode === "ssh" ? target.gitMirror.githubTransport.knownHostsSourceKey ?? "" : "")} github_token_secret=${shQuote(target.gitMirror.githubTransport.mode === "https" ? target.gitMirror.githubTransport.tokenSecretName : "")} github_token_key=${shQuote(target.gitMirror.githubTransport.mode === "https" ? target.gitMirror.githubTransport.tokenSecretKey : "")} github_token_source_ref=${shQuote(target.gitMirror.githubTransport.mode === "https" ? target.gitMirror.githubTransport.tokenSourceRef : "")} @@ -2358,24 +2507,55 @@ exists_res() { kubectl -n "$1" get "$2" "$3" >/dev/null 2>&1 && printf true || p deploy_ready() { desired=$(kubectl -n "$1" get deploy "$2" -o 'jsonpath={.spec.replicas}' 2>/dev/null || true); ready=$(kubectl -n "$1" get deploy "$2" -o 'jsonpath={.status.readyReplicas}' 2>/dev/null || true); [ -n "$desired" ] && [ "$desired" -gt 0 ] 2>/dev/null && [ "\${ready:-0}" = "$desired" ] && printf true || printf false; } sts_ready() { desired=$(kubectl -n "$1" get statefulset "$2" -o 'jsonpath={.spec.replicas}' 2>/dev/null || true); ready=$(kubectl -n "$1" get statefulset "$2" -o 'jsonpath={.status.readyReplicas}' 2>/dev/null || true); [ -n "$desired" ] && [ "$desired" -gt 0 ] 2>/dev/null && [ "\${ready:-0}" = "$desired" ] && printf true || printf false; } endpoint_ready() { endpoints=$(kubectl -n "$1" get endpoints "$2" -o 'jsonpath={.subsets[*].addresses[*].ip}' 2>/dev/null || true); [ -n "$endpoints" ] && printf true || printf false; } -github_transport_json=$(python3 - "$github_transport_mode" "$gitmirror_ns" "$github_token_secret" "$github_token_key" "$github_token_source_ref" "$github_token_source_key" <<'PY' +github_transport_json=$(python3 - "$github_transport_mode" "$gitmirror_ns" "$github_ssh_secret" "$github_ssh_private_key" "$github_ssh_private_source_ref" "$github_ssh_private_source_key" "$github_ssh_known_hosts_key" "$github_ssh_known_hosts_source_ref" "$github_ssh_known_hosts_source_key" "$github_token_secret" "$github_token_key" "$github_token_source_ref" "$github_token_source_key" <<'PY' import hashlib, json, subprocess, sys -mode, namespace, secret_name, secret_key, source_ref, source_key = sys.argv[1:7] -if mode != "https": - print(json.dumps({"mode": mode, "required": False, "ready": True, "valuesPrinted": False})) - raise SystemExit(0) -proc = subprocess.run(["kubectl", "-n", namespace, "get", "secret", secret_name, "-o", "json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) -exists = proc.returncode == 0 -data = {} -if exists: +mode, namespace, ssh_secret, ssh_private_key, ssh_private_source_ref, ssh_private_source_key, ssh_known_hosts_key, ssh_known_hosts_source_ref, ssh_known_hosts_source_key, token_secret, token_key, token_source_ref, token_source_key = sys.argv[1:14] +def read_secret(name): + proc = subprocess.run(["kubectl", "-n", namespace, "get", "secret", name, "-o", "json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if proc.returncode != 0: + return False, {}, {} try: - data = json.loads(proc.stdout).get("data") or {} + obj = json.loads(proc.stdout) except Exception: - data = {} -encoded = data.get(secret_key) if isinstance(data, dict) else None + obj = {} + return True, obj.get("data") or {}, obj.get("metadata", {}).get("annotations") or {} +def fingerprint(value): + return "sha256:" + hashlib.sha256(value.encode()).hexdigest()[:16] if value else None +if mode == "ssh": + exists, data, annotations = read_secret(ssh_secret) + private_encoded = data.get(ssh_private_key) if isinstance(data, dict) else None + known_hosts_encoded = data.get(ssh_known_hosts_key) if ssh_known_hosts_key and isinstance(data, dict) else None + private_present = isinstance(private_encoded, str) and len(private_encoded) > 0 + known_hosts_expected = bool(ssh_known_hosts_key) + known_hosts_present = isinstance(known_hosts_encoded, str) and len(known_hosts_encoded) > 0 + print(json.dumps({ + "mode": mode, + "required": True, + "ready": exists and private_present and (not known_hosts_expected or known_hosts_present), + "secretName": ssh_secret, + "privateKeySecretKey": ssh_private_key, + "privateKeySourceRef": ssh_private_source_ref, + "privateKeySourceKey": ssh_private_source_key, + "privateKeySecretExists": exists, + "privateKeyPresent": private_present, + "privateKeyBytes": len(private_encoded) if private_present else 0, + "privateKeyFingerprint": annotations.get("unidesk.ai/private-key-fingerprint") or fingerprint(private_encoded), + "knownHostsSecretKey": ssh_known_hosts_key or None, + "knownHostsSourceRef": ssh_known_hosts_source_ref or None, + "knownHostsSourceKey": ssh_known_hosts_source_key or None, + "knownHostsPresent": (known_hosts_present if known_hosts_expected else None), + "knownHostsBytes": (len(known_hosts_encoded) if known_hosts_present else 0) if known_hosts_expected else None, + "knownHostsFingerprint": annotations.get("unidesk.ai/known-hosts-fingerprint") or fingerprint(known_hosts_encoded), + "valuesPrinted": False, + })) + raise SystemExit(0) +if mode != "https": + print(json.dumps({"mode": mode, "required": True, "ready": False, "valuesPrinted": False})) + raise SystemExit(0) +exists, data, _ = read_secret(token_secret) +encoded = data.get(token_key) if isinstance(data, dict) else None present = isinstance(encoded, str) and len(encoded) > 0 -fingerprint = "sha256:" + hashlib.sha256(encoded.encode()).hexdigest()[:16] if present else None -print(json.dumps({"mode": mode, "required": True, "ready": exists and present, "tokenSecretName": secret_name, "tokenSecretKey": secret_key, "tokenSourceRef": source_ref, "tokenSourceKey": source_key, "tokenSecretExists": exists, "tokenKeyPresent": present, "tokenKeyBytes": len(encoded) if present else 0, "tokenFingerprint": fingerprint, "valuesPrinted": False})) +print(json.dumps({"mode": mode, "required": True, "ready": exists and present, "tokenSecretName": token_secret, "tokenSecretKey": token_key, "tokenSourceRef": token_source_ref, "tokenSourceKey": token_source_key, "tokenSecretExists": exists, "tokenKeyPresent": present, "tokenKeyBytes": len(encoded) if present else 0, "tokenFingerprint": fingerprint(encoded), "valuesPrinted": False})) PY ) registry_ready=false diff --git a/scripts/src/hwlab-node/entry.ts b/scripts/src/hwlab-node/entry.ts index 2f38a6e6..3a81dfd9 100644 --- a/scripts/src/hwlab-node/entry.ts +++ b/scripts/src/hwlab-node/entry.ts @@ -401,7 +401,17 @@ export interface NodeRuntimeGitMirrorTargetSpec { } export type NodeRuntimeGitMirrorGithubTransportSpec = - | { mode: "ssh" } + | { + mode: "ssh"; + privateKeySecretKey: string; + privateKeySourceRef: string; + privateKeySourceKey: string; + privateKeySourceEncoding: "plain" | "base64"; + knownHostsSecretKey: string | null; + knownHostsSourceRef: string | null; + knownHostsSourceKey: string | null; + knownHostsSourceEncoding: "plain" | "base64" | null; + } | { mode: "https"; username: string; diff --git a/scripts/src/hwlab-node/render.ts b/scripts/src/hwlab-node/render.ts index 83e37ba5..55abdd62 100644 --- a/scripts/src/hwlab-node/render.ts +++ b/scripts/src/hwlab-node/render.ts @@ -131,7 +131,21 @@ export function nodeRuntimeGitMirrorGithubTransportEnv(mirror: NodeRuntimeGitMir export function nodeRuntimeGitMirrorGithubTransportSummary(mirror: NodeRuntimeGitMirrorTargetSpec): Record { const transport = mirror.githubTransport; - if (transport.mode === "ssh") return { mode: "ssh", secretName: mirror.secretName, valuesPrinted: false }; + if (transport.mode === "ssh") { + return { + mode: "ssh", + secretName: mirror.secretName, + privateKeySecretKey: transport.privateKeySecretKey, + privateKeySourceRef: transport.privateKeySourceRef, + privateKeySourceKey: transport.privateKeySourceKey, + privateKeySourceEncoding: transport.privateKeySourceEncoding, + knownHostsSecretKey: transport.knownHostsSecretKey, + knownHostsSourceRef: transport.knownHostsSourceRef, + knownHostsSourceKey: transport.knownHostsSourceKey, + knownHostsSourceEncoding: transport.knownHostsSourceEncoding, + valuesPrinted: false, + }; + } return { mode: "https", username: transport.username, diff --git a/scripts/src/hwlab-node/status.ts b/scripts/src/hwlab-node/status.ts index a6b27828..ec7cb962 100644 --- a/scripts/src/hwlab-node/status.ts +++ b/scripts/src/hwlab-node/status.ts @@ -67,6 +67,13 @@ export function nodeRuntimeGitMirrorStatus(scoped: ReturnType/dev/null || true); ready=$(kubectl -n \"$1\" get deploy \"$2\" -o 'jsonpath={.status.readyReplicas}' 2>/dev/null || true); [ -n \"$desired\" ] && [ \"$desired\" -gt 0 ] 2>/dev/null && [ \"${ready:-0}\" = \"$desired\" ] && printf true || printf false; }", "exists_res() { kubectl -n \"$1\" get \"$2\" \"$3\" >/dev/null 2>&1 && printf true || printf false; }", "endpoint_ready() { endpoints=$(kubectl -n \"$1\" get endpoints \"$2\" -o 'jsonpath={.subsets[*].addresses[*].ip}' 2>/dev/null || true); [ -n \"$endpoints\" ] && printf true || printf false; }", - "github_transport_json=$(python3 - \"$github_transport_mode\" \"$namespace\" \"$github_token_secret\" \"$github_token_key\" \"$github_token_source_ref\" \"$github_token_source_key\" <<'PY'", + "github_transport_json=$(python3 - \"$github_transport_mode\" \"$namespace\" \"$github_ssh_secret\" \"$github_ssh_private_key\" \"$github_ssh_private_source_ref\" \"$github_ssh_private_source_key\" \"$github_ssh_known_hosts_key\" \"$github_ssh_known_hosts_source_ref\" \"$github_ssh_known_hosts_source_key\" \"$github_token_secret\" \"$github_token_key\" \"$github_token_source_ref\" \"$github_token_source_key\" <<'PY'", "import hashlib, json, subprocess, sys", - "mode, namespace, secret_name, secret_key, source_ref, source_key = sys.argv[1:7]", - "if mode != 'https':", - " print(json.dumps({'mode': mode, 'required': False, 'ready': True, 'valuesPrinted': False}))", - " raise SystemExit(0)", - "proc = subprocess.run(['kubectl', '-n', namespace, 'get', 'secret', secret_name, '-o', 'json'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)", - "exists = proc.returncode == 0", - "data = {}", - "if exists:", + "mode, namespace, ssh_secret, ssh_private_key, ssh_private_source_ref, ssh_private_source_key, ssh_known_hosts_key, ssh_known_hosts_source_ref, ssh_known_hosts_source_key, token_secret, token_key, token_source_ref, token_source_key = sys.argv[1:14]", + "def read_secret(name):", + " proc = subprocess.run(['kubectl', '-n', namespace, 'get', 'secret', name, '-o', 'json'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)", + " if proc.returncode != 0:", + " return False, {}, {}", " try:", - " data = json.loads(proc.stdout).get('data') or {}", + " obj = json.loads(proc.stdout)", " except Exception:", - " data = {}", - "encoded = data.get(secret_key) if isinstance(data, dict) else None", + " obj = {}", + " return True, obj.get('data') or {}, obj.get('metadata', {}).get('annotations') or {}", + "def fingerprint(value):", + " return 'sha256:' + hashlib.sha256(value.encode()).hexdigest()[:16] if value else None", + "if mode == 'ssh':", + " exists, data, annotations = read_secret(ssh_secret)", + " private_encoded = data.get(ssh_private_key) if isinstance(data, dict) else None", + " known_hosts_encoded = data.get(ssh_known_hosts_key) if ssh_known_hosts_key and isinstance(data, dict) else None", + " private_present = isinstance(private_encoded, str) and len(private_encoded) > 0", + " known_hosts_expected = bool(ssh_known_hosts_key)", + " known_hosts_present = isinstance(known_hosts_encoded, str) and len(known_hosts_encoded) > 0", + " print(json.dumps({'mode': mode, 'required': True, 'ready': exists and private_present and (not known_hosts_expected or known_hosts_present), 'secretName': ssh_secret, 'privateKeySecretKey': ssh_private_key, 'privateKeySourceRef': ssh_private_source_ref, 'privateKeySourceKey': ssh_private_source_key, 'privateKeySecretExists': exists, 'privateKeyPresent': private_present, 'privateKeyBytes': len(private_encoded) if private_present else 0, 'privateKeyFingerprint': annotations.get('unidesk.ai/private-key-fingerprint') or fingerprint(private_encoded), 'knownHostsSecretKey': ssh_known_hosts_key or None, 'knownHostsSourceRef': ssh_known_hosts_source_ref or None, 'knownHostsSourceKey': ssh_known_hosts_source_key or None, 'knownHostsPresent': (known_hosts_present if known_hosts_expected else None), 'knownHostsBytes': (len(known_hosts_encoded) if known_hosts_present else 0) if known_hosts_expected else None, 'knownHostsFingerprint': annotations.get('unidesk.ai/known-hosts-fingerprint') or fingerprint(known_hosts_encoded), 'valuesPrinted': False}))", + " raise SystemExit(0)", + "if mode != 'https':", + " print(json.dumps({'mode': mode, 'required': True, 'ready': False, 'valuesPrinted': False}))", + " raise SystemExit(0)", + "exists, data, _ = read_secret(token_secret)", + "encoded = data.get(token_key) if isinstance(data, dict) else None", "present = isinstance(encoded, str) and len(encoded) > 0", - "fingerprint = 'sha256:' + hashlib.sha256(encoded.encode()).hexdigest()[:16] if present else None", - "print(json.dumps({'mode': mode, 'required': True, 'ready': exists and present, 'tokenSecretName': secret_name, 'tokenSecretKey': secret_key, 'tokenSourceRef': source_ref, 'tokenSourceKey': source_key, 'tokenSecretExists': exists, 'tokenKeyPresent': present, 'tokenKeyBytes': len(encoded) if present else 0, 'tokenFingerprint': fingerprint, 'valuesPrinted': False}))", + "print(json.dumps({'mode': mode, 'required': True, 'ready': exists and present, 'tokenSecretName': token_secret, 'tokenSecretKey': token_key, 'tokenSourceRef': token_source_ref, 'tokenSourceKey': token_source_key, 'tokenSecretExists': exists, 'tokenKeyPresent': present, 'tokenKeyBytes': len(encoded) if present else 0, 'tokenFingerprint': fingerprint(encoded), 'valuesPrinted': False}))", "PY", ")", "summary_json=$(kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc '/etc/git-mirror/status.sh' 2>/tmp/hwlab-node-gitmirror-status.err || true)", diff --git a/scripts/src/hwlab-node/web-probe.ts b/scripts/src/hwlab-node/web-probe.ts index 5b1b2f92..debdf17b 100644 --- a/scripts/src/hwlab-node/web-probe.ts +++ b/scripts/src/hwlab-node/web-probe.ts @@ -1184,7 +1184,27 @@ export function nodeRuntimeGitMirrorTarget(spec: HwlabRuntimeLaneSpec): NodeRunt export function nodeRuntimeGitMirrorGithubTransportSpec(raw: Record, path: string): NodeRuntimeGitMirrorGithubTransportSpec { const mode = stringValue(raw.mode, `${path}.mode`); - if (mode === "ssh") return { mode }; + if (mode === "ssh") { + const knownHostsSecretKey = optionalStringValue(raw.knownHostsSecretKey, `${path}.knownHostsSecretKey`); + const knownHostsSourceRef = optionalStringValue(raw.knownHostsSourceRef, `${path}.knownHostsSourceRef`); + const knownHostsSourceKey = optionalStringValue(raw.knownHostsSourceKey, `${path}.knownHostsSourceKey`); + const knownHostsSourceEncoding = raw.knownHostsSourceEncoding === undefined ? null : gitMirrorSecretSourceEncoding(raw.knownHostsSourceEncoding, `${path}.knownHostsSourceEncoding`); + const knownHostsValues = [knownHostsSecretKey, knownHostsSourceRef, knownHostsSourceKey, knownHostsSourceEncoding]; + if (knownHostsValues.some((value) => value !== null) && knownHostsValues.some((value) => value === null)) { + throw new Error(`${path}.knownHostsSecretKey/sourceRef/sourceKey/sourceEncoding must be declared together`); + } + return { + mode, + privateKeySecretKey: stringValue(raw.privateKeySecretKey, `${path}.privateKeySecretKey`), + privateKeySourceRef: stringValue(raw.privateKeySourceRef, `${path}.privateKeySourceRef`), + privateKeySourceKey: stringValue(raw.privateKeySourceKey, `${path}.privateKeySourceKey`), + privateKeySourceEncoding: gitMirrorSecretSourceEncoding(raw.privateKeySourceEncoding, `${path}.privateKeySourceEncoding`), + knownHostsSecretKey, + knownHostsSourceRef, + knownHostsSourceKey, + knownHostsSourceEncoding, + }; + } if (mode !== "https") throw new Error(`${path}.mode must be ssh or https`); return { mode, @@ -1196,6 +1216,12 @@ export function nodeRuntimeGitMirrorGithubTransportSpec(raw: Record, path: string): NodeRuntimeGitMirrorEgressProxySpec { const mode = stringValue(raw.mode, `${path}.mode`); if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be k8s-service-cluster-ip`);