Files
kuber/lib/config.ts
T
2026-09-03 11:28:30 +07:00

163 lines
4.5 KiB
TypeScript

import { basename, isAbsolute, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { KuberConfig, KuberHookContext } from "../types";
import { IMAGE_REGISTRY } from "../const";
const DEFAULT_CONFIG_FILE = ".kuberrc.ts";
export const DEFAULT_REGISTRY = IMAGE_REGISTRY;
export const DEFAULT_ROLLOUT_TIMEOUT_MS = 300_000;
type Hooks = Pick<
KuberConfig,
"compose" | "preBuild" | "postBuild" | "postRender" | "postApply"
>;
export type ResolvedKuberConfig = Hooks & {
project: string;
projectConfigured: boolean;
composeFile?: string;
registry: string;
rolloutTimeoutMs: number;
configFile?: string;
};
function requireNonEmptyString(value: unknown, field: string): string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`${field} must be a non-empty string`);
}
return value.trim();
}
function validateConfig(config: unknown, file: string): KuberConfig {
if (!config || typeof config !== "object" || Array.isArray(config)) {
throw new Error(`${file} must default export a configuration object`);
}
const value = config as Record<string, unknown>;
for (const field of [
"compose",
"preBuild",
"postBuild",
"postRender",
"postApply",
]) {
if (value[field] !== undefined && typeof value[field] !== "function") {
throw new Error(`${field} in ${file} must be a function`);
}
}
if (value.project !== undefined)
requireNonEmptyString(value.project, "project");
if (value.composeFile !== undefined)
requireNonEmptyString(value.composeFile, "composeFile");
if (value.registry !== undefined)
requireNonEmptyString(value.registry, "registry");
if (
value.rolloutTimeoutMs !== undefined &&
(typeof value.rolloutTimeoutMs !== "number" ||
!Number.isFinite(value.rolloutTimeoutMs) ||
value.rolloutTimeoutMs <= 0)
) {
throw new Error(`rolloutTimeoutMs in ${file} must be a positive number`);
}
return config as KuberConfig;
}
export function extractConfigArgument(args: string[]): {
configPath?: string;
rawArgs: string[];
} {
let configPath: string | undefined;
const rawArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const argument = args[index]!;
if (argument === "--") {
rawArgs.push(...args.slice(index));
break;
}
if (argument.startsWith("--config=")) {
const value = argument.slice("--config=".length);
if (!value) throw new Error("--config requires a path");
if (configPath) throw new Error("--config may only be specified once");
configPath = value;
continue;
}
if (argument === "--config") {
const value = args[index + 1];
if (!value || value.startsWith("-")) {
throw new Error(`${argument} requires a path`);
}
if (configPath) throw new Error("--config may only be specified once");
configPath = value;
index += 1;
continue;
}
rawArgs.push(argument);
}
return { configPath, rawArgs };
}
export function resolveConfigArgument(args: string[]): string | undefined {
return extractConfigArgument(args).configPath;
}
export async function loadConfig(
cwd: string,
requestedPath?: string,
): Promise<ResolvedKuberConfig> {
const configFile = resolve(cwd, requestedPath ?? DEFAULT_CONFIG_FILE);
const exists = await Bun.file(configFile).exists();
if (!exists && requestedPath) {
throw new Error(`Config file cannot be found: ${configFile}`);
}
const raw = exists
? validateConfig(
(await import(pathToFileURL(configFile).href)).default,
configFile,
)
: {};
const composeFile = raw.composeFile?.trim();
const registry = (raw.registry?.trim() ?? DEFAULT_REGISTRY).replace(
/\/+$/,
"",
);
if (!registry) throw new Error(`registry in ${configFile} cannot be empty`);
return {
project: raw.project?.trim() ?? basename(cwd),
projectConfigured: raw.project !== undefined,
composeFile: composeFile
? isAbsolute(composeFile)
? composeFile
: resolve(cwd, composeFile)
: undefined,
registry,
rolloutTimeoutMs: raw.rolloutTimeoutMs ?? DEFAULT_ROLLOUT_TIMEOUT_MS,
configFile: exists ? configFile : undefined,
compose: raw.compose,
preBuild: raw.preBuild,
postBuild: raw.postBuild,
postRender: raw.postRender,
postApply: raw.postApply,
};
}
export function hookContext(
config: ResolvedKuberConfig,
cwd: string,
composeFile: string,
): KuberHookContext {
return {
cwd,
project: config.project,
composeFile,
configFile: config.configFile,
};
}