fix: bootstrap v03 provider secret

This commit is contained in:
Codex
2026-06-08 16:56:41 +00:00
parent 59b3b15328
commit d9e6f70e72
4 changed files with 391 additions and 45 deletions
+186 -4
View File
@@ -6,7 +6,7 @@ import { runHwlabG14Command } from "./hwlab-g14";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane } from "./hwlab-node-lanes";
type SecretAction = "status" | "ensure";
type SecretPreset = "openfga" | "master-server-admin-api-key";
type SecretPreset = "openfga" | "master-server-admin-api-key" | "code-agent-provider";
type DelegatedNodeDomain = "control-plane" | "git-mirror";
interface NodeSecretOptions {
@@ -33,6 +33,9 @@ interface RuntimeSecretSpec {
openFgaDbUser: string;
openFgaDbHost: string;
masterAdminApiKeySecret: string;
codeAgentProviderSecret: string;
codeAgentProviderSourceNamespace: string;
codeAgentProviderSourceSecret: string;
fieldManager: string;
}
@@ -41,6 +44,10 @@ const MASTER_ADMIN_API_KEY_KEY = "api-key";
const OPENFGA_AUTHN_KEY = "authn-preshared-key";
const OPENFGA_DATASTORE_URI_KEY = "datastore-uri";
const OPENFGA_POSTGRES_PASSWORD_KEY = "postgres-password";
const CODE_AGENT_PROVIDER_OPENAI_KEY = "openai-api-key";
const CODE_AGENT_PROVIDER_OPENCODE_KEY = "opencode-api-key";
const CODE_AGENT_PROVIDER_SOURCE_NAMESPACE = "hwlab-v02";
const CODE_AGENT_PROVIDER_SOURCE_SECRET = "hwlab-v02-code-agent-provider";
export async function runHwlabNodeCommand(_config: Config, args: string[]): Promise<Record<string, unknown>> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) return hwlabNodeHelp();
@@ -70,6 +77,8 @@ export function hwlabNodeHelp(): Record<string, unknown> {
"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-openfga --confirm",
"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-code-agent-provider",
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider --confirm",
],
};
}
@@ -186,7 +195,7 @@ function rewriteDelegatedNodeString(value: string, scoped: ReturnType<typeof par
function parseSecretOptions(args: string[]): NodeSecretOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "ensure") {
throw new Error("secret usage: status|ensure --node NODE --lane vNN --name hwlab-vNN-openfga|hwlab-vNN-master-server-admin-api-key [--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-code-agent-provider [--dry-run|--confirm]");
}
const node = requiredOption(args, "--node");
assertNodeId(node);
@@ -212,8 +221,24 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
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}`);
}
return {
action: actionRaw,
node,
lane,
name,
key,
preset: "code-agent-provider",
confirm,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 900),
};
}
if (name !== spec.openFgaSecret) {
throw new Error(`secret status/ensure supports --name ${spec.openFgaSecret} or ${spec.masterAdminApiKeySecret} for --lane ${lane}`);
throw new Error(`secret status/ensure supports --name ${spec.openFgaSecret}, ${spec.masterAdminApiKeySecret}, or ${spec.codeAgentProviderSecret} for --lane ${lane}`);
}
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}`);
@@ -245,6 +270,9 @@ function runtimeSecretSpec(input: { node: string; lane: string }): RuntimeSecret
openFgaDbUser: "hwlab_openfga",
openFgaDbHost: `${namespace}-postgres.${namespace}.svc.cluster.local`,
masterAdminApiKeySecret: `${namespace}-master-server-admin-api-key`,
codeAgentProviderSecret: `${namespace}-code-agent-provider`,
codeAgentProviderSourceNamespace: CODE_AGENT_PROVIDER_SOURCE_NAMESPACE,
codeAgentProviderSourceSecret: CODE_AGENT_PROVIDER_SOURCE_SECRET,
fieldManager: `unidesk-hwlab-node-${input.lane}-secret`,
};
}
@@ -254,7 +282,12 @@ function runNodeSecret(options: NodeSecretOptions): Record<string, unknown> {
const input = options.preset === "master-server-admin-api-key" && options.action === "ensure" && !options.dryRun
? readMasterAdminApiKey().key
: "";
const result = runTransScript(options.node, options.preset === "openfga" ? openFgaSecretScript(options, spec) : masterAdminApiKeySecretScript(options, spec), input, options.timeoutSeconds);
const script = options.preset === "openfga"
? openFgaSecretScript(options, spec)
: options.preset === "master-server-admin-api-key"
? masterAdminApiKeySecretScript(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;
const ok = dryRunOk ? true : status.ok === true;
@@ -492,6 +525,107 @@ function masterAdminApiKeySecretScript(options: NodeSecretOptions, spec: Runtime
].join("\n");
}
function codeAgentProviderSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
return [
"set +e",
`namespace=${shellQuote(spec.namespace)}`,
`name=${shellQuote(spec.codeAgentProviderSecret)}`,
`source_namespace=${shellQuote(spec.codeAgentProviderSourceNamespace)}`,
`source_name=${shellQuote(spec.codeAgentProviderSourceSecret)}`,
`openai_key=${shellQuote(CODE_AGENT_PROVIDER_OPENAI_KEY)}`,
`opencode_key=${shellQuote(CODE_AGENT_PROVIDER_OPENCODE_KEY)}`,
`selected_key=${shellQuote(options.key ?? "")}`,
`action_request=${shellQuote(options.action)}`,
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
`field_manager=${shellQuote(spec.fieldManager)}`,
"preset=code-agent-provider",
"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_openai_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$openai_key\")",
"before_opencode_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$opencode_key\")",
"source_exists=$(secret_exists_flag \"$source_namespace\" \"$source_name\")",
"source_openai_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$openai_key\")",
"source_opencode_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$opencode_key\")",
"before_openai_present=$([ -n \"$before_openai_b64\" ] && printf yes || printf no)",
"before_opencode_present=$([ -n \"$before_opencode_b64\" ] && printf yes || printf no)",
"source_openai_present=$([ -n \"$source_openai_b64\" ] && printf yes || printf no)",
"source_opencode_present=$([ -n \"$source_opencode_b64\" ] && printf yes || printf no)",
"before_openai_bytes=$(decoded_length \"$before_openai_b64\")",
"before_opencode_bytes=$(decoded_length \"$before_opencode_b64\")",
"source_openai_bytes=$(decoded_length \"$source_openai_b64\")",
"source_opencode_bytes=$(decoded_length \"$source_opencode_b64\")",
"action=observed",
"mutation=false",
"apply_exit=",
"if [ \"$action_request\" = ensure ]; then",
" missing_target=false",
" [ \"$before_exists\" = yes ] && { [ \"$before_openai_bytes\" -gt 0 ] || [ \"$before_opencode_bytes\" -gt 0 ]; } || missing_target=true",
" missing_source=false",
" [ \"$source_exists\" = yes ] && { [ \"$source_openai_bytes\" -gt 0 ] || [ \"$source_opencode_bytes\" -gt 0 ]; } || missing_source=true",
" if [ \"$missing_source\" = true ]; then",
" action=source-missing-provider-key",
" 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: code-agent-provider",
"type: Opaque",
"data:",
" openai-api-key: $source_openai_b64",
" opencode-api-key: $source_opencode_b64",
"EOF_SECRET",
" apply_exit=$?",
" if [ \"$apply_exit\" -eq 0 ]; then action=copied-from-source; mutation=true; else action=apply-failed; fi",
" fi",
"fi",
"after_exists=$(secret_exists_flag \"$namespace\" \"$name\")",
"after_openai_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$openai_key\")",
"after_opencode_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$opencode_key\")",
"after_openai_present=$([ -n \"$after_openai_b64\" ] && printf yes || printf no)",
"after_opencode_present=$([ -n \"$after_opencode_b64\" ] && printf yes || printf no)",
"after_openai_bytes=$(decoded_length \"$after_openai_b64\")",
"after_opencode_bytes=$(decoded_length \"$after_opencode_b64\")",
"printf 'namespace\\t%s\\n' \"$namespace\"",
"printf 'secret\\t%s\\n' \"$name\"",
"printf 'preset\\t%s\\n' \"$preset\"",
"printf 'sourceNamespace\\t%s\\n' \"$source_namespace\"",
"printf 'sourceSecret\\t%s\\n' \"$source_name\"",
"printf 'selectedKey\\t%s\\n' \"$selected_key\"",
"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 'beforeOpenaiPresent\\t%s\\n' \"$before_openai_present\"",
"printf 'beforeOpenaiBytes\\t%s\\n' \"$before_openai_bytes\"",
"printf 'beforeOpencodePresent\\t%s\\n' \"$before_opencode_present\"",
"printf 'beforeOpencodeBytes\\t%s\\n' \"$before_opencode_bytes\"",
"printf 'sourceExists\\t%s\\n' \"$source_exists\"",
"printf 'sourceOpenaiPresent\\t%s\\n' \"$source_openai_present\"",
"printf 'sourceOpenaiBytes\\t%s\\n' \"$source_openai_bytes\"",
"printf 'sourceOpencodePresent\\t%s\\n' \"$source_opencode_present\"",
"printf 'sourceOpencodeBytes\\t%s\\n' \"$source_opencode_bytes\"",
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
"printf 'afterOpenaiPresent\\t%s\\n' \"$after_openai_present\"",
"printf 'afterOpenaiBytes\\t%s\\n' \"$after_openai_bytes\"",
"printf 'afterOpencodePresent\\t%s\\n' \"$after_opencode_present\"",
"printf 'afterOpencodeBytes\\t%s\\n' \"$after_opencode_bytes\"",
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
"if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi",
].join("\n");
}
function secretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string, spec: RuntimeSecretSpec): Record<string, unknown> {
const fields = keyValueLinesFromText(text);
if (fields.preset === "master-server-admin-api-key") {
@@ -513,6 +647,50 @@ 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 === "code-agent-provider") {
const beforeOpenaiBytes = numericField(fields.beforeOpenaiBytes);
const beforeOpencodeBytes = numericField(fields.beforeOpencodeBytes);
const sourceOpenaiBytes = numericField(fields.sourceOpenaiBytes);
const sourceOpencodeBytes = numericField(fields.sourceOpencodeBytes);
const afterOpenaiBytes = numericField(fields.afterOpenaiBytes);
const afterOpencodeBytes = numericField(fields.afterOpencodeBytes);
const openaiReady = fields.afterOpenaiPresent === "yes" && typeof afterOpenaiBytes === "number" && afterOpenaiBytes > 0;
const opencodeReady = fields.afterOpencodePresent === "yes" && typeof afterOpencodeBytes === "number" && afterOpencodeBytes > 0;
const healthy = fields.afterExists === "yes" && (openaiReady || opencodeReady);
return {
ok: commandOk && healthy,
namespace: fields.namespace || spec.namespace,
secret: fields.secret || spec.codeAgentProviderSecret,
preset: "code-agent-provider",
source: {
namespace: fields.sourceNamespace || spec.codeAgentProviderSourceNamespace,
secret: fields.sourceSecret || spec.codeAgentProviderSourceSecret,
exists: fields.sourceExists === "yes",
openaiApiKey: { keyPresent: fields.sourceOpenaiPresent === "yes", valueBytes: sourceOpenaiBytes },
opencodeApiKey: { keyPresent: fields.sourceOpencodePresent === "yes", valueBytes: sourceOpencodeBytes },
},
selectedKey: fields.selectedKey || null,
action: fields.action || null,
dryRun: fields.dryRun === "true",
mutation: fields.mutation === "true",
before: {
exists: fields.beforeExists === "yes",
openaiApiKey: { keyPresent: fields.beforeOpenaiPresent === "yes", valueBytes: beforeOpenaiBytes },
opencodeApiKey: { keyPresent: fields.beforeOpencodePresent === "yes", valueBytes: beforeOpencodeBytes },
},
after: {
exists: fields.afterExists === "yes",
openaiApiKey: { keyPresent: fields.afterOpenaiPresent === "yes", valueBytes: afterOpenaiBytes },
opencodeApiKey: { keyPresent: fields.afterOpencodePresent === "yes", valueBytes: afterOpencodeBytes },
requiredAnyProviderKeyPresent: openaiReady || opencodeReady,
},
applyExitCode: numericField(fields.applyExitCode),
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
valuesRedacted: true,
summary: healthy ? `${fields.secret || spec.codeAgentProviderSecret} has a usable provider key` : `${fields.secret || spec.codeAgentProviderSecret} missing provider keys`,
};
}
const afterAuthnBytes = numericField(fields.afterAuthnBytes);
const afterUriBytes = numericField(fields.afterDatastoreUriBytes);
const afterPasswordBytes = numericField(fields.afterPostgresPasswordBytes);
@@ -553,6 +731,10 @@ function secretStatusFromText(text: string, commandOk: boolean, exitCode: number
};
}
export function nodeSecretStatusFromTextForTest(text: string, commandOk: boolean, exitCode: number | null, stderr: string, node = "G14", lane = "v03"): Record<string, unknown> {
return secretStatusFromText(text, commandOk, exitCode, stderr, runtimeSecretSpec({ node, lane }));
}
function readMasterAdminApiKey(): { key: string; source: string } {
if (!existsSync(MASTER_ADMIN_API_KEY_ENV)) throw new Error(`HWLAB_API_KEY source missing: ${MASTER_ADMIN_API_KEY_ENV}`);
const content = readFileSync(MASTER_ADMIN_API_KEY_ENV, "utf8");