fix: bootstrap hwlab v02 admin api key secret

This commit is contained in:
Codex
2026-06-05 13:55:38 +00:00
parent 171325f48d
commit cb0f508dc7
4 changed files with 213 additions and 12 deletions
+2 -1
View File
@@ -11,11 +11,12 @@ export interface CommandResult {
timedOut: boolean;
}
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): CommandResult {
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv; input?: string } = {}): CommandResult {
const result = spawnSync(command[0], command.slice(1), {
cwd,
encoding: "utf8",
env: options.env,
input: options.input,
maxBuffer: 1024 * 1024 * 8,
timeout: options.timeoutMs,
});
+206 -8
View File
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { createHash } from "node:crypto";
import { createHash, randomBytes } from "node:crypto";
import { repoRoot, rootPath, type Config } from "./config";
import { runCommand } from "./command";
import { readJob, startJob } from "./jobs";
@@ -36,6 +36,9 @@ const V02_OPENFGA_DATASTORE_URI_SECRET_KEY = "datastore-uri";
const V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY = "postgres-password";
const V02_OPENFGA_DB_NAME = "hwlab_openfga";
const V02_OPENFGA_DB_USER = "hwlab_openfga";
const V02_MASTER_ADMIN_API_KEY_SECRET = "hwlab-v02-master-server-admin-api-key";
const V02_MASTER_ADMIN_API_KEY_SECRET_KEY = "api-key";
const V02_MASTER_ADMIN_API_KEY_LOCAL_ENV = "/root/.config/hwlab-v02/master-server-admin-api-key.env";
const V02_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab";
const V02_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const GIT_MIRROR_NAMESPACE = "devops-infra";
@@ -190,8 +193,8 @@ interface G14SecretOptions {
dryRun: boolean;
confirm: boolean;
name: string;
key?: typeof V02_OPENFGA_AUTHN_SECRET_KEY | typeof V02_OPENFGA_DATASTORE_URI_SECRET_KEY | typeof V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY;
preset: "openfga" | "generic-delete";
key?: string;
preset: "openfga" | "master-server-admin-api-key" | "generic-delete";
timeoutSeconds: number;
}
@@ -503,7 +506,7 @@ function parseObservabilityOptions(args: string[]): G14ObservabilityOptions {
function parseSecretOptions(args: string[]): G14SecretOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "delete") {
throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-openfga [--dry-run|--confirm] | delete --lane v02 --name <obsolete-secret> [--dry-run|--confirm]");
throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-openfga|hwlab-v02-master-server-admin-api-key [--dry-run|--confirm] | delete --lane v02 --name <obsolete-secret> [--dry-run|--confirm]");
}
const lane = optionValue(args, "--lane") ?? "v02";
if (lane !== "v02") throw new Error("secret currently supports --lane v02");
@@ -515,7 +518,7 @@ function parseSecretOptions(args: string[]): G14SecretOptions {
if (actionRaw === "delete") {
if (key !== undefined) throw new Error("secret delete does not accept --key; it deletes a whole obsolete Secret object");
if (!/^hwlab-v02-[a-z0-9-]+$/u.test(name)) throw new Error("secret delete requires a hwlab-v02-* Secret name");
if (name === V02_OPENFGA_SECRET || name === "hwlab-v02-postgres") throw new Error(`secret delete refuses required v0.2 Secret ${name}`);
if (name === V02_OPENFGA_SECRET || name === V02_MASTER_ADMIN_API_KEY_SECRET || name === "hwlab-v02-postgres") throw new Error(`secret delete refuses required v0.2 Secret ${name}`);
return {
action: actionRaw,
lane,
@@ -526,8 +529,21 @@ function parseSecretOptions(args: string[]): G14SecretOptions {
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
};
}
if (name === V02_MASTER_ADMIN_API_KEY_SECRET) {
if (key !== undefined && key !== V02_MASTER_ADMIN_API_KEY_SECRET_KEY) throw new Error(`secret ${V02_MASTER_ADMIN_API_KEY_SECRET} supports only key ${V02_MASTER_ADMIN_API_KEY_SECRET_KEY}`);
return {
action: actionRaw,
lane,
confirm,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
name,
key: key ?? V02_MASTER_ADMIN_API_KEY_SECRET_KEY,
preset: "master-server-admin-api-key",
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
};
}
if (name !== V02_OPENFGA_SECRET) {
throw new Error(`secret status/ensure currently supports only --name ${V02_OPENFGA_SECRET}; use secret delete for obsolete Secret objects`);
throw new Error(`secret status/ensure currently supports only --name ${V02_OPENFGA_SECRET} or ${V02_MASTER_ADMIN_API_KEY_SECRET}; use secret delete for obsolete Secret objects`);
}
if (key !== undefined && key !== V02_OPENFGA_AUTHN_SECRET_KEY && key !== V02_OPENFGA_DATASTORE_URI_SECRET_KEY && key !== V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY) {
throw new Error(`secret ${V02_OPENFGA_SECRET} supports keys ${V02_OPENFGA_AUTHN_SECRET_KEY}, ${V02_OPENFGA_DATASTORE_URI_SECRET_KEY}, and ${V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY}`);
@@ -577,6 +593,26 @@ function commandJson(command: string[], timeoutMs = 60_000): CommandJsonResult {
};
}
function commandJsonWithInput(command: string[], input: string, timeoutMs = 60_000): CommandJsonResult {
const result = runCommand(command, repoRoot, { timeoutMs, input });
let parsed: unknown | null = null;
if (result.stdout.trim().length > 0) {
try {
parsed = JSON.parse(result.stdout) as unknown;
} catch {
parsed = null;
}
}
return {
ok: result.exitCode === 0,
command,
exitCode: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
parsed,
};
}
function cliJson(args: string[], timeoutMs = 60_000): CommandJsonResult {
return commandJson(["bun", "scripts/cli.ts", ...args], timeoutMs);
}
@@ -636,8 +672,59 @@ function textHash(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
function generateHwlabApiKey(): string {
return `hwl_live_${randomBytes(32).toString("base64url")}`;
}
function hwlabApiKeyPrefix(value: string): string {
const text = value.trim();
if (!text.startsWith("hwl_live_")) return "";
const remainder = text.slice("hwl_live_".length);
const dot = remainder.indexOf(".");
const segment = dot === -1 ? remainder : remainder.slice(0, dot);
return `hwl_live_${segment}`.slice(0, 24);
}
function localMasterAdminApiKeyStatus(): Record<string, unknown> {
if (!existsSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV)) {
return { exists: false, path: V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, mode: null, valueBytes: 0, keyPrefix: null };
}
const stat = statSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV);
const content = readFileSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, "utf8");
const match = content.match(/^HWLAB_API_KEY=(.+)$/mu);
const value = match?.[1]?.trim() ?? "";
return {
exists: true,
path: V02_MASTER_ADMIN_API_KEY_LOCAL_ENV,
mode: `0${(stat.mode & 0o777).toString(8)}`,
valueBytes: Buffer.byteLength(value, "utf8"),
keyPrefix: value ? hwlabApiKeyPrefix(value) : null,
validPrefix: value.startsWith("hwl_live_"),
};
}
function readOrCreateLocalMasterAdminApiKey(dryRun: boolean): { key: string | null; created: boolean; status: Record<string, unknown> } {
const existing = localMasterAdminApiKeyStatus();
if (existing.exists === true) {
const content = readFileSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, "utf8");
const match = content.match(/^HWLAB_API_KEY=(.+)$/mu);
const key = match?.[1]?.trim() ?? "";
if (!key.startsWith("hwl_live_")) throw new Error(`${V02_MASTER_ADMIN_API_KEY_LOCAL_ENV} exists but does not contain a hwl_live_ HWLAB_API_KEY`);
if (!dryRun && existing.mode !== "0600") chmodSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, 0o600);
return { key, created: false, status: localMasterAdminApiKeyStatus() };
}
if (dryRun) return { key: null, created: false, status: existing };
const key = generateHwlabApiKey();
mkdirSync(dirname(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV), { recursive: true, mode: 0o700 });
writeFileSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, `# HWLAB v0.2 master server admin API key; do not commit or print.\nHWLAB_API_KEY=${key}\n`, { mode: 0o600 });
return { key, created: true, status: localMasterAdminApiKeyStatus() };
}
function redactLargePayloads(value: string): string {
return value
.replace(/hwl_live_[A-Za-z0-9._:-]{12,}/gu, (payload: string) => {
return `hwl_live_<redacted chars=${payload.length} sha256=${textHash(payload)}>`;
})
.replace(/manifest_b64='([^']{128,})'/gu, (_match, payload: string) => {
return `manifest_b64='<omitted base64 chars=${payload.length} sha256=${textHash(payload)}>'`;
})
@@ -781,6 +868,10 @@ function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
return cliJson(["ssh", `${G14_PROVIDER}:k3s`, ...args], timeoutMs);
}
function g14K3sInlineScriptWithInput(script: string, input: string, timeoutMs = 60_000): CommandJsonResult {
return commandJsonWithInput(["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "script", "--", script], input, timeoutMs);
}
function v02CicdRepoEnsureScript(): string {
return [
`cicd_repo=${shellQuote(V02_CICD_REPO)}`,
@@ -3346,6 +3437,7 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
function v02SecretScript(options: G14SecretOptions): string {
if (options.preset === "generic-delete") return v02DeleteSecretScript(options);
if (options.preset === "master-server-admin-api-key") return v02MasterAdminApiKeySecretScript(options);
return [
v02OpenFgaSecretScript(options),
].join("\n");
@@ -3541,6 +3633,69 @@ function v02OpenFgaSecretScript(options: G14SecretOptions): string {
].join("\n");
}
function v02MasterAdminApiKeySecretScript(options: G14SecretOptions): string {
return [
"set +e",
`namespace=${shellQuote(V02_RUNTIME_NAMESPACE)}`,
`name=${shellQuote(V02_MASTER_ADMIN_API_KEY_SECRET)}`,
`api_key_name=${shellQuote(V02_MASTER_ADMIN_API_KEY_SECRET_KEY)}`,
`action_request=${shellQuote(options.action)}`,
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
`field_manager=${shellQuote(V02_SECRET_FIELD_MANAGER)}`,
"preset=master-server-admin-api-key",
"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; }",
"decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }",
"decoded_prefix() { if [ -n \"$1\" ]; then value=$(printf '%s' \"$1\" | base64 -d 2>/dev/null || true); printf '%s' \"$value\" | cut -c1-24; value=; fi; }",
"before_exists=$(secret_exists_flag)",
"before_api_key_b64=$(secret_b64_key \"$api_key_name\")",
"before_api_key_present=$([ -n \"$before_api_key_b64\" ] && printf yes || printf no)",
"before_api_key_bytes=$(decoded_length \"$before_api_key_b64\")",
"before_api_key_prefix=$(decoded_prefix \"$before_api_key_b64\")",
"action=observed",
"mutation=false",
"apply_exit=",
"if [ \"$action_request\" = ensure ]; then",
" missing_secret=false",
" [ \"$before_api_key_present\" = yes ] && [ \"$before_api_key_bytes\" -gt 0 ] || missing_secret=true",
" if [ \"$dry_run\" = true ]; then",
" if [ \"$missing_secret\" = true ]; then action=would-ensure; else action=kept; fi",
" else",
" api_key=$(cat)",
" case \"$api_key\" in hwl_live_*) ;; *) action=api-key-invalid; apply_exit=43 ;; esac",
" if [ -z \"$apply_exit\" ]; then",
" kubectl -n \"$namespace\" create secret generic \"$name\" --from-literal=\"$api_key_name=$api_key\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -",
" apply_exit=$?",
" if [ \"$apply_exit\" -eq 0 ]; then action=ensured; mutation=true; else action=apply-failed; fi",
" fi",
" api_key=",
" fi",
"fi",
"after_exists=$(secret_exists_flag)",
"after_api_key_b64=$(secret_b64_key \"$api_key_name\")",
"after_api_key_present=$([ -n \"$after_api_key_b64\" ] && printf yes || printf no)",
"after_api_key_bytes=$(decoded_length \"$after_api_key_b64\")",
"after_api_key_prefix=$(decoded_prefix \"$after_api_key_b64\")",
"printf 'namespace\\t%s\\n' \"$namespace\"",
"printf 'secret\\t%s\\n' \"$name\"",
"printf 'key\\t%s\\n' \"$api_key_name\"",
"printf 'preset\\t%s\\n' \"$preset\"",
"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 'beforeApiKeyPresent\\t%s\\n' \"$before_api_key_present\"",
"printf 'beforeApiKeyBytes\\t%s\\n' \"$before_api_key_bytes\"",
"printf 'beforeApiKeyPrefix\\t%s\\n' \"$before_api_key_prefix\"",
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
"printf 'afterApiKeyPresent\\t%s\\n' \"$after_api_key_present\"",
"printf 'afterApiKeyBytes\\t%s\\n' \"$after_api_key_bytes\"",
"printf 'afterApiKeyPrefix\\t%s\\n' \"$after_api_key_prefix\"",
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
"if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi",
].join("\n");
}
function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record<string, unknown> {
const fields = keyValueLinesFromText(text);
if (fields.preset === "generic-delete") {
@@ -3568,6 +3723,39 @@ function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: num
: `${fields.secret || "secret"} still exists`,
};
}
if (fields.preset === "master-server-admin-api-key") {
const afterBytes = numericField(fields.afterApiKeyBytes);
const healthy =
fields.afterExists === "yes" &&
fields.afterApiKeyPresent === "yes" &&
typeof afterBytes === "number" && afterBytes > 0 &&
String(fields.afterApiKeyPrefix ?? "").startsWith("hwl_live_");
return {
ok: commandOk && healthy,
namespace: fields.namespace || V02_RUNTIME_NAMESPACE,
secret: fields.secret || V02_MASTER_ADMIN_API_KEY_SECRET,
key: fields.key || V02_MASTER_ADMIN_API_KEY_SECRET_KEY,
preset: "master-server-admin-api-key",
action: fields.action || null,
dryRun: fields.dryRun === "true",
mutation: fields.mutation === "true",
before: {
exists: fields.beforeExists === "yes",
apiKey: { keyPresent: fields.beforeApiKeyPresent === "yes", valueBytes: numericField(fields.beforeApiKeyBytes), keyPrefix: fields.beforeApiKeyPrefix || null },
},
after: {
exists: fields.afterExists === "yes",
apiKey: { keyPresent: fields.afterApiKeyPresent === "yes", valueBytes: afterBytes, keyPrefix: fields.afterApiKeyPrefix || null },
},
applyExitCode: numericField(fields.applyExitCode),
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
valuesRedacted: true,
summary: healthy
? `${fields.secret || V02_MASTER_ADMIN_API_KEY_SECRET}/${fields.key || V02_MASTER_ADMIN_API_KEY_SECRET_KEY} exists`
: `${fields.secret || V02_MASTER_ADMIN_API_KEY_SECRET}/${fields.key || V02_MASTER_ADMIN_API_KEY_SECRET_KEY} missing`,
};
}
if (fields.preset === "openfga") {
const afterAuthnBytes = numericField(fields.afterAuthnBytes);
const afterUriBytes = numericField(fields.afterDatastoreUriBytes);
@@ -3643,7 +3831,12 @@ function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: num
function runG14Secret(options: G14SecretOptions): Record<string, unknown> {
const script = v02SecretScript(options);
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
const localAdminApiKey = options.preset === "master-server-admin-api-key"
? readOrCreateLocalMasterAdminApiKey(options.action !== "ensure" || options.dryRun)
: null;
const result = options.preset === "master-server-admin-api-key"
? g14K3sInlineScriptWithInput(script, localAdminApiKey?.key ?? "", options.timeoutSeconds * 1000 + 2000)
: g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
const status = v02SecretStatusFromText(statusText(result), isCommandSuccess(result), result.exitCode, result.stderr);
const dryRunOk = options.action === "ensure" && options.dryRun && isCommandSuccess(result);
const deleteDryRunOk = options.action === "delete" && options.dryRun && isCommandSuccess(result);
@@ -3658,6 +3851,9 @@ function runG14Secret(options: G14SecretOptions): Record<string, unknown> {
preset: options.preset,
mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : options.action === "delete" ? "confirmed-delete" : "confirmed-ensure",
status,
localMasterServer: localAdminApiKey
? { created: localAdminApiKey.created, envFile: localAdminApiKey.status, valuesRedacted: true }
: undefined,
mutation: status.mutation === true,
result: compactCommandResult(result),
valuesRedacted: true,
@@ -7458,6 +7654,8 @@ export function hwlabG14Help(): Record<string, unknown> {
"bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-openfga",
"bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-openfga --dry-run",
"bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-openfga --confirm",
"bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-master-server-admin-api-key",
"bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-master-server-admin-api-key --confirm",
"bun scripts/cli.ts hwlab g14 secret delete --lane v02 --name <obsolete-hwlab-v02-secret> --dry-run",
"bun scripts/cli.ts hwlab g14 secret delete --lane v02 --name <obsolete-hwlab-v02-secret> --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror status",