Add HWLAB v0.3 lane expansion config
This commit is contained in:
@@ -89,6 +89,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
|
||||
## 规格
|
||||
|
||||
- 规格文档是微服务、稳定外部服务、短连接 CLI 和系统能力的权威出处;代码开发和测试代码编写必须先对齐对应 `docs/reference/spec-*.md`,再修改实现或测试。
|
||||
- 平行 runtime lane 扩容规格、`v0.3` 之后 `v0.4`/`v0.5`/`v0.6` 只改 `deploy/deploy.yaml` 配置并通过 repo-owned CLI 扩容的规则:[docs/reference/spec-lane-expansion.md](docs/reference/spec-lane-expansion.md)。
|
||||
- v0.2 服务规格总览、保留服务清单、稳定外部服务边界和废弃范围:[docs/reference/spec-v02-services.md](docs/reference/spec-v02-services.md);`hwlab-gateway-simu`、`hwlab-box-simu` 和 `hwlab-patch-panel` 已废弃,不再保留 spec。
|
||||
- v0.2 登录与鉴权规格、Keycloak OIDC、Web session 和 CLI API key:[docs/reference/spec-v02-auth.md](docs/reference/spec-v02-auth.md)。
|
||||
- v0.2 用户和权限管理规格、code agent session 归属和工具能力授权:[docs/reference/spec-user-access.md](docs/reference/spec-user-access.md)。
|
||||
@@ -148,6 +149,8 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
|
||||
- G14 artifact build helper:`node scripts/g14-artifact-publish.mjs --publish ...`,只能由 G14 Tekton task 携带 CI artifact identity 调用;人工发布走 G14 poller/GitOps,不走 legacy CLI CD。
|
||||
- G14 monorepo 组件计划:`node scripts/g14-ci-plan.mjs --base-ref <ref> --target-ref <ref> --pretty`,只读分析 affected/reused services,基于内建 service-path component model 与 `deploy/deploy.yaml`;细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。
|
||||
- G14 GitOps 渲染:`npm run g14:gitops:render`;source 分支不再运行生成物 drift check,发布态由 Tekton 写入 `G14-gitops`。
|
||||
- 平行 lane 配置扩容:`npm run lane:expand -- configure --lane v03 --from v02 --write`,只写 `deploy/deploy.yaml` 的新 lane 声明,后续 `v04`/`v05`/`v06` 复用同一入口。
|
||||
- GitOps lane 严格 TS 检查:`npm run gitops:ts:check`;新拆 GitOps 模块直接进入 `scripts/src/*.ts` 和 `tsconfig.gitops.json`,不要继续堆进单个 `.mjs`。
|
||||
- DEV 依赖 runtime base 构建:`npm run dev-runtime-base:build`
|
||||
- Legacy D601 DEV CD:旧脚本入口已删除;事故回放只读历史 issue/commit,不恢复旧命令。
|
||||
- v0.2 WEB 等价短连接 CLI:在 `G14:/root/hwlab-v02` 或当前 v0.2 worktree 直接运行 `bun tools/hwlab-cli/bin/hwlab-cli.ts client ... --base-url http://74.48.78.17:19666`,默认走 Cloud Web 同源 API;细则见 [docs/reference/spec-v02-hwlab-cli.md](docs/reference/spec-v02-hwlab-cli.md)。
|
||||
|
||||
@@ -308,21 +308,21 @@
|
||||
},
|
||||
"lanes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"v02": {
|
||||
"$ref": "#/$defs/v02Lane"
|
||||
"patternProperties": {
|
||||
"^v[0-9]{2,}$": {
|
||||
"$ref": "#/$defs/runtimeLane"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"v02Lane": {
|
||||
"runtimeLane": {
|
||||
"type": "object",
|
||||
"required": ["name", "sourceBranch", "gitopsBranch", "namespace", "envReuseServices", "bootScripts", "serviceDeclarations", "envRecipe", "bootConfig"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "const": "v0.2" },
|
||||
"sourceBranch": { "type": "string", "const": "v0.2" },
|
||||
"gitopsBranch": { "type": "string", "const": "v0.2-gitops" },
|
||||
"namespace": { "type": "string", "const": "hwlab-v02" },
|
||||
"name": { "type": "string", "pattern": "^v0\\.[0-9]+$" },
|
||||
"sourceBranch": { "type": "string", "pattern": "^v0\\.[0-9]+$" },
|
||||
"gitopsBranch": { "type": "string", "pattern": "^v0\\.[0-9]+-gitops$" },
|
||||
"namespace": { "type": "string", "pattern": "^hwlab-v[0-9]{2,}$" },
|
||||
"endpoint": { "type": "string" },
|
||||
"publicEndpoints": {
|
||||
"type": "object",
|
||||
|
||||
@@ -216,6 +216,142 @@ lanes:
|
||||
HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: agentrun-v01
|
||||
HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE: agentrun-v01
|
||||
HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git
|
||||
v03:
|
||||
name: v0.3
|
||||
sourceBranch: v0.3
|
||||
gitopsBranch: v0.3-gitops
|
||||
namespace: hwlab-v03
|
||||
endpoint: https://hwlab-v03.74-48-78-17.nip.io
|
||||
publicEndpoints:
|
||||
frontend: https://hwlab-v03.74-48-78-17.nip.io
|
||||
api: https://hwlab-v03.74-48-78-17.nip.io
|
||||
legacyFrontend: http://74.48.78.17:19766
|
||||
legacyApi: http://74.48.78.17:19767
|
||||
artifactCatalog: deploy/artifact-catalog.v03.json
|
||||
runtimePath: deploy/gitops/g14/runtime-v03
|
||||
imageTagMode: full
|
||||
envReuseServices:
|
||||
- hwlab-cloud-api
|
||||
- hwlab-cloud-web
|
||||
- hwlab-gateway
|
||||
- hwlab-edge-proxy
|
||||
- hwlab-agent-skills
|
||||
bootScripts:
|
||||
hwlab-cloud-api: deploy/runtime/boot/hwlab-cloud-api.sh
|
||||
hwlab-cloud-web: deploy/runtime/boot/hwlab-cloud-web.sh
|
||||
hwlab-gateway: deploy/runtime/boot/hwlab-gateway.sh
|
||||
hwlab-edge-proxy: deploy/runtime/boot/hwlab-edge-proxy.sh
|
||||
hwlab-agent-skills: deploy/runtime/boot/hwlab-agent-skills.sh
|
||||
serviceDeclarations:
|
||||
hwlab-cloud-api:
|
||||
runtimeKind: bun-command
|
||||
entrypoint: cmd/hwlab-cloud-api/main.ts
|
||||
artifactKind: bun-command
|
||||
healthPath: /health/live
|
||||
healthPort: 6667
|
||||
componentPaths:
|
||||
- cmd/hwlab-cloud-api/
|
||||
- cmd/hwlab-codex-api-responses-forwarder/
|
||||
- cmd/hwlab-deepseek-responses-bridge/
|
||||
- deploy/runtime/boot/hwlab-codex-api-responses-forwarder.sh
|
||||
- deploy/runtime/boot/hwlab-deepseek-responses-bridge.sh
|
||||
- internal/cloud/
|
||||
- internal/db/
|
||||
- internal/audit/
|
||||
- internal/agent/agentrun-dispatch.mjs
|
||||
- internal/agent/prompts/
|
||||
- skills/hwlab-agent-runtime/
|
||||
env: {}
|
||||
observable: true
|
||||
hwlab-cloud-web:
|
||||
runtimeKind: cloud-web
|
||||
entrypoint: web/hwlab-cloud-web/index.html
|
||||
artifactKind: cloud-web
|
||||
healthPath: /health/live
|
||||
healthPort: 8080
|
||||
componentPaths:
|
||||
- web/hwlab-cloud-web/
|
||||
- internal/dev-entrypoint/cloud-web-runtime.mjs
|
||||
- internal/dev-entrypoint/cloud-web-proxy.mjs
|
||||
- internal/dev-entrypoint/cloud-web-routes.mjs
|
||||
- tools/src/hwlab-cli/trace-renderer.ts
|
||||
env: {}
|
||||
observable: true
|
||||
hwlab-gateway:
|
||||
runtimeKind: bun-command
|
||||
entrypoint: cmd/hwlab-gateway/main.ts
|
||||
artifactKind: bun-command
|
||||
healthPath: /health/live
|
||||
healthPort: 7001
|
||||
componentPaths:
|
||||
- cmd/hwlab-gateway/
|
||||
- internal/sim/
|
||||
env: {}
|
||||
observable: false
|
||||
hwlab-edge-proxy:
|
||||
runtimeKind: bun-command
|
||||
entrypoint: cmd/hwlab-edge-proxy/main.ts
|
||||
artifactKind: bun-command
|
||||
healthPath: /health
|
||||
healthPort: 6667
|
||||
componentPaths:
|
||||
- cmd/hwlab-edge-proxy/
|
||||
- internal/dev-entrypoint/http.mjs
|
||||
env: {}
|
||||
observable: true
|
||||
hwlab-agent-skills:
|
||||
runtimeKind: skills-bundle
|
||||
entrypoint: skills/hwlab-agent-runtime/SKILL.md
|
||||
artifactKind: skills-bundle
|
||||
healthPath: /health/live
|
||||
healthPort: 7430
|
||||
componentPaths:
|
||||
- skills/
|
||||
env: {}
|
||||
observable: true
|
||||
envRecipe:
|
||||
osPackages:
|
||||
- ca-certificates
|
||||
- git
|
||||
bunVersion: 1.3.13
|
||||
launcherPath: deploy/runtime/launcher/hwlab-env-reuse-launcher.ts
|
||||
runtimeNodeModulesPath: /opt/hwlab-env/node_modules
|
||||
additionalEnvPaths:
|
||||
- internal/dev-entrypoint/artifact-runtime.mjs
|
||||
- scripts/artifact-publish.mjs
|
||||
- scripts/g14-artifact-publish.mjs
|
||||
- scripts/src/dev-artifact-services.mjs
|
||||
hwpodAliases:
|
||||
- command: hwpod
|
||||
script: tools/hwpod-cli.ts
|
||||
- command: hwpod-ctl
|
||||
script: tools/hwpod-ctl.ts
|
||||
- command: hwpod-compiler
|
||||
script: tools/hwpod-compiler-cli.ts
|
||||
bootConfig:
|
||||
template: default
|
||||
overrides: {}
|
||||
services:
|
||||
- serviceId: hwlab-cloud-api
|
||||
env:
|
||||
HWLAB_CLOUD_DB_URL: secretRef:hwlab-cloud-api-v03-db/database-url
|
||||
HWLAB_CLOUD_DB_CONTRACT: v03-redacted-presence-only
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1"
|
||||
HWLAB_BOOTSTRAP_ADMIN_ID: usr_v03_admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME: HWLAB v0.3 Admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH: secretRef:hwlab-v03-bootstrap-admin/password-hash
|
||||
HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID: key_master_server_admin
|
||||
HWLAB_BOOTSTRAP_ADMIN_API_KEY: secretRef:hwlab-v03-master-server-admin-api-key/api-key
|
||||
HWLAB_KEYCLOAK_ISSUER: https://auth.74-48-78-17.nip.io/realms/hwlab
|
||||
HWLAB_KEYCLOAK_CLIENT_ID: hwlab-cloud-web
|
||||
HWLAB_KEYCLOAK_CLIENT_SECRET: secretRef:hwlab-cloud-web-client/client-secret
|
||||
HWLAB_CODE_AGENT_ADAPTER: agentrun-v01
|
||||
AGENTRUN_MGR_URL: http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080
|
||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: G14
|
||||
HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: agentrun-v01
|
||||
HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE: agentrun-v01
|
||||
HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git
|
||||
services:
|
||||
- serviceId: hwlab-cloud-api
|
||||
namespace: hwlab-dev
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# HWLAB 平行 Runtime Lane 扩容规格
|
||||
|
||||
本文是 HWLAB `v0.x` 平行 runtime lane 的长期扩容规格。`v0.3` 之后的 `v0.4`、`v0.5`、`v0.6` 等扩容必须沿用同一模式:人工只维护 `deploy/deploy.yaml` 中的 lane 声明,然后通过 repo-owned CLI 生成或应用扩容动作;不得再为每个新 lane 复制多处代码、手写 GitOps YAML、手动拼 namespace/Secret/FRP/Tekton/Argo 对象,或恢复 D601/G14 旧兼容路径。
|
||||
|
||||
## 标准入口
|
||||
|
||||
新增 lane 的配置入口是:
|
||||
|
||||
```bash
|
||||
node scripts/hwlab-lane-expand.mjs configure --lane v03 --from v02 --write
|
||||
```
|
||||
|
||||
该命令只写入 `deploy/deploy.yaml` 的 `lanes.<lane>` 配置,并按 lane id 推导以下稳定映射:
|
||||
|
||||
| 输入 | 派生结果 |
|
||||
|---|---|
|
||||
| `v03` | source branch `v0.3` |
|
||||
| `v03` | GitOps branch `v0.3-gitops` |
|
||||
| `v03` | namespace `hwlab-v03` |
|
||||
| `v03` | runtime path `deploy/gitops/g14/runtime-v03` |
|
||||
| `v03` | artifact catalog `deploy/artifact-catalog.v03.json` |
|
||||
| `v03` | Argo Application `hwlab-g14-v03` |
|
||||
| `v03` | Tekton Pipeline `hwlab-v03-ci-image-publish` |
|
||||
| `v03` | default legacy FRP ports `19766/19767` |
|
||||
|
||||
后续 lane 按同一公式递增,默认端口为 `19466 + minor * 100` 和后一位 API 端口;如端口已被占用,必须在 CLI 参数中显式给出 `--web-port` / `--api-port`,并把结果写回 `deploy/deploy.yaml`,不要在 render、Tekton 或 FRP 文件里手改。
|
||||
|
||||
## 单一配置点
|
||||
|
||||
`deploy/deploy.yaml` 是平行 runtime lane 的人写配置源。每个 `lanes.<lane>` 必须包含:
|
||||
|
||||
- `name`、`sourceBranch`、`gitopsBranch`、`namespace`、`artifactCatalog`、`runtimePath` 和公网 endpoint 映射。
|
||||
- `envReuseServices`、`bootScripts`、`serviceDeclarations`、`envRecipe`、`bootConfig` 和 per-service runtime env。
|
||||
- lane 独立 SecretRef 名称,例如 `hwlab-cloud-api-v03-db`、`hwlab-v03-bootstrap-admin`、`hwlab-v03-master-server-admin-api-key`。
|
||||
|
||||
新增服务、调整组件边界、调整 env image 配方或变更 HWPOD alias,也只允许先改 lane 配置和 schema;planner、renderer、artifact helper 和受控发布 CLI 必须读取 `deploy.lanes.<lane>`,不得新增 `if lane === "v04"` 这类 per-lane 分支。
|
||||
|
||||
## GitOps 模块化与严格 TS
|
||||
|
||||
`v0.3` 起,GitOps 扩容逻辑不得继续堆进单个 `scripts/g14-gitops-render.mjs`。该文件只保留历史入口兼容、参数解析和高层编排;新增或抽出的 GitOps 逻辑必须进入 `scripts/src/` 下的领域模块,并优先使用 `.ts`。
|
||||
|
||||
第一批严格 TS 模块是 `scripts/src/g14-gitops-lane.ts`,负责 runtime lane 的命名、端口、endpoint、branch、catalog 和 `deploy.lanes.<lane>` 回填。该模块由 `tsconfig.gitops.json` 以 `strict`、`exactOptionalPropertyTypes`、`noUncheckedIndexedAccess` 等选项检查;验证入口是:
|
||||
|
||||
```bash
|
||||
npm run gitops:ts:check
|
||||
```
|
||||
|
||||
后续从 renderer、Tekton、Argo、FRP、Git mirror 或 artifact helper 继续拆出的模块,必须直接纳入该严格 TS 检查范围;不要先拆成 `.mjs` 再另开长期迁移债。`scripts/g14-gitops-render.mjs` 因为导入 TS 模块,实际执行必须走 Bun 入口:
|
||||
|
||||
```bash
|
||||
npm run g14:gitops:render -- --lane v03
|
||||
```
|
||||
|
||||
生成的 Tekton 任务和 control-plane reconciler 也必须使用 `node scripts/run-bun.mjs scripts/g14-gitops-render.mjs ...`,不得再用裸 `node scripts/g14-gitops-render.mjs ...`。
|
||||
|
||||
## 废弃路径
|
||||
|
||||
`G14` 和 D601 旧分支/runtime 只保留归档或事故对照价值,不再是新 HWLAB runtime lane 的 CI/CD 支持对象。`G14` 的归档分支名是 `v0.1`;CI/CD 不再为了 `v0.1` 保持 namespace、Pipeline、GitOps branch、poller、reconciler 或 FRP 兼容。若旧对象仍存在,应通过受控 CLI 分阶段退役,不能作为 v0.3+ 发布证据。
|
||||
|
||||
## 验收边界
|
||||
|
||||
一个新 lane 只有同时满足以下条件才算扩容完成:
|
||||
|
||||
- 远端 source branch 和 GitOps branch 存在,并与 `deploy.lanes.<lane>` 声明一致。
|
||||
- G14 k3s 中存在独立 namespace、Tekton Pipeline/RBAC、Argo AppProject/Application 和 runtime desired state。
|
||||
- runtime namespace、SecretRef、PVC、ServiceAccount、FRP proxy、artifact catalog 和 observability 对象均使用 lane 独立命名。
|
||||
- `control-plane trigger-current --lane <lane>` 能创建 commit-pinned PipelineRun,promotion 只写入对应 GitOps branch/path。
|
||||
- 公网 Web/API health 命中该 lane 的 namespace 和 revision;不得用 DEV/PROD、D601 或 `v0.1` health 作为通过证据。
|
||||
|
||||
## 当前状态
|
||||
|
||||
`v0.2` 是既有加法 lane,`v0.3` 是本规格收敛后的下一条 lane。`v0.3` 之后新增 lane 时,优先使用 `scripts/hwlab-lane-expand.mjs` 从最新稳定 lane 派生配置,再补受控 CLI 的 `--lane <lane>` 验证;如果某处仍要求改 JS/TS 代码才能识别新 lane,应视为扩容抽象缺口并修通用读取逻辑,而不是复制旧分支。
|
||||
Generated
+16
@@ -13,6 +13,9 @@
|
||||
"pg": "^8.21.0",
|
||||
"playwright": "1.59.1",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@openai/codex": {
|
||||
@@ -323,6 +326,19 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
+6
-1
@@ -26,7 +26,9 @@
|
||||
"web:check": "cd web/hwlab-cloud-web && bun run check",
|
||||
"web:build": "(cd web/hwlab-cloud-web && bun run build)",
|
||||
"artifact-catalog:preview-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --blocked --no-write",
|
||||
"g14:gitops:render": "node scripts/g14-gitops-render.mjs",
|
||||
"lane:expand": "node scripts/hwlab-lane-expand.mjs",
|
||||
"g14:gitops:render": "node scripts/run-bun.mjs scripts/g14-gitops-render.mjs",
|
||||
"gitops:ts:check": "tsc --noEmit -p tsconfig.gitops.json",
|
||||
"docs:validate:m3-rollout": "node scripts/validate-m3-rollout-runbook.mjs",
|
||||
"d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs",
|
||||
"report:lifecycle:test": "node scripts/report-lifecycle.test.mjs",
|
||||
@@ -47,5 +49,8 @@
|
||||
"pg": "^8.21.0",
|
||||
"playwright": "1.59.1",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
+185
-139
@@ -7,6 +7,14 @@ import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
applyRuntimeLaneDeployConfig,
|
||||
defaultEndpointForRuntimeLane,
|
||||
defaultPortForRuntimeLane,
|
||||
isRuntimeLane,
|
||||
runtimeLaneConfig,
|
||||
versionNameForRuntimeLane
|
||||
} from "./src/g14-gitops-lane.ts";
|
||||
import { readStructuredFile } from "./src/structured-config.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -17,6 +25,8 @@ const defaultSourceRepo = process.env.HWLAB_G14_GIT_URL || "git@github.com:pikas
|
||||
const defaultGitReadUrl = process.env.HWLAB_G14_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 || defaultV02GitReadUrl;
|
||||
const defaultRuntimeLaneGitWriteUrl = process.env.HWLAB_RUNTIME_LANE_GIT_WRITE_URL || defaultV02GitWriteUrl;
|
||||
const defaultBranch = process.env.HWLAB_G14_BRANCH || "G14";
|
||||
const defaultGitopsBranch = process.env.HWLAB_G14_GITOPS_BRANCH || "G14-gitops";
|
||||
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
|
||||
@@ -97,11 +107,12 @@ const primitiveValidationTasks = Object.freeze([
|
||||
{
|
||||
name: "g14-contract-check",
|
||||
commands: [
|
||||
"node --check scripts/g14-gitops-render.mjs",
|
||||
"npm run gitops:ts:check",
|
||||
"node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --help >/dev/null",
|
||||
"render_check_revision=$(git rev-parse HEAD)",
|
||||
"render_check_dir=$(mktemp -d)",
|
||||
"node scripts/g14-gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --source-revision \"$render_check_revision\"",
|
||||
"node scripts/g14-gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --check --source-revision \"$render_check_revision\"",
|
||||
"node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --source-revision \"$render_check_revision\"",
|
||||
"node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --check --source-revision \"$render_check_revision\"",
|
||||
`node --input-type=module <<'NODE'
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -124,6 +135,7 @@ const forbiddenCiFragments = [
|
||||
];
|
||||
const scanTargets = [
|
||||
"scripts/g14-gitops-render.mjs",
|
||||
"scripts/src/g14-gitops-lane.ts",
|
||||
"scripts/artifact-publish.mjs",
|
||||
"scripts/g14-artifact-publish.mjs",
|
||||
"scripts/src/g14-ci-plan-lib.mjs",
|
||||
@@ -234,23 +246,26 @@ function parseArgs(argv) {
|
||||
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||
else throw new Error(`unknown argument ${arg}`);
|
||||
}
|
||||
if (args.lane === "v02") {
|
||||
if (args.sourceBranch === defaultBranch) args.sourceBranch = "v0.2";
|
||||
if (args.gitopsBranch === defaultGitopsBranch) args.gitopsBranch = "v0.2-gitops";
|
||||
if (args.catalogPath === defaultCatalogPath) args.catalogPath = "deploy/artifact-catalog.v02.json";
|
||||
if (args.runtimeEndpoint === defaultRuntimeEndpoint) args.runtimeEndpoint = defaultV02RuntimeEndpoint;
|
||||
if (args.webEndpoint === defaultWebEndpoint) args.webEndpoint = defaultV02WebEndpoint;
|
||||
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 = defaultV02GitReadUrl;
|
||||
if (!args.gitWriteUrl) args.gitWriteUrl = defaultV02GitWriteUrl;
|
||||
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(["g14", "v02"].includes(args.lane), `unknown lane ${args.lane}`);
|
||||
assert.ok(args.lane === "g14" || isRuntimeLane(args.lane), `unknown lane ${args.lane}`);
|
||||
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
|
||||
if (args.lane === "v02") {
|
||||
assert.equal(args.sourceBranch, "v0.2", "v02 source branch must be v0.2");
|
||||
assert.equal(args.gitopsBranch, "v0.2-gitops", "v02 GitOps branch must be v0.2-gitops");
|
||||
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;
|
||||
}
|
||||
@@ -263,24 +278,24 @@ function readOption(argv, index, name) {
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
"usage: node scripts/g14-gitops-render.mjs [options]",
|
||||
"usage: node scripts/run-bun.mjs scripts/g14-gitops-render.mjs [options]",
|
||||
"",
|
||||
"Render G14-only Kubernetes-native CI/CD manifests from built-in Tekton CI primitives, deploy/deploy.yaml, and deploy/k8s/*.",
|
||||
"",
|
||||
"options:",
|
||||
" --lane LANE g14 or v02; default: g14",
|
||||
" --lane LANE g14 or runtime lane such as v02/v03/v04; default: g14",
|
||||
` --out DIR default: ${defaultOutDir}`,
|
||||
` --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; v02 defaults to devops-infra git mirror",
|
||||
" --git-write-url URL GitOps write URL; v02 defaults to devops-infra git mirror write relay",
|
||||
" --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}; v02 default: ${defaultV02RuntimeEndpoint}`,
|
||||
` --web-endpoint URL default: ${defaultWebEndpoint}; v02 default: ${defaultV02WebEndpoint}`,
|
||||
` --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",
|
||||
@@ -559,32 +574,38 @@ function runtimeImageTagForService(options) {
|
||||
}
|
||||
|
||||
function artifactEnvironment(args) {
|
||||
return args.lane === "v02" ? "v02" : "dev";
|
||||
return isRuntimeLane(args.lane) ? args.lane : "dev";
|
||||
}
|
||||
|
||||
function artifactEndpoint(args) {
|
||||
return args.lane === "v02" ? defaultV02RuntimeEndpoint : defaultRuntimeEndpoint;
|
||||
return isRuntimeLane(args.lane) ? args.runtimeEndpoint : defaultRuntimeEndpoint;
|
||||
}
|
||||
|
||||
function serviceIdsForLane(lane, deploy = null) {
|
||||
return lane === "v02" ? v02ServiceIdsFromDeploy(deploy) : defaultServiceIds;
|
||||
return isRuntimeLane(lane) ? runtimeLaneServiceIdsFromDeploy(deploy, lane) : defaultServiceIds;
|
||||
}
|
||||
|
||||
function serviceIdsForProfile(profile, deploy = null) {
|
||||
return profile === "v02" ? v02ServiceIdsFromDeploy(deploy) : defaultServiceIds;
|
||||
return isRuntimeLane(profile) ? runtimeLaneServiceIdsFromDeploy(deploy, profile) : defaultServiceIds;
|
||||
}
|
||||
|
||||
function v02ServiceIdsFromDeploy(deploy) {
|
||||
const serviceIds = uniquePreserveOrder((Array.isArray(deploy?.lanes?.v02?.envReuseServices) ? deploy.lanes.v02.envReuseServices : [])
|
||||
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.v02.envReuseServices must not be empty");
|
||||
assert.ok(serviceIds.length > 0, `deploy.lanes.${lane}.envReuseServices must not be empty`);
|
||||
return serviceIds;
|
||||
}
|
||||
|
||||
function v02ObservableServicesForDeploy(deploy) {
|
||||
const declarations = deploy?.lanes?.v02?.serviceDeclarations ?? {};
|
||||
const serviceIds = new Set(v02ServiceIdsFromDeploy(deploy));
|
||||
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]) => ({
|
||||
@@ -595,12 +616,20 @@ function v02ObservableServicesForDeploy(deploy) {
|
||||
return [...declared, ...v02ExtraObservableServices];
|
||||
}
|
||||
|
||||
function v02ObservableServicesForDeploy(deploy) {
|
||||
return runtimeLaneObservableServicesForDeploy(deploy, "v02");
|
||||
}
|
||||
|
||||
function v02ObservableService(deploy, serviceId) {
|
||||
return v02ObservableServicesForDeploy(deploy).find((item) => item.serviceId === serviceId) ?? null;
|
||||
}
|
||||
|
||||
function runtimeLaneObservableServiceIdsForDeploy(deploy, lane) {
|
||||
return new Set(runtimeLaneObservableServicesForDeploy(deploy, lane).map((item) => item.serviceId));
|
||||
}
|
||||
|
||||
function v02ObservableServiceIdsForDeploy(deploy) {
|
||||
return new Set(v02ObservableServicesForDeploy(deploy).map((item) => item.serviceId));
|
||||
return runtimeLaneObservableServiceIdsForDeploy(deploy, "v02");
|
||||
}
|
||||
|
||||
function servicesParamForLane(lane, deploy = null) {
|
||||
@@ -624,7 +653,7 @@ function isV02RemovedServiceId(serviceId) {
|
||||
|
||||
function artifactCatalogSkeleton({ args, deploy, source }) {
|
||||
const environment = artifactEnvironment(args);
|
||||
const namespace = args.lane === "v02" ? "hwlab-v02" : "hwlab-dev";
|
||||
const namespace = isRuntimeLane(args.lane) ? namespaceNameForProfile(args.lane) : "hwlab-dev";
|
||||
const tag = imageTagForSource(args, source);
|
||||
return {
|
||||
catalogVersion: "v1",
|
||||
@@ -642,7 +671,7 @@ function artifactCatalogSkeleton({ args, deploy, source }) {
|
||||
publishedAt: null
|
||||
},
|
||||
allowedProfiles: [environment],
|
||||
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
|
||||
forbiddenProfiles: isRuntimeLane(args.lane) ? ["dev", "prod"] : ["prod"],
|
||||
services: serviceIdsForLane(args.lane, deploy).map((serviceId) => artifactCatalogSkeletonService({ args, deploy, source, serviceId, environment, namespace, tag }))
|
||||
};
|
||||
}
|
||||
@@ -687,47 +716,48 @@ function artifactCatalogSkeletonService({ args, deploy, source, serviceId, envir
|
||||
}
|
||||
|
||||
function namespaceNameForProfile(profile) {
|
||||
if (profile === "v02") return "hwlab-v02";
|
||||
if (isRuntimeLane(profile)) return `hwlab-${profile}`;
|
||||
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||
}
|
||||
|
||||
function runtimePathForProfile(profile) {
|
||||
if (profile === "v02") return "runtime-v02";
|
||||
if (isRuntimeLane(profile)) return `runtime-${profile}`;
|
||||
return profile === "prod" ? "runtime-prod" : "runtime-dev";
|
||||
}
|
||||
|
||||
function runtimeLabelForProfile(profile) {
|
||||
if (profile === "v02") return "v02";
|
||||
if (isRuntimeLane(profile)) return profile;
|
||||
return profile === "prod" ? "prod" : "dev";
|
||||
}
|
||||
|
||||
function gitopsTargetForProfile(profile) {
|
||||
return profile === "v02" ? "v02" : "g14";
|
||||
return isRuntimeLane(profile) ? profile : "g14";
|
||||
}
|
||||
|
||||
function profileEndpoint(args, profile) {
|
||||
if (profile === "v02") return { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint };
|
||||
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 args.lane === "v02" ? ["v02"] : ["dev", "prod"];
|
||||
return isRuntimeLane(args.lane) ? [args.lane] : ["dev", "prod"];
|
||||
}
|
||||
|
||||
function laneNames(args) {
|
||||
if (args.lane === "v02") {
|
||||
if (isRuntimeLane(args.lane)) {
|
||||
const namespace = namespaceNameForProfile(args.lane);
|
||||
return {
|
||||
gitopsTarget: "v02",
|
||||
pipeline: "hwlab-v02-ci-image-publish",
|
||||
poller: "hwlab-v02-branch-poller",
|
||||
reconciler: "hwlab-v02-control-plane-reconciler",
|
||||
serviceAccount: "hwlab-v02-tekton-runner",
|
||||
pipelineRunPrefix: "hwlab-v02-ci-poll-",
|
||||
runtimeNamespace: "hwlab-v02",
|
||||
runtimePath: "deploy/gitops/g14/runtime-v02",
|
||||
argoApplications: ["argocd/hwlab-g14-v02"]
|
||||
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: `deploy/gitops/g14/runtime-${args.lane}`,
|
||||
argoApplications: [`argocd/hwlab-g14-${args.lane}`]
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -744,7 +774,7 @@ function laneNames(args) {
|
||||
}
|
||||
|
||||
function argoApplicationName(profile) {
|
||||
return profile === "v02" ? "hwlab-g14-v02" : `hwlab-g14-${profile}`;
|
||||
return `hwlab-g14-${profile}`;
|
||||
}
|
||||
|
||||
function imageTagForSource(args, source) {
|
||||
@@ -1126,13 +1156,13 @@ function transformListNamespace(value, namespace, labels, annotations) {
|
||||
}
|
||||
|
||||
function transformServices({ services, namespace, labels, annotations, profile = "dev", deploy = null }) {
|
||||
const v02ObservableServiceIds = profile === "v02" ? v02ObservableServiceIdsForDeploy(deploy) : new Set();
|
||||
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) => !(profile === "v02" && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
|
||||
result.items = (result.items ?? []).filter((item) => !(isRuntimeLane(profile) && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
|
||||
for (const item of result.items) {
|
||||
const serviceId = serviceIdForWorkload(item, null);
|
||||
if (profile !== "v02" || !v02ObservableServiceIds.has(serviceId)) continue;
|
||||
if (!isRuntimeLane(profile) || !observableServiceIds.has(serviceId)) continue;
|
||||
label(item.metadata ??= {}, v02MetricsLabels(serviceId));
|
||||
upsertV02MetricsPort(item);
|
||||
}
|
||||
@@ -1140,17 +1170,18 @@ function transformServices({ services, namespace, labels, annotations, profile =
|
||||
return result;
|
||||
}
|
||||
|
||||
function observabilityManifest({ deploy, namespace, labels, annotations, metricsSidecarScript }) {
|
||||
function observabilityManifest({ deploy, profile, namespace, labels, annotations, metricsSidecarScript }) {
|
||||
assert.ok(isRuntimeLane(profile), `observability profile must be a runtime lane, got ${profile}`);
|
||||
const baseLabels = {
|
||||
...labels,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
"hwlab.pikastech.local/monitoring": "enabled"
|
||||
};
|
||||
const serviceMonitors = v02ObservableServicesForDeploy(deploy).map((service) => ({
|
||||
const serviceMonitors = runtimeLaneObservableServicesForDeploy(deploy, profile).map((service) => ({
|
||||
apiVersion: "monitoring.coreos.com/v1",
|
||||
kind: "ServiceMonitor",
|
||||
metadata: {
|
||||
name: `hwlab-v02-${service.serviceId}`,
|
||||
name: `hwlab-${profile}-${service.serviceId}`,
|
||||
namespace,
|
||||
labels: { ...baseLabels, "hwlab.pikastech.local/service-id": service.serviceId },
|
||||
annotations
|
||||
@@ -1159,7 +1190,7 @@ function observabilityManifest({ deploy, namespace, labels, annotations, metrics
|
||||
namespaceSelector: { matchNames: [namespace] },
|
||||
selector: {
|
||||
matchLabels: {
|
||||
"hwlab.pikastech.local/gitops-target": "v02",
|
||||
"hwlab.pikastech.local/gitops-target": profile,
|
||||
"hwlab.pikastech.local/monitoring": "enabled",
|
||||
"hwlab.pikastech.local/service-id": service.serviceId
|
||||
}
|
||||
@@ -1178,30 +1209,30 @@ function observabilityManifest({ deploy, namespace, labels, annotations, metrics
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "ConfigMap",
|
||||
metadata: { name: "hwlab-v02-metrics-sidecar", namespace, labels: baseLabels, annotations },
|
||||
metadata: { name: `hwlab-${profile}-metrics-sidecar`, namespace, labels: baseLabels, annotations },
|
||||
data: { "metrics-sidecar.mjs": metricsSidecarScript }
|
||||
},
|
||||
...serviceMonitors,
|
||||
{
|
||||
apiVersion: "monitoring.coreos.com/v1",
|
||||
kind: "PrometheusRule",
|
||||
metadata: { name: "hwlab-v02-observability", namespace, labels: baseLabels, annotations },
|
||||
metadata: { name: `hwlab-${profile}-observability`, namespace, labels: baseLabels, annotations },
|
||||
spec: {
|
||||
groups: [{
|
||||
name: "hwlab-v02-observability",
|
||||
name: `hwlab-${profile}-observability`,
|
||||
rules: [
|
||||
{
|
||||
alert: "HWLABV02MetricsTargetDown",
|
||||
expr: 'up{namespace="hwlab-v02"} == 0',
|
||||
alert: `HWLAB${profile.toUpperCase()}MetricsTargetDown`,
|
||||
expr: `up{namespace="${namespace}"} == 0`,
|
||||
for: "5m",
|
||||
labels: { severity: "warning", lane: "v02" },
|
||||
annotations: { summary: "HWLAB v0.2 metrics target is down", description: "Prometheus cannot scrape a HWLAB v0.2 metrics target." }
|
||||
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: "HWLABV02ServiceHealthProbeFailed",
|
||||
expr: 'hwlab_service_health_probe_success{namespace="hwlab-v02"} == 0',
|
||||
alert: `HWLAB${profile.toUpperCase()}ServiceHealthProbeFailed`,
|
||||
expr: `hwlab_service_health_probe_success{namespace="${namespace}"} == 0`,
|
||||
for: "5m",
|
||||
labels: { severity: "warning", lane: "v02" },
|
||||
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." }
|
||||
}
|
||||
]
|
||||
@@ -1920,8 +1951,8 @@ if [ -s /workspace/source/dev-artifacts.json ]; then
|
||||
node scripts/refresh-artifact-catalog.mjs --lane "$lane" --catalog-path "$catalog_path" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json --write
|
||||
fi
|
||||
gitops_render_started_ms="$(ci_now_ms)"
|
||||
node scripts/g14-gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
|
||||
node scripts/g14-gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
|
||||
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
|
||||
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
|
||||
ci_timing_emit gitops-render succeeded "$gitops_render_started_ms"
|
||||
check_source_head before-push
|
||||
workdir="$(mktemp -d)"
|
||||
@@ -2087,8 +2118,8 @@ revision="$(git rev-parse HEAD)"
|
||||
: "\${LANE:?LANE is required}"
|
||||
: "\${CATALOG_PATH:?CATALOG_PATH is required}"
|
||||
: "\${IMAGE_TAG_MODE:?IMAGE_TAG_MODE is required}"
|
||||
node scripts/g14-gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX"
|
||||
node scripts/g14-gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX" --check
|
||||
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX"
|
||||
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX" --check
|
||||
node <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const https = require("node:https");
|
||||
@@ -2357,20 +2388,21 @@ NODE
|
||||
|
||||
function ciLaneSettings(argsOrLane = "g14") {
|
||||
const lane = typeof argsOrLane === "string" ? argsOrLane : argsOrLane.lane;
|
||||
if (lane === "v02") {
|
||||
if (isRuntimeLane(lane)) {
|
||||
const namespace = namespaceNameForProfile(lane);
|
||||
return {
|
||||
lane: "v02",
|
||||
profile: "v02",
|
||||
gitopsTarget: "v02",
|
||||
pipelineName: "hwlab-v02-ci-image-publish",
|
||||
pipelineRunPrefix: "hwlab-v02-ci-poll",
|
||||
serviceAccountName: "hwlab-v02-tekton-runner",
|
||||
pollerName: "hwlab-v02-branch-poller",
|
||||
controlPlaneReconcilerName: "hwlab-v02-control-plane-reconciler",
|
||||
tektonDir: "tekton-v02",
|
||||
catalogPath: "deploy/artifact-catalog.v02.json",
|
||||
imageTagMode: "full",
|
||||
runtimeNamespace: "hwlab-v02"
|
||||
lane,
|
||||
profile: lane,
|
||||
gitopsTarget: lane,
|
||||
pipelineName: `${namespace}-ci-image-publish`,
|
||||
pipelineRunPrefix: `${namespace}-ci-poll`,
|
||||
serviceAccountName: `${namespace}-tekton-runner`,
|
||||
pollerName: `${namespace}-branch-poller`,
|
||||
controlPlaneReconcilerName: `${namespace}-control-plane-reconciler`,
|
||||
tektonDir: `tekton-${lane}`,
|
||||
catalogPath: typeof argsOrLane === "object" ? argsOrLane.catalogPath : `deploy/artifact-catalog.${lane}.json`,
|
||||
imageTagMode: typeof argsOrLane === "object" ? argsOrLane.imageTagMode : "full",
|
||||
runtimeNamespace: namespace
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -2391,7 +2423,10 @@ function ciLaneSettings(argsOrLane = "g14") {
|
||||
|
||||
function tektonRbac(args = { lane: "g14" }) {
|
||||
const settings = ciLaneSettings(args);
|
||||
if (settings.lane === "v02") {
|
||||
if (isRuntimeLane(settings.lane)) {
|
||||
const runtimeObserverName = `${settings.runtimeNamespace}-runtime-observer`;
|
||||
const pipelineRunWriterName = `${settings.runtimeNamespace}-pipelinerun-writer`;
|
||||
const argoReconcilerName = `${settings.runtimeNamespace}-argocd-reconciler`;
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
@@ -2401,7 +2436,7 @@ function tektonRbac(args = { lane: "g14" }) {
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: { name: "hwlab-v02-runtime-observer", namespace: "hwlab-v02" },
|
||||
metadata: { name: runtimeObserverName, namespace: settings.runtimeNamespace },
|
||||
rules: [
|
||||
{ apiGroups: [""], resources: ["pods", "services", "configmaps"], verbs: ["get", "list", "watch"] },
|
||||
{ apiGroups: ["apps"], resources: ["deployments", "statefulsets"], verbs: ["get", "list", "watch"] },
|
||||
@@ -2411,14 +2446,14 @@ function tektonRbac(args = { lane: "g14" }) {
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: { name: "hwlab-v02-runtime-observer", namespace: "hwlab-v02" },
|
||||
metadata: { name: runtimeObserverName, namespace: settings.runtimeNamespace },
|
||||
subjects: [{ kind: "ServiceAccount", name: settings.serviceAccountName, namespace: "hwlab-ci" }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-v02-runtime-observer" }
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: runtimeObserverName }
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: { name: "hwlab-v02-pipelinerun-writer", namespace: "hwlab-ci" },
|
||||
metadata: { name: pipelineRunWriterName, namespace: "hwlab-ci" },
|
||||
rules: [
|
||||
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns"], verbs: ["get", "list", "create"] },
|
||||
{ apiGroups: ["tekton.dev"], resources: ["pipelines"], verbs: ["get", "patch", "create"] },
|
||||
@@ -2430,14 +2465,14 @@ function tektonRbac(args = { lane: "g14" }) {
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: { name: "hwlab-v02-pipelinerun-writer", namespace: "hwlab-ci" },
|
||||
metadata: { name: pipelineRunWriterName, namespace: "hwlab-ci" },
|
||||
subjects: [{ kind: "ServiceAccount", name: settings.serviceAccountName, namespace: "hwlab-ci" }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-v02-pipelinerun-writer" }
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: pipelineRunWriterName }
|
||||
},
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "Role",
|
||||
metadata: { name: "hwlab-v02-argocd-reconciler", namespace: "argocd" },
|
||||
metadata: { name: argoReconcilerName, namespace: "argocd" },
|
||||
rules: [
|
||||
{ apiGroups: ["argoproj.io"], resources: ["applications", "appprojects"], verbs: ["get", "patch", "create"] },
|
||||
{ apiGroups: ["rbac.authorization.k8s.io"], resources: ["roles", "rolebindings"], verbs: ["get", "patch", "create"] }
|
||||
@@ -2446,9 +2481,9 @@ function tektonRbac(args = { lane: "g14" }) {
|
||||
{
|
||||
apiVersion: "rbac.authorization.k8s.io/v1",
|
||||
kind: "RoleBinding",
|
||||
metadata: { name: "hwlab-v02-argocd-reconciler", namespace: "argocd" },
|
||||
metadata: { name: argoReconcilerName, namespace: "argocd" },
|
||||
subjects: [{ kind: "ServiceAccount", name: settings.serviceAccountName, namespace: "hwlab-ci" }],
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-v02-argocd-reconciler" }
|
||||
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: argoReconcilerName }
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -4004,15 +4039,16 @@ function contentType(filePath) {
|
||||
}
|
||||
|
||||
function argoProject(args = { lane: "g14" }) {
|
||||
if (args.lane === "v02") {
|
||||
if (isRuntimeLane(args.lane)) {
|
||||
const namespace = namespaceNameForProfile(args.lane);
|
||||
return {
|
||||
apiVersion: "argoproj.io/v1alpha1",
|
||||
kind: "AppProject",
|
||||
metadata: { name: "hwlab-v02", namespace: "argocd" },
|
||||
metadata: { name: namespace, namespace: "argocd" },
|
||||
spec: {
|
||||
description: "HWLAB v0.2 additive GitOps project on G14; DEV/PROD remain outside this lane.",
|
||||
description: `HWLAB ${versionNameForRuntimeLane(args.lane)} additive GitOps project on G14; DEV/PROD remain outside this lane.`,
|
||||
sourceRepos: [args.gitReadUrl],
|
||||
destinations: [{ server: "https://kubernetes.default.svc", namespace: "hwlab-v02" }],
|
||||
destinations: [{ server: "https://kubernetes.default.svc", namespace }],
|
||||
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
|
||||
namespaceResourceWhitelist: [{ group: "*", kind: "*" }]
|
||||
}
|
||||
@@ -4039,8 +4075,8 @@ function argoApplication(args, profile = "dev") {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const runtimePath = runtimePathForProfile(profile);
|
||||
const gitopsTarget = gitopsTargetForProfile(profile);
|
||||
const project = profile === "v02" ? "hwlab-v02" : "hwlab-g14";
|
||||
const repoURL = profile === "v02" ? args.gitReadUrl : args.sourceRepo;
|
||||
const project = isRuntimeLane(profile) ? namespace : "hwlab-g14";
|
||||
const repoURL = isRuntimeLane(profile) ? args.gitReadUrl : args.sourceRepo;
|
||||
return {
|
||||
apiVersion: "argoproj.io/v1alpha1",
|
||||
kind: "Application",
|
||||
@@ -4053,7 +4089,7 @@ function argoApplication(args, profile = "dev") {
|
||||
project,
|
||||
source: { repoURL, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` },
|
||||
destination: { server: "https://kubernetes.default.svc", namespace },
|
||||
syncPolicy: { automated: { prune: profile === "v02", selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
|
||||
syncPolicy: { automated: { prune: isRuntimeLane(profile), selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -4061,11 +4097,11 @@ function argoApplication(args, profile = "dev") {
|
||||
function g14FrpcManifest({ profile = "dev", source = null } = {}) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const profileLabel = runtimeLabelForProfile(profile);
|
||||
const deploymentName = profile === "v02" ? "hwlab-v02-frpc" : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc";
|
||||
const deploymentName = isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc";
|
||||
const configName = `${deploymentName}-config`;
|
||||
const proxyPrefix = profile === "v02" ? "hwlab-v02" : profile === "prod" ? "hwlab-g14-prod" : "hwlab-g14";
|
||||
const webRemotePort = profile === "v02" ? 19666 : profile === "prod" ? 18666 : 17666;
|
||||
const edgeRemotePort = profile === "v02" ? 19667 : profile === "prod" ? 18667 : 17667;
|
||||
const proxyPrefix = isRuntimeLane(profile) ? `hwlab-${profile}` : profile === "prod" ? "hwlab-g14-prod" : "hwlab-g14";
|
||||
const webRemotePort = isRuntimeLane(profile) ? defaultPortForRuntimeLane(profile) : profile === "prod" ? 18666 : 17666;
|
||||
const edgeRemotePort = isRuntimeLane(profile) ? webRemotePort + 1 : profile === "prod" ? 18667 : 17667;
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": deploymentName,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
@@ -4075,7 +4111,7 @@ function g14FrpcManifest({ profile = "dev", source = null } = {}) {
|
||||
};
|
||||
const sourceCommitLabel = source?.full ? { "hwlab.pikastech.local/source-commit": source.full } : {};
|
||||
const renderedLabels = { ...labels, ...sourceCommitLabel };
|
||||
const podNetwork = profile === "v02" ? {
|
||||
const podNetwork = isRuntimeLane(profile) ? {
|
||||
hostNetwork: true,
|
||||
dnsPolicy: "ClusterFirstWithHostNet"
|
||||
} : {};
|
||||
@@ -4589,24 +4625,25 @@ function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix, ca
|
||||
};
|
||||
}
|
||||
|
||||
function v02PostgresManifest({ migrationSql, source }) {
|
||||
const namespace = "hwlab-v02";
|
||||
const name = "hwlab-v02-postgres";
|
||||
function v02PostgresManifest({ profile = "v02", migrationSql, source }) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const name = `${namespace}-postgres`;
|
||||
const dbName = `hwlab_${profile}`;
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": name,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
"hwlab.pikastech.local/environment": "v02",
|
||||
"hwlab.pikastech.local/gitops-target": "v02",
|
||||
"hwlab.pikastech.local/profile": "v02",
|
||||
"hwlab.pikastech.local/environment": profile,
|
||||
"hwlab.pikastech.local/gitops-target": profile,
|
||||
"hwlab.pikastech.local/profile": profile,
|
||||
"hwlab.pikastech.local/source-commit": source.full
|
||||
};
|
||||
const migrationSha256 = createHash("sha256").update(migrationSql).digest("hex");
|
||||
const templateLabels = {
|
||||
"app.kubernetes.io/name": name,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
"hwlab.pikastech.local/environment": "v02",
|
||||
"hwlab.pikastech.local/gitops-target": "v02",
|
||||
"hwlab.pikastech.local/profile": "v02",
|
||||
"hwlab.pikastech.local/environment": profile,
|
||||
"hwlab.pikastech.local/gitops-target": profile,
|
||||
"hwlab.pikastech.local/profile": profile,
|
||||
"hwlab.pikastech.local/migration-sha256": k8sLabelHash(migrationSha256)
|
||||
};
|
||||
const templateAnnotations = { "hwlab.pikastech.local/migration-sha256": migrationSha256 };
|
||||
@@ -4642,9 +4679,9 @@ function v02PostgresManifest({ migrationSql, source }) {
|
||||
image: "127.0.0.1:5000/hwlab/postgres:16-alpine",
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
env: [
|
||||
{ name: "POSTGRES_DB", value: "hwlab_v02" },
|
||||
{ name: "POSTGRES_USER", value: "hwlab_v02" },
|
||||
{ name: "POSTGRES_PASSWORD", valueFrom: { secretKeyRef: { name: "hwlab-v02-postgres", key: "POSTGRES_PASSWORD" } } }
|
||||
{ name: "POSTGRES_DB", value: dbName },
|
||||
{ name: "POSTGRES_USER", value: dbName },
|
||||
{ name: "POSTGRES_PASSWORD", valueFrom: { secretKeyRef: { name, key: "POSTGRES_PASSWORD" } } }
|
||||
],
|
||||
ports: [{ name: "postgres", containerPort: 5432 }],
|
||||
readinessProbe: { tcpSocket: { port: "postgres" }, initialDelaySeconds: 5, periodSeconds: 10 },
|
||||
@@ -4667,17 +4704,17 @@ function v02PostgresManifest({ migrationSql, source }) {
|
||||
};
|
||||
}
|
||||
|
||||
function v02OpenFgaManifest({ source }) {
|
||||
const namespace = "hwlab-v02";
|
||||
function v02OpenFgaManifest({ profile = "v02", source }) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const name = "hwlab-openfga";
|
||||
const image = "127.0.0.1:5000/hwlab/openfga:v1.17.0";
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": name,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
"hwlab.pikastech.local/component": "authorization",
|
||||
"hwlab.pikastech.local/environment": "v02",
|
||||
"hwlab.pikastech.local/gitops-target": "v02",
|
||||
"hwlab.pikastech.local/profile": "v02",
|
||||
"hwlab.pikastech.local/environment": profile,
|
||||
"hwlab.pikastech.local/gitops-target": profile,
|
||||
"hwlab.pikastech.local/profile": profile,
|
||||
"hwlab.pikastech.local/service-id": name,
|
||||
"hwlab.pikastech.local/source-commit": source.full
|
||||
};
|
||||
@@ -4685,9 +4722,9 @@ function v02OpenFgaManifest({ source }) {
|
||||
const selector = { "app.kubernetes.io/name": name };
|
||||
const env = [
|
||||
{ name: "OPENFGA_DATASTORE_ENGINE", value: "postgres" },
|
||||
{ name: "OPENFGA_DATASTORE_URI", valueFrom: { secretKeyRef: { name: "hwlab-v02-openfga", key: "datastore-uri" } } },
|
||||
{ name: "OPENFGA_DATASTORE_URI", valueFrom: { secretKeyRef: { name: `hwlab-${profile}-openfga`, key: "datastore-uri" } } },
|
||||
{ name: "OPENFGA_AUTHN_METHOD", value: "preshared" },
|
||||
{ name: "OPENFGA_AUTHN_PRESHARED_KEYS", valueFrom: { secretKeyRef: { name: "hwlab-v02-openfga", key: "authn-preshared-key" } } },
|
||||
{ name: "OPENFGA_AUTHN_PRESHARED_KEYS", valueFrom: { secretKeyRef: { name: `hwlab-${profile}-openfga`, key: "authn-preshared-key" } } },
|
||||
{ name: "OPENFGA_PLAYGROUND_ENABLED", value: "false" },
|
||||
{ name: "OPENFGA_HTTP_ADDR", value: "0.0.0.0:8080" },
|
||||
{ name: "OPENFGA_GRPC_ADDR", value: "0.0.0.0:8081" }
|
||||
@@ -4815,17 +4852,26 @@ async function plannedFiles(args) {
|
||||
readJson("deploy/k8s/dev/health-contract.yaml"),
|
||||
readJson("deploy/k8s/base/workloads.yaml")
|
||||
]);
|
||||
args = applyRuntimeLaneDeployConfig(args, deploy, {
|
||||
defaultBranch,
|
||||
defaultGitopsBranch,
|
||||
defaultCatalogPath,
|
||||
defaultRuntimeEndpoint,
|
||||
defaultWebEndpoint,
|
||||
defaultV02RuntimeEndpoint,
|
||||
defaultSourceRepo
|
||||
});
|
||||
const source = await resolveSourceRevision(args.sourceRevision);
|
||||
source.imageTag = imageTagForSource(args, source);
|
||||
let artifactCatalog = await readJsonIfPresent(args.catalogPath, null);
|
||||
if (!artifactCatalog && args.lane === "v02") artifactCatalog = artifactCatalogSkeleton({ args, deploy, source });
|
||||
if (!artifactCatalog && isRuntimeLane(args.lane)) artifactCatalog = artifactCatalogSkeleton({ args, deploy, source });
|
||||
if (!artifactCatalog) artifactCatalog = await readJson(args.catalogPath);
|
||||
const settings = ciLaneSettings(args);
|
||||
const profiles = laneRuntimeProfiles(args);
|
||||
const migrationSql = args.lane === "v02"
|
||||
const migrationSql = isRuntimeLane(args.lane)
|
||||
? await readFile(path.join(repoRoot, "internal/db/migrations/0001_cloud_core_skeleton.sql"), "utf8")
|
||||
: null;
|
||||
const metricsSidecarScript = args.lane === "v02"
|
||||
const metricsSidecarScript = isRuntimeLane(args.lane)
|
||||
? await readFile(path.join(repoRoot, "internal/dev-entrypoint/metrics-sidecar.mjs"), "utf8")
|
||||
: null;
|
||||
const metricsSidecarSha256 = metricsSidecarScript ? createHash("sha256").update(metricsSidecarScript).digest("hex") : null;
|
||||
@@ -4840,7 +4886,7 @@ async function plannedFiles(args) {
|
||||
}));
|
||||
|
||||
const sourceDescriptor = {
|
||||
kind: args.lane === "v02" ? "hwlab-v02-gitops-source" : "hwlab-g14-gitops-source",
|
||||
kind: isRuntimeLane(args.lane) ? `hwlab-${args.lane}-gitops-source` : "hwlab-g14-gitops-source",
|
||||
sourceCommit: source.full,
|
||||
sourceShortCommit: source.short,
|
||||
sourceImageTag: source.imageTag,
|
||||
@@ -4853,7 +4899,7 @@ async function plannedFiles(args) {
|
||||
registryPrefix: args.registryPrefix,
|
||||
runtimePaths: profiles.map((profile) => `deploy/gitops/g14/${runtimePathForProfile(profile)}`),
|
||||
publicEndpoints,
|
||||
frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile)}/${profile === "v02" ? "hwlab-v02-frpc" : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc"}`])),
|
||||
frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc"}`])),
|
||||
tektonPipeline: `hwlab-ci/${settings.pipelineName}`,
|
||||
argoApplications: profiles.map((profile) => `argocd/${argoApplicationName(profile)}`)
|
||||
};
|
||||
@@ -4867,10 +4913,10 @@ async function plannedFiles(args) {
|
||||
putJson("source.json", sourceDescriptor);
|
||||
putText("README.md", readme({ source, args }));
|
||||
putJson("registry/registry.yaml", registryManifest());
|
||||
if (args.lane === "v02") putJson("devops-infra/git-mirror.yaml", devopsInfraGitMirrorManifest());
|
||||
if (isRuntimeLane(args.lane)) putJson("devops-infra/git-mirror.yaml", devopsInfraGitMirrorManifest(args));
|
||||
putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args));
|
||||
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
|
||||
if (args.lane !== "v02") {
|
||||
if (!isRuntimeLane(args.lane)) {
|
||||
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy));
|
||||
putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args));
|
||||
}
|
||||
@@ -4890,14 +4936,14 @@ async function plannedFiles(args) {
|
||||
const endpoints = profileEndpoint(args, profile);
|
||||
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent }));
|
||||
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
|
||||
if (profile !== "v02") putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
|
||||
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 }));
|
||||
putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile));
|
||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||
if (profile === "v02") putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ migrationSql, source }));
|
||||
if (profile === "v02") putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ source }));
|
||||
if (profile === "v02") putJson(`${runtimePath}/observability.yaml`, observabilityManifest({ deploy, namespace, labels: profileLabels, annotations, metricsSidecarScript }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSql, source }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source }));
|
||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/observability.yaml`, observabilityManifest({ deploy, profile, namespace, labels: profileLabels, annotations, metricsSidecarScript }));
|
||||
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
|
||||
putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile, source }));
|
||||
}
|
||||
@@ -4913,8 +4959,8 @@ async function writeFiles(files) {
|
||||
}
|
||||
|
||||
async function cleanStaleGeneratedOutput(args) {
|
||||
if (args.lane !== "v02") return;
|
||||
for (const relativePath of ["runtime-v02", "tekton-v02"]) {
|
||||
if (!isRuntimeLane(args.lane)) return;
|
||||
for (const relativePath of [`runtime-${args.lane}`, `tekton-${args.lane}`]) {
|
||||
await rm(generatedPath(path.join(args.outDir, relativePath)), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
await writeFile(staleTektonFile, "hwlab-router\n");
|
||||
|
||||
const render = spawnSync(process.execPath, [
|
||||
"scripts/run-bun.mjs",
|
||||
"scripts/g14-gitops-render.mjs",
|
||||
"--lane", "v02",
|
||||
"--catalog-path", "deploy/artifact-catalog.v02.json",
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { readDeployConfig, writeDeployConfig } from "./src/deploy-config.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultDeployPath = "deploy/deploy.yaml";
|
||||
const defaultHost = "74.48.78.17";
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
process.stdout.write(`${usage()}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const deploy = await readDeployConfig(repoRoot, args.deployConfig);
|
||||
const result = await configureLane(deploy, args);
|
||||
|
||||
if (args.write) {
|
||||
await writeDeployConfig(repoRoot, args.deployConfig, deploy);
|
||||
for (const file of result.copiedFiles) {
|
||||
await mkdir(path.dirname(path.join(repoRoot, file.to)), { recursive: true });
|
||||
await writeFile(path.join(repoRoot, file.to), file.content, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
command: "hwlab-lane-expand",
|
||||
action: args.write ? "configured" : "planned",
|
||||
deployConfig: args.deployConfig,
|
||||
lane: result.lane,
|
||||
fromLane: result.fromLane,
|
||||
wrote: args.write,
|
||||
copiedFiles: result.copiedFiles.map((file) => ({ from: file.from, to: file.to })),
|
||||
config: laneSummary(result.config),
|
||||
next: {
|
||||
render: `node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane ${result.lane} --source-branch ${result.config.sourceBranch} --gitops-branch ${result.config.gitopsBranch} --catalog-path ${result.config.artifactCatalog} --image-tag-mode ${result.config.imageTagMode ?? "full"}`,
|
||||
controlPlane: `bun scripts/cli.ts hwlab g14 control-plane apply --lane ${result.lane} --confirm`,
|
||||
trigger: `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane ${result.lane} --confirm`
|
||||
}
|
||||
}, null, 2)}\n`);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const parsed = {
|
||||
action: "configure",
|
||||
lane: null,
|
||||
fromLane: "v02",
|
||||
deployConfig: defaultDeployPath,
|
||||
host: defaultHost,
|
||||
write: false,
|
||||
force: false,
|
||||
copyLaneFiles: true,
|
||||
help: false,
|
||||
webPort: null,
|
||||
apiPort: null,
|
||||
publicUrl: null
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "configure" || arg === "plan") parsed.action = arg;
|
||||
else if (arg === "--lane") parsed.lane = readOption(argv, ++index, arg);
|
||||
else if (arg === "--from") parsed.fromLane = readOption(argv, ++index, arg);
|
||||
else if (arg === "--deploy-config") parsed.deployConfig = readOption(argv, ++index, arg);
|
||||
else if (arg === "--host") parsed.host = readOption(argv, ++index, arg);
|
||||
else if (arg === "--web-port") parsed.webPort = parsePort(readOption(argv, ++index, arg), arg);
|
||||
else if (arg === "--api-port") parsed.apiPort = parsePort(readOption(argv, ++index, arg), arg);
|
||||
else if (arg === "--public-url") parsed.publicUrl = readOption(argv, ++index, arg).replace(/\/+$/u, "");
|
||||
else if (arg === "--write") parsed.write = true;
|
||||
else if (arg === "--force") parsed.force = true;
|
||||
else if (arg === "--no-copy-lane-files") parsed.copyLaneFiles = false;
|
||||
else if (arg === "--help" || arg === "-h") parsed.help = true;
|
||||
else throw new Error(`unknown argument ${arg}`);
|
||||
}
|
||||
if (!parsed.help) {
|
||||
assertLane(parsed.lane, "--lane");
|
||||
assertLane(parsed.fromLane, "--from");
|
||||
assert.notEqual(parsed.lane, parsed.fromLane, "--lane must differ from --from");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function readOption(argv, index, name) {
|
||||
const value = argv[index];
|
||||
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function parsePort(value, name) {
|
||||
const parsed = Number.parseInt(String(value), 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) throw new Error(`${name} must be a TCP port`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
"usage: node scripts/hwlab-lane-expand.mjs configure --lane v03 [--from v02] [--write]",
|
||||
"",
|
||||
"Create or preview a new HWLAB additive runtime lane by editing deploy/deploy.yaml only.",
|
||||
"The command derives branch, namespace, GitOps branch, artifact catalog, runtime path, FRP ports,",
|
||||
"and lane-specific SecretRef names from the target lane id so v04/v05/v06 do not require code edits.",
|
||||
"",
|
||||
"options:",
|
||||
" --lane LANE target lane id such as v03, v04, v05",
|
||||
" --from LANE source lane template; default: v02",
|
||||
" --deploy-config PATH default: deploy/deploy.yaml",
|
||||
" --host HOST public host for default legacy endpoints; default: 74.48.78.17",
|
||||
" --web-port PORT override derived web FRP port",
|
||||
" --api-port PORT override derived API FRP port",
|
||||
" --public-url URL override derived HTTPS public endpoint",
|
||||
" --write write deploy config and copied lane prompt files",
|
||||
" --force replace an existing target lane",
|
||||
" --no-copy-lane-files do not copy lane-scoped source files referenced by config"
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function configureLane(deploy, options) {
|
||||
deploy.lanes ??= {};
|
||||
const source = deploy.lanes[options.fromLane];
|
||||
assert.ok(source && typeof source === "object", `deploy.lanes.${options.fromLane} is required`);
|
||||
if (deploy.lanes[options.lane] && !options.force) {
|
||||
throw new Error(`deploy.lanes.${options.lane} already exists; pass --force to replace it`);
|
||||
}
|
||||
|
||||
const sourceInfo = laneInfo(options.fromLane, { host: options.host });
|
||||
const targetInfo = laneInfo(options.lane, {
|
||||
host: options.host,
|
||||
webPort: options.webPort,
|
||||
apiPort: options.apiPort,
|
||||
publicUrl: options.publicUrl
|
||||
});
|
||||
const replacements = laneReplacements(source, sourceInfo, targetInfo);
|
||||
const target = deepTransform(clone(source), replacements);
|
||||
|
||||
target.name = targetInfo.versionName;
|
||||
target.sourceBranch = targetInfo.sourceBranch;
|
||||
target.gitopsBranch = targetInfo.gitopsBranch;
|
||||
target.namespace = targetInfo.namespace;
|
||||
target.endpoint = targetInfo.publicUrl;
|
||||
target.publicEndpoints = {
|
||||
...(target.publicEndpoints ?? {}),
|
||||
frontend: targetInfo.publicUrl,
|
||||
api: targetInfo.publicUrl,
|
||||
legacyFrontend: `http://${options.host}:${targetInfo.webPort}`,
|
||||
legacyApi: `http://${options.host}:${targetInfo.apiPort}`
|
||||
};
|
||||
target.artifactCatalog = `deploy/artifact-catalog.${options.lane}.json`;
|
||||
target.runtimePath = `deploy/gitops/g14/runtime-${options.lane}`;
|
||||
target.imageTagMode = target.imageTagMode ?? "full";
|
||||
|
||||
deploy.lanes[options.lane] = target;
|
||||
const copiedFiles = options.copyLaneFiles ? await copiedLaneFiles(target, replacements, sourceInfo, targetInfo) : [];
|
||||
return { lane: options.lane, fromLane: options.fromLane, config: target, copiedFiles };
|
||||
}
|
||||
|
||||
function assertLane(value, label) {
|
||||
if (!/^v[0-9]{2,}$/u.test(String(value ?? ""))) throw new Error(`${label} must look like v03, v04, v05`);
|
||||
}
|
||||
|
||||
function laneInfo(lane, options = {}) {
|
||||
assertLane(lane, "lane");
|
||||
const minor = Number.parseInt(lane.slice(1), 10);
|
||||
const webPort = options.webPort ?? 19466 + minor * 100;
|
||||
const apiPort = options.apiPort ?? webPort + 1;
|
||||
const versionName = `v0.${minor}`;
|
||||
const host = options.host ?? defaultHost;
|
||||
const hostLabel = host.replaceAll(".", "-");
|
||||
return {
|
||||
lane,
|
||||
minor,
|
||||
host,
|
||||
versionName,
|
||||
sourceBranch: versionName,
|
||||
gitopsBranch: `${versionName}-gitops`,
|
||||
namespace: `hwlab-${lane}`,
|
||||
webPort,
|
||||
apiPort,
|
||||
publicUrl: options.publicUrl ?? `https://hwlab-${lane}.${hostLabel}.nip.io`
|
||||
};
|
||||
}
|
||||
|
||||
function laneReplacements(sourceConfig, sourceInfo, targetInfo) {
|
||||
const sourceLegacy = sourceConfig?.publicEndpoints ?? {};
|
||||
return [
|
||||
[sourceInfo.versionName, targetInfo.versionName],
|
||||
[sourceInfo.sourceBranch, targetInfo.sourceBranch],
|
||||
[sourceInfo.gitopsBranch, targetInfo.gitopsBranch],
|
||||
[sourceInfo.namespace, targetInfo.namespace],
|
||||
[sourceInfo.lane, targetInfo.lane],
|
||||
[`runtime-${sourceInfo.lane}`, `runtime-${targetInfo.lane}`],
|
||||
[`artifact-catalog.${sourceInfo.lane}.json`, `artifact-catalog.${targetInfo.lane}.json`],
|
||||
[`usr_${sourceInfo.lane}_admin`, `usr_${targetInfo.lane}_admin`],
|
||||
[`${sourceInfo.lane}-redacted-presence-only`, `${targetInfo.lane}-redacted-presence-only`],
|
||||
[String(sourceLegacy.legacyFrontend ?? `http://${sourceInfo.host}:${sourceInfo.webPort}`), `http://${targetInfo.host}:${targetInfo.webPort}`],
|
||||
[String(sourceLegacy.legacyApi ?? `http://${sourceInfo.host}:${sourceInfo.apiPort}`), `http://${targetInfo.host}:${targetInfo.apiPort}`],
|
||||
[String(sourceLegacy.frontend ?? sourceConfig?.endpoint ?? ""), targetInfo.publicUrl],
|
||||
[String(sourceLegacy.api ?? sourceConfig?.endpoint ?? ""), targetInfo.publicUrl]
|
||||
].filter(([from]) => from);
|
||||
}
|
||||
|
||||
function deepTransform(value, replacements) {
|
||||
if (typeof value === "string") return replaceAll(value, replacements);
|
||||
if (Array.isArray(value)) return value.map((item) => deepTransform(item, replacements));
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [replaceAll(key, replacements), deepTransform(item, replacements)]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function replaceAll(value, replacements) {
|
||||
let text = String(value);
|
||||
for (const [from, to] of replacements) text = text.split(from).join(to);
|
||||
return text;
|
||||
}
|
||||
|
||||
async function copiedLaneFiles(target, replacements, sourceInfo, targetInfo) {
|
||||
const paths = new Set();
|
||||
collectRepoPaths(target, paths);
|
||||
const copied = [];
|
||||
for (const targetPath of paths) {
|
||||
if (!targetPath.includes(targetInfo.lane) && !targetPath.includes(targetInfo.versionName)) continue;
|
||||
const sourcePath = targetPath
|
||||
.split(targetInfo.lane).join(sourceInfo.lane)
|
||||
.split(targetInfo.versionName).join(sourceInfo.versionName);
|
||||
const sourceAbs = path.join(repoRoot, sourcePath);
|
||||
const targetAbs = path.join(repoRoot, targetPath);
|
||||
if (sourceAbs === targetAbs) continue;
|
||||
let raw;
|
||||
try {
|
||||
raw = await readFile(sourceAbs, "utf8");
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") continue;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
await readFile(targetAbs, "utf8");
|
||||
continue;
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") throw error;
|
||||
}
|
||||
copied.push({ from: sourcePath, to: targetPath, content: replaceAll(raw, replacements) });
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
function collectRepoPaths(value, result) {
|
||||
if (typeof value === "string") {
|
||||
if (/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/u.test(value) && !value.startsWith("http")) result.add(value);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) collectRepoPaths(item, result);
|
||||
return;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
for (const item of Object.values(value)) collectRepoPaths(item, result);
|
||||
}
|
||||
}
|
||||
|
||||
function laneSummary(config) {
|
||||
return {
|
||||
name: config.name,
|
||||
sourceBranch: config.sourceBranch,
|
||||
gitopsBranch: config.gitopsBranch,
|
||||
namespace: config.namespace,
|
||||
artifactCatalog: config.artifactCatalog,
|
||||
runtimePath: config.runtimePath,
|
||||
publicEndpoints: config.publicEndpoints,
|
||||
serviceCount: Array.isArray(config.envReuseServices) ? config.envReuseServices.length : null
|
||||
};
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
@@ -40,7 +40,9 @@ export const DEFAULT_DOCS_ONLY_PATHS = Object.freeze([
|
||||
export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([
|
||||
"deploy/gitops/",
|
||||
"deploy/frp/",
|
||||
"scripts/g14-gitops-render.mjs"
|
||||
"scripts/g14-gitops-render.mjs",
|
||||
"scripts/src/g14-gitops-lane.ts",
|
||||
"tsconfig.gitops.json"
|
||||
]);
|
||||
|
||||
export async function createG14CiPlan(options = {}) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
type RuntimeLaneDefaults = {
|
||||
defaultBranch?: string;
|
||||
defaultGitopsBranch?: string;
|
||||
defaultCatalogPath?: string;
|
||||
defaultRuntimeEndpoint?: string;
|
||||
defaultWebEndpoint?: string;
|
||||
defaultV02RuntimeEndpoint?: string;
|
||||
defaultSourceRepo?: string;
|
||||
};
|
||||
|
||||
export type GitOpsRenderArgs = {
|
||||
lane: string;
|
||||
sourceBranch: string;
|
||||
gitopsBranch: string;
|
||||
catalogPath: string;
|
||||
imageTagMode: string;
|
||||
runtimeEndpoint: string;
|
||||
webEndpoint: string;
|
||||
sourceRepo: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type RuntimeLaneConfig = {
|
||||
sourceBranch?: string;
|
||||
gitopsBranch?: string;
|
||||
artifactCatalog?: string;
|
||||
imageTagMode?: string;
|
||||
endpoint?: string;
|
||||
publicEndpoints?: {
|
||||
frontend?: string;
|
||||
api?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
sourceRepo?: string;
|
||||
};
|
||||
|
||||
export type DeployConfig = {
|
||||
lanes?: Record<string, RuntimeLaneConfig | undefined>;
|
||||
};
|
||||
|
||||
const defaultPublicHost = "74.48.78.17";
|
||||
|
||||
export function isRuntimeLane(lane: unknown): lane is string {
|
||||
return /^v[0-9]{2,}$/u.test(String(lane ?? ""));
|
||||
}
|
||||
|
||||
export function runtimeLaneMinor(lane: string): number {
|
||||
invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
|
||||
return Number.parseInt(lane.slice(1), 10);
|
||||
}
|
||||
|
||||
export function versionNameForRuntimeLane(lane: string): string {
|
||||
return `v0.${runtimeLaneMinor(lane)}`;
|
||||
}
|
||||
|
||||
export function namespaceNameForRuntimeLane(lane: string): string {
|
||||
invariant(isRuntimeLane(lane), `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}`);
|
||||
return `runtime-${lane}`;
|
||||
}
|
||||
|
||||
export function defaultPortForRuntimeLane(lane: string): number {
|
||||
return 19466 + runtimeLaneMinor(lane) * 100;
|
||||
}
|
||||
|
||||
export function publicHostLabel(host = defaultPublicHost): string {
|
||||
return host.replaceAll(".", "-");
|
||||
}
|
||||
|
||||
export function defaultEndpointForRuntimeLane(lane: string, options: { host?: string; v02Endpoint?: string } = {}): string {
|
||||
if (lane === "v02" && options.v02Endpoint) return options.v02Endpoint;
|
||||
return `https://hwlab-${lane}.${publicHostLabel(options.host ?? defaultPublicHost)}.nip.io`;
|
||||
}
|
||||
|
||||
export function runtimeLaneConfig(deploy: DeployConfig, lane: string): RuntimeLaneConfig {
|
||||
invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
|
||||
const config = deploy.lanes?.[lane];
|
||||
invariant(isObject(config), `deploy.lanes.${lane} is required`);
|
||||
return config;
|
||||
}
|
||||
|
||||
export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): GitOpsRenderArgs {
|
||||
if (!isRuntimeLane(args.lane)) return args;
|
||||
const result: GitOpsRenderArgs = { ...args };
|
||||
const laneConfig = runtimeLaneConfig(deploy, args.lane);
|
||||
const versionName = versionNameForRuntimeLane(args.lane);
|
||||
const defaultEndpointOptions = defaults.defaultV02RuntimeEndpoint ? { v02Endpoint: defaults.defaultV02RuntimeEndpoint } : {};
|
||||
const defaultEndpoint = defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions);
|
||||
const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint;
|
||||
const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
|
||||
const configuredRuntimeEndpoint = laneConfig.publicEndpoints?.api ?? configuredEndpoint;
|
||||
const defaultBranch = defaults.defaultBranch ?? "G14";
|
||||
const defaultGitopsBranch = defaults.defaultGitopsBranch ?? "G14-gitops";
|
||||
const defaultCatalogPath = defaults.defaultCatalogPath ?? "deploy/artifact-catalog.dev.json";
|
||||
const defaultRuntimeEndpoint = defaults.defaultRuntimeEndpoint ?? "http://74.48.78.17:17667";
|
||||
const defaultWebEndpoint = defaults.defaultWebEndpoint ?? "http://74.48.78.17:17666";
|
||||
const defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git";
|
||||
const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`;
|
||||
|
||||
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.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.sourceRepo === defaultSourceRepo && laneConfig.sourceRepo) result.sourceRepo = laneConfig.sourceRepo;
|
||||
return result;
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function invariant(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"checkJs": false,
|
||||
"declaration": false,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"useUnknownInCatchVariables": true
|
||||
},
|
||||
"include": [
|
||||
"scripts/src/g14-gitops-lane.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user