Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/billing-catalog/sync.ts
T

135 lines
4.5 KiB
TypeScript

import { mkdir, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { assessLagoCatalog, buildDesiredLagoCatalog, type LagoCatalogAssessment, type LagoCatalogSnapshot } from "./catalog";
import { BillingCatalogOperatorError, parseBillingCatalogCommand, safeBillingCatalogError, type BillingCatalogCommand } from "./contracts";
import { LagoCatalogClient } from "./lago-client";
type BillingCatalogExecutionResult = {
readonly target: BillingCatalogCommand["target"];
readonly mode: BillingCatalogCommand["mode"];
readonly complete: boolean;
readonly catalogVersion: string;
readonly catalogDigestSha256: string;
readonly snapshotPath: string | null;
readonly actions: LagoCatalogAssessment["actions"];
};
function safeTimestamp(now: Date): string {
return now.toISOString().replace(/[:.]/gu, "-");
}
export async function writeRedactedCatalogSnapshot(input: {
directory: string;
target: BillingCatalogCommand["target"];
snapshot: LagoCatalogSnapshot;
assessment: LagoCatalogAssessment;
now?: Date;
}): Promise<string> {
const path = resolve(input.directory, `${input.target}-${safeTimestamp(input.now ?? new Date())}.json`);
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
await writeFile(
path,
`${JSON.stringify(
{
target: input.target,
capturedAt: (input.now ?? new Date()).toISOString(),
catalogVersion: input.assessment.catalogVersion,
catalogDigestSha256: input.assessment.catalogDigestSha256,
snapshot: input.snapshot,
},
null,
2
)}\n`,
{ encoding: "utf8", mode: 0o600 }
);
return path;
}
export async function executeBillingCatalogCommand(
command: BillingCatalogCommand,
client = new LagoCatalogClient({
apiUrl: command.lagoApiUrl,
apiKey: command.lagoApiKey,
expectedOrganizationName: command.expectedOrganizationName,
expectedOrganizationIdSha256: command.expectedOrganizationIdSha256,
})
): Promise<BillingCatalogExecutionResult> {
const desired = buildDesiredLagoCatalog();
const initialSnapshot = await client.readCatalog(desired);
const initialAssessment = assessLagoCatalog(initialSnapshot, desired);
if (command.mode === "plan") {
return {
target: command.target,
mode: command.mode,
complete: initialAssessment.complete,
catalogVersion: desired.version,
catalogDigestSha256: desired.digestSha256,
snapshotPath: null,
actions: initialAssessment.actions,
};
}
if (command.mode === "verify") {
if (!initialAssessment.complete) {
throw new BillingCatalogOperatorError("billing catalog: verification found missing or drifted Lago resources");
}
return {
target: command.target,
mode: command.mode,
complete: true,
catalogVersion: desired.version,
catalogDigestSha256: desired.digestSha256,
snapshotPath: null,
actions: initialAssessment.actions,
};
}
const snapshotPath = await writeRedactedCatalogSnapshot({
directory: command.snapshotDirectory,
target: command.target,
snapshot: initialSnapshot,
assessment: initialAssessment,
});
for (const action of initialAssessment.actions) {
if (action.action === "noop") continue;
if (action.resource === "tax") {
await client.applyTax(desired, action.action === "update");
continue;
}
const plan = desired.plans.find((candidate) => candidate.code === action.code);
if (!plan) throw new BillingCatalogOperatorError("billing catalog: desired Lago plan is unavailable");
await client.applyPlan(plan, action.action === "update");
}
const finalAssessment = assessLagoCatalog(await client.readCatalog(desired), desired);
if (!finalAssessment.complete) {
throw new BillingCatalogOperatorError("billing catalog: apply did not converge to the canonical contract");
}
return {
target: command.target,
mode: command.mode,
complete: true,
catalogVersion: desired.version,
catalogDigestSha256: desired.digestSha256,
snapshotPath,
actions: finalAssessment.actions,
};
}
async function main(): Promise<void> {
try {
const command = parseBillingCatalogCommand(process.argv.slice(2), process.env);
const result = await executeBillingCatalogCommand(command);
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} catch (error) {
process.stderr.write(`${safeBillingCatalogError(error)}\n`);
process.exitCode = 1;
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
void main();
}