diff --git a/packages/plugins/sandbox-providers/kubernetes/src/manifest.ts b/packages/plugins/sandbox-providers/kubernetes/src/manifest.ts index da2ff88c..250f2bae 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/manifest.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/manifest.ts @@ -34,15 +34,18 @@ const manifest: PaperclipPluginManifestV1 = { kubeconfig: { type: "string", format: "secret-ref", + pattern: "\\S", description: "Inline kubeconfig YAML. Paste a kubeconfig or an existing Paperclip secret reference; pasted values are stored as company secrets.", }, namespacePrefix: { type: "string", + maxLength: 20, description: "Prefix for the per-company tenant namespace (default: paperclip-).", }, companySlug: { type: "string", + maxLength: 43, description: "Override the auto-derived company slug used in the tenant namespace name.", }, imageRegistry: { @@ -111,7 +114,10 @@ const manifest: PaperclipPluginManifestV1 = { }, }, anyOf: [ - { required: ["inCluster"] }, + { + properties: { inCluster: { const: true } }, + required: ["inCluster"], + }, { required: ["kubeconfig"] }, ], }, diff --git a/packages/plugins/sandbox-providers/kubernetes/src/types.ts b/packages/plugins/sandbox-providers/kubernetes/src/types.ts index 8d984946..a16eacb7 100644 --- a/packages/plugins/sandbox-providers/kubernetes/src/types.ts +++ b/packages/plugins/sandbox-providers/kubernetes/src/types.ts @@ -28,8 +28,8 @@ export const kubernetesProviderConfigSchema = z inCluster: z.boolean().default(false), kubeconfig: z.string().optional(), - namespacePrefix: z.string().regex(/^[a-z0-9-]{1,32}$/).default("paperclip-"), - companySlug: z.string().regex(/^[a-z0-9-]{1,32}$/).optional(), + namespacePrefix: z.string().regex(/^[a-z0-9-]{1,20}$/).default("paperclip-"), + companySlug: z.string().regex(/^[a-z0-9-]{1,43}$/).optional(), imageRegistry: z.string().url().optional(), imageAllowList: z.array(z.string()).default([]), @@ -80,7 +80,7 @@ export const kubernetesProviderConfigSchema = z backend: z.enum(["sandbox-cr", "job"]).default("sandbox-cr"), }) .refine( - (cfg) => cfg.inCluster || cfg.kubeconfig, + (cfg) => cfg.inCluster || (typeof cfg.kubeconfig === "string" && cfg.kubeconfig.trim().length > 0), { message: "kubernetes provider requires one of `inCluster` or `kubeconfig`", diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/manifest.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/manifest.test.ts new file mode 100644 index 00000000..080ceee2 --- /dev/null +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/manifest.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import manifest from "../../src/manifest.js"; + +describe("manifest", () => { + const configSchema = manifest.environmentDrivers[0]?.configSchema as { + properties: Record; + anyOf: Array<{ + properties?: Record; + required?: string[]; + }>; + }; + + it("keeps namespace inputs within the Kubernetes DNS label length limit", () => { + expect(configSchema.properties.namespacePrefix.maxLength).toBe(20); + expect(configSchema.properties.companySlug.maxLength).toBe(43); + }); + + it("requires real Kubernetes credentials instead of only inCluster key presence", () => { + expect(configSchema.properties.kubeconfig.pattern).toBe("\\S"); + expect(configSchema.anyOf).toContainEqual({ + properties: { inCluster: { const: true } }, + required: ["inCluster"], + }); + expect(configSchema.anyOf).toContainEqual({ required: ["kubeconfig"] }); + }); +}); diff --git a/packages/plugins/sandbox-providers/kubernetes/test/unit/types.test.ts b/packages/plugins/sandbox-providers/kubernetes/test/unit/types.test.ts index 49aa65e3..9e85ccc6 100644 --- a/packages/plugins/sandbox-providers/kubernetes/test/unit/types.test.ts +++ b/packages/plugins/sandbox-providers/kubernetes/test/unit/types.test.ts @@ -31,6 +31,21 @@ describe("kubernetesProviderConfigSchema", () => { ).toThrow(); }); + it("bounds namespacePrefix and companySlug so their combination fits a Kubernetes namespace", () => { + expect(() => + parseKubernetesProviderConfig({ inCluster: true, namespacePrefix: "a".repeat(21) }), + ).toThrow(); + expect(() => + parseKubernetesProviderConfig({ inCluster: true, companySlug: "a".repeat(44) }), + ).toThrow(); + }); + + it("rejects whitespace-only kubeconfig", () => { + expect(() => + parseKubernetesProviderConfig({ inCluster: false, kubeconfig: " " }), + ).toThrow(/requires one of `inCluster` or `kubeconfig`/); + }); + it("rejects egressAllowCidrs entries that are not valid CIDR", () => { expect(() => parseKubernetesProviderConfig({ inCluster: true, egressAllowCidrs: ["not-a-cidr"] }),