feat: support named runtime lanes
This commit is contained in:
+34
-18
@@ -24,6 +24,7 @@ import {
|
||||
generatedPath,
|
||||
gitopsPathForProfile,
|
||||
imageTagForSource,
|
||||
isNamedRuntimeLane,
|
||||
isRuntimeLane,
|
||||
jsonManifest,
|
||||
label,
|
||||
@@ -78,7 +79,7 @@ import {
|
||||
} from "./src/gitops-render/tekton-manifests.mjs";
|
||||
import { CLOUD_CORE_MIGRATIONS } from "../internal/db/schema.ts";
|
||||
|
||||
function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev", externalPostgres = false, workbenchRedis = false, runtimeConfigMaps = false, externalSecrets = false } = {}) {
|
||||
function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev", externalPostgres = false, workbenchRedis = false, runtimeConfigMaps = false, externalSecrets = false, deploy = null } = {}) {
|
||||
const resources = ["namespace.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"];
|
||||
if (!isRuntimeLane(profile)) resources.splice(1, 0, "code-agent-codex-config.yaml");
|
||||
if (runtimeConfigMaps) resources.splice(3, 0, "configmaps.yaml");
|
||||
@@ -94,12 +95,12 @@ function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile ==
|
||||
return {
|
||||
apiVersion: "kustomize.config.k8s.io/v1beta1",
|
||||
kind: "Kustomization",
|
||||
namespace: namespaceNameForProfile(profile),
|
||||
namespace: namespaceNameForProfile(profile, deploy),
|
||||
resources
|
||||
};
|
||||
}
|
||||
|
||||
function readme({ source, args }) {
|
||||
function readme({ source, args, deploy }) {
|
||||
if (args.lane === "v02") {
|
||||
return `# HWLAB v0.2 GitOps CI/CD
|
||||
|
||||
@@ -116,6 +117,21 @@ This directory is generated by \`scripts/gitops-render.mjs --lane v02\` from sou
|
||||
- node DEV/PROD boundary: this lane does not modify archived DEV/PROD branches, \`hwlab-dev\`, \`hwlab-prod\`, 17666/17667 or 18666/18667.
|
||||
- CronJob policy: v0.2 does not generate \`hwlab-v02-branch-poller\` or \`hwlab-v02-control-plane-reconciler\`; obsolete live CronJobs should be deleted by the UniDesk control-plane apply command.
|
||||
- Gate policy: old DEV/D601/main gates do not enter this lane; new checks stay limited to the fixed branch, namespace, catalog, runtime path and Argo boundaries.
|
||||
`;
|
||||
}
|
||||
if (isNamedRuntimeLane(args.lane)) {
|
||||
const namespace = namespaceNameForProfile(args.lane, deploy);
|
||||
const runtimePath = gitopsPathForProfile(args, args.lane, deploy);
|
||||
return `# HWLAB ${args.lane} GitOps CI/CD
|
||||
|
||||
This directory is generated by \`scripts/gitops-render.mjs --lane ${args.lane}\` from the owning runtime lane declaration in \`deploy/deploy.yaml\`.
|
||||
|
||||
- Source: \`${args.sourceRepo}\` branch \`${args.sourceBranch}\` at \`${source.full}\`.
|
||||
- GitOps: generated desired state is promoted to \`${args.gitWriteUrl}\` branch \`${args.gitopsBranch}\`.
|
||||
- Artifact contract: runtime images come from \`${args.catalogPath}\`.
|
||||
- Runtime: Argo CD consumes \`${args.gitReadUrl}:${args.gitopsBranch}:${runtimePath}\` and deploys only to namespace \`${namespace}\`.
|
||||
- Public endpoints: web \`${args.webEndpoint}\`; API \`${args.runtimeEndpoint}\`.
|
||||
- Authority: branch, catalog, runtime path, namespace, public services and endpoints are read from \`deploy/deploy.yaml\`; missing named-lane fields fail closed.
|
||||
`;
|
||||
}
|
||||
return `# HWLAB node GitOps CI/CD
|
||||
@@ -160,7 +176,7 @@ async function plannedFiles(args) {
|
||||
let artifactCatalog = await readJsonIfPresent(args.catalogPath, null);
|
||||
if (!artifactCatalog && isRuntimeLane(args.lane)) artifactCatalog = artifactCatalogSkeleton({ args, deploy, source });
|
||||
if (!artifactCatalog) artifactCatalog = await readJson(args.catalogPath);
|
||||
const settings = ciLaneSettings(args);
|
||||
const settings = ciLaneSettings(args, deploy);
|
||||
const profiles = laneRuntimeProfiles(args);
|
||||
const migrationSources = isRuntimeLane(args.lane)
|
||||
? await Promise.all(CLOUD_CORE_MIGRATIONS.map(async (migration) => ({
|
||||
@@ -195,9 +211,9 @@ async function plannedFiles(args) {
|
||||
gitReadUrl: args.gitReadUrl,
|
||||
gitWriteUrl: args.gitWriteUrl,
|
||||
registryPrefix: args.registryPrefix,
|
||||
runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile)),
|
||||
runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile, deploy)),
|
||||
publicEndpoints,
|
||||
frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc"}`])),
|
||||
frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile, deploy)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc"}`])),
|
||||
tektonPipeline: `hwlab-ci/${settings.pipelineName}`,
|
||||
argoApplications: profiles.map((profile) => `argocd/${argoApplicationName(profile)}`)
|
||||
};
|
||||
@@ -209,21 +225,21 @@ async function plannedFiles(args) {
|
||||
};
|
||||
}
|
||||
putJson("source.json", sourceDescriptor);
|
||||
putText("README.md", readme({ source, args }));
|
||||
putText("README.md", readme({ source, args, deploy }));
|
||||
putJson("registry/registry.yaml", registryManifest());
|
||||
if (isRuntimeLane(args.lane)) putJson("devops-infra/git-mirror.yaml", devopsInfraGitMirrorManifest(args, deploy));
|
||||
putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args));
|
||||
putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args, deploy));
|
||||
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
|
||||
if (!isRuntimeLane(args.lane)) {
|
||||
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy));
|
||||
putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args));
|
||||
}
|
||||
putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args }));
|
||||
putJson("argocd/project.yaml", argoProject(args));
|
||||
for (const profile of profiles) putJson(`argocd/application-${profile}.yaml`, argoApplication(args, profile));
|
||||
putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args, deploy }));
|
||||
putJson("argocd/project.yaml", argoProject(args, deploy));
|
||||
for (const profile of profiles) putJson(`argocd/application-${profile}.yaml`, argoApplication(args, profile, deploy));
|
||||
|
||||
for (const profile of profiles) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const includeDeviceAgent = profile === "dev";
|
||||
const externalPostgres = externalPostgresConfigForLane(deploy, profile, args);
|
||||
const workbenchRedis = workbenchRuntimeRedisConfigForProfile(deploy, profile);
|
||||
@@ -234,9 +250,9 @@ async function plannedFiles(args) {
|
||||
runtimeNamespace.metadata.name = namespace;
|
||||
label(runtimeNamespace.metadata, profileLabels);
|
||||
annotate(runtimeNamespace.metadata, annotations);
|
||||
const runtimePath = runtimePathForProfile(profile);
|
||||
const runtimePath = runtimePathForProfile(profile, deploy);
|
||||
const endpoints = profileEndpoint(args, profile);
|
||||
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent, externalPostgres: Boolean(externalPostgres), workbenchRedis: Boolean(workbenchRedis), runtimeConfigMaps: runtimeConfigMaps.length > 0, externalSecrets: Boolean(runtimeSecretPlane) }));
|
||||
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent, externalPostgres: Boolean(externalPostgres), workbenchRedis: Boolean(workbenchRedis), runtimeConfigMaps: runtimeConfigMaps.length > 0, externalSecrets: Boolean(runtimeSecretPlane), deploy }));
|
||||
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
|
||||
if (!isRuntimeLane(profile)) putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
|
||||
putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile, deploy, nodeId: args.nodeId }));
|
||||
@@ -245,10 +261,10 @@ async function plannedFiles(args) {
|
||||
if (runtimeSecretPlane) putJson(`${runtimePath}/external-secrets.yaml`, runtimeSecretPlaneManifest({ config: runtimeSecretPlane, namespace, labels: profileLabels, annotations }));
|
||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, nodeId: args.nodeId, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||
if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({ profile, config: externalPostgres, source }));
|
||||
if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSources, source, image: runtimePostgresImageForProfile(deploy, profile) }));
|
||||
if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source }));
|
||||
if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({ profile, config: externalPostgres, source, deploy }));
|
||||
if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSources, source, image: runtimePostgresImageForProfile(deploy, profile), deploy }));
|
||||
if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source, deploy }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source, deploy }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/observability.yaml`, observabilityManifest({ deploy, profile, namespace, labels: profileLabels, annotations, metricsSidecarScript }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/opencode.yaml`, opencodeServerManifest({ profile, source, deploy, catalog: artifactCatalog, registryPrefix: args.registryPrefix, useDeployImages: args.useDeployImages, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, sourceRepo: args.sourceRepo }));
|
||||
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
applyRuntimeLaneDeployConfig,
|
||||
defaultEndpointForRuntimeLane,
|
||||
defaultPortForRuntimeLane,
|
||||
isNamedRuntimeLane,
|
||||
isRuntimeLane,
|
||||
isVersionRuntimeLane,
|
||||
runtimeNamespaceForRuntimeLane,
|
||||
runtimeRelativePathForRuntimeLane,
|
||||
runtimeLaneMirrorSpecs,
|
||||
runtimeLaneConfig,
|
||||
versionNameForRuntimeLane
|
||||
@@ -245,7 +249,7 @@ function parseArgs(argv) {
|
||||
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||
else throw new Error(`unknown argument ${arg}`);
|
||||
}
|
||||
if (isRuntimeLane(args.lane)) {
|
||||
if (isVersionRuntimeLane(args.lane)) {
|
||||
const versionName = versionNameForRuntimeLane(args.lane);
|
||||
const defaultLaneEndpoint = defaultEndpointForRuntimeLane(args.lane);
|
||||
if (args.sourceBranch === defaultBranch) args.sourceBranch = versionName;
|
||||
@@ -261,7 +265,7 @@ function parseArgs(argv) {
|
||||
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)) {
|
||||
if (isVersionRuntimeLane(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`);
|
||||
@@ -282,7 +286,7 @@ function usage() {
|
||||
"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",
|
||||
" --lane LANE node or a deploy.lanes runtime lane id such as v02/v03/staging; 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`,
|
||||
@@ -807,7 +811,7 @@ function isV02RemovedServiceId(serviceId) {
|
||||
|
||||
function artifactCatalogSkeleton({ args, deploy, source }) {
|
||||
const environment = artifactEnvironment(args);
|
||||
const namespace = isRuntimeLane(args.lane) ? namespaceNameForProfile(args.lane) : "hwlab-dev";
|
||||
const namespace = isRuntimeLane(args.lane) ? namespaceNameForProfile(args.lane, deploy) : "hwlab-dev";
|
||||
const tag = imageTagForSource(args, source);
|
||||
return {
|
||||
catalogVersion: "v1",
|
||||
@@ -869,13 +873,13 @@ function artifactCatalogSkeletonService({ args, deploy, source, serviceId, envir
|
||||
};
|
||||
}
|
||||
|
||||
function namespaceNameForProfile(profile) {
|
||||
if (isRuntimeLane(profile)) return `hwlab-${profile}`;
|
||||
function namespaceNameForProfile(profile, deploy = null) {
|
||||
if (isRuntimeLane(profile)) return runtimeNamespaceForRuntimeLane(deploy, profile);
|
||||
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||
}
|
||||
|
||||
function runtimePathForProfile(profile) {
|
||||
if (isRuntimeLane(profile)) return `runtime-${profile}`;
|
||||
function runtimePathForProfile(profile, deploy = null) {
|
||||
if (isRuntimeLane(profile)) return runtimeRelativePathForRuntimeLane(deploy, profile);
|
||||
return profile === "prod" ? "runtime-prod" : "runtime-dev";
|
||||
}
|
||||
|
||||
@@ -892,8 +896,8 @@ function gitopsRootForArgs(args) {
|
||||
return String(args.gitopsRoot || defaultOutDir).replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function gitopsPathForProfile(args, profile) {
|
||||
return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile)}`;
|
||||
function gitopsPathForProfile(args, profile, deploy = null) {
|
||||
return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile, deploy)}`;
|
||||
}
|
||||
|
||||
function gitopsPathFor(args, relativePath) {
|
||||
@@ -911,9 +915,9 @@ function laneRuntimeProfiles(args) {
|
||||
return isRuntimeLane(args.lane) ? [args.lane] : ["dev", "prod"];
|
||||
}
|
||||
|
||||
function laneNames(args) {
|
||||
function laneNames(args, deploy = null) {
|
||||
if (isRuntimeLane(args.lane)) {
|
||||
const namespace = namespaceNameForProfile(args.lane);
|
||||
const namespace = namespaceNameForProfile(args.lane, deploy);
|
||||
return {
|
||||
gitopsTarget: args.lane,
|
||||
pipeline: `${namespace}-ci-image-publish`,
|
||||
@@ -922,7 +926,7 @@ function laneNames(args) {
|
||||
serviceAccount: `${namespace}-tekton-runner`,
|
||||
pipelineRunPrefix: `${namespace}-ci-poll-`,
|
||||
runtimeNamespace: namespace,
|
||||
runtimePath: gitopsPathForProfile(args, args.lane),
|
||||
runtimePath: gitopsPathForProfile(args, args.lane, deploy),
|
||||
argoApplications: [`argocd/hwlab-node-${args.lane}`]
|
||||
};
|
||||
}
|
||||
@@ -1177,7 +1181,8 @@ function gitopsRootNodeId(gitopsRoot) {
|
||||
}
|
||||
|
||||
function runtimeFrpConfigForProfile(deploy, profile, args = {}) {
|
||||
const fallbackWebRemotePort = isRuntimeLane(profile) ? defaultPortForRuntimeLane(profile) : profile === "prod" ? 18666 : 17666;
|
||||
const namedRuntimeLane = isNamedRuntimeLane(profile);
|
||||
const fallbackWebRemotePort = isVersionRuntimeLane(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);
|
||||
@@ -1190,17 +1195,31 @@ function runtimeFrpConfigForProfile(deploy, profile, args = {}) {
|
||||
.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;
|
||||
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
||||
assert.ok(!namedRuntimeLane, `deploy.lanes.${profile}.frp.proxies.${endpointId}.${key} is required`);
|
||||
return fallback;
|
||||
};
|
||||
const proxyPort = (endpointId, fallback) => {
|
||||
const value = proxyByEndpoint.get(endpointId)?.remotePort;
|
||||
return Number.isInteger(value) ? value : fallback;
|
||||
const proxyPort = (endpointId, fallback, key = "remotePort") => {
|
||||
const value = proxyByEndpoint.get(endpointId)?.[key];
|
||||
if (Number.isInteger(value) && value > 0) return value;
|
||||
assert.ok(!namedRuntimeLane, `deploy.lanes.${profile}.frp.proxies.${endpointId}.${key} must be a positive integer`);
|
||||
return fallback;
|
||||
};
|
||||
if (namedRuntimeLane) {
|
||||
assert.ok(server && typeof server === "object" && !Array.isArray(server), `deploy.lanes.${profile}.frp.server is required`);
|
||||
assert.ok(typeof server.address === "string" && server.address.trim().length > 0, `deploy.lanes.${profile}.frp.server.address is required`);
|
||||
assert.ok(Number.isInteger(server.bindPort) && server.bindPort > 0, `deploy.lanes.${profile}.frp.server.bindPort must be a positive integer`);
|
||||
for (const endpointId of ["frontend", "api"]) assert.ok(proxyByEndpoint.has(endpointId), `deploy.lanes.${profile}.frp.proxies must declare endpointId ${endpointId}`);
|
||||
}
|
||||
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),
|
||||
webLocalHost: proxyValue("frontend", "localHost", null),
|
||||
edgeLocalHost: proxyValue("api", "localHost", null),
|
||||
webLocalPort: proxyPort("frontend", 8080, "localPort"),
|
||||
edgeLocalPort: proxyPort("api", 6667, "localPort"),
|
||||
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,
|
||||
@@ -1347,7 +1366,7 @@ function workbenchRuntimeRedisConfigForProfile(deploy, profile) {
|
||||
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)}`);
|
||||
assert.equal(namespace, namespaceNameForProfile(profile, deploy), `${label}.namespace must match ${namespaceNameForProfile(profile, deploy)}`);
|
||||
const serviceName = requiredConfigString(redis, "serviceName", label);
|
||||
const image = requiredConfigString(redis, "image", label);
|
||||
const port = requiredConfigPositiveInteger(redis, "port", label);
|
||||
@@ -1446,7 +1465,7 @@ function upsertV02MetricsPort(service) {
|
||||
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 namespace = namespaceNameForProfile(profile, deploy);
|
||||
const profileLabel = runtimeLabelForProfile(profile);
|
||||
const gitopsTarget = gitopsTargetForProfile(profile);
|
||||
const runtimeLane = isRuntimeLane(profile);
|
||||
@@ -1730,6 +1749,7 @@ function transformServices({ services, namespace, labels, annotations, profile =
|
||||
|
||||
function observabilityManifest({ deploy, profile, namespace, labels, annotations, metricsSidecarScript }) {
|
||||
assert.ok(isRuntimeLane(profile), `observability profile must be a runtime lane, got ${profile}`);
|
||||
const laneDisplayName = runtimeLaneConfig(deploy, profile).name ?? versionNameForRuntimeLane(profile);
|
||||
const includePrometheusOperatorResources = runtimePrometheusOperatorResourcesEnabled(deploy, profile);
|
||||
const baseLabels = {
|
||||
...labels,
|
||||
@@ -1785,7 +1805,7 @@ function observabilityManifest({ deploy, profile, namespace, labels, annotations
|
||||
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.` }
|
||||
annotations: { summary: `HWLAB ${laneDisplayName} metrics target is down`, description: `Prometheus cannot scrape a HWLAB ${laneDisplayName} metrics target.` }
|
||||
},
|
||||
{
|
||||
alert: `HWLAB${profile.toUpperCase()}ServiceHealthProbeFailed`,
|
||||
@@ -1822,7 +1842,7 @@ export {
|
||||
effectiveSecretPlaneNodeId, ensureObject, envReuseBootShForContainer, envReuseServiceIdsForLane,
|
||||
generatedPath, gitopsPathFor, gitopsPathForProfile, gitopsRootForArgs, gitopsRootNodeId,
|
||||
gitopsTargetForProfile, gitValue, imageFor, imageTagForSource,
|
||||
imageTagFromReference, isRuntimeLane, isV02RemovedServiceId, jsonManifest,
|
||||
imageTagFromReference, isNamedRuntimeLane, isRuntimeLane, isV02RemovedServiceId, jsonManifest,
|
||||
k8sLabelHash, label, laneNames, laneRuntimeProfiles, mergeEnvMaps,
|
||||
moonBridgeImage, moonBridgeSourceRef, namespaceNameForProfile, namespaceScopedHost,
|
||||
normalizeAccessControlUser, normalizeNavProfile, normalizeProbeTiming,
|
||||
|
||||
@@ -44,15 +44,16 @@ import {
|
||||
versionNameForRuntimeLane
|
||||
} from "./core.mjs";
|
||||
|
||||
function argoProject(args = { lane: "node" }) {
|
||||
function argoProject(args = { lane: "node" }, deploy = null) {
|
||||
if (isRuntimeLane(args.lane)) {
|
||||
const namespace = namespaceNameForProfile(args.lane);
|
||||
const namespace = namespaceNameForProfile(args.lane, deploy);
|
||||
const laneName = runtimeLaneConfig(deploy, args.lane).name ?? versionNameForRuntimeLane(args.lane);
|
||||
return {
|
||||
apiVersion: "argoproj.io/v1alpha1",
|
||||
kind: "AppProject",
|
||||
metadata: { name: namespace, namespace: "argocd" },
|
||||
spec: {
|
||||
description: `HWLAB ${versionNameForRuntimeLane(args.lane)} additive GitOps project on target node; DEV/PROD remain outside this lane.`,
|
||||
description: `HWLAB ${laneName} additive GitOps project on target node; DEV/PROD remain outside this lane.`,
|
||||
sourceRepos: [args.gitReadUrl],
|
||||
destinations: [{ server: "https://kubernetes.default.svc", namespace }],
|
||||
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
|
||||
@@ -77,9 +78,9 @@ function argoProject(args = { lane: "node" }) {
|
||||
};
|
||||
}
|
||||
|
||||
function argoApplication(args, profile = "dev") {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const runtimePath = runtimePathForProfile(profile);
|
||||
function argoApplication(args, profile = "dev", deploy = null) {
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const runtimePath = runtimePathForProfile(profile, deploy);
|
||||
const gitopsTarget = gitopsTargetForProfile(profile);
|
||||
const project = isRuntimeLane(profile) ? namespace : "hwlab-node";
|
||||
const repoURL = isRuntimeLane(profile) ? args.gitReadUrl : args.sourceRepo;
|
||||
@@ -93,7 +94,7 @@ function argoApplication(args, profile = "dev") {
|
||||
},
|
||||
spec: {
|
||||
project,
|
||||
source: { repoURL, targetRevision: args.gitopsBranch, path: gitopsPathForProfile(args, profile) },
|
||||
source: { repoURL, targetRevision: args.gitopsBranch, path: gitopsPathForProfile(args, profile, deploy) },
|
||||
destination: { server: "https://kubernetes.default.svc", namespace },
|
||||
syncPolicy: { automated: { prune: isRuntimeLane(profile), selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
|
||||
}
|
||||
@@ -101,7 +102,7 @@ function argoApplication(args, profile = "dev") {
|
||||
}
|
||||
|
||||
function nodeFrpcManifest({ profile = "dev", source = null, deploy = null, args = {} } = {}) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const profileLabel = runtimeLabelForProfile(profile);
|
||||
const deploymentName = isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc";
|
||||
const configName = `${deploymentName}-config`;
|
||||
@@ -131,15 +132,15 @@ ${authConfigText}
|
||||
[[proxies]]
|
||||
name = "${webProxyName}"
|
||||
type = "tcp"
|
||||
localIP = "hwlab-cloud-web.${namespace}.svc.cluster.local"
|
||||
localPort = 8080
|
||||
localIP = "${frpConfig.webLocalHost || `hwlab-cloud-web.${namespace}.svc.cluster.local`}"
|
||||
localPort = ${frpConfig.webLocalPort}
|
||||
remotePort = ${frpConfig.webRemotePort}
|
||||
|
||||
[[proxies]]
|
||||
name = "${edgeProxyName}"
|
||||
type = "tcp"
|
||||
localIP = "hwlab-edge-proxy.${namespace}.svc.cluster.local"
|
||||
localPort = 6667
|
||||
localIP = "${frpConfig.edgeLocalHost || `hwlab-edge-proxy.${namespace}.svc.cluster.local`}"
|
||||
localPort = ${frpConfig.edgeLocalPort}
|
||||
remotePort = ${frpConfig.edgeRemotePort}
|
||||
`;
|
||||
const configSha256 = createHash("sha256").update(configText).digest("hex");
|
||||
@@ -194,7 +195,7 @@ remotePort = ${frpConfig.edgeRemotePort}
|
||||
}
|
||||
|
||||
function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = defaultBranch, sourceRepo = defaultSourceRepo, gitReadUrl, deploy = null, registryPrefix = defaultRegistryPrefix, catalog = null, useDeployImages = false, metricsSidecarSha256 = null } = {}) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const bridgeServiceId = "hwlab-cloud-api";
|
||||
const runtimeLane = isRuntimeLane(profile);
|
||||
const digestPin = runtimeLane;
|
||||
@@ -642,8 +643,8 @@ function runtimePostgresImageForProfile(deploy, profile) {
|
||||
return typeof image === "string" && image.trim().length > 0 ? image.trim() : "postgres:16-alpine";
|
||||
}
|
||||
|
||||
function v02PostgresManifest({ profile = "v02", migrationSources, source, image = "postgres:16-alpine" }) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
function v02PostgresManifest({ profile = "v02", migrationSources, source, image = "postgres:16-alpine", deploy = null }) {
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const name = `${namespace}-postgres`;
|
||||
const dbName = `hwlab_${profile}`;
|
||||
const labels = {
|
||||
@@ -738,8 +739,8 @@ function externalPostgresConfigForLane(deploy, profile, args = {}) {
|
||||
return { serviceName: effective.serviceName, endpointAddress: effective.endpointAddress, port };
|
||||
}
|
||||
|
||||
function externalPostgresManifest({ profile = "v03", config, source }) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
function externalPostgresManifest({ profile = "v03", config, source, deploy = null }) {
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": config.serviceName,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
@@ -772,8 +773,8 @@ function externalPostgresManifest({ profile = "v03", config, source }) {
|
||||
};
|
||||
}
|
||||
|
||||
function v02OpenFgaManifest({ profile = "v02", source }) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
function v02OpenFgaManifest({ profile = "v02", source, deploy = null }) {
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const name = "hwlab-openfga";
|
||||
const image = "127.0.0.1:5000/hwlab/openfga:v1.17.0";
|
||||
const labels = {
|
||||
@@ -1200,7 +1201,7 @@ function opencodeConfigContent(config) {
|
||||
|
||||
function opencodeServerManifest({ profile = "v03", source, deploy = null, catalog = null, registryPrefix = defaultRegistryPrefix, useDeployImages = false, sourceBranch = defaultBranch, gitReadUrl = defaultV02GitReadUrl, sourceRepo = defaultSourceRepo }) {
|
||||
assert.ok(isRuntimeLane(profile), `opencode-server profile must be a runtime lane, got ${profile}`);
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const namespace = namespaceNameForProfile(profile, deploy);
|
||||
const name = "opencode-server";
|
||||
const helperServiceId = "hwlab-cloud-web";
|
||||
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, profile);
|
||||
|
||||
@@ -29,10 +29,10 @@ import {
|
||||
primitiveValidationTaskNames
|
||||
} from "./tekton-scripts.mjs";
|
||||
|
||||
function ciLaneSettings(argsOrLane = "node") {
|
||||
function ciLaneSettings(argsOrLane = "node", deploy = null) {
|
||||
const lane = typeof argsOrLane === "string" ? argsOrLane : argsOrLane.lane;
|
||||
if (isRuntimeLane(lane)) {
|
||||
const namespace = namespaceNameForProfile(lane);
|
||||
const namespace = namespaceNameForProfile(lane, deploy);
|
||||
return {
|
||||
lane,
|
||||
profile: lane,
|
||||
@@ -64,8 +64,8 @@ function ciLaneSettings(argsOrLane = "node") {
|
||||
};
|
||||
}
|
||||
|
||||
function tektonRbac(args = { lane: "node" }) {
|
||||
const settings = ciLaneSettings(args);
|
||||
function tektonRbac(args = { lane: "node" }, deploy = null) {
|
||||
const settings = ciLaneSettings(args, deploy);
|
||||
if (isRuntimeLane(settings.lane)) {
|
||||
const runtimeObserverName = `${settings.runtimeNamespace}-runtime-observer`;
|
||||
const pipelineRunWriterName = `${settings.runtimeNamespace}-pipelinerun-writer`;
|
||||
@@ -435,7 +435,7 @@ node scripts/ci/runtime-ready.mjs
|
||||
`;
|
||||
}
|
||||
|
||||
function runtimeReadyTask({ profile = "dev" } = {}) {
|
||||
function runtimeReadyTask({ profile = "dev", deploy = null } = {}) {
|
||||
return {
|
||||
name: "runtime-ready",
|
||||
runAfter: ["gitops-promote"],
|
||||
@@ -457,7 +457,7 @@ function runtimeReadyTask({ profile = "dev" } = {}) {
|
||||
},
|
||||
params: [
|
||||
{ name: "revision", value: "$(params.revision)" },
|
||||
{ name: "runtime-namespace", value: namespaceNameForProfile(profile) },
|
||||
{ name: "runtime-namespace", value: namespaceNameForProfile(profile, deploy) },
|
||||
{ name: "gitops-target", value: gitopsTargetForProfile(profile) },
|
||||
{ name: "argo-application", value: argoApplicationName(profile) },
|
||||
{ name: "timeout-ms", value: "$(params.runtime-ready-timeout-ms)" }
|
||||
@@ -478,12 +478,12 @@ function imagePublishTaskSet(args = { lane: "node" }, deploy = null) {
|
||||
}
|
||||
|
||||
function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
const settings = ciLaneSettings(args);
|
||||
const runtimePath = gitopsPathForProfile(args, settings.profile);
|
||||
const settings = ciLaneSettings(args, deploy);
|
||||
const runtimePath = gitopsPathForProfile(args, settings.profile, deploy);
|
||||
const preFlushRuntimeReadyTasks = isRuntimeLane(settings.lane)
|
||||
? []
|
||||
: [{
|
||||
...runtimeReadyTask({ profile: settings.profile }),
|
||||
...runtimeReadyTask({ profile: settings.profile, deploy }),
|
||||
when: [{ input: "$(tasks.gitops-promote.results.runtime-ready-required)", operator: "in", values: ["true"] }]
|
||||
}];
|
||||
return {
|
||||
@@ -607,9 +607,9 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
};
|
||||
}
|
||||
|
||||
function tektonPipelineRunTemplate({ source, args }) {
|
||||
const settings = ciLaneSettings(args);
|
||||
const runtimePath = gitopsPathForProfile(args, settings.profile);
|
||||
function tektonPipelineRunTemplate({ source, args, deploy = null }) {
|
||||
const settings = ciLaneSettings(args, deploy);
|
||||
const runtimePath = gitopsPathForProfile(args, settings.profile, deploy);
|
||||
return {
|
||||
apiVersion: "tekton.dev/v1",
|
||||
kind: "PipelineRun",
|
||||
@@ -656,7 +656,7 @@ function tektonPipelineRunTemplate({ source, args }) {
|
||||
}
|
||||
|
||||
function tektonPollerCronJob(args, deploy) {
|
||||
const settings = ciLaneSettings(args);
|
||||
const settings = ciLaneSettings(args, deploy);
|
||||
if (settings.lane === "v02") throw new Error("v02 CI/CD is manually triggered by UniDesk CLI and must not render a branch poller CronJob");
|
||||
return {
|
||||
apiVersion: "batch/v1",
|
||||
@@ -707,7 +707,7 @@ function tektonPollerCronJob(args, deploy) {
|
||||
{ name: "LANE", value: settings.lane },
|
||||
{ name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath },
|
||||
{ name: "IMAGE_TAG_MODE", value: args.imageTagMode || settings.imageTagMode },
|
||||
{ name: "RUNTIME_PATH", value: gitopsPathForProfile(args, settings.profile) },
|
||||
{ name: "RUNTIME_PATH", value: gitopsPathForProfile(args, settings.profile, deploy) },
|
||||
{ name: "GIT_URL", value: args.sourceRepo },
|
||||
{ name: "GIT_READ_URL", value: args.gitReadUrl },
|
||||
{ name: "SOURCE_BRANCH", value: args.sourceBranch },
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
applyRuntimeLaneDeployConfig,
|
||||
isNamedRuntimeLane,
|
||||
isRuntimeLane,
|
||||
isVersionRuntimeLane,
|
||||
runtimeNamespaceForRuntimeLane,
|
||||
runtimeRelativePathForRuntimeLane
|
||||
} from "./runtime-lane.ts";
|
||||
import {
|
||||
namespaceNameForProfile,
|
||||
parseArgs,
|
||||
runtimeFrpConfigForProfile,
|
||||
runtimePathForProfile,
|
||||
transformServices
|
||||
} from "./gitops-render/core.mjs";
|
||||
|
||||
const namedLaneDeploy = {
|
||||
nodes: {
|
||||
NC01: {
|
||||
gitopsRoot: "deploy/gitops/node/NC01",
|
||||
sourceRepo: "git@github.com:pikasTech/HWLAB.git"
|
||||
}
|
||||
},
|
||||
lanes: {
|
||||
production: {
|
||||
name: "Production",
|
||||
node: "NC01",
|
||||
sourceBranch: "release",
|
||||
gitopsBranch: "hwlab-production",
|
||||
gitReadUrl: "http://gitea.example/mirrors/hwlab.git",
|
||||
gitWriteUrl: "http://git-mirror-write.example/pikasTech/HWLAB.git",
|
||||
artifactCatalog: "deploy/artifact-catalog.production.json",
|
||||
runtimePath: "runtime-production",
|
||||
namespace: "hwlab-production",
|
||||
imageTagMode: "full",
|
||||
publicEndpoints: {
|
||||
frontend: "https://hwlab.example",
|
||||
api: "https://api.hwlab.example"
|
||||
},
|
||||
publicServices: {
|
||||
nodes: {
|
||||
NC01: [{
|
||||
name: "hwlab-cloud-web-public",
|
||||
selector: { "app.kubernetes.io/name": "hwlab-cloud-web" },
|
||||
port: 80,
|
||||
targetPort: 8080,
|
||||
nodePort: 32010
|
||||
}]
|
||||
}
|
||||
},
|
||||
frp: {
|
||||
server: { address: "203.0.113.10", bindPort: 22000 },
|
||||
proxies: [
|
||||
{ name: "production-web", endpointId: "frontend", localHost: "web.hwlab-production.svc.cluster.local", localPort: 8080, remotePort: 22090 },
|
||||
{ name: "production-api", endpointId: "api", localHost: "api.hwlab-production.svc.cluster.local", localPort: 6667, remotePort: 22088 }
|
||||
]
|
||||
},
|
||||
envReuseServices: ["hwlab-cloud-web"],
|
||||
serviceDeclarations: {
|
||||
"hwlab-cloud-web": { healthPort: 8080, healthPath: "/health/live" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const defaultArgs = {
|
||||
lane: "production",
|
||||
nodeId: "node",
|
||||
outDir: "deploy/gitops/node",
|
||||
gitopsRoot: "deploy/gitops/node",
|
||||
sourceBranch: "node",
|
||||
gitopsBranch: "node-gitops",
|
||||
catalogPath: "deploy/artifact-catalog.dev.json",
|
||||
imageTagMode: "full",
|
||||
runtimeEndpoint: "http://74.48.78.17:17667",
|
||||
webEndpoint: "http://74.48.78.17:17666",
|
||||
sourceRepo: "git@github.com:pikasTech/HWLAB.git"
|
||||
};
|
||||
|
||||
assert.equal(isRuntimeLane("production"), true);
|
||||
assert.equal(isNamedRuntimeLane("production"), true);
|
||||
assert.equal(isVersionRuntimeLane("production"), false);
|
||||
assert.equal(isVersionRuntimeLane("v03"), true);
|
||||
assert.equal(parseArgs(["--lane", "production", "--no-write"]).lane, "production");
|
||||
|
||||
const resolved = applyRuntimeLaneDeployConfig(defaultArgs, namedLaneDeploy);
|
||||
assert.deepEqual({
|
||||
nodeId: resolved.nodeId,
|
||||
gitopsRoot: resolved.gitopsRoot,
|
||||
sourceBranch: resolved.sourceBranch,
|
||||
gitopsBranch: resolved.gitopsBranch,
|
||||
gitReadUrl: resolved.gitReadUrl,
|
||||
gitWriteUrl: resolved.gitWriteUrl,
|
||||
catalogPath: resolved.catalogPath,
|
||||
runtimeEndpoint: resolved.runtimeEndpoint,
|
||||
webEndpoint: resolved.webEndpoint
|
||||
}, {
|
||||
nodeId: "NC01",
|
||||
gitopsRoot: "deploy/gitops/node/NC01",
|
||||
sourceBranch: "release",
|
||||
gitopsBranch: "hwlab-production",
|
||||
gitReadUrl: "http://gitea.example/mirrors/hwlab.git",
|
||||
gitWriteUrl: "http://git-mirror-write.example/pikasTech/HWLAB.git",
|
||||
catalogPath: "deploy/artifact-catalog.production.json",
|
||||
runtimeEndpoint: "https://api.hwlab.example",
|
||||
webEndpoint: "https://hwlab.example"
|
||||
});
|
||||
assert.equal(runtimeNamespaceForRuntimeLane(namedLaneDeploy, "production"), "hwlab-production");
|
||||
assert.equal(runtimeRelativePathForRuntimeLane(namedLaneDeploy, "production"), "runtime-production");
|
||||
assert.equal(namespaceNameForProfile("production", namedLaneDeploy), "hwlab-production");
|
||||
assert.equal(runtimePathForProfile("production", namedLaneDeploy), "runtime-production");
|
||||
|
||||
const services = transformServices({
|
||||
services: { apiVersion: "v1", kind: "List", items: [] },
|
||||
namespace: "hwlab-production",
|
||||
labels: {},
|
||||
annotations: {},
|
||||
profile: "production",
|
||||
deploy: namedLaneDeploy,
|
||||
nodeId: "NC01"
|
||||
});
|
||||
assert.deepEqual(services.items[0].spec.ports[0], { name: "http", port: 80, targetPort: 8080, nodePort: 32010 });
|
||||
|
||||
const frp = runtimeFrpConfigForProfile(namedLaneDeploy, "production", { nodeId: "NC01" });
|
||||
assert.deepEqual({
|
||||
serverAddr: frp.serverAddr,
|
||||
serverPort: frp.serverPort,
|
||||
webLocalHost: frp.webLocalHost,
|
||||
webLocalPort: frp.webLocalPort,
|
||||
webRemotePort: frp.webRemotePort,
|
||||
edgeLocalHost: frp.edgeLocalHost,
|
||||
edgeLocalPort: frp.edgeLocalPort,
|
||||
edgeRemotePort: frp.edgeRemotePort
|
||||
}, {
|
||||
serverAddr: "203.0.113.10",
|
||||
serverPort: 22000,
|
||||
webLocalHost: "web.hwlab-production.svc.cluster.local",
|
||||
webLocalPort: 8080,
|
||||
webRemotePort: 22090,
|
||||
edgeLocalHost: "api.hwlab-production.svc.cluster.local",
|
||||
edgeLocalPort: 6667,
|
||||
edgeRemotePort: 22088
|
||||
});
|
||||
|
||||
const incompleteDeploy = structuredClone(namedLaneDeploy);
|
||||
delete incompleteDeploy.lanes.production.namespace;
|
||||
assert.throws(
|
||||
() => applyRuntimeLaneDeployConfig(defaultArgs, incompleteDeploy),
|
||||
/deploy\.lanes\.production\.namespace must be a non-empty string/u
|
||||
);
|
||||
|
||||
const versionLaneDeploy = {
|
||||
nodes: { G14: { gitopsRoot: "deploy/gitops/node/G14", publicHost: "74.48.78.17" } },
|
||||
lanes: { v03: { node: "G14" } }
|
||||
};
|
||||
const versionResolved = applyRuntimeLaneDeployConfig({ ...defaultArgs, lane: "v03" }, versionLaneDeploy);
|
||||
assert.equal(versionResolved.sourceBranch, "v0.3");
|
||||
assert.equal(versionResolved.gitopsBranch, "v0.3-gitops");
|
||||
assert.equal(versionResolved.catalogPath, "deploy/artifact-catalog.v03.json");
|
||||
assert.equal(runtimeNamespaceForRuntimeLane(versionLaneDeploy, "v03"), "hwlab-v03");
|
||||
assert.equal(runtimeRelativePathForRuntimeLane(versionLaneDeploy, "v03"), "runtime-v03");
|
||||
|
||||
console.log(JSON.stringify({ ok: true, tests: 18, namedLane: "production", versionLane: "v03" }));
|
||||
+76
-15
@@ -26,11 +26,15 @@ export type GitOpsRenderArgs = {
|
||||
};
|
||||
|
||||
export type RuntimeLaneConfig = {
|
||||
name?: string;
|
||||
node?: string;
|
||||
sourceBranch?: string;
|
||||
gitopsBranch?: string;
|
||||
gitReadUrl?: string;
|
||||
gitWriteUrl?: string;
|
||||
artifactCatalog?: string;
|
||||
runtimePath?: string;
|
||||
namespace?: string;
|
||||
imageTagMode?: string;
|
||||
endpoint?: string;
|
||||
externalPostgres?: {
|
||||
@@ -75,11 +79,20 @@ const defaultGitopsRoot = "deploy/gitops/node";
|
||||
const defaultPublicHost = "74.48.78.17";
|
||||
|
||||
export function isRuntimeLane(lane: unknown): lane is string {
|
||||
const value = String(lane ?? "");
|
||||
return !["node", "dev", "prod"].includes(value) && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(value);
|
||||
}
|
||||
|
||||
export function isVersionRuntimeLane(lane: unknown): lane is string {
|
||||
return /^v[0-9]{2,}$/u.test(String(lane ?? ""));
|
||||
}
|
||||
|
||||
export function isNamedRuntimeLane(lane: unknown): lane is string {
|
||||
return isRuntimeLane(lane) && !isVersionRuntimeLane(lane);
|
||||
}
|
||||
|
||||
export function runtimeLaneMinor(lane: string): number {
|
||||
invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
|
||||
invariant(isVersionRuntimeLane(lane), `version runtime lane must look like v02/v03, got ${lane}`);
|
||||
return Number.parseInt(lane.slice(1), 10);
|
||||
}
|
||||
|
||||
@@ -88,12 +101,12 @@ export function versionNameForRuntimeLane(lane: string): string {
|
||||
}
|
||||
|
||||
export function namespaceNameForRuntimeLane(lane: string): string {
|
||||
invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
|
||||
invariant(isVersionRuntimeLane(lane), `version runtime lane must look like v02/v03, got ${lane}`);
|
||||
return `hwlab-${lane}`;
|
||||
}
|
||||
|
||||
export function runtimePathForRuntimeLane(lane: string): string {
|
||||
invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
|
||||
invariant(isVersionRuntimeLane(lane), `version runtime lane must look like v02/v03, got ${lane}`);
|
||||
return `runtime-${lane}`;
|
||||
}
|
||||
|
||||
@@ -133,9 +146,10 @@ export function defaultEndpointForRuntimeLane(lane: string, options: { host?: st
|
||||
}
|
||||
|
||||
export function runtimeLaneConfig(deploy: DeployConfig, lane: string): RuntimeLaneConfig {
|
||||
invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
|
||||
invariant(isRuntimeLane(lane), `runtime lane must be a stable lowercase id, got ${lane}`);
|
||||
const config = deploy.lanes?.[lane];
|
||||
invariant(isObject(config), `deploy.lanes.${lane} is required`);
|
||||
if (isNamedRuntimeLane(lane)) validateNamedRuntimeLaneConfig(config, lane);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -145,13 +159,14 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep
|
||||
const laneConfig = runtimeLaneConfig(deploy, args.lane);
|
||||
const configuredNodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults);
|
||||
const rootNodeId = nodeIdFromGitopsRoot(result.gitopsRoot);
|
||||
const nodeId = result.nodeId && result.nodeId !== defaultNodeId ? result.nodeId : rootNodeId ?? configuredNodeId;
|
||||
const requestedNodeId = result.nodeId && result.nodeId !== defaultNodeId ? result.nodeId : rootNodeId ?? configuredNodeId;
|
||||
const nodeId = isNamedRuntimeLane(args.lane) ? configuredNodeId : requestedNodeId;
|
||||
const node = nodeConfig(deploy, nodeId);
|
||||
const versionName = versionNameForRuntimeLane(args.lane);
|
||||
const versionName = isVersionRuntimeLane(args.lane) ? versionNameForRuntimeLane(args.lane) : null;
|
||||
const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {};
|
||||
if (node.publicHost) defaultEndpointOptions.host = node.publicHost;
|
||||
if (defaults.defaultV02RuntimeEndpoint) defaultEndpointOptions.v02Endpoint = defaults.defaultV02RuntimeEndpoint;
|
||||
const defaultEndpoint = defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions);
|
||||
const defaultEndpoint = versionName ? defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions) : null;
|
||||
const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint;
|
||||
const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
|
||||
const configuredRuntimeEndpoint = laneConfig.publicEndpoints?.api ?? configuredEndpoint;
|
||||
@@ -163,14 +178,32 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep
|
||||
const defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git";
|
||||
const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`;
|
||||
|
||||
if (isNamedRuntimeLane(args.lane)) {
|
||||
const configuredSourceRepo = laneConfig.sourceRepo ?? node.sourceRepo;
|
||||
invariant(typeof node.gitopsRoot === "string" && node.gitopsRoot.trim().length > 0, `deploy.nodes.${nodeId}.gitopsRoot is required for named runtime lane ${args.lane}`);
|
||||
requiredString(configuredSourceRepo, `deploy.lanes.${args.lane}.sourceRepo or deploy.nodes.${nodeId}.sourceRepo`);
|
||||
result.nodeId = configuredNodeId;
|
||||
result.gitopsRoot = normalizeRepoPath(node.gitopsRoot);
|
||||
result.sourceBranch = requiredString(laneConfig.sourceBranch, `deploy.lanes.${args.lane}.sourceBranch`);
|
||||
result.gitopsBranch = requiredString(laneConfig.gitopsBranch, `deploy.lanes.${args.lane}.gitopsBranch`);
|
||||
result.catalogPath = normalizeRepoPath(requiredString(laneConfig.artifactCatalog, `deploy.lanes.${args.lane}.artifactCatalog`));
|
||||
result.imageTagMode = requiredString(laneConfig.imageTagMode, `deploy.lanes.${args.lane}.imageTagMode`);
|
||||
result.runtimeEndpoint = requiredString(laneConfig.publicEndpoints?.api, `deploy.lanes.${args.lane}.publicEndpoints.api`);
|
||||
result.webEndpoint = requiredString(laneConfig.publicEndpoints?.frontend, `deploy.lanes.${args.lane}.publicEndpoints.frontend`);
|
||||
result.sourceRepo = configuredSourceRepo as string;
|
||||
result.gitReadUrl = requiredString(laneConfig.gitReadUrl, `deploy.lanes.${args.lane}.gitReadUrl`);
|
||||
result.gitWriteUrl = requiredString(laneConfig.gitWriteUrl, `deploy.lanes.${args.lane}.gitWriteUrl`);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!result.nodeId || result.nodeId === defaultNodeId) result.nodeId = nodeId;
|
||||
if (!result.gitopsRoot || result.gitopsRoot === (defaults.defaultGitopsRoot ?? defaultGitopsRoot)) result.gitopsRoot = gitopsRootForNode(deploy, nodeId, defaults);
|
||||
if (result.sourceBranch === defaultBranch || result.sourceBranch === versionName) result.sourceBranch = laneConfig.sourceBranch ?? versionName;
|
||||
if (result.gitopsBranch === defaultGitopsBranch || result.gitopsBranch === `${versionName}-gitops`) result.gitopsBranch = laneConfig.gitopsBranch ?? `${versionName}-gitops`;
|
||||
if (result.sourceBranch === defaultBranch || result.sourceBranch === versionName) result.sourceBranch = laneConfig.sourceBranch ?? versionName ?? result.sourceBranch;
|
||||
if (result.gitopsBranch === defaultGitopsBranch || result.gitopsBranch === `${versionName}-gitops`) result.gitopsBranch = laneConfig.gitopsBranch ?? (versionName ? `${versionName}-gitops` : result.gitopsBranch);
|
||||
if (result.catalogPath === defaultCatalogPath || result.catalogPath === defaultCatalog) result.catalogPath = laneConfig.artifactCatalog ?? defaultCatalog;
|
||||
if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode;
|
||||
if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint;
|
||||
if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint;
|
||||
if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint ?? result.runtimeEndpoint;
|
||||
if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint ?? result.webEndpoint;
|
||||
if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo;
|
||||
return result;
|
||||
}
|
||||
@@ -185,20 +218,48 @@ function nodeIdFromGitopsRoot(gitopsRoot: unknown): string | undefined {
|
||||
export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] {
|
||||
return Object.entries(deploy.lanes ?? {})
|
||||
.filter(([lane, config]) => isRuntimeLane(lane) && isObject(config))
|
||||
.sort(([left], [right]) => runtimeLaneMinor(left) - runtimeLaneMinor(right))
|
||||
.sort(([left], [right]) => left.localeCompare(right, "en"))
|
||||
.map(([lane, config]) => {
|
||||
const laneConfig = config as RuntimeLaneConfig;
|
||||
const versionName = versionNameForRuntimeLane(lane);
|
||||
const versionName = isVersionRuntimeLane(lane) ? versionNameForRuntimeLane(lane) : null;
|
||||
return {
|
||||
lane,
|
||||
sourceBranch: laneConfig.sourceBranch ?? versionName,
|
||||
gitopsBranch: laneConfig.gitopsBranch ?? `${versionName}-gitops`,
|
||||
sourceBranch: laneConfig.sourceBranch ?? versionName ?? requiredString(laneConfig.sourceBranch, `deploy.lanes.${lane}.sourceBranch`),
|
||||
gitopsBranch: laneConfig.gitopsBranch ?? (versionName ? `${versionName}-gitops` : requiredString(laneConfig.gitopsBranch, `deploy.lanes.${lane}.gitopsBranch`)),
|
||||
artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`,
|
||||
runtimePath: runtimeGitopsPathForRuntimeLane(deploy, lane, defaults),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function runtimeNamespaceForRuntimeLane(deploy: DeployConfig, lane: string): string {
|
||||
const laneConfig = runtimeLaneConfig(deploy, lane);
|
||||
return laneConfig.namespace ?? namespaceNameForRuntimeLane(lane);
|
||||
}
|
||||
|
||||
export function runtimeRelativePathForRuntimeLane(deploy: DeployConfig, lane: string): string {
|
||||
const laneConfig = runtimeLaneConfig(deploy, lane);
|
||||
return normalizeRepoPath(laneConfig.runtimePath ?? runtimePathForRuntimeLane(lane));
|
||||
}
|
||||
|
||||
function validateNamedRuntimeLaneConfig(config: RuntimeLaneConfig, lane: string): void {
|
||||
const label = `deploy.lanes.${lane}`;
|
||||
for (const key of ["name", "node", "sourceBranch", "gitopsBranch", "gitReadUrl", "gitWriteUrl", "artifactCatalog", "runtimePath", "namespace", "imageTagMode"] as const) {
|
||||
requiredString(config[key], `${label}.${key}`);
|
||||
}
|
||||
invariant(config.imageTagMode === "short" || config.imageTagMode === "full", `${label}.imageTagMode must be short or full`);
|
||||
invariant(isObject(config.publicEndpoints), `${label}.publicEndpoints is required`);
|
||||
requiredString(config.publicEndpoints.frontend, `${label}.publicEndpoints.frontend`);
|
||||
requiredString(config.publicEndpoints.api, `${label}.publicEndpoints.api`);
|
||||
invariant(Array.isArray((config as Record<string, unknown>).envReuseServices) && ((config as Record<string, unknown>).envReuseServices as unknown[]).length > 0, `${label}.envReuseServices must not be empty`);
|
||||
invariant(isObject((config as Record<string, unknown>).serviceDeclarations), `${label}.serviceDeclarations is required`);
|
||||
}
|
||||
|
||||
function requiredString(value: unknown, label: string): string {
|
||||
invariant(typeof value === "string" && value.trim().length > 0, `${label} must be a non-empty string`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function normalizeRepoPath(value: string): string {
|
||||
return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user