1865 lines
90 KiB
JavaScript
1865 lines
90 KiB
JavaScript
#!/usr/bin/env node
|
|
// SPEC: PJ2026-01060505 Workbench Performance; PJ2026-0106 平台运维
|
|
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
applyRuntimeLaneDeployConfig,
|
|
defaultEndpointForRuntimeLane,
|
|
defaultPortForRuntimeLane,
|
|
isRuntimeLane,
|
|
runtimeLaneMirrorSpecs,
|
|
runtimeLaneConfig,
|
|
versionNameForRuntimeLane
|
|
} from "../runtime-lane.ts";
|
|
import { readStructuredFile } from "../structured-config.mjs";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
const defaultOutDir = "deploy/gitops/node";
|
|
const defaultRegistryPrefix = process.env.HWLAB_NODE_REGISTRY_PREFIX || "127.0.0.1:5000/hwlab";
|
|
const defaultSourceRepo = process.env.HWLAB_NODE_GIT_URL || "git@github.com:pikasTech/HWLAB.git";
|
|
const defaultGitReadUrl = process.env.HWLAB_NODE_GIT_READ_URL || null;
|
|
const defaultV02GitReadUrl = process.env.HWLAB_V02_GIT_READ_URL || "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
|
|
const defaultV02GitWriteUrl = process.env.HWLAB_V02_GIT_WRITE_URL || "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
|
|
const defaultRuntimeLaneGitReadUrl = process.env.HWLAB_RUNTIME_LANE_GIT_READ_URL || "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git";
|
|
const defaultRuntimeLaneGitWriteUrl = process.env.HWLAB_RUNTIME_LANE_GIT_WRITE_URL || defaultV02GitWriteUrl;
|
|
const defaultBranch = process.env.HWLAB_NODE_BRANCH || "node";
|
|
const defaultGitopsBranch = process.env.HWLAB_NODE_GITOPS_BRANCH || "node-gitops";
|
|
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
|
|
const defaultRuntimeEndpoint = process.env.HWLAB_NODE_RUNTIME_ENDPOINT || "http://74.48.78.17:17667";
|
|
const defaultWebEndpoint = process.env.HWLAB_NODE_WEB_ENDPOINT || "http://74.48.78.17:17666";
|
|
const defaultProdRuntimeEndpoint = process.env.HWLAB_NODE_PROD_RUNTIME_ENDPOINT || "http://74.48.78.17:18667";
|
|
const defaultProdWebEndpoint = process.env.HWLAB_NODE_PROD_WEB_ENDPOINT || "http://74.48.78.17:18666";
|
|
const defaultV02RuntimeEndpoint = process.env.HWLAB_V02_RUNTIME_ENDPOINT || "https://hwlab.74-48-78-17.nip.io";
|
|
const defaultV02WebEndpoint = process.env.HWLAB_V02_WEB_ENDPOINT || "https://hwlab.74-48-78-17.nip.io";
|
|
const defaultProxyUrl = process.env.HWLAB_NODE_PROXY_URL || "http://127.0.0.1:10808";
|
|
const defaultAllProxyUrl = process.env.HWLAB_NODE_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
|
|
const defaultNoProxy = process.env.HWLAB_NODE_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local";
|
|
const ciToolsRunnerImage = process.env.HWLAB_NODE_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1";
|
|
const buildkitRunnerImage = process.env.HWLAB_NODE_BUILDKIT_IMAGE || "moby/buildkit:rootless";
|
|
const defaultDevBaseImage = process.env.HWLAB_NODE_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
|
|
const defaultServiceIds = Object.freeze([
|
|
"hwlab-cloud-api",
|
|
"hwlab-cloud-web",
|
|
"hwlab-gateway",
|
|
"hwlab-edge-proxy",
|
|
"hwlab-agent-skills"
|
|
]);
|
|
const v02ExtraObservableServices = Object.freeze([
|
|
{ serviceId: "hwlab-deepseek-proxy", port: 4000, healthPath: "/health/live" }
|
|
]);
|
|
const v02RemovedServiceIds = new Set([
|
|
"hwlab-agent-mgr",
|
|
"hwlab-agent-worker",
|
|
"hwlab-agent-worker-template",
|
|
"hwlab-code-agent-workspace",
|
|
"hwlab-router",
|
|
"hwlab-tunnel-client",
|
|
"hwlab-gateway-simu",
|
|
"hwlab-box-simu",
|
|
"hwlab-patch-panel"
|
|
]);
|
|
const v02RemovedServiceEnvNames = new Set([
|
|
"CODEX_HOME",
|
|
"OPENAI_API_KEY",
|
|
"HWLAB_CODE_AGENT_PROVIDER",
|
|
"HWLAB_CODE_AGENT_MODEL",
|
|
"HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
|
"HWLAB_CODE_AGENT_WORKSPACE",
|
|
"HWLAB_CODE_AGENT_CODEX_HOME",
|
|
"HWLAB_CODE_AGENT_CODEX_WORKSPACE",
|
|
"HWLAB_CODE_AGENT_CODEX_SANDBOX",
|
|
"HWLAB_CODE_AGENT_CODEX_COMMAND",
|
|
"HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED",
|
|
"HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR",
|
|
"HWLAB_M3_GATEWAY_SIMU_1_URL",
|
|
"HWLAB_M3_GATEWAY_SIMU_2_URL",
|
|
"HWLAB_M3_PATCH_PANEL_URL",
|
|
"HWLAB_GATEWAY_SIMU_URL",
|
|
"HWLAB_BOX_SIMU_URL",
|
|
"HWLAB_PATCH_PANEL_URL",
|
|
"HWLAB_ROUTER_URL",
|
|
"HWLAB_TUNNEL_CLIENT_URL"
|
|
]);
|
|
const moonBridgeSourceRef = process.env.HWLAB_NODE_MOONBRIDGE_REF || "1b99888d3dae889b79ee602cb875c7907f7e76f2";
|
|
const moonBridgeImage = process.env.HWLAB_NODE_MOONBRIDGE_IMAGE || `${defaultRegistryPrefix}/moonbridge:${moonBridgeSourceRef.slice(0, 12)}`;
|
|
const deepSeekProfileModel = process.env.HWLAB_NODE_DEEPSEEK_PROFILE_MODEL || "deepseek-chat";
|
|
const codexApiProfileModel = process.env.HWLAB_NODE_CODEX_API_PROFILE_MODEL || "gpt-5.5";
|
|
const codexApiProfileBaseUrl = process.env.HWLAB_NODE_CODEX_API_PROFILE_BASE_URL || "http://127.0.0.1:49280/responses";
|
|
const codexApiForwarderPort = process.env.HWLAB_NODE_CODEX_API_FORWARDER_PORT || "49280";
|
|
const codexApiForwarderUpstreamBaseUrl = process.env.HWLAB_NODE_CODEX_API_UPSTREAM_BASE_URL || "https://hyueapi.com";
|
|
const sourceCommitPattern = /^[a-f0-9]{7,40}$/u;
|
|
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
|
|
|
|
function k8sLabelHash(value) {
|
|
return String(value || "").slice(0, 16);
|
|
}
|
|
|
|
const primitiveValidationTasks = Object.freeze([
|
|
{
|
|
name: "repo-reports-guard",
|
|
commands: ["node scripts/repo-reports-guard.mjs"]
|
|
},
|
|
{
|
|
name: "node-contract-check",
|
|
commands: [
|
|
"node --check scripts/gitops-render.mjs",
|
|
`node --input-type=module <<'NODE'
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const root = process.cwd();
|
|
const ciFileName = "CI" + ".json";
|
|
const forbiddenCiFragments = [
|
|
"ci" + "-json",
|
|
"HWLAB_NODE_TEKTON" + "_SINGLE_IMAGE_PUBLISH",
|
|
"single" + "-dind",
|
|
"docker" + ":29",
|
|
"DOCKER" + "_HOST",
|
|
"Docker" + "-in-Docker",
|
|
"D" + "IND",
|
|
"docker" + " push",
|
|
"--build" + "-backend",
|
|
"HWLAB_ARTIFACT" + "_BUILD_BACKEND",
|
|
"--legacy" + "-source-images",
|
|
"HWLAB_NODE_USE_DEPLOY_IMAGES" + "=0"
|
|
];
|
|
const scanTargets = [
|
|
"scripts/gitops-render.mjs",
|
|
"scripts/src/runtime-lane.ts",
|
|
"scripts/artifact-publish.mjs",
|
|
"scripts/src/ci-plan-lib.mjs",
|
|
"deploy/gitops/node/tekton"
|
|
];
|
|
const skipDirs = new Set([".git", "node_modules", ".worktree"]);
|
|
|
|
function walkFiles(relativePath) {
|
|
const absolutePath = path.join(root, relativePath);
|
|
let stat;
|
|
try {
|
|
stat = statSync(absolutePath);
|
|
} catch {
|
|
return [];
|
|
}
|
|
if (stat.isFile()) return [relativePath];
|
|
if (!stat.isDirectory()) return [];
|
|
const files = [];
|
|
for (const entry of readdirSync(absolutePath, { withFileTypes: true })) {
|
|
if (entry.isDirectory() && skipDirs.has(entry.name)) continue;
|
|
files.push(...walkFiles(path.join(relativePath, entry.name)));
|
|
}
|
|
return files;
|
|
}
|
|
|
|
const ciFiles = walkFiles(".").filter((filePath) => path.basename(filePath) === ciFileName);
|
|
const violations = [];
|
|
for (const filePath of scanTargets.flatMap(walkFiles)) {
|
|
const text = readFileSync(path.join(root, filePath), "utf8");
|
|
for (const fragment of forbiddenCiFragments) {
|
|
if (text.includes(fragment)) violations.push({ filePath, fragment });
|
|
}
|
|
}
|
|
if (ciFiles.length || violations.length) {
|
|
console.error(JSON.stringify({ status: "failed", ciFiles, violations }, null, 2));
|
|
process.exit(1);
|
|
}
|
|
NODE`,
|
|
"node --check scripts/artifact-publish.mjs"
|
|
]
|
|
},
|
|
{
|
|
name: "codex-api-forwarder-check",
|
|
commands: [
|
|
"node scripts/run-bun.mjs test cmd/hwlab-codex-api-responses-forwarder/main.test.ts"
|
|
]
|
|
}
|
|
]);
|
|
|
|
function proxyEnv() {
|
|
return [
|
|
{ name: "HTTP_PROXY", value: defaultProxyUrl },
|
|
{ name: "HTTPS_PROXY", value: defaultProxyUrl },
|
|
{ name: "ALL_PROXY", value: defaultAllProxyUrl },
|
|
{ name: "NO_PROXY", value: defaultNoProxy },
|
|
{ name: "http_proxy", value: defaultProxyUrl },
|
|
{ name: "https_proxy", value: defaultProxyUrl },
|
|
{ name: "all_proxy", value: defaultAllProxyUrl },
|
|
{ name: "no_proxy", value: defaultNoProxy }
|
|
];
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
lane: process.env.HWLAB_GITOPS_LANE || "node",
|
|
nodeId: process.env.HWLAB_NODE_ID || "node",
|
|
outDir: defaultOutDir,
|
|
gitopsRoot: defaultOutDir,
|
|
catalogPath: defaultCatalogPath,
|
|
imageTagMode: process.env.HWLAB_ARTIFACT_IMAGE_TAG_MODE || "short",
|
|
registryPrefix: defaultRegistryPrefix,
|
|
sourceRevision: null,
|
|
sourceBranch: defaultBranch,
|
|
gitopsBranch: defaultGitopsBranch,
|
|
sourceRepo: defaultSourceRepo,
|
|
gitReadUrl: defaultGitReadUrl,
|
|
gitWriteUrl: null,
|
|
runtimeEndpoint: defaultRuntimeEndpoint,
|
|
webEndpoint: defaultWebEndpoint,
|
|
prodRuntimeEndpoint: defaultProdRuntimeEndpoint,
|
|
prodWebEndpoint: defaultProdWebEndpoint,
|
|
useDeployImages: true,
|
|
check: false,
|
|
write: true,
|
|
help: false
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--lane") args.lane = readOption(argv, ++index, arg);
|
|
else if (arg === "--node") args.nodeId = readOption(argv, ++index, arg);
|
|
else if (arg === "--out") args.outDir = readOption(argv, ++index, arg);
|
|
else if (arg === "--gitops-root") args.gitopsRoot = readOption(argv, ++index, arg);
|
|
else if (arg === "--catalog-path") args.catalogPath = readOption(argv, ++index, arg);
|
|
else if (arg === "--image-tag-mode") args.imageTagMode = readOption(argv, ++index, arg);
|
|
else if (arg === "--registry-prefix") args.registryPrefix = readOption(argv, ++index, arg).replace(/\/+$/u, "");
|
|
else if (arg === "--source-revision") args.sourceRevision = readOption(argv, ++index, arg);
|
|
else if (arg === "--source-branch") args.sourceBranch = readOption(argv, ++index, arg);
|
|
else if (arg === "--gitops-branch") args.gitopsBranch = readOption(argv, ++index, arg);
|
|
else if (arg === "--source-repo") args.sourceRepo = readOption(argv, ++index, arg);
|
|
else if (arg === "--git-read-url") args.gitReadUrl = readOption(argv, ++index, arg);
|
|
else if (arg === "--git-write-url") args.gitWriteUrl = readOption(argv, ++index, arg);
|
|
else if (arg === "--runtime-endpoint") args.runtimeEndpoint = readOption(argv, ++index, arg);
|
|
else if (arg === "--web-endpoint") args.webEndpoint = readOption(argv, ++index, arg);
|
|
else if (arg === "--prod-runtime-endpoint") args.prodRuntimeEndpoint = readOption(argv, ++index, arg);
|
|
else if (arg === "--prod-web-endpoint") args.prodWebEndpoint = readOption(argv, ++index, arg);
|
|
else if (arg === "--use-deploy-images") args.useDeployImages = true;
|
|
else if (arg === "--check") {
|
|
args.check = true;
|
|
args.write = false;
|
|
} else if (arg === "--no-write") args.write = false;
|
|
else if (arg === "--help" || arg === "-h") args.help = true;
|
|
else throw new Error(`unknown argument ${arg}`);
|
|
}
|
|
if (isRuntimeLane(args.lane)) {
|
|
const versionName = versionNameForRuntimeLane(args.lane);
|
|
const defaultLaneEndpoint = defaultEndpointForRuntimeLane(args.lane);
|
|
if (args.sourceBranch === defaultBranch) args.sourceBranch = versionName;
|
|
if (args.gitopsBranch === defaultGitopsBranch) args.gitopsBranch = `${versionName}-gitops`;
|
|
if (args.catalogPath === defaultCatalogPath) args.catalogPath = `deploy/artifact-catalog.${args.lane}.json`;
|
|
if (args.runtimeEndpoint === defaultRuntimeEndpoint) args.runtimeEndpoint = defaultLaneEndpoint;
|
|
if (args.webEndpoint === defaultWebEndpoint) args.webEndpoint = defaultLaneEndpoint;
|
|
if (args.imageTagMode === "short") args.imageTagMode = "full";
|
|
if (!args.gitReadUrl) args.gitReadUrl = defaultRuntimeLaneGitReadUrl;
|
|
if (!args.gitWriteUrl) args.gitWriteUrl = defaultRuntimeLaneGitWriteUrl;
|
|
}
|
|
if (!args.gitReadUrl) args.gitReadUrl = args.sourceRepo;
|
|
if (!args.gitWriteUrl) args.gitWriteUrl = args.sourceRepo;
|
|
assert.ok(args.lane === "node" || isRuntimeLane(args.lane), `unknown lane ${args.lane}`);
|
|
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
|
|
if (isRuntimeLane(args.lane)) {
|
|
const versionName = versionNameForRuntimeLane(args.lane);
|
|
assert.equal(args.sourceBranch, versionName, `${args.lane} source branch must be ${versionName}`);
|
|
assert.equal(args.gitopsBranch, `${versionName}-gitops`, `${args.lane} GitOps branch must be ${versionName}-gitops`);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function readOption(argv, index, name) {
|
|
const value = argv[index];
|
|
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
return value;
|
|
}
|
|
|
|
function usage() {
|
|
return [
|
|
"usage: node scripts/run-bun.mjs scripts/gitops-render.mjs [options]",
|
|
"",
|
|
"Render node-scoped Kubernetes-native CI/CD manifests from built-in Tekton CI primitives, deploy/deploy.yaml, and deploy/k8s/*.",
|
|
"",
|
|
"options:",
|
|
" --lane LANE node or runtime lane such as v02/v03/v04; default: node",
|
|
` --node NODE deployment node id from deploy.nodes; runtime lanes default from deploy.lanes.<lane>.node`,
|
|
` --out DIR default: ${defaultOutDir}`,
|
|
` --gitops-root DIR path inside GitOps repo; default comes from deploy.nodes.<node>.gitopsRoot`,
|
|
` --catalog-path PATH default: ${defaultCatalogPath}`,
|
|
" --image-tag-mode MODE short or full; v02 uses full source commit IDs",
|
|
` --source-repo URL default: ${defaultSourceRepo}`,
|
|
" --git-read-url URL read-only source URL; runtime lanes default to devops-infra git mirror",
|
|
" --git-write-url URL GitOps write URL; runtime lanes default to devops-infra git mirror write relay",
|
|
` --source-branch BRANCH default: ${defaultBranch}`,
|
|
` --gitops-branch BRANCH default: ${defaultGitopsBranch}`,
|
|
" --source-revision SHA source commit used for image tags and runtime annotations; default: git rev-parse HEAD",
|
|
` --registry-prefix PREFIX default: ${defaultRegistryPrefix}`,
|
|
` --runtime-endpoint URL default: ${defaultRuntimeEndpoint}; runtime lane default is derived from deploy.yaml/public URL`,
|
|
` --web-endpoint URL default: ${defaultWebEndpoint}; runtime lane default is derived from deploy.yaml/public URL`,
|
|
` --prod-runtime-endpoint URL default: ${defaultProdRuntimeEndpoint}`,
|
|
` --prod-web-endpoint URL default: ${defaultProdWebEndpoint}`,
|
|
" --use-deploy-images default and only supported mode: render workload images from deploy/artifact-catalog.dev.json per service",
|
|
" --check verify generated files are current without writing",
|
|
" --no-write plan and print generated file list without writing"
|
|
].join("\n");
|
|
}
|
|
|
|
function generatedPath(filePath) {
|
|
return path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
|
|
}
|
|
|
|
async function readJson(relativePath) {
|
|
return readStructuredFile(repoRoot, relativePath);
|
|
}
|
|
|
|
async function readJsonIfPresent(relativePath, fallback = null) {
|
|
try {
|
|
return await readJson(relativePath);
|
|
} catch (error) {
|
|
if (error && error.code === "ENOENT") return fallback;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function attachAccessControlEnv(deploy) {
|
|
const byProfile = {};
|
|
for (const [profile, laneConfig] of Object.entries(deploy?.lanes ?? {})) {
|
|
if (!isRuntimeLane(profile)) continue;
|
|
const accessControl = laneConfig?.accessControl;
|
|
if (!accessControl || typeof accessControl !== "object" || Array.isArray(accessControl)) continue;
|
|
const users = await readConfigRef(accessControl.usersRef, `${profile}.accessControl.usersRef`);
|
|
const navProfiles = await readConfigRef(accessControl.navProfilesRef, `${profile}.accessControl.navProfilesRef`);
|
|
const env = accessControlEnvFromConfig({ users, navProfiles });
|
|
if (Object.keys(env).length > 0) byProfile[profile] = { "hwlab-cloud-api": env };
|
|
}
|
|
Object.defineProperty(deploy, "__accessControlEnvByProfile", { value: byProfile, enumerable: false, configurable: true });
|
|
}
|
|
|
|
async function readConfigRef(ref, label) {
|
|
assert.equal(typeof ref, "string", `${label} must be a file reference`);
|
|
const [filePath, fragment = ""] = ref.split("#");
|
|
assert.ok(filePath, `${label} must include a file path`);
|
|
const root = await readJson(filePath);
|
|
return configFragment(root, fragment || "", label);
|
|
}
|
|
|
|
function configFragment(value, fragment, label) {
|
|
const pathText = String(fragment ?? "").replace(/^\//u, "");
|
|
if (!pathText) return value;
|
|
return pathText.split(/[./]/u).filter(Boolean).reduce((current, segment) => {
|
|
assert.ok(current && typeof current === "object" && Object.hasOwn(current, segment), `${label} fragment is missing ${segment}`);
|
|
return current[segment];
|
|
}, value);
|
|
}
|
|
|
|
function accessControlEnvFromConfig({ users, navProfiles }) {
|
|
assert.ok(Array.isArray(users), "accessControl usersRef must resolve to an array");
|
|
assert.ok(Array.isArray(navProfiles), "accessControl navProfilesRef must resolve to an array");
|
|
const env = {};
|
|
const renderedUsers = users.map((user, index) => normalizeAccessControlUser(user, index, env));
|
|
const renderedProfiles = navProfiles.map(normalizeNavProfile);
|
|
env.HWLAB_ACCESS_CONTROL_USERS_JSON = JSON.stringify(renderedUsers);
|
|
env.HWLAB_ACCESS_CONTROL_NAV_PROFILES_JSON = JSON.stringify(renderedProfiles);
|
|
return env;
|
|
}
|
|
|
|
function normalizeAccessControlUser(user, index, env) {
|
|
assert.ok(user && typeof user === "object" && !Array.isArray(user), `accessControl user ${index} must be an object`);
|
|
const passwordHashRef = user.passwordHashRef && typeof user.passwordHashRef === "object" && !Array.isArray(user.passwordHashRef) ? user.passwordHashRef : null;
|
|
const passwordHashEnv = configString(user.passwordHashEnv) || configString(passwordHashRef?.env);
|
|
const secretRef = configString(passwordHashRef?.secretRef) || secretRefFromParts(passwordHashRef);
|
|
if (passwordHashEnv && secretRef) env[passwordHashEnv] = `secretRef:${secretRef}`;
|
|
return {
|
|
id: requiredAccessConfigString(user.id, `accessControl user ${index}.id`),
|
|
username: requiredAccessConfigString(user.username, `accessControl user ${index}.username`),
|
|
displayName: configString(user.displayName) || configString(user.username),
|
|
email: configString(user.email) || null,
|
|
role: configString(user.role) === "admin" ? "admin" : "user",
|
|
status: configString(user.status) === "disabled" ? "disabled" : "active",
|
|
navProfile: requiredAccessConfigString(user.navProfile, `accessControl user ${index}.navProfile`),
|
|
passwordHashEnv: passwordHashEnv || null
|
|
};
|
|
}
|
|
|
|
function normalizeNavProfile(profile, index) {
|
|
assert.ok(profile && typeof profile === "object" && !Array.isArray(profile), `accessControl nav profile ${index} must be an object`);
|
|
const allowedIds = Array.isArray(profile.allowedIds) ? profile.allowedIds.map(configString).filter(Boolean) : [];
|
|
assert.ok(allowedIds.length > 0, `accessControl nav profile ${index}.allowedIds must not be empty`);
|
|
return {
|
|
id: requiredAccessConfigString(profile.id, `accessControl nav profile ${index}.id`),
|
|
displayName: configString(profile.displayName) || configString(profile.id),
|
|
allowedIds
|
|
};
|
|
}
|
|
|
|
function secretRefFromParts(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
const sourceRef = configString(value.sourceRef ?? value.secretName);
|
|
const targetKey = configString(value.targetKey ?? value.key);
|
|
return sourceRef && targetKey ? `${sourceRef}/${targetKey}` : "";
|
|
}
|
|
|
|
function configString(value) {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function requiredAccessConfigString(value, label) {
|
|
const text = configString(value);
|
|
assert.ok(text, `${label} is required`);
|
|
return text;
|
|
}
|
|
|
|
function optionalObject(value) {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
}
|
|
|
|
async function gitValue(args) {
|
|
const result = await execFileAsync("git", args, { cwd: repoRoot, timeout: 10000, maxBuffer: 1024 * 1024 });
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
async function resolveSourceRevision(value) {
|
|
const revision = value || await gitValue(["rev-parse", "HEAD"]);
|
|
const full = sourceCommitPattern.test(revision) && revision.length === 40
|
|
? revision
|
|
: await gitValue(["rev-parse", revision]);
|
|
assert.match(full, /^[a-f0-9]{40}$/u, "source revision must resolve to a 40-char git commit");
|
|
return { full, short: full.slice(0, 7) };
|
|
}
|
|
|
|
function jsonManifest(value) {
|
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
}
|
|
|
|
function textFile(value) {
|
|
return value.endsWith("\n") ? value : `${value}\n`;
|
|
}
|
|
|
|
function cloneJson(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function ensureObject(value, name) {
|
|
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${name} must be an object`);
|
|
return value;
|
|
}
|
|
|
|
function asItems(list, name) {
|
|
assert.equal(list?.kind, "List", `${name} must be a Kubernetes List`);
|
|
assert.ok(Array.isArray(list.items), `${name}.items must be an array`);
|
|
return list.items;
|
|
}
|
|
|
|
function serviceIdForWorkload(item, container) {
|
|
return (
|
|
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
|
|
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
|
|
container?.name
|
|
);
|
|
}
|
|
|
|
function upsertEnv(envList, name, value) {
|
|
if (!Array.isArray(envList)) return;
|
|
const existing = envList.find((entry) => entry?.name === name);
|
|
if (existing) {
|
|
delete existing.valueFrom;
|
|
existing.value = value;
|
|
} else {
|
|
envList.push({ name, value });
|
|
}
|
|
}
|
|
|
|
function upsertEnvEntry(envList, entry) {
|
|
if (!Array.isArray(envList) || !entry?.name) return;
|
|
const existing = envList.find((item) => item?.name === entry.name);
|
|
if (existing) {
|
|
delete existing.value;
|
|
delete existing.valueFrom;
|
|
Object.assign(existing, cloneJson(entry));
|
|
} else {
|
|
envList.push(cloneJson(entry));
|
|
}
|
|
}
|
|
|
|
function syncCodeAgentWorkspaceInitContainer(initContainers, { image, runtimeCommit, runtimeImageTag }) {
|
|
const initContainer = Array.isArray(initContainers)
|
|
? initContainers.find((entry) => entry?.name === "hwlab-code-agent-workspace-init")
|
|
: null;
|
|
if (!initContainer) return;
|
|
initContainer.image = image;
|
|
initContainer.imagePullPolicy = "IfNotPresent";
|
|
initContainer.env ??= [];
|
|
upsertEnv(initContainer.env, "HWLAB_WORKSPACE_SOURCE_COMMIT", runtimeCommit);
|
|
upsertEnv(initContainer.env, "HWLAB_IMAGE", image);
|
|
upsertEnv(initContainer.env, "HWLAB_IMAGE_TAG", runtimeImageTag);
|
|
}
|
|
|
|
function annotate(metadata, annotations) {
|
|
metadata.annotations = { ...(metadata.annotations ?? {}), ...annotations };
|
|
}
|
|
|
|
function label(metadata, labels) {
|
|
metadata.labels = { ...(metadata.labels ?? {}), ...labels };
|
|
}
|
|
|
|
function imageFor(registryPrefix, serviceId, imageTag) {
|
|
return `${registryPrefix}/${serviceId}:${imageTag}`;
|
|
}
|
|
|
|
function imageTagFromReference(image) {
|
|
const value = String(image ?? "");
|
|
const slashIndex = value.lastIndexOf("/");
|
|
const colonIndex = value.lastIndexOf(":");
|
|
if (colonIndex <= slashIndex) return null;
|
|
return value.slice(colonIndex + 1).split("@", 1)[0] || null;
|
|
}
|
|
|
|
function repositoryFromImage(image) {
|
|
const value = String(image ?? "").split("@", 1)[0];
|
|
const slashIndex = value.lastIndexOf("/");
|
|
const colonIndex = value.lastIndexOf(":");
|
|
return colonIndex > slashIndex ? value.slice(0, colonIndex) : value;
|
|
}
|
|
|
|
function digestPinnedImage(image, digest) {
|
|
if (!/^sha256:[a-f0-9]{64}$/u.test(String(digest ?? ""))) return image;
|
|
const repository = repositoryFromImage(image);
|
|
return repository ? `${repository}@${digest}` : image;
|
|
}
|
|
|
|
function catalogServiceById(catalog, serviceId) {
|
|
return (catalog?.services ?? []).find((service) => service?.serviceId === serviceId) ?? null;
|
|
}
|
|
|
|
function v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds = null) {
|
|
if (envReuseServiceIds?.has(serviceId)) return true;
|
|
const service = catalogServiceById(catalog, serviceId);
|
|
return service?.runtimeMode === v02EnvReuseRuntimeMode || service?.envReuse === true;
|
|
}
|
|
|
|
function bootShForService(deployService, serviceId) {
|
|
assert.ok(deployService?.bootSh, `deploy lane bootScripts.${serviceId} is required`);
|
|
const value = deployService.bootSh;
|
|
const text = String(value).replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
assert.ok(text && !text.startsWith("/") && !text.split("/").includes(".."), `${serviceId} boot script must be repo-relative`);
|
|
return text;
|
|
}
|
|
|
|
function envReuseServiceIdsForLane(deploy, lane) {
|
|
if (!isRuntimeLane(lane)) return new Set();
|
|
const configured = deploy?.lanes?.[lane]?.envReuseServices;
|
|
const allowed = new Set(serviceIdsForLane(lane, deploy));
|
|
return new Set((Array.isArray(configured) ? configured : []).filter((serviceId) => allowed.has(serviceId)));
|
|
}
|
|
|
|
function deployServiceForBoot(deploy, serviceId, lane = "v02") {
|
|
return {
|
|
serviceId,
|
|
bootSh: deploy?.lanes?.[lane]?.bootScripts?.[serviceId]
|
|
};
|
|
}
|
|
|
|
function bootMetadataForService({ args, catalog, deployService, serviceId, source }) {
|
|
const catalogService = catalogServiceById(catalog, serviceId);
|
|
const environmentImage = catalogService?.environmentImage ?? imageFor(args.registryPrefix, `${serviceId}-env`, `env-${String(catalogService?.environmentInputHash ?? source.full).slice(0, 12)}`);
|
|
const environmentDigest = catalogService?.environmentDigest ?? catalogService?.digest ?? null;
|
|
return {
|
|
runtimeMode: v02EnvReuseRuntimeMode,
|
|
environmentImage,
|
|
environmentDigest,
|
|
environmentInputHash: catalogService?.environmentInputHash ?? null,
|
|
codeInputHash: catalogService?.codeInputHash ?? null,
|
|
bootRepo: catalogService?.bootRepo ?? args.sourceRepo,
|
|
bootCommit: source.full,
|
|
bootSh: catalogService?.bootSh ?? bootShForService(deployService, serviceId)
|
|
};
|
|
}
|
|
|
|
function envReuseBootShForContainer({ serviceId, container, defaultBootSh }) {
|
|
if (serviceId === "hwlab-cloud-api" && container?.name === "hwlab-codex-api-forwarder") {
|
|
return "deploy/runtime/boot/hwlab-codex-api-responses-forwarder.sh";
|
|
}
|
|
return defaultBootSh;
|
|
}
|
|
|
|
function applyEnvReuseBootEnv(container, bootMetadata, { sourceBranch, gitReadUrl, bootSh = bootMetadata?.bootSh } = {}) {
|
|
if (!container || !bootMetadata) return;
|
|
delete container.command;
|
|
delete container.args;
|
|
container.env ??= [];
|
|
upsertEnv(container.env, "HWLAB_RUNTIME_MODE", bootMetadata.runtimeMode);
|
|
upsertEnv(container.env, "HWLAB_BOOT_REPO", bootMetadata.bootRepo);
|
|
upsertEnv(container.env, "HWLAB_BOOT_COMMIT", bootMetadata.bootCommit);
|
|
upsertEnv(container.env, "HWLAB_BOOT_REF", sourceBranch);
|
|
upsertEnv(container.env, "HWLAB_BOOT_SH", bootSh);
|
|
upsertEnv(container.env, "HWLAB_BOOT_READ_URL", gitReadUrl ?? defaultV02GitReadUrl);
|
|
upsertEnv(container.env, "HWLAB_ENVIRONMENT_IMAGE", bootMetadata.environmentImage ?? "");
|
|
upsertEnv(container.env, "HWLAB_ENVIRONMENT_DIGEST", bootMetadata.environmentDigest ?? "");
|
|
upsertEnv(container.env, "HWLAB_ENVIRONMENT_INPUT_HASH", bootMetadata.environmentInputHash ?? "");
|
|
upsertEnv(container.env, "HWLAB_CODE_INPUT_HASH", bootMetadata.codeInputHash ?? "");
|
|
upsertEnv(container.env, "HWLAB_IMAGE_DIGEST", bootMetadata.environmentDigest ?? "unknown");
|
|
applyEnvReuseStartupProbe(container);
|
|
}
|
|
|
|
function applyEnvReuseStartupProbe(container) {
|
|
const probe = container.readinessProbe ?? container.livenessProbe;
|
|
if (!probe?.httpGet && !probe?.tcpSocket && !probe?.exec) return;
|
|
container.startupProbe ??= {
|
|
...cloneJson(probe),
|
|
periodSeconds: 10,
|
|
timeoutSeconds: Math.max(Number(probe.timeoutSeconds ?? 1), 2),
|
|
failureThreshold: 30,
|
|
successThreshold: 1
|
|
};
|
|
}
|
|
|
|
function applyServiceHealthProbe(container, healthProbe) {
|
|
if (!healthProbe || typeof healthProbe !== "object" || Array.isArray(healthProbe)) return;
|
|
for (const [probeName, key] of [["readinessProbe", "readiness"], ["livenessProbe", "liveness"], ["startupProbe", "startup"]]) {
|
|
const probe = container?.[probeName];
|
|
const timing = normalizeProbeTiming(healthProbe[key]);
|
|
if (!probe || Object.keys(timing).length === 0) continue;
|
|
Object.assign(probe, timing);
|
|
}
|
|
}
|
|
|
|
function applyServiceShutdownDrainLifecycle(container, shutdownDrain) {
|
|
if (!container || !shutdownDrain || shutdownDrain.enabled !== true) return;
|
|
const pathValue = typeof shutdownDrain.path === "string" && /^\/[A-Za-z0-9/_:.-]+$/u.test(shutdownDrain.path) ? shutdownDrain.path : "/internal/workbench/drain";
|
|
const sleepSeconds = boundedInteger(shutdownDrain.sleepSeconds, 3, 0, 30);
|
|
const timeoutMs = boundedInteger(shutdownDrain.timeoutMs, 2500, 100, 30000);
|
|
container.env ??= [];
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_SSE_DRAIN_TIMEOUT_MS", String(timeoutMs));
|
|
const command = [
|
|
'port="${PORT:-${HWLAB_PORT:-${HWLAB_CLOUD_API_PORT:-6667}}}"',
|
|
`url="http://127.0.0.1:\${port}${pathValue}"`,
|
|
'/usr/local/bin/bun -e \'const [url, hostname] = process.argv.slice(2); await fetch(url, { method: "POST", headers: { "x-hwlab-drain-hostname": hostname || "" } }).catch(() => {});\' "$url" "${HOSTNAME:-}" >/dev/null 2>&1 || true',
|
|
`sleep ${sleepSeconds}`
|
|
].join("; ");
|
|
container.lifecycle ??= {};
|
|
container.lifecycle.preStop = { exec: { command: ["sh", "-c", command] } };
|
|
}
|
|
|
|
function boundedInteger(value, fallback, minimum, maximum) {
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed)) return fallback;
|
|
return Math.max(minimum, Math.min(maximum, parsed));
|
|
}
|
|
|
|
function normalizeProbeTiming(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
const result = {};
|
|
for (const field of ["initialDelaySeconds", "periodSeconds", "timeoutSeconds", "failureThreshold", "successThreshold"]) {
|
|
const raw = value[field];
|
|
const minimum = field === "initialDelaySeconds" ? 0 : 1;
|
|
if (Number.isInteger(raw) && raw >= minimum) result[field] = raw;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function useLocalHealthProbe(container, pathValue = "/health") {
|
|
if (!container) return;
|
|
for (const probeName of ["readinessProbe", "livenessProbe"]) {
|
|
const probe = container[probeName];
|
|
if (!probe?.httpGet) continue;
|
|
probe.httpGet.path = pathValue;
|
|
}
|
|
}
|
|
|
|
function annotateEnvReusePodTemplate(podMetadata, bootMetadata) {
|
|
if (!podMetadata || !bootMetadata) return;
|
|
annotate(podMetadata, {
|
|
"hwlab.pikastech.local/runtime-mode": bootMetadata.runtimeMode,
|
|
"hwlab.pikastech.local/boot-repo": bootMetadata.bootRepo,
|
|
"hwlab.pikastech.local/boot-commit": bootMetadata.bootCommit,
|
|
"hwlab.pikastech.local/boot-sh": bootMetadata.bootSh,
|
|
"hwlab.pikastech.local/environment-digest": bootMetadata.environmentDigest ?? "not_published",
|
|
"hwlab.pikastech.local/environment-input-hash": bootMetadata.environmentInputHash ?? "unknown",
|
|
"hwlab.pikastech.local/code-input-hash": bootMetadata.codeInputHash ?? "unknown"
|
|
});
|
|
label(podMetadata, {
|
|
"hwlab.pikastech.local/runtime-mode": bootMetadata.runtimeMode,
|
|
"hwlab.pikastech.local/boot-commit": bootMetadata.bootCommit
|
|
});
|
|
}
|
|
|
|
function runtimeArtifactForService({ catalog, deploy, serviceId, source, registryPrefix, useDeployImages = false, digestPin = false, envReuseServiceIds = null }) {
|
|
const catalogService = catalogServiceById(catalog, serviceId);
|
|
if (v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds)) {
|
|
const environmentImage = catalogService?.environmentImage ?? imageFor(registryPrefix, `${serviceId}-env`, `env-${String(catalogService?.environmentInputHash ?? source.full).slice(0, 12)}`);
|
|
const runtimeImage = digestPin && useDeployImages ? digestPinnedImage(environmentImage, catalogService?.environmentDigest ?? catalogService?.digest) : environmentImage;
|
|
return {
|
|
image: runtimeImage,
|
|
imageTag: imageTagFromReference(environmentImage) ?? source.imageTag ?? source.short,
|
|
commit: source.full,
|
|
runtimeMode: v02EnvReuseRuntimeMode,
|
|
environmentDigest: catalogService?.environmentDigest ?? catalogService?.digest ?? null
|
|
};
|
|
}
|
|
const fallbackImageTag = source.imageTag ?? source.short;
|
|
const image = useDeployImages && catalogService?.image
|
|
? catalogService.image
|
|
: imageFor(registryPrefix, serviceId, fallbackImageTag);
|
|
const runtimeImage = digestPin && useDeployImages ? digestPinnedImage(image, catalogService?.digest) : image;
|
|
const imageTag = useDeployImages && catalogService?.imageTag
|
|
? catalogService.imageTag
|
|
: imageTagFromReference(image) ?? fallbackImageTag;
|
|
const commit = useDeployImages
|
|
? catalogService?.sourceCommitId ?? catalogService?.commitId ?? imageTag ?? source.full
|
|
: source.full;
|
|
return { image: runtimeImage, imageTag, commit };
|
|
}
|
|
|
|
function runtimeImageForService(options) {
|
|
return runtimeArtifactForService(options).image;
|
|
}
|
|
|
|
function runtimeCommitForService(options) {
|
|
return runtimeArtifactForService(options).commit;
|
|
}
|
|
|
|
function runtimeImageTagForService(options) {
|
|
return runtimeArtifactForService(options).imageTag;
|
|
}
|
|
|
|
function artifactEnvironment(args) {
|
|
return isRuntimeLane(args.lane) ? args.lane : "dev";
|
|
}
|
|
|
|
function artifactEndpoint(args) {
|
|
return isRuntimeLane(args.lane) ? args.runtimeEndpoint : defaultRuntimeEndpoint;
|
|
}
|
|
|
|
function serviceIdsForLane(lane, deploy = null) {
|
|
return isRuntimeLane(lane) ? runtimeLaneServiceIdsFromDeploy(deploy, lane) : defaultServiceIds;
|
|
}
|
|
|
|
function serviceIdsForProfile(profile, deploy = null) {
|
|
return isRuntimeLane(profile) ? runtimeLaneServiceIdsFromDeploy(deploy, profile) : defaultServiceIds;
|
|
}
|
|
|
|
function runtimeLaneServiceIdsFromDeploy(deploy, lane) {
|
|
const laneConfig = runtimeLaneConfig(deploy, lane);
|
|
const serviceIds = uniquePreserveOrder((Array.isArray(laneConfig.envReuseServices) ? laneConfig.envReuseServices : [])
|
|
.map((item) => String(item ?? "").trim())
|
|
.filter(Boolean));
|
|
assert.ok(serviceIds.length > 0, `deploy.lanes.${lane}.envReuseServices must not be empty`);
|
|
return serviceIds;
|
|
}
|
|
|
|
function v02ServiceIdsFromDeploy(deploy) {
|
|
return runtimeLaneServiceIdsFromDeploy(deploy, "v02");
|
|
}
|
|
|
|
function runtimeLaneObservableServicesForDeploy(deploy, lane) {
|
|
const laneConfig = runtimeLaneConfig(deploy, lane);
|
|
const declarations = laneConfig.serviceDeclarations ?? {};
|
|
const serviceIds = new Set(runtimeLaneServiceIdsFromDeploy(deploy, lane));
|
|
const declared = Object.entries(declarations)
|
|
.filter(([serviceId, declaration]) => serviceIds.has(serviceId) && declaration?.observable !== false && Number.isInteger(declaration?.healthPort))
|
|
.map(([serviceId, declaration]) => ({
|
|
serviceId,
|
|
port: declaration.healthPort,
|
|
healthPath: declaration.healthPath || "/health/live"
|
|
}));
|
|
return [...declared, ...v02ExtraObservableServices];
|
|
}
|
|
|
|
function v02ObservableServicesForDeploy(deploy) {
|
|
return runtimeLaneObservableServicesForDeploy(deploy, "v02");
|
|
}
|
|
|
|
function v02ObservableService(deploy, serviceId) {
|
|
return v02ObservableServicesForDeploy(deploy).find((item) => item.serviceId === serviceId) ?? null;
|
|
}
|
|
|
|
function runtimeLaneObservableService(deploy, lane, serviceId) {
|
|
return runtimeLaneObservableServicesForDeploy(deploy, lane).find((item) => item.serviceId === serviceId) ?? null;
|
|
}
|
|
|
|
function runtimeLaneObservableServiceIdsForDeploy(deploy, lane) {
|
|
return new Set(runtimeLaneObservableServicesForDeploy(deploy, lane).map((item) => item.serviceId));
|
|
}
|
|
|
|
function v02ObservableServiceIdsForDeploy(deploy) {
|
|
return runtimeLaneObservableServiceIdsForDeploy(deploy, "v02");
|
|
}
|
|
|
|
function servicesParamForLane(lane, deploy = null) {
|
|
return serviceIdsForLane(lane, deploy).join(",");
|
|
}
|
|
|
|
function uniquePreserveOrder(values) {
|
|
const seen = new Set();
|
|
const result = [];
|
|
for (const value of values) {
|
|
if (seen.has(value)) continue;
|
|
seen.add(value);
|
|
result.push(value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function isV02RemovedServiceId(serviceId) {
|
|
return v02RemovedServiceIds.has(serviceId);
|
|
}
|
|
|
|
function artifactCatalogSkeleton({ args, deploy, source }) {
|
|
const environment = artifactEnvironment(args);
|
|
const namespace = isRuntimeLane(args.lane) ? namespaceNameForProfile(args.lane) : "hwlab-dev";
|
|
const tag = imageTagForSource(args, source);
|
|
return {
|
|
catalogVersion: "v1",
|
|
kind: "hwlab-artifact-catalog",
|
|
environment,
|
|
profile: environment,
|
|
namespace,
|
|
endpoint: artifactEndpoint(args),
|
|
commitId: tag,
|
|
artifactState: "contract-skeleton",
|
|
publish: {
|
|
registryPrefix: args.registryPrefix,
|
|
sourceCommitId: source.full,
|
|
imageTag: tag,
|
|
publishedAt: null
|
|
},
|
|
allowedProfiles: [environment],
|
|
forbiddenProfiles: isRuntimeLane(args.lane) ? ["dev", "prod"] : ["prod"],
|
|
services: serviceIdsForLane(args.lane, deploy).map((serviceId) => artifactCatalogSkeletonService({ args, deploy, source, serviceId, environment, namespace, tag }))
|
|
};
|
|
}
|
|
|
|
function artifactCatalogSkeletonService({ args, deploy, source, serviceId, environment, namespace, tag }) {
|
|
const base = {
|
|
serviceId,
|
|
profile: environment,
|
|
namespace,
|
|
commitId: tag,
|
|
sourceCommitId: source.full,
|
|
image: imageFor(args.registryPrefix, serviceId, tag),
|
|
imageTag: tag,
|
|
digest: null,
|
|
buildBackend: "contract-skeleton"
|
|
};
|
|
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, args.lane);
|
|
if (!envReuseServiceIds.has(serviceId)) return base;
|
|
const bootSh = bootShForService(deployServiceForBoot(deploy, serviceId, args.lane), serviceId);
|
|
const environmentImage = imageFor(args.registryPrefix, `${serviceId}-env`, `env-${String(source.full).slice(0, 12)}`);
|
|
return {
|
|
...base,
|
|
image: environmentImage,
|
|
imageTag: imageTagFromReference(environmentImage),
|
|
runtimeMode: v02EnvReuseRuntimeMode,
|
|
envReuse: true,
|
|
environmentImage,
|
|
environmentDigest: null,
|
|
environmentInputHash: null,
|
|
bootRepo: args.sourceRepo,
|
|
bootCommit: source.full,
|
|
bootSh,
|
|
codeInputHash: null,
|
|
bootEnvEvidence: {
|
|
env: {
|
|
HWLAB_BOOT_REPO: args.sourceRepo,
|
|
HWLAB_BOOT_COMMIT: source.full,
|
|
HWLAB_BOOT_SH: bootSh
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function namespaceNameForProfile(profile) {
|
|
if (isRuntimeLane(profile)) return `hwlab-${profile}`;
|
|
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
|
|
}
|
|
|
|
function runtimePathForProfile(profile) {
|
|
if (isRuntimeLane(profile)) return `runtime-${profile}`;
|
|
return profile === "prod" ? "runtime-prod" : "runtime-dev";
|
|
}
|
|
|
|
function runtimeLabelForProfile(profile) {
|
|
if (isRuntimeLane(profile)) return profile;
|
|
return profile === "prod" ? "prod" : "dev";
|
|
}
|
|
|
|
function gitopsTargetForProfile(profile) {
|
|
return isRuntimeLane(profile) ? profile : "node";
|
|
}
|
|
|
|
function gitopsRootForArgs(args) {
|
|
return String(args.gitopsRoot || defaultOutDir).replace(/\/+$/u, "");
|
|
}
|
|
|
|
function gitopsPathForProfile(args, profile) {
|
|
return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile)}`;
|
|
}
|
|
|
|
function gitopsPathFor(args, relativePath) {
|
|
return `${gitopsRootForArgs(args)}/${String(relativePath).replace(/^\/+/, "")}`;
|
|
}
|
|
|
|
function profileEndpoint(args, profile) {
|
|
if (isRuntimeLane(profile)) return { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint };
|
|
return profile === "prod"
|
|
? { runtimeEndpoint: args.prodRuntimeEndpoint, webEndpoint: args.prodWebEndpoint }
|
|
: { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint };
|
|
}
|
|
|
|
function laneRuntimeProfiles(args) {
|
|
return isRuntimeLane(args.lane) ? [args.lane] : ["dev", "prod"];
|
|
}
|
|
|
|
function laneNames(args) {
|
|
if (isRuntimeLane(args.lane)) {
|
|
const namespace = namespaceNameForProfile(args.lane);
|
|
return {
|
|
gitopsTarget: args.lane,
|
|
pipeline: `${namespace}-ci-image-publish`,
|
|
poller: `${namespace}-branch-poller`,
|
|
reconciler: `${namespace}-control-plane-reconciler`,
|
|
serviceAccount: `${namespace}-tekton-runner`,
|
|
pipelineRunPrefix: `${namespace}-ci-poll-`,
|
|
runtimeNamespace: namespace,
|
|
runtimePath: gitopsPathForProfile(args, args.lane),
|
|
argoApplications: [`argocd/hwlab-node-${args.lane}`]
|
|
};
|
|
}
|
|
return {
|
|
gitopsTarget: "node",
|
|
pipeline: "hwlab-node-ci-image-publish",
|
|
poller: "hwlab-node-branch-poller",
|
|
reconciler: "hwlab-node-control-plane-reconciler",
|
|
serviceAccount: "hwlab-tekton-runner",
|
|
pipelineRunPrefix: "hwlab-node-ci-poll-",
|
|
runtimeNamespace: "hwlab-dev",
|
|
runtimePath: gitopsPathForProfile(args, "dev"),
|
|
argoApplications: ["argocd/hwlab-node-dev", "argocd/hwlab-node-prod"]
|
|
};
|
|
}
|
|
|
|
function argoApplicationName(profile) {
|
|
return `hwlab-node-${profile}`;
|
|
}
|
|
|
|
function imageTagForSource(args, source) {
|
|
return args.imageTagMode === "full" ? source.full : source.short;
|
|
}
|
|
|
|
function deepSeekBaseUrl(namespace) {
|
|
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
|
|
}
|
|
|
|
function codeAgentNoProxy(namespace) {
|
|
return [
|
|
`${namespace}.svc.cluster.local`,
|
|
".svc",
|
|
".cluster.local",
|
|
"hyueapi.com",
|
|
".hyueapi.com",
|
|
"hyui.com",
|
|
".hyui.com",
|
|
"127.0.0.1",
|
|
"localhost",
|
|
"::1",
|
|
"10.0.0.0/8",
|
|
"10.42.0.0/16",
|
|
"10.43.0.0/16",
|
|
"api.minimaxi.com",
|
|
".minimaxi.com"
|
|
].join(",");
|
|
}
|
|
|
|
function namespaceScopedHost(value, namespace) {
|
|
if (typeof value !== "string") return value;
|
|
return value.replace(/\.hwlab-dev\.svc\.cluster\.local/gu, `.${namespace}.svc.cluster.local`);
|
|
}
|
|
|
|
function secretNameForProfile(secretName, profile) {
|
|
if (!isRuntimeLane(profile)) return secretName;
|
|
if (secretName === "hwlab-cloud-api-dev-db") return `hwlab-cloud-api-${profile}-db`;
|
|
if (secretName === "hwlab-code-agent-provider") return `hwlab-${profile}-code-agent-provider`;
|
|
if (secretName === "hwlab-code-agent-codex-auth") return `hwlab-${profile}-code-agent-codex-auth`;
|
|
if (secretName === "hwlab-opencode-server-auth") return `hwlab-${profile}-opencode-server-auth`;
|
|
if (secretName === "hwlab-bootstrap-admin") return `hwlab-${profile}-bootstrap-admin`;
|
|
return secretName;
|
|
}
|
|
|
|
function secretRefObjectForProfile(secretRef, profile) {
|
|
const ref = configString(secretRef);
|
|
const slashIndex = ref.indexOf("/");
|
|
assert.ok(slashIndex > 0 && slashIndex < ref.length - 1, `secretRef ${JSON.stringify(secretRef)} must be name/key`);
|
|
return { name: secretNameForProfile(ref.slice(0, slashIndex), profile), key: ref.slice(slashIndex + 1), optional: true };
|
|
}
|
|
|
|
function rewritePodSecretRefs(podSpec, profile) {
|
|
if (!podSpec || !isRuntimeLane(profile)) return;
|
|
for (const volume of podSpec.volumes ?? []) {
|
|
if (volume?.secret?.secretName) volume.secret.secretName = secretNameForProfile(volume.secret.secretName, profile);
|
|
}
|
|
}
|
|
|
|
function deployEnvEntry(name, value, namespace, profile = "dev") {
|
|
if (typeof value === "string" && value.startsWith("secretRef:")) {
|
|
return { name, valueFrom: { secretKeyRef: secretRefObjectForProfile(value.slice("secretRef:".length), profile) } };
|
|
}
|
|
return { name, value: namespaceScopedHost(value, namespace) };
|
|
}
|
|
|
|
function applyDeployEnv(envList, deployService, namespace, profile = "dev") {
|
|
if (!deployService?.env || typeof deployService.env !== "object" || Array.isArray(deployService.env)) return;
|
|
for (const [name, value] of Object.entries(deployService.env)) {
|
|
upsertEnvEntry(envList, deployEnvEntry(name, value, namespace, profile));
|
|
}
|
|
}
|
|
|
|
function applyDeployConfigMapMounts(podSpec, container, deployService) {
|
|
const mounts = Array.isArray(deployService?.configMapMounts) ? deployService.configMapMounts : [];
|
|
if (mounts.length === 0 || !podSpec || !container) return;
|
|
podSpec.volumes ??= [];
|
|
container.volumeMounts ??= [];
|
|
for (const mount of mounts) {
|
|
const name = String(mount?.name ?? "").trim();
|
|
const configMapName = String(mount?.configMapName ?? "").trim();
|
|
const mountPath = String(mount?.mountPath ?? "").trim();
|
|
if (!name || !configMapName || !mountPath) continue;
|
|
const items = Array.isArray(mount.items)
|
|
? mount.items
|
|
.map((item) => ({ key: String(item?.key ?? "").trim(), path: String(item?.path ?? "").trim() }))
|
|
.filter((item) => item.key && item.path)
|
|
: [];
|
|
const volume = { name, configMap: { name: configMapName } };
|
|
if (items.length > 0) volume.configMap.items = items;
|
|
const existingVolume = podSpec.volumes.find((entry) => entry?.name === name);
|
|
if (existingVolume) Object.assign(existingVolume, volume);
|
|
else podSpec.volumes.push(volume);
|
|
const volumeMount = { name, mountPath, readOnly: mount.readOnly !== false };
|
|
const existingMount = container.volumeMounts.find((entry) => entry?.name === name);
|
|
if (existingMount) Object.assign(existingMount, volumeMount);
|
|
else container.volumeMounts.push(volumeMount);
|
|
}
|
|
}
|
|
|
|
function removeV02LegacySimulatorEnv(envList) {
|
|
if (!Array.isArray(envList)) return envList;
|
|
return envList.filter((entry) => !v02RemovedServiceEnvNames.has(entry?.name));
|
|
}
|
|
|
|
function removeV02RepoOwnedCodeAgentPodConfig(deploymentSpec, podSpec) {
|
|
if (!podSpec) return;
|
|
podSpec.initContainers = (podSpec.initContainers ?? []).filter((entry) => entry?.name !== "hwlab-code-agent-workspace-init");
|
|
const removedVolumeNames = new Set([
|
|
"hwlab-code-agent-workspace",
|
|
"hwlab-code-agent-codex-home",
|
|
"hwlab-code-agent-codex-config",
|
|
"hwlab-code-agent-codex-auth"
|
|
]);
|
|
podSpec.volumes = (podSpec.volumes ?? []).filter((volume) => !removedVolumeNames.has(volume?.name));
|
|
for (const container of podSpec.containers ?? []) {
|
|
container.volumeMounts = (container.volumeMounts ?? []).filter((mount) => !removedVolumeNames.has(mount?.name));
|
|
}
|
|
if (deploymentSpec?.strategy?.type === "Recreate") delete deploymentSpec.strategy;
|
|
}
|
|
|
|
function applyDeployServiceReplicas(item, deployService) {
|
|
if (!(item?.kind === "Deployment" || item?.kind === "StatefulSet")) return;
|
|
const replicas = deployService?.replicas;
|
|
if (!Number.isInteger(replicas) || replicas < 0) return;
|
|
item.spec ??= {};
|
|
item.spec.replicas = replicas;
|
|
}
|
|
|
|
function deployServicesForProfile(deploy, profile) {
|
|
const allowed = new Set(serviceIdsForProfile(profile, deploy));
|
|
const services = new Map((deploy.services ?? [])
|
|
.filter((service) => allowed.has(service.serviceId))
|
|
.map((service) => {
|
|
const copy = cloneJson(service);
|
|
copy.env = mergeEnvMaps(
|
|
copy.env,
|
|
serviceDeclarationEnvForProfile(deploy, profile, service.serviceId),
|
|
accessControlEnvForProfile(deploy, profile, service.serviceId),
|
|
runtimeStoreEnvForProfile(deploy, profile, service.serviceId),
|
|
workbenchRuntimeClientEnvForProfile(deploy, profile, service.serviceId),
|
|
workbenchRuntimeFactsQueryEnvForProfile(deploy, profile, service.serviceId),
|
|
workbenchRuntimeSessionsSummaryEnvForProfile(deploy, profile, service.serviceId),
|
|
workbenchRuntimePostgresEnvForProfile(deploy, profile, service.serviceId),
|
|
workbenchRuntimeRedisEnvForProfile(deploy, profile, service.serviceId)
|
|
);
|
|
return [service.serviceId, copy];
|
|
}));
|
|
const laneServices = isRuntimeLane(profile) && Array.isArray(deploy.lanes?.[profile]?.services)
|
|
? deploy.lanes[profile].services
|
|
: [];
|
|
for (const override of laneServices) {
|
|
const existing = services.get(override.serviceId) ?? {
|
|
serviceId: override.serviceId,
|
|
env: mergeEnvMaps(
|
|
serviceDeclarationEnvForProfile(deploy, profile, override.serviceId),
|
|
accessControlEnvForProfile(deploy, profile, override.serviceId),
|
|
runtimeStoreEnvForProfile(deploy, profile, override.serviceId),
|
|
workbenchRuntimeClientEnvForProfile(deploy, profile, override.serviceId),
|
|
workbenchRuntimeFactsQueryEnvForProfile(deploy, profile, override.serviceId),
|
|
workbenchRuntimeSessionsSummaryEnvForProfile(deploy, profile, override.serviceId),
|
|
workbenchRuntimePostgresEnvForProfile(deploy, profile, override.serviceId),
|
|
workbenchRuntimeRedisEnvForProfile(deploy, profile, override.serviceId)
|
|
)
|
|
};
|
|
services.set(override.serviceId, {
|
|
...existing,
|
|
...cloneJson(override),
|
|
env: mergeEnvMaps(existing.env, override.env)
|
|
});
|
|
}
|
|
return services;
|
|
}
|
|
|
|
function accessControlEnvForProfile(deploy, profile, serviceId) {
|
|
return deploy?.__accessControlEnvByProfile?.[profile]?.[serviceId] ?? {};
|
|
}
|
|
|
|
function serviceDeclarationEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile)) return {};
|
|
const env = deploy?.lanes?.[profile]?.serviceDeclarations?.[serviceId]?.env;
|
|
if (!env || typeof env !== "object" || Array.isArray(env)) return {};
|
|
return cloneJson(env);
|
|
}
|
|
|
|
function runtimeWorkbenchTraceTimelinePolicy(deploy, profile) {
|
|
if (!isRuntimeLane(profile)) return null;
|
|
const traceTimeline = runtimeLaneConfig(deploy, profile)?.workbench?.traceTimeline;
|
|
if (!traceTimeline || typeof traceTimeline !== "object" || Array.isArray(traceTimeline)) return null;
|
|
const result = {};
|
|
if (typeof traceTimeline.autoExpandRunning === "boolean") result.autoExpandRunning = traceTimeline.autoExpandRunning;
|
|
if (typeof traceTimeline.autoCollapseTerminal === "boolean") result.autoCollapseTerminal = traceTimeline.autoCollapseTerminal;
|
|
return Object.keys(result).length > 0 ? result : null;
|
|
}
|
|
|
|
function runtimeWorkbenchRawHwlabEventWindowPolicy(deploy, profile) {
|
|
if (!isRuntimeLane(profile)) return null;
|
|
const policy = runtimeLaneConfig(deploy, profile)?.workbench?.rawHwlabEventWindow;
|
|
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return null;
|
|
return {
|
|
enabled: policy.enabled,
|
|
maxEntries: policy.maxEntries,
|
|
maxRetainedBytes: policy.maxRetainedBytes
|
|
};
|
|
}
|
|
|
|
function runtimeAgentObserverPolicy(deploy, profile) {
|
|
if (!isRuntimeLane(profile)) return null;
|
|
const policy = runtimeLaneConfig(deploy, profile)?.agentObserver;
|
|
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return null;
|
|
return cloneJson(policy);
|
|
}
|
|
|
|
function runtimeTraceExplorerUrlTemplate(deploy, profile) {
|
|
if (!isRuntimeLane(profile)) return null;
|
|
const template = runtimeLaneConfig(deploy, profile)?.observability?.traceExplorerUrlTemplate;
|
|
return typeof template === "string" && template.trim().length > 0 ? template.trim() : null;
|
|
}
|
|
|
|
function runtimePrometheusOperatorResourcesEnabled(deploy, profile) {
|
|
if (!isRuntimeLane(profile)) return true;
|
|
const enabled = runtimeLaneConfig(deploy, profile)?.observability?.prometheusOperatorResources;
|
|
return enabled !== false;
|
|
}
|
|
|
|
function effectiveSecretPlaneNodeId(args = {}) {
|
|
return gitopsRootNodeId(args.gitopsRoot) ?? String(args.nodeId ?? "").trim().toUpperCase();
|
|
}
|
|
|
|
function gitopsRootNodeId(gitopsRoot) {
|
|
const normalized = String(gitopsRoot ?? "").replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
const match = normalized.match(/(?:^|\/)deploy\/gitops\/node\/([^/]+)$/u);
|
|
return match?.[1] ? match[1].trim().toUpperCase() : null;
|
|
}
|
|
|
|
function runtimeFrpConfigForProfile(deploy, profile, args = {}) {
|
|
const fallbackWebRemotePort = isRuntimeLane(profile) ? defaultPortForRuntimeLane(profile) : profile === "prod" ? 18666 : 17666;
|
|
const fallbackEdgeRemotePort = isRuntimeLane(profile) ? fallbackWebRemotePort + 1 : profile === "prod" ? 18667 : 17667;
|
|
const baseFrp = isRuntimeLane(profile) ? runtimeLaneConfig(deploy, profile)?.frp : deploy?.frp;
|
|
const nodeId = effectiveSecretPlaneNodeId(args);
|
|
const nodeFrp = nodeId ? baseFrp?.nodes?.[nodeId] : null;
|
|
const frp = nodeFrp && typeof nodeFrp === "object" && !Array.isArray(nodeFrp) ? { ...baseFrp, ...nodeFrp } : baseFrp;
|
|
const server = frp && typeof frp === "object" && !Array.isArray(frp) ? frp.server : null;
|
|
const proxies = Array.isArray(frp?.proxies) ? frp.proxies : [];
|
|
const proxyByEndpoint = new Map(proxies
|
|
.filter((proxy) => proxy && typeof proxy === "object" && !Array.isArray(proxy))
|
|
.map((proxy) => [String(proxy.endpointId || proxy.name || ""), proxy]));
|
|
const proxyValue = (endpointId, key, fallback) => {
|
|
const value = proxyByEndpoint.get(endpointId)?.[key];
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
};
|
|
const proxyPort = (endpointId, fallback) => {
|
|
const value = proxyByEndpoint.get(endpointId)?.remotePort;
|
|
return Number.isInteger(value) ? value : fallback;
|
|
};
|
|
return {
|
|
serverAddr: typeof server?.address === "string" && server.address.trim().length > 0 ? server.address.trim() : "74.48.78.17",
|
|
serverPort: Number.isInteger(server?.bindPort) ? server.bindPort : 7000,
|
|
webRemotePort: proxyPort("frontend", fallbackWebRemotePort),
|
|
edgeRemotePort: proxyPort("api", fallbackEdgeRemotePort),
|
|
webProxyName: proxyValue("frontend", "name", null),
|
|
edgeProxyName: proxyValue("api", "name", null),
|
|
authSecretName: typeof frp?.auth?.secretName === "string" && frp.auth.secretName.trim().length > 0 ? frp.auth.secretName.trim() : null,
|
|
authSecretKey: typeof frp?.auth?.secretKey === "string" && frp.auth.secretKey.trim().length > 0 ? frp.auth.secretKey.trim() : null
|
|
};
|
|
}
|
|
|
|
function runtimeStoreEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile) || serviceId !== "hwlab-cloud-api") return {};
|
|
const postgres = runtimeLaneConfig(deploy, profile)?.runtimeStore?.postgres;
|
|
if (!postgres || typeof postgres !== "object" || Array.isArray(postgres)) return {};
|
|
const env = {};
|
|
if (postgres.sslMode === "disable" || postgres.sslMode === "require") env.HWLAB_CLOUD_DB_SSL_MODE = postgres.sslMode;
|
|
if (Number.isInteger(postgres.poolMax)) env.HWLAB_CLOUD_DB_POOL_MAX = String(postgres.poolMax);
|
|
if (Number.isInteger(postgres.connectionTimeoutMs)) {
|
|
env.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS = String(postgres.connectionTimeoutMs);
|
|
}
|
|
if (Number.isInteger(postgres.queryTimeoutMs)) {
|
|
env.HWLAB_CLOUD_DB_QUERY_TIMEOUT_MS = String(postgres.queryTimeoutMs);
|
|
}
|
|
if (Number.isInteger(postgres.queryRetryMaxAttempts)) {
|
|
env.HWLAB_CLOUD_DB_QUERY_RETRY_MAX_ATTEMPTS = String(postgres.queryRetryMaxAttempts);
|
|
}
|
|
if (Number.isInteger(postgres.queryRetryInitialDelayMs)) {
|
|
env.HWLAB_CLOUD_DB_QUERY_RETRY_INITIAL_DELAY_MS = String(postgres.queryRetryInitialDelayMs);
|
|
}
|
|
if (Number.isInteger(postgres.queryRetryMaxDelayMs)) {
|
|
env.HWLAB_CLOUD_DB_QUERY_RETRY_MAX_DELAY_MS = String(postgres.queryRetryMaxDelayMs);
|
|
}
|
|
return env;
|
|
}
|
|
|
|
function workbenchRuntimeRedisEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile) || serviceId !== "hwlab-workbench-runtime") return {};
|
|
const redis = workbenchRuntimeRedisConfigForProfile(deploy, profile);
|
|
if (!redis || typeof redis !== "object" || Array.isArray(redis)) return {};
|
|
const env = {
|
|
HWLAB_WORKBENCH_CACHE_REDIS_ENABLED: booleanEnv(Boolean(redis.enabled))
|
|
};
|
|
const namespace = typeof redis.namespace === "string" ? redis.namespace.trim() : "";
|
|
const serviceName = typeof redis.serviceName === "string" ? redis.serviceName.trim() : "";
|
|
const port = Number.isInteger(redis.port) ? redis.port : null;
|
|
if (namespace) env.HWLAB_WORKBENCH_CACHE_REDIS_NAMESPACE = namespace;
|
|
if (serviceName) env.HWLAB_WORKBENCH_CACHE_REDIS_SERVICE_NAME = serviceName;
|
|
if (port !== null) env.HWLAB_WORKBENCH_CACHE_REDIS_PORT = String(port);
|
|
if (namespace && serviceName && port !== null) {
|
|
env.HWLAB_WORKBENCH_CACHE_REDIS_URL = `redis://${serviceName}.${namespace}.svc.cluster.local:${port}/0`;
|
|
}
|
|
const ttl = redis.ttl ?? {};
|
|
if (Number.isInteger(ttl.sessionsSummaryMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_SESSIONS_SUMMARY_MS = String(ttl.sessionsSummaryMs);
|
|
if (Number.isInteger(ttl.terminalTurnMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_TERMINAL_TURN_MS = String(ttl.terminalTurnMs);
|
|
if (Number.isInteger(ttl.terminalTracePageMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_TERMINAL_TRACE_PAGE_MS = String(ttl.terminalTracePageMs);
|
|
if (Number.isInteger(ttl.runningPageMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_TTL_RUNNING_PAGE_MS = String(ttl.runningPageMs);
|
|
const limits = redis.limits ?? {};
|
|
if (Number.isInteger(limits.maxKeyBytes)) env.HWLAB_WORKBENCH_CACHE_REDIS_MAX_KEY_BYTES = String(limits.maxKeyBytes);
|
|
if (Number.isInteger(limits.maxPayloadBytes)) env.HWLAB_WORKBENCH_CACHE_REDIS_MAX_PAYLOAD_BYTES = String(limits.maxPayloadBytes);
|
|
const timeouts = redis.timeouts ?? {};
|
|
if (Number.isInteger(timeouts.connectMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_CONNECT_TIMEOUT_MS = String(timeouts.connectMs);
|
|
if (Number.isInteger(timeouts.operationMs)) env.HWLAB_WORKBENCH_CACHE_REDIS_OPERATION_TIMEOUT_MS = String(timeouts.operationMs);
|
|
const labels = redis.metrics?.labels;
|
|
if (labels && typeof labels === "object" && !Array.isArray(labels)) {
|
|
env.HWLAB_WORKBENCH_CACHE_REDIS_METRICS_LABELS = JSON.stringify(labels);
|
|
}
|
|
const behavior = redis.unavailableBehavior ?? {};
|
|
if (typeof behavior.livenessFailure === "boolean") env.HWLAB_WORKBENCH_CACHE_REDIS_UNAVAILABLE_LIVENESS_FAILURE = booleanEnv(behavior.livenessFailure);
|
|
if (typeof behavior.readMode === "string") env.HWLAB_WORKBENCH_CACHE_REDIS_UNAVAILABLE_READ_MODE = behavior.readMode;
|
|
if (typeof behavior.fallbackStateSource === "boolean") env.HWLAB_WORKBENCH_CACHE_REDIS_UNAVAILABLE_FALLBACK_STATE_SOURCE = booleanEnv(behavior.fallbackStateSource);
|
|
if (typeof redis.disableSwitchEnv === "string" && redis.disableSwitchEnv.trim()) {
|
|
env[redis.disableSwitchEnv.trim()] = booleanEnv(Boolean(redis.enabled));
|
|
}
|
|
return env;
|
|
}
|
|
|
|
function workbenchRuntimeClientEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile) || serviceId !== "hwlab-cloud-api") return {};
|
|
const client = runtimeLaneConfig(deploy, profile)?.workbenchRuntime?.client;
|
|
if (!client || typeof client !== "object" || Array.isArray(client)) return {};
|
|
const env = {};
|
|
if (Number.isInteger(client.timeoutMs)) env.HWLAB_WORKBENCH_RUNTIME_TIMEOUT_MS = String(client.timeoutMs);
|
|
return env;
|
|
}
|
|
|
|
function workbenchRuntimeFactsQueryEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile) || serviceId !== "hwlab-cloud-api") return {};
|
|
const factsQuery = runtimeLaneConfig(deploy, profile)?.workbenchRuntime?.factsQuery;
|
|
if (!factsQuery || typeof factsQuery !== "object" || Array.isArray(factsQuery)) return {};
|
|
const env = {};
|
|
if (Number.isInteger(factsQuery.maxAttempts)) env.HWLAB_WORKBENCH_FACTS_QUERY_MAX_ATTEMPTS = String(factsQuery.maxAttempts);
|
|
if (Number.isInteger(factsQuery.backoffBaseMs)) env.HWLAB_WORKBENCH_FACTS_QUERY_BACKOFF_BASE_MS = String(factsQuery.backoffBaseMs);
|
|
if (Number.isInteger(factsQuery.backoffMaxMs)) env.HWLAB_WORKBENCH_FACTS_QUERY_BACKOFF_MAX_MS = String(factsQuery.backoffMaxMs);
|
|
return env;
|
|
}
|
|
|
|
function workbenchRuntimeSessionsSummaryEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile) || serviceId !== "hwlab-workbench-runtime") return {};
|
|
const sessionsSummary = runtimeLaneConfig(deploy, profile)?.workbenchRuntime?.sessionsSummary;
|
|
if (!sessionsSummary || typeof sessionsSummary !== "object" || Array.isArray(sessionsSummary)) return {};
|
|
const env = {};
|
|
if (Number.isInteger(sessionsSummary.maxAttempts)) env.HWLAB_WORKBENCH_SESSIONS_SUMMARY_QUERY_MAX_ATTEMPTS = String(sessionsSummary.maxAttempts);
|
|
if (Number.isInteger(sessionsSummary.attemptTimeoutMs)) env.HWLAB_WORKBENCH_SESSIONS_SUMMARY_QUERY_ATTEMPT_TIMEOUT_MS = String(sessionsSummary.attemptTimeoutMs);
|
|
if (Number.isInteger(sessionsSummary.backoffMs)) env.HWLAB_WORKBENCH_SESSIONS_SUMMARY_QUERY_BACKOFF_MS = String(sessionsSummary.backoffMs);
|
|
return env;
|
|
}
|
|
|
|
function workbenchRuntimePostgresEnvForProfile(deploy, profile, serviceId) {
|
|
if (!isRuntimeLane(profile) || serviceId !== "hwlab-workbench-runtime") return {};
|
|
const postgres = runtimeLaneConfig(deploy, profile)?.workbenchRuntime?.postgres;
|
|
if (!postgres || typeof postgres !== "object" || Array.isArray(postgres)) return {};
|
|
const env = {};
|
|
if (Number.isInteger(postgres.poolMax)) env.HWLAB_WORKBENCH_RUNTIME_DB_POOL_MAX = String(postgres.poolMax);
|
|
if (Number.isInteger(postgres.queryTimeoutMs)) env.HWLAB_WORKBENCH_RUNTIME_QUERY_TIMEOUT_MS = String(postgres.queryTimeoutMs);
|
|
if (Number.isInteger(postgres.readyTimeoutMs)) env.HWLAB_WORKBENCH_RUNTIME_READY_TIMEOUT_MS = String(postgres.readyTimeoutMs);
|
|
return env;
|
|
}
|
|
|
|
function configObject(value, label) {
|
|
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
|
|
return value;
|
|
}
|
|
|
|
function requiredConfigString(record, key, label) {
|
|
const value = record[key];
|
|
assert.ok(typeof value === "string" && value.length > 0, `${label}.${key} must be a non-empty string`);
|
|
return value;
|
|
}
|
|
|
|
function optionalConfigString(record, key, label) {
|
|
const value = record[key];
|
|
if (value === undefined || value === null || value === "") return null;
|
|
assert.ok(typeof value === "string", `${label}.${key} must be a string when set`);
|
|
return value;
|
|
}
|
|
|
|
function requiredConfigPositiveInteger(record, key, label) {
|
|
const value = record[key];
|
|
assert.ok(Number.isInteger(value) && value > 0, `${label}.${key} must be a positive integer`);
|
|
return value;
|
|
}
|
|
|
|
function workbenchRuntimeRedisConfigForProfile(deploy, profile) {
|
|
if (!isRuntimeLane(profile)) return null;
|
|
const label = `deploy.lanes.${profile}.workbenchRuntime.cache.redis`;
|
|
const redis = runtimeLaneConfig(deploy, profile)?.workbenchRuntime?.cache?.redis;
|
|
if (!redis || redis.enabled !== true) return null;
|
|
configObject(redis, label);
|
|
const namespace = requiredConfigString(redis, "namespace", label);
|
|
assert.equal(namespace, namespaceNameForProfile(profile), `${label}.namespace must match ${namespaceNameForProfile(profile)}`);
|
|
const serviceName = requiredConfigString(redis, "serviceName", label);
|
|
const image = requiredConfigString(redis, "image", label);
|
|
const port = requiredConfigPositiveInteger(redis, "port", label);
|
|
assert.ok(port <= 65535, `${label}.port must be a valid TCP port`);
|
|
const resources = configObject(redis.resources, `${label}.resources`);
|
|
configObject(resources.requests, `${label}.resources.requests`);
|
|
configObject(resources.limits, `${label}.resources.limits`);
|
|
const memoryPolicy = configObject(redis.memoryPolicy, `${label}.memoryPolicy`);
|
|
assert.equal(memoryPolicy.persistence, "disabled", `${label}.memoryPolicy.persistence must be disabled`);
|
|
requiredConfigString(memoryPolicy, "maxMemory", `${label}.memoryPolicy`);
|
|
requiredConfigString(memoryPolicy, "eviction", `${label}.memoryPolicy`);
|
|
return redis;
|
|
}
|
|
|
|
function booleanEnv(value) {
|
|
return value ? "1" : "0";
|
|
}
|
|
|
|
function mergeEnvMaps(...values) {
|
|
const result = {};
|
|
for (const value of values) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
Object.assign(result, value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function runtimeNodeIdForProfile(deploy, profile, fallback = "node") {
|
|
if (!isRuntimeLane(profile)) return fallback;
|
|
if (typeof fallback === "string" && fallback.trim() && fallback !== "node") return fallback;
|
|
const nodeId = deploy?.lanes?.[profile]?.node;
|
|
return typeof nodeId === "string" && nodeId.length > 0 ? nodeId : fallback;
|
|
}
|
|
|
|
function v02MetricsLabels(serviceId) {
|
|
return {
|
|
"hwlab.pikastech.local/monitoring": "enabled",
|
|
"hwlab.pikastech.local/service-id": serviceId
|
|
};
|
|
}
|
|
|
|
function v02MetricsSidecarVolume(profile = "v02") {
|
|
return { name: "hwlab-metrics-sidecar", configMap: { name: `hwlab-${profile}-metrics-sidecar` } };
|
|
}
|
|
|
|
function v02MetricsSidecarAnnotations(metricsSidecarSha256) {
|
|
return metricsSidecarSha256 ? { "hwlab.pikastech.local/metrics-sidecar-sha256": metricsSidecarSha256 } : {};
|
|
}
|
|
|
|
function v02MetricsSidecarContainer({ deploy, serviceId, namespace, gitopsTarget, profile = "v02" }) {
|
|
const service = runtimeLaneObservableService(deploy, profile, serviceId);
|
|
assert.ok(service, `unknown ${profile} observable service ${serviceId}`);
|
|
const extraMetricsEnv = serviceId === "hwlab-cloud-api"
|
|
? [
|
|
{ name: "HWLAB_METRICS_EXTRA_URL", value: `http://127.0.0.1:${service.port}/v1/web-performance/metrics` },
|
|
{ name: "HWLAB_METRICS_EXTRA_TIMEOUT_MS", value: "2000" }
|
|
]
|
|
: [];
|
|
return {
|
|
name: "hwlab-metrics",
|
|
image: ciToolsRunnerImage,
|
|
imagePullPolicy: "IfNotPresent",
|
|
command: ["node", "/metrics/metrics-sidecar.mjs"],
|
|
env: [
|
|
{ name: "HWLAB_METRICS_SERVICE_ID", value: serviceId },
|
|
{ name: "HWLAB_METRICS_NAMESPACE", value: namespace },
|
|
{ name: "HWLAB_METRICS_GITOPS_TARGET", value: gitopsTarget },
|
|
{ name: "HWLAB_METRICS_PORT", value: "9100" },
|
|
{ name: "HWLAB_METRICS_TARGET_URL", value: `http://127.0.0.1:${service.port}${service.healthPath}` },
|
|
{ name: "HWLAB_METRICS_TARGET_TIMEOUT_MS", value: "2000" },
|
|
...extraMetricsEnv
|
|
],
|
|
ports: [{ name: "metrics", containerPort: 9100 }],
|
|
readinessProbe: { httpGet: { path: "/health/live", port: "metrics" }, initialDelaySeconds: 3, periodSeconds: 10 },
|
|
livenessProbe: { httpGet: { path: "/health/live", port: "metrics" }, initialDelaySeconds: 10, periodSeconds: 20 },
|
|
volumeMounts: [{ name: "hwlab-metrics-sidecar", mountPath: "/metrics", readOnly: true }]
|
|
};
|
|
}
|
|
|
|
function upsertV02MetricsSidecar(podSpec, options) {
|
|
if (!podSpec || !Array.isArray(podSpec.containers)) return;
|
|
podSpec.containers = podSpec.containers.filter((container) => container?.name !== "hwlab-metrics");
|
|
podSpec.containers.push(v02MetricsSidecarContainer(options));
|
|
podSpec.volumes ??= [];
|
|
podSpec.volumes = podSpec.volumes.filter((volume) => volume?.name !== "hwlab-metrics-sidecar");
|
|
podSpec.volumes.push(v02MetricsSidecarVolume(options.profile));
|
|
}
|
|
|
|
function upsertV02MetricsPort(service) {
|
|
service.spec ??= {};
|
|
service.spec.ports ??= [];
|
|
service.spec.ports = service.spec.ports.filter((port) => port?.name !== "metrics");
|
|
service.spec.ports.push({ name: "metrics", port: 9100, targetPort: "metrics" });
|
|
}
|
|
|
|
function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = defaultBranch, gitReadUrl, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", nodeId = "node", useDeployImages = false, metricsSidecarSha256 = null }) {
|
|
const result = cloneJson(workloads);
|
|
const deployServices = deployServicesForProfile(deploy, profile);
|
|
const namespace = namespaceNameForProfile(profile);
|
|
const profileLabel = runtimeLabelForProfile(profile);
|
|
const gitopsTarget = gitopsTargetForProfile(profile);
|
|
const runtimeLane = isRuntimeLane(profile);
|
|
const digestPin = runtimeLane;
|
|
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, profile);
|
|
const runtimeObservableServiceIds = runtimeLane ? runtimeLaneObservableServiceIdsForDeploy(deploy, profile) : new Set();
|
|
const stableRuntimeLabels = {
|
|
"app.kubernetes.io/part-of": "hwlab",
|
|
"hwlab.pikastech.local/environment": profileLabel,
|
|
"hwlab.pikastech.local/profile": profileLabel
|
|
};
|
|
result.items = asItems(result, "workloads").filter((item) => !(runtimeLane && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
|
|
for (const item of asItems(result, "workloads")) {
|
|
item.metadata.namespace = namespace;
|
|
label(item.metadata, {
|
|
...stableRuntimeLabels,
|
|
"hwlab.pikastech.local/gitops-target": gitopsTarget,
|
|
"hwlab.pikastech.local/gitops-render-source-commit": source.full,
|
|
"hwlab.pikastech.local/source-commit": source.full,
|
|
"unidesk.ai/node-staging": profile === "dev" ? "true" : "false"
|
|
});
|
|
annotate(item.metadata, {
|
|
"hwlab.pikastech.local/gitops-note": profile === "v02" ? "HWLAB v0.2 GitOps target." : profile === "prod" ? "node PROD GitOps target." : "node DEV GitOps target.",
|
|
"hwlab.pikastech.local/gitops-render-source-commit": source.full,
|
|
"hwlab.pikastech.local/source-commit": source.full
|
|
});
|
|
if (item.kind === "Job") {
|
|
annotate(item.metadata, {
|
|
"argocd.argoproj.io/sync-options": "Force=true,Replace=true"
|
|
});
|
|
if (item.spec?.suspend === true) {
|
|
annotate(item.metadata, {
|
|
"argocd.argoproj.io/ignore-healthcheck": "true"
|
|
});
|
|
}
|
|
}
|
|
const podTemplate = item?.spec?.template;
|
|
if (!podTemplate?.spec?.containers) continue;
|
|
const templateServiceId = serviceIdForWorkload(item, podTemplate.spec.containers[0]);
|
|
const templateArtifact = runtimeArtifactForService({ catalog, deploy, serviceId: templateServiceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
|
|
const templateSourceCommit = runtimeLane ? (templateArtifact.commit ?? source.full) : source.full;
|
|
const templateArtifactCommit = templateArtifact.commit;
|
|
label(podTemplate.metadata ??= {}, {
|
|
...stableRuntimeLabels,
|
|
"hwlab.pikastech.local/gitops-target": gitopsTarget,
|
|
"hwlab.pikastech.local/gitops-render-source-commit": source.full,
|
|
"hwlab.pikastech.local/source-commit": templateSourceCommit,
|
|
"hwlab.pikastech.local/artifact-source-commit": templateArtifactCommit
|
|
});
|
|
if (runtimeLane && runtimeObservableServiceIds.has(templateServiceId)) {
|
|
label(item.metadata, v02MetricsLabels(templateServiceId));
|
|
label(podTemplate.metadata, v02MetricsLabels(templateServiceId));
|
|
annotate(podTemplate.metadata, v02MetricsSidecarAnnotations(metricsSidecarSha256));
|
|
upsertV02MetricsSidecar(podTemplate.spec, { deploy, serviceId: templateServiceId, namespace, gitopsTarget, profile });
|
|
}
|
|
if ((item.kind === "Deployment" || item.kind === "StatefulSet") && item.spec?.selector?.matchLabels) {
|
|
item.spec.selector.matchLabels = {
|
|
...item.spec.selector.matchLabels,
|
|
...stableRuntimeLabels
|
|
};
|
|
}
|
|
annotate(podTemplate.metadata, {
|
|
"hwlab.pikastech.local/gitops-render-source-commit": source.full,
|
|
"hwlab.pikastech.local/source-commit": templateSourceCommit,
|
|
"hwlab.pikastech.local/artifact-source-commit": templateArtifactCommit,
|
|
"hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs"
|
|
});
|
|
for (const container of podTemplate.spec.containers) {
|
|
if (container.name === "hwlab-metrics") continue;
|
|
const serviceId = serviceIdForWorkload(item, container);
|
|
const deployService = deployServices.get(serviceId);
|
|
if (!deployService) continue;
|
|
applyDeployServiceReplicas(item, deployService);
|
|
const artifact = runtimeArtifactForService({ catalog, deploy, serviceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
|
|
const envReuse = runtimeLane && v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds);
|
|
const bootDeployService = envReuse ? deployServiceForBoot(deploy, serviceId, profile) : null;
|
|
const bootMetadata = envReuse ? bootMetadataForService({ args: { sourceRepo: deploy?.lanes?.[profile]?.sourceRepo || defaultSourceRepo, registryPrefix }, catalog, deployService: bootDeployService, serviceId, source }) : null;
|
|
const image = artifact.image;
|
|
const runtimeCommit = artifact.commit;
|
|
const runtimeImageTag = artifact.imageTag;
|
|
container.image = image;
|
|
container.imagePullPolicy = "IfNotPresent";
|
|
container.env ??= [];
|
|
for (const entry of container.env) {
|
|
if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace);
|
|
}
|
|
applyDeployEnv(container.env, deployService, namespace, profile);
|
|
applyDeployConfigMapMounts(podTemplate.spec, container, deployService);
|
|
if (runtimeLane) container.env = removeV02LegacySimulatorEnv(container.env);
|
|
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
|
|
upsertEnv(container.env, "HWLAB_COMMIT_ID", runtimeCommit);
|
|
upsertEnv(container.env, "HWLAB_IMAGE", image);
|
|
upsertEnv(container.env, "HWLAB_IMAGE_TAG", runtimeImageTag);
|
|
if (serviceId === "hwlab-agent-skills") {
|
|
upsertEnv(container.env, "HWLAB_SKILLS_COMMIT_ID", runtimeCommit);
|
|
}
|
|
upsertEnv(container.env, "HWLAB_GITOPS_TARGET", gitopsTarget);
|
|
upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel);
|
|
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", runtimeLane ? runtimeCommit : source.full);
|
|
if (runtimeLane && serviceId === "hwlab-cloud-api") {
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT", runtimeCommit);
|
|
}
|
|
if (bootMetadata) {
|
|
applyEnvReuseBootEnv(container, bootMetadata, {
|
|
sourceBranch,
|
|
gitReadUrl,
|
|
bootSh: envReuseBootShForContainer({ serviceId, container, defaultBootSh: bootMetadata.bootSh })
|
|
});
|
|
if (container.name === serviceId) annotateEnvReusePodTemplate(podTemplate.metadata, bootMetadata);
|
|
}
|
|
if (runtimeLane && container.name === serviceId) {
|
|
applyServiceHealthProbe(container, deploy?.lanes?.[profile]?.serviceDeclarations?.[serviceId]?.healthProbe);
|
|
}
|
|
if (serviceId === "hwlab-cloud-api" || serviceId === "hwlab-edge-proxy") {
|
|
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint);
|
|
}
|
|
if (runtimeLane && (serviceId === "hwlab-cloud-api" || serviceId === "hwlab-cloud-web")) {
|
|
const agentObserverPolicy = runtimeAgentObserverPolicy(deploy, profile);
|
|
if (agentObserverPolicy) {
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_RETENTION_EVENT_LIMIT", String(agentObserverPolicy.retentionEventLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_RETENTION_SCAN_LIMIT", String(agentObserverPolicy.retentionScanLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_RETENTION_TIMEOUT_MS", String(agentObserverPolicy.retentionTimeoutMs));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_RETENTION_GROUP_PREFIX", String(agentObserverPolicy.retentionGroupPrefix));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_SSE_HEARTBEAT_MS", String(agentObserverPolicy.sseHeartbeatMs));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_RECONNECT_DELAY_MS", String(agentObserverPolicy.reconnectDelayMs));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_BATCH_MAX_EVENTS", String(agentObserverPolicy.batchMaxEvents));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_LIVE_BUFFER_LIMIT", String(agentObserverPolicy.liveBufferLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_IDENTITY_WINDOW_LIMIT", String(agentObserverPolicy.identityWindowLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_REDUCER_EVENT_LIMIT", String(agentObserverPolicy.reducerEventLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_REDUCER_ENTITY_LIMIT", String(agentObserverPolicy.reducerEntityLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_TREE_NODE_LIMIT", String(agentObserverPolicy.treeNodeLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_INSPECTOR_EVENT_TAIL_LIMIT", String(agentObserverPolicy.inspectorEventTailLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_VIRTUAL_LIST_WINDOW_LIMIT", String(agentObserverPolicy.virtualListWindowLimit));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_CHANGE_HIGHLIGHT_MS", String(agentObserverPolicy.changeHighlightMs));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_STALE_AFTER_MS", String(agentObserverPolicy.staleAfterMs));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_REDUCED_MOTION", String(agentObserverPolicy.reducedMotion));
|
|
if (serviceId === "hwlab-cloud-api") {
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX", String(agentObserverPolicy.retentionGroupPrefix));
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS", String(agentObserverPolicy.retentionTimeoutMs));
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT", String(agentObserverPolicy.retentionScanLimit));
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT", String(agentObserverPolicy.retentionEventLimit));
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT", String(agentObserverPolicy.liveBufferLimit));
|
|
}
|
|
if (serviceId === "hwlab-cloud-web") {
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_NODE_ID", runtimeNodeIdForProfile(deploy, profile, nodeId));
|
|
upsertEnv(container.env, "HWLAB_AGENT_OBSERVER_LANE", profile);
|
|
}
|
|
}
|
|
}
|
|
if (runtimeLane && serviceId === "hwlab-edge-proxy") {
|
|
useLocalHealthProbe(container, "/health");
|
|
}
|
|
if (serviceId === "hwlab-cloud-api") {
|
|
if (runtimeLane) {
|
|
removeV02RepoOwnedCodeAgentPodConfig(item.spec, podTemplate.spec);
|
|
applyServiceShutdownDrainLifecycle(container, deployService.shutdownDrain);
|
|
upsertEnv(container.env, "HWLAB_M3_IO_CONTROL_ENABLED", "false");
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_REPO_URL", gitReadUrl ?? defaultRuntimeLaneGitReadUrl);
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", runtimeNodeIdForProfile(deploy, profile, nodeId));
|
|
upsertEnv(container.env, "HWLAB_OPENFGA_MODE", "enforce");
|
|
upsertEnv(container.env, "HWLAB_OPENFGA_API_URL", `http://hwlab-openfga.${namespace}.svc.cluster.local:8080`);
|
|
upsertEnvEntry(container.env, deployEnvEntry("HWLAB_OPENFGA_AUTHN_TOKEN", `secretRef:hwlab-${profile}-openfga/authn-preshared-key`, namespace, profile));
|
|
upsertEnv(container.env, "UNIDESK_MAIN_SERVER_IP", "http://74.48.78.17:18081");
|
|
} else {
|
|
syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag });
|
|
}
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", runtimeLane ? (configString(runtimeLaneConfig(deploy, profile)?.opencode?.providerProfile) || "dsflash-go") : "deepseek");
|
|
if (runtimeLane) upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR", "repo-owned");
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel);
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_MODEL", codexApiProfileModel);
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_BASE_URL", codexApiProfileBaseUrl);
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL", codexApiForwarderUpstreamBaseUrl);
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT", codexApiForwarderPort);
|
|
upsertEnv(container.env, "HWLAB_PREINSTALLED_SKILLS_DIR", "/app/skills");
|
|
upsertEnv(container.env, "HWLAB_USER_SKILLS_DIR", "/data/user-skills");
|
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_SKILLS_DIRS", "/app/skills:/data/user-skills");
|
|
upsertEnv(container.env, "NO_PROXY", codeAgentNoProxy(namespace));
|
|
upsertEnv(container.env, "no_proxy", codeAgentNoProxy(namespace));
|
|
}
|
|
if (serviceId === "hwlab-cloud-web") {
|
|
upsertEnv(container.env, "HWLAB_API_BASE_URL", `http://hwlab-cloud-api.${namespace}.svc.cluster.local:6667`);
|
|
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", webEndpoint);
|
|
const traceTimelinePolicy = runtimeWorkbenchTraceTimelinePolicy(deploy, profile);
|
|
if (typeof traceTimelinePolicy?.autoExpandRunning === "boolean") {
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_TRACE_AUTO_EXPAND_RUNNING", booleanEnv(traceTimelinePolicy.autoExpandRunning));
|
|
}
|
|
if (typeof traceTimelinePolicy?.autoCollapseTerminal === "boolean") {
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_TRACE_AUTO_COLLAPSE_TERMINAL", booleanEnv(traceTimelinePolicy.autoCollapseTerminal));
|
|
}
|
|
const rawHwlabEventWindowPolicy = runtimeWorkbenchRawHwlabEventWindowPolicy(deploy, profile);
|
|
if (rawHwlabEventWindowPolicy) {
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED", booleanEnv(rawHwlabEventWindowPolicy.enabled));
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES", String(rawHwlabEventWindowPolicy.maxEntries));
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES", String(rawHwlabEventWindowPolicy.maxRetainedBytes));
|
|
}
|
|
const traceExplorerUrlTemplate = runtimeTraceExplorerUrlTemplate(deploy, profile);
|
|
if (traceExplorerUrlTemplate) {
|
|
upsertEnv(container.env, "HWLAB_WORKBENCH_TRACE_EXPLORER_URL_TEMPLATE", traceExplorerUrlTemplate);
|
|
}
|
|
}
|
|
}
|
|
rewritePodSecretRefs(podTemplate.spec, profile);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function transformHealthContract(value, namespace, labels, annotations, runtimeEndpoint, webEndpoint, profile = "dev") {
|
|
const result = transformListNamespace(value, namespace, labels, annotations);
|
|
if (result.metadata?.name === "hwlab-dev-health-contract") {
|
|
result.metadata.name = `hwlab-${profile}-health-contract`;
|
|
}
|
|
if (result.metadata?.labels && Object.hasOwn(result.metadata.labels, "app.kubernetes.io/name")) {
|
|
result.metadata.labels["app.kubernetes.io/name"] = `hwlab-${profile}-health-contract`;
|
|
}
|
|
if (result.data && typeof result.data === "object") {
|
|
if (Object.hasOwn(result.data, "endpoint")) result.data.endpoint = runtimeEndpoint;
|
|
if (Object.hasOwn(result.data, "cloud-web")) {
|
|
result.data["cloud-web"] = `GET /health/live on ${webEndpoint}; consumes cloud-api only`;
|
|
}
|
|
if (Object.hasOwn(result.data, "cloud-api")) {
|
|
result.data["cloud-api"] = `GET /health/live through ${runtimeEndpoint}`;
|
|
}
|
|
if (Object.hasOwn(result.data, "tunnel-client")) {
|
|
result.data["tunnel-client"] = "disabled for node GitOps; no FRP or production traffic tunnel is required";
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function transformListNamespace(value, namespace, labels, annotations) {
|
|
const result = cloneJson(value);
|
|
if (result.kind === "List") {
|
|
for (const item of result.items ?? []) {
|
|
item.metadata ??= {};
|
|
item.metadata.namespace = namespace;
|
|
label(item.metadata, labels);
|
|
annotate(item.metadata, annotations);
|
|
}
|
|
} else {
|
|
result.metadata ??= {};
|
|
result.metadata.namespace = namespace;
|
|
label(result.metadata, labels);
|
|
annotate(result.metadata, annotations);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function transformServices({ services, namespace, labels, annotations, profile = "dev", deploy = null, nodeId = "node" }) {
|
|
const observableServiceIds = isRuntimeLane(profile) ? runtimeLaneObservableServiceIdsForDeploy(deploy, profile) : new Set();
|
|
const result = transformListNamespace(services, namespace, labels, annotations);
|
|
if (result.kind === "List") {
|
|
result.items = (result.items ?? []).filter((item) => !(isRuntimeLane(profile) && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
|
|
for (const item of result.items) {
|
|
const serviceId = serviceIdForWorkload(item, null);
|
|
if (!isRuntimeLane(profile) || !observableServiceIds.has(serviceId)) continue;
|
|
label(item.metadata ??= {}, v02MetricsLabels(serviceId));
|
|
upsertV02MetricsPort(item);
|
|
}
|
|
const nodePublicServices = deploy?.lanes?.[profile]?.publicServices?.nodes?.[nodeId];
|
|
assert.ok(nodePublicServices === undefined || Array.isArray(nodePublicServices), `deploy.lanes.${profile}.publicServices.nodes.${nodeId} must be an array`);
|
|
for (const service of nodePublicServices ?? []) {
|
|
assert.ok(service && typeof service === "object" && !Array.isArray(service), `deploy.lanes.${profile}.publicServices.nodes.${nodeId} entries must be objects`);
|
|
assert.ok(typeof service.name === "string" && service.name.length > 0, `deploy.lanes.${profile}.publicServices.nodes.${nodeId}[].name is required`);
|
|
assert.ok(service.selector && typeof service.selector === "object" && !Array.isArray(service.selector), `deploy.lanes.${profile}.publicServices.nodes.${nodeId}[].selector is required`);
|
|
for (const key of ["port", "targetPort", "nodePort"]) {
|
|
assert.ok(Number.isInteger(service[key]) && service[key] > 0, `deploy.lanes.${profile}.publicServices.nodes.${nodeId}[].${key} must be a positive integer`);
|
|
}
|
|
result.items.push({
|
|
apiVersion: "v1",
|
|
kind: "Service",
|
|
metadata: {
|
|
name: service.name,
|
|
namespace,
|
|
labels: { ...labels, "app.kubernetes.io/name": service.name },
|
|
annotations: { ...annotations }
|
|
},
|
|
spec: {
|
|
type: "NodePort",
|
|
selector: cloneJson(service.selector),
|
|
ports: [{ name: "http", port: service.port, targetPort: service.targetPort, nodePort: service.nodePort }]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function observabilityManifest({ deploy, profile, namespace, labels, annotations, metricsSidecarScript }) {
|
|
assert.ok(isRuntimeLane(profile), `observability profile must be a runtime lane, got ${profile}`);
|
|
const includePrometheusOperatorResources = runtimePrometheusOperatorResourcesEnabled(deploy, profile);
|
|
const baseLabels = {
|
|
...labels,
|
|
"app.kubernetes.io/part-of": "hwlab",
|
|
"hwlab.pikastech.local/monitoring": "enabled"
|
|
};
|
|
const serviceMonitors = runtimeLaneObservableServicesForDeploy(deploy, profile).map((service) => ({
|
|
apiVersion: "monitoring.coreos.com/v1",
|
|
kind: "ServiceMonitor",
|
|
metadata: {
|
|
name: `hwlab-${profile}-${service.serviceId}`,
|
|
namespace,
|
|
labels: { ...baseLabels, "hwlab.pikastech.local/service-id": service.serviceId },
|
|
annotations
|
|
},
|
|
spec: {
|
|
namespaceSelector: { matchNames: [namespace] },
|
|
selector: {
|
|
matchLabels: {
|
|
"hwlab.pikastech.local/gitops-target": profile,
|
|
"hwlab.pikastech.local/monitoring": "enabled",
|
|
"hwlab.pikastech.local/service-id": service.serviceId
|
|
}
|
|
},
|
|
targetLabels: [
|
|
"hwlab.pikastech.local/gitops-target",
|
|
"hwlab.pikastech.local/service-id"
|
|
],
|
|
endpoints: [{ port: "metrics", path: "/metrics", interval: "30s", scrapeTimeout: "10s" }]
|
|
}
|
|
}));
|
|
return {
|
|
apiVersion: "v1",
|
|
kind: "List",
|
|
items: [
|
|
{
|
|
apiVersion: "v1",
|
|
kind: "ConfigMap",
|
|
metadata: { name: `hwlab-${profile}-metrics-sidecar`, namespace, labels: baseLabels, annotations },
|
|
data: { "metrics-sidecar.mjs": metricsSidecarScript }
|
|
},
|
|
...(includePrometheusOperatorResources ? serviceMonitors : []),
|
|
...(includePrometheusOperatorResources ? [{
|
|
apiVersion: "monitoring.coreos.com/v1",
|
|
kind: "PrometheusRule",
|
|
metadata: { name: `hwlab-${profile}-observability`, namespace, labels: baseLabels, annotations },
|
|
spec: {
|
|
groups: [{
|
|
name: `hwlab-${profile}-observability`,
|
|
rules: [
|
|
{
|
|
alert: `HWLAB${profile.toUpperCase()}MetricsTargetDown`,
|
|
expr: `up{namespace="${namespace}"} == 0`,
|
|
for: "5m",
|
|
labels: { severity: "warning", lane: profile },
|
|
annotations: { summary: `HWLAB ${versionNameForRuntimeLane(profile)} metrics target is down`, description: `Prometheus cannot scrape a HWLAB ${versionNameForRuntimeLane(profile)} metrics target.` }
|
|
},
|
|
{
|
|
alert: `HWLAB${profile.toUpperCase()}ServiceHealthProbeFailed`,
|
|
expr: `hwlab_service_health_probe_success{namespace="${namespace}"} == 0`,
|
|
for: "5m",
|
|
labels: { severity: "warning", lane: profile },
|
|
annotations: { summary: "HWLAB v0.2 service health probe failed", description: "The metrics sidecar cannot reach the service health endpoint inside the pod network." }
|
|
}
|
|
]
|
|
}]
|
|
}
|
|
}] : [])
|
|
]
|
|
};
|
|
}
|
|
|
|
export {
|
|
applyDeployConfigMapMounts, applyDeployEnv, applyDeployServiceReplicas,
|
|
applyEnvReuseBootEnv, applyEnvReuseStartupProbe, applyRuntimeLaneDeployConfig,
|
|
argoApplicationName, artifactCatalogSkeleton, artifactEndpoint, artifactEnvironment,
|
|
asItems, attachAccessControlEnv, annotate, booleanEnv, bootMetadataForService,
|
|
bootShForService, boundedInteger, buildkitRunnerImage, catalogServiceById,
|
|
ciToolsRunnerImage, cloneJson, codeAgentNoProxy, codexApiForwarderPort,
|
|
codexApiForwarderUpstreamBaseUrl, codexApiProfileBaseUrl, codexApiProfileModel,
|
|
configObject, configString, deepSeekBaseUrl, deepSeekProfileModel, defaultAllProxyUrl,
|
|
defaultBranch, defaultCatalogPath, defaultDevBaseImage, defaultEndpointForRuntimeLane,
|
|
defaultGitopsBranch, defaultGitReadUrl, defaultNoProxy, defaultOutDir,
|
|
defaultPortForRuntimeLane, defaultProdRuntimeEndpoint, defaultProdWebEndpoint,
|
|
defaultProxyUrl, defaultRegistryPrefix, defaultRuntimeEndpoint,
|
|
defaultRuntimeLaneGitReadUrl, defaultRuntimeLaneGitWriteUrl, defaultServiceIds,
|
|
defaultSourceRepo, defaultV02GitReadUrl, defaultV02GitWriteUrl,
|
|
defaultV02RuntimeEndpoint, defaultV02WebEndpoint, defaultWebEndpoint,
|
|
deployEnvEntry, deployServiceForBoot, deployServicesForProfile, digestPinnedImage,
|
|
effectiveSecretPlaneNodeId, ensureObject, envReuseBootShForContainer, envReuseServiceIdsForLane,
|
|
generatedPath, gitopsPathFor, gitopsPathForProfile, gitopsRootForArgs, gitopsRootNodeId,
|
|
gitopsTargetForProfile, gitValue, imageFor, imageTagForSource,
|
|
imageTagFromReference, isRuntimeLane, isV02RemovedServiceId, jsonManifest,
|
|
k8sLabelHash, label, laneNames, laneRuntimeProfiles, mergeEnvMaps,
|
|
moonBridgeImage, moonBridgeSourceRef, namespaceNameForProfile, namespaceScopedHost,
|
|
normalizeAccessControlUser, normalizeNavProfile, normalizeProbeTiming,
|
|
observabilityManifest, optionalConfigString, optionalObject, parseArgs, primitiveValidationTasks,
|
|
profileEndpoint, proxyEnv, readConfigRef, readJson, readJsonIfPresent,
|
|
readOption, readStructuredFile, removeV02LegacySimulatorEnv, repoRoot,
|
|
removeV02RepoOwnedCodeAgentPodConfig, repositoryFromImage,
|
|
requiredAccessConfigString, requiredConfigPositiveInteger, requiredConfigString,
|
|
resolveSourceRevision, rewritePodSecretRefs,
|
|
runtimeArtifactForService, runtimeCommitForService, runtimeFrpConfigForProfile,
|
|
runtimeImageForService, runtimeImageTagForService, runtimeLabelForProfile,
|
|
runtimeLaneConfig, runtimeLaneMirrorSpecs, runtimeLaneObservableService,
|
|
runtimeLaneObservableServiceIdsForDeploy, runtimeLaneObservableServicesForDeploy,
|
|
runtimeLaneServiceIdsFromDeploy, runtimeNodeIdForProfile,
|
|
runtimePathForProfile, runtimePrometheusOperatorResourcesEnabled,
|
|
runtimeStoreEnvForProfile, runtimeTraceExplorerUrlTemplate,
|
|
runtimeWorkbenchTraceTimelinePolicy, secretNameForProfile,
|
|
secretRefFromParts, secretRefObjectForProfile, serviceDeclarationEnvForProfile,
|
|
serviceIdForWorkload, serviceIdsForLane, serviceIdsForProfile,
|
|
servicesParamForLane, sourceCommitPattern, syncCodeAgentWorkspaceInitContainer,
|
|
textFile, transformHealthContract, transformListNamespace, transformServices,
|
|
transformWorkloads, uniquePreserveOrder, upsertEnv, upsertEnvEntry,
|
|
upsertV02MetricsPort, upsertV02MetricsSidecar, usage, useLocalHealthProbe,
|
|
v02EnvReuseEnabled, v02EnvReuseRuntimeMode, v02ExtraObservableServices,
|
|
v02MetricsLabels, v02MetricsSidecarAnnotations, v02MetricsSidecarContainer,
|
|
v02MetricsSidecarVolume, v02ObservableService, v02ObservableServiceIdsForDeploy,
|
|
v02ObservableServicesForDeploy, v02RemovedServiceEnvNames, v02RemovedServiceIds,
|
|
v02ServiceIdsFromDeploy, versionNameForRuntimeLane, workbenchRuntimeClientEnvForProfile,
|
|
workbenchRuntimeFactsQueryEnvForProfile, workbenchRuntimePostgresEnvForProfile,
|
|
workbenchRuntimeRedisConfigForProfile, workbenchRuntimeRedisEnvForProfile,
|
|
workbenchRuntimeSessionsSummaryEnvForProfile
|
|
};
|