134 lines
5.1 KiB
TypeScript
134 lines
5.1 KiB
TypeScript
import { hiveGridBillingCatalogVersion } from "@hivegrid/entitlements";
|
|
|
|
export type BillingCatalogTarget = "staging" | "production";
|
|
export type BillingCatalogMode = "plan" | "apply" | "verify";
|
|
export type BillingCatalogEnvironment = Readonly<Record<string, string | undefined>>;
|
|
|
|
export type BillingCatalogCommand = {
|
|
readonly target: BillingCatalogTarget;
|
|
readonly mode: BillingCatalogMode;
|
|
readonly lagoApiUrl: string;
|
|
readonly lagoApiKey: string;
|
|
readonly expectedOrganizationName: string;
|
|
readonly expectedOrganizationIdSha256: string;
|
|
readonly snapshotDirectory: string;
|
|
};
|
|
|
|
const STAGING_ORGANIZATION_NAME = "HiveGrid Staging";
|
|
const STAGING_ORGANIZATION_ID_SHA256 = "3b1134a731c67b704bb0b3298ce031213079966b348aa762800f9c92556085a8";
|
|
const PRODUCTION_ORGANIZATION_NAME = "HiveGrid";
|
|
const PRODUCTION_ORGANIZATION_ID_SHA256 = "c54f682e7528f2f1d556a5f539d6d200aa7d9163c9aec31f8fb5b21be632505e";
|
|
const MODE_ARGUMENTS = new Map<string, BillingCatalogMode>([
|
|
["--plan", "plan"],
|
|
["--apply", "apply"],
|
|
["--verify", "verify"],
|
|
]);
|
|
|
|
export class BillingCatalogOperatorError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "BillingCatalogOperatorError";
|
|
}
|
|
}
|
|
|
|
function required(env: BillingCatalogEnvironment, name: string): string {
|
|
const value = env[name]?.trim();
|
|
if (!value) throw new BillingCatalogOperatorError(`billing catalog: ${name} is required`);
|
|
return value;
|
|
}
|
|
|
|
function valueArgument(argv: readonly string[], name: string): string | undefined {
|
|
const prefix = `${name}=`;
|
|
const values = argv.filter((argument) => argument.startsWith(prefix)).map((argument) => argument.slice(prefix.length));
|
|
if (values.length > 1) throw new BillingCatalogOperatorError(`billing catalog: ${name} may only be provided once`);
|
|
return values[0];
|
|
}
|
|
|
|
function parseTarget(argv: readonly string[]): BillingCatalogTarget {
|
|
const target = valueArgument(argv, "--target");
|
|
if (target !== "staging" && target !== "production") {
|
|
throw new BillingCatalogOperatorError("billing catalog: --target=staging|production is required");
|
|
}
|
|
return target;
|
|
}
|
|
|
|
function parseMode(argv: readonly string[]): BillingCatalogMode {
|
|
const modes = argv.flatMap((argument) => {
|
|
const mode = MODE_ARGUMENTS.get(argument);
|
|
return mode ? [mode] : [];
|
|
});
|
|
if (modes.length > 1) throw new BillingCatalogOperatorError("billing catalog: provide exactly one mode");
|
|
return modes[0] ?? "plan";
|
|
}
|
|
|
|
function assertKnownArguments(argv: readonly string[]): void {
|
|
for (const argument of argv) {
|
|
if (MODE_ARGUMENTS.has(argument) || argument.startsWith("--target=") || argument.startsWith("--confirm=")) {
|
|
continue;
|
|
}
|
|
throw new BillingCatalogOperatorError("billing catalog: unknown argument");
|
|
}
|
|
}
|
|
|
|
function normalizeApiUrl(value: string): string {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(value);
|
|
} catch {
|
|
throw new BillingCatalogOperatorError("billing catalog: LAGO_API_URL must be an absolute URL");
|
|
}
|
|
if (
|
|
!new Set(["http:", "https:"]).has(parsed.protocol) ||
|
|
parsed.username ||
|
|
parsed.password ||
|
|
parsed.search ||
|
|
parsed.hash ||
|
|
(parsed.pathname !== "" && parsed.pathname !== "/")
|
|
) {
|
|
throw new BillingCatalogOperatorError("billing catalog: LAGO_API_URL must be an HTTP(S) origin");
|
|
}
|
|
return parsed.origin;
|
|
}
|
|
|
|
function requireOrganizationContract(target: BillingCatalogTarget): { name: string; idSha256: string } {
|
|
if (target === "staging") {
|
|
return { name: STAGING_ORGANIZATION_NAME, idSha256: STAGING_ORGANIZATION_ID_SHA256 };
|
|
}
|
|
return {
|
|
name: PRODUCTION_ORGANIZATION_NAME,
|
|
idSha256: PRODUCTION_ORGANIZATION_ID_SHA256,
|
|
};
|
|
}
|
|
|
|
export function parseBillingCatalogCommand(argv: readonly string[], env: BillingCatalogEnvironment): BillingCatalogCommand {
|
|
assertKnownArguments(argv);
|
|
const target = parseTarget(argv);
|
|
const mode = parseMode(argv);
|
|
const confirmation = valueArgument(argv, "--confirm");
|
|
const expectedConfirmation = `${target}:${hiveGridBillingCatalogVersion}`;
|
|
if (mode === "apply" && confirmation !== expectedConfirmation) {
|
|
throw new BillingCatalogOperatorError(`billing catalog: apply requires --confirm=${expectedConfirmation}`);
|
|
}
|
|
if (mode !== "apply" && confirmation !== undefined) {
|
|
throw new BillingCatalogOperatorError("billing catalog: --confirm is valid only with --apply");
|
|
}
|
|
if (env.NODE_ENV?.trim() !== "production") {
|
|
throw new BillingCatalogOperatorError("billing catalog: NODE_ENV must be production so the operator path fails closed");
|
|
}
|
|
const organization = requireOrganizationContract(target);
|
|
return {
|
|
target,
|
|
mode,
|
|
lagoApiUrl: normalizeApiUrl(required(env, "LAGO_API_URL")),
|
|
lagoApiKey: required(env, "LAGO_API_KEY"),
|
|
expectedOrganizationName: organization.name,
|
|
expectedOrganizationIdSha256: organization.idSha256,
|
|
snapshotDirectory: env.HIVEGRID_BILLING_CATALOG_SNAPSHOT_DIR?.trim() || "tmp/billing-catalog-snapshots",
|
|
};
|
|
}
|
|
|
|
export function safeBillingCatalogError(error: unknown): string {
|
|
if (error instanceof BillingCatalogOperatorError) return error.message;
|
|
return "billing catalog: operator command failed; inspect redacted service logs";
|
|
}
|