163 lines
4.5 KiB
TypeScript
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,
|
|
};
|
|
}
|