diff --git a/src/server/execute.ts b/src/server/execute.ts index b5a4b91..85b2e3b 100644 --- a/src/server/execute.ts +++ b/src/server/execute.ts @@ -356,8 +356,20 @@ export async function execute(ctx: AdapterExecutionContext): Promise {}); return; } @@ -550,11 +572,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise = {}): SelfPodInfo { pvcClaimName: "paperclip-data", secretVolumes: [], inheritedEnv: {}, + inheritedEnvValueFrom: [], + inheritedEnvFrom: [], ...overrides, }; } @@ -331,6 +333,50 @@ describe("buildJobManifest", () => { const apiUrl = job.spec?.template?.spec?.containers[0]?.env?.find((e) => e.name === "PAPERCLIP_API_URL"); expect(apiUrl?.value).toBe("http://paperclip:8080"); }); + + it("includes valueFrom env vars from selfPod", () => { + selfPod.inheritedEnvValueFrom = [ + { name: "ANTHROPIC_API_KEY", valueFrom: { secretKeyRef: { name: "api-keys", key: "anthropic" } } }, + ]; + const { job } = buildJobManifest({ ctx, selfPod }); + const envList = job.spec?.template?.spec?.containers[0]?.env ?? []; + const apiKeyEntry = envList.find((e) => e.name === "ANTHROPIC_API_KEY"); + expect(apiKeyEntry?.valueFrom?.secretKeyRef?.name).toBe("api-keys"); + expect(apiKeyEntry?.valueFrom?.secretKeyRef?.key).toBe("anthropic"); + expect(apiKeyEntry?.value).toBeUndefined(); + }); + + it("literal env overrides valueFrom with the same name", () => { + selfPod.inheritedEnv = { MY_VAR: "literal-value" }; + selfPod.inheritedEnvValueFrom = [ + { name: "MY_VAR", valueFrom: { secretKeyRef: { name: "sec", key: "k" } } }, + ]; + const { job } = buildJobManifest({ ctx, selfPod }); + const envList = job.spec?.template?.spec?.containers[0]?.env ?? []; + const myVar = envList.filter((e) => e.name === "MY_VAR"); + expect(myVar).toHaveLength(1); + expect(myVar[0]?.value).toBe("literal-value"); + expect(myVar[0]?.valueFrom).toBeUndefined(); + }); + + it("includes envFrom sources from selfPod on the container", () => { + selfPod.inheritedEnvFrom = [ + { secretRef: { name: "api-secrets" } }, + { configMapRef: { name: "app-config" } }, + ]; + const { job } = buildJobManifest({ ctx, selfPod }); + const container = job.spec?.template?.spec?.containers[0]; + expect(container?.envFrom).toHaveLength(2); + expect(container?.envFrom?.[0]?.secretRef?.name).toBe("api-secrets"); + expect(container?.envFrom?.[1]?.configMapRef?.name).toBe("app-config"); + }); + + it("omits envFrom when selfPod has none", () => { + selfPod.inheritedEnvFrom = []; + const { job } = buildJobManifest({ ctx, selfPod }); + const container = job.spec?.template?.spec?.containers[0]; + expect(container?.envFrom).toBeUndefined(); + }); }); describe("resources", () => { diff --git a/src/server/job-manifest.ts b/src/server/job-manifest.ts index 74a4276..849fca7 100644 --- a/src/server/job-manifest.ts +++ b/src/server/job-manifest.ts @@ -148,12 +148,22 @@ function buildEnvVars( // HOME must be /paperclip to match PVC mount and enable session resume merged.HOME = "/paperclip"; - // Convert to V1EnvVar array + // Convert literal env to V1EnvVar array const envVars: k8s.V1EnvVar[] = Object.entries(merged).map(([name, value]) => ({ name, value, })); + // Append valueFrom entries from the Deployment container (secretKeyRef, + // configMapKeyRef, fieldRef, etc.). Skip any whose name was already set + // by a literal value — the literal value wins (same precedence as above). + const literalNames = new Set(Object.keys(merged)); + for (const entry of selfPod.inheritedEnvValueFrom) { + if (!literalNames.has(entry.name)) { + envVars.push(entry); + } + } + return envVars; } @@ -377,6 +387,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult { workingDir, command: ["sh", "-c", mainCommand], env: envVars, + ...(selfPod.inheritedEnvFrom.length > 0 ? { envFrom: selfPod.inheritedEnvFrom } : {}), volumeMounts, securityContext, resources: containerResources, diff --git a/src/server/k8s-client.ts b/src/server/k8s-client.ts index bd908ff..f2ea4ef 100644 --- a/src/server/k8s-client.ts +++ b/src/server/k8s-client.ts @@ -20,8 +20,12 @@ export interface SelfPodInfo { dnsConfig: k8s.V1PodDNSConfig | undefined; pvcClaimName: string | null; secretVolumes: SelfPodSecretVolume[]; - /** Env vars inherited from the Deployment container. */ + /** Env vars inherited from the Deployment container (literal name/value pairs). */ inheritedEnv: Record; + /** Env vars with valueFrom (secretKeyRef, configMapKeyRef, etc.) from the Deployment container. */ + inheritedEnvValueFrom: k8s.V1EnvVar[]; + /** envFrom sources (secretRef, configMapRef) from the Deployment container. */ + inheritedEnvFrom: k8s.V1EnvFromSource[]; } let cachedSelfPod: SelfPodInfo | null = null; @@ -134,12 +138,21 @@ export async function getSelfPodInfo(kubeconfigPath?: string): Promise = {}; + const inheritedEnvValueFrom: k8s.V1EnvVar[] = []; for (const envItem of mainContainer.env ?? []) { if (!envItem.name) continue; - const value = envItem.value ?? ""; - if (value) inheritedEnv[envItem.name] = value; + if (envItem.valueFrom) { + // Preserve valueFrom entries (secretKeyRef, configMapKeyRef, fieldRef, etc.) + inheritedEnvValueFrom.push({ name: envItem.name, valueFrom: envItem.valueFrom }); + } else { + const value = envItem.value ?? ""; + if (value) inheritedEnv[envItem.name] = value; + } } + // Capture envFrom sources (secretRef, configMapRef) from the container spec + const inheritedEnvFrom: k8s.V1EnvFromSource[] = mainContainer.envFrom ?? []; + cachedSelfPod = { namespace, image: mainContainer.image, @@ -150,6 +163,8 @@ export async function getSelfPodInfo(kubeconfigPath?: string): Promise