feat: add provider-backed microservices

This commit is contained in:
Codex
2026-05-05 07:56:03 +00:00
parent ef70ca972b
commit abd40fa252
24 changed files with 1656 additions and 51 deletions
+100
View File
@@ -35,10 +35,46 @@ export interface UniDeskConfig {
};
};
docker: { composeFile: string; projectName: string };
microservices: UniDeskMicroserviceConfig[];
paths: { stateDir: string; logsDir: string; docsReferenceDir: string };
sshForwarding: { mode: string; keyDir: string; host: string; port: number; user: string };
}
export interface UniDeskMicroserviceConfig {
id: string;
name: string;
providerId: string;
description: string;
repository: {
url: string;
commitId: string;
dockerfile: string;
composeFile: string;
composeService: string;
containerName: string;
};
backend: {
nodeBaseUrl: string;
nodeBindHost: string;
nodePort: number;
proxyMode: string;
frontendOnly: boolean;
public: boolean;
allowedPathPrefixes: string[];
healthPath: string;
timeoutMs: number;
};
development: {
providerId: string;
sshPassthrough: boolean;
worktreePath: string;
};
frontend: {
route: string;
integrated: boolean;
};
}
const moduleDir = dirname(fileURLToPath(import.meta.url));
export const repoRoot = join(moduleDir, "..", "..");
@@ -70,6 +106,68 @@ function portPair(obj: Record<string, unknown>, key: string): { port: number; co
return { port: numberField(value, "port", `network.${key}`), containerPort: numberField(value, "containerPort", `network.${key}`) };
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
return value;
}
function optionalArray(value: unknown, name: string): Record<string, unknown>[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
return value.map((item, index) => asRecord(item, `${name}[${index}]`));
}
function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const value = obj[key];
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
throw new Error(`${path}.${key} must be an array of non-empty strings`);
}
return value as string[];
}
function microserviceConfig(item: Record<string, unknown>, index: number): UniDeskMicroserviceConfig {
const path = `microservices[${index}]`;
const repository = asRecord(item.repository, `${path}.repository`);
const backend = asRecord(item.backend, `${path}.backend`);
const development = asRecord(item.development, `${path}.development`);
const frontend = asRecord(item.frontend, `${path}.frontend`);
return {
id: stringField(item, "id", path),
name: stringField(item, "name", path),
providerId: stringField(item, "providerId", path),
description: stringField(item, "description", path),
repository: {
url: stringField(repository, "url", `${path}.repository`),
commitId: stringField(repository, "commitId", `${path}.repository`),
dockerfile: stringField(repository, "dockerfile", `${path}.repository`),
composeFile: stringField(repository, "composeFile", `${path}.repository`),
composeService: stringField(repository, "composeService", `${path}.repository`),
containerName: stringField(repository, "containerName", `${path}.repository`),
},
backend: {
nodeBaseUrl: stringField(backend, "nodeBaseUrl", `${path}.backend`),
nodeBindHost: stringField(backend, "nodeBindHost", `${path}.backend`),
nodePort: numberField(backend, "nodePort", `${path}.backend`),
proxyMode: stringField(backend, "proxyMode", `${path}.backend`),
frontendOnly: booleanField(backend, "frontendOnly", `${path}.backend`),
public: booleanField(backend, "public", `${path}.backend`),
allowedPathPrefixes: stringArrayField(backend, "allowedPathPrefixes", `${path}.backend`),
healthPath: stringField(backend, "healthPath", `${path}.backend`),
timeoutMs: numberField(backend, "timeoutMs", `${path}.backend`),
},
development: {
providerId: stringField(development, "providerId", `${path}.development`),
sshPassthrough: booleanField(development, "sshPassthrough", `${path}.development`),
worktreePath: stringField(development, "worktreePath", `${path}.development`),
},
frontend: {
route: stringField(frontend, "route", `${path}.frontend`),
integrated: booleanField(frontend, "integrated", `${path}.frontend`),
},
};
}
export function readConfig(): UniDeskConfig {
const configPath = rootPath("config.json");
if (!existsSync(configPath)) throw new Error(`config.json not found at ${configPath}`);
@@ -82,6 +180,7 @@ export function readConfig(): UniDeskConfig {
const auth = asRecord(parsed.auth, "auth");
const providerGateway = asRecord(parsed.providerGateway, "providerGateway");
const docker = asRecord(parsed.docker, "docker");
const microservices = optionalArray(parsed.microservices, "microservices").map(microserviceConfig);
const paths = asRecord(parsed.paths, "paths");
const sshForwarding = asRecord(parsed.sshForwarding, "sshForwarding");
const labels = asRecord(providerGateway.labels, "providerGateway.labels");
@@ -129,6 +228,7 @@ export function readConfig(): UniDeskConfig {
},
},
docker: { composeFile: stringField(docker, "composeFile", "docker"), projectName: stringField(docker, "projectName", "docker") },
microservices,
paths: {
stateDir: stringField(paths, "stateDir", "paths"),
logsDir: stringField(paths, "logsDir", "paths"),