fix: support HWLAB bootstrap admin secret
This commit is contained in:
+178
-8
@@ -6,7 +6,7 @@ import { runHwlabG14Command } from "./hwlab-g14";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane } from "./hwlab-node-lanes";
|
||||
|
||||
type SecretAction = "status" | "ensure" | "cleanup-owned-postgres" | "cleanup-obsolete";
|
||||
type SecretPreset = "openfga" | "master-server-admin-api-key" | "code-agent-provider" | "cloud-api-db" | "owned-postgres-cleanup" | "obsolete-secret-cleanup";
|
||||
type SecretPreset = "openfga" | "master-server-admin-api-key" | "bootstrap-admin" | "code-agent-provider" | "cloud-api-db" | "owned-postgres-cleanup" | "obsolete-secret-cleanup";
|
||||
type DelegatedNodeDomain = "control-plane" | "git-mirror";
|
||||
|
||||
interface NodeSecretOptions {
|
||||
@@ -35,6 +35,10 @@ interface RuntimeSecretSpec {
|
||||
openFgaDbUser: string;
|
||||
openFgaDbHost: string;
|
||||
masterAdminApiKeySecret: string;
|
||||
bootstrapAdminSecret: string;
|
||||
bootstrapAdminPasswordHashKey: string;
|
||||
bootstrapAdminSourceNamespace: string;
|
||||
bootstrapAdminSourceSecret: string;
|
||||
cloudApiDbSecret: string;
|
||||
cloudApiDbKey: string;
|
||||
cloudApiDbName: string;
|
||||
@@ -52,6 +56,9 @@ interface RuntimeSecretSpec {
|
||||
|
||||
const MASTER_ADMIN_API_KEY_ENV = "/root/.config/hwlab-v02/master-server-admin-api-key.env";
|
||||
const MASTER_ADMIN_API_KEY_KEY = "api-key";
|
||||
const BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY = "password-hash";
|
||||
const BOOTSTRAP_ADMIN_SOURCE_NAMESPACE = "hwlab-v02";
|
||||
const BOOTSTRAP_ADMIN_SOURCE_SECRET = "hwlab-v02-bootstrap-admin";
|
||||
const OPENFGA_AUTHN_KEY = "authn-preshared-key";
|
||||
const OPENFGA_DATASTORE_URI_KEY = "datastore-uri";
|
||||
const OPENFGA_POSTGRES_PASSWORD_KEY = "postgres-password";
|
||||
@@ -89,6 +96,8 @@ export function hwlabNodeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes git-mirror status --node G14 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-openfga",
|
||||
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-master-server-admin-api-key --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-bootstrap-admin",
|
||||
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-bootstrap-admin --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-cloud-api-v03-db",
|
||||
"bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --confirm",
|
||||
@@ -215,7 +224,7 @@ function rewriteDelegatedNodeString(value: string, scoped: ReturnType<typeof par
|
||||
function parseSecretOptions(args: string[]): NodeSecretOptions {
|
||||
const [actionRaw] = args;
|
||||
if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "cleanup-owned-postgres" && actionRaw !== "cleanup-obsolete") {
|
||||
throw new Error("secret usage: status|ensure --node NODE --lane vNN --name hwlab-vNN-openfga|hwlab-vNN-master-server-admin-api-key|hwlab-cloud-api-vNN-db|hwlab-vNN-code-agent-provider [--dry-run|--confirm] | cleanup-owned-postgres --node NODE --lane vNN [--dry-run|--confirm] | cleanup-obsolete --node NODE --lane vNN --name hwpod-vNN-db [--dry-run|--confirm]");
|
||||
throw new Error("secret usage: status|ensure --node NODE --lane vNN --name hwlab-vNN-openfga|hwlab-vNN-master-server-admin-api-key|hwlab-vNN-bootstrap-admin|hwlab-cloud-api-vNN-db|hwlab-vNN-code-agent-provider [--dry-run|--confirm] | cleanup-owned-postgres --node NODE --lane vNN [--dry-run|--confirm] | cleanup-obsolete --node NODE --lane vNN --name hwpod-vNN-db [--dry-run|--confirm]");
|
||||
}
|
||||
const node = requiredOption(args, "--node");
|
||||
assertNodeId(node);
|
||||
@@ -271,6 +280,20 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 900),
|
||||
};
|
||||
}
|
||||
if (name === spec.bootstrapAdminSecret) {
|
||||
if (key !== undefined && key !== BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY) throw new Error(`secret ${name} supports only key ${BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY}`);
|
||||
return {
|
||||
action: actionRaw,
|
||||
node,
|
||||
lane,
|
||||
name,
|
||||
key: key ?? BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
|
||||
preset: "bootstrap-admin",
|
||||
confirm,
|
||||
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 900),
|
||||
};
|
||||
}
|
||||
if (name === spec.codeAgentProviderSecret) {
|
||||
if (key !== undefined && key !== CODE_AGENT_PROVIDER_OPENAI_KEY && key !== CODE_AGENT_PROVIDER_OPENCODE_KEY) {
|
||||
throw new Error(`secret ${name} supports keys ${CODE_AGENT_PROVIDER_OPENAI_KEY} and ${CODE_AGENT_PROVIDER_OPENCODE_KEY}`);
|
||||
@@ -305,7 +328,7 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
|
||||
};
|
||||
}
|
||||
if (name !== spec.openFgaSecret) {
|
||||
throw new Error(`secret status/ensure supports --name ${spec.openFgaSecret}, ${spec.masterAdminApiKeySecret}, ${spec.cloudApiDbSecret}, or ${spec.codeAgentProviderSecret} for --lane ${lane}; obsolete ${spec.obsoleteHwpodDbSecret} must use cleanup-obsolete`);
|
||||
throw new Error(`secret status/ensure supports --name ${spec.openFgaSecret}, ${spec.masterAdminApiKeySecret}, ${spec.bootstrapAdminSecret}, ${spec.cloudApiDbSecret}, or ${spec.codeAgentProviderSecret} for --lane ${lane}; obsolete ${spec.obsoleteHwpodDbSecret} must use cleanup-obsolete`);
|
||||
}
|
||||
if (key !== undefined && key !== OPENFGA_AUTHN_KEY && key !== OPENFGA_DATASTORE_URI_KEY && key !== OPENFGA_POSTGRES_PASSWORD_KEY) {
|
||||
throw new Error(`secret ${name} supports keys ${OPENFGA_AUTHN_KEY}, ${OPENFGA_DATASTORE_URI_KEY}, and ${OPENFGA_POSTGRES_PASSWORD_KEY}`);
|
||||
@@ -346,6 +369,10 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
|
||||
openFgaDbUser: platformDb ? `openfga_${input.lane}_app` : "hwlab_openfga",
|
||||
openFgaDbHost: platformDb ? platformPostgresHost : legacyPostgresHost,
|
||||
masterAdminApiKeySecret: `${namespace}-master-server-admin-api-key`,
|
||||
bootstrapAdminSecret: `${namespace}-bootstrap-admin`,
|
||||
bootstrapAdminPasswordHashKey: BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
|
||||
bootstrapAdminSourceNamespace: BOOTSTRAP_ADMIN_SOURCE_NAMESPACE,
|
||||
bootstrapAdminSourceSecret: BOOTSTRAP_ADMIN_SOURCE_SECRET,
|
||||
cloudApiDbSecret: `hwlab-cloud-api-${input.lane}-db`,
|
||||
cloudApiDbKey: CLOUD_API_DB_KEY,
|
||||
cloudApiDbName: `hwlab_${input.lane}`,
|
||||
@@ -372,11 +399,13 @@ function runNodeSecret(options: NodeSecretOptions): Record<string, unknown> {
|
||||
? spec.platformDb ? platformDbSecretStatusScript(options, spec) : openFgaSecretScript(options, spec)
|
||||
: options.preset === "master-server-admin-api-key"
|
||||
? masterAdminApiKeySecretScript(options, spec)
|
||||
: options.preset === "cloud-api-db"
|
||||
? spec.platformDb ? platformDbSecretStatusScript(options, spec) : cloudApiDbSecretScript(options, spec)
|
||||
: options.preset === "owned-postgres-cleanup"
|
||||
? ownedPostgresCleanupScript(options, spec)
|
||||
: codeAgentProviderSecretScript(options, spec);
|
||||
: options.preset === "bootstrap-admin"
|
||||
? bootstrapAdminSecretScript(options, spec)
|
||||
: options.preset === "cloud-api-db"
|
||||
? spec.platformDb ? platformDbSecretStatusScript(options, spec) : cloudApiDbSecretScript(options, spec)
|
||||
: options.preset === "owned-postgres-cleanup"
|
||||
? ownedPostgresCleanupScript(options, spec)
|
||||
: codeAgentProviderSecretScript(options, spec);
|
||||
const result = runTransScript(options.node, script, input, options.timeoutSeconds);
|
||||
const status = secretStatusFromText(statusText(result), result.exitCode === 0, result.exitCode, result.stderr, spec);
|
||||
const dryRunOk = options.action === "ensure" && options.dryRun && result.exitCode === 0;
|
||||
@@ -1282,6 +1311,107 @@ function masterAdminApiKeySecretScript(options: NodeSecretOptions, spec: Runtime
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
`namespace=${shellQuote(spec.namespace)}`,
|
||||
`name=${shellQuote(spec.bootstrapAdminSecret)}`,
|
||||
`source_namespace=${shellQuote(spec.bootstrapAdminSourceNamespace)}`,
|
||||
`source_name=${shellQuote(spec.bootstrapAdminSourceSecret)}`,
|
||||
`password_hash_key=${shellQuote(spec.bootstrapAdminPasswordHashKey)}`,
|
||||
`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 \"$1\" get secret \"$2\" >/dev/null 2>&1 && printf yes || printf no; }",
|
||||
"secret_b64_key() { kubectl -n \"$1\" get secret \"$2\" -o \"go-template={{ index .data \\\"$3\\\" }}\" 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 \"$namespace\" \"$name\")",
|
||||
"before_hash_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$password_hash_key\")",
|
||||
"source_exists=$(secret_exists_flag \"$source_namespace\" \"$source_name\")",
|
||||
"source_hash_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$password_hash_key\")",
|
||||
"before_hash_present=$([ -n \"$before_hash_b64\" ] && printf yes || printf no)",
|
||||
"source_hash_present=$([ -n \"$source_hash_b64\" ] && printf yes || printf no)",
|
||||
"before_hash_bytes=$(decoded_length \"$before_hash_b64\")",
|
||||
"source_hash_bytes=$(decoded_length \"$source_hash_b64\")",
|
||||
"action=observed",
|
||||
"mutation=false",
|
||||
"apply_exit=",
|
||||
"rollout_restart_exit=",
|
||||
"rollout_status_exit=",
|
||||
"if [ \"$action_request\" = ensure ]; then",
|
||||
" missing_target=false",
|
||||
" [ \"$before_exists\" = yes ] && [ \"$before_hash_bytes\" -gt 0 ] || missing_target=true",
|
||||
" missing_source=false",
|
||||
" [ \"$source_exists\" = yes ] && [ \"$source_hash_bytes\" -gt 0 ] || missing_source=true",
|
||||
" if [ \"$missing_source\" = true ]; then",
|
||||
" action=source-missing-password-hash",
|
||||
" apply_exit=44",
|
||||
" elif [ \"$dry_run\" = true ]; then",
|
||||
" if [ \"$missing_target\" = true ]; then action=would-copy-from-source; else action=kept; fi",
|
||||
" elif [ \"$missing_target\" = false ]; then",
|
||||
" action=kept",
|
||||
" else",
|
||||
" cat <<EOF_SECRET | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -",
|
||||
"apiVersion: v1",
|
||||
"kind: Secret",
|
||||
"metadata:",
|
||||
" name: $name",
|
||||
" namespace: $namespace",
|
||||
" labels:",
|
||||
" app.kubernetes.io/part-of: hwlab",
|
||||
" hwlab.pikastech.local/secret-preset: bootstrap-admin",
|
||||
"type: Opaque",
|
||||
"data:",
|
||||
" password-hash: $source_hash_b64",
|
||||
"EOF_SECRET",
|
||||
" apply_exit=$?",
|
||||
" 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=copied-from-source; mutation=true; fi",
|
||||
" else action=apply-failed; fi",
|
||||
" fi",
|
||||
"fi",
|
||||
"after_exists=$(secret_exists_flag \"$namespace\" \"$name\")",
|
||||
"after_hash_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$password_hash_key\")",
|
||||
"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 'sourceNamespace\\t%s\\n' \"$source_namespace\"",
|
||||
"printf 'sourceSecret\\t%s\\n' \"$source_name\"",
|
||||
"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 'sourceExists\\t%s\\n' \"$source_exists\"",
|
||||
"printf 'sourcePasswordHashPresent\\t%s\\n' \"$source_hash_present\"",
|
||||
"printf 'sourcePasswordHashBytes\\t%s\\n' \"$source_hash_bytes\"",
|
||||
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
|
||||
"printf 'afterPasswordHashPresent\\t%s\\n' \"$after_hash_present\"",
|
||||
"printf 'afterPasswordHashBytes\\t%s\\n' \"$after_hash_bytes\"",
|
||||
"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 codeAgentProviderSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
@@ -1633,6 +1763,46 @@ function secretStatusFromText(text: string, commandOk: boolean, exitCode: number
|
||||
summary: healthy ? `${fields.secret || spec.masterAdminApiKeySecret}/${MASTER_ADMIN_API_KEY_KEY} exists` : `${fields.secret || spec.masterAdminApiKeySecret}/${MASTER_ADMIN_API_KEY_KEY} missing`,
|
||||
};
|
||||
}
|
||||
if (fields.preset === "bootstrap-admin") {
|
||||
const beforeHashBytes = numericField(fields.beforePasswordHashBytes);
|
||||
const sourceHashBytes = numericField(fields.sourcePasswordHashBytes);
|
||||
const afterHashBytes = numericField(fields.afterPasswordHashBytes);
|
||||
const healthy = fields.afterExists === "yes" &&
|
||||
fields.afterPasswordHashPresent === "yes" &&
|
||||
typeof afterHashBytes === "number" && afterHashBytes > 0;
|
||||
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 },
|
||||
},
|
||||
action: fields.action || null,
|
||||
dryRun: fields.dryRun === "true",
|
||||
mutation: fields.mutation === "true",
|
||||
before: {
|
||||
exists: fields.beforeExists === "yes",
|
||||
passwordHash: { keyPresent: fields.beforePasswordHashPresent === "yes", valueBytes: beforeHashBytes },
|
||||
},
|
||||
after: {
|
||||
exists: fields.afterExists === "yes",
|
||||
passwordHash: { keyPresent: fields.afterPasswordHashPresent === "yes", valueBytes: afterHashBytes },
|
||||
},
|
||||
cloudApiDeployment: fields.cloudApiDeployment || spec.cloudApiDeployment,
|
||||
applyExitCode: numericField(fields.applyExitCode),
|
||||
rolloutRestartExitCode: numericField(fields.rolloutRestartExitCode),
|
||||
rolloutStatusExitCode: numericField(fields.rolloutStatusExitCode),
|
||||
exitCode,
|
||||
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
|
||||
valuesRedacted: true,
|
||||
summary: healthy ? `${fields.secret || spec.bootstrapAdminSecret}/${spec.bootstrapAdminPasswordHashKey} exists` : `${fields.secret || spec.bootstrapAdminSecret}/${spec.bootstrapAdminPasswordHashKey} missing`,
|
||||
};
|
||||
}
|
||||
if (fields.preset === "code-agent-provider") {
|
||||
const beforeOpenaiBytes = numericField(fields.beforeOpenaiBytes);
|
||||
const beforeOpencodeBytes = numericField(fields.beforeOpencodeBytes);
|
||||
|
||||
Reference in New Issue
Block a user