feat: add yaml-first AgentRun lane ops
This commit is contained in:
@@ -28,3 +28,133 @@ auth:
|
||||
client:
|
||||
role: render-only
|
||||
transport: direct-http
|
||||
|
||||
controlPlane:
|
||||
default:
|
||||
node: G14
|
||||
lane: v01
|
||||
|
||||
nodes:
|
||||
G14:
|
||||
route: G14
|
||||
kubeRoute: G14:k3s
|
||||
D601:
|
||||
route: D601
|
||||
kubeRoute: D601:k3s
|
||||
|
||||
lanes:
|
||||
v01:
|
||||
node: G14
|
||||
version: v0.1
|
||||
source:
|
||||
repository: pikasTech/agentrun
|
||||
branch: v0.1
|
||||
remote: git@github.com:pikasTech/agentrun.git
|
||||
workspace: /root/agentrun-v01
|
||||
runtime:
|
||||
namespace: agentrun-v01
|
||||
managerDeployment: agentrun-mgr
|
||||
managerService: agentrun-mgr
|
||||
managerPort: 8080
|
||||
internalBaseUrl: http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080
|
||||
ci:
|
||||
namespace: agentrun-ci
|
||||
pipeline: agentrun-v01-ci-image-publish
|
||||
pipelineRunPrefix: agentrun-v01-ci
|
||||
serviceAccountName: agentrun-v01-tekton-runner
|
||||
registryPrefix: 127.0.0.1:5000/agentrun
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
gitops:
|
||||
branch: v0.1-gitops
|
||||
path: deploy/gitops/g14/runtime-v01
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-g14-v01
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
readService: git-mirror-http
|
||||
writeService: git-mirror-write
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git
|
||||
cachePvc: git-mirror-cache
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
syncJobPrefix: git-mirror-agentrun-sync-manual
|
||||
flushJobPrefix: git-mirror-agentrun-flush-manual
|
||||
repositories:
|
||||
- key: agentrun
|
||||
repository: pikasTech/agentrun
|
||||
sourceBranch: v0.1
|
||||
gitopsBranch: v0.1-gitops
|
||||
- key: unidesk
|
||||
repository: pikasTech/unidesk
|
||||
sourceBranch: master
|
||||
- key: agent_skills
|
||||
repository: pikasTech/agent_skills
|
||||
sourceBranch: master
|
||||
database:
|
||||
mode: local-postgres
|
||||
secretRef:
|
||||
name: agentrun-v01-mgr-db
|
||||
key: DATABASE_URL
|
||||
localPostgresExpectedAbsent: false
|
||||
|
||||
v02:
|
||||
node: D601
|
||||
version: v0.2
|
||||
source:
|
||||
repository: pikasTech/agentrun
|
||||
branch: v0.2
|
||||
remote: git@github.com:pikasTech/agentrun.git
|
||||
workspace: /home/ubuntu/workspace/agentrun-v02
|
||||
runtime:
|
||||
namespace: agentrun-v02
|
||||
managerDeployment: agentrun-mgr
|
||||
managerService: agentrun-mgr
|
||||
managerPort: 8080
|
||||
internalBaseUrl: http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080
|
||||
ci:
|
||||
namespace: agentrun-ci
|
||||
pipeline: agentrun-v02-ci-image-publish
|
||||
pipelineRunPrefix: agentrun-v02-ci
|
||||
serviceAccountName: agentrun-v02-tekton-runner
|
||||
registryPrefix: 127.0.0.1:5000/agentrun
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
gitops:
|
||||
branch: v0.2-gitops
|
||||
path: deploy/gitops/node/d601/runtime-v02
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-d601-v02
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
readService: git-mirror-http
|
||||
writeService: git-mirror-write
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
cachePvc: hwlab-git-mirror-cache
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
syncJobPrefix: git-mirror-agentrun-d601-v02-sync-manual
|
||||
flushJobPrefix: git-mirror-agentrun-d601-v02-flush-manual
|
||||
repositories:
|
||||
- key: agentrun
|
||||
repository: pikasTech/agentrun
|
||||
sourceBranch: v0.2
|
||||
gitopsBranch: v0.2-gitops
|
||||
- key: unidesk
|
||||
repository: pikasTech/unidesk
|
||||
sourceBranch: master
|
||||
- key: agent_skills
|
||||
repository: pikasTech/agent_skills
|
||||
sourceBranch: master
|
||||
database:
|
||||
mode: external-postgres
|
||||
provider: PK01
|
||||
configRef: config/platform-db/postgres-pk01.yaml
|
||||
database: agentrun_v02
|
||||
user: agentrun_v02
|
||||
sslmode: require
|
||||
secretSourceRef: agentrun/d601-v02-mgr-db.env
|
||||
secretRef:
|
||||
name: agentrun-v02-mgr-db
|
||||
key: DATABASE_URL
|
||||
localPostgresExpectedAbsent: true
|
||||
|
||||
@@ -200,6 +200,36 @@ postgres:
|
||||
user: n8n
|
||||
address: 202.98.17.68/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: agentrun_v02
|
||||
user: agentrun_v02
|
||||
address: 10.0.8.0/22
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: postgres
|
||||
user: agentrun_v02
|
||||
address: 10.0.8.0/22
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: agentrun_v02
|
||||
user: agentrun_v02
|
||||
address: 36.49.29.73/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: postgres
|
||||
user: agentrun_v02
|
||||
address: 36.49.29.73/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: agentrun_v02
|
||||
user: agentrun_v02
|
||||
address: 74.48.78.17/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: postgres
|
||||
user: agentrun_v02
|
||||
address: 74.48.78.17/32
|
||||
method: scram-sha-256
|
||||
|
||||
secrets:
|
||||
source: master-local
|
||||
@@ -247,6 +277,20 @@ secrets:
|
||||
N8N_DB_NAME: n8n
|
||||
randomHex:
|
||||
N8N_DB_PASSWORD: 32
|
||||
- name: agentrun-v02-db-credentials
|
||||
sourceRef: platform-db/agentrun-v02-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- AGENTRUN_V02_DB_USER
|
||||
- AGENTRUN_V02_DB_PASSWORD
|
||||
- AGENTRUN_V02_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
AGENTRUN_V02_DB_USER: agentrun_v02
|
||||
AGENTRUN_V02_DB_NAME: agentrun_v02
|
||||
randomHex:
|
||||
AGENTRUN_V02_DB_PASSWORD: 32
|
||||
|
||||
objects:
|
||||
roles:
|
||||
@@ -277,6 +321,15 @@ objects:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: agentrun_v02
|
||||
passwordRef:
|
||||
sourceRef: platform-db/agentrun-v02-db.env
|
||||
key: AGENTRUN_V02_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
databases:
|
||||
- name: sub2api
|
||||
owner: sub2api
|
||||
@@ -293,6 +346,11 @@ objects:
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: agentrun_v02
|
||||
owner: agentrun_v02
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
|
||||
exports:
|
||||
connectionStrings:
|
||||
@@ -341,6 +399,21 @@ exports:
|
||||
- scope: platform-infra
|
||||
secret: n8n-secrets
|
||||
key: DATABASE_URL
|
||||
- name: agentrun-v02-database-url
|
||||
sourceSecretRef: platform-db/agentrun-v02-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(AGENTRUN_V02_DB_USER):$(AGENTRUN_V02_DB_PASSWORD)@$(PGHOST):5432/$(AGENTRUN_V02_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 82.156.23.220
|
||||
writeToSecretSource:
|
||||
sourceRef: agentrun/d601-v02-mgr-db.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: agentrun-v02
|
||||
secret: agentrun-v02-mgr-db
|
||||
key: DATABASE_URL
|
||||
|
||||
backup:
|
||||
phase: minimum-restoreable
|
||||
@@ -377,6 +450,9 @@ observability:
|
||||
- kind: psql-app-role
|
||||
database: n8n
|
||||
user: n8n
|
||||
- kind: psql-app-role
|
||||
database: agentrun_v02
|
||||
user: agentrun_v02
|
||||
- kind: disk-free
|
||||
path: /var/lib/postgresql/16/main
|
||||
minFreeGiB: 10
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
|
||||
export const AGENTRUN_CONFIG_PATH = "config/agentrun.yaml";
|
||||
|
||||
export interface AgentRunGitMirrorRepositorySpec {
|
||||
readonly key: string;
|
||||
readonly repository: string;
|
||||
readonly sourceBranch: string;
|
||||
readonly gitopsBranch?: string;
|
||||
}
|
||||
|
||||
export interface AgentRunLaneSpec {
|
||||
readonly lane: string;
|
||||
readonly nodeId: string;
|
||||
readonly nodeRoute: string;
|
||||
readonly nodeKubeRoute: string;
|
||||
readonly version: string;
|
||||
readonly source: {
|
||||
readonly repository: string;
|
||||
readonly branch: string;
|
||||
readonly remote: string;
|
||||
readonly workspace: string;
|
||||
};
|
||||
readonly runtime: {
|
||||
readonly namespace: string;
|
||||
readonly managerDeployment: string;
|
||||
readonly managerService: string;
|
||||
readonly managerPort: number;
|
||||
readonly internalBaseUrl: string;
|
||||
};
|
||||
readonly ci: {
|
||||
readonly namespace: string;
|
||||
readonly pipeline: string;
|
||||
readonly pipelineRunPrefix: string;
|
||||
readonly serviceAccountName: string;
|
||||
readonly registryPrefix: string;
|
||||
readonly toolsImage: string;
|
||||
};
|
||||
readonly gitops: {
|
||||
readonly branch: string;
|
||||
readonly path: string;
|
||||
readonly argoNamespace: string;
|
||||
readonly argoApplication: string;
|
||||
readonly repoURL: string;
|
||||
};
|
||||
readonly gitMirror: {
|
||||
readonly namespace: string;
|
||||
readonly readService: string;
|
||||
readonly writeService: string;
|
||||
readonly readUrl: string;
|
||||
readonly writeUrl: string;
|
||||
readonly cachePvc: string;
|
||||
readonly toolsImage: string;
|
||||
readonly syncJobPrefix: string;
|
||||
readonly flushJobPrefix: string;
|
||||
readonly repositories: readonly AgentRunGitMirrorRepositorySpec[];
|
||||
};
|
||||
readonly database: {
|
||||
readonly mode: "local-postgres" | "external-postgres";
|
||||
readonly provider: string | null;
|
||||
readonly configRef: string | null;
|
||||
readonly database: string | null;
|
||||
readonly user: string | null;
|
||||
readonly sslmode: "require" | null;
|
||||
readonly secretSourceRef: string | null;
|
||||
readonly secretRef: { readonly name: string; readonly key: string };
|
||||
readonly localPostgresExpectedAbsent: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AgentRunLaneTarget {
|
||||
readonly configPath: string;
|
||||
readonly spec: AgentRunLaneSpec;
|
||||
}
|
||||
|
||||
interface AgentRunNodeSpec {
|
||||
readonly id: string;
|
||||
readonly route: string;
|
||||
readonly kubeRoute: string;
|
||||
}
|
||||
|
||||
interface AgentRunControlPlaneConfig {
|
||||
readonly sourcePath: string;
|
||||
readonly defaultTarget: {
|
||||
readonly node: string;
|
||||
readonly lane: string;
|
||||
};
|
||||
readonly nodes: Record<string, AgentRunNodeSpec>;
|
||||
readonly lanes: Record<string, AgentRunLaneSpec>;
|
||||
}
|
||||
|
||||
export function resolveAgentRunLaneTarget(options: { node?: string | null; lane?: string | null }, env: NodeJS.ProcessEnv = process.env): AgentRunLaneTarget {
|
||||
const config = readAgentRunControlPlaneConfig(env);
|
||||
const requestedNode = options.node ?? null;
|
||||
const requestedLane = options.lane ?? null;
|
||||
if (requestedLane !== null) {
|
||||
const spec = config.lanes[requestedLane];
|
||||
if (spec === undefined) throw new Error(`${config.sourcePath}: controlPlane.lanes.${requestedLane} is not declared`);
|
||||
if (requestedNode !== null && requestedNode !== spec.nodeId) throw new Error(`--node ${requestedNode} does not match controlPlane.lanes.${requestedLane}.node=${spec.nodeId}`);
|
||||
return { configPath: config.sourcePath, spec };
|
||||
}
|
||||
if (requestedNode !== null) {
|
||||
const candidates = Object.values(config.lanes).filter((lane) => lane.nodeId === requestedNode);
|
||||
if (candidates.length === 0) throw new Error(`${config.sourcePath}: no AgentRun lane is declared for node ${requestedNode}`);
|
||||
if (candidates.length > 1) throw new Error(`--lane is required because node ${requestedNode} has multiple AgentRun lanes: ${candidates.map((lane) => lane.lane).join(", ")}`);
|
||||
return { configPath: config.sourcePath, spec: candidates[0] };
|
||||
}
|
||||
const spec = config.lanes[config.defaultTarget.lane];
|
||||
if (spec === undefined) throw new Error(`${config.sourcePath}: default controlPlane lane ${config.defaultTarget.lane} is not declared`);
|
||||
if (spec.nodeId !== config.defaultTarget.node) throw new Error(`${config.sourcePath}: controlPlane.default.node does not match default lane node`);
|
||||
return { configPath: config.sourcePath, spec };
|
||||
}
|
||||
|
||||
export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
return {
|
||||
node: {
|
||||
id: spec.nodeId,
|
||||
route: spec.nodeRoute,
|
||||
kubeRoute: spec.nodeKubeRoute,
|
||||
},
|
||||
lane: spec.lane,
|
||||
version: spec.version,
|
||||
source: {
|
||||
repository: spec.source.repository,
|
||||
branch: spec.source.branch,
|
||||
workspace: spec.source.workspace,
|
||||
},
|
||||
runtime: {
|
||||
namespace: spec.runtime.namespace,
|
||||
managerDeployment: spec.runtime.managerDeployment,
|
||||
managerService: spec.runtime.managerService,
|
||||
managerPort: spec.runtime.managerPort,
|
||||
internalBaseUrl: spec.runtime.internalBaseUrl,
|
||||
},
|
||||
ci: {
|
||||
namespace: spec.ci.namespace,
|
||||
pipeline: spec.ci.pipeline,
|
||||
pipelineRunPrefix: spec.ci.pipelineRunPrefix,
|
||||
serviceAccountName: spec.ci.serviceAccountName,
|
||||
registryPrefix: spec.ci.registryPrefix,
|
||||
},
|
||||
gitops: {
|
||||
branch: spec.gitops.branch,
|
||||
path: spec.gitops.path,
|
||||
argoNamespace: spec.gitops.argoNamespace,
|
||||
argoApplication: spec.gitops.argoApplication,
|
||||
repoURL: spec.gitops.repoURL,
|
||||
},
|
||||
gitMirror: {
|
||||
namespace: spec.gitMirror.namespace,
|
||||
readService: spec.gitMirror.readService,
|
||||
writeService: spec.gitMirror.writeService,
|
||||
readUrl: spec.gitMirror.readUrl,
|
||||
writeUrl: spec.gitMirror.writeUrl,
|
||||
cachePvc: spec.gitMirror.cachePvc,
|
||||
repositories: spec.gitMirror.repositories.map((repo) => ({
|
||||
key: repo.key,
|
||||
repository: repo.repository,
|
||||
sourceBranch: repo.sourceBranch,
|
||||
gitopsBranch: repo.gitopsBranch ?? null,
|
||||
})),
|
||||
},
|
||||
database: {
|
||||
mode: spec.database.mode,
|
||||
provider: spec.database.provider,
|
||||
configRef: spec.database.configRef,
|
||||
database: spec.database.database,
|
||||
user: spec.database.user,
|
||||
sslmode: spec.database.sslmode,
|
||||
secretSourceRef: spec.database.secretSourceRef,
|
||||
secretRef: spec.database.secretRef,
|
||||
localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: string): string {
|
||||
return `${spec.ci.pipelineRunPrefix}-${sourceCommit.slice(0, 12)}`;
|
||||
}
|
||||
|
||||
function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
|
||||
const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
|
||||
const root = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, configPath);
|
||||
const controlPlane = recordField(root, "controlPlane", configPath);
|
||||
const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`);
|
||||
const nodes = parseNodes(recordField(controlPlane, "nodes", `${configPath}.controlPlane`), configPath);
|
||||
const lanes = parseLanes(recordField(controlPlane, "lanes", `${configPath}.controlPlane`), nodes, configPath);
|
||||
return {
|
||||
sourcePath: configPath,
|
||||
defaultTarget: {
|
||||
node: stringField(defaultTarget, "node", `${configPath}.controlPlane.default`),
|
||||
lane: stringField(defaultTarget, "lane", `${configPath}.controlPlane.default`),
|
||||
},
|
||||
nodes,
|
||||
lanes,
|
||||
};
|
||||
}
|
||||
|
||||
function parseNodes(input: Record<string, unknown>, configPath: string): Record<string, AgentRunNodeSpec> {
|
||||
const result: Record<string, AgentRunNodeSpec> = {};
|
||||
for (const [id, raw] of Object.entries(input)) {
|
||||
validateSimpleId(id, `${configPath}.controlPlane.nodes`);
|
||||
const node = asRecord(raw, `${configPath}.controlPlane.nodes.${id}`);
|
||||
result[id] = {
|
||||
id,
|
||||
route: stringField(node, "route", `${configPath}.controlPlane.nodes.${id}`),
|
||||
kubeRoute: stringField(node, "kubeRoute", `${configPath}.controlPlane.nodes.${id}`),
|
||||
};
|
||||
}
|
||||
if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.nodes must declare at least one node`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseLanes(input: Record<string, unknown>, nodes: Record<string, AgentRunNodeSpec>, configPath: string): Record<string, AgentRunLaneSpec> {
|
||||
const result: Record<string, AgentRunLaneSpec> = {};
|
||||
for (const [lane, raw] of Object.entries(input)) {
|
||||
validateSimpleId(lane, `${configPath}.controlPlane.lanes`);
|
||||
const item = asRecord(raw, `${configPath}.controlPlane.lanes.${lane}`);
|
||||
const nodeId = stringField(item, "node", `${configPath}.controlPlane.lanes.${lane}`);
|
||||
const node = nodes[nodeId];
|
||||
if (node === undefined) throw new Error(`${configPath}.controlPlane.lanes.${lane}.node references undeclared node ${nodeId}`);
|
||||
result[lane] = parseLane(lane, node, item, configPath);
|
||||
}
|
||||
if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.lanes must declare at least one lane`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, unknown>, configPath: string): AgentRunLaneSpec {
|
||||
const path = `${configPath}.controlPlane.lanes.${lane}`;
|
||||
const source = recordField(input, "source", path);
|
||||
const runtime = recordField(input, "runtime", path);
|
||||
const ci = recordField(input, "ci", path);
|
||||
const gitops = recordField(input, "gitops", path);
|
||||
const gitMirror = recordField(input, "gitMirror", path);
|
||||
const database = recordField(input, "database", path);
|
||||
return {
|
||||
lane,
|
||||
nodeId: node.id,
|
||||
nodeRoute: node.route,
|
||||
nodeKubeRoute: node.kubeRoute,
|
||||
version: stringField(input, "version", path),
|
||||
source: {
|
||||
repository: stringField(source, "repository", `${path}.source`),
|
||||
branch: stringField(source, "branch", `${path}.source`),
|
||||
remote: stringField(source, "remote", `${path}.source`),
|
||||
workspace: absolutePathField(source, "workspace", `${path}.source`),
|
||||
},
|
||||
runtime: {
|
||||
namespace: stringField(runtime, "namespace", `${path}.runtime`),
|
||||
managerDeployment: stringField(runtime, "managerDeployment", `${path}.runtime`),
|
||||
managerService: stringField(runtime, "managerService", `${path}.runtime`),
|
||||
managerPort: integerField(runtime, "managerPort", `${path}.runtime`),
|
||||
internalBaseUrl: urlField(runtime, "internalBaseUrl", `${path}.runtime`),
|
||||
},
|
||||
ci: {
|
||||
namespace: stringField(ci, "namespace", `${path}.ci`),
|
||||
pipeline: stringField(ci, "pipeline", `${path}.ci`),
|
||||
pipelineRunPrefix: stringField(ci, "pipelineRunPrefix", `${path}.ci`),
|
||||
serviceAccountName: stringField(ci, "serviceAccountName", `${path}.ci`),
|
||||
registryPrefix: stringField(ci, "registryPrefix", `${path}.ci`),
|
||||
toolsImage: stringField(ci, "toolsImage", `${path}.ci`),
|
||||
},
|
||||
gitops: {
|
||||
branch: stringField(gitops, "branch", `${path}.gitops`),
|
||||
path: relativePathField(gitops, "path", `${path}.gitops`),
|
||||
argoNamespace: stringField(gitops, "argoNamespace", `${path}.gitops`),
|
||||
argoApplication: stringField(gitops, "argoApplication", `${path}.gitops`),
|
||||
repoURL: urlField(gitops, "repoURL", `${path}.gitops`),
|
||||
},
|
||||
gitMirror: {
|
||||
namespace: stringField(gitMirror, "namespace", `${path}.gitMirror`),
|
||||
readService: stringField(gitMirror, "readService", `${path}.gitMirror`),
|
||||
writeService: stringField(gitMirror, "writeService", `${path}.gitMirror`),
|
||||
readUrl: urlField(gitMirror, "readUrl", `${path}.gitMirror`),
|
||||
writeUrl: urlField(gitMirror, "writeUrl", `${path}.gitMirror`),
|
||||
cachePvc: stringField(gitMirror, "cachePvc", `${path}.gitMirror`),
|
||||
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`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseGitMirrorRepository(input: Record<string, unknown>, path: string): AgentRunGitMirrorRepositorySpec {
|
||||
const gitopsBranch = optionalStringField(input, "gitopsBranch", path);
|
||||
return {
|
||||
key: stringField(input, "key", path),
|
||||
repository: stringField(input, "repository", path),
|
||||
sourceBranch: stringField(input, "sourceBranch", path),
|
||||
...(gitopsBranch === undefined ? {} : { gitopsBranch }),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDatabase(input: Record<string, unknown>, path: string): AgentRunLaneSpec["database"] {
|
||||
const mode = enumField(input, "mode", path, ["local-postgres", "external-postgres"]);
|
||||
const sslmode = optionalStringField(input, "sslmode", path);
|
||||
if (sslmode !== undefined && sslmode !== "require") throw new Error(`${path}.sslmode must be require when set`);
|
||||
return {
|
||||
mode,
|
||||
provider: optionalStringField(input, "provider", path) ?? null,
|
||||
configRef: optionalStringField(input, "configRef", path) ?? null,
|
||||
database: optionalStringField(input, "database", path) ?? null,
|
||||
user: optionalStringField(input, "user", path) ?? null,
|
||||
sslmode: sslmode === undefined ? null : "require",
|
||||
secretSourceRef: optionalStringField(input, "secretSourceRef", path) ?? null,
|
||||
secretRef: parseSecretRef(recordField(input, "secretRef", path), `${path}.secretRef`),
|
||||
localPostgresExpectedAbsent: booleanField(input, "localPostgresExpectedAbsent", path),
|
||||
};
|
||||
}
|
||||
|
||||
function parseSecretRef(input: Record<string, unknown>, path: string): { name: string; key: string } {
|
||||
return {
|
||||
name: stringField(input, "name", path),
|
||||
key: stringField(input, "key", path),
|
||||
};
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
function recordField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
|
||||
return asRecord(obj[key], `${path}.${key}`);
|
||||
}
|
||||
|
||||
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function optionalStringField(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`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
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 integerField(obj: Record<string, unknown>, key: string, path: string): number {
|
||||
const value = obj[key];
|
||||
if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer`);
|
||||
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 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(", ")}`);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function absolutePathField(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 an absolute path without ..`);
|
||||
return value;
|
||||
}
|
||||
|
||||
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 urlField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = stringField(obj, key, path);
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
|
||||
} catch {
|
||||
throw new Error(`${path}.${key} must be an http(s) URL`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateSimpleId(value: string, path: string): void {
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${value} must use a simple id`);
|
||||
}
|
||||
+329
-4
@@ -5,6 +5,13 @@ import type { RenderedCliResult } from "./output";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
import { runRemoteSshCommandCapture } from "./remote";
|
||||
import { startJob } from "./jobs";
|
||||
import {
|
||||
AGENTRUN_CONFIG_PATH,
|
||||
agentRunLaneSummary,
|
||||
agentRunPipelineRunName,
|
||||
resolveAgentRunLaneTarget,
|
||||
type AgentRunLaneSpec,
|
||||
} from "./agentrun-lanes";
|
||||
|
||||
const g14SourceRoute = "G14:/root/agentrun-v01";
|
||||
const g14K3sRoute = "G14:k3s";
|
||||
@@ -56,6 +63,8 @@ export function agentRunHelp(): unknown {
|
||||
"bun scripts/cli.ts agentrun apply -f - --dry-run",
|
||||
"bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin",
|
||||
"bun scripts/cli.ts agentrun explain task",
|
||||
"bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02",
|
||||
"bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
|
||||
"bun scripts/cli.ts agentrun control-plane status",
|
||||
"bun scripts/cli.ts agentrun control-plane status --full",
|
||||
"bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-v01-ci-<short-sha>",
|
||||
@@ -92,6 +101,7 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
|
||||
}
|
||||
if (config === null) throw new Error("agentrun control-plane and git-mirror commands require UniDesk config");
|
||||
if (group === "control-plane") {
|
||||
if (action === "plan") return await controlPlanePlan(config, parseStatusOptions(actionArgs));
|
||||
if (action === "status") return await status(config, parseStatusOptions(actionArgs));
|
||||
if (action === "expose") return await exposeAgentRun(config, parseConfirmOptions(actionArgs));
|
||||
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(actionArgs));
|
||||
@@ -217,8 +227,10 @@ function agentRunHelpText(args: string[]): string {
|
||||
return [
|
||||
"Usage: bun scripts/cli.ts agentrun control-plane <action> [options]",
|
||||
"",
|
||||
"Actions: status, expose, trigger-current, refresh, cleanup-runs, cleanup-released-pvs",
|
||||
"Actions: plan, status, expose, trigger-current, refresh, cleanup-runs, cleanup-released-pvs",
|
||||
"Examples:",
|
||||
" bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02",
|
||||
" bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
|
||||
" bun scripts/cli.ts agentrun control-plane status",
|
||||
" bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-v01-ci-<short-sha>",
|
||||
" bun scripts/cli.ts agentrun control-plane expose --dry-run",
|
||||
@@ -1645,6 +1657,8 @@ interface DisclosureOptions {
|
||||
}
|
||||
|
||||
interface StatusOptions extends DisclosureOptions {
|
||||
node: string | null;
|
||||
lane: string | null;
|
||||
sourceCommit: string | null;
|
||||
pipelineRun: string | null;
|
||||
targetMode: "latest-source-head" | "source-commit" | "pipeline-run";
|
||||
@@ -1656,7 +1670,14 @@ interface TimedValue<T> {
|
||||
}
|
||||
|
||||
function parseDisclosureOptions(args: string[]): DisclosureOptions {
|
||||
for (const arg of args) {
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--node" || arg === "--lane") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg !== "--full" && arg !== "--raw") throw new Error(`unsupported status option: ${arg}`);
|
||||
}
|
||||
const raw = args.includes("--raw");
|
||||
@@ -1664,6 +1685,8 @@ function parseDisclosureOptions(args: string[]): DisclosureOptions {
|
||||
}
|
||||
|
||||
function parseStatusOptions(args: string[]): StatusOptions {
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
let sourceCommit: string | null = null;
|
||||
let pipelineRun: string | null = null;
|
||||
let full = false;
|
||||
@@ -1679,6 +1702,20 @@ function parseStatusOptions(args: string[]): StatusOptions {
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--node") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
|
||||
node = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--lane") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
|
||||
lane = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--source-commit") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--source-commit requires a value");
|
||||
@@ -1690,7 +1727,7 @@ function parseStatusOptions(args: string[]): StatusOptions {
|
||||
if (arg === "--pipeline-run") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--pipeline-run requires a value");
|
||||
if (!isAgentRunPipelineRunName(value)) throw new Error("--pipeline-run must be an agentrun-v01-ci-<12+ hex> PipelineRun name");
|
||||
if (!isAgentRunPipelineRunName(value)) throw new Error("--pipeline-run must be an agentrun-vNN-ci-<12+ hex> PipelineRun name");
|
||||
pipelineRun = value;
|
||||
index += 1;
|
||||
continue;
|
||||
@@ -1701,6 +1738,8 @@ function parseStatusOptions(args: string[]): StatusOptions {
|
||||
return {
|
||||
full,
|
||||
raw,
|
||||
node,
|
||||
lane,
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
targetMode: pipelineRun !== null ? "pipeline-run" : sourceCommit !== null ? "source-commit" : "latest-source-head",
|
||||
@@ -1779,7 +1818,42 @@ function positiveIntegerOption(args: string[], name: string, defaultValue: numbe
|
||||
return Math.min(value, maxValue);
|
||||
}
|
||||
|
||||
async function controlPlanePlan(_config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
|
||||
const target = resolveAgentRunLaneTarget(options);
|
||||
const spec = target.spec;
|
||||
return {
|
||||
ok: true,
|
||||
command: "agentrun control-plane plan",
|
||||
mode: "yaml-declared-node-lane",
|
||||
configPath: target.configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
plannedChecks: [
|
||||
"source-branch-exists",
|
||||
"source-worktree-exists-and-clean",
|
||||
"git-mirror-services-ready",
|
||||
"ci-namespace-pipeline-serviceaccount",
|
||||
"argo-application-alignment",
|
||||
"runtime-namespace-manager-service",
|
||||
"database-secretref-present",
|
||||
"local-postgres-absent-when-external",
|
||||
],
|
||||
deploymentBoundary: {
|
||||
mutation: false,
|
||||
note: "plan/status are read-only. Long writes must use controlled AgentRun/Platform DB commands and this YAML target.",
|
||||
},
|
||||
next: {
|
||||
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
|
||||
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function status(config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
|
||||
if (options.node !== null || options.lane !== null) {
|
||||
return await statusYamlLane(config, options, resolveAgentRunLaneTarget(options));
|
||||
}
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, g14SourceRoute, ["script", "--", [
|
||||
"cd /root/agentrun-v01",
|
||||
"git fetch origin v0.1 >/dev/null 2>&1 || true",
|
||||
@@ -1930,6 +2004,103 @@ async function status(config: UniDeskConfig, options: StatusOptions): Promise<Re
|
||||
};
|
||||
}
|
||||
|
||||
async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["script", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourcePayload = captureJsonPayload(sourceProbe.value);
|
||||
const sourceCommit = options.sourceCommit
|
||||
?? (options.pipelineRun !== null ? null : stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead));
|
||||
const pipelineRun = options.pipelineRun ?? (sourceCommit ? agentRunPipelineRunName(spec, sourceCommit) : null);
|
||||
const [runtimeProbe, mirrorProbe] = await Promise.all([
|
||||
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneRuntimeStatusScript(spec, pipelineRun)])),
|
||||
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneGitMirrorStatusScript(spec)])),
|
||||
]);
|
||||
const runtimePayload = captureJsonPayload(runtimeProbe.value);
|
||||
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
|
||||
const pipeline = record(runtimePayload.pipeline);
|
||||
const argo = record(runtimePayload.argo);
|
||||
const manager = record(runtimePayload.manager);
|
||||
const database = record(runtimePayload.database);
|
||||
const localPostgres = record(runtimePayload.localPostgres);
|
||||
const blockers = [
|
||||
...(sourcePayload.workspaceExists === true ? [] : ["source-worktree-missing"]),
|
||||
...(sourcePayload.remoteBranchExists === true ? [] : ["source-branch-missing"]),
|
||||
...(sourcePayload.workspaceClean === true || sourcePayload.workspaceExists !== true ? [] : ["source-worktree-dirty"]),
|
||||
...(mirrorPayload.readReady === true ? [] : ["git-mirror-read-not-ready"]),
|
||||
...(mirrorPayload.writeReady === true ? [] : ["git-mirror-write-not-ready"]),
|
||||
...(mirrorPayload.cachePvcExists === true ? [] : ["git-mirror-cache-pvc-missing"]),
|
||||
...(runtimePayload.ciNamespaceExists === true ? [] : ["ci-namespace-missing"]),
|
||||
...(pipeline.exists === true ? [] : ["pipeline-missing"]),
|
||||
...(runtimePayload.serviceAccountExists === true ? [] : ["ci-serviceaccount-missing"]),
|
||||
...(argo.exists === true ? [] : ["argo-application-missing"]),
|
||||
...(runtimePayload.runtimeNamespaceExists === true ? [] : ["runtime-namespace-missing"]),
|
||||
...(manager.deploymentExists === true ? [] : ["manager-deployment-missing"]),
|
||||
...(manager.serviceExists === true ? [] : ["manager-service-missing"]),
|
||||
...(spec.database.mode === "external-postgres" && database.secretPresent !== true ? ["database-secret-missing"] : []),
|
||||
...(spec.database.localPostgresExpectedAbsent && localPostgres.absent !== true ? ["local-postgres-present"] : []),
|
||||
];
|
||||
return {
|
||||
ok: sourceProbe.value.exitCode === 0 && runtimeProbe.value.exitCode === 0 && mirrorProbe.value.exitCode === 0 && blockers.length === 0,
|
||||
command: "agentrun control-plane status",
|
||||
mode: "yaml-declared-node-lane",
|
||||
configPath: target.configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
summary: {
|
||||
aligned: blockers.length === 0,
|
||||
blockers,
|
||||
sourceCommit,
|
||||
expectedPipelineRun: pipelineRun,
|
||||
source: {
|
||||
workspaceExists: sourcePayload.workspaceExists ?? false,
|
||||
workspaceClean: sourcePayload.workspaceClean ?? null,
|
||||
localHead: sourcePayload.localHead ?? null,
|
||||
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
|
||||
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
|
||||
},
|
||||
gitMirror: {
|
||||
readReady: mirrorPayload.readReady ?? false,
|
||||
writeReady: mirrorPayload.writeReady ?? false,
|
||||
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
|
||||
repositoryCount: Array.isArray(mirrorPayload.repositories) ? mirrorPayload.repositories.length : null,
|
||||
},
|
||||
ci: {
|
||||
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
|
||||
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
|
||||
pipeline,
|
||||
pipelineRun: record(runtimePayload.pipelineRun),
|
||||
},
|
||||
argo,
|
||||
runtime: {
|
||||
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
|
||||
manager,
|
||||
database,
|
||||
localPostgres,
|
||||
},
|
||||
},
|
||||
timings: {
|
||||
sourceMs: sourceProbe.elapsedMs,
|
||||
runtimeMs: runtimeProbe.elapsedMs,
|
||||
gitMirrorMs: mirrorProbe.elapsedMs,
|
||||
totalMs: sourceProbe.elapsedMs + Math.max(runtimeProbe.elapsedMs, mirrorProbe.elapsedMs),
|
||||
},
|
||||
source: sourcePayload,
|
||||
runtime: runtimePayload,
|
||||
gitMirror: mirrorPayload,
|
||||
captures: {
|
||||
source: compactCapture(sourceProbe.value, { full: options.full || options.raw || sourceProbe.value.exitCode !== 0 }),
|
||||
runtime: compactCapture(runtimeProbe.value, { full: options.full || options.raw || runtimeProbe.value.exitCode !== 0 }),
|
||||
gitMirror: compactCapture(mirrorProbe.value, { full: options.full || options.raw || mirrorProbe.value.exitCode !== 0 }),
|
||||
},
|
||||
next: {
|
||||
plan: `bun scripts/cli.ts agentrun control-plane plan --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
|
||||
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
|
||||
statusFull: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane} --full`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function exposeAgentRun(_config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
||||
const clientConfig = readAgentRunClientConfig();
|
||||
const exposure = clientConfig.publicExposure;
|
||||
@@ -2406,6 +2577,160 @@ function cleanupReleasedPvsScript(options: CleanupReleasedPvOptions): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function yamlLaneSourceStatusScript(spec: AgentRunLaneSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
`expected_workspace=${shQuote(spec.source.workspace)}`,
|
||||
`source_branch=${shQuote(spec.source.branch)}`,
|
||||
"workspace_exists=false",
|
||||
"workspace_clean=null",
|
||||
"local_head=null",
|
||||
"branch=null",
|
||||
"remote_url=null",
|
||||
"remote_branch_exists=false",
|
||||
"remote_branch_commit=null",
|
||||
"status_short=''",
|
||||
"if [ -d .git ] || git rev-parse --show-toplevel >/dev/null 2>&1; then",
|
||||
" actual_workspace=$(pwd)",
|
||||
" workspace_exists=true",
|
||||
" branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
|
||||
" remote_url=$(git remote get-url origin 2>/dev/null || true)",
|
||||
" local_head=$(git rev-parse HEAD 2>/dev/null || true)",
|
||||
" status_short=$(git status --short 2>/dev/null || true)",
|
||||
" if [ -z \"$status_short\" ]; then workspace_clean=true; else workspace_clean=false; fi",
|
||||
" git fetch origin \"$source_branch\" >/dev/null 2>&1 || true",
|
||||
" remote_branch_commit=$(git rev-parse \"refs/remotes/origin/$source_branch\" 2>/dev/null || true)",
|
||||
" if [ -n \"$remote_branch_commit\" ]; then remote_branch_exists=true; fi",
|
||||
"else",
|
||||
" actual_workspace=$(pwd)",
|
||||
"fi",
|
||||
"export expected_workspace source_branch workspace_exists workspace_clean local_head branch remote_url remote_branch_exists remote_branch_commit status_short actual_workspace",
|
||||
"node <<'NODE'",
|
||||
"function nullable(value) { return value && value !== 'null' ? value : null; }",
|
||||
"function booleanValue(value) { if (value === 'true') return true; if (value === 'false') return false; return null; }",
|
||||
"const env = process.env;",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: env.workspace_exists === 'true',",
|
||||
" expectedWorkspace: env.expected_workspace,",
|
||||
" actualWorkspace: env.actual_workspace || null,",
|
||||
" workspaceExists: env.workspace_exists === 'true',",
|
||||
" workspaceClean: booleanValue(env.workspace_clean),",
|
||||
" branch: nullable(env.branch),",
|
||||
" remoteUrl: nullable(env.remote_url),",
|
||||
" localHead: nullable(env.local_head),",
|
||||
" remoteBranch: env.source_branch,",
|
||||
" remoteBranchExists: env.remote_branch_exists === 'true',",
|
||||
" remoteBranchCommit: nullable(env.remote_branch_commit),",
|
||||
" statusShort: nullable(env.status_short),",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function yamlLaneRuntimeStatusScript(spec: AgentRunLaneSpec, pipelineRun: string | null): string {
|
||||
return [
|
||||
"set +e",
|
||||
`runtime_namespace=${shQuote(spec.runtime.namespace)}`,
|
||||
`ci_namespace=${shQuote(spec.ci.namespace)}`,
|
||||
`pipeline_name=${shQuote(spec.ci.pipeline)}`,
|
||||
`pipeline_run=${pipelineRun === null ? "''" : shQuote(pipelineRun)}`,
|
||||
`service_account=${shQuote(spec.ci.serviceAccountName)}`,
|
||||
`argo_namespace=${shQuote(spec.gitops.argoNamespace)}`,
|
||||
`argo_application=${shQuote(spec.gitops.argoApplication)}`,
|
||||
`manager_deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
||||
`manager_service=${shQuote(spec.runtime.managerService)}`,
|
||||
`database_secret=${shQuote(spec.database.secretRef.name)}`,
|
||||
`database_key=${shQuote(spec.database.secretRef.key)}`,
|
||||
"export runtime_namespace ci_namespace pipeline_name pipeline_run service_account argo_namespace argo_application manager_deployment manager_service database_secret database_key",
|
||||
"tmp_dir=$(mktemp -d)",
|
||||
"trap 'rm -rf \"$tmp_dir\"' EXIT",
|
||||
"kubectl get ns \"$runtime_namespace\" -o json > \"$tmp_dir/runtime-ns.json\" 2>/dev/null",
|
||||
"runtime_ns_exit=$?",
|
||||
"kubectl get ns \"$ci_namespace\" -o json > \"$tmp_dir/ci-ns.json\" 2>/dev/null",
|
||||
"ci_ns_exit=$?",
|
||||
"kubectl -n \"$ci_namespace\" get pipeline \"$pipeline_name\" -o json > \"$tmp_dir/pipeline.json\" 2>/dev/null",
|
||||
"pipeline_exit=$?",
|
||||
"kubectl -n \"$ci_namespace\" get serviceaccount \"$service_account\" -o json > \"$tmp_dir/serviceaccount.json\" 2>/dev/null",
|
||||
"sa_exit=$?",
|
||||
"if [ -n \"$pipeline_run\" ]; then kubectl -n \"$ci_namespace\" get pipelinerun \"$pipeline_run\" -o json > \"$tmp_dir/pipelinerun.json\" 2>/dev/null; pr_exit=$?; else pr_exit=2; fi",
|
||||
"kubectl -n \"$argo_namespace\" get application \"$argo_application\" -o json > \"$tmp_dir/argo.json\" 2>/dev/null",
|
||||
"argo_exit=$?",
|
||||
"kubectl -n \"$runtime_namespace\" get deploy \"$manager_deployment\" -o json > \"$tmp_dir/manager-deploy.json\" 2>/dev/null",
|
||||
"manager_deploy_exit=$?",
|
||||
"kubectl -n \"$runtime_namespace\" get svc \"$manager_service\" -o json > \"$tmp_dir/manager-svc.json\" 2>/dev/null",
|
||||
"manager_svc_exit=$?",
|
||||
"kubectl -n \"$runtime_namespace\" get secret \"$database_secret\" -o json > \"$tmp_dir/db-secret.json\" 2>/dev/null",
|
||||
"db_secret_exit=$?",
|
||||
"kubectl -n \"$runtime_namespace\" get deploy,sts,svc,secret -o name > \"$tmp_dir/runtime-names.txt\" 2>/dev/null",
|
||||
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const path = require('node:path');",
|
||||
"const dir = process.env.NODE_TMP;",
|
||||
"function readJson(name) { try { return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { return null; } }",
|
||||
"function exists(exitName) { return process.env[exitName] === '0'; }",
|
||||
"function condition(obj) { return obj?.status?.conditions?.[0] || null; }",
|
||||
"function envValue(deploy, name) { return deploy?.spec?.template?.spec?.containers?.[0]?.env?.find((entry) => entry.name === name)?.value || null; }",
|
||||
"const pipelineRun = readJson('pipelinerun.json');",
|
||||
"const argo = readJson('argo.json');",
|
||||
"const managerDeploy = readJson('manager-deploy.json');",
|
||||
"const managerSvc = readJson('manager-svc.json');",
|
||||
"const dbSecret = readJson('db-secret.json');",
|
||||
"let names = ''; try { names = fs.readFileSync(path.join(dir, 'runtime-names.txt'), 'utf8'); } catch {}",
|
||||
"const c = condition(pipelineRun);",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: exists('RUNTIME_NS_EXIT') && exists('CI_NS_EXIT'),",
|
||||
" runtimeNamespaceExists: exists('RUNTIME_NS_EXIT'),",
|
||||
" ciNamespaceExists: exists('CI_NS_EXIT'),",
|
||||
" serviceAccountExists: exists('SERVICE_ACCOUNT_EXIT'),",
|
||||
" pipeline: { exists: exists('PIPELINE_EXIT'), name: process.env.pipeline_name },",
|
||||
" pipelineRun: { exists: exists('PIPELINERUN_EXIT'), name: process.env.pipeline_run || null, status: c?.status || null, reason: c?.reason || null, startTime: pipelineRun?.status?.startTime || null, completionTime: pipelineRun?.status?.completionTime || null },",
|
||||
" argo: { exists: exists('ARGO_EXIT'), namespace: process.env.argo_namespace, application: process.env.argo_application, revision: argo?.status?.sync?.revision || null, syncStatus: argo?.status?.sync?.status || null, healthStatus: argo?.status?.health?.status || null },",
|
||||
" manager: { deploymentExists: exists('MANAGER_DEPLOY_EXIT'), serviceExists: exists('MANAGER_SVC_EXIT'), deployment: process.env.manager_deployment, service: process.env.manager_service, image: managerDeploy?.spec?.template?.spec?.containers?.[0]?.image || null, sourceCommit: envValue(managerDeploy, 'AGENTRUN_SOURCE_COMMIT'), servicePorts: Array.isArray(managerSvc?.spec?.ports) ? managerSvc.spec.ports.map((port) => ({ name: port.name || null, port: port.port || null, targetPort: port.targetPort || null })) : [] },",
|
||||
" database: { secretPresent: exists('DB_SECRET_EXIT'), secretName: process.env.database_secret, key: process.env.database_key, keyPresent: Boolean(dbSecret?.data && Object.prototype.hasOwnProperty.call(dbSecret.data, process.env.database_key || '')) , valuesPrinted: false },",
|
||||
" localPostgres: { absent: !/postgres/i.test(names), matchingObjects: names.split(/\\r?\\n/).filter((line) => /postgres/i.test(line)).slice(0, 20) },",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
`namespace=${shQuote(spec.gitMirror.namespace)}`,
|
||||
`read_service=${shQuote(spec.gitMirror.readService)}`,
|
||||
`write_service=${shQuote(spec.gitMirror.writeService)}`,
|
||||
`cache_pvc=${shQuote(spec.gitMirror.cachePvc)}`,
|
||||
`repositories_json=${shQuote(JSON.stringify(spec.gitMirror.repositories))}`,
|
||||
"kubectl -n \"$namespace\" get svc \"$read_service\" -o json >/tmp/agentrun-gitmirror-read.json 2>/dev/null",
|
||||
"read_exit=$?",
|
||||
"kubectl -n \"$namespace\" get svc \"$write_service\" -o json >/tmp/agentrun-gitmirror-write.json 2>/dev/null",
|
||||
"write_exit=$?",
|
||||
"kubectl -n \"$namespace\" get pvc \"$cache_pvc\" -o json >/tmp/agentrun-gitmirror-cache.json 2>/dev/null",
|
||||
"cache_exit=$?",
|
||||
"kubectl -n \"$namespace\" get deploy,svc,pvc -o name > /tmp/agentrun-gitmirror-names.txt 2>/dev/null",
|
||||
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" REPOSITORIES_JSON=\"$repositories_json\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');",
|
||||
"let names = ''; try { names = fs.readFileSync('/tmp/agentrun-gitmirror-names.txt', 'utf8'); } catch {}",
|
||||
"const readReady = process.env.READ_EXIT === '0';",
|
||||
"const writeReady = process.env.WRITE_EXIT === '0';",
|
||||
"const cachePvcExists = process.env.CACHE_EXIT === '0';",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: readReady && writeReady && cachePvcExists,",
|
||||
" namespace: process.env.NAMESPACE,",
|
||||
" readReady,",
|
||||
" writeReady,",
|
||||
" cachePvcExists,",
|
||||
" resources: names.split(/\\r?\\n/).filter(Boolean).slice(0, 40),",
|
||||
" repositories,",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function cleanupRunsPlanNodeScript(): string {
|
||||
return String.raw`
|
||||
const fs = require("node:fs");
|
||||
@@ -4429,7 +4754,7 @@ function pipelineRunName(sourceCommit: string): string {
|
||||
}
|
||||
|
||||
function isAgentRunPipelineRunName(value: string): boolean {
|
||||
return /^agentrun-v01-ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
|
||||
return /^agentrun-v[0-9]+-ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
|
||||
}
|
||||
|
||||
function statusTargetArg(options: StatusOptions, target: Record<string, unknown>): string {
|
||||
|
||||
Reference in New Issue
Block a user