fix: bootstrap v03 provider secret
This commit is contained in:
@@ -16,6 +16,8 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P
|
|||||||
|
|
||||||
当现有 CLI 对某个 CI/CD 操作缺字段、缺动作、缺状态或缺权限时,处理顺序是先补 CLI,再执行发布或治理动作。临时低层 route 写操作只允许用于一次性止血,并且必须随后把稳定能力补进 CLI 与本参考文档;不能把手工 `kubectl apply/delete/annotate`、原生 GitHub CLI、手写 REST 请求或 registry shell 脚本沉淀成长期流程。长时观察仍遵守 60 秒短查询和 submit-and-poll 语义,不用单个 `trans`/`tran` 等待完整 PipelineRun 或 Argo rollout 结束。
|
当现有 CLI 对某个 CI/CD 操作缺字段、缺动作、缺状态或缺权限时,处理顺序是先补 CLI,再执行发布或治理动作。临时低层 route 写操作只允许用于一次性止血,并且必须随后把稳定能力补进 CLI 与本参考文档;不能把手工 `kubectl apply/delete/annotate`、原生 GitHub CLI、手写 REST 请求或 registry shell 脚本沉淀成长期流程。长时观察仍遵守 60 秒短查询和 submit-and-poll 语义,不用单个 `trans`/`tran` 等待完整 PipelineRun 或 Argo rollout 结束。
|
||||||
|
|
||||||
|
`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`。
|
||||||
|
|
||||||
## Command Model
|
## Command Model
|
||||||
|
|
||||||
- `help` 输出命令索引,适合作为交互式入口。
|
- `help` 输出命令索引,适合作为交互式入口。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { activeV02PipelineRuns, g14ObservabilityQueryAssertion, gitMirrorFlushJobManifest, gitMirrorStatusSummary, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14Help, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parseK8sCpuMillicores, parseK8sMemoryMiB, parsePipelineTaskRunMetrics, parseV02TriggerSnapshot, rolloutRecordBody, runtimeLanePipelineRunManifest, semanticChangelogBullets, summarizeV02CdStatus, v02CloseoutVerdict, v02CommitAlignment, v02ControlPlaneRefreshScriptHash, v02ControlPlaneRenderScript, v02ExistingPipelineRunReuseDecision, v02FalseGreenGuard, v02GitMirrorPreSyncWaitMs, v02LatestOnlyTargetValidation, v02PipelineServiceIds, v02PrAutomationCommentBody, v02ReusableGitMirrorPreSyncMarker, v02ReusableRefreshMarker, v02StatusHistoryPolicy, v02TaskRunPerformanceSummary } from "./src/hwlab-g14";
|
import { activeV02PipelineRuns, cleanupPipelineRunTargetCandidateFromTextForTest, g14ObservabilityQueryAssertion, gitMirrorFlushJobManifest, gitMirrorStatusSummary, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14Help, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parseK8sCpuMillicores, parseK8sMemoryMiB, parsePipelineTaskRunMetrics, parseV02TriggerSnapshot, rolloutRecordBody, runtimeLanePipelineRunManifest, semanticChangelogBullets, summarizeV02CdStatus, v02CloseoutVerdict, v02CommitAlignment, v02ControlPlaneRefreshScriptHash, v02ControlPlaneRenderScript, v02ExistingPipelineRunReuseDecision, v02FalseGreenGuard, v02GitMirrorPreSyncWaitMs, v02LatestOnlyTargetValidation, v02PipelineServiceIds, v02PrAutomationCommentBody, v02ReusableGitMirrorPreSyncMarker, v02ReusableRefreshMarker, v02StatusHistoryPolicy, v02TaskRunPerformanceSummary } from "./src/hwlab-g14";
|
||||||
import { hwlabNodeHelp } from "./src/hwlab-node";
|
import { hwlabNodeHelp, nodeSecretStatusFromTextForTest } from "./src/hwlab-node";
|
||||||
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec } from "./src/hwlab-node-lanes";
|
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec } from "./src/hwlab-node-lanes";
|
||||||
import { runCommand } from "./src/command";
|
import { runCommand } from "./src/command";
|
||||||
|
|
||||||
@@ -124,6 +124,50 @@ assertCondition(
|
|||||||
"v0.2 secret help must expose controlled OpenFGA and master-server admin API key SecretRef bootstrap paths",
|
"v0.2 secret help must expose controlled OpenFGA and master-server admin API key SecretRef bootstrap paths",
|
||||||
hwlabHelpUsage,
|
hwlabHelpUsage,
|
||||||
);
|
);
|
||||||
|
assertCondition(
|
||||||
|
hwlabHelpUsage.some((line) => line.includes("hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-code-agent-provider"))
|
||||||
|
&& hwlabHelpUsage.some((line) => line.includes("hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider --confirm"))
|
||||||
|
&& hwlabNodeHelpJson.includes("hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-code-agent-provider")
|
||||||
|
&& hwlabNodeHelpJson.includes("hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider --confirm"),
|
||||||
|
"v0.3 node-scoped secret help must expose the controlled code-agent provider SecretRef bootstrap path",
|
||||||
|
{ hwlabHelpUsage, hwlabNodeHelp: hwlabNodeHelp() },
|
||||||
|
);
|
||||||
|
const codeAgentProviderStatus = nodeSecretStatusFromTextForTest([
|
||||||
|
"namespace\thwlab-v03",
|
||||||
|
"secret\thwlab-v03-code-agent-provider",
|
||||||
|
"preset\tcode-agent-provider",
|
||||||
|
"sourceNamespace\thwlab-v02",
|
||||||
|
"sourceSecret\thwlab-v02-code-agent-provider",
|
||||||
|
"action\tcopied-from-source",
|
||||||
|
"dryRun\tfalse",
|
||||||
|
"mutation\ttrue",
|
||||||
|
"beforeExists\tno",
|
||||||
|
"beforeOpenaiPresent\tno",
|
||||||
|
"beforeOpenaiBytes\t0",
|
||||||
|
"beforeOpencodePresent\tno",
|
||||||
|
"beforeOpencodeBytes\t0",
|
||||||
|
"sourceExists\tyes",
|
||||||
|
"sourceOpenaiPresent\tyes",
|
||||||
|
"sourceOpenaiBytes\t48",
|
||||||
|
"sourceOpencodePresent\tyes",
|
||||||
|
"sourceOpencodeBytes\t64",
|
||||||
|
"afterExists\tyes",
|
||||||
|
"afterOpenaiPresent\tyes",
|
||||||
|
"afterOpenaiBytes\t48",
|
||||||
|
"afterOpencodePresent\tyes",
|
||||||
|
"afterOpencodeBytes\t64",
|
||||||
|
"applyExitCode\t0",
|
||||||
|
].join("\n"), true, 0, "");
|
||||||
|
assertCondition(
|
||||||
|
record(codeAgentProviderStatus).ok === true
|
||||||
|
&& record(codeAgentProviderStatus).valuesRedacted === true
|
||||||
|
&& record(record(codeAgentProviderStatus).after).requiredAnyProviderKeyPresent === true
|
||||||
|
&& JSON.stringify(codeAgentProviderStatus).includes("hwlab-v02-code-agent-provider")
|
||||||
|
&& !JSON.stringify(codeAgentProviderStatus).includes("sk-")
|
||||||
|
&& !JSON.stringify(codeAgentProviderStatus).includes("base64"),
|
||||||
|
"code-agent provider Secret status must be redacted while proving at least one runtime provider key is present",
|
||||||
|
codeAgentProviderStatus,
|
||||||
|
);
|
||||||
assertCondition(
|
assertCondition(
|
||||||
hwlabHelpUsage.some((line) => line.includes("upstream-image status --name openfga --tag v1.17.0"))
|
hwlabHelpUsage.some((line) => line.includes("upstream-image status --name openfga --tag v1.17.0"))
|
||||||
&& hwlabHelpUsage.some((line) => line.includes("upstream-image ensure --name openfga --tag v1.17.0 --confirm")),
|
&& hwlabHelpUsage.some((line) => line.includes("upstream-image ensure --name openfga --tag v1.17.0 --confirm")),
|
||||||
@@ -481,10 +525,28 @@ assertCondition(
|
|||||||
"control-plane cleanup-runs must protect the latest PipelineRun per lane by default",
|
"control-plane cleanup-runs must protect the latest PipelineRun per lane by default",
|
||||||
);
|
);
|
||||||
assertCondition(
|
assertCondition(
|
||||||
sourceText.includes("selectedReason: ageMinutes === null ? \"missing-creation-timestamp\" : \"below-min-age\"")
|
record(cleanupPipelineRunTargetCandidateFromTextForTest({
|
||||||
&& sourceText.includes("target-pipelinerun-not-found-or-not-terminal")
|
targetPipelineRun: "hwlab-v02-ci-poll-d6b01b261f1b",
|
||||||
&& sourceText.indexOf("const target = terminalRuns.find((item) => item.name === targetPipelineRun)") < sourceText.indexOf("target-pipelinerun-not-found-or-not-terminal"),
|
text: "hwlab-v02-ci-poll-d6b01b261f1b\t2026-06-08T16:15:31Z\tTrue\tCompleted",
|
||||||
"targeted cleanup-runs must report below-min-age terminal PipelineRuns instead of hiding them as not found",
|
nowMs: Date.parse("2026-06-08T16:51:31Z"),
|
||||||
|
minAgeMinutes: 60,
|
||||||
|
})).selectedReason === "below-min-age"
|
||||||
|
&& record(cleanupPipelineRunTargetCandidateFromTextForTest({
|
||||||
|
targetPipelineRun: "hwlab-v02-ci-poll-d6b01b261f1b",
|
||||||
|
text: "hwlab-v02-ci-poll-d6b01b261f1b\t2026-06-08T16:15:31Z\tUnknown\tRunning",
|
||||||
|
nowMs: Date.parse("2026-06-08T18:00:00Z"),
|
||||||
|
})).selectedReason === "target-pipelinerun-not-terminal"
|
||||||
|
&& record(cleanupPipelineRunTargetCandidateFromTextForTest({
|
||||||
|
targetPipelineRun: "hwlab-v02-ci-poll-d6b01b261f1b",
|
||||||
|
text: "",
|
||||||
|
commandOk: false,
|
||||||
|
exitCode: 1,
|
||||||
|
stderr: "Error from server (NotFound): pipelineruns.tekton.dev \"hwlab-v02-ci-poll-d6b01b261f1b\" not found",
|
||||||
|
})).reason === "target-pipelinerun-not-found"
|
||||||
|
&& sourceText.includes("targetPipelineRun !== undefined")
|
||||||
|
&& sourceText.includes("cleanupPipelineRunFieldsJsonPath(false)")
|
||||||
|
&& !sourceText.includes("target-pipelinerun-not-found-or-not-terminal"),
|
||||||
|
"targeted cleanup-runs must query the requested PipelineRun directly and distinguish below-min-age, not-terminal, and not-found states",
|
||||||
);
|
);
|
||||||
assertCondition(
|
assertCondition(
|
||||||
hwlabHelpUsage.some((line) => line.includes("hwlab nodes control-plane cleanup-runs --node G14 --lane v03 --pipeline-run"))
|
hwlabHelpUsage.some((line) => line.includes("hwlab nodes control-plane cleanup-runs --node G14 --lane v03 --pipeline-run"))
|
||||||
|
|||||||
+135
-35
@@ -2568,6 +2568,14 @@ function listV02PipelineRunsCompactFromText(text: string, commandOk: boolean, co
|
|||||||
return parsePipelineRunRows(text, limit, nowMs);
|
return parsePipelineRunRows(text, limit, nowMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CleanupPipelineRunRow {
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
ageMinutes: number | null;
|
||||||
|
status: string | null;
|
||||||
|
reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function pipelinePrefixesForLane(lane: G14ControlPlaneOptions["lane"]): string[] {
|
function pipelinePrefixesForLane(lane: G14ControlPlaneOptions["lane"]): string[] {
|
||||||
if (isHwlabRuntimeLane(lane)) return [`${hwlabRuntimeLaneSpec(lane).pipelineRunPrefix}-`];
|
if (isHwlabRuntimeLane(lane)) return [`${hwlabRuntimeLaneSpec(lane).pipelineRunPrefix}-`];
|
||||||
if (lane === "g14") return ["hwlab-g14-ci-poll-"];
|
if (lane === "g14") return ["hwlab-g14-ci-poll-"];
|
||||||
@@ -2586,10 +2594,132 @@ function tailText(value: unknown, maxBytes = 2000): string {
|
|||||||
return text.length <= maxBytes ? text : text.slice(-maxBytes);
|
return text.length <= maxBytes ? text : text.slice(-maxBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupPipelineRunFieldsJsonPath(range: boolean): string {
|
||||||
|
const body = [
|
||||||
|
"{.metadata.name}",
|
||||||
|
"{\"\\t\"}",
|
||||||
|
"{.metadata.creationTimestamp}",
|
||||||
|
"{\"\\t\"}",
|
||||||
|
"{.status.conditions[0].status}",
|
||||||
|
"{\"\\t\"}",
|
||||||
|
"{.status.conditions[0].reason}",
|
||||||
|
"{\"\\n\"}",
|
||||||
|
].join("");
|
||||||
|
return range ? `jsonpath={range .items[*]}${body}{end}` : `jsonpath=${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCleanupPipelineRunLine(line: string, nowMs: number): CleanupPipelineRunRow | null {
|
||||||
|
const [name = "", createdAt = "", status = "", reason = ""] = line.trim().split("\t");
|
||||||
|
if (name.length === 0) return null;
|
||||||
|
const createdMs = Date.parse(createdAt);
|
||||||
|
const ageMinutes = Number.isFinite(createdMs) ? Math.floor((nowMs - createdMs) / 60000) : null;
|
||||||
|
return { name, createdAt, ageMinutes, status: status || null, reason: reason || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupPipelineRunTargetCandidate(input: {
|
||||||
|
targetPipelineRun: string;
|
||||||
|
text: string;
|
||||||
|
commandOk: boolean;
|
||||||
|
exitCode: number | null;
|
||||||
|
stderr: string;
|
||||||
|
minAgeMinutes: number;
|
||||||
|
nowMs: number;
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
if (!input.commandOk) {
|
||||||
|
const queryError = tailText(input.stderr);
|
||||||
|
const notFound = /notfound|not found/i.test(queryError);
|
||||||
|
return {
|
||||||
|
name: input.targetPipelineRun,
|
||||||
|
createdAt: null,
|
||||||
|
ageMinutes: null,
|
||||||
|
status: null,
|
||||||
|
reason: notFound ? "target-pipelinerun-not-found" : "target-pipelinerun-query-failed",
|
||||||
|
selected: false,
|
||||||
|
queryExitCode: input.exitCode,
|
||||||
|
...(queryError.length > 0 ? { queryError } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const target = parseCleanupPipelineRunLine(input.text, input.nowMs);
|
||||||
|
if (target === null || target.name !== input.targetPipelineRun) {
|
||||||
|
return {
|
||||||
|
name: input.targetPipelineRun,
|
||||||
|
createdAt: null,
|
||||||
|
ageMinutes: null,
|
||||||
|
status: null,
|
||||||
|
reason: "target-pipelinerun-query-empty",
|
||||||
|
selected: false,
|
||||||
|
queryOutputPreview: tailText(input.text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (target.status !== "True" && target.status !== "False") {
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
selected: false,
|
||||||
|
selectedReason: "target-pipelinerun-not-terminal",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ageMinutes = typeof target.ageMinutes === "number" ? target.ageMinutes : null;
|
||||||
|
const belowMinAge = ageMinutes === null || ageMinutes < input.minAgeMinutes;
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
selected: !belowMinAge,
|
||||||
|
...(belowMinAge
|
||||||
|
? { selectedReason: ageMinutes === null ? "missing-creation-timestamp" : "below-min-age" }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupPipelineRunTargetCandidateFromTextForTest(input: {
|
||||||
|
targetPipelineRun: string;
|
||||||
|
text: string;
|
||||||
|
commandOk?: boolean;
|
||||||
|
exitCode?: number | null;
|
||||||
|
stderr?: string;
|
||||||
|
minAgeMinutes?: number;
|
||||||
|
nowMs?: number;
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
return cleanupPipelineRunTargetCandidate({
|
||||||
|
targetPipelineRun: input.targetPipelineRun,
|
||||||
|
text: input.text,
|
||||||
|
commandOk: input.commandOk ?? true,
|
||||||
|
exitCode: input.exitCode ?? 0,
|
||||||
|
stderr: input.stderr ?? "",
|
||||||
|
minAgeMinutes: input.minAgeMinutes ?? 60,
|
||||||
|
nowMs: input.nowMs ?? Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record<string, unknown>[] {
|
function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record<string, unknown>[] {
|
||||||
if (options.lane !== "g14" && options.lane !== "all" && !isHwlabRuntimeLane(options.lane)) {
|
if (options.lane !== "g14" && options.lane !== "all" && !isHwlabRuntimeLane(options.lane)) {
|
||||||
throw new Error("control-plane cleanup-runs requires --lane v02|v03|g14|all");
|
throw new Error("control-plane cleanup-runs requires --lane v02|v03|g14|all");
|
||||||
}
|
}
|
||||||
|
const targetPipelineRun = options.pipelineRun ?? (options.sourceCommit === undefined
|
||||||
|
? undefined
|
||||||
|
: isHwlabRuntimeLane(options.lane)
|
||||||
|
? runtimeLanePipelineRunName(hwlabRuntimeLaneSpec(options.lane), options.sourceCommit)
|
||||||
|
: v02PipelineRunName(options.sourceCommit));
|
||||||
|
const now = Date.now();
|
||||||
|
if (targetPipelineRun !== undefined) {
|
||||||
|
const targetResult = g14K3s([
|
||||||
|
"kubectl",
|
||||||
|
"get",
|
||||||
|
"pipelinerun",
|
||||||
|
"-n",
|
||||||
|
CI_NAMESPACE,
|
||||||
|
targetPipelineRun,
|
||||||
|
"-o",
|
||||||
|
cleanupPipelineRunFieldsJsonPath(false),
|
||||||
|
], 60_000);
|
||||||
|
return [cleanupPipelineRunTargetCandidate({
|
||||||
|
targetPipelineRun,
|
||||||
|
text: statusText(targetResult),
|
||||||
|
commandOk: isCommandSuccess(targetResult),
|
||||||
|
exitCode: targetResult.exitCode,
|
||||||
|
stderr: commandErrorSummary(targetResult),
|
||||||
|
minAgeMinutes: options.minAgeMinutes,
|
||||||
|
nowMs: now,
|
||||||
|
})];
|
||||||
|
}
|
||||||
const result = g14K3s([
|
const result = g14K3s([
|
||||||
"kubectl",
|
"kubectl",
|
||||||
"get",
|
"get",
|
||||||
@@ -2597,28 +2727,18 @@ function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record<string
|
|||||||
"-n",
|
"-n",
|
||||||
CI_NAMESPACE,
|
CI_NAMESPACE,
|
||||||
"-o",
|
"-o",
|
||||||
'jsonpath={range .items[*]}{.metadata.name}{"\\t"}{.metadata.creationTimestamp}{"\\t"}{.status.conditions[0].status}{"\\t"}{.status.conditions[0].reason}{"\\n"}{end}',
|
cleanupPipelineRunFieldsJsonPath(true),
|
||||||
], 60_000);
|
], 60_000);
|
||||||
if (!isCommandSuccess(result)) {
|
if (!isCommandSuccess(result)) {
|
||||||
throw new Error(`failed to list hwlab-ci PipelineRuns: ${commandErrorSummary(result)}`);
|
throw new Error(`failed to list hwlab-ci PipelineRuns: ${commandErrorSummary(result)}`);
|
||||||
}
|
}
|
||||||
const targetPipelineRun = options.pipelineRun ?? (options.sourceCommit === undefined
|
|
||||||
? undefined
|
|
||||||
: isHwlabRuntimeLane(options.lane)
|
|
||||||
? runtimeLanePipelineRunName(hwlabRuntimeLaneSpec(options.lane), options.sourceCommit)
|
|
||||||
: v02PipelineRunName(options.sourceCommit));
|
|
||||||
const prefixes = pipelinePrefixesForLane(options.lane);
|
const prefixes = pipelinePrefixesForLane(options.lane);
|
||||||
const now = Date.now();
|
|
||||||
const terminalRuns = statusText(result)
|
const terminalRuns = statusText(result)
|
||||||
.split(/\r?\n/u)
|
.split(/\r?\n/u)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => {
|
.map((line) => parseCleanupPipelineRunLine(line, now))
|
||||||
const [name = "", createdAt = "", status = "", reason = ""] = line.split("\t");
|
.filter((item): item is CleanupPipelineRunRow => item !== null)
|
||||||
const createdMs = Date.parse(createdAt);
|
|
||||||
const ageMinutes = Number.isFinite(createdMs) ? Math.floor((now - createdMs) / 60000) : null;
|
|
||||||
return { name, createdAt, ageMinutes, status: status || null, reason: reason || null };
|
|
||||||
})
|
|
||||||
.filter((item) => item.name.length > 0 && prefixes.some((prefix) => item.name.startsWith(prefix)))
|
.filter((item) => item.name.length > 0 && prefixes.some((prefix) => item.name.startsWith(prefix)))
|
||||||
.filter((item) => item.status === "True" || item.status === "False")
|
.filter((item) => item.status === "True" || item.status === "False")
|
||||||
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
||||||
@@ -2629,28 +2749,6 @@ function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record<string
|
|||||||
.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))[0];
|
.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))[0];
|
||||||
if (latest !== undefined) protectedLatestByPrefix.set(prefix, latest.name);
|
if (latest !== undefined) protectedLatestByPrefix.set(prefix, latest.name);
|
||||||
}
|
}
|
||||||
if (targetPipelineRun !== undefined) {
|
|
||||||
const target = terminalRuns.find((item) => item.name === targetPipelineRun);
|
|
||||||
if (target === undefined) {
|
|
||||||
return [{
|
|
||||||
name: targetPipelineRun,
|
|
||||||
createdAt: null,
|
|
||||||
ageMinutes: null,
|
|
||||||
status: null,
|
|
||||||
reason: "target-pipelinerun-not-found-or-not-terminal",
|
|
||||||
selected: false,
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
const ageMinutes = typeof target.ageMinutes === "number" ? target.ageMinutes : null;
|
|
||||||
const belowMinAge = ageMinutes === null || ageMinutes < options.minAgeMinutes;
|
|
||||||
return [{
|
|
||||||
...target,
|
|
||||||
selected: !belowMinAge,
|
|
||||||
...(belowMinAge
|
|
||||||
? { selectedReason: ageMinutes === null ? "missing-creation-timestamp" : "below-min-age" }
|
|
||||||
: {}),
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
const candidates = terminalRuns
|
const candidates = terminalRuns
|
||||||
.filter((item) => typeof item.ageMinutes === "number" && item.ageMinutes >= options.minAgeMinutes)
|
.filter((item) => typeof item.ageMinutes === "number" && item.ageMinutes >= options.minAgeMinutes)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
@@ -8808,6 +8906,8 @@ export function hwlabG14Help(): Record<string, unknown> {
|
|||||||
"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 ensure --lane v02 --name hwlab-v02-master-server-admin-api-key --confirm",
|
||||||
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-master-server-admin-api-key",
|
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-master-server-admin-api-key",
|
||||||
"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 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",
|
||||||
"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> --dry-run",
|
||||||
"bun scripts/cli.ts hwlab g14 secret delete --lane v02 --name <obsolete-hwlab-v02-secret> --confirm",
|
"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",
|
"bun scripts/cli.ts hwlab g14 git-mirror status",
|
||||||
|
|||||||
+186
-4
@@ -6,7 +6,7 @@ import { runHwlabG14Command } from "./hwlab-g14";
|
|||||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane } from "./hwlab-node-lanes";
|
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane } from "./hwlab-node-lanes";
|
||||||
|
|
||||||
type SecretAction = "status" | "ensure";
|
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";
|
type DelegatedNodeDomain = "control-plane" | "git-mirror";
|
||||||
|
|
||||||
interface NodeSecretOptions {
|
interface NodeSecretOptions {
|
||||||
@@ -33,6 +33,9 @@ interface RuntimeSecretSpec {
|
|||||||
openFgaDbUser: string;
|
openFgaDbUser: string;
|
||||||
openFgaDbHost: string;
|
openFgaDbHost: string;
|
||||||
masterAdminApiKeySecret: string;
|
masterAdminApiKeySecret: string;
|
||||||
|
codeAgentProviderSecret: string;
|
||||||
|
codeAgentProviderSourceNamespace: string;
|
||||||
|
codeAgentProviderSourceSecret: string;
|
||||||
fieldManager: string;
|
fieldManager: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +44,10 @@ const MASTER_ADMIN_API_KEY_KEY = "api-key";
|
|||||||
const OPENFGA_AUTHN_KEY = "authn-preshared-key";
|
const OPENFGA_AUTHN_KEY = "authn-preshared-key";
|
||||||
const OPENFGA_DATASTORE_URI_KEY = "datastore-uri";
|
const OPENFGA_DATASTORE_URI_KEY = "datastore-uri";
|
||||||
const OPENFGA_POSTGRES_PASSWORD_KEY = "postgres-password";
|
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>> {
|
export async function runHwlabNodeCommand(_config: Config, args: string[]): Promise<Record<string, unknown>> {
|
||||||
if (args.length === 0 || args.includes("--help") || args.includes("-h")) return hwlabNodeHelp();
|
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 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-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 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 {
|
function parseSecretOptions(args: string[]): NodeSecretOptions {
|
||||||
const [actionRaw] = args;
|
const [actionRaw] = args;
|
||||||
if (actionRaw !== "status" && actionRaw !== "ensure") {
|
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");
|
const node = requiredOption(args, "--node");
|
||||||
assertNodeId(node);
|
assertNodeId(node);
|
||||||
@@ -212,8 +221,24 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
|
|||||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 900),
|
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) {
|
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) {
|
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}`);
|
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",
|
openFgaDbUser: "hwlab_openfga",
|
||||||
openFgaDbHost: `${namespace}-postgres.${namespace}.svc.cluster.local`,
|
openFgaDbHost: `${namespace}-postgres.${namespace}.svc.cluster.local`,
|
||||||
masterAdminApiKeySecret: `${namespace}-master-server-admin-api-key`,
|
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`,
|
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
|
const input = options.preset === "master-server-admin-api-key" && options.action === "ensure" && !options.dryRun
|
||||||
? readMasterAdminApiKey().key
|
? 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 status = secretStatusFromText(statusText(result), result.exitCode === 0, result.exitCode, result.stderr, spec);
|
||||||
const dryRunOk = options.action === "ensure" && options.dryRun && result.exitCode === 0;
|
const dryRunOk = options.action === "ensure" && options.dryRun && result.exitCode === 0;
|
||||||
const ok = dryRunOk ? true : status.ok === true;
|
const ok = dryRunOk ? true : status.ok === true;
|
||||||
@@ -492,6 +525,107 @@ function masterAdminApiKeySecretScript(options: NodeSecretOptions, spec: Runtime
|
|||||||
].join("\n");
|
].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> {
|
function secretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string, spec: RuntimeSecretSpec): Record<string, unknown> {
|
||||||
const fields = keyValueLinesFromText(text);
|
const fields = keyValueLinesFromText(text);
|
||||||
if (fields.preset === "master-server-admin-api-key") {
|
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`,
|
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 afterAuthnBytes = numericField(fields.afterAuthnBytes);
|
||||||
const afterUriBytes = numericField(fields.afterDatastoreUriBytes);
|
const afterUriBytes = numericField(fields.afterDatastoreUriBytes);
|
||||||
const afterPasswordBytes = numericField(fields.afterPostgresPasswordBytes);
|
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 } {
|
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}`);
|
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");
|
const content = readFileSync(MASTER_ADMIN_API_KEY_ENV, "utf8");
|
||||||
|
|||||||
Reference in New Issue
Block a user