fix: address 6 critical/minor code review findings (FAR-15)

1. Fix resources.* dotted-key config — UI fields now correctly read
2. Fix operator precedence bug in container status key (add parens)
3. Add missing RBAC checks to testEnvironment (jobs/list, secrets/*, pvc)
4. Add bail timer log message for debuggability
5. Make result-event detection robust to JSON whitespace variations
6. Remove namespace short-circuit so all checks run on first attempt

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Gandalf the Greybeard
2026-04-23 23:15:01 +00:00
parent 9f79efdf36
commit ef73586a41
4 changed files with 32 additions and 24 deletions
+6 -3
View File
@@ -176,7 +176,7 @@ async function waitForPod(
const containerStatuses = pod.status?.containerStatuses ?? [];
// Log phase transitions
const statusKey = `${phase}:${initStatuses.map((s) => s.state?.waiting?.reason ?? s.state?.terminated?.reason ?? "ok").join(",")}:${containerStatuses.map((s) => s.state?.waiting?.reason ?? s.state?.running ? "running" : "waiting").join(",")}`;
const statusKey = `${phase}:${initStatuses.map((s) => s.state?.waiting?.reason ?? s.state?.terminated?.reason ?? "ok").join(",")}:${containerStatuses.map((s) => s.state?.waiting?.reason ?? (s.state?.running ? "running" : "waiting")).join(",")}`;
if (statusKey !== lastStatus) {
const details: string[] = [`phase=${phase}`];
for (const init of initStatuses) {
@@ -301,7 +301,10 @@ export async function streamPodLogsOnce(
if (stopSignal.stopped) {
if (!writable.destroyed) writable.destroy();
if (!bailTimer && bailResolve) {
bailTimer = setTimeout(bailResolve, LOG_STREAM_BAIL_TIMEOUT_MS);
bailTimer = setTimeout(() => {
onLog("stderr", "[paperclip] Log stream bail timer fired — forcing return\n").catch(() => {});
bailResolve!();
}, LOG_STREAM_BAIL_TIMEOUT_MS);
}
}
}, 200);
@@ -958,7 +961,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
// from the beginning of the log, giving us the full output.
// We use a cheap string scan for the result-event guard (avoids a full JSON parse here;
// the authoritative parse happens once below after all fallbacks complete).
const hasResultEvent = stdout.includes('"type":"result"');
const hasResultEvent = stdout.split("\n").some((l) => { try { return JSON.parse(l).type === "result"; } catch { return false; } });
const needsOneShot = !stdout.trim() || (stdout.trim() && !hasResultEvent);
if (needsOneShot) {
if (!stdout.trim()) {
+4 -4
View File
@@ -438,10 +438,10 @@ describe("buildJobManifest", () => {
it("uses configured resource overrides", () => {
ctx.config = {
resources: {
requests: { cpu: "500m", memory: "1Gi" },
limits: { cpu: "2000m", memory: "4Gi" },
},
"resources.requests.cpu": "500m",
"resources.requests.memory": "1Gi",
"resources.limits.cpu": "2000m",
"resources.limits.memory": "4Gi",
};
const { job } = buildJobManifest({ ctx, selfPod });
const resources = job.spec?.template?.spec?.containers[0]?.resources;
+6 -8
View File
@@ -345,7 +345,6 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
const extraArgs = asStringArray(config.extraArgs);
const timeoutSec = asNumber(config.timeoutSec, 0);
const ttlSeconds = asNumber(config.ttlSecondsAfterFinished, 300);
const resources = parseObject(config.resources);
const nodeSelector = parseKeyValueConfig(config.nodeSelector);
const tolerations = Array.isArray(config.tolerations) ? config.tolerations : [];
const extraLabels = parseKeyValueConfig(config.labels);
@@ -427,17 +426,16 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
// Build env vars
const envVars = buildEnvVars(ctx, selfPod, config);
// Resource defaults
const resourceRequests = parseObject(resources.requests);
const resourceLimits = parseObject(resources.limits);
// Resource defaults — UI stores dotted keys (e.g. "resources.requests.cpu")
// as flat config entries, so read them directly from config with the dotted key.
const containerResources: k8s.V1ResourceRequirements = {
requests: {
cpu: asString(resourceRequests.cpu, "1000m"),
memory: asString(resourceRequests.memory, "2Gi"),
cpu: asString(config["resources.requests.cpu"], "1000m"),
memory: asString(config["resources.requests.memory"], "2Gi"),
},
limits: {
cpu: asString(resourceLimits.cpu, "4000m"),
memory: asString(resourceLimits.memory, "8Gi"),
cpu: asString(config["resources.limits.cpu"], "4000m"),
memory: asString(config["resources.limits.memory"], "8Gi"),
},
};
+16 -9
View File
@@ -85,8 +85,13 @@ async function checkRbac(
{ resource: "jobs", group: "batch", verb: "create", code: "k8s_rbac_job_create", label: "create Jobs" },
{ resource: "jobs", group: "batch", verb: "delete", code: "k8s_rbac_job_delete", label: "delete Jobs" },
{ resource: "jobs", group: "batch", verb: "get", code: "k8s_rbac_job_get", label: "get Jobs" },
{ resource: "jobs", group: "batch", verb: "list", code: "k8s_rbac_job_list", label: "list Jobs" },
{ resource: "pods", group: "", verb: "list", code: "k8s_rbac_pod_list", label: "list Pods" },
{ resource: "pods/log", group: "", verb: "get", code: "k8s_rbac_pod_log", label: "get Pod logs" },
{ resource: "secrets", group: "", verb: "create", code: "k8s_rbac_secret_create", label: "create Secrets" },
{ resource: "secrets", group: "", verb: "delete", code: "k8s_rbac_secret_delete", label: "delete Secrets" },
{ resource: "secrets", group: "", verb: "get", code: "k8s_rbac_secret_get", label: "get Secrets" },
{ resource: "persistentvolumeclaims", group: "", verb: "get", code: "k8s_rbac_pvc_get", label: "get PersistentVolumeClaims" },
];
for (const check of rbacChecks) {
@@ -221,16 +226,18 @@ export async function testEnvironment(
// 2. Target namespace exists
const nsOk = await checkNamespace(namespace, selfPod.namespace, checks, kubeconfigPath);
if (!nsOk) {
return { adapterType: ctx.adapterType, status: summarizeStatus(checks), checks, testedAt: new Date().toISOString() };
}
// 3-5. Run remaining checks in parallel
await Promise.all([
checkRbac(namespace, checks, kubeconfigPath),
checkSecret(namespace, secretRef, checks, kubeconfigPath),
checkPvc(selfPod, checks, kubeconfigPath),
]);
// 3-5. Run remaining checks even if namespace check failed so operators see
// all issues at once instead of fixing them one at a time.
if (nsOk) {
await Promise.all([
checkRbac(namespace, checks, kubeconfigPath),
checkSecret(namespace, secretRef, checks, kubeconfigPath),
checkPvc(selfPod, checks, kubeconfigPath),
]);
} else {
await checkRbac(namespace, checks, kubeconfigPath);
}
return {
adapterType: ctx.adapterType,