1340 lines
59 KiB
TypeScript
1340 lines
59 KiB
TypeScript
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;
|
|
clusterSecretStoreName: 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<Record<string, unknown> | 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 JD01",
|
|
"bun scripts/cli.ts platform-infra secret-plane apply --target JD01 --dry-run",
|
|
"bun scripts/cli.ts platform-infra secret-plane apply --target JD01 --confirm",
|
|
"bun scripts/cli.ts platform-infra secret-plane status --target JD01",
|
|
"bun scripts/cli.ts platform-infra secret-plane validate --target JD01",
|
|
],
|
|
};
|
|
}
|
|
|
|
function readSecretPlaneConfig(): SecretPlaneConfig {
|
|
const root = readYamlRecord<Record<string, unknown>>(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<string, unknown>, 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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"),
|
|
clusterSecretStoreName: y.kubernetesNameField(record, "clusterSecretStoreName", "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<string, unknown>, 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<string, unknown> {
|
|
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<Record<string, unknown>> {
|
|
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<Record<string, unknown>> {
|
|
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<Record<string, unknown>> {
|
|
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<string> {
|
|
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 rewriteEsoManifestNamespace(text, target.namespace);
|
|
}
|
|
|
|
function rewriteEsoManifestNamespace(text: string, namespaceName: string): string {
|
|
const rewritten = text
|
|
.replace(/^(\s*namespace:\s*)default\s*$/gmu, `$1${namespaceName}`)
|
|
.replaceAll("external-secrets-webhook.default.svc", `external-secrets-webhook.${namespaceName}.svc`)
|
|
.replaceAll("--service-namespace=default", `--service-namespace=${namespaceName}`)
|
|
.replaceAll("--secret-namespace=default", `--secret-namespace=${namespaceName}`);
|
|
const remainingRuntimeDefaultRefs = [
|
|
"external-secrets-webhook.default.svc",
|
|
"--service-namespace=default",
|
|
"--secret-namespace=default",
|
|
].filter((needle) => rewritten.includes(needle));
|
|
if (remainingRuntimeDefaultRefs.length > 0) throw new Error(`ESO manifest namespace rewrite left runtime default references: ${remainingRuntimeDefaultRefs.join(", ")}`);
|
|
return rewritten;
|
|
}
|
|
|
|
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: ClusterSecretStore
|
|
metadata:
|
|
name: ${probe.clusterSecretStoreName}
|
|
labels:
|
|
app.kubernetes.io/name: ${probe.clusterSecretStoreName}
|
|
app.kubernetes.io/component: cluster-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}
|
|
namespace: ${target.namespace}
|
|
---
|
|
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", "ClusterSecretStore", "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/ClusterSecretStore/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"
|
|
eso_wait_out="$tmp/eso-wait.out"; eso_wait_err="$tmp/eso-wait.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 -n ${target.namespace} wait --for=condition=available --timeout=${secretPlane.validation.timeoutSeconds}s deployment/${secretPlane.eso.controllerDeploymentName} deployment/${secretPlane.eso.webhookDeploymentName} deployment/${secretPlane.eso.certControllerDeploymentName} >"$eso_wait_out" 2>"$eso_wait_err"
|
|
eso_wait_rc=$?
|
|
else
|
|
eso_wait_rc=1
|
|
printf '%s\\n' 'skipped because ESO CRDs are not established' >"$eso_wait_err"
|
|
fi
|
|
if [ "$eso_wait_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 deployments are not available' >"$workload_err"
|
|
fi
|
|
python3 - "$ns_rc" "$token_rc" "$eso_rc" "$crd_rc" "$eso_wait_rc" "$workload_rc" "$token_action" "$ns_out" "$ns_err" "$token_out" "$token_err" "$eso_out" "$eso_err" "$crd_out" "$crd_err" "$eso_wait_out" "$eso_wait_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, eso_wait_rc, workload_rc = [int(value) for value in sys.argv[1:7]]
|
|
paths = sys.argv[8:]
|
|
payload = {
|
|
"ok": ns_rc == 0 and token_rc == 0 and eso_rc == 0 and crd_rc == 0 and eso_wait_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[7],
|
|
"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])},
|
|
"esoDeploymentsReady": {"exitCode": eso_wait_rc, "stdoutTail": text(paths[8]), "stderrTail": text(paths[9])},
|
|
"secretPlaneWorkload": {"exitCode": workload_rc, "stdoutTail": text(paths[10]), "stderrTail": text(paths[11])},
|
|
},
|
|
"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 clustersecretstore kubectl get clustersecretstore ${secretPlane.syncProbe.clusterSecretStoreName}
|
|
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 ready_condition(item):
|
|
return any(c.get("type") == "Ready" and c.get("status") == "True" for c in condition_summary(item))
|
|
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 {}
|
|
clustersecretstore = load("clustersecretstore") 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)})
|
|
secretstore_ready = ready_condition(secretstore)
|
|
clustersecretstore_ready = ready_condition(clustersecretstore)
|
|
synced = target_secret["fingerprints"].get("${secretPlane.syncProbe.remoteProperty}") == "${secretPlane.syncProbe.expectedFingerprint}"
|
|
payload = {
|
|
"ready": eso_ready and vault_ready and crds_ready and secretstore_ready and clustersecretstore_ready,
|
|
"target": "${target.id}",
|
|
"route": "${target.route}",
|
|
"namespace": "${target.namespace}",
|
|
"namespaceExists": rc("ns") == 0,
|
|
"crdsReady": crds_ready,
|
|
"esoReady": eso_ready,
|
|
"vaultReady": vault_ready,
|
|
"secretStoreReady": secretstore_ready,
|
|
"clusterSecretStoreReady": clustersecretstore_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)},
|
|
"clusterSecretStore": {"name": "${secretPlane.syncProbe.clusterSecretStoreName}", "conditions": condition_summary(clustersecretstore)},
|
|
"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 -i "$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
|
|
if [ "$sync_rc" -eq 0 ]; then
|
|
kubectl -n ${target.namespace} rollout restart deployment/${secretPlane.syncProbe.consumer.deploymentName} >"$tmp/consumer-rollout-restart.out" 2>"$tmp/consumer-rollout-restart.err"
|
|
consumer_rollout_restart_rc=$?
|
|
if [ "$consumer_rollout_restart_rc" -eq 0 ]; then
|
|
kubectl -n ${target.namespace} rollout status deployment/${secretPlane.syncProbe.consumer.deploymentName} --timeout=25s >"$tmp/consumer-rollout.out" 2>"$tmp/consumer-rollout.err"
|
|
consumer_rollout_rc=$?
|
|
else
|
|
consumer_rollout_rc=1
|
|
printf '%s\\n' 'consumer rollout restart failed' >"$tmp/consumer-rollout.err"
|
|
fi
|
|
else
|
|
consumer_rollout_restart_rc=1
|
|
consumer_rollout_rc=1
|
|
printf '%s\\n' 'secret sync not ready' >"$tmp/consumer-rollout-restart.err"
|
|
printf '%s\\n' 'secret sync not ready' >"$tmp/consumer-rollout.err"
|
|
fi
|
|
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 ] && [ "$consumer_rollout_rc" -eq 0 ] && [ -n "$consumer_pod" ]; then
|
|
kubectl -n ${target.namespace} exec -i "$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"; elif [ "$consumer_rollout_rc" -ne 0 ]; then printf '%s\\n' 'consumer rollout not ready' >"$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 get clustersecretstore ${secretPlane.syncProbe.clusterSecretStoreName} -o json >"$tmp/clustersecretstore.json" 2>"$tmp/clustersecretstore.err"
|
|
clustersecretstore_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_rollout_restart_rc" "$consumer_rollout_rc" "$consumer_env_rc" "$secretstore_rc" "$clustersecretstore_rc" "$externalsecret_rc" "$secret_fingerprint" "$tmp" <<'PY'
|
|
import json, os, sys
|
|
vault_put_rc, vault_metadata_rc, sync_rc, consumer_rollout_restart_rc, consumer_rollout_rc, consumer_env_rc, secretstore_rc, clustersecretstore_rc, externalsecret_rc = [int(value) for value in sys.argv[1:10]]
|
|
secret_fingerprint = sys.argv[10]
|
|
tmp = sys.argv[11]
|
|
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_rollout_restart_rc == 0 and consumer_rollout_rc == 0 and consumer_env_rc == 0 and secretstore_rc == 0 and clustersecretstore_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": {
|
|
"rolloutRestartExitCode": consumer_rollout_restart_rc,
|
|
"rolloutExitCode": consumer_rollout_rc,
|
|
"rolloutStdoutTail": text("consumer-rollout.out"),
|
|
"rolloutStderrTail": text("consumer-rollout.err"),
|
|
"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")},
|
|
"clusterSecretStore": {"exitCode": clustersecretstore_rc, "conditions": conditions(load("clustersecretstore.json")), "stderrTail": text("clustersecretstore.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<string, unknown> {
|
|
return {
|
|
path: configLabel,
|
|
version: secretPlane.version,
|
|
kind: secretPlane.kind,
|
|
metadata: secretPlane.metadata,
|
|
target: targetSummary(target),
|
|
};
|
|
}
|
|
|
|
function targetSummary(target: SecretPlaneTarget): Record<string, unknown> {
|
|
return {
|
|
id: target.id,
|
|
route: target.route,
|
|
namespace: target.namespace,
|
|
role: target.role,
|
|
createNamespace: target.createNamespace,
|
|
};
|
|
}
|
|
|
|
function vaultSummary(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget): Record<string, unknown> {
|
|
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<string, unknown> {
|
|
const probe = secretPlane.syncProbe;
|
|
return {
|
|
dataFlow: "Vault KV v2 -> ESO SecretStore/ClusterSecretStore -> ExternalSecret -> Kubernetes Secret -> consumer env",
|
|
secretStoreName: probe.secretStoreName,
|
|
clusterSecretStoreName: probe.clusterSecretStoreName,
|
|
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<Record<string, unknown>> {
|
|
const kinds = manifestObjectSummary(yaml).map((item) => item.kind);
|
|
return [
|
|
{ name: "target-is-active", ok: target.role === "active", detail: "PoC target must be the YAML-selected active secret-plane target." },
|
|
{ name: "target-route-is-k3s", ok: target.route === `${target.id}:k3s`, detail: "Secret plane deployment uses the selected node k3s route." },
|
|
{ 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("ClusterSecretStore") && kinds.includes("ExternalSecret"), detail: "Vault backend, ESO SecretStore/ClusterSecretStore and ExternalSecret PoC objects are rendered from YAML." },
|
|
];
|
|
}
|
|
|
|
function manifestObjectSummary(yaml: string): Array<Record<string, string>> {
|
|
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<string, string> => item !== null);
|
|
}
|
|
|
|
function compactStatus(parsed: Record<string, unknown>, full: boolean): Record<string, unknown> {
|
|
if (full) return parsed;
|
|
return {
|
|
ready: parsed.ready,
|
|
target: parsed.target,
|
|
namespace: parsed.namespace,
|
|
crdsReady: parsed.crdsReady,
|
|
esoReady: parsed.esoReady,
|
|
vaultReady: parsed.vaultReady,
|
|
secretStoreReady: parsed.secretStoreReady,
|
|
clusterSecretStoreReady: parsed.clusterSecretStoreReady,
|
|
consumerReady: parsed.consumerReady,
|
|
syncReady: parsed.syncReady,
|
|
deployments: parsed.deployments,
|
|
secretStore: parsed.secretStore,
|
|
clusterSecretStore: parsed.clusterSecretStore,
|
|
externalSecret: parsed.externalSecret,
|
|
targetSecret: parsed.targetSecret,
|
|
tokenSecret: parsed.tokenSecret,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function renderPlan(result: Record<string, unknown>): 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)],
|
|
["STORE", stringValue(syncProbe.secretStoreName), "clusterStore", stringValue(syncProbe.clusterSecretStoreName)],
|
|
["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: ${stringValue(target.id)} 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<string, unknown>): 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"],
|
|
["secretStore", boolText(summary.secretStoreReady), stringValue(record(summary.secretStore).name)],
|
|
["clusterStore", boolText(summary.clusterSecretStoreReady), stringValue(record(summary.clusterSecretStore).name)],
|
|
["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<string, unknown>, 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<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function arrayRecords(value: unknown): Record<string, unknown>[] {
|
|
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)];
|
|
}
|