Release 2.6.2-rc1
This commit is contained in:
@@ -236,6 +236,155 @@ async function internalFixture(
|
||||
}
|
||||
|
||||
describe("build controller", () => {
|
||||
test("correlates a UUID build request with the stored build and emits only summary keys", async () => {
|
||||
const { controller, request, store } = await fixture();
|
||||
request.id = "123e4567-e89b-42d3-a456-426614174000";
|
||||
const lines: string[] = [];
|
||||
const output = spyOn(console, "info").mockImplementation((line) => {
|
||||
lines.push(String(line));
|
||||
});
|
||||
try {
|
||||
await controller.submitBuild(request);
|
||||
const summary = JSON.parse(lines[0]!);
|
||||
const record = await store.getBuild(request.id);
|
||||
expect(summary.buildRequestId).toBe(request.id);
|
||||
expect(record?.metadata.name).toBe(summary.buildRequestId);
|
||||
expect(record?.spec.jobName).toMatch(/^kuber-build-/);
|
||||
expect(Object.keys(summary).sort()).toEqual(
|
||||
[
|
||||
"buildRequestId",
|
||||
"elapsedMs",
|
||||
"event",
|
||||
"jobCreated",
|
||||
"materialize",
|
||||
"outcome",
|
||||
"phases",
|
||||
"queueWaitMs",
|
||||
].sort(),
|
||||
);
|
||||
expect(lines[0]).not.toContain(request.spec.image);
|
||||
expect(lines[0]).not.toContain(request.spec.workspace);
|
||||
} finally {
|
||||
output.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("ignores summary logger failures without changing build success", async () => {
|
||||
const { controller, request } = await fixture();
|
||||
const output = spyOn(console, "info").mockImplementation(() => {
|
||||
throw new Error("logger unavailable");
|
||||
});
|
||||
try {
|
||||
const status = await controller.submitBuild(request);
|
||||
expect(status.state).toBe("queued");
|
||||
} finally {
|
||||
output.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("attributes deferred store work and serialized queue wait without logging identifiers", async () => {
|
||||
let release!: () => void;
|
||||
let entered!: () => void;
|
||||
const blocked = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const started = new Promise<void>((resolve) => {
|
||||
entered = resolve;
|
||||
});
|
||||
class SlowStore extends MemoryBuildStore {
|
||||
override async createBuild(record: BuildRecord) {
|
||||
entered();
|
||||
await blocked;
|
||||
return super.createBuild(record);
|
||||
}
|
||||
}
|
||||
const { controller, request } = await fixture(1024, new SlowStore());
|
||||
const lines: string[] = [];
|
||||
const output = spyOn(console, "info").mockImplementation((line) => {
|
||||
lines.push(String(line));
|
||||
});
|
||||
try {
|
||||
const first = controller.submitBuild(request);
|
||||
await started;
|
||||
const second = controller.submitBuild(request);
|
||||
await Bun.sleep(25);
|
||||
release();
|
||||
await Promise.all([first, second]);
|
||||
expect(lines).toHaveLength(2);
|
||||
const [created, existing] = lines.map((line) => JSON.parse(line));
|
||||
expect(created.phases.recordCreateMs).toBeGreaterThan(15);
|
||||
expect(created.phases.materializeMs).toBeGreaterThan(0);
|
||||
expect(existing.queueWaitMs).toBeGreaterThan(15);
|
||||
expect(existing.jobCreated).toBe(false);
|
||||
expect(existing.phases.materializeMs).toBeUndefined();
|
||||
for (const line of lines) {
|
||||
expect(line).not.toContain(request.id);
|
||||
expect(line).not.toContain(request.spec.workspace);
|
||||
expect(line).not.toContain(request.spec.image);
|
||||
expect(line).not.toContain("Dockerfile");
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
output.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("attributes deferred materialization and logs one safe failure summary", async () => {
|
||||
const { cas, store, kubernetes, request, root } = await fixture();
|
||||
let release!: () => void;
|
||||
let entered!: () => void;
|
||||
const blocked = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const started = new Promise<void>((resolve) => {
|
||||
entered = resolve;
|
||||
});
|
||||
const secret = "private-path-and-credential";
|
||||
const controller = new BuildController({
|
||||
cas,
|
||||
store,
|
||||
kubernetes,
|
||||
namespace: "builds",
|
||||
workspaceRoot: join(root, "workspaces"),
|
||||
workspaceClaimName: "workspaces",
|
||||
cacheImage: "cache",
|
||||
materialize: async () => {
|
||||
entered();
|
||||
await blocked;
|
||||
throw new Error(secret);
|
||||
},
|
||||
});
|
||||
const lines: string[] = [];
|
||||
const output = spyOn(console, "info").mockImplementation((line) => {
|
||||
lines.push(String(line));
|
||||
});
|
||||
try {
|
||||
const submission = controller.submitBuild(request);
|
||||
const failure = submission.catch((error: unknown) => error);
|
||||
await started;
|
||||
await Bun.sleep(25);
|
||||
release();
|
||||
const error = await failure;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe(secret);
|
||||
expect(lines).toHaveLength(1);
|
||||
const summary = JSON.parse(lines[0]!);
|
||||
expect(summary).toMatchObject({
|
||||
event: "build_submission_timing",
|
||||
outcome: "failure",
|
||||
jobCreated: false,
|
||||
});
|
||||
expect(summary.phases.materializeMs).toBeGreaterThan(15);
|
||||
expect(summary.phases.failureCleanupMs).toBeGreaterThanOrEqual(0);
|
||||
expect(summary.phases.jobCreateMs).toBeUndefined();
|
||||
expect(lines[0]).not.toContain(secret);
|
||||
expect(lines[0]).not.toContain(request.id);
|
||||
} finally {
|
||||
release();
|
||||
output.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("publishes multiple service destinations in one Job and resolves every canonical reference", async () => {
|
||||
const resolved: string[] = [];
|
||||
const { controller, request, kubernetes } = await internalFixture(
|
||||
|
||||
@@ -40,16 +40,25 @@ class MemoryPersistence implements MaintenancePersistence {
|
||||
const lease = { acquire: async () => ({ release: async () => {} }) };
|
||||
|
||||
describe("maintenance override", () => {
|
||||
test("normalizes only DNS hostnames", () => {
|
||||
test("normalizes DNS hostnames and leading wildcards only", () => {
|
||||
expect(normalizeMaintenanceHost(" Sub.Domain.COM. ")).toBe(
|
||||
"sub.domain.com",
|
||||
);
|
||||
expect(normalizeMaintenanceHost(" *.EXAMPLE.COM. ")).toBe("*.example.com");
|
||||
expect(normalizeMaintenanceHost(" *.Sub.Example.COM. ")).toBe("*.sub.example.com");
|
||||
for (const host of [
|
||||
"http://example.com",
|
||||
"example.com:443",
|
||||
"127.0.0.1",
|
||||
"[::1]",
|
||||
"*.example.com",
|
||||
"*",
|
||||
"*.",
|
||||
"example.*.com",
|
||||
"a.*.example.com",
|
||||
"**.example.com",
|
||||
"*.*.example.com",
|
||||
"*.example.com:443",
|
||||
"*.127.0.0.1",
|
||||
"example",
|
||||
"a..com",
|
||||
])
|
||||
@@ -74,6 +83,28 @@ describe("maintenance override", () => {
|
||||
expect(persistence.state).toEqual({ hosts: [] });
|
||||
});
|
||||
|
||||
test("persists normalized wildcards and renders them as Host rules", async () => {
|
||||
const persistence = new MemoryPersistence();
|
||||
const service = new MaintenanceService(persistence, lease);
|
||||
expect(await service.set(" *.EXAMPLE.COM. ", true)).toMatchObject({
|
||||
host: "*.example.com",
|
||||
enabled: true,
|
||||
hosts: ["*.example.com"],
|
||||
});
|
||||
expect(persistence.state).toEqual({ hosts: ["*.example.com"] });
|
||||
expect(persistence.resources.at(-1)).toMatchObject({
|
||||
spec: { routes: [{ match: "Host(`*.example.com`)" }] },
|
||||
});
|
||||
expect(await service.status("*.EXAMPLE.COM.")).toMatchObject({
|
||||
host: "*.example.com",
|
||||
enabled: true,
|
||||
});
|
||||
expect(await service.set("*.example.com", false)).toMatchObject({
|
||||
enabled: false,
|
||||
hosts: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("renders the single shared error route and rewrite middleware", () => {
|
||||
expect(maintenanceMiddleware()).toMatchObject({
|
||||
metadata: { name: "maintenance-override", namespace: "routing" },
|
||||
@@ -100,6 +131,9 @@ describe("maintenance override", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(maintenanceRoute(["*.example.com", "a.example.com"])).toMatchObject({
|
||||
spec: { routes: [{ match: "Host(`*.example.com`) || Host(`a.example.com`)" }] },
|
||||
});
|
||||
});
|
||||
|
||||
test("maps an unavailable global lease to a retryable API conflict", async () => {
|
||||
|
||||
@@ -26,6 +26,59 @@ function digest(data: Uint8Array | string): Sha256Digest {
|
||||
}
|
||||
|
||||
describe("source materialization", () => {
|
||||
test("collects aggregate CAS and filesystem timing with manifest counts", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-materialize-"));
|
||||
roots.push(root);
|
||||
const data = Buffer.from("hello");
|
||||
const manifest: WorkspaceManifest = {
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files: [
|
||||
{
|
||||
path: "example",
|
||||
type: "file",
|
||||
digest: digest(data),
|
||||
size: data.byteLength,
|
||||
mode: 0o644,
|
||||
},
|
||||
],
|
||||
};
|
||||
const manifestData = Buffer.from(JSON.stringify(manifest));
|
||||
const workspace = digest(manifestData);
|
||||
let release!: () => void;
|
||||
let entered!: () => void;
|
||||
const blocked = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const started = new Promise<void>((resolve) => {
|
||||
entered = resolve;
|
||||
});
|
||||
const timing = { casReadMs: 0, fsWriteMs: 0 };
|
||||
const submission = materializeWorkspace(
|
||||
{
|
||||
get: async (requested) => {
|
||||
if (requested === workspace) return manifestData;
|
||||
entered();
|
||||
await blocked;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
workspace,
|
||||
join(root, "workspace"),
|
||||
timing,
|
||||
);
|
||||
await started;
|
||||
await Bun.sleep(25);
|
||||
release();
|
||||
await submission;
|
||||
expect(timing).toMatchObject({
|
||||
fileCount: 1,
|
||||
manifestBytes: manifestData.byteLength,
|
||||
fileBytes: data.byteLength,
|
||||
});
|
||||
expect(timing.casReadMs).toBeGreaterThan(15);
|
||||
expect(timing.fsWriteMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("bounds concurrent CAS reads while materializing many files", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-materialize-"));
|
||||
roots.push(root);
|
||||
@@ -151,6 +204,28 @@ describe("source materialization", () => {
|
||||
),
|
||||
),
|
||||
).toThrow("conflicts");
|
||||
for (const [files, conflict] of [
|
||||
[["a/b/c", "a"], "Workspace path conflicts with a directory: a"],
|
||||
[["a/b/c", "a/b"], "Workspace path conflicts with a directory: a/b"],
|
||||
[["a", "a/b/c"], "Workspace path conflicts with a file: a/b/c"],
|
||||
] as const) {
|
||||
expect(() =>
|
||||
parseWorkspaceManifest(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files: files.map((path) => ({
|
||||
path,
|
||||
type: "file",
|
||||
digest: `sha256:${"a".repeat(64)}`,
|
||||
size: 0,
|
||||
mode: 0o644,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
),
|
||||
).toThrow(conflict);
|
||||
}
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-materialize-"));
|
||||
roots.push(root);
|
||||
@@ -178,4 +253,20 @@ describe("source materialization", () => {
|
||||
).rejects.toThrow("Unsafe symlink");
|
||||
await expect(lstat(destination)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("accepts a large manifest with distinct nested paths", () => {
|
||||
const manifest: WorkspaceManifest = {
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files: Array.from({ length: 3_000 }, (_, index) => ({
|
||||
path: `dir-${index}/nested/file`,
|
||||
type: "file" as const,
|
||||
digest: `sha256:${"a".repeat(64)}` as Sha256Digest,
|
||||
size: 0,
|
||||
mode: 0o644 as const,
|
||||
})),
|
||||
};
|
||||
expect(
|
||||
parseWorkspaceManifest(Buffer.from(JSON.stringify(manifest))),
|
||||
).toEqual(manifest);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user