diff --git a/config/platform-infra/secret-plane.yaml b/config/platform-infra/secret-plane.yaml new file mode 100644 index 00000000..cab4042b --- /dev/null +++ b/config/platform-infra/secret-plane.yaml @@ -0,0 +1,65 @@ +version: 1 +kind: platform-infra-secret-plane +metadata: + id: hwlab-secret-plane + owner: HWLAB + spec: pikasTech/HWLAB#2233 + relatedIssues: + - 2233 +defaults: + targetId: D601 +targets: + - id: D601 + route: D601:k3s + namespace: platform-infra + role: active + enabled: true + createNamespace: true +eso: + enabled: true + version: v2.7.0 + manifestUrl: https://github.com/external-secrets/external-secrets/releases/download/v2.7.0/external-secrets.yaml + releaseName: external-secrets + controllerDeploymentName: external-secrets + webhookDeploymentName: external-secrets-webhook + certControllerDeploymentName: external-secrets-cert-controller + crds: + - secretstores.external-secrets.io + - externalsecrets.external-secrets.io + - clustersecretstores.external-secrets.io +vault: + mode: dev-kv-v2 + deploymentName: hwlab-secret-plane-vault + serviceName: hwlab-secret-plane-vault + tokenSecretName: hwlab-secret-plane-vault-token + tokenSecretKey: token + image: + repository: hashicorp/vault + tag: 1.20.3 + pullPolicy: IfNotPresent + port: 8200 + bootstrap: + tokenMode: generated-if-missing + tokenLengthBytes: 32 +syncProbe: + secretStoreName: hwlab-secret-plane-vault + externalSecretName: hwlab-secret-plane-poc + targetSecretName: hwlab-secret-plane-poc-sync + refreshInterval: 15s + vaultMountPath: secret + remotePath: hwlab-secret-plane/poc + remoteProperty: password + expectedFingerprint: sha256:7b47b343642e442d94ae889778113b0137eb8db255d9c03cb42f2582adfa2f2f + testValueSource: + mode: repo-poc-static + value: hwlab-secret-plane-poc-d601 + consumer: + deploymentName: hwlab-secret-plane-consumer + envName: POC_PASSWORD + image: + repository: busybox + tag: 1.36.1 + pullPolicy: IfNotPresent +validation: + timeoutSeconds: 45 + pollSeconds: 3 diff --git a/scripts/src/platform-infra-secret-plane.ts b/scripts/src/platform-infra-secret-plane.ts new file mode 100644 index 00000000..a8095029 --- /dev/null +++ b/scripts/src/platform-infra-secret-plane.ts @@ -0,0 +1,1252 @@ +import { Buffer } from "node:buffer"; +import { createHash } from "node:crypto"; +import type { UniDeskConfig } from "./config"; +import { rootPath } from "./config"; +import type { RenderedCliResult } from "./output"; +import { + capture, + compactCapture, + createYamlFieldReader, + parseJsonOutput, + readYamlRecord, + shQuote, +} from "./platform-infra-ops-library"; + +const configFile = rootPath("config", "platform-infra", "secret-plane.yaml"); +const configLabel = "config/platform-infra/secret-plane.yaml"; +const fieldManager = "unidesk-platform-infra-secret-plane"; +const serviceId = "secret-plane"; +const y = createYamlFieldReader(configLabel); + +type ImagePullPolicy = "Always" | "IfNotPresent" | "Never"; + +interface ImageSpec { + repository: string; + tag: string; + pullPolicy: ImagePullPolicy; +} + +interface SecretPlaneTarget { + id: string; + route: string; + namespace: string; + role: "active" | "standby"; + enabled: boolean; + createNamespace: boolean; +} + +interface EsoConfig { + enabled: boolean; + version: string; + manifestUrl: string; + releaseName: string; + controllerDeploymentName: string; + webhookDeploymentName: string; + certControllerDeploymentName: string; + crds: string[]; +} + +interface VaultConfig { + mode: "dev-kv-v2"; + deploymentName: string; + serviceName: string; + tokenSecretName: string; + tokenSecretKey: string; + image: ImageSpec; + port: number; + bootstrap: { + tokenMode: "generated-if-missing"; + tokenLengthBytes: number; + }; +} + +interface SyncProbeConfig { + secretStoreName: string; + externalSecretName: string; + targetSecretName: string; + refreshInterval: string; + vaultMountPath: string; + remotePath: string; + remoteProperty: string; + expectedFingerprint: string; + testValueSource: { + mode: "repo-poc-static"; + value: string; + }; + consumer: { + deploymentName: string; + envName: string; + image: ImageSpec; + }; +} + +interface SecretPlaneConfig { + version: number; + kind: "platform-infra-secret-plane"; + metadata: { + id: string; + owner: string; + spec: string; + relatedIssues: number[]; + }; + defaults: { targetId: string }; + targets: SecretPlaneTarget[]; + eso: EsoConfig; + vault: VaultConfig; + syncProbe: SyncProbeConfig; + validation: { + timeoutSeconds: number; + pollSeconds: number; + }; +} + +interface CommonOptions { + targetId: string | null; + full: boolean; + raw: boolean; +} + +interface ApplyOptions extends CommonOptions { + dryRun: boolean; + confirm: boolean; +} + +export async function runSecretPlaneCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { + const [action] = args; + if (action === undefined || action === "plan") { + const options = parseCommonOptions(args.slice(action === "plan" ? 1 : 0)); + const result = plan(options); + return options.full || options.raw ? result : renderPlan(result); + } + if (action === "apply") return await apply(config, parseApplyOptions(args.slice(1))); + if (action === "status") { + const options = parseCommonOptions(args.slice(1)); + const result = await status(config, options); + return options.full || options.raw ? result : renderStatus(result); + } + if (action === "validate") return await validate(config, parseCommonOptions(args.slice(1))); + return { + ok: false, + error: "unsupported-platform-infra-secret-plane-command", + args, + usage: [ + "bun scripts/cli.ts platform-infra secret-plane plan --target D601", + "bun scripts/cli.ts platform-infra secret-plane apply --target D601 --dry-run", + "bun scripts/cli.ts platform-infra secret-plane apply --target D601 --confirm", + "bun scripts/cli.ts platform-infra secret-plane status --target D601", + "bun scripts/cli.ts platform-infra secret-plane validate --target D601", + ], + }; +} + +function readSecretPlaneConfig(): SecretPlaneConfig { + const root = readYamlRecord>(configFile, "platform-infra-secret-plane"); + const version = y.integerField(root, "version", ""); + if (version !== 1) throw new Error(`${configLabel}.version must be 1`); + const metadata = y.objectField(root, "metadata", ""); + const defaults = y.objectField(root, "defaults", ""); + const eso = parseEso(y.objectField(root, "eso", "")); + const vault = parseVault(y.objectField(root, "vault", "")); + const syncProbe = parseSyncProbe(y.objectField(root, "syncProbe", "")); + const validation = y.objectField(root, "validation", ""); + const parsed: SecretPlaneConfig = { + version, + kind: "platform-infra-secret-plane", + metadata: { + id: y.stringField(metadata, "id", "metadata"), + owner: y.stringField(metadata, "owner", "metadata"), + spec: y.stringField(metadata, "spec", "metadata"), + relatedIssues: y.numberArrayField(metadata, "relatedIssues", "metadata"), + }, + defaults: { targetId: y.stringField(defaults, "targetId", "defaults") }, + targets: y.arrayOfRecords(root.targets, "targets").map(parseTarget), + eso, + vault, + syncProbe, + validation: { + timeoutSeconds: y.integerField(validation, "timeoutSeconds", "validation"), + pollSeconds: y.integerField(validation, "pollSeconds", "validation"), + }, + }; + if (parsed.targets.length === 0) throw new Error(`${configLabel}.targets must not be empty`); + resolveTarget(parsed, parsed.defaults.targetId); + if (!/^sha256:[a-f0-9]{64}$/u.test(parsed.syncProbe.expectedFingerprint)) throw new Error(`${configLabel}.syncProbe.expectedFingerprint must be a sha256 fingerprint`); + const actualFingerprint = sha256Fingerprint(parsed.syncProbe.testValueSource.value); + if (actualFingerprint !== parsed.syncProbe.expectedFingerprint) throw new Error(`${configLabel}.syncProbe.expectedFingerprint does not match syncProbe.testValueSource.value`); + if (parsed.validation.timeoutSeconds < 5 || parsed.validation.timeoutSeconds > 55) throw new Error(`${configLabel}.validation.timeoutSeconds must be between 5 and 55`); + if (parsed.validation.pollSeconds < 1 || parsed.validation.pollSeconds > parsed.validation.timeoutSeconds) throw new Error(`${configLabel}.validation.pollSeconds must be between 1 and timeoutSeconds`); + return parsed; +} + +function parseTarget(record: Record, index: number): SecretPlaneTarget { + const path = `targets[${index}]`; + return { + id: y.stringField(record, "id", path), + route: y.stringField(record, "route", path), + namespace: y.kubernetesNameField(record, "namespace", path), + role: y.enumField(record, "role", path, ["active", "standby"] as const), + enabled: y.booleanField(record, "enabled", path), + createNamespace: y.booleanField(record, "createNamespace", path), + }; +} + +function parseEso(record: Record): EsoConfig { + return { + enabled: y.booleanField(record, "enabled", "eso"), + version: y.stringField(record, "version", "eso"), + manifestUrl: y.httpsUrlField(record, "manifestUrl", "eso"), + releaseName: y.stringField(record, "releaseName", "eso"), + controllerDeploymentName: y.kubernetesNameField(record, "controllerDeploymentName", "eso"), + webhookDeploymentName: y.kubernetesNameField(record, "webhookDeploymentName", "eso"), + certControllerDeploymentName: y.kubernetesNameField(record, "certControllerDeploymentName", "eso"), + crds: y.stringArrayField(record, "crds", "eso"), + }; +} + +function parseVault(record: Record): VaultConfig { + const bootstrap = y.objectField(record, "bootstrap", "vault"); + return { + mode: y.enumField(record, "mode", "vault", ["dev-kv-v2"] as const), + deploymentName: y.kubernetesNameField(record, "deploymentName", "vault"), + serviceName: y.kubernetesNameField(record, "serviceName", "vault"), + tokenSecretName: y.kubernetesNameField(record, "tokenSecretName", "vault"), + tokenSecretKey: y.stringField(record, "tokenSecretKey", "vault"), + image: parseImage(y.objectField(record, "image", "vault"), "vault.image"), + port: y.portField(record, "port", "vault"), + bootstrap: { + tokenMode: y.enumField(bootstrap, "tokenMode", "vault.bootstrap", ["generated-if-missing"] as const), + tokenLengthBytes: y.integerField(bootstrap, "tokenLengthBytes", "vault.bootstrap"), + }, + }; +} + +function parseSyncProbe(record: Record): SyncProbeConfig { + const testValueSource = y.objectField(record, "testValueSource", "syncProbe"); + const consumer = y.objectField(record, "consumer", "syncProbe"); + const remoteProperty = y.stringField(record, "remoteProperty", "syncProbe"); + if (!/^[A-Za-z0-9._-]+$/u.test(remoteProperty)) throw new Error(`${configLabel}.syncProbe.remoteProperty must be a simple key`); + return { + secretStoreName: y.kubernetesNameField(record, "secretStoreName", "syncProbe"), + externalSecretName: y.kubernetesNameField(record, "externalSecretName", "syncProbe"), + targetSecretName: y.kubernetesNameField(record, "targetSecretName", "syncProbe"), + refreshInterval: y.stringField(record, "refreshInterval", "syncProbe"), + vaultMountPath: y.stringField(record, "vaultMountPath", "syncProbe"), + remotePath: y.stringField(record, "remotePath", "syncProbe"), + remoteProperty, + expectedFingerprint: y.stringField(record, "expectedFingerprint", "syncProbe"), + testValueSource: { + mode: y.enumField(testValueSource, "mode", "syncProbe.testValueSource", ["repo-poc-static"] as const), + value: y.stringField(testValueSource, "value", "syncProbe.testValueSource"), + }, + consumer: { + deploymentName: y.kubernetesNameField(consumer, "deploymentName", "syncProbe.consumer"), + envName: y.envKeyField(consumer, "envName", "syncProbe.consumer"), + image: parseImage(y.objectField(consumer, "image", "syncProbe.consumer"), "syncProbe.consumer.image"), + }, + }; +} + +function parseImage(record: Record, path: string): ImageSpec { + const image = { + repository: y.stringField(record, "repository", path), + tag: y.stringField(record, "tag", path), + pullPolicy: y.enumField(record, "pullPolicy", path, ["Always", "IfNotPresent", "Never"] as const), + }; + if (!/^[A-Za-z0-9._/:@-]+$/u.test(`${image.repository}:${image.tag}`)) throw new Error(`${configLabel}.${path} must render a valid image reference`); + return image; +} + +function resolveTarget(secretPlane: SecretPlaneConfig, targetId: string | null): SecretPlaneTarget { + const resolved = targetId ?? secretPlane.defaults.targetId; + const target = secretPlane.targets.find((item) => item.id.toLowerCase() === resolved.toLowerCase()); + if (target === undefined) throw new Error(`unknown secret-plane target ${resolved}; known targets: ${secretPlane.targets.map((item) => item.id).join(", ")}`); + if (!target.enabled) throw new Error(`secret-plane target ${target.id} is disabled in ${configLabel}`); + return target; +} + +function plan(options: CommonOptions): Record { + const secretPlane = readSecretPlaneConfig(); + const target = resolveTarget(secretPlane, options.targetId); + const yaml = renderWorkloadManifest(secretPlane, target); + const policy = policyChecks(secretPlane, target, yaml); + return { + ok: policy.every((check) => check.ok), + action: "platform-infra-secret-plane-plan", + mutation: false, + config: configSummary(secretPlane, target), + renderPlan: { + target: targetSummary(target), + objects: manifestObjectSummary(yaml), + eso: { + enabled: secretPlane.eso.enabled, + version: secretPlane.eso.version, + manifestUrl: secretPlane.eso.manifestUrl, + namespaceRewrite: target.namespace, + crds: secretPlane.eso.crds, + deployments: [ + secretPlane.eso.controllerDeploymentName, + secretPlane.eso.webhookDeploymentName, + secretPlane.eso.certControllerDeploymentName, + ], + }, + vault: vaultSummary(secretPlane, target), + syncProbe: syncProbeSummary(secretPlane), + }, + policy, + next: { + dryRun: `bun scripts/cli.ts platform-infra secret-plane apply --target ${target.id} --dry-run`, + apply: `bun scripts/cli.ts platform-infra secret-plane apply --target ${target.id} --confirm`, + status: `bun scripts/cli.ts platform-infra secret-plane status --target ${target.id}`, + validate: `bun scripts/cli.ts platform-infra secret-plane validate --target ${target.id}`, + }, + }; +} + +async function apply(config: UniDeskConfig, options: ApplyOptions): Promise> { + const secretPlane = readSecretPlaneConfig(); + const target = resolveTarget(secretPlane, options.targetId); + const workloadYaml = renderWorkloadManifest(secretPlane, target); + const policy = policyChecks(secretPlane, target, workloadYaml); + if (!policy.every((check) => check.ok)) { + return { ok: false, action: "platform-infra-secret-plane-apply", mode: "policy-blocked", mutation: false, policy }; + } + const esoYaml = secretPlane.eso.enabled ? await fetchEsoManifest(secretPlane, target) : ""; + const script = options.dryRun + ? dryRunScript(secretPlane, target, esoYaml, workloadYaml) + : applyScript(secretPlane, target, esoYaml, workloadYaml); + const result = await capture(config, target.route, ["sh"], script); + const parsed = parseJsonOutput(result.stdout); + return { + ok: result.exitCode === 0 && parsed?.ok === true, + action: "platform-infra-secret-plane-apply", + mode: options.dryRun ? "dry-run" : "confirmed", + mutation: !options.dryRun, + target: targetSummary(target), + config: configSummary(secretPlane, target), + policy, + remote: parsed ?? compactCapture(result, { full: true }), + next: { + status: `bun scripts/cli.ts platform-infra secret-plane status --target ${target.id}`, + validate: `bun scripts/cli.ts platform-infra secret-plane validate --target ${target.id}`, + }, + }; +} + +async function status(config: UniDeskConfig, options: CommonOptions): Promise> { + const secretPlane = readSecretPlaneConfig(); + const target = resolveTarget(secretPlane, options.targetId); + const result = await capture(config, target.route, ["sh"], statusScript(secretPlane, target, options.full)); + const parsed = parseJsonOutput(result.stdout); + const summary = parsed === null ? null : compactStatus(parsed, options.full); + return { + ok: result.exitCode === 0 && parsed?.ready === true, + action: "platform-infra-secret-plane-status", + mutation: false, + target: targetSummary(target), + config: configSummary(secretPlane, target), + summary, + remote: options.raw ? parsed : summary ?? compactCapture(result, { full: true }), + next: { + plan: `bun scripts/cli.ts platform-infra secret-plane plan --target ${target.id}`, + apply: `bun scripts/cli.ts platform-infra secret-plane apply --target ${target.id} --confirm`, + validate: `bun scripts/cli.ts platform-infra secret-plane validate --target ${target.id}`, + }, + }; +} + +async function validate(config: UniDeskConfig, options: CommonOptions): Promise> { + const secretPlane = readSecretPlaneConfig(); + const target = resolveTarget(secretPlane, options.targetId); + const result = await capture(config, target.route, ["sh"], validateScript(secretPlane, target, options.full)); + const parsed = parseJsonOutput(result.stdout); + return { + ok: result.exitCode === 0 && parsed?.ok === true, + action: "platform-infra-secret-plane-validate", + mutation: true, + target: targetSummary(target), + config: configSummary(secretPlane, target), + validation: parsed ?? null, + remote: options.raw ? parsed : compactCapture(result, { full: options.full || result.exitCode !== 0 }), + }; +} + +async function fetchEsoManifest(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget): Promise { + const response = await fetch(secretPlane.eso.manifestUrl, { redirect: "follow" }); + if (!response.ok) throw new Error(`failed to fetch ESO manifest ${secretPlane.eso.manifestUrl}: HTTP ${response.status}`); + const text = await response.text(); + if (!text.includes(secretPlane.eso.version) || !text.includes("kind: CustomResourceDefinition")) { + throw new Error(`ESO manifest ${secretPlane.eso.manifestUrl} does not look like ${secretPlane.eso.version} static install YAML`); + } + return text.replace(/^(\s*namespace:\s*)default\s*$/gmu, `$1${target.namespace}`); +} + +function renderWorkloadManifest(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget): string { + const vault = secretPlane.vault; + const probe = secretPlane.syncProbe; + const vaultImage = imageRef(vault.image); + const consumerImage = imageRef(probe.consumer.image); + return `apiVersion: v1 +kind: Namespace +metadata: + name: ${target.namespace} + labels: + app.kubernetes.io/name: platform-infra + app.kubernetes.io/managed-by: unidesk + unidesk.ai/runtime-node: ${target.id} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all + namespace: ${target.namespace} + labels: + app.kubernetes.io/name: platform-infra + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + ingress: + - {} + egress: + - {} +--- +apiVersion: v1 +kind: Service +metadata: + name: ${vault.serviceName} + namespace: ${target.namespace} + labels: + app.kubernetes.io/name: ${vault.deploymentName} + app.kubernetes.io/component: vault-backend + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk + unidesk.ai/runtime-node: ${target.id} +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: ${vault.deploymentName} + app.kubernetes.io/component: vault-backend + ports: + - name: http + port: ${vault.port} + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${vault.deploymentName} + namespace: ${target.namespace} + labels: + app.kubernetes.io/name: ${vault.deploymentName} + app.kubernetes.io/component: vault-backend + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk + unidesk.ai/runtime-node: ${target.id} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ${vault.deploymentName} + app.kubernetes.io/component: vault-backend + template: + metadata: + labels: + app.kubernetes.io/name: ${vault.deploymentName} + app.kubernetes.io/component: vault-backend + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk + annotations: + unidesk.ai/secret-plane-mode: ${vault.mode} + unidesk.ai/bootstrap-token-mode: ${vault.bootstrap.tokenMode} + spec: + containers: + - name: vault + image: ${vaultImage} + imagePullPolicy: ${vault.image.pullPolicy} + args: + - server + - -dev + env: + - name: VAULT_DEV_ROOT_TOKEN_ID + valueFrom: + secretKeyRef: + name: ${vault.tokenSecretName} + key: ${vault.tokenSecretKey} + - name: VAULT_DEV_LISTEN_ADDRESS + value: "0.0.0.0:${vault.port}" + - name: VAULT_ADDR + value: "http://127.0.0.1:${vault.port}" + ports: + - name: http + containerPort: ${vault.port} + readinessProbe: + httpGet: + path: /v1/sys/health + port: http + initialDelaySeconds: 3 + periodSeconds: 5 +--- +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: ${probe.secretStoreName} + namespace: ${target.namespace} + labels: + app.kubernetes.io/name: ${probe.secretStoreName} + app.kubernetes.io/component: secretstore + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk +spec: + provider: + vault: + server: "http://${vault.serviceName}.${target.namespace}.svc.cluster.local:${vault.port}" + path: ${probe.vaultMountPath} + version: v2 + auth: + tokenSecretRef: + name: ${vault.tokenSecretName} + key: ${vault.tokenSecretKey} +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: ${probe.externalSecretName} + namespace: ${target.namespace} + labels: + app.kubernetes.io/name: ${probe.externalSecretName} + app.kubernetes.io/component: external-secret + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk +spec: + refreshInterval: ${probe.refreshInterval} + secretStoreRef: + name: ${probe.secretStoreName} + kind: SecretStore + target: + name: ${probe.targetSecretName} + creationPolicy: Owner + data: + - secretKey: ${probe.remoteProperty} + remoteRef: + key: ${probe.remotePath} + property: ${probe.remoteProperty} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${probe.consumer.deploymentName} + namespace: ${target.namespace} + labels: + app.kubernetes.io/name: ${probe.consumer.deploymentName} + app.kubernetes.io/component: secret-consumer + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk + unidesk.ai/runtime-node: ${target.id} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ${probe.consumer.deploymentName} + app.kubernetes.io/component: secret-consumer + template: + metadata: + labels: + app.kubernetes.io/name: ${probe.consumer.deploymentName} + app.kubernetes.io/component: secret-consumer + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk + spec: + containers: + - name: consumer + image: ${consumerImage} + imagePullPolicy: ${probe.consumer.image.pullPolicy} + command: + - sh + - -c + - while true; do sleep 3600; done + env: + - name: ${probe.consumer.envName} + valueFrom: + secretKeyRef: + name: ${probe.targetSecretName} + key: ${probe.remoteProperty} +`; +} + +function dryRunScript(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget, esoYaml: string, workloadYaml: string): string { + const esoEncoded = Buffer.from(esoYaml, "utf8").toString("base64"); + const workloadEncoded = Buffer.from(workloadYaml, "utf8").toString("base64"); + return ` +set -u +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +eso="$tmp/external-secrets.yaml" +workload="$tmp/secret-plane.yaml" +core="$tmp/secret-plane-core.yaml" +custom="$tmp/secret-plane-custom.yaml" +printf '%s' '${esoEncoded}' | base64 -d >"$eso" +printf '%s' '${workloadEncoded}' | base64 -d >"$workload" +python3 - "$workload" "$core" "$custom" <<'PY' +import re, sys +source, core_path, custom_path = sys.argv[1:4] +text = open(source, encoding="utf-8").read() +core_docs = [] +custom_docs = [] +for doc in re.split(r"^---\\s*$", text, flags=re.M): + if not doc.strip(): + continue + match = re.search(r"^\\s*kind:\\s*([A-Za-z0-9]+)\\s*$", doc, flags=re.M) + kind = match.group(1) if match else "" + if kind in {"SecretStore", "ExternalSecret"}: + custom_docs.append(doc.strip() + "\\n") + else: + core_docs.append(doc.strip() + "\\n") +open(core_path, "w", encoding="utf-8").write("---\\n".join(core_docs)) +open(custom_path, "w", encoding="utf-8").write("---\\n".join(custom_docs)) +PY +client_out="$tmp/client.out" +client_err="$tmp/client.err" +server_out="$tmp/server.out" +server_err="$tmp/server.err" +custom_out="$tmp/custom.out" +custom_err="$tmp/custom.err" +kubectl apply --dry-run=client --validate=false -f "$eso" -f "$core" >"$client_out" 2>"$client_err" +client_rc=$? +if kubectl get crd ${secretPlane.eso.crds.map((name) => name).join(" ")} >/dev/null 2>&1; then + kubectl apply --dry-run=client --validate=false -f "$custom" >"$custom_out" 2>"$custom_err" + custom_rc=$? + custom_disposition=executed +else + custom_rc=0 + custom_disposition=skipped-crds-missing + printf '%s\\n' 'SecretStore/ExternalSecret dry-run skipped because ESO CRDs are not installed yet; real apply installs CRDs first.' >"$custom_out" + : >"$custom_err" +fi +if kubectl get namespace ${target.namespace} >/dev/null 2>&1; then + kubectl apply --server-side --dry-run=server --validate=false --field-manager=${fieldManager} -f "$eso" -f "$core" >"$server_out" 2>"$server_err" + server_rc=$? + server_disposition=executed +else + server_rc=0 + server_disposition=skipped-namespace-missing + printf '%s\\n' 'server dry-run skipped because namespace does not exist yet' >"$server_out" + : >"$server_err" +fi +python3 - "$client_rc" "$custom_rc" "$server_rc" "$custom_disposition" "$server_disposition" "$client_out" "$client_err" "$custom_out" "$custom_err" "$server_out" "$server_err" <<'PY' +import json, os, sys +client_rc, custom_rc, server_rc = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]) +def text(path, limit=1200): + try: + return open(path, encoding="utf-8", errors="replace").read()[-limit:] + except FileNotFoundError: + return "" +payload = { + "ok": client_rc == 0 and custom_rc == 0 and server_rc == 0, + "target": "${target.id}", + "route": "${target.route}", + "namespace": "${target.namespace}", + "mode": "dry-run", + "esoManifestBytes": ${Buffer.byteLength(esoYaml, "utf8")}, + "workloadManifestBytes": ${Buffer.byteLength(workloadYaml, "utf8")}, + "clientDryRun": {"exitCode": client_rc, "stdoutTail": text(sys.argv[6]), "stderrTail": text(sys.argv[7])}, + "customResourceDryRun": {"exitCode": custom_rc, "disposition": sys.argv[4], "stdoutTail": text(sys.argv[8]), "stderrTail": text(sys.argv[9])}, + "serverDryRun": {"exitCode": server_rc, "disposition": sys.argv[5], "stdoutTail": text(sys.argv[10]), "stderrTail": text(sys.argv[11])}, + "valuesPrinted": False, +} +print(json.dumps(payload, ensure_ascii=False, indent=2)) +sys.exit(0 if payload["ok"] else 1) +PY +`; +} + +function applyScript(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget, esoYaml: string, workloadYaml: string): string { + const esoEncoded = Buffer.from(esoYaml, "utf8").toString("base64"); + const workloadEncoded = Buffer.from(workloadYaml, "utf8").toString("base64"); + const crdArgs = secretPlane.eso.crds.map((name) => `crd/${name}`).join(" "); + return ` +set -u +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +eso="$tmp/external-secrets.yaml" +workload="$tmp/secret-plane.yaml" +printf '%s' '${esoEncoded}' | base64 -d >"$eso" +printf '%s' '${workloadEncoded}' | base64 -d >"$workload" +ns_out="$tmp/ns.out"; ns_err="$tmp/ns.err" +token_out="$tmp/token.out"; token_err="$tmp/token.err" +eso_out="$tmp/eso.out"; eso_err="$tmp/eso.err" +crd_out="$tmp/crd.out"; crd_err="$tmp/crd.err" +workload_out="$tmp/workload.out"; workload_err="$tmp/workload.err" +kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$ns_out" 2>"$ns_err" +ns_rc=$? +token_action=not-run +token_rc=1 +if [ "$ns_rc" -eq 0 ]; then + if kubectl -n ${target.namespace} get secret ${secretPlane.vault.tokenSecretName} >/dev/null 2>&1; then + token_action=kept-existing + token_rc=0 + : >"$token_out" + : >"$token_err" + else + token_action=generated-if-missing + token_file="$tmp/vault-token" + (head -c ${secretPlane.vault.bootstrap.tokenLengthBytes} /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c $(( ${secretPlane.vault.bootstrap.tokenLengthBytes} * 2 ))) >"$token_file" 2>"$token_err" || true + if [ ! -s "$token_file" ]; then + printf '%s' "unidesk-$(${dateCommand()})-$$" >"$token_file" + fi + kubectl -n ${target.namespace} create secret generic ${secretPlane.vault.tokenSecretName} --from-file=${secretPlane.vault.tokenSecretKey}="$token_file" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$token_out" 2>>"$token_err" + token_rc=$? + fi +else + printf '%s\\n' 'namespace apply failed; skipped token secret' >"$token_err" +fi +if [ "$ns_rc" -eq 0 ] && [ "$token_rc" -eq 0 ]; then + kubectl apply --server-side --force-conflicts --validate=false --field-manager=${fieldManager} -f "$eso" >"$eso_out" 2>"$eso_err" + eso_rc=$? +else + eso_rc=1 + printf '%s\\n' 'skipped because namespace or token secret failed' >"$eso_err" +fi +if [ "$eso_rc" -eq 0 ]; then + kubectl wait --for=condition=Established --timeout=25s ${crdArgs} >"$crd_out" 2>"$crd_err" + crd_rc=$? +else + crd_rc=1 + printf '%s\\n' 'skipped because ESO manifest apply failed' >"$crd_err" +fi +if [ "$crd_rc" -eq 0 ]; then + kubectl apply --server-side --force-conflicts --validate=false --field-manager=${fieldManager} -f "$workload" >"$workload_out" 2>"$workload_err" + workload_rc=$? +else + workload_rc=1 + printf '%s\\n' 'skipped because ESO CRDs are not established' >"$workload_err" +fi +python3 - "$ns_rc" "$token_rc" "$eso_rc" "$crd_rc" "$workload_rc" "$token_action" "$ns_out" "$ns_err" "$token_out" "$token_err" "$eso_out" "$eso_err" "$crd_out" "$crd_err" "$workload_out" "$workload_err" <<'PY' +import json, sys +def text(path, limit=1200): + try: + return open(path, encoding="utf-8", errors="replace").read()[-limit:] + except FileNotFoundError: + return "" +ns_rc, token_rc, eso_rc, crd_rc, workload_rc = [int(value) for value in sys.argv[1:6]] +paths = sys.argv[7:] +payload = { + "ok": ns_rc == 0 and token_rc == 0 and eso_rc == 0 and crd_rc == 0 and workload_rc == 0, + "target": "${target.id}", + "route": "${target.route}", + "namespace": "${target.namespace}", + "mode": "confirmed", + "secretMaterial": { + "name": "${secretPlane.vault.tokenSecretName}", + "key": "${secretPlane.vault.tokenSecretKey}", + "action": sys.argv[6], + "valuesPrinted": False, + }, + "steps": { + "namespace": {"exitCode": ns_rc, "stdoutTail": text(paths[0]), "stderrTail": text(paths[1])}, + "tokenSecret": {"exitCode": token_rc, "stdoutTail": text(paths[2]), "stderrTail": text(paths[3]), "valuesPrinted": False}, + "externalSecretsOperator": {"exitCode": eso_rc, "stdoutTail": text(paths[4]), "stderrTail": text(paths[5])}, + "crdsEstablished": {"exitCode": crd_rc, "stdoutTail": text(paths[6]), "stderrTail": text(paths[7])}, + "secretPlaneWorkload": {"exitCode": workload_rc, "stdoutTail": text(paths[8]), "stderrTail": text(paths[9])}, + }, + "next": { + "status": "bun scripts/cli.ts platform-infra secret-plane status --target ${target.id}", + "validate": "bun scripts/cli.ts platform-infra secret-plane validate --target ${target.id}" + }, + "valuesPrinted": False, +} +print(json.dumps(payload, ensure_ascii=False, indent=2)) +sys.exit(0 if payload["ok"] else 1) +PY +`; +} + +function statusScript(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget, full: boolean): string { + const esoDeployments = [ + secretPlane.eso.controllerDeploymentName, + secretPlane.eso.webhookDeploymentName, + secretPlane.eso.certControllerDeploymentName, + ]; + const directDeployments = [...esoDeployments, secretPlane.vault.deploymentName, secretPlane.syncProbe.consumer.deploymentName]; + const deploymentArgs = directDeployments.map((name) => `deployment/${name}`).join(" "); + const crdArgs = secretPlane.eso.crds.map((name) => `crd/${name}`).join(" "); + return ` +set -u +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +capture_json() { + name="$1" + shift + "$@" -o json >"$tmp/$name.json" 2>"$tmp/$name.err" + rc=$? + printf '%s' "$rc" >"$tmp/$name.rc" +} +capture_json ns kubectl get namespace ${target.namespace} +capture_json deployments kubectl -n ${target.namespace} get ${deploymentArgs} +capture_json pods kubectl -n ${target.namespace} get pods -l app.kubernetes.io/part-of=platform-infra +capture_json services kubectl -n ${target.namespace} get service ${secretPlane.vault.serviceName} +capture_json crds kubectl get ${crdArgs} +capture_json secretstore kubectl -n ${target.namespace} get secretstore ${secretPlane.syncProbe.secretStoreName} +capture_json externalsecret kubectl -n ${target.namespace} get externalsecret ${secretPlane.syncProbe.externalSecretName} +capture_json targetsecret kubectl -n ${target.namespace} get secret ${secretPlane.syncProbe.targetSecretName} +capture_json tokensecret kubectl -n ${target.namespace} get secret ${secretPlane.vault.tokenSecretName} +python3 - "$tmp" <<'PY' +import base64, hashlib, json, os, sys +tmp = sys.argv[1] +def rc(name): + try: + return int(open(os.path.join(tmp, f"{name}.rc"), encoding="utf-8").read() or "1") + except FileNotFoundError: + return 1 +def load(name): + path = os.path.join(tmp, f"{name}.json") + try: + return json.load(open(path, encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError): + return None +def list_items(name): + data = load(name) + if isinstance(data, dict) and isinstance(data.get("items"), list): + return data["items"] + if isinstance(data, dict) and data.get("kind") != "Status": + return [data] + return [] +def deployment_summary(item): + spec = item.get("spec") or {} + status = item.get("status") or {} + desired = spec.get("replicas", 1) + available = status.get("availableReplicas", 0) + containers = ((spec.get("template") or {}).get("spec") or {}).get("containers", []) + return { + "name": item.get("metadata", {}).get("name"), + "desired": desired, + "readyReplicas": status.get("readyReplicas", 0), + "availableReplicas": available, + "updatedReplicas": status.get("updatedReplicas", 0), + "ready": available >= desired, + "images": [container.get("image") for container in containers], + } +def service_summary(item): + spec = item.get("spec") or {} + return { + "name": item.get("metadata", {}).get("name"), + "type": spec.get("type", "ClusterIP"), + "clusterIP": spec.get("clusterIP"), + "ports": [{"name": p.get("name"), "port": p.get("port"), "targetPort": p.get("targetPort")} for p in spec.get("ports", [])], + } +def condition_summary(item): + status = item.get("status") or {} + return [ + {"type": c.get("type"), "status": c.get("status"), "reason": c.get("reason"), "message": c.get("message")} + for c in status.get("conditions", []) + ] +def secret_key_summary(item, keys): + data = (item or {}).get("data") or {} + out = {"name": (item or {}).get("metadata", {}).get("name"), "ready": bool(data), "keys": sorted(data.keys()), "missingKeys": [key for key in keys if key not in data], "fingerprints": {}, "valuesPrinted": False} + for key in keys: + raw = data.get(key) + if isinstance(raw, str): + try: + decoded = base64.b64decode(raw) + out["fingerprints"][key] = "sha256:" + hashlib.sha256(decoded).hexdigest() + except Exception: + out["fingerprints"][key] = "decode-error" + return out +deployments = [deployment_summary(item) for item in list_items("deployments")] +deployment_ready = {item["name"]: item["ready"] for item in deployments} +crds = list_items("crds") +crd_names = sorted([item.get("metadata", {}).get("name") for item in crds if isinstance(item, dict)]) +target_secret = secret_key_summary(load("targetsecret"), ["${secretPlane.syncProbe.remoteProperty}"]) +token_secret = secret_key_summary(load("tokensecret"), ["${secretPlane.vault.tokenSecretKey}"]) +secretstore = load("secretstore") or {} +externalsecret = load("externalsecret") or {} +eso_ready = all(deployment_ready.get(name) is True for name in ${JSON.stringify(esoDeployments)}) +vault_ready = deployment_ready.get("${secretPlane.vault.deploymentName}") is True +consumer_ready = deployment_ready.get("${secretPlane.syncProbe.consumer.deploymentName}") is True +crds_ready = all(name in crd_names for name in ${JSON.stringify(secretPlane.eso.crds)}) +synced = target_secret["fingerprints"].get("${secretPlane.syncProbe.remoteProperty}") == "${secretPlane.syncProbe.expectedFingerprint}" +payload = { + "ready": eso_ready and vault_ready and crds_ready, + "target": "${target.id}", + "route": "${target.route}", + "namespace": "${target.namespace}", + "namespaceExists": rc("ns") == 0, + "crdsReady": crds_ready, + "esoReady": eso_ready, + "vaultReady": vault_ready, + "consumerReady": consumer_ready, + "syncReady": synced, + "deployments": deployments, + "services": [service_summary(item) for item in list_items("services")], + "secretStore": {"name": "${secretPlane.syncProbe.secretStoreName}", "conditions": condition_summary(secretstore)}, + "externalSecret": {"name": "${secretPlane.syncProbe.externalSecretName}", "conditions": condition_summary(externalsecret), "refreshTime": (externalsecret.get("status") or {}).get("refreshTime")}, + "targetSecret": target_secret, + "tokenSecret": {"name": token_secret["name"], "ready": token_secret["ready"], "keys": token_secret["keys"], "missingKeys": token_secret["missingKeys"], "valuesPrinted": False}, + "valuesPrinted": False, +} +if ${full ? "False" : "True"}: + for item in payload["deployments"]: + item["images"] = item["images"][:1] +print(json.dumps(payload, ensure_ascii=False, indent=2)) +sys.exit(0 if payload["ready"] else 1) +PY +`; +} + +function validateScript(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget, full: boolean): string { + const testValue = secretPlane.syncProbe.testValueSource.value; + const vaultKvPath = `${secretPlane.syncProbe.vaultMountPath}/${secretPlane.syncProbe.remotePath}`; + const vaultPutCommand = `set -u; export VAULT_ADDR=http://127.0.0.1:${secretPlane.vault.port}; export VAULT_TOKEN="$VAULT_DEV_ROOT_TOKEN_ID"; vault kv put ${shQuote(vaultKvPath)} ${secretPlane.syncProbe.remoteProperty}="$(cat)" >/dev/null`; + const vaultMetadataCommand = `set -u; export VAULT_ADDR=http://127.0.0.1:${secretPlane.vault.port}; export VAULT_TOKEN="$VAULT_DEV_ROOT_TOKEN_ID"; vault kv metadata get -format=json ${shQuote(vaultKvPath)} >/tmp/unidesk-vault-metadata.json`; + const consumerEnvRef = `\${${secretPlane.syncProbe.consumer.envName}:-}`; + const consumerCommand = `test "${consumerEnvRef}" = "$(cat)"`; + return ` +set -u +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +expected="$tmp/expected" +printf '%s' ${shQuote(testValue)} >"$expected" +vault_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${secretPlane.vault.deploymentName},app.kubernetes.io/component=vault-backend -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/vault-pod.err" || true)" +printf '%s' "$vault_pod" >"$tmp/vault-pod.txt" +if [ -n "$vault_pod" ]; then + kubectl -n ${target.namespace} exec "$vault_pod" -- sh -lc ${shQuote(vaultPutCommand)} <"$expected" >"$tmp/vault-put.out" 2>"$tmp/vault-put.err" + vault_put_rc=$? + kubectl -n ${target.namespace} exec "$vault_pod" -- sh -lc ${shQuote(vaultMetadataCommand)} >"$tmp/vault-metadata.out" 2>"$tmp/vault-metadata.err" + vault_metadata_rc=$? +else + vault_put_rc=1 + vault_metadata_rc=1 + printf '%s\\n' 'vault pod not found' >"$tmp/vault-put.err" + printf '%s\\n' 'vault pod not found' >"$tmp/vault-metadata.err" +fi +deadline=$(( $(date +%s) + ${secretPlane.validation.timeoutSeconds} )) +sync_rc=1 +secret_fingerprint=missing +while [ "$(date +%s)" -le "$deadline" ]; do + if kubectl -n ${target.namespace} get secret ${secretPlane.syncProbe.targetSecretName} -o jsonpath='{.data.${secretPlane.syncProbe.remoteProperty}}' >"$tmp/secret.b64" 2>"$tmp/secret.err"; then + if base64 -d "$tmp/secret.b64" >"$tmp/secret.value" 2>>"$tmp/secret.err"; then + secret_fingerprint="$(sha256sum "$tmp/secret.value" | awk '{print "sha256:" $1}')" + if cmp -s "$expected" "$tmp/secret.value"; then + sync_rc=0 + break + fi + fi + fi + sleep ${secretPlane.validation.pollSeconds} +done +consumer_pod="$(kubectl -n ${target.namespace} get pod -l app.kubernetes.io/name=${secretPlane.syncProbe.consumer.deploymentName},app.kubernetes.io/component=secret-consumer -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/consumer-pod.err" || true)" +printf '%s' "$consumer_pod" >"$tmp/consumer-pod.txt" +if [ "$sync_rc" -eq 0 ] && [ -n "$consumer_pod" ]; then + kubectl -n ${target.namespace} exec "$consumer_pod" -- sh -lc ${shQuote(consumerCommand)} <"$expected" >"$tmp/consumer-env.out" 2>"$tmp/consumer-env.err" + consumer_env_rc=$? +else + consumer_env_rc=1 + if [ -z "$consumer_pod" ]; then printf '%s\\n' 'consumer pod not found' >"$tmp/consumer-env.err"; else printf '%s\\n' 'secret sync not ready' >"$tmp/consumer-env.err"; fi +fi +kubectl -n ${target.namespace} get secretstore ${secretPlane.syncProbe.secretStoreName} -o json >"$tmp/secretstore.json" 2>"$tmp/secretstore.err" +secretstore_rc=$? +kubectl -n ${target.namespace} get externalsecret ${secretPlane.syncProbe.externalSecretName} -o json >"$tmp/externalsecret.json" 2>"$tmp/externalsecret.err" +externalsecret_rc=$? +python3 - "$vault_put_rc" "$vault_metadata_rc" "$sync_rc" "$consumer_env_rc" "$secretstore_rc" "$externalsecret_rc" "$secret_fingerprint" "$tmp" <<'PY' +import json, os, sys +vault_put_rc, vault_metadata_rc, sync_rc, consumer_env_rc, secretstore_rc, externalsecret_rc = [int(value) for value in sys.argv[1:7]] +secret_fingerprint = sys.argv[7] +tmp = sys.argv[8] +def text(name, limit=3000): + try: + return open(os.path.join(tmp, name), encoding="utf-8", errors="replace").read()[-limit:] + except FileNotFoundError: + return "" +def load(name): + try: + return json.load(open(os.path.join(tmp, name), encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError): + return None +def conditions(obj): + status = (obj or {}).get("status") or {} + return [{"type": c.get("type"), "status": c.get("status"), "reason": c.get("reason"), "message": c.get("message")} for c in status.get("conditions", [])] +payload = { + "ok": vault_put_rc == 0 and vault_metadata_rc == 0 and sync_rc == 0 and consumer_env_rc == 0 and secretstore_rc == 0 and externalsecret_rc == 0, + "target": "${target.id}", + "namespace": "${target.namespace}", + "checks": { + "vaultKvWrite": {"exitCode": vault_put_rc, "pod": text("vault-pod.txt", 200), "stderrTail": text("vault-put.err")}, + "vaultKvMetadata": {"exitCode": vault_metadata_rc, "stderrTail": text("vault-metadata.err")}, + "externalSecretSync": { + "exitCode": sync_rc, + "targetSecretName": "${secretPlane.syncProbe.targetSecretName}", + "targetKey": "${secretPlane.syncProbe.remoteProperty}", + "fingerprint": secret_fingerprint, + "expectedFingerprint": "${secretPlane.syncProbe.expectedFingerprint}", + "valuesPrinted": False, + "stderrTail": text("secret.err"), + }, + "consumerEnvInjection": { + "exitCode": consumer_env_rc, + "deploymentName": "${secretPlane.syncProbe.consumer.deploymentName}", + "pod": text("consumer-pod.txt", 200), + "envName": "${secretPlane.syncProbe.consumer.envName}", + "valuesPrinted": False, + "stderrTail": text("consumer-env.err"), + }, + "secretStore": {"exitCode": secretstore_rc, "conditions": conditions(load("secretstore.json")), "stderrTail": text("secretstore.err")}, + "externalSecret": {"exitCode": externalsecret_rc, "conditions": conditions(load("externalsecret.json")), "stderrTail": text("externalsecret.err")}, + }, + "boundary": "platform-infra only; no HWLAB namespace or workload integration was created", + "valuesPrinted": False, +} +if ${full ? "False" : "True"}: + for check in payload["checks"].values(): + if isinstance(check, dict): + for key in list(check.keys()): + if key.endswith("Tail") and isinstance(check[key], str): + check[key] = check[key][-1200:] +print(json.dumps(payload, ensure_ascii=False, indent=2)) +sys.exit(0 if payload["ok"] else 1) +PY +`; +} + +function dateCommand(): string { + return "date +%s"; +} + +function configSummary(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget): Record { + return { + path: configLabel, + version: secretPlane.version, + kind: secretPlane.kind, + metadata: secretPlane.metadata, + target: targetSummary(target), + }; +} + +function targetSummary(target: SecretPlaneTarget): Record { + return { + id: target.id, + route: target.route, + namespace: target.namespace, + role: target.role, + createNamespace: target.createNamespace, + }; +} + +function vaultSummary(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget): Record { + const vault = secretPlane.vault; + return { + mode: vault.mode, + deploymentName: vault.deploymentName, + serviceName: vault.serviceName, + serviceDns: `${vault.serviceName}.${target.namespace}.svc.cluster.local:${vault.port}`, + image: imageRef(vault.image), + pullPolicy: vault.image.pullPolicy, + tokenSecret: { + name: vault.tokenSecretName, + key: vault.tokenSecretKey, + tokenMode: vault.bootstrap.tokenMode, + valuesPrinted: false, + }, + }; +} + +function syncProbeSummary(secretPlane: SecretPlaneConfig): Record { + const probe = secretPlane.syncProbe; + return { + dataFlow: "Vault KV v2 -> ESO SecretStore -> ExternalSecret -> Kubernetes Secret -> consumer env", + secretStoreName: probe.secretStoreName, + externalSecretName: probe.externalSecretName, + targetSecretName: probe.targetSecretName, + refreshInterval: probe.refreshInterval, + remoteRef: { + mountPath: probe.vaultMountPath, + key: probe.remotePath, + property: probe.remoteProperty, + }, + expectedFingerprint: probe.expectedFingerprint, + consumer: { + deploymentName: probe.consumer.deploymentName, + envName: probe.consumer.envName, + image: imageRef(probe.consumer.image), + }, + valuesPrinted: false, + }; +} + +function policyChecks(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget, yaml: string): Array> { + const kinds = manifestObjectSummary(yaml).map((item) => item.kind); + return [ + { name: "target-is-d601", ok: target.id === "D601" && target.route === "D601:k3s", detail: "PoC stays on D601 per HWLAB#2233 correction." }, + { name: "namespace-is-platform-infra", ok: target.namespace === "platform-infra", detail: "Secret plane is external platform infrastructure and not an HWLAB namespace." }, + { name: "no-hwlab-workloads", ok: !/namespace:\s*hwlab/iu.test(yaml) && !/hwlab-v0?3/iu.test(yaml), detail: "This PoC must not integrate into HWLAB v0.3 yet." }, + { name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Secret plane services stay ClusterIP-only." }, + { name: "no-hardcoded-token", ok: !yaml.includes("VAULT_DEV_ROOT_TOKEN_ID:") && secretPlane.vault.bootstrap.tokenMode === "generated-if-missing", detail: "Vault dev token is generated into a Kubernetes Secret when missing and never committed." }, + { name: "required-objects-rendered", ok: kinds.includes("Deployment") && kinds.includes("SecretStore") && kinds.includes("ExternalSecret"), detail: "Vault backend, ESO SecretStore and ExternalSecret PoC objects are rendered from YAML." }, + ]; +} + +function manifestObjectSummary(yaml: string): Array> { + return yaml.split(/^---\s*$/mu).map((document) => { + const kind = /^\s*kind:\s*([A-Za-z0-9]+)\s*$/mu.exec(document)?.[1]; + const name = /^\s*name:\s*([A-Za-z0-9._-]+)\s*$/mu.exec(document)?.[1]; + const namespace = /^\s*namespace:\s*([A-Za-z0-9._-]+)\s*$/mu.exec(document)?.[1]; + return kind === undefined || name === undefined ? null : { kind, name, namespace: namespace ?? "" }; + }).filter((item): item is Record => item !== null); +} + +function compactStatus(parsed: Record, full: boolean): Record { + if (full) return parsed; + return { + ready: parsed.ready, + target: parsed.target, + namespace: parsed.namespace, + crdsReady: parsed.crdsReady, + esoReady: parsed.esoReady, + vaultReady: parsed.vaultReady, + consumerReady: parsed.consumerReady, + syncReady: parsed.syncReady, + deployments: parsed.deployments, + secretStore: parsed.secretStore, + externalSecret: parsed.externalSecret, + targetSecret: parsed.targetSecret, + tokenSecret: parsed.tokenSecret, + valuesPrinted: false, + }; +} + +function renderPlan(result: Record): RenderedCliResult { + const config = record(result.config); + const target = record(config.target); + const renderPlan = record(result.renderPlan); + const eso = record(renderPlan.eso); + const vault = record(renderPlan.vault); + const syncProbe = record(renderPlan.syncProbe); + const policy = arrayRecords(result.policy); + const failed = policy.filter((item) => item.ok === false); + const next = record(result.next); + const rows = [ + ["TARGET", stringValue(target.id), "route", stringValue(target.route)], + ["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)], + ["ESO", stringValue(eso.version), "manifest", stringValue(eso.manifestUrl)], + ["VAULT", stringValue(vault.deploymentName), "service", stringValue(vault.serviceDns)], + ["SYNC", stringValue(syncProbe.externalSecretName), "targetSecret", stringValue(syncProbe.targetSecretName)], + ["POLICY", failed.length === 0 ? "ok" : `failed=${failed.length}`, "valuesPrinted", "false"], + ]; + return rendered(result, "platform-infra secret-plane plan", [ + "PLATFORM-INFRA SECRET-PLANE PLAN", + ...table(["FIELD", "VALUE", "DETAIL", "VALUE"], rows), + "", + "NEXT", + ` dry-run: ${stringValue(next.dryRun)}`, + ` apply: ${stringValue(next.apply)}`, + ` status: ${stringValue(next.status)}`, + ` validate: ${stringValue(next.validate)}`, + "", + "Boundary: D601 platform-infra only; no HWLAB v0.3 integration is rendered.", + "Disclosure: Secret values are not printed; only object/key/fingerprint summaries are shown.", + ]); +} + +function renderStatus(result: Record): RenderedCliResult { + const target = record(result.target); + const summary = record(result.summary); + const deployments = arrayRecords(summary.deployments).map((item) => [ + stringValue(item.name), + boolText(item.ready), + `${stringValue(item.readyReplicas, "0")}/${stringValue(item.desired, "0")}`, + ]); + const targetSecret = record(summary.targetSecret); + const tokenSecret = record(summary.tokenSecret); + return rendered(result, "platform-infra secret-plane status", [ + "PLATFORM-INFRA SECRET-PLANE STATUS", + ...table(["TARGET", "ROUTE", "NAMESPACE", "READY"], [[stringValue(target.id), stringValue(target.route), stringValue(target.namespace), boolText(summary.ready)]]), + "", + "WORKLOADS", + ...(deployments.length === 0 ? ["-"] : table(["NAME", "READY", "REPLICAS"], deployments)), + "", + "CHECKS", + ...table(["CHECK", "VALUE", "DETAIL"], [ + ["crds", boolText(summary.crdsReady), "ESO API installed"], + ["eso", boolText(summary.esoReady), "controller/webhook/cert-controller"], + ["vault", boolText(summary.vaultReady), "Vault dev KV v2 backend"], + ["sync", boolText(summary.syncReady), `secret=${stringValue(targetSecret.name)} key=${stringValue(arrayValues(targetSecret.keys).join(","), "-")}`], + ["token", boolText(tokenSecret.ready), `secret=${stringValue(tokenSecret.name)} valuesPrinted=false`], + ]), + "", + `NEXT bun scripts/cli.ts platform-infra secret-plane validate --target ${stringValue(target.id)}`, + ]); +} + +function rendered(result: Record, command: string, lines: string[]): RenderedCliResult { + return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain" }; +} + +function parseApplyOptions(args: string[]): ApplyOptions { + const options = parseCommonOptions(args); + if (args.includes("--confirm") && args.includes("--dry-run")) throw new Error("apply accepts only one of --confirm or --dry-run"); + return { ...options, dryRun: !args.includes("--confirm") || args.includes("--dry-run"), confirm: args.includes("--confirm") }; +} + +function parseCommonOptions(args: string[]): CommonOptions { + let targetId: string | null = null; + let full = false; + let raw = false; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--target") { + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value"); + targetId = value; + index += 1; + } else if (arg.startsWith("--target=")) { + targetId = arg.slice("--target=".length); + } else if (arg === "--full") { + full = true; + } else if (arg === "--raw") { + raw = true; + full = true; + } else if (arg === "--dry-run" || arg === "--confirm") { + continue; + } else { + throw new Error(`unsupported option: ${arg}`); + } + } + if (targetId !== null && !/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error("--target must be a simple target id"); + return { targetId, full, raw }; +} + +function imageRef(image: ImageSpec): string { + return `${image.repository}:${image.tag}`; +} + +function sha256Fingerprint(value: string): string { + return `sha256:${createHash("sha256").update(value).digest("hex")}`; +} + +function record(value: unknown): Record { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; +} + +function arrayRecords(value: unknown): Record[] { + return Array.isArray(value) ? value.map(record) : []; +} + +function arrayValues(value: unknown): unknown[] { + return Array.isArray(value) ? value : []; +} + +function stringValue(value: unknown, fallback = "-"): string { + if (value === undefined || value === null || value === "") return fallback; + return String(value); +} + +function boolText(value: unknown): string { + return value === true ? "yes" : value === false ? "no" : "-"; +} + +function table(headers: string[], rows: string[][]): string[] { + const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0))); + const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd(); + return [renderRow(headers), ...rows.map(renderRow)]; +} diff --git a/scripts/src/platform-infra/entry.ts b/scripts/src/platform-infra/entry.ts index b56264cc..79c8f4dd 100644 --- a/scripts/src/platform-infra/entry.ts +++ b/scripts/src/platform-infra/entry.ts @@ -314,7 +314,7 @@ export interface ManagedResourceCleanupPlan { export function platformInfraHelp(): unknown { const target = sub2ApiHelpTargetSummary(); return { - command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability ...", + command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability|secret-plane ...", output: "json", usage: [ "bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]", @@ -360,8 +360,13 @@ export function platformInfraHelp(): unknown { "bun scripts/cli.ts platform-infra observability search --target D601 --grep 'no rollout found' [--lookback-minutes 360] [--candidate-limit 80] [--limit 20]", "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D601 --business-trace-id [--full|--raw]", "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D601 --run-id [--command-id ] [--runner-job-id ]", + "bun scripts/cli.ts platform-infra secret-plane plan --target D601", + "bun scripts/cli.ts platform-infra secret-plane apply --target D601 --dry-run", + "bun scripts/cli.ts platform-infra secret-plane apply --target D601 --confirm", + "bun scripts/cli.ts platform-infra secret-plane status --target D601", + "bun scripts/cli.ts platform-infra secret-plane validate --target D601", ], - description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows and OpenTelemetry tracing. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.", + description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows, OpenTelemetry tracing and the independent D601 secret plane. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.", target, codexPool: { usage: [ diff --git a/scripts/src/platform-infra/options.ts b/scripts/src/platform-infra/options.ts index dc280014..cff11905 100644 --- a/scripts/src/platform-infra/options.ts +++ b/scripts/src/platform-infra/options.ts @@ -56,6 +56,10 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin const { runPlatformObservabilityCommand } = await import("../platform-infra-observability"); return await runPlatformObservabilityCommand(config, args.slice(1)); } + if (target === "secret-plane") { + const { runSecretPlaneCommand } = await import("../platform-infra-secret-plane"); + return await runSecretPlaneCommand(config, args.slice(1)); + } if (target !== "sub2api") return unsupported(args); if (action === "plan" || action === undefined) { const planArgs = args.slice(2);