192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test";
|
|
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { composeToKubernetes, envFromToSecrets } from "../../lib/convert";
|
|
import type { ComposeSpecification, Service } from "../../schema/docker.d";
|
|
import {
|
|
BundleArtifactProvider,
|
|
LocalArtifactProvider,
|
|
createArtifactBundle,
|
|
} from "../../shared/artifacts";
|
|
|
|
const temporaryDirectories: string[] = [];
|
|
|
|
async function temporaryDirectory(): Promise<string> {
|
|
const path = await mkdtemp(join(tmpdir(), "kuber-artifacts-"));
|
|
temporaryDirectories.push(path);
|
|
return path;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories
|
|
.splice(0)
|
|
.map((path) => rm(path, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("conversion artifacts", () => {
|
|
test("bundle rendering matches the default local filesystem rendering", async () => {
|
|
const workspace = await temporaryDirectory();
|
|
const env = "MODE=production\nTOKEN=a=b\n";
|
|
const config = "enabled=true\n";
|
|
await writeFile(join(workspace, ".env"), env);
|
|
await writeFile(join(workspace, "app.conf"), config);
|
|
|
|
const compose = {
|
|
services: {
|
|
app: {
|
|
image: "app",
|
|
env_file: ".env",
|
|
volumes: ["./app.conf:/etc/app.conf:ro"],
|
|
},
|
|
},
|
|
} as ComposeSpecification;
|
|
|
|
const local = await composeToKubernetes("project", compose, workspace);
|
|
const bundle = JSON.parse(
|
|
JSON.stringify(createArtifactBundle({ ".env": env, "app.conf": config })),
|
|
);
|
|
const bundled = await composeToKubernetes(
|
|
"project",
|
|
compose,
|
|
workspace,
|
|
{},
|
|
{},
|
|
new BundleArtifactProvider(bundle),
|
|
);
|
|
|
|
expect(bundled).toEqual(local);
|
|
});
|
|
|
|
test("bundle providers preserve optional and required missing-file behavior", async () => {
|
|
const provider = new BundleArtifactProvider(createArtifactBundle({}));
|
|
|
|
expect(
|
|
await envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{ env_file: [{ path: "missing.env", required: false }] } as Service,
|
|
process.cwd(),
|
|
{},
|
|
provider,
|
|
),
|
|
).toEqual([]);
|
|
await expect(
|
|
envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{ env_file: "missing.env" } as Service,
|
|
process.cwd(),
|
|
{},
|
|
provider,
|
|
),
|
|
).rejects.toThrow("Artifact not found");
|
|
});
|
|
|
|
test("default local providers preserve env-file tilde path behavior", async () => {
|
|
const workspace = await temporaryDirectory();
|
|
await mkdir(join(workspace, "~"));
|
|
await writeFile(join(workspace, "~", "legacy.env"), "LEGACY=yes\n");
|
|
|
|
const [secret] = await envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{ env_file: "~/legacy.env" } as Service,
|
|
workspace,
|
|
);
|
|
expect(secret?.stringData).toEqual({ LEGACY: "yes" });
|
|
});
|
|
|
|
test("strict local providers reject traversal and absolute paths", async () => {
|
|
const workspace = await temporaryDirectory();
|
|
const provider = new LocalArtifactProvider({ workspace, strict: true });
|
|
|
|
await expect(
|
|
envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{ env_file: "../outside.env" } as Service,
|
|
workspace,
|
|
{},
|
|
provider,
|
|
),
|
|
).rejects.toThrow("escapes the workspace");
|
|
await expect(
|
|
envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{ env_file: join(workspace, "absolute.env") } as Service,
|
|
workspace,
|
|
{},
|
|
provider,
|
|
),
|
|
).rejects.toThrow("workspace-relative");
|
|
});
|
|
|
|
test("strict local providers reject symlinks escaping the workspace", async () => {
|
|
const workspace = await temporaryDirectory();
|
|
const outside = await temporaryDirectory();
|
|
await writeFile(join(outside, "secret.env"), "TOKEN=secret\n");
|
|
await symlink(join(outside, "secret.env"), join(workspace, "secret.env"));
|
|
|
|
await expect(
|
|
envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{ env_file: "secret.env" } as Service,
|
|
workspace,
|
|
{},
|
|
new LocalArtifactProvider({ workspace, strict: true }),
|
|
),
|
|
).rejects.toThrow("escapes the workspace");
|
|
});
|
|
|
|
test("strict local providers reject missing files below escaping symlinks", async () => {
|
|
const workspace = await temporaryDirectory();
|
|
const outside = await temporaryDirectory();
|
|
await symlink(outside, join(workspace, "outside"));
|
|
|
|
await expect(
|
|
envFromToSecrets(
|
|
"project",
|
|
"app",
|
|
{
|
|
env_file: [{ path: "outside/missing.env", required: false }],
|
|
} as Service,
|
|
workspace,
|
|
{},
|
|
new LocalArtifactProvider({ workspace, strict: true }),
|
|
),
|
|
).rejects.toThrow("escapes the workspace");
|
|
});
|
|
|
|
test("bundle providers reject unsafe paths and byte or count excesses", () => {
|
|
expect(
|
|
() =>
|
|
new BundleArtifactProvider(createArtifactBundle({ "../secret": "x" })),
|
|
).toThrow("escapes the workspace");
|
|
expect(
|
|
() =>
|
|
new BundleArtifactProvider(createArtifactBundle({ config: "four" }), {
|
|
maxArtifactBytes: 3,
|
|
}),
|
|
).toThrow("exceeds the 3 byte limit");
|
|
expect(
|
|
() =>
|
|
new BundleArtifactProvider(
|
|
createArtifactBundle({ first: "1", second: "2" }),
|
|
{ maxArtifactCount: 1 },
|
|
),
|
|
).toThrow("Artifact count exceeds the 1 limit");
|
|
expect(
|
|
() =>
|
|
new BundleArtifactProvider(
|
|
createArtifactBundle({ first: "12", second: "34" }),
|
|
{ maxTotalBytes: 3 },
|
|
),
|
|
).toThrow("Artifact bytes exceed the 3 byte limit");
|
|
});
|
|
});
|