feat: move AgentRun deploy truth to UniDesk YAML

This commit is contained in:
Codex
2026-06-13 06:01:27 +00:00
parent 8954a48c92
commit 3717becd77
8 changed files with 1918 additions and 47 deletions
+279
View File
@@ -10,6 +10,19 @@ export interface AgentRunGitMirrorRepositorySpec {
readonly gitopsBranch?: string;
}
export interface AgentRunSecretRef {
readonly namespace: string;
readonly name: string;
readonly key: string;
}
export interface AgentRunLaneSecretSpec {
readonly id: string;
readonly sourceRef: string;
readonly sourceKey: string;
readonly targetRef: AgentRunSecretRef;
}
export interface AgentRunLaneSpec {
readonly lane: string;
readonly nodeId: string;
@@ -19,6 +32,7 @@ export interface AgentRunLaneSpec {
readonly source: {
readonly repository: string;
readonly branch: string;
readonly bootstrapFromBranch: string | null;
readonly remote: string;
readonly workspace: string;
};
@@ -44,13 +58,51 @@ export interface AgentRunLaneSpec {
readonly argoApplication: string;
readonly repoURL: string;
};
readonly deployment: {
readonly format: "unidesk-yaml-only";
readonly gitopsRoot: string;
readonly runtimeRenderDir: string;
readonly artifactCatalogPath: string;
readonly argocd: {
readonly project: string;
readonly applicationFile: string;
};
readonly manager: {
readonly serviceAccount: string;
readonly apiKeySecretRef: { readonly name: string; readonly key: string };
readonly unideskSshEndpointEnv: { readonly name: string; readonly value: string } | null;
readonly bootRepoUrl: string;
readonly imageBuild: AgentRunImageBuildSpec;
readonly resources: AgentRunContainerResources;
};
readonly runner: {
readonly serviceAccount: string;
readonly jobNamePrefix: string;
readonly apiKeySecretRef: { readonly name: string; readonly key: string };
};
readonly localPostgres: {
readonly enabled: boolean;
readonly serviceName: string | null;
readonly image: string | null;
readonly storage: string | null;
readonly port: number | null;
};
};
readonly gitMirror: {
readonly namespace: string;
readonly readService: string;
readonly readDeployment: string;
readonly writeService: string;
readonly writeDeployment: string;
readonly readUrl: string;
readonly writeUrl: string;
readonly cachePvc: string;
readonly cacheHostPath: string | null;
readonly sshSecretName: string;
readonly githubProxy: {
readonly host: string;
readonly port: number;
};
readonly toolsImage: string;
readonly syncJobPrefix: string;
readonly flushJobPrefix: string;
@@ -67,6 +119,30 @@ export interface AgentRunLaneSpec {
readonly secretRef: { readonly name: string; readonly key: string };
readonly localPostgresExpectedAbsent: boolean;
};
readonly secrets: readonly AgentRunLaneSecretSpec[];
}
export interface AgentRunContainerResources {
readonly requests: {
readonly cpu: string;
readonly memory: string;
};
readonly limits: {
readonly cpu: string;
readonly memory: string;
};
}
export interface AgentRunImageBuildSpec {
readonly context: string;
readonly containerfile: string;
readonly repository: string;
readonly network: string;
readonly httpProxy: string | null;
readonly httpsProxy: string | null;
readonly noProxy: readonly string[];
readonly envIdentityFiles: readonly string[];
readonly timeoutSeconds: number;
}
export interface AgentRunLaneTarget {
@@ -124,6 +200,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
source: {
repository: spec.source.repository,
branch: spec.source.branch,
bootstrapFromBranch: spec.source.bootstrapFromBranch,
workspace: spec.source.workspace,
},
runtime: {
@@ -147,13 +224,50 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
argoApplication: spec.gitops.argoApplication,
repoURL: spec.gitops.repoURL,
},
deployment: {
format: spec.deployment.format,
gitopsRoot: spec.deployment.gitopsRoot,
runtimeRenderDir: spec.deployment.runtimeRenderDir,
artifactCatalogPath: spec.deployment.artifactCatalogPath,
argocd: spec.deployment.argocd,
manager: {
serviceAccount: spec.deployment.manager.serviceAccount,
apiKeySecretRef: spec.deployment.manager.apiKeySecretRef,
unideskSshEndpointEnv: spec.deployment.manager.unideskSshEndpointEnv === null
? null
: { name: spec.deployment.manager.unideskSshEndpointEnv.name, valuesPrinted: false },
bootRepoUrl: spec.deployment.manager.bootRepoUrl,
imageBuild: {
context: spec.deployment.manager.imageBuild.context,
containerfile: spec.deployment.manager.imageBuild.containerfile,
repository: spec.deployment.manager.imageBuild.repository,
network: spec.deployment.manager.imageBuild.network,
proxyConfigured: spec.deployment.manager.imageBuild.httpProxy !== null || spec.deployment.manager.imageBuild.httpsProxy !== null,
noProxyCount: spec.deployment.manager.imageBuild.noProxy.length,
envIdentityFileCount: spec.deployment.manager.imageBuild.envIdentityFiles.length,
timeoutSeconds: spec.deployment.manager.imageBuild.timeoutSeconds,
},
resources: spec.deployment.manager.resources,
},
runner: {
serviceAccount: spec.deployment.runner.serviceAccount,
jobNamePrefix: spec.deployment.runner.jobNamePrefix,
apiKeySecretRef: spec.deployment.runner.apiKeySecretRef,
},
localPostgres: spec.deployment.localPostgres,
},
gitMirror: {
namespace: spec.gitMirror.namespace,
readService: spec.gitMirror.readService,
readDeployment: spec.gitMirror.readDeployment,
writeService: spec.gitMirror.writeService,
writeDeployment: spec.gitMirror.writeDeployment,
readUrl: spec.gitMirror.readUrl,
writeUrl: spec.gitMirror.writeUrl,
cachePvc: spec.gitMirror.cachePvc,
cacheHostPath: spec.gitMirror.cacheHostPath,
sshSecretName: spec.gitMirror.sshSecretName,
githubProxy: { host: spec.gitMirror.githubProxy.host, port: spec.gitMirror.githubProxy.port },
repositories: spec.gitMirror.repositories.map((repo) => ({
key: repo.key,
repository: repo.repository,
@@ -173,6 +287,13 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
valuesPrinted: false,
},
secrets: spec.secrets.map((secret) => ({
id: secret.id,
sourceRef: secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
sourceKey: secret.sourceKey,
targetRef: secret.targetRef,
valuesPrinted: false,
})),
};
}
@@ -233,6 +354,7 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
const runtime = recordField(input, "runtime", path);
const ci = recordField(input, "ci", path);
const gitops = recordField(input, "gitops", path);
const deployment = recordField(input, "deployment", path);
const gitMirror = recordField(input, "gitMirror", path);
const database = recordField(input, "database", path);
return {
@@ -244,6 +366,7 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
source: {
repository: stringField(source, "repository", `${path}.source`),
branch: stringField(source, "branch", `${path}.source`),
bootstrapFromBranch: optionalStringField(source, "bootstrapFromBranch", `${path}.source`) ?? null,
remote: stringField(source, "remote", `${path}.source`),
workspace: absolutePathField(source, "workspace", `${path}.source`),
},
@@ -269,19 +392,118 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
argoApplication: stringField(gitops, "argoApplication", `${path}.gitops`),
repoURL: urlField(gitops, "repoURL", `${path}.gitops`),
},
deployment: parseDeployment(deployment, `${path}.deployment`),
gitMirror: {
namespace: stringField(gitMirror, "namespace", `${path}.gitMirror`),
readService: stringField(gitMirror, "readService", `${path}.gitMirror`),
readDeployment: stringField(gitMirror, "readDeployment", `${path}.gitMirror`),
writeService: stringField(gitMirror, "writeService", `${path}.gitMirror`),
writeDeployment: stringField(gitMirror, "writeDeployment", `${path}.gitMirror`),
readUrl: urlField(gitMirror, "readUrl", `${path}.gitMirror`),
writeUrl: urlField(gitMirror, "writeUrl", `${path}.gitMirror`),
cachePvc: stringField(gitMirror, "cachePvc", `${path}.gitMirror`),
cacheHostPath: optionalAbsolutePathField(gitMirror, "cacheHostPath", `${path}.gitMirror`) ?? null,
sshSecretName: stringField(gitMirror, "sshSecretName", `${path}.gitMirror`),
githubProxy: parseGithubProxy(recordField(gitMirror, "githubProxy", `${path}.gitMirror`), `${path}.gitMirror.githubProxy`),
toolsImage: stringField(gitMirror, "toolsImage", `${path}.gitMirror`),
syncJobPrefix: stringField(gitMirror, "syncJobPrefix", `${path}.gitMirror`),
flushJobPrefix: stringField(gitMirror, "flushJobPrefix", `${path}.gitMirror`),
repositories: arrayField(gitMirror, "repositories", `${path}.gitMirror`).map((repo, index) => parseGitMirrorRepository(repo, `${path}.gitMirror.repositories[${index}]`)),
},
database: parseDatabase(database, `${path}.database`),
secrets: arrayField(input, "secrets", path).map((secret, index) => parseLaneSecret(secret, `${path}.secrets[${index}]`)),
};
}
function parseDeployment(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"] {
const argocd = recordField(input, "argocd", path);
const manager = recordField(input, "manager", path);
const runner = recordField(input, "runner", path);
const localPostgres = recordField(input, "localPostgres", path);
return {
format: enumField(input, "format", path, ["unidesk-yaml-only"]),
gitopsRoot: relativePathField(input, "gitopsRoot", path),
runtimeRenderDir: relativePathField(input, "runtimeRenderDir", path),
artifactCatalogPath: relativePathField(input, "artifactCatalogPath", path),
argocd: {
project: stringField(argocd, "project", `${path}.argocd`),
applicationFile: relativePathField(argocd, "applicationFile", `${path}.argocd`),
},
manager: {
serviceAccount: stringField(manager, "serviceAccount", `${path}.manager`),
apiKeySecretRef: parseSecretRef(recordField(manager, "apiKeySecretRef", `${path}.manager`), `${path}.manager.apiKeySecretRef`),
unideskSshEndpointEnv: optionalEnvPair(manager, "unideskSshEndpointEnv", `${path}.manager`),
bootRepoUrl: urlField(manager, "bootRepoUrl", `${path}.manager`),
imageBuild: parseImageBuild(recordField(manager, "imageBuild", `${path}.manager`), `${path}.manager.imageBuild`),
resources: parseContainerResources(recordField(manager, "resources", `${path}.manager`), `${path}.manager.resources`),
},
runner: {
serviceAccount: stringField(runner, "serviceAccount", `${path}.runner`),
jobNamePrefix: stringField(runner, "jobNamePrefix", `${path}.runner`),
apiKeySecretRef: parseSecretRef(recordField(runner, "apiKeySecretRef", `${path}.runner`), `${path}.runner.apiKeySecretRef`),
},
localPostgres: parseLocalPostgres(localPostgres, `${path}.localPostgres`),
};
}
function parseLocalPostgres(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"]["localPostgres"] {
const enabled = booleanField(input, "enabled", path);
if (!enabled) {
return {
enabled: false,
serviceName: optionalStringField(input, "serviceName", path) ?? null,
image: optionalStringField(input, "image", path) ?? null,
storage: optionalStringField(input, "storage", path) ?? null,
port: optionalIntegerField(input, "port", path) ?? null,
};
}
return {
enabled: true,
serviceName: stringField(input, "serviceName", path),
image: stringField(input, "image", path),
storage: stringField(input, "storage", path),
port: integerField(input, "port", path),
};
}
function parseContainerResources(input: Record<string, unknown>, path: string): AgentRunContainerResources {
const requests = recordField(input, "requests", path);
const limits = recordField(input, "limits", path);
return {
requests: {
cpu: stringField(requests, "cpu", `${path}.requests`),
memory: stringField(requests, "memory", `${path}.requests`),
},
limits: {
cpu: stringField(limits, "cpu", `${path}.limits`),
memory: stringField(limits, "memory", `${path}.limits`),
},
};
}
function parseImageBuild(input: Record<string, unknown>, path: string): AgentRunImageBuildSpec {
return {
context: relativePathField(input, "context", path),
containerfile: relativePathField(input, "containerfile", path),
repository: stringField(input, "repository", path),
network: stringField(input, "network", path),
httpProxy: optionalStringField(input, "httpProxy", path) ?? null,
httpsProxy: optionalStringField(input, "httpsProxy", path) ?? null,
noProxy: stringArrayField(input, "noProxy", path),
envIdentityFiles: stringArrayField(input, "envIdentityFiles", path).map((item, index) => {
if (item.startsWith("/") || item.includes("..")) throw new Error(`${path}.envIdentityFiles[${index}] must be a relative path without ..`);
return item;
}),
timeoutSeconds: integerField(input, "timeoutSeconds", path),
};
}
function parseLaneSecret(input: Record<string, unknown>, path: string): AgentRunLaneSecretSpec {
return {
id: stringField(input, "id", path),
sourceRef: secretSourceRefField(input, "sourceRef", path),
sourceKey: stringField(input, "sourceKey", path),
targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
};
}
@@ -319,6 +541,31 @@ function parseSecretRef(input: Record<string, unknown>, path: string): { name: s
};
}
function parseNamespacedSecretRef(input: Record<string, unknown>, path: string): AgentRunSecretRef {
return {
namespace: stringField(input, "namespace", path),
name: stringField(input, "name", path),
key: stringField(input, "key", path),
};
}
function parseGithubProxy(input: Record<string, unknown>, path: string): { host: string; port: number } {
return {
host: stringField(input, "host", path),
port: integerField(input, "port", path),
};
}
function optionalEnvPair(obj: Record<string, unknown>, key: string, path: string): { name: string; value: string } | null {
const value = obj[key];
if (value === undefined || value === null) return null;
const record = asRecord(value, `${path}.${key}`);
return {
name: stringField(record, "name", `${path}.${key}`),
value: stringField(record, "value", `${path}.${key}`),
};
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
@@ -353,12 +600,28 @@ function integerField(obj: Record<string, unknown>, key: string, path: string):
return Number(value);
}
function optionalIntegerField(obj: Record<string, unknown>, key: string, path: string): number | undefined {
const value = obj[key];
if (value === undefined || value === null) return undefined;
if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer when set`);
return Number(value);
}
function arrayField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown>[] {
const value = obj[key];
if (!Array.isArray(value)) throw new Error(`${path}.${key} must be a YAML array`);
return value.map((item, index) => asRecord(item, `${path}.${key}[${index}]`));
}
function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const value = obj[key];
if (!Array.isArray(value)) throw new Error(`${path}.${key} must be a YAML array`);
return value.map((item, index) => {
if (typeof item !== "string" || item.trim().length === 0) throw new Error(`${path}.${key}[${index}] must be a non-empty string`);
return item.trim();
});
}
function enumField<T extends string>(obj: Record<string, unknown>, key: string, path: string, values: readonly T[]): T {
const value = stringField(obj, key, path);
if (!values.includes(value as T)) throw new Error(`${path}.${key} must be one of ${values.join(", ")}`);
@@ -371,12 +634,28 @@ function absolutePathField(obj: Record<string, unknown>, key: string, path: stri
return value;
}
function optionalAbsolutePathField(obj: Record<string, unknown>, key: string, path: string): string | undefined {
const value = obj[key];
if (value === undefined || value === null) return undefined;
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string when set`);
const trimmed = value.trim();
if (!trimmed.startsWith("/") || trimmed.includes("..")) throw new Error(`${path}.${key} must be an absolute path without ..`);
return trimmed;
}
function relativePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be a relative path without ..`);
return value;
}
function secretSourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.includes("..")) throw new Error(`${path}.${key} must not contain ..`);
if (!value.startsWith("/") && value.startsWith(".")) throw new Error(`${path}.${key} must be absolute or relative without a leading dot`);
return value;
}
function urlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
try {