diff --git a/packages/plugins/sandbox-providers/kubernetes/src/network-policy.ts b/packages/plugins/sandbox-providers/kubernetes/src/network-policy.ts index c13652c0..65e07f3e 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/network-policy.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/network-policy.ts @@ -92,6 +92,7 @@ export function buildNetworkPolicyManifests(input: BuildNetworkPolicyInput): Rec : []), ...input.egressAllowCidrs.map((cidr) => ({ to: [{ ipBlock: { cidr } }], + ports: [{ protocol: "TCP", port: 443 }], })), ], }, diff --git a/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts b/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts index 9578260e..e71f2047 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/plugin.ts @@ -490,7 +490,7 @@ const plugin = definePlugin({ return { exitCode: execResult.exitCode, - timedOut: false, + timedOut: execResult.timedOut, stdout: execResult.stdout, stderr: execResult.stderr, metadata: { diff --git a/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts b/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts index 7392fad6..4d7d7f83 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/pod-exec.ts @@ -14,6 +14,13 @@ import { Exec } from "@kubernetes/client-node"; import { PassThrough } from "node:stream"; import type { KubeConfig } from "@kubernetes/client-node"; +export interface ExecInPodResult { + exitCode: number; + timedOut: boolean; + stdout: string; + stderr: string; +} + export async function execInPod( kc: KubeConfig, namespace: string, @@ -22,7 +29,7 @@ export async function execInPod( command: string[], stdin?: string, timeoutMs?: number, -): Promise<{ exitCode: number; stdout: string; stderr: string }> { +): Promise { const exec = new Exec(kc); const stdoutStream = new PassThrough(); const stderrStream = new PassThrough(); @@ -43,25 +50,26 @@ export async function execInPod( stderrData += chunk.toString("utf-8"); }); - return await new Promise<{ exitCode: number; stdout: string; stderr: string }>( + return await new Promise( (resolve, reject) => { let settled = false; const timeout = typeof timeoutMs === "number" && timeoutMs > 0 ? setTimeout(() => { - finishWithTransportFailure(`Kubernetes exec timed out after ${timeoutMs}ms`); + finishWithTransportFailure(`Kubernetes exec timed out after ${timeoutMs}ms`, true); }, timeoutMs) : null; - const finish = (result: { exitCode: number; stdout: string; stderr: string }) => { + const finish = (result: ExecInPodResult) => { if (settled) return; settled = true; if (timeout) clearTimeout(timeout); resolve(result); }; - const finishWithTransportFailure = (message: string) => { + const finishWithTransportFailure = (message: string, timedOut = false) => { const separator = stderrData.length > 0 && !stderrData.endsWith("\n") ? "\n" : ""; finish({ exitCode: 1, + timedOut, stdout: stdoutData, stderr: `${stderrData}${separator}${message}`, }); @@ -80,7 +88,7 @@ export async function execInPod( (status) => { // status.status is "Success" | "Failure" if (status.status === "Success") { - finish({ exitCode: 0, stdout: stdoutData, stderr: stderrData }); + finish({ exitCode: 0, timedOut: false, stdout: stdoutData, stderr: stderrData }); return; } // On failure, the exit code surfaces via @@ -93,7 +101,7 @@ export async function execInPod( const exitCode = exitCodeCause?.message ? Number(exitCodeCause.message) : 1; - finish({ exitCode, stdout: stdoutData, stderr: stderrData }); + finish({ exitCode, timedOut: false, stdout: stdoutData, stderr: stderrData }); }, ); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts index db5d1101..aec46bb1 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/network-policy.test.ts @@ -39,10 +39,11 @@ describe("buildNetworkPolicyManifests", () => { it("includes user-supplied CIDRs in egress allow", () => { const [, egress] = buildNetworkPolicyManifests({ ...baseInput, egressAllowCidrs: ["10.0.0.0/8"] }); - const cidrRule = egress.spec.egress.find((r: { to: { ipBlock?: { cidr: string } }[] }) => + const cidrRule = egress.spec.egress.find((r: { to: { ipBlock?: { cidr: string } }[]; ports?: { protocol: string; port: number }[] }) => r.to.some((t) => t.ipBlock?.cidr === "10.0.0.0/8"), ); expect(cidrRule).toBeDefined(); + expect(cidrRule?.ports).toEqual([{ protocol: "TCP", port: 443 }]); }); it("adds a public HTTPS fallback when standard mode receives FQDN allow-list entries", () => { diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts index 513289d1..24648da1 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/pod-exec.test.ts @@ -22,7 +22,7 @@ describe("execInPod", () => { }); const result = await execInPod({} as never, "ns", "pod-1", "agent", ["echo", "ok"]); - expect(result).toEqual({ exitCode: 0, stdout: "ok\n", stderr: "" }); + expect(result).toEqual({ exitCode: 0, timedOut: false, stdout: "ok\n", stderr: "" }); }); it("returns an execution failure if the websocket closes before a status frame", async () => { @@ -35,6 +35,7 @@ describe("execInPod", () => { await expect(resultPromise).resolves.toMatchObject({ exitCode: 1, + timedOut: false, stderr: expect.stringContaining("websocket closed before status frame"), }); }); @@ -45,6 +46,7 @@ describe("execInPod", () => { const result = await execInPod({} as never, "ns", "pod-1", "agent", ["sleep", "60"], undefined, 5); expect(result.exitCode).toBe(1); + expect(result.timedOut).toBe(true); expect(result.stderr).toContain("Kubernetes exec timed out after 5ms"); }); });