feat: move AgentRun deploy truth to UniDesk YAML
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { AgentRunLaneSpec } from "./agentrun-lanes";
|
||||
|
||||
export interface AgentRunArtifactService {
|
||||
readonly serviceId: string;
|
||||
readonly image: string;
|
||||
readonly digest: string;
|
||||
readonly repositoryDigest: string;
|
||||
readonly imageTag: string;
|
||||
readonly artifactKind: string;
|
||||
readonly status: string;
|
||||
readonly envIdentity: string;
|
||||
readonly envImage: string;
|
||||
readonly envDigest: string;
|
||||
readonly envRepositoryDigest: string;
|
||||
readonly bootCommit: string;
|
||||
readonly bootScript: string;
|
||||
readonly provenance: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentRunArtifactCatalog {
|
||||
readonly lane: string;
|
||||
readonly sourceBranch: string;
|
||||
readonly gitopsBranch: string;
|
||||
readonly sourceCommitId: string;
|
||||
readonly summary: string;
|
||||
readonly services: readonly AgentRunArtifactService[];
|
||||
}
|
||||
|
||||
export interface AgentRunGitopsRenderInput {
|
||||
readonly sourceCommit: string;
|
||||
readonly image: AgentRunArtifactService;
|
||||
}
|
||||
|
||||
export interface AgentRunRenderedFile {
|
||||
readonly path: string;
|
||||
readonly content: string;
|
||||
}
|
||||
|
||||
export function renderAgentRunControlPlaneManifests(spec: AgentRunLaneSpec): readonly Record<string, unknown>[] {
|
||||
return [
|
||||
{ apiVersion: "v1", kind: "Namespace", metadata: { name: spec.ci.namespace } },
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "ServiceAccount",
|
||||
metadata: {
|
||||
name: spec.ci.serviceAccountName,
|
||||
namespace: spec.ci.namespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: {
|
||||
name: spec.ci.serviceAccountName,
|
||||
namespace: spec.ci.namespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
rules: [
|
||||
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns", "taskruns"], verbs: ["get", "list", "watch", "create", "patch", "update"] },
|
||||
{ apiGroups: [""], resources: ["pods", "pods/log", "secrets", "configmaps", "persistentvolumeclaims"], verbs: ["get", "list", "watch", "create", "patch", "update", "delete"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: {
|
||||
name: spec.ci.serviceAccountName,
|
||||
namespace: spec.ci.namespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
subjects: [{ kind: "ServiceAccount", name: spec.ci.serviceAccountName }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: spec.ci.serviceAccountName },
|
||||
},
|
||||
agentRunPipelineManifest(spec),
|
||||
agentRunArgoProjectManifest(spec),
|
||||
agentRunArgoApplicationManifest(spec),
|
||||
];
|
||||
}
|
||||
|
||||
export function renderAgentRunGitopsFiles(spec: AgentRunLaneSpec, input: AgentRunGitopsRenderInput): readonly AgentRunRenderedFile[] {
|
||||
const catalog = agentRunArtifactCatalog(spec, input.sourceCommit, input.image);
|
||||
const source = {
|
||||
lane: spec.version,
|
||||
sourceCommit: input.sourceCommit,
|
||||
generatedBy: "unidesk config/agentrun.yaml",
|
||||
configSource: "config/agentrun.yaml",
|
||||
};
|
||||
return [
|
||||
{ path: "source.json", content: `${JSON.stringify(source, null, 2)}\n` },
|
||||
{ path: spec.deployment.artifactCatalogPath, content: `${JSON.stringify(catalog, null, 2)}\n` },
|
||||
{ path: `${spec.deployment.gitopsRoot}/argocd/project.yaml`, content: yaml(agentRunArgoProjectManifest(spec)) },
|
||||
{ path: `${spec.deployment.gitopsRoot}/argocd/${spec.deployment.argocd.applicationFile}`, content: yaml(agentRunArgoApplicationManifest(spec)) },
|
||||
{ path: `${spec.deployment.gitopsRoot}/${spec.deployment.runtimeRenderDir}/kustomization.yaml`, content: yaml(agentRunKustomizationManifest(spec)) },
|
||||
{ path: `${spec.deployment.gitopsRoot}/${spec.deployment.runtimeRenderDir}/namespace.yaml`, content: yaml(agentRunRuntimeNamespaceManifest(spec)) },
|
||||
...(spec.deployment.localPostgres.enabled ? [{ path: `${spec.deployment.gitopsRoot}/${spec.deployment.runtimeRenderDir}/postgres.yaml`, content: yaml(agentRunPostgresManifest(spec)) }] : []),
|
||||
{ path: `${spec.deployment.gitopsRoot}/${spec.deployment.runtimeRenderDir}/mgr.yaml`, content: yamlAll(agentRunManagerManifests(spec, input.sourceCommit, input.image)) },
|
||||
{ path: `${spec.deployment.gitopsRoot}/${spec.deployment.runtimeRenderDir}/runner-rbac.yaml`, content: yamlAll(agentRunRunnerRbacManifests(spec)) },
|
||||
];
|
||||
}
|
||||
|
||||
export function placeholderAgentRunImage(spec: AgentRunLaneSpec, sourceCommit: string): AgentRunArtifactService {
|
||||
const digest = `sha256:${"0".repeat(64)}`;
|
||||
const image = `${spec.ci.registryPrefix}/agentrun-mgr-env:${sourceCommit}`;
|
||||
return {
|
||||
serviceId: "agentrun-mgr",
|
||||
artifactKind: "env-reuse",
|
||||
status: "placeholder",
|
||||
image,
|
||||
digest,
|
||||
repositoryDigest: `${spec.ci.registryPrefix}/agentrun-mgr-env@${digest}`,
|
||||
imageTag: sourceCommit,
|
||||
envIdentity: sourceCommit,
|
||||
envImage: image,
|
||||
envDigest: digest,
|
||||
envRepositoryDigest: `${spec.ci.registryPrefix}/agentrun-mgr-env@${digest}`,
|
||||
bootCommit: sourceCommit,
|
||||
bootScript: "deploy/runtime/boot/agentrun-boot.sh",
|
||||
provenance: {
|
||||
sourceCommitId: sourceCommit,
|
||||
source: "placeholder",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function agentRunImageArtifact(spec: AgentRunLaneSpec, input: {
|
||||
sourceCommit: string;
|
||||
envIdentity: string;
|
||||
digest: string;
|
||||
status: string;
|
||||
}): AgentRunArtifactService {
|
||||
const image = `${spec.ci.registryPrefix}/${spec.deployment.manager.imageBuild.repository}:${input.envIdentity}`;
|
||||
return {
|
||||
serviceId: "agentrun-mgr",
|
||||
artifactKind: "env-reuse",
|
||||
status: input.status,
|
||||
image,
|
||||
digest: input.digest,
|
||||
repositoryDigest: `${spec.ci.registryPrefix}/${spec.deployment.manager.imageBuild.repository}@${input.digest}`,
|
||||
imageTag: input.envIdentity,
|
||||
envIdentity: input.envIdentity,
|
||||
envImage: image,
|
||||
envDigest: input.digest,
|
||||
envRepositoryDigest: `${spec.ci.registryPrefix}/${spec.deployment.manager.imageBuild.repository}@${input.digest}`,
|
||||
bootCommit: input.sourceCommit,
|
||||
bootScript: "deploy/runtime/boot/agentrun-boot.sh",
|
||||
provenance: {
|
||||
sourceCommitId: input.sourceCommit,
|
||||
source: "unidesk-yaml-only",
|
||||
configSource: "config/agentrun.yaml",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function renderedFilesDigest(files: readonly AgentRunRenderedFile[]): string {
|
||||
const hash = createHash("sha256");
|
||||
for (const file of [...files].sort((left, right) => left.path.localeCompare(right.path))) {
|
||||
hash.update(file.path);
|
||||
hash.update("\0");
|
||||
hash.update(file.content);
|
||||
hash.update("\0");
|
||||
}
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
export function renderedObjectsDigest(objects: readonly Record<string, unknown>[]): string {
|
||||
return `sha256:${createHash("sha256").update(yamlAll(objects)).digest("hex")}`;
|
||||
}
|
||||
|
||||
function agentRunPipelineManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
apiVersion: "tekton.dev/v1",
|
||||
kind: "Pipeline",
|
||||
metadata: {
|
||||
name: spec.ci.pipeline,
|
||||
namespace: spec.ci.namespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
spec: {
|
||||
params: [
|
||||
{ name: "git-url", type: "string", default: spec.source.remote },
|
||||
{ name: "git-read-url", type: "string", default: spec.gitMirror.readUrl },
|
||||
{ name: "git-write-url", type: "string", default: spec.gitMirror.writeUrl },
|
||||
{ name: "source-branch", type: "string", default: spec.source.branch },
|
||||
{ name: "gitops-branch", type: "string", default: spec.gitops.branch },
|
||||
{ name: "revision", type: "string" },
|
||||
{ name: "registry-prefix", type: "string", default: spec.ci.registryPrefix },
|
||||
{ name: "tools-image", type: "string", default: spec.ci.toolsImage },
|
||||
],
|
||||
workspaces: [{ name: "source" }, { name: "git-ssh" }],
|
||||
tasks: [
|
||||
gitopsSmokeTask(spec),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function gitopsSmokeTask(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
name: "render-smoke",
|
||||
workspaces: [{ name: "source", workspace: "source" }],
|
||||
taskSpec: {
|
||||
params: [{ name: "revision" }, { name: "tools-image" }],
|
||||
workspaces: [{ name: "source" }],
|
||||
steps: [
|
||||
{
|
||||
name: "render-smoke",
|
||||
image: "$(params.tools-image)",
|
||||
script: [
|
||||
"#!/bin/sh",
|
||||
"set -eu",
|
||||
"echo '{\"event\":\"agentrun-ci-render-smoke\",\"status\":\"placeholder\",\"reason\":\"unidesk-yaml-only-control-plane\",\"valuesPrinted\":false}'",
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
},
|
||||
params: [
|
||||
{ name: "revision", value: "$(params.revision)" },
|
||||
{ name: "tools-image", value: "$(params.tools-image)" },
|
||||
],
|
||||
when: [{ input: spec.deployment.format, operator: "in", values: ["unidesk-yaml-only"] }],
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunArgoProjectManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
apiVersion: "argoproj.io/v1alpha1",
|
||||
kind: "AppProject",
|
||||
metadata: {
|
||||
name: spec.deployment.argocd.project,
|
||||
namespace: spec.gitops.argoNamespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
spec: {
|
||||
description: `AgentRun ${spec.version} GitOps lane`,
|
||||
sourceRepos: [spec.gitops.repoURL, spec.source.remote],
|
||||
destinations: [{ server: "https://kubernetes.default.svc", namespace: spec.runtime.namespace }],
|
||||
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
|
||||
namespaceResourceWhitelist: [{ group: "*", kind: "*" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunArgoApplicationManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
apiVersion: "argoproj.io/v1alpha1",
|
||||
kind: "Application",
|
||||
metadata: {
|
||||
name: spec.gitops.argoApplication,
|
||||
namespace: spec.gitops.argoNamespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
spec: {
|
||||
project: spec.deployment.argocd.project,
|
||||
source: {
|
||||
repoURL: spec.gitops.repoURL,
|
||||
targetRevision: spec.gitops.branch,
|
||||
path: spec.gitops.path,
|
||||
},
|
||||
destination: {
|
||||
server: "https://kubernetes.default.svc",
|
||||
namespace: spec.runtime.namespace,
|
||||
},
|
||||
syncPolicy: {
|
||||
automated: { prune: false, selfHeal: true },
|
||||
syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunKustomizationManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
apiVersion: "kustomize.config.k8s.io/v1beta1",
|
||||
kind: "Kustomization",
|
||||
resources: [
|
||||
"namespace.yaml",
|
||||
...(spec.deployment.localPostgres.enabled ? ["postgres.yaml"] : []),
|
||||
"mgr.yaml",
|
||||
"runner-rbac.yaml",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunRuntimeNamespaceManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "Namespace",
|
||||
metadata: {
|
||||
name: spec.runtime.namespace,
|
||||
labels: agentRunLabels(spec),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunPostgresManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
const localPostgres = spec.deployment.localPostgres;
|
||||
if (!localPostgres.enabled || localPostgres.serviceName === null || localPostgres.image === null || localPostgres.storage === null || localPostgres.port === null) {
|
||||
throw new Error(`localPostgres is enabled for ${spec.version} without renderable YAML fields`);
|
||||
}
|
||||
const name = localPostgres.serviceName;
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
items: [
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "Service",
|
||||
metadata: { name, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
spec: { selector: { "app.kubernetes.io/name": name }, ports: [{ name: "postgres", port: localPostgres.port, targetPort: "postgres" }] },
|
||||
},
|
||||
{
|
||||
apiVersion: "apps/v1",
|
||||
kind: "StatefulSet",
|
||||
metadata: { name, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
spec: {
|
||||
serviceName: name,
|
||||
replicas: 1,
|
||||
selector: { matchLabels: { "app.kubernetes.io/name": name } },
|
||||
template: {
|
||||
metadata: { labels: { ...agentRunLabels(spec), "app.kubernetes.io/name": name } },
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: "postgres",
|
||||
image: localPostgres.image,
|
||||
ports: [{ name: "postgres", containerPort: localPostgres.port }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
volumeClaimTemplates: [
|
||||
{
|
||||
metadata: { name: "data" },
|
||||
spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: localPostgres.storage } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunManagerManifests(spec: AgentRunLaneSpec, sourceCommit: string, image: AgentRunArtifactService): readonly Record<string, unknown>[] {
|
||||
const imageRef = image.envRepositoryDigest || image.repositoryDigest;
|
||||
return [
|
||||
{ apiVersion: "v1", kind: "ServiceAccount", metadata: { name: spec.deployment.manager.serviceAccount, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) } },
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "Service",
|
||||
metadata: { name: spec.runtime.managerService, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
spec: {
|
||||
selector: { "app.kubernetes.io/name": spec.runtime.managerDeployment },
|
||||
ports: [{ name: "http", port: spec.runtime.managerPort, targetPort: "http" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deployment",
|
||||
metadata: { name: spec.runtime.managerDeployment, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
spec: {
|
||||
replicas: 1,
|
||||
selector: { matchLabels: { "app.kubernetes.io/name": spec.runtime.managerDeployment } },
|
||||
template: {
|
||||
metadata: {
|
||||
labels: { ...agentRunLabels(spec), "app.kubernetes.io/name": spec.runtime.managerDeployment },
|
||||
annotations: {
|
||||
"agentrun.pikastech.local/lane": spec.version,
|
||||
"agentrun.pikastech.local/source-commit": sourceCommit,
|
||||
"agentrun.pikastech.local/env-identity": image.envIdentity,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
serviceAccountName: spec.deployment.manager.serviceAccount,
|
||||
containers: [
|
||||
{
|
||||
name: "mgr",
|
||||
image: imageRef,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
ports: [{ name: "http", containerPort: 8080 }],
|
||||
env: managerEnv(spec, sourceCommit, imageRef, image.envIdentity),
|
||||
readinessProbe: { httpGet: { path: "/health/readiness", port: "http" } },
|
||||
livenessProbe: { httpGet: { path: "/health/live", port: "http" } },
|
||||
resources: spec.deployment.manager.resources,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: { name: `${spec.deployment.manager.serviceAccount}-runner-job-controller`, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
rules: [
|
||||
{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["create", "get", "list", "watch"] },
|
||||
{ apiGroups: [""], resources: ["pods"], verbs: ["get", "list", "watch"] },
|
||||
{ apiGroups: [""], resources: ["persistentvolumeclaims"], verbs: ["create", "get", "list", "watch", "delete"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: { name: `${spec.deployment.manager.serviceAccount}-runner-job-controller`, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
subjects: [{ kind: "ServiceAccount", name: spec.deployment.manager.serviceAccount }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: `${spec.deployment.manager.serviceAccount}-runner-job-controller` },
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: { name: `${spec.deployment.manager.serviceAccount}-provider-secret-manager`, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
rules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["create", "delete", "get", "list", "patch", "update"] }],
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: { name: `${spec.deployment.manager.serviceAccount}-provider-secret-manager`, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
subjects: [{ kind: "ServiceAccount", name: spec.deployment.manager.serviceAccount }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: `${spec.deployment.manager.serviceAccount}-provider-secret-manager` },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function managerEnv(spec: AgentRunLaneSpec, sourceCommit: string, imageRef: string, envIdentity: string): readonly Record<string, unknown>[] {
|
||||
return [
|
||||
{ name: "AGENTRUN_LANE", value: spec.version },
|
||||
{ name: "DATABASE_URL", valueFrom: { secretKeyRef: spec.database.secretRef } },
|
||||
{ name: "AGENTRUN_SOURCE_COMMIT", value: sourceCommit },
|
||||
{ name: "AGENTRUN_BOOT_COMMIT", value: sourceCommit },
|
||||
{ name: "AGENTRUN_BOOT_MODE", value: "mgr" },
|
||||
{ name: "AGENTRUN_BOOT_REPO_URL", value: spec.deployment.manager.bootRepoUrl },
|
||||
{ name: "AGENTRUN_ENV_IDENTITY", value: envIdentity },
|
||||
{ name: "AGENTRUN_RUNTIME_NAMESPACE", value: spec.runtime.namespace },
|
||||
{ name: "AGENTRUN_INTERNAL_MGR_URL", value: spec.runtime.internalBaseUrl },
|
||||
{ name: "AGENTRUN_RUNNER_IMAGE", value: imageRef },
|
||||
{ name: "AGENTRUN_RUNNER_SERVICE_ACCOUNT", value: spec.deployment.runner.serviceAccount },
|
||||
{ name: "AGENTRUN_API_KEY", valueFrom: { secretKeyRef: spec.deployment.manager.apiKeySecretRef } },
|
||||
...(spec.deployment.manager.unideskSshEndpointEnv === null ? [] : [{ name: spec.deployment.manager.unideskSshEndpointEnv.name, value: spec.deployment.manager.unideskSshEndpointEnv.value }]),
|
||||
];
|
||||
}
|
||||
|
||||
function agentRunRunnerRbacManifests(spec: AgentRunLaneSpec): readonly Record<string, unknown>[] {
|
||||
return [
|
||||
{ apiVersion: "v1", kind: "ServiceAccount", metadata: { name: spec.deployment.runner.serviceAccount, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) } },
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: { name: `${spec.deployment.runner.serviceAccount}-secret-reader`, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
rules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["get"] }],
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: { name: `${spec.deployment.runner.serviceAccount}-secret-reader`, namespace: spec.runtime.namespace, labels: agentRunLabels(spec) },
|
||||
subjects: [{ kind: "ServiceAccount", name: spec.deployment.runner.serviceAccount }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: `${spec.deployment.runner.serviceAccount}-secret-reader` },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function agentRunArtifactCatalog(spec: AgentRunLaneSpec, sourceCommit: string, image: AgentRunArtifactService): AgentRunArtifactCatalog {
|
||||
return {
|
||||
lane: spec.version,
|
||||
sourceBranch: spec.source.branch,
|
||||
gitopsBranch: spec.gitops.branch,
|
||||
sourceCommitId: sourceCommit,
|
||||
summary: image.status === "placeholder" ? "build=0 reuse=0 placeholder=1" : "build=1 reuse=0 placeholder=0",
|
||||
services: [image],
|
||||
};
|
||||
}
|
||||
|
||||
function agentRunLabels(spec: AgentRunLaneSpec): Record<string, string> {
|
||||
return {
|
||||
"app.kubernetes.io/part-of": "agentrun",
|
||||
"agentrun.pikastech.local/lane": spec.version,
|
||||
"agentrun.pikastech.local/node": spec.nodeId,
|
||||
};
|
||||
}
|
||||
|
||||
function yaml(value: unknown): string {
|
||||
return `${Bun.YAML.stringify(value).trim()}\n`;
|
||||
}
|
||||
|
||||
function yamlAll(values: readonly unknown[]): string {
|
||||
return `${values.map((value) => Bun.YAML.stringify(value).trim()).join("\n---\n")}\n`;
|
||||
}
|
||||
+944
-42
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user