fix: preflight codex stdio mount resources
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "hwlab-code-agent-codex-config",
|
||||
"namespace": "hwlab-dev",
|
||||
"labels": {
|
||||
"app.kubernetes.io/name": "hwlab-code-agent-codex-config",
|
||||
"hwlab.pikastech.local/service-id": "hwlab-cloud-api"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"config.toml": "model_provider = \"OpenAI\"\nmodel = \"gpt-5.4\"\nreview_model = \"gpt-5.4\"\nmodel_reasoning_effort = \"xhigh\"\ndisable_response_storage = true\nnetwork_access = \"enabled\"\nmodel_context_window = 1000000\nmodel_auto_compact_token_limit = 900000\n\n[model_providers.OpenAI]\nname = \"OpenAI\"\nbase_url = \"https://hyueapi.com\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n\n[model_providers.crs]\nrequest_max_retries = 1000\nstream_max_retries = 1000\nname = \"crs\"\nbase_url = \"https://api.gptclubapi.xyz/openai\"\nwire_api = \"responses\"\nrequires_openai_auth = true\nenv_key = \"CRS_OAI_KEY\"\n"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"kind": "Kustomization",
|
||||
"resources": [
|
||||
"namespace.yaml",
|
||||
"code-agent-codex-config.yaml",
|
||||
"services.yaml",
|
||||
"workloads.yaml"
|
||||
],
|
||||
|
||||
@@ -108,6 +108,12 @@ apply preflight 检查的 SecretRef 固定为:
|
||||
- `hwlab-cloud-api-dev-db/database-url`
|
||||
- `hwlab-cloud-api-dev-db-admin/admin-url`
|
||||
- `hwlab-code-agent-provider/openai-api-key`
|
||||
- `hwlab-code-agent-codex-auth/auth.json`
|
||||
|
||||
Code Agent Codex stdio 还必须在 preflight 阶段验证非 secret ConfigMap
|
||||
`hwlab-code-agent-codex-config/config.toml` 的存在性和 key 名;该 ConfigMap 由
|
||||
`deploy/k8s/base/code-agent-codex-config.yaml` 纳入 DEV desired-state。Codex
|
||||
auth 仍然只能作为外部 Secret 管理,禁止把 `auth.json` 内容写进仓库或报告。
|
||||
|
||||
preflight 和报告只能记录 Secret 名、key 名、存在性、key-name 观察结果和脱敏
|
||||
命令摘要;不得读取或打印 Secret value。阻塞时输出结构化 blocker,包含
|
||||
|
||||
@@ -39,7 +39,11 @@ const forbiddenControlPlanePattern = /docker-desktop|desktop-control-plane|127\.
|
||||
const requiredDevSecretRefs = Object.freeze([
|
||||
Object.freeze({ secretName: "hwlab-cloud-api-dev-db", secretKey: "database-url" }),
|
||||
Object.freeze({ secretName: "hwlab-cloud-api-dev-db-admin", secretKey: "admin-url" }),
|
||||
Object.freeze({ secretName: "hwlab-code-agent-provider", secretKey: "openai-api-key" })
|
||||
Object.freeze({ secretName: "hwlab-code-agent-provider", secretKey: "openai-api-key" }),
|
||||
Object.freeze({ secretName: "hwlab-code-agent-codex-auth", secretKey: "auth.json" })
|
||||
]);
|
||||
const requiredDevConfigMapRefs = Object.freeze([
|
||||
Object.freeze({ configMapName: "hwlab-code-agent-codex-config", configMapKey: "config.toml" })
|
||||
]);
|
||||
const lockAnnotationPrefix = "hwlab.pikastech.local";
|
||||
const lockAnnotationFields = {
|
||||
@@ -1288,6 +1292,13 @@ function parseSecretDescribeKeys(stdout) {
|
||||
return [...keys].sort();
|
||||
}
|
||||
|
||||
function parseKeyLines(stdout) {
|
||||
return String(stdout ?? "")
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function commandObservation(args, result) {
|
||||
return {
|
||||
command: shellCommand("kubectl", args),
|
||||
@@ -1483,6 +1494,97 @@ function secretRefBlocker(ref, observation) {
|
||||
});
|
||||
}
|
||||
|
||||
async function observeRequiredConfigMapRef(ctx, kubectlContext, args, ref) {
|
||||
const existsArgs = ["-n", args.targetNamespace, "get", "configmap", ref.configMapName, "-o", "name"];
|
||||
const keyArgs = [
|
||||
"-n",
|
||||
args.targetNamespace,
|
||||
"get",
|
||||
"configmap",
|
||||
ref.configMapName,
|
||||
"-o",
|
||||
"go-template={{range $k,$v := .data}}{{printf \"%s\\n\" $k}}{{end}}"
|
||||
];
|
||||
const exists = await preflightKubectl(ctx, kubectlContext, existsArgs, { timeoutMs: 10000 });
|
||||
if (exists.code !== 0) {
|
||||
return {
|
||||
configMapName: ref.configMapName,
|
||||
configMapKey: ref.configMapKey,
|
||||
status: "missing-configmap",
|
||||
exists: false,
|
||||
keyPresent: false,
|
||||
keysObserved: [],
|
||||
existenceCommand: shellCommand("kubectl", existsArgs),
|
||||
keyCommand: null,
|
||||
error: commandSummary(exists),
|
||||
configMapValueRead: false,
|
||||
configMapValuePrinted: false
|
||||
};
|
||||
}
|
||||
const keys = await preflightKubectl(ctx, kubectlContext, keyArgs, { timeoutMs: 10000 });
|
||||
if (keys.code !== 0) {
|
||||
return {
|
||||
configMapName: ref.configMapName,
|
||||
configMapKey: ref.configMapKey,
|
||||
status: "key-observation-blocked",
|
||||
exists: true,
|
||||
keyPresent: false,
|
||||
keysObserved: [],
|
||||
existenceCommand: shellCommand("kubectl", existsArgs),
|
||||
keyCommand: shellCommand("kubectl", keyArgs),
|
||||
error: commandSummary(keys),
|
||||
configMapValueRead: false,
|
||||
configMapValuePrinted: false
|
||||
};
|
||||
}
|
||||
const keysObserved = parseKeyLines(keys.stdout);
|
||||
const keyPresent = keysObserved.includes(ref.configMapKey);
|
||||
return {
|
||||
configMapName: ref.configMapName,
|
||||
configMapKey: ref.configMapKey,
|
||||
status: keyPresent ? "present" : "missing-key",
|
||||
exists: true,
|
||||
keyPresent,
|
||||
keysObserved,
|
||||
existenceCommand: shellCommand("kubectl", existsArgs),
|
||||
keyCommand: shellCommand("kubectl", keyArgs),
|
||||
configMapValueRead: false,
|
||||
configMapValuePrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function configMapRefBlocker(ref, observation) {
|
||||
const scope = `configmap:${ref.configMapName}/${ref.configMapKey}`;
|
||||
if (observation.status === "missing-configmap") {
|
||||
return blocker({
|
||||
type: "runtime_blocker",
|
||||
scope,
|
||||
reason: `Required DEV ConfigMap ${ref.configMapName} was not observed in ${defaultNamespace}.`,
|
||||
impact: "The Code Agent Codex stdio provider pod would fail to mount config.toml after mutation.",
|
||||
safeNextAction: `Create or restore ConfigMap ${ref.configMapName}/${ref.configMapKey} in ${defaultNamespace}, then retry DEV CD.`,
|
||||
retryable: true
|
||||
});
|
||||
}
|
||||
if (observation.status === "key-observation-blocked") {
|
||||
return blocker({
|
||||
type: "runtime_blocker",
|
||||
scope,
|
||||
reason: `Required DEV ConfigMap ${ref.configMapName} exists but key names could not be verified.`,
|
||||
impact: "DEV CD cannot prove the Code Agent Codex config mount before applying workloads.",
|
||||
safeNextAction: `Restore key-name visibility for ${ref.configMapName}, then retry.`,
|
||||
retryable: true
|
||||
});
|
||||
}
|
||||
return blocker({
|
||||
type: "runtime_blocker",
|
||||
scope,
|
||||
reason: `Required DEV ConfigMap key ${ref.configMapName}/${ref.configMapKey} was not observed.`,
|
||||
impact: "The Code Agent Codex stdio provider pod would reference a missing config.toml key after mutation.",
|
||||
safeNextAction: `Add key ${ref.configMapKey} to ConfigMap ${ref.configMapName}, then retry DEV CD.`,
|
||||
retryable: true
|
||||
});
|
||||
}
|
||||
|
||||
async function runDevCdPreflight({ ctx, args, kubectlContext }) {
|
||||
const blockers = [];
|
||||
const preflight = {
|
||||
@@ -1502,11 +1604,14 @@ async function runDevCdPreflight({ ctx, args, kubectlContext }) {
|
||||
},
|
||||
controlPlane: null,
|
||||
secretRefs: [],
|
||||
configMaps: [],
|
||||
safety: {
|
||||
writeSideEffectsAttempted: false,
|
||||
prodTouched: false,
|
||||
secretValuesRead: false,
|
||||
secretValuesPrinted: false,
|
||||
configMapValuesRead: false,
|
||||
configMapValuesPrinted: false,
|
||||
secretKeyNamesOnly: true
|
||||
},
|
||||
blockers
|
||||
@@ -1560,14 +1665,20 @@ async function runDevCdPreflight({ ctx, args, kubectlContext }) {
|
||||
}
|
||||
|
||||
if (blockers.length === 0) {
|
||||
preflight.secretRefs = await Promise.all(
|
||||
requiredDevSecretRefs.map((ref) => observeRequiredSecretRef(ctx, kubectlContext, args, ref))
|
||||
);
|
||||
[preflight.secretRefs, preflight.configMaps] = await Promise.all([
|
||||
Promise.all(requiredDevSecretRefs.map((ref) => observeRequiredSecretRef(ctx, kubectlContext, args, ref))),
|
||||
Promise.all(requiredDevConfigMapRefs.map((ref) => observeRequiredConfigMapRef(ctx, kubectlContext, args, ref)))
|
||||
]);
|
||||
for (const [index, observation] of preflight.secretRefs.entries()) {
|
||||
if (observation.status !== "present") {
|
||||
blockers.push(secretRefBlocker(requiredDevSecretRefs[index], observation));
|
||||
}
|
||||
}
|
||||
for (const [index, observation] of preflight.configMaps.entries()) {
|
||||
if (observation.status !== "present") {
|
||||
blockers.push(configMapRefBlocker(requiredDevConfigMapRefs[index], observation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preflight.status = blockers.length === 0 ? "pass" : "blocked";
|
||||
|
||||
@@ -236,7 +236,11 @@ function makeRunCommand({
|
||||
secretRefs = {
|
||||
"hwlab-cloud-api-dev-db": ["database-url"],
|
||||
"hwlab-cloud-api-dev-db-admin": ["admin-url"],
|
||||
"hwlab-code-agent-provider": ["openai-api-key"]
|
||||
"hwlab-code-agent-provider": ["openai-api-key"],
|
||||
"hwlab-code-agent-codex-auth": ["auth.json"]
|
||||
},
|
||||
configMaps = {
|
||||
"hwlab-code-agent-codex-config": ["config.toml"]
|
||||
}
|
||||
} = {}) {
|
||||
let lease = heldLock ? leaseFromLock(heldLock) : null;
|
||||
@@ -314,6 +318,17 @@ function makeRunCommand({
|
||||
stderr: ""
|
||||
};
|
||||
}
|
||||
if (command.includes("kubectl") && args.includes("get") && args.includes("configmap")) {
|
||||
const name = args[args.indexOf("configmap") + 1];
|
||||
const keys = configMaps[name];
|
||||
if (!keys) {
|
||||
return { code: 1, stdout: "", stderr: `Error from server (NotFound): configmaps "${name}" not found` };
|
||||
}
|
||||
if (args.some((arg) => String(arg).startsWith("go-template="))) {
|
||||
return { code: 0, stdout: `${keys.join("\n")}\n`, stderr: "" };
|
||||
}
|
||||
return { code: 0, stdout: `configmap/${name}\n`, stderr: "" };
|
||||
}
|
||||
if (command.includes("kubectl") && args.includes("get") && args.includes("lease")) {
|
||||
if (!lease) return { code: 1, stdout: "", stderr: "Error from server (NotFound): leases.coordination.k8s.io \"hwlab-dev-cd-lock\" not found" };
|
||||
return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" };
|
||||
@@ -1169,11 +1184,18 @@ test("apply preflight passes with D601 native k3s and required SecretRef keys be
|
||||
[
|
||||
"hwlab-cloud-api-dev-db/database-url:present",
|
||||
"hwlab-cloud-api-dev-db-admin/admin-url:present",
|
||||
"hwlab-code-agent-provider/openai-api-key:present"
|
||||
"hwlab-code-agent-provider/openai-api-key:present",
|
||||
"hwlab-code-agent-codex-auth/auth.json:present"
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
preflight.configMaps.map((ref) => `${ref.configMapName}/${ref.configMapKey}:${ref.status}`),
|
||||
["hwlab-code-agent-codex-config/config.toml:present"]
|
||||
);
|
||||
assert.equal(preflight.safety.secretValuesRead, false);
|
||||
assert.equal(preflight.safety.secretValuesPrinted, false);
|
||||
assert.equal(preflight.safety.configMapValuesRead, false);
|
||||
assert.equal(preflight.safety.configMapValuesPrinted, false);
|
||||
|
||||
const firstWriteIndex = commandLog.findIndex((entry) =>
|
||||
entry.command.includes("kubectl") && ["create", "replace", "apply", "patch", "delete", "rollout", "wait", "logs"].some((verb) => entry.args.includes(verb))
|
||||
@@ -1206,7 +1228,8 @@ test("apply preflight blocks missing SecretRef before Lease or side-effect comma
|
||||
commandLog,
|
||||
secretRefs: {
|
||||
"hwlab-cloud-api-dev-db": ["database-url"],
|
||||
"hwlab-cloud-api-dev-db-admin": ["admin-url"]
|
||||
"hwlab-cloud-api-dev-db-admin": ["admin-url"],
|
||||
"hwlab-code-agent-codex-auth": ["auth.json"]
|
||||
}
|
||||
}),
|
||||
httpGetJson: makeHttpGetJson(),
|
||||
@@ -1227,6 +1250,42 @@ test("apply preflight blocks missing SecretRef before Lease or side-effect comma
|
||||
assertNoWriteSideEffects(commandLog);
|
||||
});
|
||||
|
||||
test("apply preflight blocks missing Codex ConfigMap before Lease or workload apply", async () => {
|
||||
const repoRoot = await makeRepo();
|
||||
const commandLog = [];
|
||||
let output = "";
|
||||
const code = await runDevCdApply([
|
||||
"--apply",
|
||||
"--confirm-dev",
|
||||
"--confirmed-non-production",
|
||||
"--owner-task-id",
|
||||
"task-missing-codex-config",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json")
|
||||
], {
|
||||
repoRoot,
|
||||
env: {},
|
||||
runCommand: makeRunCommand({
|
||||
commandLog,
|
||||
configMaps: {}
|
||||
}),
|
||||
httpGetJson: makeHttpGetJson(),
|
||||
now: () => new Date(iso(100000)),
|
||||
stdout: { write: (chunk) => { output += chunk; } }
|
||||
});
|
||||
|
||||
const summary = JSON.parse(output);
|
||||
const writtenReport = JSON.parse(await readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8"));
|
||||
assert.equal(code, 2);
|
||||
assert.equal(summary.status, "blocked");
|
||||
assert.equal(summary.preflight.status, "blocked");
|
||||
assert.equal(summary.blockers.some((blocker) => blocker.scope === "configmap:hwlab-code-agent-codex-config/config.toml"), true);
|
||||
assert.equal(writtenReport.devCdApply.preflight.safety.writeSideEffectsAttempted, false);
|
||||
assert.equal(writtenReport.devCdApply.preflight.safety.configMapValuesPrinted, false);
|
||||
assertNoWriteSideEffects(commandLog);
|
||||
});
|
||||
|
||||
test("apply preflight blocks non-D601 kubeconfig path before kubectl mutation", async () => {
|
||||
const repoRoot = await makeRepo();
|
||||
const commandLog = [];
|
||||
|
||||
Reference in New Issue
Block a user