feat: v2
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user