feat: add .kuberrc.ts configuration
This commit is contained in:
@@ -63,6 +63,9 @@ describe("CLI command definitions", () => {
|
||||
type: "string",
|
||||
alias: "o",
|
||||
});
|
||||
expect((main as Command).args?.config).toMatchObject({
|
||||
type: "string",
|
||||
});
|
||||
});
|
||||
|
||||
test("defines both database inspection subcommands", () => {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import type { V1Deployment } from "@kubernetes/client-node";
|
||||
import {
|
||||
DEFAULT_BUILDERS,
|
||||
DEFAULT_REGISTRY,
|
||||
DEFAULT_ROLLOUT_TIMEOUT_MS,
|
||||
extractConfigArgument,
|
||||
loadConfig,
|
||||
resolveConfigArgument,
|
||||
} from "../../lib/config";
|
||||
import { composeToKubernetes } from "../../lib/convert";
|
||||
|
||||
const directories: string[] = [];
|
||||
|
||||
async function temporaryDirectory() {
|
||||
const directory = await mkdtemp(join(tmpdir(), "kuber-config-"));
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
describe("kuber config", () => {
|
||||
test("uses operational defaults when .kuberrc.ts is absent", async () => {
|
||||
const cwd = await temporaryDirectory();
|
||||
expect(await loadConfig(cwd)).toMatchObject({
|
||||
project: cwd.split("/").at(-1),
|
||||
registry: DEFAULT_REGISTRY,
|
||||
builders: DEFAULT_BUILDERS,
|
||||
rolloutTimeoutMs: DEFAULT_ROLLOUT_TIMEOUT_MS,
|
||||
configFile: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("loads TypeScript from a custom config path", async () => {
|
||||
const cwd = await temporaryDirectory();
|
||||
const configFile = join(cwd, "production.kuberrc.ts");
|
||||
await Bun.write(
|
||||
configFile,
|
||||
`export default {
|
||||
project: "configured-project",
|
||||
composeFile: "deploy/compose.yml",
|
||||
registry: "registry.example.com/",
|
||||
builders: { amd64: "user@amd", remoteRoot: "build-root" },
|
||||
rolloutTimeoutMs: 42_000,
|
||||
async preBuild() {},
|
||||
};`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(cwd, "production.kuberrc.ts");
|
||||
expect(config).toMatchObject({
|
||||
project: "configured-project",
|
||||
composeFile: join(cwd, "deploy", "compose.yml"),
|
||||
registry: "registry.example.com",
|
||||
builders: {
|
||||
amd64: "user@amd",
|
||||
arm64: DEFAULT_BUILDERS.arm64,
|
||||
remoteRoot: "build-root",
|
||||
},
|
||||
rolloutTimeoutMs: 42_000,
|
||||
configFile,
|
||||
});
|
||||
expect(config.preBuild).toBeFunction();
|
||||
});
|
||||
|
||||
test("requires explicit config paths to exist", async () => {
|
||||
const cwd = await temporaryDirectory();
|
||||
await expect(loadConfig(cwd, "missing.ts")).rejects.toThrow(
|
||||
"Config file cannot be found",
|
||||
);
|
||||
});
|
||||
|
||||
test("requires a default object export", async () => {
|
||||
const cwd = await temporaryDirectory();
|
||||
await Bun.write(join(cwd, ".kuberrc.ts"), "export const project = 'bad';");
|
||||
await expect(loadConfig(cwd)).rejects.toThrow(
|
||||
"must default export a configuration object",
|
||||
);
|
||||
});
|
||||
|
||||
test("validates hook and operational values", async () => {
|
||||
const cwd = await temporaryDirectory();
|
||||
await Bun.write(
|
||||
join(cwd, ".kuberrc.ts"),
|
||||
"export default { preBuild: true, rolloutTimeoutMs: -1 };",
|
||||
);
|
||||
await expect(loadConfig(cwd)).rejects.toThrow("preBuild");
|
||||
});
|
||||
|
||||
test("uses the configured registry in rendered build images", async () => {
|
||||
const resources = await composeToKubernetes(
|
||||
"project",
|
||||
{ services: { app: { build: "." } } },
|
||||
process.cwd(),
|
||||
{},
|
||||
"registry.example.com/team",
|
||||
);
|
||||
const deployment = resources.find(
|
||||
(resource) => resource.kind === "Deployment",
|
||||
) as V1Deployment | undefined;
|
||||
expect(deployment?.spec?.template.spec?.containers[0]?.image).toBe(
|
||||
"registry.example.com/team/kuber/project-app:latest",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("global config argument", () => {
|
||||
test.each([
|
||||
[["--config", "one.ts", "up"], "one.ts"],
|
||||
[["up", "--config", "two.ts"], "two.ts"],
|
||||
[["--config=three.ts", "up"], "three.ts"],
|
||||
])("reads a config path from %j", (args, expected) => {
|
||||
expect(resolveConfigArgument(args)).toBe(expected);
|
||||
});
|
||||
|
||||
test("rejects a config option without a path", () => {
|
||||
expect(() => resolveConfigArgument(["up", "--config"])).toThrow(
|
||||
"requires a path",
|
||||
);
|
||||
});
|
||||
|
||||
test("does not parse config arguments after --", () => {
|
||||
expect(
|
||||
resolveConfigArgument(["exec", "app", "--", "--config", "x.ts"]),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("removes the global option before subcommand dispatch", () => {
|
||||
expect(
|
||||
extractConfigArgument(["logs", "--config", "production.ts"]),
|
||||
).toEqual({
|
||||
configPath: "production.ts",
|
||||
rawArgs: ["logs"],
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects duplicate config options", () => {
|
||||
expect(() =>
|
||||
extractConfigArgument(["--config=one.ts", "--config=two.ts"]),
|
||||
).toThrow("only be specified once");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user