diff --git a/README.md b/README.md index eb05f1d..b4b34a0 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ export default { Operational defaults: -- `project`: current working directory name +- `project`: Compose top-level `name`, falling back to the current working directory name - `composeFile`: the first recognized Compose filename in the working directory - `registry`: `registry.neko-piranha.ts.net` - `builders.amd64`: `kuber@astral-th` @@ -151,6 +151,9 @@ Operational defaults: - `builders.remoteRoot`: `kuber-build` - `rolloutTimeoutMs`: `300000` +Project-name precedence is `.kuberrc.ts project`, Compose top-level `name`, then +the current working directory name. + Configuration hooks can be synchronous or asynchronous and receive mutable values: diff --git a/command/main.ts b/command/main.ts index 4c7ffc7..701a5c6 100644 --- a/command/main.ts +++ b/command/main.ts @@ -15,7 +15,7 @@ import { up } from "./up"; export const main = defineCommand({ meta: { name: "kuber", - version: "1.2.0", + version: "1.2.1", description: "Docker Compose -> K8s translation layer", }, args: { diff --git a/lib/config.ts b/lib/config.ts index dcb4bdd..cc4c113 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -20,6 +20,7 @@ type Hooks = Pick< export type ResolvedKuberConfig = Hooks & { project: string; + projectConfigured: boolean; composeFile?: string; registry: string; builders: { @@ -157,6 +158,7 @@ export async function loadConfig( return { project: raw.project?.trim() ?? basename(cwd), + projectConfigured: raw.project !== undefined, composeFile: composeFile ? isAbsolute(composeFile) ? composeFile diff --git a/lib/context.ts b/lib/context.ts index 2ff5a5c..aca4bd4 100644 --- a/lib/context.ts +++ b/lib/context.ts @@ -15,6 +15,14 @@ type AppContext = { const appContext = new AsyncLocalStorage(); +export function resolveProjectName( + config: Pick, + compose: ComposeSpecification, +): string { + if (config.projectConfigured) return config.project; + return compose.name?.trim() || config.project; +} + export async function provideContext( fn: () => T, configPath?: string, @@ -26,6 +34,19 @@ export async function provideContext( ? Promise.resolve(config.composeFile) : resolveComposeFile(cwd), ); + const parsedCompose = createCachedGetter("parsedCompose", async () => { + const composeFile = await path(); + if (!composeFile) throw new Error("Compose file cannot be found"); + return readCompose(composeFile); + }); + + if (!config.projectConfigured) { + const composeFile = await path(); + if (composeFile) { + config.project = resolveProjectName(config, await parsedCompose()); + } + } + const getHookContext = createCachedGetter("hookContext", async () => { const composeFile = await path(); if (!composeFile) throw new Error("Compose file cannot be found"); @@ -33,7 +54,7 @@ export async function provideContext( }); const compose = createCachedGetter("compose", async () => { const context = await getHookContext(); - const value = await readCompose(context.composeFile); + const value = await parsedCompose(); await config.compose?.(value, context); return value; }); diff --git a/package.json b/package.json index f6395d0..c8ca910 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dmgnr/kuber", - "version": "1.2.0", + "version": "1.2.1", "description": "Docker Compose to Kubernetes translation layer", "bin": { "kuber": "dist/index.js" diff --git a/tests/lib/core.test.ts b/tests/lib/core.test.ts index 29d0a97..1ec2f04 100644 --- a/tests/lib/core.test.ts +++ b/tests/lib/core.test.ts @@ -7,7 +7,7 @@ import { resolveComposeArch, withComposeArch, } from "../../lib/arch"; -import { createCachedGetter, ctx } from "../../lib/context"; +import { createCachedGetter, ctx, resolveProjectName } from "../../lib/context"; import { toEnvVars, toTable } from "../../lib/format"; import { deepMerge, getPodContainerName, throwWhen } from "../../lib/shared"; @@ -131,6 +131,27 @@ describe("shared helpers", () => { }); describe("context helpers", () => { + test("resolves project names using config, Compose, then directory precedence", () => { + expect( + resolveProjectName( + { project: "directory", projectConfigured: false }, + { name: "compose" }, + ), + ).toBe("compose"); + expect( + resolveProjectName( + { project: "config", projectConfigured: true }, + { name: "compose" }, + ), + ).toBe("config"); + expect( + resolveProjectName( + { project: "directory", projectConfigured: false }, + {}, + ), + ).toBe("directory"); + }); + test("rejects access outside an application context", () => { expect(() => ctx()).toThrow("Not in context"); }); diff --git a/tests/lib/yaml.test.ts b/tests/lib/yaml.test.ts index 9a4598e..eabbe05 100644 --- a/tests/lib/yaml.test.ts +++ b/tests/lib/yaml.test.ts @@ -50,8 +50,12 @@ describe("Compose parsing", () => { test("parses and validates a minimal Compose project", async () => { const directory = await temporaryDirectory(); const path = join(directory, "compose.yml"); - await writeFile(path, "services:\n app:\n image: nginx:latest\n"); + await writeFile( + path, + "name: compose-project\nservices:\n app:\n image: nginx:latest\n", + ); expect(await readCompose(path)).toMatchObject({ + name: "compose-project", services: { app: { image: "nginx:latest" } }, }); }); diff --git a/types.d.ts b/types.d.ts index 3a6b692..ecf9220 100644 --- a/types.d.ts +++ b/types.d.ts @@ -35,7 +35,7 @@ export interface KuberBuildersConfig { } export interface KuberConfig { - /** Kubernetes namespace/project name. Defaults to the working directory name. */ + /** Kubernetes namespace/project name. Overrides the Compose name and working directory. */ project?: string; /** Compose file path, resolved relative to the working directory. */ composeFile?: string;