refactor: normalize sub2api yaml envelope
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
version: 1
|
||||
kind: platform-infra-sub2api-codex-pool
|
||||
|
||||
metadata:
|
||||
id: sub2api-codex-pool
|
||||
owner: unidesk
|
||||
relatedIssues:
|
||||
- 339
|
||||
- 340
|
||||
|
||||
pool:
|
||||
groupName: unidesk-codex-pool
|
||||
apiKeyName: unidesk-codex-pool-api-key
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
version: 1
|
||||
kind: platform-infra-sub2api
|
||||
|
||||
metadata:
|
||||
id: sub2api
|
||||
owner: unidesk
|
||||
relatedIssues:
|
||||
- 220
|
||||
- 339
|
||||
- 340
|
||||
|
||||
image:
|
||||
repository: weishaw/sub2api
|
||||
tag: 0.1.136
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
dependencyImages:
|
||||
postgres: postgres:18-alpine
|
||||
redis: redis:8-alpine
|
||||
|
||||
targets:
|
||||
- id: G14
|
||||
route: G14:k3s
|
||||
|
||||
@@ -131,6 +131,13 @@ export interface CodexTempUnschedulablePolicy {
|
||||
}
|
||||
|
||||
interface CodexPoolConfig {
|
||||
version: number;
|
||||
kind: string;
|
||||
metadata: {
|
||||
id: string;
|
||||
owner: string;
|
||||
relatedIssues: number[];
|
||||
};
|
||||
groupName: string;
|
||||
apiKeyName: string;
|
||||
apiKeySecretName: string;
|
||||
@@ -1209,11 +1216,17 @@ function resolveProfileFiles(codexDir: string, profile: CodexPoolProfileConfig):
|
||||
|
||||
function readCodexPoolConfig(): CodexPoolConfig {
|
||||
const defaults = defaultCodexPoolConfig();
|
||||
if (!existsSync(codexPoolConfigPath)) return defaults;
|
||||
if (!existsSync(codexPoolConfigPath)) throw new Error(`${codexPoolConfigPath} is required`);
|
||||
const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
|
||||
if (!isRecord(parsed)) throw new Error(`${codexPoolConfigPath} must contain a YAML object`);
|
||||
const version = integerConfigField(parsed, "version", "");
|
||||
if (version !== 1) throw new Error(`${codexPoolConfigPath}.version must be 1`);
|
||||
const kind = requiredStringConfigField(parsed, "kind", "");
|
||||
if (kind !== "platform-infra-sub2api-codex-pool") throw new Error(`${codexPoolConfigPath}.kind must be platform-infra-sub2api-codex-pool`);
|
||||
const metadata = requiredRecordConfigField(parsed, "metadata", "");
|
||||
const pool = parsed.pool;
|
||||
if (!isRecord(pool)) throw new Error(`${codexPoolConfigPath}.pool must be a YAML object`);
|
||||
if (!isRecord(parsed.profiles)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`);
|
||||
rejectSchedulableYamlField(pool, "pool");
|
||||
const defaultTempUnschedulable = readTempUnschedulablePolicy(pool.defaultTempUnschedulable, "pool.defaultTempUnschedulable", defaults.defaultTempUnschedulable);
|
||||
const defaultAccountPriorityValue = readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority");
|
||||
@@ -1233,6 +1246,13 @@ function readCodexPoolConfig(): CodexPoolConfig {
|
||||
? Math.max(1, declaredAccountCapacity)
|
||||
: readOwnerConcurrency(pool.minOwnerConcurrency, "pool.minOwnerConcurrency");
|
||||
const config: CodexPoolConfig = {
|
||||
version,
|
||||
kind,
|
||||
metadata: {
|
||||
id: requiredStringConfigField(metadata, "id", "metadata"),
|
||||
owner: requiredStringConfigField(metadata, "owner", "metadata"),
|
||||
relatedIssues: integerArrayConfigField(metadata, "relatedIssues", "metadata"),
|
||||
},
|
||||
groupName: stringValue(pool.groupName) ?? defaults.groupName,
|
||||
apiKeyName: stringValue(pool.apiKeyName) ?? defaults.apiKeyName,
|
||||
apiKeySecretName: stringValue(pool.apiKeySecretName) ?? defaults.apiKeySecretName,
|
||||
@@ -1264,6 +1284,13 @@ function readCodexPoolConfig(): CodexPoolConfig {
|
||||
|
||||
function defaultCodexPoolConfig(): CodexPoolConfig {
|
||||
return {
|
||||
version: 1,
|
||||
kind: "platform-infra-sub2api-codex-pool",
|
||||
metadata: {
|
||||
id: "sub2api-codex-pool",
|
||||
owner: "unidesk",
|
||||
relatedIssues: [],
|
||||
},
|
||||
groupName: defaultPoolGroupName,
|
||||
apiKeyName: defaultPoolApiKeyName,
|
||||
apiKeySecretName: defaultPoolApiKeySecretName,
|
||||
@@ -1853,6 +1880,9 @@ function tempUnschedulableSummary(policy: CodexTempUnschedulablePolicy): Record<
|
||||
function codexPoolConfigSummary(pool: CodexPoolConfig): Record<string, unknown> {
|
||||
const accountCapacityTotal = desiredAccountCapacityTotal(pool);
|
||||
return {
|
||||
version: pool.version,
|
||||
kind: pool.kind,
|
||||
metadata: pool.metadata,
|
||||
groupName: pool.groupName,
|
||||
apiKeyName: pool.apiKeyName,
|
||||
apiKeySecretName: pool.apiKeySecretName,
|
||||
@@ -3395,6 +3425,36 @@ function stringValue(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function requiredStringConfigField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = stringValue(obj[key]);
|
||||
const prefix = path.length > 0 ? `${path}.` : "";
|
||||
if (value === null) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be a non-empty string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredRecordConfigField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
|
||||
const value = obj[key];
|
||||
const prefix = path.length > 0 ? `${path}.` : "";
|
||||
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be a YAML object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function integerConfigField(obj: Record<string, unknown>, key: string, path: string): number {
|
||||
const value = obj[key];
|
||||
const prefix = path.length > 0 ? `${path}.` : "";
|
||||
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be an integer`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function integerArrayConfigField(obj: Record<string, unknown>, key: string, path: string): number[] {
|
||||
const value = obj[key];
|
||||
const prefix = path.length > 0 ? `${path}.` : "";
|
||||
if (!Array.isArray(value) || !value.every((item) => typeof item === "number" && Number.isInteger(item))) {
|
||||
throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be an array of integers`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | null {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user