Merge pull request #1170 from pikasTech/fix/1148-codeagent-agentrun-yaml
fix(hwlab): expose D518 Code Agent runtime in YAML
This commit is contained in:
@@ -689,6 +689,17 @@ lanes:
|
||||
sourceRef: hwlab/d518-v03-code-agent-provider.env
|
||||
openaiSourceKey: OPENAI_API_KEY
|
||||
opencodeSourceKey: OPENCODE_API_KEY
|
||||
codeAgentRuntime:
|
||||
enabled: true
|
||||
adapter: agentrun-v01
|
||||
managerUrl: http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080
|
||||
apiKeySecretName: hwlab-v03-master-server-admin-api-key
|
||||
apiKeySecretKey: api-key
|
||||
runnerNamespace: agentrun-v02
|
||||
secretNamespace: agentrun-v02
|
||||
repoUrlFrom: runtimeGitReadUrl
|
||||
providerIdFrom: runtimeNodeId
|
||||
defaultProviderProfile: deepseek
|
||||
publicExposure:
|
||||
mode: pk01-caddy-frp
|
||||
publicBaseUrl: https://hwlab.pikapython.com
|
||||
|
||||
@@ -313,6 +313,19 @@ export interface HwlabRuntimeCodeAgentProviderSpec {
|
||||
readonly opencodeSourceKey?: string;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeCodeAgentRuntimeSpec {
|
||||
readonly enabled: boolean;
|
||||
readonly adapter: string;
|
||||
readonly managerUrl: string;
|
||||
readonly apiKeySecretName: string;
|
||||
readonly apiKeySecretKey: string;
|
||||
readonly runnerNamespace: string;
|
||||
readonly secretNamespace: string;
|
||||
readonly repoUrlFrom: "runtimeGitReadUrl";
|
||||
readonly providerIdFrom: "runtimeNodeId";
|
||||
readonly defaultProviderProfile: string;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeSourceWorkspaceSpec {
|
||||
readonly requiredCommands: readonly string[];
|
||||
readonly requiredFiles: readonly string[];
|
||||
@@ -361,6 +374,7 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
|
||||
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
|
||||
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
|
||||
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly runtimeStore?: HwlabRuntimeStoreSpec;
|
||||
@@ -412,6 +426,7 @@ interface HwlabLaneConfig {
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
|
||||
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
|
||||
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
|
||||
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly runtimeStore?: HwlabRuntimeStoreSpec;
|
||||
@@ -655,6 +670,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`),
|
||||
bootstrapAdmin: bootstrapAdminConfig(raw.bootstrapAdmin, `lanes.${id}.bootstrapAdmin`),
|
||||
codeAgentProvider: codeAgentProviderConfig(raw.codeAgentProvider, `lanes.${id}.codeAgentProvider`),
|
||||
codeAgentRuntime: codeAgentRuntimeConfig(raw.codeAgentRuntime, `lanes.${id}.codeAgentRuntime`),
|
||||
sourceWorkspace: sourceWorkspaceConfig(raw.sourceWorkspace, `lanes.${id}.sourceWorkspace`),
|
||||
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
|
||||
runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`),
|
||||
@@ -679,6 +695,7 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<
|
||||
buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit),
|
||||
bootstrapAdmin: mergeOptionalRecord(baseRaw.bootstrapAdmin, targetRaw.bootstrapAdmin),
|
||||
codeAgentProvider: mergeOptionalRecord(baseRaw.codeAgentProvider, targetRaw.codeAgentProvider),
|
||||
codeAgentRuntime: mergeOptionalRecord(baseRaw.codeAgentRuntime, targetRaw.codeAgentRuntime),
|
||||
sourceWorkspace: mergeOptionalRecord(baseRaw.sourceWorkspace, targetRaw.sourceWorkspace),
|
||||
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
|
||||
runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore),
|
||||
@@ -746,6 +763,30 @@ function codeAgentProviderConfig(value: unknown, path: string): HwlabRuntimeCode
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentRuntimeConfig(value: unknown, path: string): HwlabRuntimeCodeAgentRuntimeSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
const managerUrl = stringField(raw, "managerUrl", path);
|
||||
try {
|
||||
const parsed = new URL(managerUrl);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
|
||||
} catch {
|
||||
throw new Error(`${path}.managerUrl must be an http(s) URL`);
|
||||
}
|
||||
return {
|
||||
enabled: booleanField(raw, "enabled", path),
|
||||
adapter: stringField(raw, "adapter", path),
|
||||
managerUrl,
|
||||
apiKeySecretName: stringField(raw, "apiKeySecretName", path),
|
||||
apiKeySecretKey: secretKeyField(raw, "apiKeySecretKey", path),
|
||||
runnerNamespace: stringField(raw, "runnerNamespace", path),
|
||||
secretNamespace: stringField(raw, "secretNamespace", path),
|
||||
repoUrlFrom: enumStringField(raw, "repoUrlFrom", path, ["runtimeGitReadUrl"]),
|
||||
providerIdFrom: enumStringField(raw, "providerIdFrom", path, ["runtimeNodeId"]),
|
||||
defaultProviderProfile: stringField(raw, "defaultProviderProfile", path),
|
||||
};
|
||||
}
|
||||
|
||||
function commandNameField(value: string, path: string): string {
|
||||
if (!/^[A-Za-z0-9._+-]+$/u.test(value)) throw new Error(`${path} must be a simple command name`);
|
||||
return value;
|
||||
@@ -1351,6 +1392,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }),
|
||||
...(config.bootstrapAdmin === undefined ? {} : { bootstrapAdmin: config.bootstrapAdmin }),
|
||||
...(config.codeAgentProvider === undefined ? {} : { codeAgentProvider: config.codeAgentProvider }),
|
||||
...(config.codeAgentRuntime === undefined ? {} : { codeAgentRuntime: config.codeAgentRuntime }),
|
||||
...(config.sourceWorkspace === undefined ? {} : { sourceWorkspace: config.sourceWorkspace }),
|
||||
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
|
||||
...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }),
|
||||
|
||||
@@ -144,6 +144,21 @@ export function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string,
|
||||
rolloutDeployment: spec.bootstrapAdmin.rolloutDeployment,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
codeAgentRuntime: spec.codeAgentRuntime === undefined ? null : {
|
||||
enabled: spec.codeAgentRuntime.enabled,
|
||||
adapter: spec.codeAgentRuntime.adapter,
|
||||
managerUrl: spec.codeAgentRuntime.managerUrl,
|
||||
apiKeySecretName: spec.codeAgentRuntime.apiKeySecretName,
|
||||
apiKeySecretKey: spec.codeAgentRuntime.apiKeySecretKey,
|
||||
runnerNamespace: spec.codeAgentRuntime.runnerNamespace,
|
||||
secretNamespace: spec.codeAgentRuntime.secretNamespace,
|
||||
repoUrlFrom: spec.codeAgentRuntime.repoUrlFrom,
|
||||
repoUrl: spec.gitReadUrl,
|
||||
providerIdFrom: spec.codeAgentRuntime.providerIdFrom,
|
||||
providerId: spec.nodeId,
|
||||
defaultProviderProfile: spec.codeAgentRuntime.defaultProviderProfile,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
sourceWorkspace: spec.sourceWorkspace === undefined ? null : {
|
||||
requiredCommands: spec.sourceWorkspace.requiredCommands,
|
||||
requiredFiles: spec.sourceWorkspace.requiredFiles,
|
||||
|
||||
@@ -36,7 +36,7 @@ import { runTransHostScript } from "./public-exposure";
|
||||
import { compactRuntimeCommand, runNodeHostScriptAsync } from "./runtime-common";
|
||||
import { compactNodeRuntimeGitMirrorStatus, nodeRuntimeGitMirrorStatus } from "./status";
|
||||
import { keyValueLinesFromText, numericField, optionValue, record, shellQuote } from "./utils";
|
||||
import { externalPostgresBridgeStatus, externalPostgresSecretStatus, getNodeRuntimePipelineRun, isLocalPostgresObject, nodeRuntimeRenderOverlay } from "./web-probe";
|
||||
import { externalPostgresBridgeStatus, externalPostgresSecretStatus, getNodeRuntimePipelineRun, isLocalPostgresObject, nodeRuntimeCodeAgentRuntimeStatus, nodeRuntimeRenderOverlay } from "./web-probe";
|
||||
import { webObserveShort, webObserveText } from "./web-probe-observe";
|
||||
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
|
||||
|
||||
@@ -190,6 +190,7 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
|
||||
const workloadReadiness = parseNodeRuntimeWorkloadReadiness(workloadReadinessProbe?.stdout ?? "");
|
||||
const bridge = externalPostgresBridgeStatus(spec, namespaceExists);
|
||||
const secrets = externalPostgresSecretStatus(spec, namespaceExists);
|
||||
const codeAgentRuntime = nodeRuntimeCodeAgentRuntimeStatus(spec, namespaceExists);
|
||||
const publicProbes = nodeRuntimePublicProbeStatus(spec);
|
||||
const gitMirror = nodeRuntimeGitMirrorStatus(scoped);
|
||||
const gitMirrorCompact = compactNodeRuntimeGitMirrorStatus(gitMirror);
|
||||
@@ -198,7 +199,21 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
|
||||
const workloadsReady = workloadReadiness.length > 0 && workloadReadiness.every((item) => item.ready);
|
||||
const localPostgresExpectedAbsent = nodeRuntimeLocalPostgresExpectedAbsent(spec);
|
||||
const localPostgresReady = localPostgresExpectedAbsent ? localPostgresObjects.length === 0 : localPostgresObjects.length > 0;
|
||||
const runtimeReady = namespaceExists && localPostgresReady && workloadsReady && (activeExternalPostgres === undefined || (bridge.ready && secrets.ready));
|
||||
const runtimeReady = namespaceExists && localPostgresReady && workloadsReady && (activeExternalPostgres === undefined || (bridge.ready && secrets.ready)) && codeAgentRuntime.ready === true;
|
||||
const codeAgentRuntimeDegradedReason = typeof codeAgentRuntime.degradedReason === "string" ? codeAgentRuntime.degradedReason : null;
|
||||
const runtimeDegradedReason = !namespaceExists
|
||||
? "runtime-namespace-missing"
|
||||
: !localPostgresReady
|
||||
? "runtime-local-postgres-not-ready"
|
||||
: !workloadsReady
|
||||
? "runtime-workloads-not-ready"
|
||||
: activeExternalPostgres !== undefined && bridge.ready !== true
|
||||
? "external-postgres-bridge-not-ready"
|
||||
: activeExternalPostgres !== undefined && secrets.ready !== true
|
||||
? "external-postgres-secrets-not-ready"
|
||||
: codeAgentRuntime.ready !== true
|
||||
? codeAgentRuntimeDegradedReason ?? "code-agent-runtime-not-ready"
|
||||
: "runtime-not-ready";
|
||||
const argoReady = argo.exitCode === 0 && repoURL === spec.argoRepoUrl && targetRevision === spec.gitopsBranch && path === spec.runtimePath && syncStatus === "Synced" && health === "Healthy";
|
||||
const pipelineRunReady = pipelineRunProbe !== null && pipelineRunProbe.status === "True";
|
||||
const pipelineRunDegradedReason = typeof pipelineRunDiagnostics?.degradedReason === "string"
|
||||
@@ -254,6 +269,7 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
|
||||
workloadResult: workloads === null ? null : compactRuntimeCommand(workloads),
|
||||
externalPostgresBridge: bridge,
|
||||
externalPostgresSecrets: secrets,
|
||||
codeAgentRuntime,
|
||||
},
|
||||
publicProbes,
|
||||
gitMirror: {
|
||||
@@ -274,7 +290,7 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
|
||||
: "public-probe-not-ready"
|
||||
: pipelineRunDegradedReason
|
||||
: "argo-not-synced-healthy"
|
||||
: namespaceExists ? "runtime-not-ready" : "runtime-namespace-missing"
|
||||
: runtimeDegradedReason
|
||||
: "control-plane-not-ready",
|
||||
next: {
|
||||
plan: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
@@ -516,6 +532,8 @@ export function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, un
|
||||
externalPostgresReady: runtime.externalPostgresBridge === undefined && runtime.externalPostgresSecrets === undefined
|
||||
? null
|
||||
: record(runtime.externalPostgresBridge).ready === true && record(runtime.externalPostgresSecrets).ready === true,
|
||||
codeAgentRuntimeReady: record(runtime.codeAgentRuntime).required === true ? record(runtime.codeAgentRuntime).ready === true : null,
|
||||
codeAgentRuntimeReason: typeof record(runtime.codeAgentRuntime).degradedReason === "string" ? record(runtime.codeAgentRuntime).degradedReason : null,
|
||||
},
|
||||
publicProbe: {
|
||||
ready: publicProbes.ready === true,
|
||||
@@ -1082,6 +1100,7 @@ export function renderNodeRuntimeControlPlaneOnNode(spec: HwlabRuntimeLaneSpec,
|
||||
" envRecipe: { ...(lane.envRecipe || {}), downloadStack },",
|
||||
"};",
|
||||
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
|
||||
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
"if (overlay.gitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.gitMirror;",
|
||||
"fs.writeFileSync(path, YAML.stringify(doc));",
|
||||
"NODE",
|
||||
@@ -1166,6 +1185,7 @@ export function renderNodeRuntimeControlPlaneLocal(spec: HwlabRuntimeLaneSpec, s
|
||||
" envRecipe: { ...(lane.envRecipe || {}), downloadStack },",
|
||||
"};",
|
||||
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
|
||||
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
"if (overlay.gitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.gitMirror;",
|
||||
"fs.writeFileSync(path, YAML.stringify(doc));",
|
||||
"NODE",
|
||||
@@ -1261,6 +1281,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" publicApiUrl: overlay.publicApiUrl,",
|
||||
" externalPostgres: overlay.externalPostgres,",
|
||||
" runtimeStore: overlay.runtimeStore,",
|
||||
" codeAgentRuntime: overlay.codeAgentRuntime,",
|
||||
" observability: overlay.observability,",
|
||||
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
|
||||
" dockerProxyHttp: overlay.dockerProxyHttp,",
|
||||
@@ -1310,6 +1331,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" envRecipe: { ...envRecipe, downloadStack },",
|
||||
"};",
|
||||
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
|
||||
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
"fs.writeFileSync(file, YAML.stringify(doc));",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));",
|
||||
"NODE_UNIDESK_DEPLOY_YAML_OVERLAY`;",
|
||||
@@ -1348,6 +1370,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" runtimeRenderDir: overlay.runtimeRenderDir,",
|
||||
" runtimeNamespace: overlay.runtimeNamespace,",
|
||||
" externalPostgres: overlay.externalPostgres,",
|
||||
" codeAgentRuntime: overlay.codeAgentRuntime,",
|
||||
" publicExposure: overlay.publicExposure,",
|
||||
" observability: overlay.observability,",
|
||||
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
|
||||
@@ -1526,18 +1549,30 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" return changed;",
|
||||
"}",
|
||||
"function patchRuntimeEnv(item, podSpec) {",
|
||||
" if (!isObject(podSpec)) return { publicEndpointChanged: false, dbSslModeChanged: false };",
|
||||
" if (!isObject(podSpec)) return { publicEndpointChanged: false, dbSslModeChanged: false, codeAgentRuntimeChanged: false };",
|
||||
" let publicEndpointChanged = false;",
|
||||
" let dbSslModeChanged = false;",
|
||||
" let codeAgentRuntimeChanged = false;",
|
||||
" const pg = overlay.externalPostgres;",
|
||||
" const codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
" for (const group of ['containers', 'initContainers']) {",
|
||||
" for (const container of Array.isArray(podSpec[group]) ? podSpec[group] : []) {",
|
||||
" if (!isObject(container)) continue;",
|
||||
" if (envValue(container, 'HWLAB_PUBLIC_ENDPOINT') !== undefined) publicEndpointChanged = setEnvValue(container, 'HWLAB_PUBLIC_ENDPOINT', expectedPublicEndpoint(item)) || publicEndpointChanged;",
|
||||
" if (pg && pg.sslmode && envValue(container, 'HWLAB_CLOUD_DB_SSL_MODE') !== undefined) dbSslModeChanged = setEnvValue(container, 'HWLAB_CLOUD_DB_SSL_MODE', pg.sslmode) || dbSslModeChanged;",
|
||||
" if (codeAgentRuntime && codeAgentRuntime.enabled && workloadName(item) === 'hwlab-cloud-api' && container.name === 'hwlab-cloud-api') {",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_CODE_AGENT_ADAPTER', String(codeAgentRuntime.adapter)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'AGENTRUN_MGR_URL', String(codeAgentRuntime.managerUrl)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvFromSecret(container, 'AGENTRUN_API_KEY', String(codeAgentRuntime.apiKeySecretName), String(codeAgentRuntime.apiKeySecretKey)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE', String(codeAgentRuntime.runnerNamespace)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE', String(codeAgentRuntime.secretNamespace)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_CODE_AGENT_AGENTRUN_REPO_URL', String(codeAgentRuntime.repoUrl)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID', String(codeAgentRuntime.providerId)) || codeAgentRuntimeChanged;",
|
||||
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE', String(codeAgentRuntime.defaultProviderProfile)) || codeAgentRuntimeChanged;",
|
||||
" }",
|
||||
" }",
|
||||
" }",
|
||||
" return { publicEndpointChanged, dbSslModeChanged };",
|
||||
" return { publicEndpointChanged, dbSslModeChanged, codeAgentRuntimeChanged };",
|
||||
"}",
|
||||
"function patchRuntimeWorkloads() {",
|
||||
" let observabilityChanged = false;",
|
||||
@@ -1546,6 +1581,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" let gitReadUrlChanged = false;",
|
||||
" let publicEndpointChanged = false;",
|
||||
" let dbSslModeChanged = false;",
|
||||
" let codeAgentRuntimeChanged = false;",
|
||||
" for (const file of yamlFiles(runtimePath)) {",
|
||||
" if (path.basename(file) === 'kustomization.yaml') continue;",
|
||||
" const docs = readYamlDocuments(file);",
|
||||
@@ -1571,14 +1607,15 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" changed = gitUrlChanged || changed;",
|
||||
" gitReadUrlChanged = gitReadUrlChanged || gitUrlChanged;",
|
||||
" const envChanged = patchRuntimeEnv(item, podSpecFor(item));",
|
||||
" changed = envChanged.publicEndpointChanged || envChanged.dbSslModeChanged || changed;",
|
||||
" changed = envChanged.publicEndpointChanged || envChanged.dbSslModeChanged || envChanged.codeAgentRuntimeChanged || changed;",
|
||||
" publicEndpointChanged = publicEndpointChanged || envChanged.publicEndpointChanged;",
|
||||
" dbSslModeChanged = dbSslModeChanged || envChanged.dbSslModeChanged;",
|
||||
" codeAgentRuntimeChanged = codeAgentRuntimeChanged || envChanged.codeAgentRuntimeChanged;",
|
||||
" }",
|
||||
" }",
|
||||
" if (changed) writeYamlDocuments(file, docs);",
|
||||
" }",
|
||||
" return { observabilityChanged, startupProbeChanged, imageRewriteChanged, gitReadUrlChanged, publicEndpointChanged, dbSslModeChanged };",
|
||||
" return { observabilityChanged, startupProbeChanged, imageRewriteChanged, gitReadUrlChanged, publicEndpointChanged, dbSslModeChanged, codeAgentRuntimeChanged };",
|
||||
"}",
|
||||
"function patchKustomization() {",
|
||||
" const file = path.join(runtimePath, 'kustomization.yaml');",
|
||||
@@ -1750,7 +1787,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
"const externalPostgresChanged = patchExternalPostgres();",
|
||||
"const healthContractChanged = patchHealthContract();",
|
||||
"const publicExposureChanged = patchPublicExposure();",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-postprocess', ok: true, runtimePath, sourcePath, pathRelocated: sourcePath !== runtimePath, observabilityPrometheusOperator: overlay.observability ? overlay.observability.prometheusOperator : null, runtimeImageRewriteCount: (overlay.runtimeImageRewrites || []).length, kustomizationChanged, observabilityWorkloadsChanged: runtimeWorkloadsChanged.observabilityChanged, startupProbeChanged: runtimeWorkloadsChanged.startupProbeChanged, runtimeImageRewriteChanged: runtimeWorkloadsChanged.imageRewriteChanged, gitReadUrlChanged: runtimeWorkloadsChanged.gitReadUrlChanged, publicEndpointChanged: runtimeWorkloadsChanged.publicEndpointChanged, dbSslModeChanged: runtimeWorkloadsChanged.dbSslModeChanged, externalPostgresChanged, healthContractChanged, publicExposureChanged }));",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-postprocess', ok: true, runtimePath, sourcePath, pathRelocated: sourcePath !== runtimePath, observabilityPrometheusOperator: overlay.observability ? overlay.observability.prometheusOperator : null, runtimeImageRewriteCount: (overlay.runtimeImageRewrites || []).length, kustomizationChanged, observabilityWorkloadsChanged: runtimeWorkloadsChanged.observabilityChanged, startupProbeChanged: runtimeWorkloadsChanged.startupProbeChanged, runtimeImageRewriteChanged: runtimeWorkloadsChanged.imageRewriteChanged, gitReadUrlChanged: runtimeWorkloadsChanged.gitReadUrlChanged, publicEndpointChanged: runtimeWorkloadsChanged.publicEndpointChanged, dbSslModeChanged: runtimeWorkloadsChanged.dbSslModeChanged, codeAgentRuntimeChanged: runtimeWorkloadsChanged.codeAgentRuntimeChanged, externalPostgresChanged, healthContractChanged, publicExposureChanged }));",
|
||||
"NODE_UNIDESK_RUNTIME_GITOPS_POSTPROCESS`;",
|
||||
"}",
|
||||
"function runtimeGitopsVerifyScript() {",
|
||||
@@ -1758,6 +1795,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" runtimePath: overlay.runtimePath,",
|
||||
" runtimeNamespace: overlay.runtimeNamespace,",
|
||||
" externalPostgres: overlay.externalPostgres,",
|
||||
" codeAgentRuntime: overlay.codeAgentRuntime,",
|
||||
" publicExposure: overlay.publicExposure,",
|
||||
" observability: overlay.observability,",
|
||||
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
|
||||
@@ -1803,6 +1841,11 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" const item = container.env.find((env) => env && env.name === name);",
|
||||
" return item ? item.value : undefined;",
|
||||
"}",
|
||||
"function envSecretRef(container, name) {",
|
||||
" if (!isObject(container) || !Array.isArray(container.env)) return {};",
|
||||
" const item = container.env.find((env) => env && env.name === name);",
|
||||
" return item && item.valueFrom && item.valueFrom.secretKeyRef ? item.valueFrom.secretKeyRef : {};",
|
||||
"}",
|
||||
"function isEnvReuseContainer(container) { return envValue(container, 'HWLAB_RUNTIME_MODE') === 'env-reuse-git-mirror-checkout' || envValue(container, 'HWLAB_BOOT_SH') !== undefined || envValue(container, 'HWLAB_BOOT_COMMIT') !== undefined; }",
|
||||
"function workloadName(item) { return item && item.metadata && item.metadata.labels && item.metadata.labels['app.kubernetes.io/name'] ? String(item.metadata.labels['app.kubernetes.io/name']) : String(item && item.metadata && item.metadata.name || ''); }",
|
||||
"function expectedPublicEndpoint(item) { return workloadName(item) === 'hwlab-cloud-web' ? overlay.publicWebUrl : overlay.publicApiUrl; }",
|
||||
@@ -1814,7 +1857,17 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" const staleGitReadUrls = [];",
|
||||
" const wrongPublicEndpoints = [];",
|
||||
" const wrongDbSslModes = [];",
|
||||
" const wrongCodeAgentRuntimeEnvs = [];",
|
||||
" const rewriteSources = new Set((overlay.runtimeImageRewrites || []).map((item) => item && item.source).filter(Boolean));",
|
||||
" const codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
" function checkCodeAgentRuntimeValue(item, file, container, envName, expected) {",
|
||||
" const value = envValue(container, envName);",
|
||||
" if (value !== expected) wrongCodeAgentRuntimeEnvs.push({ ...workloadRef(item, file, container), envName, kind: 'value', expected, value: value ?? null });",
|
||||
" }",
|
||||
" function checkCodeAgentRuntimeSecret(item, file, container, envName, expectedSecret, expectedKey) {",
|
||||
" const secretRef = envSecretRef(container, envName);",
|
||||
" if (secretRef.name !== expectedSecret || secretRef.key !== expectedKey) wrongCodeAgentRuntimeEnvs.push({ ...workloadRef(item, file, container), envName, kind: 'secretKeyRef', expectedSecret, expectedKey, secret: secretRef.name ?? null, key: secretRef.key ?? null });",
|
||||
" }",
|
||||
" for (const file of yamlFiles(runtimePath)) {",
|
||||
" if (path.basename(file) === 'kustomization.yaml') continue;",
|
||||
" for (const doc of readYamlDocuments(file)) {",
|
||||
@@ -1831,12 +1884,22 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (publicEndpoint !== undefined && publicEndpoint !== expectedPublicEndpoint(item)) wrongPublicEndpoints.push({ ...workloadRef(item, file, container), value: publicEndpoint, expected: expectedPublicEndpoint(item) });",
|
||||
" const dbSslMode = envValue(container, 'HWLAB_CLOUD_DB_SSL_MODE');",
|
||||
" if (overlay.externalPostgres && overlay.externalPostgres.sslmode && dbSslMode !== undefined && dbSslMode !== overlay.externalPostgres.sslmode) wrongDbSslModes.push({ ...workloadRef(item, file, container), value: dbSslMode, expected: overlay.externalPostgres.sslmode });",
|
||||
" if (codeAgentRuntime && codeAgentRuntime.enabled && workloadName(item) === 'hwlab-cloud-api' && container.name === 'hwlab-cloud-api') {",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_CODE_AGENT_ADAPTER', String(codeAgentRuntime.adapter));",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'AGENTRUN_MGR_URL', String(codeAgentRuntime.managerUrl));",
|
||||
" checkCodeAgentRuntimeSecret(item, file, container, 'AGENTRUN_API_KEY', String(codeAgentRuntime.apiKeySecretName), String(codeAgentRuntime.apiKeySecretKey));",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE', String(codeAgentRuntime.runnerNamespace));",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE', String(codeAgentRuntime.secretNamespace));",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_CODE_AGENT_AGENTRUN_REPO_URL', String(codeAgentRuntime.repoUrl));",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID', String(codeAgentRuntime.providerId));",
|
||||
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE', String(codeAgentRuntime.defaultProviderProfile));",
|
||||
" }",
|
||||
" }",
|
||||
" if (Array.isArray(podSpec.volumes) && podSpec.volumes.some((volume) => volume && volume.name === 'hwlab-metrics-sidecar')) metricsRefs.push(workloadRef(item, file, { name: 'volume/hwlab-metrics-sidecar' }));",
|
||||
" }",
|
||||
" }",
|
||||
" }",
|
||||
" return { metricsRefs, missingStartupProbes, publicRuntimeImages, staleGitReadUrls, wrongPublicEndpoints, wrongDbSslModes };",
|
||||
" return { metricsRefs, missingStartupProbes, publicRuntimeImages, staleGitReadUrls, wrongPublicEndpoints, wrongDbSslModes, wrongCodeAgentRuntimeEnvs };",
|
||||
"}",
|
||||
"const checks = [];",
|
||||
"const workloadCheck = workloadChecks();",
|
||||
@@ -1858,6 +1921,8 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
"checks.push('runtime-public-endpoint');",
|
||||
"if (workloadCheck.wrongDbSslModes.length > 0) fail('runtime-db-ssl-mode-mismatch', { refs: workloadCheck.wrongDbSslModes.slice(0, 12), count: workloadCheck.wrongDbSslModes.length });",
|
||||
"if (overlay.externalPostgres && overlay.externalPostgres.sslmode) checks.push('runtime-db-ssl-mode');",
|
||||
"if (workloadCheck.wrongCodeAgentRuntimeEnvs.length > 0) fail('code-agent-runtime-env-mismatch', { refs: workloadCheck.wrongCodeAgentRuntimeEnvs.slice(0, 12), count: workloadCheck.wrongCodeAgentRuntimeEnvs.length });",
|
||||
"if (overlay.codeAgentRuntime && overlay.codeAgentRuntime.enabled) checks.push('code-agent-runtime-env');",
|
||||
"const pg = overlay.externalPostgres;",
|
||||
"if (pg && pg.serviceName) {",
|
||||
" const access = pg.runtimeAccess || { endpointAddress: pg.endpointAddress, port: pg.port };",
|
||||
|
||||
@@ -94,6 +94,21 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
|
||||
stepEnv: spec.stepEnv,
|
||||
observability: spec.observability,
|
||||
runtimeStore: spec.runtimeStore,
|
||||
codeAgentRuntime: spec.codeAgentRuntime === undefined ? undefined : {
|
||||
enabled: spec.codeAgentRuntime.enabled,
|
||||
adapter: spec.codeAgentRuntime.adapter,
|
||||
managerUrl: spec.codeAgentRuntime.managerUrl,
|
||||
apiKeySecretName: spec.codeAgentRuntime.apiKeySecretName,
|
||||
apiKeySecretKey: spec.codeAgentRuntime.apiKeySecretKey,
|
||||
runnerNamespace: spec.codeAgentRuntime.runnerNamespace,
|
||||
secretNamespace: spec.codeAgentRuntime.secretNamespace,
|
||||
repoUrlFrom: spec.codeAgentRuntime.repoUrlFrom,
|
||||
repoUrl: spec.gitReadUrl,
|
||||
providerIdFrom: spec.codeAgentRuntime.providerIdFrom,
|
||||
providerId: spec.nodeId,
|
||||
defaultProviderProfile: spec.codeAgentRuntime.defaultProviderProfile,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
runtimeImageRewrites: spec.runtimeImageRewrites,
|
||||
runtimeImageBuilds: spec.runtimeImageBuilds,
|
||||
registryPrefix: spec.registryPrefix,
|
||||
@@ -1286,6 +1301,165 @@ export function isLocalPostgresObject(name: string, spec: HwlabRuntimeLaneSpec):
|
||||
return true;
|
||||
}
|
||||
|
||||
export function nodeRuntimeCodeAgentRuntimeStatus(spec: HwlabRuntimeLaneSpec, namespaceExists: boolean): Record<string, unknown> {
|
||||
const runtime = spec.codeAgentRuntime;
|
||||
if (runtime === undefined || runtime.enabled === false) return { required: false, ready: true, enabled: runtime?.enabled ?? false };
|
||||
if (!namespaceExists) return { required: true, ready: false, enabled: true, degradedReason: "runtime-namespace-missing" };
|
||||
const managerTarget = nodeRuntimeCodeAgentManagerTarget(runtime.managerUrl, runtime.runnerNamespace);
|
||||
const runnerNamespace = runNodeK3sArgs(spec, ["kubectl", "get", "ns", runtime.runnerNamespace, "-o", "name"], 60);
|
||||
const secretNamespace = runNodeK3sArgs(spec, ["kubectl", "get", "ns", runtime.secretNamespace, "-o", "name"], 60);
|
||||
const managerNamespace = runNodeK3sArgs(spec, ["kubectl", "get", "ns", managerTarget.namespace, "-o", "name"], 60);
|
||||
const managerService = runNodeK3sArgs(spec, ["kubectl", "-n", managerTarget.namespace, "get", "service", managerTarget.serviceName, "-o", "name"], 60);
|
||||
const managerDeployment = runNodeK3sArgs(spec, ["kubectl", "-n", managerTarget.namespace, "get", "deployment", managerTarget.serviceName, "-o", "jsonpath={.status.readyReplicas}{\"\\n\"}{.spec.replicas}{\"\\n\"}"], 60);
|
||||
const [managerReadyReplicasRaw = "", managerDesiredReplicasRaw = ""] = managerDeployment.stdout.split(/\r?\n/u);
|
||||
const managerReadyReplicas = numericField(managerReadyReplicasRaw);
|
||||
const managerDesiredReplicas = numericField(managerDesiredReplicasRaw);
|
||||
const managerDeploymentReady = managerDeployment.exitCode === 0 && (managerReadyReplicas ?? 0) >= Math.max(1, managerDesiredReplicas ?? 1);
|
||||
const apiKey = secretKeyStatus(spec, runtime.apiKeySecretName, runtime.apiKeySecretKey);
|
||||
const cloudApiEnv = nodeRuntimeCodeAgentCloudApiEnvStatus(spec, runtime);
|
||||
const ready = runnerNamespace.exitCode === 0
|
||||
&& secretNamespace.exitCode === 0
|
||||
&& managerNamespace.exitCode === 0
|
||||
&& managerService.exitCode === 0
|
||||
&& managerDeploymentReady
|
||||
&& apiKey.present === true
|
||||
&& cloudApiEnv.ready === true;
|
||||
const degradedReason = ready
|
||||
? undefined
|
||||
: runnerNamespace.exitCode !== 0
|
||||
? "agentrun-runner-namespace-missing"
|
||||
: secretNamespace.exitCode !== 0
|
||||
? "agentrun-secret-namespace-missing"
|
||||
: managerNamespace.exitCode !== 0
|
||||
? "agentrun-manager-namespace-missing"
|
||||
: managerService.exitCode !== 0
|
||||
? "agentrun-manager-service-missing"
|
||||
: !managerDeploymentReady
|
||||
? "agentrun-manager-deployment-not-ready"
|
||||
: apiKey.present !== true
|
||||
? "agentrun-api-key-secret-missing"
|
||||
: "hwlab-cloud-api-code-agent-env-mismatch";
|
||||
return {
|
||||
required: true,
|
||||
enabled: true,
|
||||
ready,
|
||||
degradedReason,
|
||||
adapter: runtime.adapter,
|
||||
managerUrl: runtime.managerUrl,
|
||||
manager: {
|
||||
namespace: managerTarget.namespace,
|
||||
serviceName: managerTarget.serviceName,
|
||||
serviceExists: managerService.exitCode === 0,
|
||||
deploymentReady: managerDeploymentReady,
|
||||
readyReplicas: managerReadyReplicas,
|
||||
desiredReplicas: managerDesiredReplicas,
|
||||
namespaceResult: compactRuntimeCommand(managerNamespace),
|
||||
serviceResult: compactRuntimeCommand(managerService),
|
||||
deploymentResult: compactRuntimeCommand(managerDeployment),
|
||||
},
|
||||
runnerNamespace: {
|
||||
name: runtime.runnerNamespace,
|
||||
exists: runnerNamespace.exitCode === 0,
|
||||
result: compactRuntimeCommand(runnerNamespace),
|
||||
},
|
||||
secretNamespace: {
|
||||
name: runtime.secretNamespace,
|
||||
exists: secretNamespace.exitCode === 0,
|
||||
result: compactRuntimeCommand(secretNamespace),
|
||||
},
|
||||
apiKey,
|
||||
cloudApiEnv,
|
||||
repoUrlFrom: runtime.repoUrlFrom,
|
||||
providerIdFrom: runtime.providerIdFrom,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function nodeRuntimeCodeAgentManagerTarget(managerUrl: string, fallbackNamespace: string): { namespace: string; serviceName: string } {
|
||||
let parsed: URL | null = null;
|
||||
try {
|
||||
parsed = new URL(managerUrl);
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
const host = parsed?.hostname ?? "";
|
||||
const parts = host.split(".").filter(Boolean);
|
||||
if (parts.length >= 3 && parts[2] === "svc") return { serviceName: parts[0], namespace: parts[1] };
|
||||
return { serviceName: parts[0] || "agentrun-mgr", namespace: parts[1] || fallbackNamespace };
|
||||
}
|
||||
|
||||
function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runtime: NonNullable<HwlabRuntimeLaneSpec["codeAgentRuntime"]>): Record<string, unknown> {
|
||||
const deployment = runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "deployment", "hwlab-cloud-api", "-o", "json"], 60);
|
||||
if (deployment.exitCode !== 0) {
|
||||
return {
|
||||
ready: false,
|
||||
deploymentExists: false,
|
||||
mismatches: [{ name: "hwlab-cloud-api", reason: "deployment-missing" }],
|
||||
result: compactCodeAgentDeploymentProbeResult(deployment),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(deployment.stdout) as unknown;
|
||||
} catch (error) {
|
||||
return {
|
||||
ready: false,
|
||||
deploymentExists: true,
|
||||
mismatches: [{ name: "hwlab-cloud-api", reason: "deployment-json-invalid", message: error instanceof Error ? error.message : String(error) }],
|
||||
result: compactCodeAgentDeploymentProbeResult(deployment),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const deploy = record(parsed);
|
||||
const specRecord = record(record(record(deploy.spec).template).spec);
|
||||
const containers = Array.isArray(specRecord.containers) ? specRecord.containers.map(record) : [];
|
||||
const container = containers.find((item) => item.name === "hwlab-cloud-api") ?? containers[0];
|
||||
const env = Array.isArray(container?.env) ? container.env.map(record) : [];
|
||||
const envByName = new Map(env.map((item) => [String(item.name || ""), item]));
|
||||
const mismatches: Array<Record<string, unknown>> = [];
|
||||
const expectValue = (name: string, expected: string): void => {
|
||||
const item = envByName.get(name);
|
||||
const value = typeof item?.value === "string" ? item.value : null;
|
||||
if (value !== expected) mismatches.push({ name, expected, value, kind: "value" });
|
||||
};
|
||||
const expectSecret = (name: string, expectedSecret: string, expectedKey: string): void => {
|
||||
const item = envByName.get(name);
|
||||
const secretRef = record(record(item?.valueFrom).secretKeyRef);
|
||||
if (secretRef.name !== expectedSecret || secretRef.key !== expectedKey) {
|
||||
mismatches.push({ name, expectedSecret, expectedKey, secret: secretRef.name ?? null, key: secretRef.key ?? null, kind: "secretKeyRef" });
|
||||
}
|
||||
};
|
||||
expectValue("HWLAB_CODE_AGENT_ADAPTER", runtime.adapter);
|
||||
expectValue("AGENTRUN_MGR_URL", runtime.managerUrl);
|
||||
expectSecret("AGENTRUN_API_KEY", runtime.apiKeySecretName, runtime.apiKeySecretKey);
|
||||
expectValue("HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE", runtime.runnerNamespace);
|
||||
expectValue("HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE", runtime.secretNamespace);
|
||||
expectValue("HWLAB_CODE_AGENT_AGENTRUN_REPO_URL", spec.gitReadUrl);
|
||||
expectValue("HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", spec.nodeId);
|
||||
expectValue("HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", runtime.defaultProviderProfile);
|
||||
return {
|
||||
ready: mismatches.length === 0,
|
||||
deploymentExists: true,
|
||||
deploymentName: "hwlab-cloud-api",
|
||||
containerName: container?.name ?? null,
|
||||
checkedEnvCount: 8,
|
||||
mismatches,
|
||||
result: compactCodeAgentDeploymentProbeResult(deployment),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactCodeAgentDeploymentProbeResult(result: CommandResult): Record<string, unknown> {
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
|
||||
stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
|
||||
stderrTail: result.stderr.slice(-600),
|
||||
timedOut: result.timedOut === true,
|
||||
};
|
||||
}
|
||||
|
||||
export function externalPostgresBridgeStatus(spec: HwlabRuntimeLaneSpec, namespaceExists: boolean): Record<string, unknown> {
|
||||
const pg = hwlabRuntimeActiveExternalPostgres(spec);
|
||||
if (pg === undefined) return { required: false, ready: true };
|
||||
|
||||
Reference in New Issue
Block a user