Files
kuber/tests/server/build-job.test.ts
2026-10-06 15:31:51 +00:00

466 lines
15 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { createBuildJob } from "../../server/build-job";
import {
createBuildpacksJob,
DEFAULT_BUILDPACKS_IMAGE,
} from "../../server/buildpacks-job";
import type { BuildSpec } from "../../shared/build-protocol";
function spec(architecture: "arm64" | "amd64"): BuildSpec {
return {
architecture,
image: "registry.example.com/kuber/demo-web:latest",
context: "apps/web",
dockerfile: "docker/Web.Dockerfile",
target: "production",
buildArgs: ["NODE_ENV=production"],
workspace: `sha256:${"a".repeat(64)}`,
};
}
describe("BuildKit Job generation", () => {
test("quotes multiple image names as one BuildKit image exporter", () => {
const job = createBuildJob({
name: "build-multi",
namespace: "default",
spec: spec("amd64"),
workspaceClaimName: "workspace",
cacheImage: "registry.example.com/cache/demo-web",
pushImage: "registry.example.com/kuber/demo-web:latest",
pushImages: ["registry.example.com/kuber/demo-worker:latest"],
});
expect((job.spec as any).template.spec.containers[0].args).toContain(
'--output=type=image,"name=registry.example.com/kuber/demo-web:latest,registry.example.com/kuber/demo-worker:latest",push=true',
);
});
test.each(["arm64", "amd64"] as const)(
"generates a rootless %s job",
(architecture) => {
const job = createBuildJob({
name: `build-${architecture}`,
namespace: "kuber-system",
spec: spec(architecture),
workspaceClaimName: "build-workspaces",
workspaceSubPath: "snapshot",
cacheImage: "registry.example.com/cache/demo-web",
registrySecretName: "registry-auth",
nodeSelector: { "kubernetes.io/arch": "wrong", pool: "builders" },
});
const jobSpec = job.spec as any;
const pod = jobSpec.template.spec;
const container = pod.containers[0];
expect(pod.nodeSelector["kubernetes.io/arch"]).toBe(architecture);
expect(pod.nodeSelector.pool).toBe("builders");
if (architecture === "amd64") {
expect(pod.tolerations).toEqual([
{
key: "arch",
operator: "Equal",
value: "amd64",
effect: "NoExecute",
},
]);
} else {
expect(pod.tolerations).toBeUndefined();
}
expect(pod.automountServiceAccountToken).toBe(false);
expect(pod.securityContext).toMatchObject({
runAsNonRoot: true,
runAsUser: 1000,
seccompProfile: { type: "Unconfined" },
});
expect(container.securityContext).toEqual({
runAsNonRoot: true,
runAsUser: 1000,
allowPrivilegeEscalation: true,
seccompProfile: { type: "Unconfined" },
appArmorProfile: { type: "Unconfined" },
});
expect(container.env).toContainEqual({
name: "BUILDKITD_FLAGS",
value: "--oci-worker-no-process-sandbox",
});
expect(container.args).toContain(`--opt=platform=linux/${architecture}`);
expect(container.args).toContain(
"--import-cache=type=registry,ref=registry.example.com/cache/demo-web",
);
expect(container.args).toContain(
"--export-cache=type=registry,ref=registry.example.com/cache/demo-web,mode=max",
);
expect(container.args).toContain(
"--output=type=image,name=registry.example.com/kuber/demo-web:latest,push=true",
);
expect(container.volumeMounts).toContainEqual({
name: "workspace",
mountPath: "/workspace",
readOnly: true,
subPath: "snapshot",
});
expect(pod.volumes).toContainEqual({
name: "registry-auth",
secret: {
secretName: "registry-auth",
items: [{ key: ".dockerconfigjson", path: "config.json" }],
},
});
expect(
jobSpec.template.metadata.annotations[
"container.apparmor.security.beta.kubernetes.io/buildkit"
],
).toBe("unconfined");
},
);
test("merges caller tolerations with the amd64 placement toleration", () => {
const amd64Toleration = {
key: "arch",
operator: "Equal",
value: "amd64",
effect: "NoExecute",
};
const customToleration = {
key: "workload",
operator: "Equal",
value: "build",
effect: "NoSchedule",
};
const job = createBuildJob({
name: "build-tolerations",
namespace: "default",
spec: spec("amd64"),
workspaceClaimName: "workspace",
cacheImage: "registry.example.com/cache/demo-web",
tolerations: [customToleration, amd64Toleration],
});
expect((job.spec as any).template.spec.tolerations).toEqual([
customToleration,
amd64Toleration,
]);
});
test("preserves caller tolerations for arm64 without adding architecture placement", () => {
const customToleration = {
key: "workload",
operator: "Equal",
value: "build",
effect: "NoSchedule",
};
const job = createBuildJob({
name: "build-arm64-tolerations",
namespace: "default",
spec: spec("arm64"),
workspaceClaimName: "workspace",
cacheImage: "registry.example.com/cache/demo-web",
tolerations: [customToleration],
});
expect((job.spec as any).template.spec.tolerations).toEqual([
customToleration,
]);
});
test("uses pushImage for output when set", () => {
const job = createBuildJob({
name: "build-push",
namespace: "default",
spec: spec("amd64"),
workspaceClaimName: "workspace",
cacheImage: "registry.example.com/cache/demo-web",
pushImage: "internal.registry:5000/kuber/demo-web:latest",
});
const container = (job.spec as any).template.spec.containers[0];
expect(container.args).toContain(
"--output=type=image,name=internal.registry:5000/kuber/demo-web:latest,push=true",
);
expect(container.args).not.toContainEqual(
expect.stringContaining("--output=type=image,name=registry.example.com"),
);
});
test("adds insecure flags when configured", () => {
const job = createBuildJob({
name: "build-insecure",
namespace: "default",
spec: spec("amd64"),
workspaceClaimName: "workspace",
cacheImage: "internal.registry:5000/cache/demo-web",
pushImage: "internal.registry:5000/kuber/demo-web:latest",
pushRegistryInsecure: true,
cacheRegistryInsecure: true,
});
const container = (job.spec as any).template.spec.containers[0];
expect(container.args).toContain(
"--import-cache=type=registry,ref=internal.registry:5000/cache/demo-web,registry.insecure=true",
);
expect(container.args).toContain(
"--export-cache=type=registry,ref=internal.registry:5000/cache/demo-web,mode=max,registry.insecure=true",
);
expect(container.args).toContain(
"--output=type=image,name=internal.registry:5000/kuber/demo-web:latest,push=true,registry.insecure=true",
);
});
test("no insecure flags when not configured", () => {
const job = createBuildJob({
name: "build-secure",
namespace: "default",
spec: spec("amd64"),
workspaceClaimName: "workspace",
cacheImage: "registry.example.com/cache/demo-web",
});
const container = (job.spec as any).template.spec.containers[0];
for (const arg of container.args) {
expect(arg).not.toContain("registry.insecure");
}
});
test("rejects traversal and invalid Kubernetes names", () => {
expect(() =>
createBuildJob({
name: "Invalid_Name",
namespace: "default",
spec: spec("arm64"),
workspaceClaimName: "workspace",
cacheImage: "cache",
}),
).toThrow("DNS label");
expect(() =>
createBuildJob({
name: "valid",
namespace: "default",
spec: { ...spec("arm64"), context: "../outside" },
workspaceClaimName: "workspace",
cacheImage: "cache",
}),
).toThrow("safe workspace-relative");
expect(() =>
createBuildJob({
name: "valid",
namespace: "default",
spec: spec("arm64"),
workspaceClaimName: "workspace",
workspaceSubPath: "../outside",
cacheImage: "cache",
}),
).toThrow("Workspace subPath");
});
});
describe("Buildpacks Job generation", () => {
test("mounts custom package separately, preserves builtins and passes explicit creator order", () => {
const uri =
"https://github.com/example/bun/releases/download/v1/buildpack.cnb";
const job = createBuildpacksJob({
...options("arm64"),
spec: { ...options("arm64").spec, buildpackUri: uri },
buildpackSubPath: "workspaces/job-buildpack",
});
const pod = (job.spec as any).template.spec;
const creator = pod.containers[0];
expect(creator.args).toContain("--buildpacks");
expect(creator.args).toContain("/custom-buildpacks");
expect(creator.args).toContain("--order");
expect(creator.args).toContain("/package/order.toml");
expect(creator.volumeMounts).toContainEqual({
name: "workspace",
mountPath: "/package",
subPath: "workspaces/job-buildpack",
readOnly: true,
});
expect(creator.volumeMounts).toContainEqual({
name: "custom-buildpacks",
mountPath: "/custom-buildpacks",
readOnly: true,
});
expect(pod.initContainers[0].args[0]).toContain("/cnb/buildpacks/.");
expect(pod.initContainers[1].name).toBe("copy-source");
expect(
pod.initContainers[1].volumeMounts.some(
(mount: any) => mount.mountPath === "/package",
),
).toBe(false);
expect(() =>
createBuildpacksJob({
...options(),
spec: { ...options().spec, buildpackUri: uri },
}),
).toThrow(/staged/);
expect(() =>
createBuildpacksJob({
...options(),
spec: { ...options().spec, buildpackUri: uri },
buildpackSubPath: "../outside",
}),
).toThrow(/safe/);
});
const options = (architecture: "amd64" | "arm64" = "amd64") => ({
name: "build-cnb",
namespace: "builds",
spec: {
...spec(architecture),
builder: "buildpacks" as const,
dockerfile: undefined,
target: undefined,
buildArgs: [],
},
workspaceClaimName: "workspaces",
workspaceSubPath: "snapshot",
cacheImage: `registry.example.com/cache/demo-web-cnb-${architecture}`,
registrySecretName: "registry-auth",
nodeSelector: { pool: "builders", "kubernetes.io/arch": "wrong" },
});
test.each(["amd64", "arm64"] as const)(
"uses pinned lifecycle and isolated writable directories on %s",
(architecture) => {
const job = createBuildpacksJob(options(architecture));
const pod = (job.spec as any).template.spec;
const init = pod.initContainers[0];
const creator = pod.containers[0];
expect(creator.name).toBe("buildkit");
expect(init.image).toBe(DEFAULT_BUILDPACKS_IMAGE);
expect(creator.image).toBe(DEFAULT_BUILDPACKS_IMAGE);
expect(init.command).toEqual(["/bin/sh", "-ec"]);
expect(init.args).toEqual([
'cp -R "$1/." /cnb-app/ && chmod -R u+rwX /cnb-app',
"--",
"/workspace/apps/web",
]);
expect(init.volumeMounts[0]).toEqual({
name: "workspace",
mountPath: "/workspace",
readOnly: true,
subPath: "snapshot",
});
expect(creator.command).toEqual(["/cnb/lifecycle/creator"]);
expect(creator.args).toEqual([
"--app",
"/cnb-app",
"--layers",
"/layers",
"--platform",
"/platform",
"--cache-image",
`registry.example.com/cache/demo-web-cnb-${architecture}`,
"--uid",
"1000",
"--gid",
"1000",
"registry.example.com/kuber/demo-web:latest",
]);
expect(creator.volumeMounts).not.toContainEqual(
expect.objectContaining({ name: "workspace" }),
);
for (const name of ["app", "layers", "cache", "platform", "tmp"])
expect(pod.volumes).toContainEqual({ name, emptyDir: {} });
expect(pod.securityContext).toMatchObject({
runAsUser: 1000,
runAsGroup: 1000,
fsGroup: 1000,
seccompProfile: { type: "RuntimeDefault" },
});
expect(creator.securityContext.allowPrivilegeEscalation).toBe(false);
expect(pod.automountServiceAccountToken).toBe(false);
expect(pod.nodeSelector).toEqual({
pool: "builders",
"kubernetes.io/arch": architecture,
});
expect(pod.tolerations?.length ?? 0).toBe(
architecture === "amd64" ? 1 : 0,
);
expect(creator.env).toContainEqual({
name: "CNB_PLATFORM_API",
value: "0.15",
});
expect(creator.env).toContainEqual({
name: "DOCKER_CONFIG",
value: "/docker-config",
});
expect(creator.volumeMounts).toContainEqual({
name: "registry-auth",
mountPath: "/docker-config",
readOnly: true,
});
expect(pod.volumes).toContainEqual({
name: "registry-auth",
secret: {
secretName: "registry-auth",
items: [{ key: ".dockerconfigjson", path: "config.json" }],
},
});
expect(pod.imagePullSecrets).toEqual([{ name: "registry-auth" }]);
},
);
test("exports additional tags and scopes insecure registry flags", () => {
const job = createBuildpacksJob({
...options(),
cacheImage: "cache.local:5000/demo-cnb-amd64",
pushImage: "internal.local:5000/web:latest",
pushImages: [
"internal.local:5000/worker:latest",
"internal.local:5000/other:latest",
],
pushRegistryInsecure: true,
cacheRegistryInsecure: true,
});
expect((job.spec as any).template.spec.containers[0].args).toEqual([
"--app",
"/cnb-app",
"--layers",
"/layers",
"--platform",
"/platform",
"--cache-image",
"cache.local:5000/demo-cnb-amd64",
"--uid",
"1000",
"--gid",
"1000",
"--insecure-registry",
"internal.local:5000",
"--insecure-registry",
"cache.local:5000",
"--tag",
"internal.local:5000/worker:latest",
"--tag",
"internal.local:5000/other:latest",
"internal.local:5000/web:latest",
]);
});
test("rejects unpinned builders and incompatible options", () => {
expect(() =>
createBuildpacksJob({
...options(),
pushImages: ["another.example.com/worker:latest"],
}),
).toThrow("same registry");
expect(() =>
createBuildpacksJob({
...options(),
buildpacksImage: "heroku/builder:26",
}),
).toThrow("digest-pinned");
expect(() =>
createBuildpacksJob({
...options(),
buildpacksImage: "heroku/builder@sha256:deadbeef",
}),
).toThrow("digest-pinned");
expect(() =>
createBuildpacksJob({
...options(),
spec: { ...options().spec, dockerfile: "Dockerfile" },
}),
).toThrow("does not support");
expect(() =>
createBuildpacksJob({
...options(),
spec: { ...options().spec, context: "../escape" },
}),
).toThrow("safe workspace-relative");
});
});