feat: make hwlab v03 admin secret yaml driven
This commit is contained in:
@@ -95,6 +95,16 @@ lanes:
|
||||
public:
|
||||
webUrl: http://74.48.78.17:20666
|
||||
apiUrl: http://74.48.78.17:20667
|
||||
bootstrapAdmin:
|
||||
username: admin
|
||||
displayName: HWLAB v0.3 Admin
|
||||
passwordSourceRef: hwlab/g14-v03-bootstrap-admin.env
|
||||
passwordSourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
passwordHashTransform: hwlab-sha256
|
||||
secretName: hwlab-v03-bootstrap-admin
|
||||
secretKey: password-hash
|
||||
rollout:
|
||||
deployment: hwlab-cloud-api
|
||||
targets:
|
||||
D601:
|
||||
workspace: /home/ubuntu/workspace/hwlab-v03
|
||||
@@ -142,6 +152,16 @@ lanes:
|
||||
public:
|
||||
webUrl: https://hwlab.pikapython.com
|
||||
apiUrl: https://hwlab.pikapython.com
|
||||
bootstrapAdmin:
|
||||
username: admin
|
||||
displayName: HWLAB v0.3 Admin
|
||||
passwordSourceRef: hwlab/d601-v03-bootstrap-admin.env
|
||||
passwordSourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
passwordHashTransform: hwlab-sha256
|
||||
secretName: hwlab-v03-bootstrap-admin
|
||||
secretKey: password-hash
|
||||
rollout:
|
||||
deployment: hwlab-cloud-api
|
||||
publicExposure:
|
||||
mode: pk01-caddy-frp
|
||||
publicBaseUrl: https://hwlab.pikapython.com
|
||||
|
||||
@@ -20,7 +20,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动
|
||||
|
||||
`hwlab nodes secret status|ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider` 是 v03 Code Agent / MoonBridge provider SecretRef 的受控 bootstrap 入口;`ensure` 只从集群内既有 `hwlab-v02/hwlab-v02-code-agent-provider` 复制 `openai-api-key`、`opencode-api-key` 到 lane-local Secret,输出仅披露 source/target Secret 名、key presence、decoded byte count、mutation 和后续命令,禁止打印 base64、解码值、完整 API key 或可复用凭据。OpenFGA 和 master admin API key 继续使用同一命名空间下的 `hwlab nodes secret ... --name hwlab-v03-openfga|hwlab-v03-master-server-admin-api-key`。
|
||||
|
||||
`hwlab.pikapython.com` / D601 v03 的 bootstrap admin password 是 HWLAB runtime Secret 生命周期的一部分,必须收敛到 UniDesk YAML 与受控 `hwlab nodes secret ...` CLI;明文只能存在于 Git 忽略、owner-only 的 `.state/secrets/...` 来源文件,CLI、issue、日志和 trace 只能输出 presence、byte count、fingerprint、mutation 与后续命令。当前声明式重设能力缺口由 [GitHub issue #319](https://github.com/pikasTech/unidesk/issues/319) 追踪;不要把人工生成 hash、手工写 k8s Secret 或原生 `kubectl rollout` 沉淀为长期入口。
|
||||
G14/D601 v03 的 bootstrap admin password 是 HWLAB runtime Secret 生命周期的一部分,必须收敛到 `config/hwlab-node-lanes.yaml` 的 `bootstrapAdmin` 声明与受控 `hwlab nodes secret status|ensure --node <node> --lane v03 --name hwlab-v03-bootstrap-admin` CLI。明文只能存在于 Git 忽略、owner-only 的 `.state/secrets/...` sourceRef 文件;CLI 在本地把明文转换为 HWLAB 兼容 password hash,只向运行面同步 `password-hash`,并在输出中只披露 sourceRef、sourceKey、target Secret/key、presence、byte count、fingerprint、mutation 与后续命令。不要把人工生成 hash、手工写 k8s Secret 或原生 `kubectl rollout` 沉淀为长期入口。
|
||||
|
||||
`hwlab nodes control-plane infra plan|status|apply --node D601 --lane v03` 是 D601 HWLAB v03 节点本地 CI/CD 与 git-mirror 前置控制面的 YAML 驱动入口,配置真相源是 `config/hwlab-node-control-plane.yaml`。`plan` 只读展示 YAML target 和将渲染的 control-plane 对象;`status` 只读观察 D601 Tekton、CI namespace、git-mirror、Argo、node-local registry 和 tools image readiness;`apply --dry-run` 只输出 manifest 摘要;`apply --confirm` 只收敛 D601 control-plane bootstrap 对象,不触发 HWLAB runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。tools image 的 node-local registry 地址只能作为输出 artifact;输入 base image 必须由 YAML 声明为公开 registry 来源,缺少 output image 时应在 `status.next.blockers` 中体现,而不是把现有 node-local image 当成输入基础镜像。
|
||||
|
||||
|
||||
@@ -120,6 +120,17 @@ export interface HwlabRuntimePublicExposureSpec {
|
||||
readonly apiProxy: HwlabRuntimePublicExposureFrpcProxySpec;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeBootstrapAdminSpec {
|
||||
readonly username: string;
|
||||
readonly displayName: string;
|
||||
readonly passwordSourceRef: string;
|
||||
readonly passwordSourceKey: string;
|
||||
readonly passwordHashTransform: "hwlab-sha256";
|
||||
readonly secretName: string;
|
||||
readonly secretKey: string;
|
||||
readonly rolloutDeployment: string;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeLaneSpec {
|
||||
readonly lane: HwlabRuntimeLane;
|
||||
readonly nodeId: string;
|
||||
@@ -156,6 +167,7 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly publicApiUrl: string;
|
||||
readonly stepEnv: Record<string, string>;
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
|
||||
readonly observability: HwlabRuntimeObservabilitySpec;
|
||||
@@ -196,6 +208,7 @@ interface HwlabLaneConfig {
|
||||
readonly public: { readonly webUrl: string; readonly apiUrl: string };
|
||||
readonly stepEnv: Record<string, string>;
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
|
||||
readonly observability: HwlabRuntimeObservabilitySpec;
|
||||
@@ -396,6 +409,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
},
|
||||
stepEnv: optionalStringRecord(raw.stepEnv, `lanes.${id}.stepEnv`),
|
||||
buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`),
|
||||
bootstrapAdmin: bootstrapAdminConfig(raw.bootstrapAdmin, `lanes.${id}.bootstrapAdmin`),
|
||||
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
|
||||
publicExposure: publicExposureConfig(raw.publicExposure, `lanes.${id}.publicExposure`),
|
||||
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
|
||||
@@ -414,6 +428,7 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<
|
||||
public: mergeOptionalRecord(baseRaw.public, targetRaw.public),
|
||||
stepEnv: mergeOptionalRecord(baseRaw.stepEnv, targetRaw.stepEnv) ?? {},
|
||||
buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit),
|
||||
bootstrapAdmin: mergeOptionalRecord(baseRaw.bootstrapAdmin, targetRaw.bootstrapAdmin),
|
||||
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
|
||||
publicExposure: mergeOptionalRecord(baseRaw.publicExposure, targetRaw.publicExposure),
|
||||
observability: mergeOptionalRecord(baseRaw.observability, targetRaw.observability),
|
||||
@@ -430,6 +445,36 @@ function buildkitConfig(value: unknown, path: string): HwlabRuntimeBuildkitSpec
|
||||
};
|
||||
}
|
||||
|
||||
function sourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = stringField(obj, key, path);
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${path}.${key} has an unsupported format`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function secretKeyField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = stringField(obj, key, path);
|
||||
if (!/^[A-Za-z0-9_.-]+$/u.test(value)) throw new Error(`${path}.${key} has an unsupported format`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function bootstrapAdminConfig(value: unknown, path: string): HwlabRuntimeBootstrapAdminSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
const transform = stringField(raw, "passwordHashTransform", path);
|
||||
if (transform !== "hwlab-sha256") throw new Error(`${path}.passwordHashTransform must be hwlab-sha256`);
|
||||
const rollout = asRecord(raw.rollout, `${path}.rollout`);
|
||||
return {
|
||||
username: stringField(raw, "username", path),
|
||||
displayName: stringField(raw, "displayName", path),
|
||||
passwordSourceRef: sourceRefField(raw, "passwordSourceRef", path),
|
||||
passwordSourceKey: secretKeyField(raw, "passwordSourceKey", path),
|
||||
passwordHashTransform: transform,
|
||||
secretName: stringField(raw, "secretName", path),
|
||||
secretKey: secretKeyField(raw, "secretKey", path),
|
||||
rolloutDeployment: stringField(rollout, "deployment", `${path}.rollout`),
|
||||
};
|
||||
}
|
||||
|
||||
function externalPostgresComponentConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresComponentSpec {
|
||||
const raw = asRecord(value, path);
|
||||
return {
|
||||
@@ -603,6 +648,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
publicApiUrl: config.public.apiUrl,
|
||||
stepEnv: config.stepEnv,
|
||||
...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }),
|
||||
...(config.bootstrapAdmin === undefined ? {} : { bootstrapAdmin: config.bootstrapAdmin }),
|
||||
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
|
||||
publicExposure: config.publicExposure,
|
||||
observability: config.observability,
|
||||
|
||||
+278
-11
@@ -1,4 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { repoRoot, rootPath, type Config } from "./config";
|
||||
@@ -32,6 +32,17 @@ interface NodeSecretOptions {
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
interface BootstrapAdminSecretMaterial {
|
||||
ok: boolean;
|
||||
sourceRef: string | null;
|
||||
sourceKey: string | null;
|
||||
sourcePath: string | null;
|
||||
sourcePresent: boolean;
|
||||
sourceFingerprint: string | null;
|
||||
passwordHash: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface NodePublicExposureOptions {
|
||||
action: "public-exposure";
|
||||
node: string;
|
||||
@@ -62,6 +73,11 @@ interface RuntimeSecretSpec {
|
||||
masterAdminApiKeySecret: string;
|
||||
bootstrapAdminSecret: string;
|
||||
bootstrapAdminPasswordHashKey: string;
|
||||
bootstrapAdminUsername: string;
|
||||
bootstrapAdminDisplayName: string;
|
||||
bootstrapAdminPasswordSourceRef?: string;
|
||||
bootstrapAdminPasswordSourceKey?: string;
|
||||
bootstrapAdminPasswordHashTransform?: "hwlab-sha256";
|
||||
bootstrapAdminSourceNamespace: string;
|
||||
bootstrapAdminSourceSecret: string;
|
||||
cloudApiDbSecret: string;
|
||||
@@ -304,6 +320,17 @@ function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string, unknown
|
||||
webUrl: spec.publicWebUrl,
|
||||
apiUrl: spec.publicApiUrl,
|
||||
},
|
||||
bootstrapAdmin: spec.bootstrapAdmin === undefined ? null : {
|
||||
username: spec.bootstrapAdmin.username,
|
||||
displayName: spec.bootstrapAdmin.displayName,
|
||||
passwordSourceRef: spec.bootstrapAdmin.passwordSourceRef,
|
||||
passwordSourceKey: spec.bootstrapAdmin.passwordSourceKey,
|
||||
passwordHashTransform: spec.bootstrapAdmin.passwordHashTransform,
|
||||
secretName: spec.bootstrapAdmin.secretName,
|
||||
secretKey: spec.bootstrapAdmin.secretKey,
|
||||
rolloutDeployment: spec.bootstrapAdmin.rolloutDeployment,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
publicExposure: spec.publicExposure === null ? null : publicExposureSummary(spec.publicExposure),
|
||||
downloadProfile: {
|
||||
id: spec.downloadProfileId,
|
||||
@@ -2980,6 +3007,61 @@ function readSecretSourceValue(secretRoot: string, sourceRef: string, key: strin
|
||||
return { ok: true, value, sourcePath };
|
||||
}
|
||||
|
||||
function secretSourcePaths(sourceRef: string): string[] {
|
||||
const paths = [join(repoRoot, ".state", "secrets", sourceRef)];
|
||||
const marker = "/.worktree/";
|
||||
const index = repoRoot.indexOf(marker);
|
||||
if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", sourceRef));
|
||||
return [...new Set(paths)];
|
||||
}
|
||||
|
||||
function displayRepoPath(path: string): string {
|
||||
const normalizedRoot = repoRoot.replace(/\/+$/u, "");
|
||||
if (path === normalizedRoot) return ".";
|
||||
if (path.startsWith(`${normalizedRoot}/`)) return path.slice(normalizedRoot.length + 1);
|
||||
const marker = "/.worktree/";
|
||||
const index = normalizedRoot.indexOf(marker);
|
||||
if (index >= 0) {
|
||||
const mainRoot = normalizedRoot.slice(0, index);
|
||||
if (path === mainRoot) return ".";
|
||||
if (path.startsWith(`${mainRoot}/`)) return path.slice(mainRoot.length + 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function hwlabPasswordHash(password: string): string {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
return `sha256:${salt}:${createHash("sha256").update(`${salt}:${password}`).digest("hex")}`;
|
||||
}
|
||||
|
||||
function readBootstrapAdminSecretMaterial(spec: RuntimeSecretSpec): BootstrapAdminSecretMaterial {
|
||||
const sourceRef = spec.bootstrapAdminPasswordSourceRef;
|
||||
const sourceKey = spec.bootstrapAdminPasswordSourceKey;
|
||||
if (sourceRef === undefined || sourceKey === undefined || spec.bootstrapAdminPasswordHashTransform === undefined) {
|
||||
return { ok: false, sourceRef: sourceRef ?? null, sourceKey: sourceKey ?? null, sourcePath: null, sourcePresent: false, sourceFingerprint: null, passwordHash: null, error: "bootstrap-admin-yaml-source-missing" };
|
||||
}
|
||||
const paths = secretSourcePaths(sourceRef);
|
||||
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
|
||||
if (!existsSync(sourcePath)) {
|
||||
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: false, sourceFingerprint: null, passwordHash: null, error: "secret-source-missing" };
|
||||
}
|
||||
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
|
||||
const password = values[sourceKey];
|
||||
if (password === undefined || password.length === 0) {
|
||||
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: true, sourceFingerprint: null, passwordHash: null, error: "secret-key-missing" };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
sourceRef,
|
||||
sourceKey,
|
||||
sourcePath,
|
||||
sourcePresent: true,
|
||||
sourceFingerprint: `sha256:${createHash("sha256").update(password).digest("hex").slice(0, 16)}`,
|
||||
passwordHash: hwlabPasswordHash(password),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function parseEnvFile(text: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const rawLine of text.split(/\r?\n/u)) {
|
||||
@@ -3280,6 +3362,7 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
|
||||
const namespace = `hwlab-${input.lane}`;
|
||||
const runtimeLaneSpec = isHwlabRuntimeLane(input.lane) ? hwlabRuntimeLaneSpecForNode(input.lane, input.node) : undefined;
|
||||
const externalPostgres = runtimeLaneSpec?.externalPostgres;
|
||||
const bootstrapAdmin = runtimeLaneSpec?.bootstrapAdmin;
|
||||
const platformDb = externalPostgres !== undefined || /^v0*[3-9]\d*$/.test(input.lane);
|
||||
const platformPostgresService = externalPostgres?.serviceName ?? "g14-platform-postgres";
|
||||
const legacyPostgresHost = `${namespace}-postgres.${namespace}.svc.cluster.local`;
|
||||
@@ -3307,8 +3390,13 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
|
||||
openFgaDbUser,
|
||||
openFgaDbHost: platformDb ? platformPostgresHost : legacyPostgresHost,
|
||||
masterAdminApiKeySecret: `${namespace}-master-server-admin-api-key`,
|
||||
bootstrapAdminSecret: `${namespace}-bootstrap-admin`,
|
||||
bootstrapAdminPasswordHashKey: BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
|
||||
bootstrapAdminSecret: bootstrapAdmin?.secretName ?? `${namespace}-bootstrap-admin`,
|
||||
bootstrapAdminPasswordHashKey: bootstrapAdmin?.secretKey ?? BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
|
||||
bootstrapAdminUsername: bootstrapAdmin?.username ?? "admin",
|
||||
bootstrapAdminDisplayName: bootstrapAdmin?.displayName ?? `HWLAB ${input.lane} Admin`,
|
||||
...(bootstrapAdmin?.passwordSourceRef === undefined ? {} : { bootstrapAdminPasswordSourceRef: bootstrapAdmin.passwordSourceRef }),
|
||||
...(bootstrapAdmin?.passwordSourceKey === undefined ? {} : { bootstrapAdminPasswordSourceKey: bootstrapAdmin.passwordSourceKey }),
|
||||
...(bootstrapAdmin?.passwordHashTransform === undefined ? {} : { bootstrapAdminPasswordHashTransform: bootstrapAdmin.passwordHashTransform }),
|
||||
bootstrapAdminSourceNamespace: BOOTSTRAP_ADMIN_SOURCE_NAMESPACE,
|
||||
bootstrapAdminSourceSecret: BOOTSTRAP_ADMIN_SOURCE_SECRET,
|
||||
cloudApiDbSecret: externalPostgres?.cloudApi.secretName ?? `hwlab-cloud-api-${input.lane}-db`,
|
||||
@@ -3333,15 +3421,18 @@ function runNodeSecret(options: NodeSecretOptions): Record<string, unknown> {
|
||||
if (spec.externalPostgres !== undefined && options.action === "ensure" && (options.preset === "cloud-api-db" || options.preset === "openfga")) {
|
||||
return runExternalPostgresSecretEnsure(options, spec);
|
||||
}
|
||||
const bootstrapAdminMaterial = options.preset === "bootstrap-admin" ? readBootstrapAdminSecretMaterial(spec) : null;
|
||||
const input = options.preset === "master-server-admin-api-key" && options.action === "ensure" && !options.dryRun
|
||||
? readMasterAdminApiKey().key
|
||||
: options.preset === "bootstrap-admin" && options.action === "ensure" && !options.dryRun && bootstrapAdminMaterial?.ok === true
|
||||
? bootstrapAdminMaterial.passwordHash ?? ""
|
||||
: "";
|
||||
const script = options.preset === "openfga"
|
||||
? spec.platformDb ? platformDbSecretStatusScript(options, spec) : openFgaSecretScript(options, spec)
|
||||
: options.preset === "master-server-admin-api-key"
|
||||
? masterAdminApiKeySecretScript(options, spec)
|
||||
: options.preset === "bootstrap-admin"
|
||||
? bootstrapAdminSecretScript(options, spec)
|
||||
? bootstrapAdminSecretScript(options, spec, bootstrapAdminMaterial)
|
||||
: options.preset === "cloud-api-db"
|
||||
? spec.platformDb ? platformDbSecretStatusScript(options, spec) : cloudApiDbSecretScript(options, spec)
|
||||
: options.preset === "owned-postgres-cleanup"
|
||||
@@ -4823,7 +4914,146 @@ function masterAdminApiKeySecretScript(options: NodeSecretOptions, spec: Runtime
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
|
||||
function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec, material: BootstrapAdminSecretMaterial | null): string {
|
||||
const yamlSourceEnabled = spec.bootstrapAdminPasswordSourceRef !== undefined && spec.bootstrapAdminPasswordSourceKey !== undefined && spec.bootstrapAdminPasswordHashTransform !== undefined;
|
||||
if (!yamlSourceEnabled) return legacyBootstrapAdminSecretScript(options, spec);
|
||||
const materialOk = material?.ok === true;
|
||||
return [
|
||||
"set +e",
|
||||
`namespace=${shellQuote(spec.namespace)}`,
|
||||
`name=${shellQuote(spec.bootstrapAdminSecret)}`,
|
||||
`password_hash_key=${shellQuote(spec.bootstrapAdminPasswordHashKey)}`,
|
||||
`username=${shellQuote(spec.bootstrapAdminUsername)}`,
|
||||
`display_name=${shellQuote(spec.bootstrapAdminDisplayName)}`,
|
||||
`source_ref=${shellQuote(spec.bootstrapAdminPasswordSourceRef ?? "")}`,
|
||||
`source_key=${shellQuote(spec.bootstrapAdminPasswordSourceKey ?? "")}`,
|
||||
`source_path=${shellQuote(material?.sourcePath === null || material?.sourcePath === undefined ? "" : displayRepoPath(material.sourcePath))}`,
|
||||
`source_present=${shellQuote(material?.sourcePresent === true ? "yes" : "no")}`,
|
||||
`source_fingerprint=${shellQuote(material?.sourceFingerprint ?? "")}`,
|
||||
`source_error=${shellQuote(material?.error ?? "")}`,
|
||||
`transform=${shellQuote(spec.bootstrapAdminPasswordHashTransform ?? "")}`,
|
||||
`material_ok=${shellQuote(materialOk ? "true" : "false")}`,
|
||||
`cloud_api_deployment=${shellQuote(spec.cloudApiDeployment)}`,
|
||||
`action_request=${shellQuote(options.action)}`,
|
||||
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
|
||||
`field_manager=${shellQuote(spec.fieldManager)}`,
|
||||
"preset=bootstrap-admin",
|
||||
"secret_exists_flag() { kubectl -n \"$namespace\" get secret \"$name\" >/dev/null 2>&1 && printf yes || printf no; }",
|
||||
"secret_b64_key() { kubectl -n \"$namespace\" get secret \"$name\" -o \"go-template={{ index .data \\\"$1\\\" }}\" 2>/dev/null || true; }",
|
||||
"secret_annotation() { kubectl -n \"$namespace\" get secret \"$name\" -o \"go-template={{ with .metadata.annotations }}{{ index . \\\"$1\\\" }}{{ end }}\" 2>/dev/null || true; }",
|
||||
"decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }",
|
||||
"before_exists=$(secret_exists_flag)",
|
||||
"before_hash_b64=$(secret_b64_key \"$password_hash_key\")",
|
||||
"before_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-ref)",
|
||||
"before_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-key)",
|
||||
"before_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-fingerprint)",
|
||||
"before_username=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username)",
|
||||
"before_hash_present=$([ -n \"$before_hash_b64\" ] && printf yes || printf no)",
|
||||
"before_hash_bytes=$(decoded_length \"$before_hash_b64\")",
|
||||
"action=observed",
|
||||
"mutation=false",
|
||||
"apply_exit=",
|
||||
"rollout_restart_exit=",
|
||||
"rollout_status_exit=",
|
||||
"if [ \"$action_request\" = ensure ]; then",
|
||||
" needs_sync=false",
|
||||
" [ \"$before_exists\" = yes ] && [ \"$before_hash_bytes\" -gt 0 ] || needs_sync=true",
|
||||
" [ \"$before_source_ref\" = \"$source_ref\" ] && [ \"$before_source_key\" = \"$source_key\" ] && [ \"$before_source_fingerprint\" = \"$source_fingerprint\" ] && [ \"$before_username\" = \"$username\" ] || needs_sync=true",
|
||||
" if [ \"$material_ok\" != true ]; then",
|
||||
" action=${source_error:-secret-source-invalid}",
|
||||
" apply_exit=44",
|
||||
" elif [ \"$dry_run\" = true ]; then",
|
||||
" if [ \"$needs_sync\" = true ]; then action=would-sync-from-yaml-source; else action=kept; fi",
|
||||
" elif [ \"$needs_sync\" = false ]; then",
|
||||
" action=kept",
|
||||
" else",
|
||||
" password_hash=$(cat)",
|
||||
" case \"$password_hash\" in sha256:*:*) ;; *) action=password-hash-invalid; apply_exit=45 ;; esac",
|
||||
" if [ -z \"$apply_exit\" ]; then",
|
||||
" cat <<EOF_SECRET | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -",
|
||||
"apiVersion: v1",
|
||||
"kind: Secret",
|
||||
"metadata:",
|
||||
" name: $name",
|
||||
" namespace: $namespace",
|
||||
" annotations:",
|
||||
" hwlab.pikastech.local/bootstrap-admin-username: \"$username\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-display-name: \"$display_name\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-ref: \"$source_ref\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-key: \"$source_key\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-fingerprint: \"$source_fingerprint\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-password-transform: \"$transform\"",
|
||||
" labels:",
|
||||
" app.kubernetes.io/part-of: hwlab",
|
||||
" hwlab.pikastech.local/secret-preset: bootstrap-admin",
|
||||
"type: Opaque",
|
||||
"stringData:",
|
||||
" $password_hash_key: \"$password_hash\"",
|
||||
"EOF_SECRET",
|
||||
" apply_exit=$?",
|
||||
" fi",
|
||||
" if [ \"$apply_exit\" -eq 0 ]; then",
|
||||
" kubectl -n \"$namespace\" rollout restart \"deployment/$cloud_api_deployment\" >/tmp/hwlab-bootstrap-admin-rollout-restart.out 2>/tmp/hwlab-bootstrap-admin-rollout-restart.err",
|
||||
" rollout_restart_exit=$?",
|
||||
" if [ \"$rollout_restart_exit\" -eq 0 ]; then",
|
||||
" kubectl -n \"$namespace\" rollout status \"deployment/$cloud_api_deployment\" --timeout=180s >/tmp/hwlab-bootstrap-admin-rollout-status.out 2>/tmp/hwlab-bootstrap-admin-rollout-status.err",
|
||||
" rollout_status_exit=$?",
|
||||
" fi",
|
||||
" if [ -n \"$rollout_restart_exit\" ] && [ \"$rollout_restart_exit\" != 0 ]; then action=rollout-restart-failed",
|
||||
" elif [ -n \"$rollout_status_exit\" ] && [ \"$rollout_status_exit\" != 0 ]; then action=rollout-status-failed",
|
||||
" else action=synced-from-yaml-source; mutation=true; fi",
|
||||
" else action=apply-failed; fi",
|
||||
" password_hash=",
|
||||
" fi",
|
||||
"fi",
|
||||
"after_exists=$(secret_exists_flag)",
|
||||
"after_hash_b64=$(secret_b64_key \"$password_hash_key\")",
|
||||
"after_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-ref)",
|
||||
"after_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-key)",
|
||||
"after_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-fingerprint)",
|
||||
"after_username=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username)",
|
||||
"after_hash_present=$([ -n \"$after_hash_b64\" ] && printf yes || printf no)",
|
||||
"after_hash_bytes=$(decoded_length \"$after_hash_b64\")",
|
||||
"printf 'namespace\\t%s\\n' \"$namespace\"",
|
||||
"printf 'secret\\t%s\\n' \"$name\"",
|
||||
"printf 'key\\t%s\\n' \"$password_hash_key\"",
|
||||
"printf 'preset\\t%s\\n' \"$preset\"",
|
||||
"printf 'username\\t%s\\n' \"$username\"",
|
||||
"printf 'displayName\\t%s\\n' \"$display_name\"",
|
||||
"printf 'sourceRef\\t%s\\n' \"$source_ref\"",
|
||||
"printf 'sourceKey\\t%s\\n' \"$source_key\"",
|
||||
"printf 'sourcePath\\t%s\\n' \"$source_path\"",
|
||||
"printf 'sourceExists\\t%s\\n' \"$source_present\"",
|
||||
"printf 'sourceFingerprint\\t%s\\n' \"$source_fingerprint\"",
|
||||
"printf 'passwordHashTransform\\t%s\\n' \"$transform\"",
|
||||
"printf 'action\\t%s\\n' \"$action\"",
|
||||
"printf 'dryRun\\t%s\\n' \"$dry_run\"",
|
||||
"printf 'mutation\\t%s\\n' \"$mutation\"",
|
||||
"printf 'beforeExists\\t%s\\n' \"$before_exists\"",
|
||||
"printf 'beforePasswordHashPresent\\t%s\\n' \"$before_hash_present\"",
|
||||
"printf 'beforePasswordHashBytes\\t%s\\n' \"$before_hash_bytes\"",
|
||||
"printf 'beforeSourceRef\\t%s\\n' \"$before_source_ref\"",
|
||||
"printf 'beforeSourceKey\\t%s\\n' \"$before_source_key\"",
|
||||
"printf 'beforeSourceFingerprint\\t%s\\n' \"$before_source_fingerprint\"",
|
||||
"printf 'beforeUsername\\t%s\\n' \"$before_username\"",
|
||||
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
|
||||
"printf 'afterPasswordHashPresent\\t%s\\n' \"$after_hash_present\"",
|
||||
"printf 'afterPasswordHashBytes\\t%s\\n' \"$after_hash_bytes\"",
|
||||
"printf 'afterSourceRef\\t%s\\n' \"$after_source_ref\"",
|
||||
"printf 'afterSourceKey\\t%s\\n' \"$after_source_key\"",
|
||||
"printf 'afterSourceFingerprint\\t%s\\n' \"$after_source_fingerprint\"",
|
||||
"printf 'afterUsername\\t%s\\n' \"$after_username\"",
|
||||
"printf 'cloudApiDeployment\\t%s\\n' \"$cloud_api_deployment\"",
|
||||
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
|
||||
"printf 'rolloutRestartExitCode\\t%s\\n' \"$rollout_restart_exit\"",
|
||||
"printf 'rolloutStatusExitCode\\t%s\\n' \"$rollout_status_exit\"",
|
||||
"if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi",
|
||||
"if [ -n \"$rollout_restart_exit\" ] && [ \"$rollout_restart_exit\" != 0 ]; then exit \"$rollout_restart_exit\"; fi",
|
||||
"if [ -n \"$rollout_status_exit\" ] && [ \"$rollout_status_exit\" != 0 ]; then exit \"$rollout_status_exit\"; fi",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function legacyBootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
`namespace=${shellQuote(spec.namespace)}`,
|
||||
@@ -5279,31 +5509,68 @@ function secretStatusFromText(text: string, commandOk: boolean, exitCode: number
|
||||
const beforeHashBytes = numericField(fields.beforePasswordHashBytes);
|
||||
const sourceHashBytes = numericField(fields.sourcePasswordHashBytes);
|
||||
const afterHashBytes = numericField(fields.afterPasswordHashBytes);
|
||||
const healthy = fields.afterExists === "yes" &&
|
||||
const yamlSourceMode = typeof fields.sourceRef === "string" && fields.sourceRef.length > 0;
|
||||
const targetHashReady = fields.afterExists === "yes" &&
|
||||
fields.afterPasswordHashPresent === "yes" &&
|
||||
typeof afterHashBytes === "number" && afterHashBytes > 0;
|
||||
const yamlSourceReady = !yamlSourceMode || (
|
||||
fields.sourceExists === "yes" &&
|
||||
typeof fields.sourceFingerprint === "string" &&
|
||||
fields.sourceFingerprint.length > 0 &&
|
||||
fields.afterSourceRef === fields.sourceRef &&
|
||||
fields.afterSourceKey === fields.sourceKey &&
|
||||
fields.afterSourceFingerprint === fields.sourceFingerprint &&
|
||||
fields.afterUsername === fields.username
|
||||
);
|
||||
const healthy = targetHashReady && yamlSourceReady;
|
||||
return {
|
||||
ok: commandOk && healthy,
|
||||
namespace: fields.namespace || spec.namespace,
|
||||
secret: fields.secret || spec.bootstrapAdminSecret,
|
||||
key: fields.key || spec.bootstrapAdminPasswordHashKey,
|
||||
preset: "bootstrap-admin",
|
||||
source: {
|
||||
namespace: fields.sourceNamespace || spec.bootstrapAdminSourceNamespace,
|
||||
secret: fields.sourceSecret || spec.bootstrapAdminSourceSecret,
|
||||
exists: fields.sourceExists === "yes",
|
||||
passwordHash: { keyPresent: fields.sourcePasswordHashPresent === "yes", valueBytes: sourceHashBytes },
|
||||
account: {
|
||||
username: fields.username || spec.bootstrapAdminUsername,
|
||||
displayName: fields.displayName || spec.bootstrapAdminDisplayName,
|
||||
},
|
||||
source: yamlSourceMode
|
||||
? {
|
||||
sourceRef: fields.sourceRef,
|
||||
sourceKey: fields.sourceKey || null,
|
||||
sourcePath: fields.sourcePath || null,
|
||||
exists: fields.sourceExists === "yes",
|
||||
fingerprint: fields.sourceFingerprint || null,
|
||||
passwordHashTransform: fields.passwordHashTransform || spec.bootstrapAdminPasswordHashTransform || null,
|
||||
valuesRedacted: true,
|
||||
}
|
||||
: {
|
||||
namespace: fields.sourceNamespace || spec.bootstrapAdminSourceNamespace,
|
||||
secret: fields.sourceSecret || spec.bootstrapAdminSourceSecret,
|
||||
exists: fields.sourceExists === "yes",
|
||||
passwordHash: { keyPresent: fields.sourcePasswordHashPresent === "yes", valueBytes: sourceHashBytes },
|
||||
},
|
||||
action: fields.action || null,
|
||||
dryRun: fields.dryRun === "true",
|
||||
mutation: fields.mutation === "true",
|
||||
before: {
|
||||
exists: fields.beforeExists === "yes",
|
||||
passwordHash: { keyPresent: fields.beforePasswordHashPresent === "yes", valueBytes: beforeHashBytes },
|
||||
...(yamlSourceMode ? {
|
||||
sourceRef: fields.beforeSourceRef || null,
|
||||
sourceKey: fields.beforeSourceKey || null,
|
||||
sourceFingerprint: fields.beforeSourceFingerprint || null,
|
||||
username: fields.beforeUsername || null,
|
||||
} : {}),
|
||||
},
|
||||
after: {
|
||||
exists: fields.afterExists === "yes",
|
||||
passwordHash: { keyPresent: fields.afterPasswordHashPresent === "yes", valueBytes: afterHashBytes },
|
||||
...(yamlSourceMode ? {
|
||||
sourceRef: fields.afterSourceRef || null,
|
||||
sourceKey: fields.afterSourceKey || null,
|
||||
sourceFingerprint: fields.afterSourceFingerprint || null,
|
||||
username: fields.afterUsername || null,
|
||||
} : {}),
|
||||
},
|
||||
cloudApiDeployment: fields.cloudApiDeployment || spec.cloudApiDeployment,
|
||||
applyExitCode: numericField(fields.applyExitCode),
|
||||
|
||||
Reference in New Issue
Block a user