import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import type { JsonRecord } from "../../src/common/types.js"; import { AgentRunError, errorToJson } from "../../src/common/errors.js"; interface RenderOptions { outDir: string; deployFile: string; catalogFile?: string; sourceCommit: string; registryPrefix: string; requireCatalog: boolean; check: boolean; } interface ArtifactCatalog { lane: string; sourceBranch: string; gitopsBranch: string; sourceCommitId: string; summary?: string; services: CatalogService[]; } interface CatalogService { serviceId: string; image: string; digest: string; repositoryDigest: string; imageTag: string; artifactKind?: string; status?: string; envIdentity?: string; envImage?: string; envDigest?: string; envRepositoryDigest?: string; bootCommit?: string; bootScript?: string; provenance?: JsonRecord; } const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git"; export async function runGitopsRenderCli(argv: string[]): Promise { try { const options = parseArgs(argv); const summary = await renderGitops(options); console.log(JSON.stringify({ ok: true, data: summary })); } catch (error) { console.log(JSON.stringify({ ok: false, failureKind: error instanceof AgentRunError ? error.failureKind : "infra-failed", message: error instanceof Error ? error.message : String(error), error: errorToJson(error) })); process.exitCode = 1; } } export async function renderGitops(options: RenderOptions): Promise { const deploy = JSON.parse(await readFile(options.deployFile, "utf8")) as JsonRecord; const runtimeNamespace = stringField(deploy, "runtimeNamespace", "agentrun-v01"); const gitopsBranch = stringField(deploy, "gitopsBranch", "v0.1-gitops"); const runtimePath = stringField(deploy, "runtimePath", "deploy/gitops/g14/runtime-v01"); const catalog = await loadCatalog(options, gitopsBranch); const image = imageForService(catalog, "agentrun-mgr", options); if (options.check) await rm(options.outDir, { recursive: true, force: true }); await mkdir(path.join(options.outDir, "argocd"), { recursive: true }); await mkdir(path.join(options.outDir, "runtime-v01"), { recursive: true }); await writeFile(path.join(options.outDir, "source.json"), `${JSON.stringify({ lane: "v0.1", sourceCommit: options.sourceCommit, generatedBy: "scripts/agentrun-gitops-render.ts" }, null, 2)}\n`); await writeFile(path.join(options.outDir, "artifact-catalog.v01.json"), `${JSON.stringify(catalog, null, 2)}\n`); await writeFile(path.join(options.outDir, "argocd", "project.yaml"), projectYaml(runtimeNamespace)); await writeFile(path.join(options.outDir, "argocd", "application-v01.yaml"), applicationYaml(gitopsBranch, runtimePath, runtimeNamespace)); await writeFile(path.join(options.outDir, "runtime-v01", "kustomization.yaml"), kustomizationYaml()); await writeFile(path.join(options.outDir, "runtime-v01", "namespace.yaml"), namespaceYaml(runtimeNamespace)); await writeFile(path.join(options.outDir, "runtime-v01", "postgres.yaml"), postgresYaml(runtimeNamespace)); await writeFile(path.join(options.outDir, "runtime-v01", "mgr.yaml"), managerYaml(runtimeNamespace, image, options.sourceCommit)); await writeFile(path.join(options.outDir, "runtime-v01", "runner-rbac.yaml"), runnerRbacYaml(runtimeNamespace)); return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: repositoryDigestForService(image), sourceCommit: options.sourceCommit, envIdentity: image.envIdentity ?? null, artifactStatus: image.status ?? null }; } async function loadCatalog(options: RenderOptions, gitopsBranch: string): Promise { if (options.catalogFile) return JSON.parse(await readFile(options.catalogFile, "utf8")) as ArtifactCatalog; if (options.requireCatalog) throw new AgentRunError("schema-invalid", "artifact catalog is required for promotion render", { httpStatus: 2 }); const digest = `sha256:${"0".repeat(64)}`; const image = `${options.registryPrefix}/agentrun-mgr-env:${options.sourceCommit}`; return { lane: "v0.1", sourceBranch: "v0.1", gitopsBranch, sourceCommitId: options.sourceCommit, summary: "build=1 reuse=0 unsafeReuse=0", services: [{ serviceId: "agentrun-mgr", artifactKind: "env-reuse", status: "placeholder", image, digest, repositoryDigest: `${options.registryPrefix}/agentrun-mgr-env@${digest}`, imageTag: options.sourceCommit, envIdentity: options.sourceCommit, envImage: image, envDigest: digest, envRepositoryDigest: `${options.registryPrefix}/agentrun-mgr-env@${digest}`, bootCommit: options.sourceCommit, bootScript: "deploy/runtime/boot/agentrun-boot.sh" }], }; } function imageForService(catalog: ArtifactCatalog, serviceId: string, options: RenderOptions): CatalogService { const service = catalog.services.find((item) => item.serviceId === serviceId); if (!service) throw new AgentRunError("schema-invalid", `catalog missing service ${serviceId}`, { httpStatus: 2 }); const digest = service.envDigest ?? service.digest; if (!/^sha256:[a-f0-9]{64}$/u.test(digest)) throw new AgentRunError("schema-invalid", `catalog service ${serviceId} has invalid digest`, { httpStatus: 2 }); if (options.requireCatalog && digest === `sha256:${"0".repeat(64)}`) throw new AgentRunError("schema-invalid", "placeholder digest is not allowed in promotion render", { httpStatus: 2 }); return service; } function repositoryDigestForService(service: CatalogService): string { if (service.envRepositoryDigest) return service.envRepositoryDigest; if (service.repositoryDigest) return service.repositoryDigest; const image = service.envImage ?? service.image; const digest = service.envDigest ?? service.digest; return `${image.slice(0, image.lastIndexOf(":"))}@${digest}`; } function projectYaml(namespace: string): string { return `apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: name: agentrun-v01 namespace: argocd spec: description: AgentRun v0.1 GitOps lane sourceRepos: - git@github.com:pikasTech/agentrun.git destinations: - server: https://kubernetes.default.svc namespace: ${namespace} clusterResourceWhitelist: - group: "" kind: Namespace namespaceResourceWhitelist: - group: "*" kind: "*" `; } function applicationYaml(gitopsBranch: string, runtimePath: string, namespace: string): string { return `apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: agentrun-g14-v01 namespace: argocd spec: project: agentrun-v01 source: repoURL: git@github.com:pikasTech/agentrun.git targetRevision: ${gitopsBranch} path: ${runtimePath} destination: server: https://kubernetes.default.svc namespace: ${namespace} syncPolicy: automated: prune: false selfHeal: true syncOptions: - CreateNamespace=true - ApplyOutOfSyncOnly=true `; } function kustomizationYaml(): string { return `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - namespace.yaml - postgres.yaml - mgr.yaml - runner-rbac.yaml `; } function namespaceYaml(namespace: string): string { return `apiVersion: v1 kind: Namespace metadata: name: ${namespace} labels: app.kubernetes.io/part-of: agentrun agentrun.pikastech.local/lane: v0.1 `; } function postgresYaml(namespace: string): string { return `apiVersion: v1 kind: Service metadata: name: agentrun-v01-postgres namespace: ${namespace} spec: selector: app.kubernetes.io/name: agentrun-v01-postgres ports: - name: postgres port: 5432 targetPort: postgres --- apiVersion: apps/v1 kind: StatefulSet metadata: name: agentrun-v01-postgres namespace: ${namespace} spec: serviceName: agentrun-v01-postgres replicas: 1 selector: matchLabels: app.kubernetes.io/name: agentrun-v01-postgres template: metadata: labels: app.kubernetes.io/name: agentrun-v01-postgres spec: containers: - name: postgres image: postgres:16-alpine ports: - name: postgres containerPort: 5432 env: - name: POSTGRES_DB value: agentrun_v01 - name: POSTGRES_USER valueFrom: secretKeyRef: name: agentrun-v01-postgres key: username - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: agentrun-v01-postgres key: password volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gi `; } function managerYaml(namespace: string, image: CatalogService, sourceCommit: string): string { const imageRef = repositoryDigestForService(image); const envIdentity = image.envIdentity ?? image.imageTag ?? "unknown"; return `apiVersion: v1 kind: ServiceAccount metadata: name: agentrun-v01-mgr namespace: ${namespace} --- apiVersion: v1 kind: Service metadata: name: agentrun-mgr namespace: ${namespace} spec: selector: app.kubernetes.io/name: agentrun-mgr ports: - name: http port: 8080 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: agentrun-mgr namespace: ${namespace} spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: agentrun-mgr template: metadata: labels: app.kubernetes.io/name: agentrun-mgr annotations: agentrun.pikastech.local/lane: v0.1 agentrun.pikastech.local/source-commit: ${JSON.stringify(sourceCommit)} agentrun.pikastech.local/env-identity: ${JSON.stringify(envIdentity)} spec: serviceAccountName: agentrun-v01-mgr containers: - name: mgr image: ${imageRef} imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 env: - name: AGENTRUN_LANE value: v0.1 - name: DATABASE_URL valueFrom: secretKeyRef: name: agentrun-v01-mgr-db key: DATABASE_URL - name: AGENTRUN_SOURCE_COMMIT value: ${JSON.stringify(sourceCommit)} - name: AGENTRUN_BOOT_COMMIT value: ${JSON.stringify(sourceCommit)} - name: AGENTRUN_BOOT_MODE value: mgr - name: AGENTRUN_BOOT_REPO_URL value: ${JSON.stringify(defaultBootRepoUrl)} - name: AGENTRUN_ENV_IDENTITY value: ${JSON.stringify(envIdentity)} - name: AGENTRUN_RUNTIME_NAMESPACE value: ${JSON.stringify(namespace)} - name: AGENTRUN_INTERNAL_MGR_URL value: ${JSON.stringify(`http://agentrun-mgr.${namespace}.svc.cluster.local:8080`)} - name: AGENTRUN_RUNNER_IMAGE value: ${JSON.stringify(imageRef)} - name: AGENTRUN_RUNNER_SERVICE_ACCOUNT value: "agentrun-v01-runner" readinessProbe: httpGet: path: /health/readiness port: http livenessProbe: httpGet: path: /health/live port: http resources: requests: cpu: 100m memory: 256Mi limits: cpu: 800m memory: 1Gi --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: agentrun-v01-mgr-runner-job-controller namespace: ${namespace} 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: agentrun-v01-mgr-runner-job-controller namespace: ${namespace} subjects: - kind: ServiceAccount name: agentrun-v01-mgr roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: agentrun-v01-mgr-runner-job-controller --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: agentrun-v01-mgr-provider-secret-manager namespace: ${namespace} rules: - apiGroups: [""] resources: ["secrets"] verbs: ["create", "get", "list", "patch", "update"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: agentrun-v01-mgr-provider-secret-manager namespace: ${namespace} subjects: - kind: ServiceAccount name: agentrun-v01-mgr roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: agentrun-v01-mgr-provider-secret-manager `; } function runnerRbacYaml(namespace: string): string { return `apiVersion: v1 kind: ServiceAccount metadata: name: agentrun-v01-runner namespace: ${namespace} --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: agentrun-v01-runner-secret-reader namespace: ${namespace} rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: agentrun-v01-runner-secret-reader namespace: ${namespace} subjects: - kind: ServiceAccount name: agentrun-v01-runner roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: agentrun-v01-runner-secret-reader `; } function parseArgs(argv: string[]): RenderOptions { const flags = new Map(); for (let index = 0; index < argv.length; index += 1) { const item = argv[index] ?? ""; if (!item.startsWith("--")) continue; const key = item.slice(2); const next = argv[index + 1]; if (next === undefined || next.startsWith("--")) flags.set(key, true); else { flags.set(key, next); index += 1; } } const options: RenderOptions = { outDir: stringFlag(flags, "out", "deploy/gitops/g14"), deployFile: stringFlag(flags, "deploy-file", "deploy/deploy.json"), sourceCommit: stringFlag(flags, "source-commit", "source-check"), registryPrefix: stringFlag(flags, "registry-prefix", "127.0.0.1:5000/agentrun"), requireCatalog: flags.get("require-catalog") === true, check: flags.get("check") === true, }; const catalogFile = optionalStringFlag(flags, "catalog"); if (catalogFile) options.catalogFile = catalogFile; return options; } function stringFlag(flags: Map, key: string, fallback: string): string { const value = flags.get(key); return typeof value === "string" && value.length > 0 ? value : fallback; } function optionalStringFlag(flags: Map, key: string): string | undefined { const value = flags.get(key); return typeof value === "string" && value.length > 0 ? value : undefined; } function stringField(record: JsonRecord, key: string, fallback: string): string { const value = record[key]; return typeof value === "string" && value.length > 0 ? value : fallback; }