refactor: normalize sub2api yaml envelope

This commit is contained in:
Codex
2026-06-14 01:54:15 +00:00
parent ac4ae5079c
commit a6123b1d48
4 changed files with 122 additions and 51 deletions
+35 -50
View File
@@ -24,6 +24,13 @@ type DatabaseMode = "bundled" | "external-pending" | "external-active";
type RedisMode = "bundled-persistent" | "local-ephemeral";
interface Sub2ApiConfig {
version: number;
kind: string;
metadata: {
id: string;
owner: string;
relatedIssues: number[];
};
image: {
repository: string;
tag: string;
@@ -391,6 +398,12 @@ function readSub2ApiConfig(): Sub2ApiConfig {
const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${configPath} must contain a YAML object`);
const root = parsed as Record<string, unknown>;
const version = integerField(root, "version", "");
if (version !== 1) throw new Error(`${configPath}.version must be 1`);
const kind = stringField(root, "kind", "");
if (kind !== "platform-infra-sub2api") throw new Error(`${configPath}.kind must be platform-infra-sub2api`);
const metadata = objectField(root, "metadata", "");
const relatedIssues = integerArrayField(metadata, "relatedIssues", "metadata");
const image = (parsed as { image?: unknown }).image;
if (typeof image !== "object" || image === null || Array.isArray(image)) throw new Error(`${configPath}.image must be an object`);
const record = image as Record<string, unknown>;
@@ -410,6 +423,13 @@ function readSub2ApiConfig(): Sub2ApiConfig {
const targets = parseTargets(root);
const runtime = parseRuntime(root);
return {
version,
kind,
metadata: {
id: stringField(metadata, "id", "metadata"),
owner: stringField(metadata, "owner", "metadata"),
relatedIssues,
},
image: { repository, tag, pullPolicy },
dependencyImages,
security: {
@@ -427,7 +447,7 @@ function readSub2ApiConfig(): Sub2ApiConfig {
function parseDependencyImages(root: Record<string, unknown>): Sub2ApiConfig["dependencyImages"] {
const value = root.dependencyImages;
if (value === undefined) return { postgres: "postgres:18-alpine", redis: "redis:8-alpine" };
if (value === undefined) throw new Error(`${configPath}.dependencyImages must be an object`);
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.dependencyImages must be an object`);
const record = value as Record<string, unknown>;
const postgres = stringField(record, "postgres", "dependencyImages");
@@ -439,7 +459,7 @@ function parseDependencyImages(root: Record<string, unknown>): Sub2ApiConfig["de
function parseTargets(root: Record<string, unknown>): Sub2ApiTargetConfig[] {
const value = root.targets;
if (value === undefined) return defaultTargets();
if (value === undefined) throw new Error(`${configPath}.targets must be an array`);
if (!Array.isArray(value)) throw new Error(`${configPath}.targets must be an array`);
const targets = value.map((item, index) => {
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error(`${configPath}.targets[${index}] must be an object`);
@@ -476,26 +496,6 @@ function parseTargets(root: Record<string, unknown>): Sub2ApiTargetConfig[] {
return targets;
}
function defaultTargets(): Sub2ApiTargetConfig[] {
return [
{
id: "G14",
route: "G14:k3s",
namespace,
role: "active",
enabled: true,
databaseMode: "bundled",
redisMode: "bundled-persistent",
appReplicas: 1,
redisReplicas: 1,
image: {},
dependencyImages: {},
publicExposure: null,
egressProxy: null,
},
];
}
function targetImageOverride(record: Record<string, unknown>, path: string): Partial<Sub2ApiConfig["image"]> {
if (record.image === undefined) return {};
const image = objectField(record, "image", path);
@@ -638,34 +638,7 @@ function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgressProx
function parseRuntime(root: Record<string, unknown>): Sub2ApiConfig["runtime"] {
const value = root.runtime;
if (value === undefined) {
return {
database: {
mode: "external",
sourceRef: "platform-db/postgres-active.env",
sourceKeys: {
user: "SUB2API_DB_USER",
password: "SUB2API_DB_PASSWORD",
dbName: "SUB2API_DB_NAME",
},
secretName,
passwordKey: "POSTGRES_PASSWORD",
host: "pika01-postgres.pending.local",
port: 5432,
user: "sub2api",
dbName: "sub2api",
sslMode: "prefer",
pendingAllowed: true,
},
secrets: {
root: ".state/secrets",
appSourceRef: "platform-infra/sub2api.env",
},
redis: { serviceName: "sub2api-redis", persistence: false },
appData: { mode: "persistent-pvc" },
sentinel: { mode: "singleton", enabledOnTargets: ["G14"] },
};
}
if (value === undefined) throw new Error(`${configPath}.runtime must be an object`);
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.runtime must be an object`);
const runtime = value as Record<string, unknown>;
const database = objectField(runtime, "database", "runtime");
@@ -760,6 +733,14 @@ function integerField(obj: Record<string, unknown>, key: string, path: string):
return value;
}
function integerArrayField(obj: Record<string, unknown>, key: string, path: string): number[] {
const value = obj[key];
if (!Array.isArray(value) || !value.every((item) => typeof item === "number" && Number.isInteger(item))) {
throw new Error(`${configPath}.${path}.${key} must be an array of integers`);
}
return value;
}
function isKubernetesName(value: string): boolean {
return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value);
}
@@ -1541,6 +1522,10 @@ function plan(options: TargetOptions): Record<string, unknown> {
serviceDns: `${serviceName}.${target.namespace}.svc.cluster.local:8080`,
},
config: {
path: configPath,
version: sub2api.version,
kind: sub2api.kind,
metadata: sub2api.metadata,
image: imageRef(sub2api, target),
pullPolicy: targetImage(sub2api, target).pullPolicy,
dependencyImages: targetDependencyImages(sub2api, target),