fix: 将 production release 归并回 v0.3 单线历史
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: PipelineRun
|
||||||
|
metadata:
|
||||||
|
generateName: hwlab-nc01-production-ci-poll-
|
||||||
|
namespace: hwlab-ci
|
||||||
|
annotations:
|
||||||
|
pipelinesascode.tekton.dev/on-event: "[push]"
|
||||||
|
pipelinesascode.tekton.dev/on-target-branch: "[release]"
|
||||||
|
pipelinesascode.tekton.dev/on-cel-expression: "event == 'push' && target_branch == 'release' && node == 'NC01'"
|
||||||
|
pipelinesascode.tekton.dev/max-keep-runs: "8"
|
||||||
|
unidesk.ai/owning-config-ref: "config/hwlab-node-lanes.yaml#lanes.production.targets.NC01"
|
||||||
|
unidesk.ai/effective-config-sha256: sha256:f0b827a59094630ba9a78a63c39a624613f93f87e685779732ebc38bd694af39
|
||||||
|
unidesk.ai/source-artifact-renderer: hwlab-runtime-lane
|
||||||
|
unidesk.ai/source-artifact-mode: remote-pipeline-annotation
|
||||||
|
pipelinesascode.tekton.dev/pipeline: ci/pipelines/hwlab-nc01-production-ci-image-publish.yaml
|
||||||
|
hwlab.pikastech.local/ci-contract: tekton-native-primitive-tasks
|
||||||
|
hwlab.pikastech.local/download-profile: jd01-node-default
|
||||||
|
hwlab.pikastech.local/gitops-branch: release-gitops
|
||||||
|
hwlab.pikastech.local/network-profile: jd01-node-ci-egress
|
||||||
|
hwlab.pikastech.local/node: NC01
|
||||||
|
hwlab.pikastech.local/policy: native-per-service-taskrun-image-publish
|
||||||
|
hwlab.pikastech.local/runtime-path: deploy/gitops/node/nc01/runtime-production
|
||||||
|
hwlab.pikastech.local/source-branch: release
|
||||||
|
hwlab.pikastech.local/source-config: .tekton/hwlab-nc01-production-pac.yaml
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: hwlab-nc01-production-pac
|
||||||
|
app.kubernetes.io/part-of: hwlab
|
||||||
|
unidesk.ai/source-commit: "{{ revision }}"
|
||||||
|
hwlab.pikastech.local/gitops-target: production
|
||||||
|
hwlab.pikastech.local/source-commit: "{{ revision }}"
|
||||||
|
hwlab.pikastech.local/trigger: pipelines-as-code
|
||||||
|
spec:
|
||||||
|
timeouts:
|
||||||
|
pipeline: 1h0m0s
|
||||||
|
taskRunTemplate:
|
||||||
|
serviceAccountName: hwlab-nc01-production-tekton-runner
|
||||||
|
podTemplate:
|
||||||
|
hostNetwork: true
|
||||||
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 1000
|
||||||
|
pipelineRef:
|
||||||
|
name: hwlab-nc01-production-ci-image-publish
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
volumeClaimTemplate:
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 8Gi
|
||||||
|
- name: git-ssh
|
||||||
|
secret:
|
||||||
|
secretName: hwlab-git-ssh
|
||||||
|
params:
|
||||||
|
- name: git-url
|
||||||
|
value: "{{ repo_url }}"
|
||||||
|
- name: git-read-url
|
||||||
|
value: "{{ git_read_url }}"
|
||||||
|
- name: git-write-url
|
||||||
|
value: "{{ git_write_url }}"
|
||||||
|
- name: source-branch
|
||||||
|
value: release
|
||||||
|
- name: gitops-branch
|
||||||
|
value: release-gitops
|
||||||
|
- name: lane
|
||||||
|
value: production
|
||||||
|
- name: catalog-path
|
||||||
|
value: deploy/artifact-catalog.nc01-production.json
|
||||||
|
- name: image-tag-mode
|
||||||
|
value: full
|
||||||
|
- name: runtime-path
|
||||||
|
value: deploy/gitops/node/nc01/runtime-production
|
||||||
|
- name: revision
|
||||||
|
value: "{{ revision }}"
|
||||||
|
- name: registry-prefix
|
||||||
|
value: 127.0.0.1:5000/hwlab
|
||||||
|
- name: services
|
||||||
|
value: "hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-tasktree-api,hwlab-tasktree-worker,hwlab-harnessrl-api,hwlab-harnessrl-worker,hwlab-workbench-api,hwlab-workbench-worker,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills"
|
||||||
|
- name: base-image
|
||||||
|
value: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim
|
||||||
|
- name: build-cache-mode
|
||||||
|
value: registry
|
||||||
|
- name: runtime-ready-timeout-ms
|
||||||
|
value: "60000"
|
||||||
File diff suppressed because one or more lines are too long
+145
-5
@@ -85,6 +85,9 @@ nodes:
|
|||||||
gitopsRoot: deploy/gitops/node
|
gitopsRoot: deploy/gitops/node
|
||||||
sourceRepo: git@github.com:pikasTech/HWLAB.git
|
sourceRepo: git@github.com:pikasTech/HWLAB.git
|
||||||
publicHost: 74.48.78.17
|
publicHost: 74.48.78.17
|
||||||
|
NC01:
|
||||||
|
gitopsRoot: deploy/gitops/node/nc01
|
||||||
|
sourceRepo: git@github.com:pikasTech/HWLAB.git
|
||||||
lanes:
|
lanes:
|
||||||
v02:
|
v02:
|
||||||
name: v0.2
|
name: v0.2
|
||||||
@@ -501,7 +504,7 @@ lanes:
|
|||||||
livenessFailure: false
|
livenessFailure: false
|
||||||
readMode: db-or-degraded
|
readMode: db-or-degraded
|
||||||
fallbackStateSource: false
|
fallbackStateSource: false
|
||||||
envReuseServices:
|
envReuseServices: &v03EnvReuseServices
|
||||||
- hwlab-cloud-api
|
- hwlab-cloud-api
|
||||||
- hwlab-workbench-runtime
|
- hwlab-workbench-runtime
|
||||||
- hwlab-user-billing
|
- hwlab-user-billing
|
||||||
@@ -516,7 +519,7 @@ lanes:
|
|||||||
- hwlab-gateway
|
- hwlab-gateway
|
||||||
- hwlab-edge-proxy
|
- hwlab-edge-proxy
|
||||||
- hwlab-agent-skills
|
- hwlab-agent-skills
|
||||||
bootScripts:
|
bootScripts: &v03BootScripts
|
||||||
hwlab-cloud-api: deploy/runtime/boot/hwlab-cloud-api.sh
|
hwlab-cloud-api: deploy/runtime/boot/hwlab-cloud-api.sh
|
||||||
hwlab-workbench-runtime: deploy/runtime/boot/hwlab-workbench-runtime.sh
|
hwlab-workbench-runtime: deploy/runtime/boot/hwlab-workbench-runtime.sh
|
||||||
hwlab-user-billing: deploy/runtime/boot/hwlab-user-billing.sh
|
hwlab-user-billing: deploy/runtime/boot/hwlab-user-billing.sh
|
||||||
@@ -531,7 +534,7 @@ lanes:
|
|||||||
hwlab-gateway: deploy/runtime/boot/hwlab-gateway.sh
|
hwlab-gateway: deploy/runtime/boot/hwlab-gateway.sh
|
||||||
hwlab-edge-proxy: deploy/runtime/boot/hwlab-edge-proxy.sh
|
hwlab-edge-proxy: deploy/runtime/boot/hwlab-edge-proxy.sh
|
||||||
hwlab-agent-skills: deploy/runtime/boot/hwlab-agent-skills.sh
|
hwlab-agent-skills: deploy/runtime/boot/hwlab-agent-skills.sh
|
||||||
serviceDeclarations:
|
serviceDeclarations: &v03ServiceDeclarations
|
||||||
hwlab-cloud-api:
|
hwlab-cloud-api:
|
||||||
runtimeKind: bun-command
|
runtimeKind: bun-command
|
||||||
entrypoint: cmd/hwlab-cloud-api/main.ts
|
entrypoint: cmd/hwlab-cloud-api/main.ts
|
||||||
@@ -816,7 +819,7 @@ lanes:
|
|||||||
env:
|
env:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
observable: true
|
observable: true
|
||||||
envRecipe:
|
envRecipe: &v03EnvRecipe
|
||||||
osPackages:
|
osPackages:
|
||||||
- ca-certificates
|
- ca-certificates
|
||||||
- git
|
- git
|
||||||
@@ -854,7 +857,7 @@ lanes:
|
|||||||
script: tools/hwpod-ctl.ts
|
script: tools/hwpod-ctl.ts
|
||||||
- command: hwpod-compiler
|
- command: hwpod-compiler
|
||||||
script: tools/hwpod-compiler-cli.ts
|
script: tools/hwpod-compiler-cli.ts
|
||||||
bootConfig:
|
bootConfig: &v03BootConfig
|
||||||
template: default
|
template: default
|
||||||
overrides: {}
|
overrides: {}
|
||||||
configMaps:
|
configMaps:
|
||||||
@@ -1158,6 +1161,143 @@ lanes:
|
|||||||
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_INTERVAL_MS: "30000"
|
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_INTERVAL_MS: "30000"
|
||||||
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_ATTEMPTS: "5"
|
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_ATTEMPTS: "5"
|
||||||
OTEL_SERVICE_NAME: hwlab-workbench-worker
|
OTEL_SERVICE_NAME: hwlab-workbench-worker
|
||||||
|
production:
|
||||||
|
name: Production
|
||||||
|
node: NC01
|
||||||
|
sourceBranch: release
|
||||||
|
gitopsBranch: release-gitops
|
||||||
|
gitReadUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git
|
||||||
|
gitWriteUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||||
|
artifactCatalog: deploy/artifact-catalog.nc01-production.json
|
||||||
|
runtimePath: runtime-production
|
||||||
|
namespace: hwlab-production
|
||||||
|
imageTagMode: full
|
||||||
|
endpoint: https://lab.hwpod.com
|
||||||
|
publicEndpoints:
|
||||||
|
frontend: https://lab.hwpod.com
|
||||||
|
api: https://lab.hwpod.com
|
||||||
|
publicServices:
|
||||||
|
nodes:
|
||||||
|
NC01:
|
||||||
|
- name: hwlab-cloud-web-public
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: hwlab-cloud-web
|
||||||
|
port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
nodePort: 32010
|
||||||
|
frp:
|
||||||
|
enabled: false
|
||||||
|
externalPostgres:
|
||||||
|
enabled: true
|
||||||
|
serviceName: nc01-host-postgres
|
||||||
|
endpointAddress: 10.42.0.1
|
||||||
|
port: 5432
|
||||||
|
migrationSecretName: hwlab-cloud-api-production-db
|
||||||
|
migrationSecretKey: migration-database-url
|
||||||
|
workbench:
|
||||||
|
rawHwlabEventWindow:
|
||||||
|
enabled: true
|
||||||
|
maxEntries: 200
|
||||||
|
maxRetainedBytes: 1048576
|
||||||
|
envReuseServices: *v03EnvReuseServices
|
||||||
|
bootScripts: *v03BootScripts
|
||||||
|
serviceDeclarations: *v03ServiceDeclarations
|
||||||
|
services:
|
||||||
|
- serviceId: hwlab-cloud-api
|
||||||
|
shutdownDrain:
|
||||||
|
enabled: true
|
||||||
|
path: /v1/internal/workbench/drain
|
||||||
|
sleepSeconds: 3
|
||||||
|
timeoutMs: 2500
|
||||||
|
env:
|
||||||
|
HWLAB_METRICS_NAMESPACE: hwlab-production
|
||||||
|
HWLAB_CLOUD_DB_URL: secretRef:hwlab-cloud-api-production-db/database-url
|
||||||
|
HWLAB_CLOUD_DB_SSL_MODE: require
|
||||||
|
HWLAB_CLOUD_DB_CONTRACT: production-redacted-presence-only
|
||||||
|
HWLAB_ACCESS_CONTROL_REQUIRED: "1"
|
||||||
|
HWLAB_SESSION_COOKIE_SAMESITE: None
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_ID: usr_production_admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME: HWLAB Production Admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH: secretRef:hwlab-production-bootstrap-admin/password-hash
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID: key_master_server_admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_API_KEY: secretRef:hwlab-production-master-server-admin-api-key/api-key
|
||||||
|
HWLAB_USER_BILLING_URL: http://hwlab-user-billing.hwlab-production.svc.cluster.local:6670
|
||||||
|
HWLAB_WORKBENCH_RUNTIME_URL: http://hwlab-workbench-runtime.hwlab-production.svc.cluster.local:6671
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_URL: http://hwlab-project-management.hwlab-production.svc.cluster.local:6672
|
||||||
|
HWLAB_TASKTREE_URL: http://hwlab-tasktree-api.hwlab-production.svc.cluster.local:6673
|
||||||
|
HWLAB_USER_BILLING_LOGIN_RETRY_ATTEMPTS: "2"
|
||||||
|
HWLAB_USER_BILLING_LOGIN_RETRY_DELAY_MS: "250"
|
||||||
|
HWLAB_USER_BILLING_CODE_AGENT_ENABLED: "1"
|
||||||
|
HWLAB_USER_BILLING_CODE_AGENT_ESTIMATED_CREDITS: "1"
|
||||||
|
HWLAB_USER_BILLING_CODE_AGENT_USED_CREDITS: "1"
|
||||||
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
|
||||||
|
OTEL_SERVICE_NAME: hwlab-cloud-api
|
||||||
|
- serviceId: hwlab-cloud-web
|
||||||
|
env:
|
||||||
|
HWLAB_CLOUD_WEB_OPENCODE_UPSTREAM_URL: http://opencode-server.hwlab-production.svc.cluster.local:4096
|
||||||
|
- serviceId: hwlab-workbench-runtime
|
||||||
|
env:
|
||||||
|
HWLAB_WORKBENCH_RUNTIME_DB_URL: secretRef:hwlab-cloud-api-production-db/database-url
|
||||||
|
HWLAB_WORKBENCH_RUNTIME_PORT: "6671"
|
||||||
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
|
||||||
|
OTEL_SERVICE_NAME: hwlab-workbench-runtime
|
||||||
|
- serviceId: hwlab-user-billing
|
||||||
|
env:
|
||||||
|
HWLAB_USER_BILLING_DB_URL: secretRef:hwlab-cloud-api-production-db/database-url
|
||||||
|
HWLAB_USER_BILLING_DB_POOL_MAX: "2"
|
||||||
|
HWLAB_USER_BILLING_MIGRATION_TIMEOUT_SECONDS: "180"
|
||||||
|
HWLAB_USER_BILLING_BOOTSTRAP_TIMEOUT_SECONDS: "60"
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_ID: usr_production_admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME: HWLAB Production Admin
|
||||||
|
HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH: secretRef:hwlab-production-bootstrap-admin/password-hash
|
||||||
|
HWLAB_USER_BILLING_STATE_AUTHORITY: nc01-postgres
|
||||||
|
HWLAB_USER_BILLING_EXTERNAL_DB_LABEL: nc01-external-postgres
|
||||||
|
HWLAB_USER_BILLING_PORT: "6670"
|
||||||
|
HWLAB_USER_BILLING_REGISTRATION_ENABLED: "1"
|
||||||
|
HWLAB_USER_BILLING_INITIAL_CREDITS: "0"
|
||||||
|
HWLAB_USER_BILLING_CREDIT_PER_1K_TOKENS: "1"
|
||||||
|
HWLAB_USER_BILLING_MIGRATE_ON_START: "1"
|
||||||
|
HWLAB_USER_BILLING_REDIS_URL: redis://hwlab-user-billing-redis.hwlab-production.svc.cluster.local:6379/0
|
||||||
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
|
||||||
|
OTEL_SERVICE_NAME: hwlab-user-billing
|
||||||
|
- serviceId: hwlab-project-management
|
||||||
|
replicas: 1
|
||||||
|
env:
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_DB_URL: secretRef:hwlab-cloud-api-production-db/database-url
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_DB_CONNECT_TIMEOUT_MS: "5000"
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_PORT: "6672"
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_SOURCE_ROOT: /workspace/hwlab-boot/repo
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_SOURCE_ID: hwlab-production-mdtodo
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_SOURCE_NAME: HWLAB Production MDTODO
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_PROJECT_ID: project_hwlab_production
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_MDTODO_TASK_DEFAULT_LIMIT: "200"
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_MDTODO_TASK_MAX_LIMIT: "500"
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL: http://hwlab-cloud-api.hwlab-production.svc.cluster.local:6667/v1/hwpod-node-ops
|
||||||
|
HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY: secretRef:hwlab-production-master-server-admin-api-key/api-key
|
||||||
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
|
||||||
|
OTEL_SERVICE_NAME: hwlab-project-management
|
||||||
|
- serviceId: hwlab-tasktree-api
|
||||||
|
replicas: 1
|
||||||
|
env:
|
||||||
|
TASKTREE_DATABASE_URL: secretRef:hwlab-tasktree-db/database-url
|
||||||
|
TASKTREE_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233
|
||||||
|
TASKTREE_TEMPORAL_NAMESPACE: unidesk
|
||||||
|
TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-production-tasktree
|
||||||
|
TASKTREE_API_PORT: "6673"
|
||||||
|
OTEL_SERVICE_NAME: hwlab-tasktree-api
|
||||||
|
- serviceId: hwlab-tasktree-worker
|
||||||
|
replicas: 1
|
||||||
|
env:
|
||||||
|
TASKTREE_DATABASE_URL: secretRef:hwlab-tasktree-db/database-url
|
||||||
|
TASKTREE_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233
|
||||||
|
TASKTREE_TEMPORAL_NAMESPACE: unidesk
|
||||||
|
TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-production-tasktree
|
||||||
|
TASKTREE_WORKER_HEALTH_PORT: "6674"
|
||||||
|
OTEL_SERVICE_NAME: hwlab-tasktree-worker
|
||||||
|
envRecipe: *v03EnvRecipe
|
||||||
|
bootConfig: *v03BootConfig
|
||||||
services:
|
services:
|
||||||
- serviceId: hwlab-harnessrl-api
|
- serviceId: hwlab-harnessrl-api
|
||||||
namespace: hwlab-dev
|
namespace: hwlab-dev
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# R9.8.2 命名 runtime lane renderer 任务报告
|
||||||
|
|
||||||
|
## 任务定位
|
||||||
|
|
||||||
|
- MDTODO:`R9.8.2`
|
||||||
|
- Issue:`pikasTech/HWLAB#2548`
|
||||||
|
- Target:`NC01`
|
||||||
|
- targetWorkspace:`/root/hwlab-v03/.worktree/2548-production-runtime-lane`
|
||||||
|
- repository:`pikasTech/HWLAB`
|
||||||
|
- ref:`fix/2548-production-runtime-lane`
|
||||||
|
- base:`release`
|
||||||
|
- verifiedCommit:`0d9747fd5b458459349d4efa9735d3f4e459f0f8`
|
||||||
|
- PR:`pikasTech/HWLAB#2551`
|
||||||
|
|
||||||
|
## Primary workspace 轻量准备
|
||||||
|
|
||||||
|
- 只读取 AgentRun resource bundle 与以下 skill:
|
||||||
|
- `dad-dev`、`unidesk-daddev`、`unidesk-trans`。
|
||||||
|
- `git-spec`、`docs-spec`、`cli-spec`。
|
||||||
|
- `unidesk-gh`、`unidesk-cicd`。
|
||||||
|
- GitHub Issue 读取和进展写入使用 UniDesk `gh` 受控 CLI。
|
||||||
|
- 未把 runner primary workspace 当作 HWLAB 源码或 Target 验证面。
|
||||||
|
|
||||||
|
## Target 源码修改
|
||||||
|
|
||||||
|
- 将稳定 runtime lane id 识别与 `vNN` 版本默认推导拆分。
|
||||||
|
- `vNN` 继续沿用已有 branch、catalog、namespace、runtime path、endpoint 与端口默认。
|
||||||
|
- 命名 lane 从 `deploy/deploy.yaml` 读取:
|
||||||
|
- node、source/GitOps branch 与 Git read/write URL。
|
||||||
|
- artifact catalog、runtime path、namespace 与 image tag mode。
|
||||||
|
- public endpoints、service/NodePort 与 FRP 端点。
|
||||||
|
- 命名 lane 缺少必填 YAML 字段时 fail-closed,不从 lane id 推导 branch、namespace、路径、端口或 endpoint。
|
||||||
|
- 保持 `scripts/gitops-render.mjs` 单 renderer,未增加数据库 projector、migration、HTTP/SSE fallback、锁、租约或第二状态库。
|
||||||
|
- 未生成或提交 `.tekton/hwlab-nc01-production-pac.yaml`。
|
||||||
|
|
||||||
|
## Target 原入口证据
|
||||||
|
|
||||||
|
- `node scripts/run-bun.mjs scripts/src/runtime-lane.test.mjs`
|
||||||
|
- 结果:通过。
|
||||||
|
- 覆盖:命名 lane 参数解析、YAML 覆盖、namespace/runtime path、public NodePort、FRP 本地与远端端点、缺字段失败、`v03` 默认。
|
||||||
|
- 使用 Target 临时目录分别运行 `release@0d9747fd5b458459349d4efa9735d3f4e459f0f8` 原 renderer 与当前 renderer。
|
||||||
|
- `v02`:生成目录逐文件一致。
|
||||||
|
- `v03`:生成目录逐文件一致。
|
||||||
|
- `git diff --check`
|
||||||
|
- 结果:通过。
|
||||||
|
- 模块导入 smoke
|
||||||
|
- 结果:`runtime-lane.ts`、gitops-render core、runtime manifests 与 Tekton manifests 全部导入成功。
|
||||||
|
|
||||||
|
## 执行边界
|
||||||
|
|
||||||
|
- 未合并 PR。
|
||||||
|
- 受控 PR preflight:`MERGEABLE/CLEAN`,无 blocker 或 pending check。
|
||||||
|
- 未触发 PipelineRun、git mirror、Argo、runtime patch 或 rollout。
|
||||||
|
- 未操作 PK01。
|
||||||
|
- 未修改 `/root/hwlab-v03/.worktree/2548-production-pac-source`。
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
`R9.8.2` 已完成以下收口:
|
||||||
|
|
||||||
|
- runtime lane renderer 支持 YAML 显式声明的稳定 lane id。
|
||||||
|
- `v02`、`v03` 既有生成结果保持不变。
|
||||||
|
- 命名 lane 只有在 owning YAML 完整声明时才能进入现有 GitOps renderer。
|
||||||
@@ -11,14 +11,54 @@
|
|||||||
- [PJ2026-010604 公开入口](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010604-public-entry.md)
|
- [PJ2026-010604 公开入口](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010604-public-entry.md)
|
||||||
- [PJ2026-010605 运维监控](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md)
|
- [PJ2026-010605 运维监控](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010605-observability-monitoring.md)
|
||||||
|
|
||||||
目标 node/lane 的具体实现、PipelineRun 观察和运行命令仍按本仓 `AGENTS.md` 与受控 CLI 执行;需求边界、node/lane 规则和运维职责只更新 UniDesk OA。
|
职责边界:
|
||||||
|
|
||||||
|
- 目标 node/lane 的具体实现、PipelineRun 观察和运行命令按本仓 `AGENTS.md` 与受控 CLI 执行。
|
||||||
|
- 需求边界、node/lane 规则和运维职责只更新 UniDesk OA。
|
||||||
|
|
||||||
## 运行参考
|
## 运行参考
|
||||||
|
|
||||||
node/lane rollout 的 120 秒阈值是性能告警和诊断分界,不是继续盲等的理由。`trigger-current --wait` 超过阈值或返回 pending 时,先用定点 `hwlab nodes control-plane status --node <node> --lane <lane> --pipeline-run <name>`、`git-mirror status` 和 runtime workload 摘要确认卡在哪一层:PipelineRun task、Argo sync、runtime-ready、public probe 或 git mirror flush。
|
`scripts/gitops-render.mjs` 只保留一条 runtime lane 渲染链:
|
||||||
|
|
||||||
只需要给 branch-follower 或 control-plane closeout 提供一轮新的真实验证源时,优先使用文档、注释或等价的非运行面小 PR,避免把 timeout、runtime 策略或 CI/CD 逻辑改动混入验证样本。
|
- `vNN` lane:
|
||||||
|
- 继续使用既有版本号默认,包括 branch、catalog、namespace、runtime path、端口与 endpoint 推导。
|
||||||
|
- 修改 renderer 时必须用同一 source commit 对比生成目录,确认既有 lane 不漂移。
|
||||||
|
- 命名 runtime lane:
|
||||||
|
- 必须先在 `deploy/deploy.yaml` 的 `lanes.<lane>` 中声明稳定小写 id 与 node。
|
||||||
|
- 必须显式声明 source/GitOps branch、Git read/write URL、artifact catalog、runtime path、namespace 与 image tag mode。
|
||||||
|
- 必须显式声明 public endpoints、服务声明与 FRP 端点。
|
||||||
|
- 命名 lane 的 node:
|
||||||
|
- 必须在 `deploy.nodes` 中声明 `gitopsRoot` 和 `sourceRepo`。
|
||||||
|
- renderer 不从 lane 名称猜 branch、namespace、路径、端口或公开入口。
|
||||||
|
- 命名 lane 缺失 owning YAML 字段时立即失败;不得增加按具体 lane 字符串、branch 名称或端口号分支的 fallback。
|
||||||
|
- NodePort 服务只从 `lanes.<lane>.publicServices.nodes.<node>` 生成;名称、selector、service port、target port 与 node port 都由 YAML 声明。
|
||||||
|
|
||||||
PR 合并后如果目标分支被并行 PR 推进,closeout 要同时记录本 PR merge commit、当前 source head 和 ancestry 证据;只要当前 head 包含本 PR merge commit,后续 rollout 应按当前 head 收敛,避免回滚到旧 source。定点 status 可证明某个旧 PipelineRun 是否 succeeded,但最终用户入口验收必须以当前 node/lane source、GitOps revision、Argo 和 runtime 状态为准。
|
node/lane rollout 的 120 秒阈值是性能告警和诊断分界,不是继续盲等的理由:
|
||||||
|
|
||||||
runtime-ready 卡住时先看 `STATUS` 输出里的 notReady workload,再按 workload 日志和事件定位;修复应回到 source truth、PR、GitOps 和受控 sync/refresh,不把手工 patch Deployment、裸删 Pod 或临时容器改动作为交付路径。若 runtime 已 ready 但 Argo 仍 OutOfSync/Progressing,先走受控 `hwlab nodes control-plane sync|refresh --node <node> --lane <lane> --confirm` 收敛控制面,再复查 bounded status。
|
- `trigger-current --wait` 超过阈值或返回 pending 时,先执行定点 status。
|
||||||
|
- 使用 `hwlab nodes control-plane status --node <node> --lane <lane> --pipeline-run <name>` 查看 PipelineRun。
|
||||||
|
- 配合 `git-mirror status` 和 runtime workload 摘要判断卡点。
|
||||||
|
- 卡点范围包括 PipelineRun task、Argo sync、runtime-ready、public probe 或 git mirror flush。
|
||||||
|
|
||||||
|
只需要给 branch-follower 或 control-plane closeout 提供一轮新的真实验证源时:
|
||||||
|
|
||||||
|
- 优先使用文档、注释或等价的非运行面小 PR。
|
||||||
|
- 不把 timeout、runtime 策略或 CI/CD 逻辑改动混入验证样本。
|
||||||
|
|
||||||
|
PR 合并后如果目标分支被并行 PR 推进:
|
||||||
|
|
||||||
|
- closeout 同时记录本 PR merge commit、当前 source head 和 ancestry 证据。
|
||||||
|
- 当前 head 包含本 PR merge commit 时,后续 rollout 按当前 head 收敛,避免回滚到旧 source。
|
||||||
|
- 定点 status 只证明指定旧 PipelineRun 是否 succeeded。
|
||||||
|
- 最终用户入口验收以当前 node/lane source、GitOps revision、Argo 和 runtime 状态为准。
|
||||||
|
|
||||||
|
runtime-ready 卡住时:
|
||||||
|
|
||||||
|
- 先查看 `STATUS` 输出里的 notReady workload,再按 workload 日志和事件定位。
|
||||||
|
- 修复回到 source truth、PR、GitOps 和受控 sync/refresh。
|
||||||
|
- 不把手工 patch Deployment、裸删 Pod 或临时容器改动作为交付路径。
|
||||||
|
|
||||||
|
runtime 已 ready 但 Argo 仍 OutOfSync/Progressing 时:
|
||||||
|
|
||||||
|
- 走受控 `hwlab nodes control-plane sync|refresh --node <node> --lane <lane> --confirm` 收敛控制面。
|
||||||
|
- 再复查 bounded status。
|
||||||
|
|||||||
@@ -1344,35 +1344,50 @@ function namespaceForAgentRunRuntimeLane(lane) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentRunRuntimeAuthorityError(code, message) {
|
function agentRunRuntimeAuthorityError(code, message, details = {}) {
|
||||||
return Object.assign(new Error(message), {
|
return Object.assign(new Error(message), {
|
||||||
code,
|
code,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
details,
|
||||||
valuesPrinted: false
|
valuesPrinted: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function agentRunToolCredentials(env = process.env, toolCapabilities = null) {
|
function agentRunToolCredentials(env = process.env, toolCapabilities = null) {
|
||||||
const namespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TOOL_SECRET_NAMESPACE, env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE);
|
const namespace = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_TOOL_SECRET_NAMESPACE, env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE);
|
||||||
const githubSecretName = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME, "agentrun-v01-tool-github-pr");
|
|
||||||
const githubSecretKey = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_KEY, "GH_TOKEN");
|
const githubSecretKey = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_KEY, "GH_TOKEN");
|
||||||
const unideskSecretName = firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME, "agentrun-v01-tool-unidesk-ssh");
|
|
||||||
const credentials = [];
|
const credentials = [];
|
||||||
if (toolCapabilityAllowed(toolCapabilities, "github_pr")) credentials.push({
|
if (toolCapabilityAllowed(toolCapabilities, "github_pr")) {
|
||||||
|
const githubSecretName = requireAgentRunToolSecretName(env, "HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME", "github");
|
||||||
|
credentials.push({
|
||||||
tool: "github",
|
tool: "github",
|
||||||
purpose: "pull-request",
|
purpose: "pull-request",
|
||||||
secretRef: { namespace, name: githubSecretName, keys: [githubSecretKey] },
|
secretRef: { namespace, name: githubSecretName, keys: [githubSecretKey] },
|
||||||
projection: { kind: "env", envName: githubSecretKey, secretKey: githubSecretKey }
|
projection: { kind: "env", envName: githubSecretKey, secretKey: githubSecretKey }
|
||||||
});
|
});
|
||||||
if (toolCapabilityAllowed(toolCapabilities, "unidesk_ssh")) credentials.push({
|
}
|
||||||
|
if (toolCapabilityAllowed(toolCapabilities, "unidesk_ssh")) {
|
||||||
|
const unideskSecretName = requireAgentRunToolSecretName(env, "HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME", "unidesk-ssh");
|
||||||
|
credentials.push({
|
||||||
tool: "unidesk-ssh",
|
tool: "unidesk-ssh",
|
||||||
purpose: "ssh-passthrough",
|
purpose: "ssh-passthrough",
|
||||||
secretRef: { namespace, name: unideskSecretName, keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
|
secretRef: { namespace, name: unideskSecretName, keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
|
||||||
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" }
|
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" }
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requireAgentRunToolSecretName(env, envName, tool) {
|
||||||
|
const secretName = text(env[envName]);
|
||||||
|
if (secretName) return secretName;
|
||||||
|
throw agentRunRuntimeAuthorityError(
|
||||||
|
"agentrun_tool_secret_not_configured",
|
||||||
|
`${envName} is required when the ${tool} capability is enabled`,
|
||||||
|
{ tool, envName, valuesPrinted: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveToolCapabilities({ params = {}, options = {} } = {}) {
|
async function resolveToolCapabilities({ params = {}, options = {} } = {}) {
|
||||||
const ownerUserId = text(params.ownerUserId);
|
const ownerUserId = text(params.ownerUserId);
|
||||||
const accessController = options.accessController;
|
const accessController = options.accessController;
|
||||||
|
|||||||
@@ -146,11 +146,22 @@ export function agentRunFreshSessionFailure(result = {}) {
|
|||||||
|
|
||||||
export function providerCredentialSecretRef(profile, env) {
|
export function providerCredentialSecretRef(profile, env) {
|
||||||
const profileEnvKey = String(profile ?? "").toUpperCase().replace(/[^A-Z0-9]+/gu, "_");
|
const profileEnvKey = String(profile ?? "").toUpperCase().replace(/[^A-Z0-9]+/gu, "_");
|
||||||
|
const secretEnvName = `HWLAB_CODE_AGENT_AGENTRUN_${profileEnvKey}_SECRET_NAME`;
|
||||||
|
const secretName = firstNonEmpty(env[secretEnvName]);
|
||||||
|
if (!secretName) {
|
||||||
|
throw Object.assign(new Error(`AgentRun provider Secret name is not configured for profile ${profile}`), {
|
||||||
|
code: "agentrun_provider_secret_not_configured",
|
||||||
|
statusCode: 500,
|
||||||
|
profile,
|
||||||
|
envName: secretEnvName,
|
||||||
|
valuesPrinted: false
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
profile,
|
profile,
|
||||||
secretRef: {
|
secretRef: {
|
||||||
namespace: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE),
|
namespace: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE, DEFAULT_RUNNER_NAMESPACE),
|
||||||
name: firstNonEmpty(env[`HWLAB_CODE_AGENT_AGENTRUN_${profileEnvKey}_SECRET_NAME`], env[`HWLAB_CODE_AGENT_AGENTRUN_${String(profile ?? "").toUpperCase()}_SECRET_NAME`], env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_SECRET_NAME, `agentrun-v01-provider-${profile}`),
|
name: secretName,
|
||||||
keys: ["auth.json", "config.toml"],
|
keys: ["auth.json", "config.toml"],
|
||||||
mountPath: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_CODEX_HOME_MOUNT, "~/.codex")
|
mountPath: firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_CODEX_HOME_MOUNT, "~/.codex")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ test("AgentRun adapter filters resource tools and credentials through access cap
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_DEEPSEEK_SECRET_NAME: "agentrun-test-provider-deepseek",
|
||||||
HWLAB_RUNTIME_API_URL: "http://hwlab-cloud-api.hwlab-v02.svc.cluster.local:6667",
|
HWLAB_RUNTIME_API_URL: "http://hwlab-cloud-api.hwlab-v02.svc.cluster.local:6667",
|
||||||
HWLAB_RUNTIME_WEB_URL: "http://hwlab-cloud-web.hwlab-v02.svc.cluster.local:8080",
|
HWLAB_RUNTIME_WEB_URL: "http://hwlab-cloud-web.hwlab-v02.svc.cluster.local:8080",
|
||||||
HWLAB_RUNTIME_NAMESPACE: "hwlab-v02",
|
HWLAB_RUNTIME_NAMESPACE: "hwlab-v02",
|
||||||
@@ -662,8 +663,8 @@ test("cloud api /v1/agent/chat delegates v0.3 turns to AgentRun v0.1 over adapte
|
|||||||
assert.equal(Object.hasOwn(body.resourceBundleRef, "skillRefs"), false);
|
assert.equal(Object.hasOwn(body.resourceBundleRef, "skillRefs"), false);
|
||||||
assert.equal(Object.hasOwn(body.resourceBundleRef, "workspaceFiles"), false);
|
assert.equal(Object.hasOwn(body.resourceBundleRef, "workspaceFiles"), false);
|
||||||
const toolCredentials = body.executionPolicy.secretScope.toolCredentials;
|
const toolCredentials = body.executionPolicy.secretScope.toolCredentials;
|
||||||
assert.equal(toolCredentials.some((item) => item.tool === "github" && item.projection.envName === "GH_TOKEN" && item.secretRef.name === "agentrun-v01-tool-github-pr"), true);
|
assert.equal(toolCredentials.some((item) => item.tool === "github" && item.projection.envName === "GH_TOKEN" && item.secretRef.name === "agentrun-test-tool-github-pr"), true);
|
||||||
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && item.projection.envName === "UNIDESK_SSH_CLIENT_TOKEN" && item.secretRef.name === "agentrun-v01-tool-unidesk-ssh"), true);
|
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && item.projection.envName === "UNIDESK_SSH_CLIENT_TOKEN" && item.secretRef.name === "agentrun-test-tool-unidesk-ssh"), true);
|
||||||
assert.equal(body.sessionRef.sessionId, "ses_agentrun_server_test_agentrun");
|
assert.equal(body.sessionRef.sessionId, "ses_agentrun_server_test_agentrun");
|
||||||
assert.equal(body.sessionRef.metadata.hwlabProjectId, "prj_hwpod_workbench");
|
assert.equal(body.sessionRef.metadata.hwlabProjectId, "prj_hwpod_workbench");
|
||||||
assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-agentrun");
|
assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-agentrun");
|
||||||
@@ -855,6 +856,9 @@ test("cloud api /v1/agent/chat delegates v0.3 turns to AgentRun v0.1 over adapte
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_DEEPSEEK_SECRET_NAME: "agentrun-test-provider-deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
|
||||||
HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat",
|
HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat",
|
||||||
HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false",
|
HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false",
|
||||||
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false",
|
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false",
|
||||||
@@ -1138,6 +1142,7 @@ test("cloud api rejects billing failure before durable AgentRun admission withou
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_DEEPSEEK_SECRET_NAME: "agentrun-test-provider-deepseek",
|
||||||
HWLAB_ENVIRONMENT: "v03",
|
HWLAB_ENVIRONMENT: "v03",
|
||||||
HWLAB_GITOPS_PROFILE: "v03",
|
HWLAB_GITOPS_PROFILE: "v03",
|
||||||
HWLAB_USER_BILLING_CODE_AGENT_ENABLED: "1"
|
HWLAB_USER_BILLING_CODE_AGENT_ENABLED: "1"
|
||||||
@@ -1241,6 +1246,9 @@ test("cloud api preserves typed schema-invalid admission failure without claimin
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "gpt-pika",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "gpt-pika",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GPT_PIKA_SECRET_NAME: "agentrun-test-provider-gpt-pika",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
|
||||||
HWLAB_ENVIRONMENT: "v03",
|
HWLAB_ENVIRONMENT: "v03",
|
||||||
HWLAB_GITOPS_PROFILE: "v03"
|
HWLAB_GITOPS_PROFILE: "v03"
|
||||||
},
|
},
|
||||||
@@ -1447,6 +1455,9 @@ test("cloud api AgentRun adapter reports persistent thread resume when a complet
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_DEEPSEEK_SECRET_NAME: "agentrun-test-provider-deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
|
||||||
HWLAB_ENVIRONMENT: "v02",
|
HWLAB_ENVIRONMENT: "v02",
|
||||||
HWLAB_GITOPS_PROFILE: "v02"
|
HWLAB_GITOPS_PROFILE: "v02"
|
||||||
},
|
},
|
||||||
@@ -1620,6 +1631,9 @@ test("cloud api AgentRun adapter exposes invalid tool-call attribution through K
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_DEEPSEEK_SECRET_NAME: "agentrun-test-provider-deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
|
||||||
HWLAB_ENVIRONMENT: "v02",
|
HWLAB_ENVIRONMENT: "v02",
|
||||||
HWLAB_GITOPS_PROFILE: "v02"
|
HWLAB_GITOPS_PROFILE: "v02"
|
||||||
},
|
},
|
||||||
@@ -1826,6 +1840,9 @@ test("cloud api surfaces manager-side evicted-session recovery through Kafka pro
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_DEEPSEEK_SECRET_NAME: "agentrun-test-provider-deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
|
||||||
HWLAB_ENVIRONMENT: "v02",
|
HWLAB_ENVIRONMENT: "v02",
|
||||||
HWLAB_GITOPS_PROFILE: "v02"
|
HWLAB_GITOPS_PROFILE: "v02"
|
||||||
},
|
},
|
||||||
@@ -1902,7 +1919,7 @@ test("cloud api AgentRun adapter admits direct live Kafka turns without outbox p
|
|||||||
assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "minimax-m3");
|
assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "minimax-m3");
|
||||||
assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "hwlab-session-scoped");
|
assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "hwlab-session-scoped");
|
||||||
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].profile, "minimax-m3");
|
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].profile, "minimax-m3");
|
||||||
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.name, "agentrun-v01-provider-minimax-m3");
|
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.name, "agentrun-test-provider-minimax-m3");
|
||||||
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.namespace, "agentrun-v01");
|
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.namespace, "agentrun-v01");
|
||||||
return send({ id: "run_hwlab_minimax_m3", status: "pending", backendProfile: "minimax-m3", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
return send({ id: "run_hwlab_minimax_m3", status: "pending", backendProfile: "minimax-m3", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
||||||
}
|
}
|
||||||
@@ -1997,6 +2014,9 @@ test("cloud api AgentRun adapter admits direct live Kafka turns without outbox p
|
|||||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
|
||||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
||||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_MINIMAX_M3_SECRET_NAME: "agentrun-test-provider-minimax-m3",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_GITHUB_TOOL_SECRET_NAME: "agentrun-test-tool-github-pr",
|
||||||
|
HWLAB_CODE_AGENT_AGENTRUN_UNIDESK_SSH_TOOL_SECRET_NAME: "agentrun-test-tool-unidesk-ssh",
|
||||||
HWLAB_ENVIRONMENT: "v02",
|
HWLAB_ENVIRONMENT: "v02",
|
||||||
HWLAB_GITOPS_PROFILE: "v02"
|
HWLAB_GITOPS_PROFILE: "v02"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const sourceStates = new Set([
|
|||||||
"source-present",
|
"source-present",
|
||||||
"intentionally-disabled"
|
"intentionally-disabled"
|
||||||
]);
|
]);
|
||||||
const runtimeArtifactLanePattern = /^v\d{2,}$/u;
|
const runtimeArtifactLanePattern = /^[a-z][a-z0-9-]*$/u;
|
||||||
|
|
||||||
const ciArtifactIdentityEnvNames = [
|
const ciArtifactIdentityEnvNames = [
|
||||||
"HWLAB_CI_ARTIFACT_RUN_ID",
|
"HWLAB_CI_ARTIFACT_RUN_ID",
|
||||||
@@ -205,7 +205,7 @@ function printHelp() {
|
|||||||
"Commit-pinned artifact build/publish helper for controlled CI execution.",
|
"Commit-pinned artifact build/publish helper for controlled CI execution.",
|
||||||
"",
|
"",
|
||||||
"options:",
|
"options:",
|
||||||
" --lane LANE node or runtime lane vNN; default: node",
|
" --lane LANE node or a lane declared in deploy.lanes; default: node",
|
||||||
` --catalog-path PATH default: ${defaultCatalogPath}`,
|
` --catalog-path PATH default: ${defaultCatalogPath}`,
|
||||||
` --deploy-config PATH default: ${defaultDeployPath}`,
|
` --deploy-config PATH default: ${defaultDeployPath}`,
|
||||||
" --image-tag-mode MODE short or full; v02 uses full source commit IDs",
|
" --image-tag-mode MODE short or full; v02 uses full source commit IDs",
|
||||||
|
|||||||
@@ -711,9 +711,9 @@ test("artifact reuse paths preserve catalog provenance instead of current planne
|
|||||||
assert.match(artifactPublish, /envReuseRecipe: servicePlan\.envReuseRecipe \?\? null/u);
|
assert.match(artifactPublish, /envReuseRecipe: servicePlan\.envReuseRecipe \?\? null/u);
|
||||||
assert.match(artifactPublish, /componentInputHash: serviceResultValue\(env, service\.serviceId, "COMPONENT_INPUT_HASH"\) \?\? \(reusedServiceImage \? null : service\.componentInputHash \?\? null\)/u);
|
assert.match(artifactPublish, /componentInputHash: serviceResultValue\(env, service\.serviceId, "COMPONENT_INPUT_HASH"\) \?\? \(reusedServiceImage \? null : service\.componentInputHash \?\? null\)/u);
|
||||||
|
|
||||||
const gitopsRender = await readFile("scripts/gitops-render.mjs", "utf8");
|
const perServicePublish = await readFile("scripts/src/gitops-render/templates/per-service-publish.sh", "utf8");
|
||||||
assert.match(gitopsRender, /const componentInputHash = envReuse \? \(planned\.componentInputHash \|\| service\.componentInputHash \|\| ""\) : \(service\.componentInputHash \|\| ""\);/u);
|
assert.match(perServicePublish, /const componentInputHash = envReuse \? \(planned\.componentInputHash \|\| service\.componentInputHash \|\| ""\) : \(service\.componentInputHash \|\| ""\);/u);
|
||||||
assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u);
|
assert.doesNotMatch(perServicePublish, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("artifact publish ignores stale external reports for envreuse services that do not need current build artifacts", async () => {
|
test("artifact publish ignores stale external reports for envreuse services that do not need current build artifacts", async () => {
|
||||||
|
|||||||
+43
-18
@@ -24,6 +24,7 @@ import {
|
|||||||
generatedPath,
|
generatedPath,
|
||||||
gitopsPathForProfile,
|
gitopsPathForProfile,
|
||||||
imageTagForSource,
|
imageTagForSource,
|
||||||
|
isNamedRuntimeLane,
|
||||||
isRuntimeLane,
|
isRuntimeLane,
|
||||||
jsonManifest,
|
jsonManifest,
|
||||||
label,
|
label,
|
||||||
@@ -79,7 +80,7 @@ import {
|
|||||||
} from "./src/gitops-render/tekton-manifests.mjs";
|
} from "./src/gitops-render/tekton-manifests.mjs";
|
||||||
import { CLOUD_CORE_MIGRATIONS } from "../internal/db/schema.ts";
|
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"];
|
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 (!isRuntimeLane(profile)) resources.splice(1, 0, "code-agent-codex-config.yaml");
|
||||||
if (runtimeConfigMaps) resources.splice(3, 0, "configmaps.yaml");
|
if (runtimeConfigMaps) resources.splice(3, 0, "configmaps.yaml");
|
||||||
@@ -95,12 +96,12 @@ function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile ==
|
|||||||
return {
|
return {
|
||||||
apiVersion: "kustomize.config.k8s.io/v1beta1",
|
apiVersion: "kustomize.config.k8s.io/v1beta1",
|
||||||
kind: "Kustomization",
|
kind: "Kustomization",
|
||||||
namespace: namespaceNameForProfile(profile),
|
namespace: namespaceNameForProfile(profile, deploy),
|
||||||
resources
|
resources
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readme({ source, args }) {
|
function readme({ source, args, deploy }) {
|
||||||
if (args.lane === "v02") {
|
if (args.lane === "v02") {
|
||||||
return `# HWLAB v0.2 GitOps CI/CD
|
return `# HWLAB v0.2 GitOps CI/CD
|
||||||
|
|
||||||
@@ -117,6 +118,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.
|
- 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.
|
- 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.
|
- 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
|
return `# HWLAB node GitOps CI/CD
|
||||||
@@ -161,7 +177,7 @@ async function plannedFiles(args) {
|
|||||||
let artifactCatalog = await readJsonIfPresent(args.catalogPath, null);
|
let artifactCatalog = await readJsonIfPresent(args.catalogPath, null);
|
||||||
if (!artifactCatalog && isRuntimeLane(args.lane)) artifactCatalog = artifactCatalogSkeleton({ args, deploy, source });
|
if (!artifactCatalog && isRuntimeLane(args.lane)) artifactCatalog = artifactCatalogSkeleton({ args, deploy, source });
|
||||||
if (!artifactCatalog) artifactCatalog = await readJson(args.catalogPath);
|
if (!artifactCatalog) artifactCatalog = await readJson(args.catalogPath);
|
||||||
const settings = ciLaneSettings(args);
|
const settings = ciLaneSettings(args, deploy);
|
||||||
const profiles = laneRuntimeProfiles(args);
|
const profiles = laneRuntimeProfiles(args);
|
||||||
const migrationSources = isRuntimeLane(args.lane)
|
const migrationSources = isRuntimeLane(args.lane)
|
||||||
? await Promise.all(CLOUD_CORE_MIGRATIONS.map(async (migration) => ({
|
? await Promise.all(CLOUD_CORE_MIGRATIONS.map(async (migration) => ({
|
||||||
@@ -196,9 +212,9 @@ async function plannedFiles(args) {
|
|||||||
gitReadUrl: args.gitReadUrl,
|
gitReadUrl: args.gitReadUrl,
|
||||||
gitWriteUrl: args.gitWriteUrl,
|
gitWriteUrl: args.gitWriteUrl,
|
||||||
registryPrefix: args.registryPrefix,
|
registryPrefix: args.registryPrefix,
|
||||||
runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile)),
|
runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile, deploy)),
|
||||||
publicEndpoints,
|
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}`,
|
tektonPipeline: `hwlab-ci/${settings.pipelineName}`,
|
||||||
argoApplications: profiles.map((profile) => `argocd/${argoApplicationName(profile)}`)
|
argoApplications: profiles.map((profile) => `argocd/${argoApplicationName(profile)}`)
|
||||||
};
|
};
|
||||||
@@ -210,22 +226,22 @@ async function plannedFiles(args) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
putJson("source.json", sourceDescriptor);
|
putJson("source.json", sourceDescriptor);
|
||||||
putText("README.md", readme({ source, args }));
|
putText("README.md", readme({ source, args, deploy }));
|
||||||
putJson("registry/registry.yaml", registryManifest());
|
putJson("registry/registry.yaml", registryManifest());
|
||||||
if (isRuntimeLane(args.lane)) putJson("devops-infra/git-mirror.yaml", devopsInfraGitMirrorManifest(args, deploy));
|
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}/tasks.yaml`, { apiVersion: "v1", kind: "List", items: tektonTasks(args, deploy) });
|
putJson(`${settings.tektonDir}/tasks.yaml`, { apiVersion: "v1", kind: "List", items: tektonTasks(args, deploy) });
|
||||||
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
|
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
|
||||||
if (!isRuntimeLane(args.lane)) {
|
if (!isRuntimeLane(args.lane)) {
|
||||||
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy));
|
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy));
|
||||||
putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args));
|
putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args));
|
||||||
}
|
}
|
||||||
putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args }));
|
putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args, deploy }));
|
||||||
putJson("argocd/project.yaml", argoProject(args));
|
putJson("argocd/project.yaml", argoProject(args, deploy));
|
||||||
for (const profile of profiles) putJson(`argocd/application-${profile}.yaml`, argoApplication(args, profile));
|
for (const profile of profiles) putJson(`argocd/application-${profile}.yaml`, argoApplication(args, profile, deploy));
|
||||||
|
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
const namespace = namespaceNameForProfile(profile);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const includeDeviceAgent = profile === "dev";
|
const includeDeviceAgent = profile === "dev";
|
||||||
const externalPostgres = externalPostgresConfigForLane(deploy, profile, args);
|
const externalPostgres = externalPostgresConfigForLane(deploy, profile, args);
|
||||||
const workbenchRedis = workbenchRuntimeRedisConfigForProfile(deploy, profile);
|
const workbenchRedis = workbenchRuntimeRedisConfigForProfile(deploy, profile);
|
||||||
@@ -236,9 +252,9 @@ async function plannedFiles(args) {
|
|||||||
runtimeNamespace.metadata.name = namespace;
|
runtimeNamespace.metadata.name = namespace;
|
||||||
label(runtimeNamespace.metadata, profileLabels);
|
label(runtimeNamespace.metadata, profileLabels);
|
||||||
annotate(runtimeNamespace.metadata, annotations);
|
annotate(runtimeNamespace.metadata, annotations);
|
||||||
const runtimePath = runtimePathForProfile(profile);
|
const runtimePath = runtimePathForProfile(profile, deploy);
|
||||||
const endpoints = profileEndpoint(args, profile);
|
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);
|
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
|
||||||
if (!isRuntimeLane(profile)) 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, nodeId: args.nodeId }));
|
putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile, deploy, nodeId: args.nodeId }));
|
||||||
@@ -247,10 +263,19 @@ async function plannedFiles(args) {
|
|||||||
if (runtimeSecretPlane) putJson(`${runtimePath}/external-secrets.yaml`, runtimeSecretPlaneManifest({ config: runtimeSecretPlane, namespace, labels: profileLabels, annotations }));
|
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}/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 }));
|
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}/external-postgres.yaml`, externalPostgresManifest({
|
||||||
if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSources, source, image: runtimePostgresImageForProfile(deploy, profile) }));
|
profile,
|
||||||
if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source }));
|
config: externalPostgres,
|
||||||
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source }));
|
source,
|
||||||
|
deploy,
|
||||||
|
migrationSources,
|
||||||
|
catalog: artifactCatalog,
|
||||||
|
registryPrefix: args.registryPrefix,
|
||||||
|
useDeployImages: args.useDeployImages
|
||||||
|
}));
|
||||||
|
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}/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 (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 }));
|
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
|
||||||
|
|||||||
@@ -8,10 +8,32 @@ import test from "node:test";
|
|||||||
import { parse as parseYaml } from "yaml";
|
import { parse as parseYaml } from "yaml";
|
||||||
|
|
||||||
import { configString } from "./src/gitops-render/core.mjs";
|
import { configString } from "./src/gitops-render/core.mjs";
|
||||||
import { opencodeEgressProxyUrlForProfile } from "./src/gitops-render/runtime-manifests.mjs";
|
import { externalPostgresConfigForLane, externalPostgresManifest, opencodeEgressProxyUrlForProfile } from "./src/gitops-render/runtime-manifests.mjs";
|
||||||
|
|
||||||
import { CLOUD_CORE_MIGRATIONS, CLOUD_TRANSACTIONAL_REALTIME_MIGRATION_ID } from "../internal/db/schema.ts";
|
import { CLOUD_CORE_MIGRATIONS, CLOUD_TRANSACTIONAL_REALTIME_MIGRATION_ID } from "../internal/db/schema.ts";
|
||||||
|
|
||||||
|
test("production migration uses its YAML-declared database Secret key", async () => {
|
||||||
|
const deploy = parseYaml(await readFile("deploy/deploy.yaml", "utf8"));
|
||||||
|
assert.deepEqual(deploy.lanes.production.workbench.rawHwlabEventWindow, {
|
||||||
|
enabled: true,
|
||||||
|
maxEntries: 200,
|
||||||
|
maxRetainedBytes: 1048576
|
||||||
|
});
|
||||||
|
const config = externalPostgresConfigForLane(deploy, "production", { nodeId: "NC01" });
|
||||||
|
const manifest = externalPostgresManifest({
|
||||||
|
profile: "production",
|
||||||
|
config,
|
||||||
|
source: { full: "a".repeat(40), short: "a".repeat(12), imageTag: "a".repeat(12) },
|
||||||
|
deploy,
|
||||||
|
migrationSources: [{ path: "0001_test.sql", sql: "select 1;" }]
|
||||||
|
});
|
||||||
|
const job = manifest.items.find((item) => item.kind === "Job");
|
||||||
|
assert.deepEqual(job?.spec?.template?.spec?.containers?.[0]?.env?.[0]?.valueFrom?.secretKeyRef, {
|
||||||
|
name: "hwlab-cloud-api-production-db",
|
||||||
|
key: "migration-database-url"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function taskByName(pipeline, name) {
|
function taskByName(pipeline, name) {
|
||||||
const task = pipeline.spec.tasks.find((item) => item.name === name);
|
const task = pipeline.spec.tasks.find((item) => item.name === name);
|
||||||
assert.ok(task, `missing task ${name}`);
|
assert.ok(task, `missing task ${name}`);
|
||||||
@@ -296,6 +318,8 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
|||||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.2-gitops:refs\/mirror-stage\/heads\/v0\.2-gitops/u);
|
assert.match(gitMirrorScript, /refs\/heads\/v0\.2-gitops:refs\/mirror-stage\/heads\/v0\.2-gitops/u);
|
||||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.3:refs\/mirror-stage\/heads\/v0\.3/u);
|
assert.match(gitMirrorScript, /refs\/heads\/v0\.3:refs\/mirror-stage\/heads\/v0\.3/u);
|
||||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.3-gitops:refs\/mirror-stage\/heads\/v0\.3-gitops/u);
|
assert.match(gitMirrorScript, /refs\/heads\/v0\.3-gitops:refs\/mirror-stage\/heads\/v0\.3-gitops/u);
|
||||||
|
assert.match(gitMirrorScript, /refs\/heads\/release:refs\/mirror-stage\/heads\/release/u);
|
||||||
|
assert.match(gitMirrorScript, /refs\/heads\/release-gitops:refs\/mirror-stage\/heads\/release-gitops/u);
|
||||||
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14:refs\/mirror-stage\/heads\/G14/u);
|
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14:refs\/mirror-stage\/heads\/G14/u);
|
||||||
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14-gitops:refs\/mirror-stage\/heads\/G14-gitops/u);
|
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14-gitops:refs\/mirror-stage\/heads\/G14-gitops/u);
|
||||||
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/\*:refs\/mirror-stage\/heads\/\*/u);
|
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/\*:refs\/mirror-stage\/heads\/\*/u);
|
||||||
@@ -311,7 +335,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
|||||||
assert.match(gitMirrorScript, /refs\/unidesk\/snapshots\/hwlab-node-runtime/u);
|
assert.match(gitMirrorScript, /refs\/unidesk\/snapshots\/hwlab-node-runtime/u);
|
||||||
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.2/u);
|
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.2/u);
|
||||||
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.3/u);
|
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.3/u);
|
||||||
assert.match(gitMirrorScript, /for gitops_branch in 'v0\.2-gitops' 'v0\.3-gitops'/u);
|
assert.match(gitMirrorScript, /for gitops_branch in 'release-gitops' 'v0\.2-gitops' 'v0\.3-gitops'/u);
|
||||||
assert.match(gitMirrorScript, /keeps local \$name ahead of GitHub/u);
|
assert.match(gitMirrorScript, /keeps local \$name ahead of GitHub/u);
|
||||||
assert.doesNotMatch(gitMirror, /not allowlisted/u);
|
assert.doesNotMatch(gitMirror, /not allowlisted/u);
|
||||||
assert.doesNotMatch(gitMirror, /path allowlist/u);
|
assert.doesNotMatch(gitMirror, /path allowlist/u);
|
||||||
@@ -731,7 +755,25 @@ test("v03 render keeps node identity as data instead of generated structure", as
|
|||||||
assert.ok((projectManagementContainer?.volumeMounts ?? []).some((mount) => mount.name === "project-management-bootstrap" && mount.mountPath === "/etc/hwlab/project-management"));
|
assert.ok((projectManagementContainer?.volumeMounts ?? []).some((mount) => mount.name === "project-management-bootstrap" && mount.mountPath === "/etc/hwlab/project-management"));
|
||||||
assert.ok((projectManagementContainer?.env ?? []).some((entry) => entry.name === "HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH" && entry.value === "/etc/hwlab/project-management/sources.json"));
|
assert.ok((projectManagementContainer?.env ?? []).some((entry) => entry.name === "HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH" && entry.value === "/etc/hwlab/project-management/sources.json"));
|
||||||
const externalPostgres = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-postgres.yaml"), "utf8"));
|
const externalPostgres = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-postgres.yaml"), "utf8"));
|
||||||
assert.deepEqual((externalPostgres.items ?? []).map((item) => item.kind), ["Service", "EndpointSlice"]);
|
assert.deepEqual((externalPostgres.items ?? []).map((item) => item.kind), ["ConfigMap", "Job", "Service", "EndpointSlice"]);
|
||||||
|
const externalPostgresMigrationConfig = (externalPostgres.items ?? []).find((item) => item.kind === "ConfigMap");
|
||||||
|
const externalPostgresMigrationJob = (externalPostgres.items ?? []).find((item) => item.kind === "Job");
|
||||||
|
assert.deepEqual(
|
||||||
|
Object.keys(externalPostgresMigrationConfig?.data ?? {}).filter((name) => name.endsWith(".sql")),
|
||||||
|
CLOUD_CORE_MIGRATIONS.map((migration) => path.basename(migration.path))
|
||||||
|
);
|
||||||
|
assert.match(externalPostgresMigrationConfig?.data?.["run.mjs"] ?? "", /DATABASE_URL is required/u);
|
||||||
|
assert.match(externalPostgresMigrationConfig?.data?.["run.mjs"] ?? "", /hwlab-runtime-migration-started/u);
|
||||||
|
assert.equal(externalPostgresMigrationConfig?.metadata?.annotations?.["argocd.argoproj.io/sync-wave"], "-2");
|
||||||
|
assert.equal(externalPostgresMigrationJob?.metadata?.annotations?.["argocd.argoproj.io/sync-wave"], "-1");
|
||||||
|
assert.equal(
|
||||||
|
externalPostgresMigrationJob?.spec?.template?.spec?.containers?.[0]?.image,
|
||||||
|
`127.0.0.1:5000/hwlab/hwlab-cloud-api-env@sha256:${"1".repeat(64)}`
|
||||||
|
);
|
||||||
|
assert.deepEqual(externalPostgresMigrationJob?.spec?.template?.spec?.containers?.[0]?.env?.[0]?.valueFrom?.secretKeyRef, {
|
||||||
|
name: "hwlab-cloud-api-v03-db",
|
||||||
|
key: "database-url"
|
||||||
|
});
|
||||||
const externalPostgresSlice = (externalPostgres.items ?? []).find((item) => item.kind === "EndpointSlice");
|
const externalPostgresSlice = (externalPostgres.items ?? []).find((item) => item.kind === "EndpointSlice");
|
||||||
assert.equal(externalPostgresSlice?.metadata?.name, "jd01-pk01-platform-postgres-host");
|
assert.equal(externalPostgresSlice?.metadata?.name, "jd01-pk01-platform-postgres-host");
|
||||||
assert.equal(externalPostgresSlice?.metadata?.labels?.["kubernetes.io/service-name"], "jd01-pk01-platform-postgres");
|
assert.equal(externalPostgresSlice?.metadata?.labels?.["kubernetes.io/service-name"], "jd01-pk01-platform-postgres");
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const defaultPublishReportPath = tempReportPath("dev-artifacts.json");
|
|||||||
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
||||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||||
const runtimeArtifactLanePattern = /^v\d{2,}$/u;
|
const runtimeArtifactLanePattern = /^[a-z][a-z0-9-]*$/u;
|
||||||
|
|
||||||
const publishReadyStatuses = new Set(["published", "reused"]);
|
const publishReadyStatuses = new Set(["published", "reused"]);
|
||||||
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
|
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import {
|
|||||||
applyRuntimeLaneDeployConfig,
|
applyRuntimeLaneDeployConfig,
|
||||||
defaultEndpointForRuntimeLane,
|
defaultEndpointForRuntimeLane,
|
||||||
defaultPortForRuntimeLane,
|
defaultPortForRuntimeLane,
|
||||||
|
isNamedRuntimeLane,
|
||||||
isRuntimeLane,
|
isRuntimeLane,
|
||||||
|
isVersionRuntimeLane,
|
||||||
|
runtimeNamespaceForRuntimeLane,
|
||||||
|
runtimeRelativePathForRuntimeLane,
|
||||||
runtimeLaneMirrorSpecs,
|
runtimeLaneMirrorSpecs,
|
||||||
runtimeLaneConfig,
|
runtimeLaneConfig,
|
||||||
versionNameForRuntimeLane
|
versionNameForRuntimeLane
|
||||||
@@ -245,7 +249,7 @@ function parseArgs(argv) {
|
|||||||
else if (arg === "--help" || arg === "-h") args.help = true;
|
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||||
else throw new Error(`unknown argument ${arg}`);
|
else throw new Error(`unknown argument ${arg}`);
|
||||||
}
|
}
|
||||||
if (isRuntimeLane(args.lane)) {
|
if (isVersionRuntimeLane(args.lane)) {
|
||||||
const versionName = versionNameForRuntimeLane(args.lane);
|
const versionName = versionNameForRuntimeLane(args.lane);
|
||||||
const defaultLaneEndpoint = defaultEndpointForRuntimeLane(args.lane);
|
const defaultLaneEndpoint = defaultEndpointForRuntimeLane(args.lane);
|
||||||
if (args.sourceBranch === defaultBranch) args.sourceBranch = versionName;
|
if (args.sourceBranch === defaultBranch) args.sourceBranch = versionName;
|
||||||
@@ -261,7 +265,7 @@ function parseArgs(argv) {
|
|||||||
if (!args.gitWriteUrl) args.gitWriteUrl = args.sourceRepo;
|
if (!args.gitWriteUrl) args.gitWriteUrl = args.sourceRepo;
|
||||||
assert.ok(args.lane === "node" || isRuntimeLane(args.lane), `unknown lane ${args.lane}`);
|
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}`);
|
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);
|
const versionName = versionNameForRuntimeLane(args.lane);
|
||||||
assert.equal(args.sourceBranch, versionName, `${args.lane} source branch must be ${versionName}`);
|
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`);
|
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/*.",
|
"Render node-scoped Kubernetes-native CI/CD manifests from built-in Tekton CI primitives, deploy/deploy.yaml, and deploy/k8s/*.",
|
||||||
"",
|
"",
|
||||||
"options:",
|
"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`,
|
` --node NODE deployment node id from deploy.nodes; runtime lanes default from deploy.lanes.<lane>.node`,
|
||||||
` --out DIR default: ${defaultOutDir}`,
|
` --out DIR default: ${defaultOutDir}`,
|
||||||
` --gitops-root DIR path inside GitOps repo; default comes from deploy.nodes.<node>.gitopsRoot`,
|
` --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 }) {
|
function artifactCatalogSkeleton({ args, deploy, source }) {
|
||||||
const environment = artifactEnvironment(args);
|
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);
|
const tag = imageTagForSource(args, source);
|
||||||
return {
|
return {
|
||||||
catalogVersion: "v1",
|
catalogVersion: "v1",
|
||||||
@@ -869,13 +873,13 @@ function artifactCatalogSkeletonService({ args, deploy, source, serviceId, envir
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function namespaceNameForProfile(profile) {
|
function namespaceNameForProfile(profile, deploy = null) {
|
||||||
if (isRuntimeLane(profile)) return `hwlab-${profile}`;
|
if (isRuntimeLane(profile)) return runtimeNamespaceForRuntimeLane(deploy, profile);
|
||||||
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
|
return profile === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||||
}
|
}
|
||||||
|
|
||||||
function runtimePathForProfile(profile) {
|
function runtimePathForProfile(profile, deploy = null) {
|
||||||
if (isRuntimeLane(profile)) return `runtime-${profile}`;
|
if (isRuntimeLane(profile)) return runtimeRelativePathForRuntimeLane(deploy, profile);
|
||||||
return profile === "prod" ? "runtime-prod" : "runtime-dev";
|
return profile === "prod" ? "runtime-prod" : "runtime-dev";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,8 +896,8 @@ function gitopsRootForArgs(args) {
|
|||||||
return String(args.gitopsRoot || defaultOutDir).replace(/\/+$/u, "");
|
return String(args.gitopsRoot || defaultOutDir).replace(/\/+$/u, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function gitopsPathForProfile(args, profile) {
|
function gitopsPathForProfile(args, profile, deploy = null) {
|
||||||
return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile)}`;
|
return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile, deploy)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gitopsPathFor(args, relativePath) {
|
function gitopsPathFor(args, relativePath) {
|
||||||
@@ -911,9 +915,9 @@ function laneRuntimeProfiles(args) {
|
|||||||
return isRuntimeLane(args.lane) ? [args.lane] : ["dev", "prod"];
|
return isRuntimeLane(args.lane) ? [args.lane] : ["dev", "prod"];
|
||||||
}
|
}
|
||||||
|
|
||||||
function laneNames(args) {
|
function laneNames(args, deploy = null) {
|
||||||
if (isRuntimeLane(args.lane)) {
|
if (isRuntimeLane(args.lane)) {
|
||||||
const namespace = namespaceNameForProfile(args.lane);
|
const namespace = namespaceNameForProfile(args.lane, deploy);
|
||||||
return {
|
return {
|
||||||
gitopsTarget: args.lane,
|
gitopsTarget: args.lane,
|
||||||
pipeline: `${namespace}-ci-image-publish`,
|
pipeline: `${namespace}-ci-image-publish`,
|
||||||
@@ -922,7 +926,7 @@ function laneNames(args) {
|
|||||||
serviceAccount: `${namespace}-tekton-runner`,
|
serviceAccount: `${namespace}-tekton-runner`,
|
||||||
pipelineRunPrefix: `${namespace}-ci-poll-`,
|
pipelineRunPrefix: `${namespace}-ci-poll-`,
|
||||||
runtimeNamespace: namespace,
|
runtimeNamespace: namespace,
|
||||||
runtimePath: gitopsPathForProfile(args, args.lane),
|
runtimePath: gitopsPathForProfile(args, args.lane, deploy),
|
||||||
argoApplications: [`argocd/hwlab-node-${args.lane}`]
|
argoApplications: [`argocd/hwlab-node-${args.lane}`]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1177,9 +1181,11 @@ function gitopsRootNodeId(gitopsRoot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runtimeFrpConfigForProfile(deploy, profile, args = {}) {
|
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 fallbackEdgeRemotePort = isRuntimeLane(profile) ? fallbackWebRemotePort + 1 : profile === "prod" ? 18667 : 17667;
|
||||||
const baseFrp = isRuntimeLane(profile) ? runtimeLaneConfig(deploy, profile)?.frp : deploy?.frp;
|
const baseFrp = isRuntimeLane(profile) ? runtimeLaneConfig(deploy, profile)?.frp : deploy?.frp;
|
||||||
|
if (namedRuntimeLane && baseFrp?.enabled === false) return { enabled: false };
|
||||||
const nodeId = effectiveSecretPlaneNodeId(args);
|
const nodeId = effectiveSecretPlaneNodeId(args);
|
||||||
const nodeFrp = nodeId ? baseFrp?.nodes?.[nodeId] : null;
|
const nodeFrp = nodeId ? baseFrp?.nodes?.[nodeId] : null;
|
||||||
const frp = nodeFrp && typeof nodeFrp === "object" && !Array.isArray(nodeFrp) ? { ...baseFrp, ...nodeFrp } : baseFrp;
|
const frp = nodeFrp && typeof nodeFrp === "object" && !Array.isArray(nodeFrp) ? { ...baseFrp, ...nodeFrp } : baseFrp;
|
||||||
@@ -1190,17 +1196,32 @@ function runtimeFrpConfigForProfile(deploy, profile, args = {}) {
|
|||||||
.map((proxy) => [String(proxy.endpointId || proxy.name || ""), proxy]));
|
.map((proxy) => [String(proxy.endpointId || proxy.name || ""), proxy]));
|
||||||
const proxyValue = (endpointId, key, fallback) => {
|
const proxyValue = (endpointId, key, fallback) => {
|
||||||
const value = proxyByEndpoint.get(endpointId)?.[key];
|
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 proxyPort = (endpointId, fallback, key = "remotePort") => {
|
||||||
const value = proxyByEndpoint.get(endpointId)?.remotePort;
|
const value = proxyByEndpoint.get(endpointId)?.[key];
|
||||||
return Number.isInteger(value) ? value : fallback;
|
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 {
|
return {
|
||||||
|
enabled: true,
|
||||||
serverAddr: typeof server?.address === "string" && server.address.trim().length > 0 ? server.address.trim() : "74.48.78.17",
|
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,
|
serverPort: Number.isInteger(server?.bindPort) ? server.bindPort : 7000,
|
||||||
webRemotePort: proxyPort("frontend", fallbackWebRemotePort),
|
webRemotePort: proxyPort("frontend", fallbackWebRemotePort),
|
||||||
edgeRemotePort: proxyPort("api", fallbackEdgeRemotePort),
|
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),
|
webProxyName: proxyValue("frontend", "name", null),
|
||||||
edgeProxyName: proxyValue("api", "name", null),
|
edgeProxyName: proxyValue("api", "name", null),
|
||||||
authSecretName: typeof frp?.auth?.secretName === "string" && frp.auth.secretName.trim().length > 0 ? frp.auth.secretName.trim() : null,
|
authSecretName: typeof frp?.auth?.secretName === "string" && frp.auth.secretName.trim().length > 0 ? frp.auth.secretName.trim() : null,
|
||||||
@@ -1347,7 +1368,7 @@ function workbenchRuntimeRedisConfigForProfile(deploy, profile) {
|
|||||||
if (!redis || redis.enabled !== true) return null;
|
if (!redis || redis.enabled !== true) return null;
|
||||||
configObject(redis, label);
|
configObject(redis, label);
|
||||||
const namespace = requiredConfigString(redis, "namespace", 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 serviceName = requiredConfigString(redis, "serviceName", label);
|
||||||
const image = requiredConfigString(redis, "image", label);
|
const image = requiredConfigString(redis, "image", label);
|
||||||
const port = requiredConfigPositiveInteger(redis, "port", label);
|
const port = requiredConfigPositiveInteger(redis, "port", label);
|
||||||
@@ -1446,7 +1467,7 @@ function upsertV02MetricsPort(service) {
|
|||||||
function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = defaultBranch, gitReadUrl, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", nodeId = "node", useDeployImages = false, metricsSidecarSha256 = null }) {
|
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 result = cloneJson(workloads);
|
||||||
const deployServices = deployServicesForProfile(deploy, profile);
|
const deployServices = deployServicesForProfile(deploy, profile);
|
||||||
const namespace = namespaceNameForProfile(profile);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const profileLabel = runtimeLabelForProfile(profile);
|
const profileLabel = runtimeLabelForProfile(profile);
|
||||||
const gitopsTarget = gitopsTargetForProfile(profile);
|
const gitopsTarget = gitopsTargetForProfile(profile);
|
||||||
const runtimeLane = isRuntimeLane(profile);
|
const runtimeLane = isRuntimeLane(profile);
|
||||||
@@ -1737,6 +1758,7 @@ function transformServices({ services, namespace, labels, annotations, profile =
|
|||||||
|
|
||||||
function observabilityManifest({ deploy, profile, 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}`);
|
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 includePrometheusOperatorResources = runtimePrometheusOperatorResourcesEnabled(deploy, profile);
|
||||||
const baseLabels = {
|
const baseLabels = {
|
||||||
...labels,
|
...labels,
|
||||||
@@ -1792,7 +1814,7 @@ function observabilityManifest({ deploy, profile, namespace, labels, annotations
|
|||||||
expr: `up{namespace="${namespace}"} == 0`,
|
expr: `up{namespace="${namespace}"} == 0`,
|
||||||
for: "5m",
|
for: "5m",
|
||||||
labels: { severity: "warning", lane: profile },
|
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`,
|
alert: `HWLAB${profile.toUpperCase()}ServiceHealthProbeFailed`,
|
||||||
@@ -1829,7 +1851,7 @@ export {
|
|||||||
effectiveSecretPlaneNodeId, ensureObject, envReuseBootShForContainer, envReuseServiceIdsForLane,
|
effectiveSecretPlaneNodeId, ensureObject, envReuseBootShForContainer, envReuseServiceIdsForLane,
|
||||||
generatedPath, gitopsPathFor, gitopsPathForProfile, gitopsRootForArgs, gitopsRootNodeId,
|
generatedPath, gitopsPathFor, gitopsPathForProfile, gitopsRootForArgs, gitopsRootNodeId,
|
||||||
gitopsTargetForProfile, gitValue, imageFor, imageTagForSource,
|
gitopsTargetForProfile, gitValue, imageFor, imageTagForSource,
|
||||||
imageTagFromReference, isRuntimeLane, isV02RemovedServiceId, jsonManifest,
|
imageTagFromReference, isNamedRuntimeLane, isRuntimeLane, isV02RemovedServiceId, jsonManifest,
|
||||||
k8sLabelHash, label, laneNames, laneRuntimeProfiles, mergeEnvMaps,
|
k8sLabelHash, label, laneNames, laneRuntimeProfiles, mergeEnvMaps,
|
||||||
moonBridgeImage, moonBridgeSourceRef, namespaceNameForProfile, namespaceScopedHost,
|
moonBridgeImage, moonBridgeSourceRef, namespaceNameForProfile, namespaceScopedHost,
|
||||||
normalizeAccessControlUser, normalizeNavProfile, normalizeProbeTiming,
|
normalizeAccessControlUser, normalizeNavProfile, normalizeProbeTiming,
|
||||||
|
|||||||
@@ -44,15 +44,16 @@ import {
|
|||||||
versionNameForRuntimeLane
|
versionNameForRuntimeLane
|
||||||
} from "./core.mjs";
|
} from "./core.mjs";
|
||||||
|
|
||||||
function argoProject(args = { lane: "node" }) {
|
function argoProject(args = { lane: "node" }, deploy = null) {
|
||||||
if (isRuntimeLane(args.lane)) {
|
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 {
|
return {
|
||||||
apiVersion: "argoproj.io/v1alpha1",
|
apiVersion: "argoproj.io/v1alpha1",
|
||||||
kind: "AppProject",
|
kind: "AppProject",
|
||||||
metadata: { name: namespace, namespace: "argocd" },
|
metadata: { name: namespace, namespace: "argocd" },
|
||||||
spec: {
|
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],
|
sourceRepos: [args.gitReadUrl],
|
||||||
destinations: [{ server: "https://kubernetes.default.svc", namespace }],
|
destinations: [{ server: "https://kubernetes.default.svc", namespace }],
|
||||||
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
|
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
|
||||||
@@ -77,9 +78,9 @@ function argoProject(args = { lane: "node" }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function argoApplication(args, profile = "dev") {
|
function argoApplication(args, profile = "dev", deploy = null) {
|
||||||
const namespace = namespaceNameForProfile(profile);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const runtimePath = runtimePathForProfile(profile);
|
const runtimePath = runtimePathForProfile(profile, deploy);
|
||||||
const gitopsTarget = gitopsTargetForProfile(profile);
|
const gitopsTarget = gitopsTargetForProfile(profile);
|
||||||
const project = isRuntimeLane(profile) ? namespace : "hwlab-node";
|
const project = isRuntimeLane(profile) ? namespace : "hwlab-node";
|
||||||
const repoURL = isRuntimeLane(profile) ? args.gitReadUrl : args.sourceRepo;
|
const repoURL = isRuntimeLane(profile) ? args.gitReadUrl : args.sourceRepo;
|
||||||
@@ -93,7 +94,7 @@ function argoApplication(args, profile = "dev") {
|
|||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
project,
|
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 },
|
destination: { server: "https://kubernetes.default.svc", namespace },
|
||||||
syncPolicy: { automated: { prune: isRuntimeLane(profile), selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
|
syncPolicy: { automated: { prune: isRuntimeLane(profile), selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
|
||||||
}
|
}
|
||||||
@@ -101,12 +102,13 @@ function argoApplication(args, profile = "dev") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nodeFrpcManifest({ profile = "dev", source = null, deploy = null, args = {} } = {}) {
|
function nodeFrpcManifest({ profile = "dev", source = null, deploy = null, args = {} } = {}) {
|
||||||
const namespace = namespaceNameForProfile(profile);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const profileLabel = runtimeLabelForProfile(profile);
|
const profileLabel = runtimeLabelForProfile(profile);
|
||||||
const deploymentName = isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc";
|
const deploymentName = isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc";
|
||||||
const configName = `${deploymentName}-config`;
|
const configName = `${deploymentName}-config`;
|
||||||
const proxyPrefix = isRuntimeLane(profile) ? `hwlab-${profile}` : profile === "prod" ? "hwlab-node-prod" : "hwlab-node";
|
const proxyPrefix = isRuntimeLane(profile) ? `hwlab-${profile}` : profile === "prod" ? "hwlab-node-prod" : "hwlab-node";
|
||||||
const frpConfig = runtimeFrpConfigForProfile(deploy, profile, args);
|
const frpConfig = runtimeFrpConfigForProfile(deploy, profile, args);
|
||||||
|
if (frpConfig.enabled === false) return { apiVersion: "v1", kind: "List", items: [] };
|
||||||
const webProxyName = frpConfig.webProxyName || `${proxyPrefix}-cloud-web`;
|
const webProxyName = frpConfig.webProxyName || `${proxyPrefix}-cloud-web`;
|
||||||
const edgeProxyName = frpConfig.edgeProxyName || `${proxyPrefix}-edge-proxy`;
|
const edgeProxyName = frpConfig.edgeProxyName || `${proxyPrefix}-edge-proxy`;
|
||||||
const labels = {
|
const labels = {
|
||||||
@@ -131,15 +133,15 @@ ${authConfigText}
|
|||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "${webProxyName}"
|
name = "${webProxyName}"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
localIP = "hwlab-cloud-web.${namespace}.svc.cluster.local"
|
localIP = "${frpConfig.webLocalHost || `hwlab-cloud-web.${namespace}.svc.cluster.local`}"
|
||||||
localPort = 8080
|
localPort = ${frpConfig.webLocalPort}
|
||||||
remotePort = ${frpConfig.webRemotePort}
|
remotePort = ${frpConfig.webRemotePort}
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "${edgeProxyName}"
|
name = "${edgeProxyName}"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
localIP = "hwlab-edge-proxy.${namespace}.svc.cluster.local"
|
localIP = "${frpConfig.edgeLocalHost || `hwlab-edge-proxy.${namespace}.svc.cluster.local`}"
|
||||||
localPort = 6667
|
localPort = ${frpConfig.edgeLocalPort}
|
||||||
remotePort = ${frpConfig.edgeRemotePort}
|
remotePort = ${frpConfig.edgeRemotePort}
|
||||||
`;
|
`;
|
||||||
const configSha256 = createHash("sha256").update(configText).digest("hex");
|
const configSha256 = createHash("sha256").update(configText).digest("hex");
|
||||||
@@ -194,7 +196,7 @@ remotePort = ${frpConfig.edgeRemotePort}
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = defaultBranch, sourceRepo = defaultSourceRepo, gitReadUrl, deploy = null, registryPrefix = defaultRegistryPrefix, catalog = null, useDeployImages = false, metricsSidecarSha256 = null } = {}) {
|
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 bridgeServiceId = "hwlab-cloud-api";
|
||||||
const runtimeLane = isRuntimeLane(profile);
|
const runtimeLane = isRuntimeLane(profile);
|
||||||
const digestPin = runtimeLane;
|
const digestPin = runtimeLane;
|
||||||
@@ -642,8 +644,8 @@ function runtimePostgresImageForProfile(deploy, profile) {
|
|||||||
return typeof image === "string" && image.trim().length > 0 ? image.trim() : "postgres:16-alpine";
|
return typeof image === "string" && image.trim().length > 0 ? image.trim() : "postgres:16-alpine";
|
||||||
}
|
}
|
||||||
|
|
||||||
function v02PostgresManifest({ profile = "v02", migrationSources, source, image = "postgres:16-alpine" }) {
|
function v02PostgresManifest({ profile = "v02", migrationSources, source, image = "postgres:16-alpine", deploy = null }) {
|
||||||
const namespace = namespaceNameForProfile(profile);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const name = `${namespace}-postgres`;
|
const name = `${namespace}-postgres`;
|
||||||
const dbName = `hwlab_${profile}`;
|
const dbName = `hwlab_${profile}`;
|
||||||
const labels = {
|
const labels = {
|
||||||
@@ -735,11 +737,35 @@ function externalPostgresConfigForLane(deploy, profile, args = {}) {
|
|||||||
assert.ok(typeof effective.endpointAddress === "string" && effective.endpointAddress.length > 0, `deploy.lanes.${profile}.externalPostgres.endpointAddress is required`);
|
assert.ok(typeof effective.endpointAddress === "string" && effective.endpointAddress.length > 0, `deploy.lanes.${profile}.externalPostgres.endpointAddress is required`);
|
||||||
const port = Number(effective.port ?? 5432);
|
const port = Number(effective.port ?? 5432);
|
||||||
assert.ok(Number.isInteger(port) && port > 0 && port <= 65535, `deploy.lanes.${profile}.externalPostgres.port must be a valid TCP port`);
|
assert.ok(Number.isInteger(port) && port > 0 && port <= 65535, `deploy.lanes.${profile}.externalPostgres.port must be a valid TCP port`);
|
||||||
return { serviceName: effective.serviceName, endpointAddress: effective.endpointAddress, port };
|
const migrationSecretName = effective.migrationSecretName;
|
||||||
|
const migrationSecretKey = effective.migrationSecretKey;
|
||||||
|
assert.equal(typeof migrationSecretName === "string", typeof migrationSecretKey === "string", `deploy.lanes.${profile}.externalPostgres migration Secret name and key must be declared together`);
|
||||||
|
return {
|
||||||
|
serviceName: effective.serviceName,
|
||||||
|
endpointAddress: effective.endpointAddress,
|
||||||
|
port,
|
||||||
|
...(typeof migrationSecretName === "string" ? { migrationSecretName, migrationSecretKey } : {})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function externalPostgresManifest({ profile = "v03", config, source }) {
|
function externalPostgresManifest({ profile = "v03", config, source, deploy = null, migrationSources = [], catalog = null, registryPrefix = defaultRegistryPrefix, useDeployImages = false }) {
|
||||||
const namespace = namespaceNameForProfile(profile);
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
|
assert.ok(Array.isArray(migrationSources) && migrationSources.length > 0, "external Postgres migration chain is required");
|
||||||
|
const migrationData = Object.fromEntries(migrationSources.map((migration) => [path.basename(migration.path), migration.sql]));
|
||||||
|
const migrationSha256 = createHash("sha256").update(migrationSources.map((migration) => migration.sql).join("\n")).digest("hex");
|
||||||
|
const migrationRunner = externalPostgresMigrationRunner();
|
||||||
|
const migrationImage = runtimeImageForService({
|
||||||
|
catalog,
|
||||||
|
deploy,
|
||||||
|
serviceId: "hwlab-cloud-api",
|
||||||
|
source,
|
||||||
|
registryPrefix,
|
||||||
|
useDeployImages,
|
||||||
|
digestPin: true,
|
||||||
|
envReuseServiceIds: envReuseServiceIdsForLane(deploy, profile)
|
||||||
|
});
|
||||||
|
const executionSha256 = createHash("sha256").update(`${migrationSha256}\n${migrationImage}\n${migrationRunner}`).digest("hex");
|
||||||
|
const migrationName = `${namespace}-external-postgres-migrate-${executionSha256.slice(0, 12)}`;
|
||||||
const labels = {
|
const labels = {
|
||||||
"app.kubernetes.io/name": config.serviceName,
|
"app.kubernetes.io/name": config.serviceName,
|
||||||
"app.kubernetes.io/part-of": "hwlab",
|
"app.kubernetes.io/part-of": "hwlab",
|
||||||
@@ -754,6 +780,64 @@ function externalPostgresManifest({ profile = "v03", config, source }) {
|
|||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "List",
|
kind: "List",
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "ConfigMap",
|
||||||
|
metadata: {
|
||||||
|
name: migrationName,
|
||||||
|
namespace,
|
||||||
|
labels,
|
||||||
|
annotations: {
|
||||||
|
...annotations,
|
||||||
|
"argocd.argoproj.io/sync-wave": "-2",
|
||||||
|
"hwlab.pikastech.local/migration-sha256": migrationSha256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
"run.mjs": migrationRunner,
|
||||||
|
...migrationData
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "batch/v1",
|
||||||
|
kind: "Job",
|
||||||
|
metadata: {
|
||||||
|
name: migrationName,
|
||||||
|
namespace,
|
||||||
|
labels,
|
||||||
|
annotations: {
|
||||||
|
...annotations,
|
||||||
|
"argocd.argoproj.io/sync-wave": "-1",
|
||||||
|
"hwlab.pikastech.local/migration-sha256": migrationSha256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
backoffLimit: 2,
|
||||||
|
template: {
|
||||||
|
metadata: { labels: { ...labels, "app.kubernetes.io/name": migrationName } },
|
||||||
|
spec: {
|
||||||
|
restartPolicy: "Never",
|
||||||
|
containers: [{
|
||||||
|
name: "migrate",
|
||||||
|
image: migrationImage,
|
||||||
|
imagePullPolicy: "IfNotPresent",
|
||||||
|
command: ["node", "/opt/hwlab-migrations/run.mjs"],
|
||||||
|
env: [{
|
||||||
|
name: "DATABASE_URL",
|
||||||
|
valueFrom: {
|
||||||
|
secretKeyRef: {
|
||||||
|
name: config.migrationSecretName ?? `hwlab-cloud-api-${profile}-db`,
|
||||||
|
key: config.migrationSecretKey ?? "database-url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
volumeMounts: [{ name: "migrations", mountPath: "/opt/hwlab-migrations", readOnly: true }]
|
||||||
|
}],
|
||||||
|
volumes: [{ name: "migrations", configMap: { name: migrationName } }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "Service",
|
kind: "Service",
|
||||||
@@ -772,8 +856,32 @@ function externalPostgresManifest({ profile = "v03", config, source }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function v02OpenFgaManifest({ profile = "v02", source }) {
|
function externalPostgresMigrationRunner() {
|
||||||
const namespace = namespaceNameForProfile(profile);
|
return [
|
||||||
|
'import { readdir, readFile } from "node:fs/promises";',
|
||||||
|
'import { createRequire } from "node:module";',
|
||||||
|
'const require = createRequire("/opt/hwlab-env/package.json");',
|
||||||
|
'const { Pool } = require("/opt/hwlab-env/node_modules/pg");',
|
||||||
|
'const root = "/opt/hwlab-migrations";',
|
||||||
|
'const files = (await readdir(root)).filter((name) => /^\\d+_.*\\.sql$/u.test(name)).sort();',
|
||||||
|
'const connectionString = process.env.DATABASE_URL;',
|
||||||
|
'if (!connectionString) throw new Error("DATABASE_URL is required");',
|
||||||
|
'console.error(JSON.stringify({ event: "hwlab-runtime-migration-started", fileCount: files.length, valuesPrinted: false }));',
|
||||||
|
'const pool = new Pool({ connectionString, max: 1 });',
|
||||||
|
'try {',
|
||||||
|
' for (const file of files) {',
|
||||||
|
' await pool.query(await readFile(`${root}/${file}`, "utf8"));',
|
||||||
|
' console.error(JSON.stringify({ event: "hwlab-runtime-migration-applied", file, valuesPrinted: false }));',
|
||||||
|
' }',
|
||||||
|
'} finally {',
|
||||||
|
' await pool.end();',
|
||||||
|
'}',
|
||||||
|
''
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function v02OpenFgaManifest({ profile = "v02", source, deploy = null }) {
|
||||||
|
const namespace = namespaceNameForProfile(profile, deploy);
|
||||||
const name = "hwlab-openfga";
|
const name = "hwlab-openfga";
|
||||||
const image = "127.0.0.1:5000/hwlab/openfga:v1.17.0";
|
const image = "127.0.0.1:5000/hwlab/openfga:v1.17.0";
|
||||||
const labels = {
|
const labels = {
|
||||||
@@ -1200,7 +1308,7 @@ function opencodeConfigContent(config) {
|
|||||||
|
|
||||||
function opencodeServerManifest({ profile = "v03", source, deploy = null, catalog = null, registryPrefix = defaultRegistryPrefix, useDeployImages = false, sourceBranch = defaultBranch, gitReadUrl = defaultV02GitReadUrl, sourceRepo = defaultSourceRepo }) {
|
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}`);
|
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 name = "opencode-server";
|
||||||
const helperServiceId = "hwlab-cloud-web";
|
const helperServiceId = "hwlab-cloud-web";
|
||||||
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, profile);
|
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, profile);
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ import {
|
|||||||
primitiveValidationTaskNames
|
primitiveValidationTaskNames
|
||||||
} from "./tekton-scripts.mjs";
|
} from "./tekton-scripts.mjs";
|
||||||
|
|
||||||
function ciLaneSettings(argsOrLane = "node") {
|
function ciLaneSettings(argsOrLane = "node", deploy = null) {
|
||||||
const lane = typeof argsOrLane === "string" ? argsOrLane : argsOrLane.lane;
|
const lane = typeof argsOrLane === "string" ? argsOrLane : argsOrLane.lane;
|
||||||
if (isRuntimeLane(lane)) {
|
if (isRuntimeLane(lane)) {
|
||||||
const namespace = namespaceNameForProfile(lane);
|
const namespace = namespaceNameForProfile(lane, deploy);
|
||||||
return {
|
return {
|
||||||
lane,
|
lane,
|
||||||
profile: lane,
|
profile: lane,
|
||||||
@@ -64,8 +64,8 @@ function ciLaneSettings(argsOrLane = "node") {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tektonRbac(args = { lane: "node" }) {
|
function tektonRbac(args = { lane: "node" }, deploy = null) {
|
||||||
const settings = ciLaneSettings(args);
|
const settings = ciLaneSettings(args, deploy);
|
||||||
if (isRuntimeLane(settings.lane)) {
|
if (isRuntimeLane(settings.lane)) {
|
||||||
const runtimeObserverName = `${settings.runtimeNamespace}-runtime-observer`;
|
const runtimeObserverName = `${settings.runtimeNamespace}-runtime-observer`;
|
||||||
const pipelineRunWriterName = `${settings.runtimeNamespace}-pipelinerun-writer`;
|
const pipelineRunWriterName = `${settings.runtimeNamespace}-pipelinerun-writer`;
|
||||||
@@ -444,7 +444,7 @@ node scripts/ci/runtime-ready.mjs
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runtimeReadyTask({ profile = "dev" } = {}) {
|
function runtimeReadyTask({ profile = "dev", deploy = null } = {}) {
|
||||||
return {
|
return {
|
||||||
name: "runtime-ready",
|
name: "runtime-ready",
|
||||||
runAfter: ["gitops-promote"],
|
runAfter: ["gitops-promote"],
|
||||||
@@ -466,7 +466,7 @@ function runtimeReadyTask({ profile = "dev" } = {}) {
|
|||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{ name: "revision", value: "$(params.revision)" },
|
{ 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: "gitops-target", value: gitopsTargetForProfile(profile) },
|
||||||
{ name: "argo-application", value: argoApplicationName(profile) },
|
{ name: "argo-application", value: argoApplicationName(profile) },
|
||||||
{ name: "timeout-ms", value: "$(params.runtime-ready-timeout-ms)" }
|
{ name: "timeout-ms", value: "$(params.runtime-ready-timeout-ms)" }
|
||||||
@@ -505,7 +505,7 @@ function reusableTaskResource(pipelineName, task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tektonTasks(args = { lane: "node" }, deploy = null) {
|
function tektonTasks(args = { lane: "node" }, deploy = null) {
|
||||||
const settings = ciLaneSettings(args);
|
const settings = ciLaneSettings(args, deploy);
|
||||||
const prepareSource = {
|
const prepareSource = {
|
||||||
name: "prepare-source",
|
name: "prepare-source",
|
||||||
taskSpec: {
|
taskSpec: {
|
||||||
@@ -542,12 +542,12 @@ function tektonTasks(args = { lane: "node" }, deploy = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||||
const settings = ciLaneSettings(args);
|
const settings = ciLaneSettings(args, deploy);
|
||||||
const runtimePath = gitopsPathForProfile(args, settings.profile);
|
const runtimePath = gitopsPathForProfile(args, settings.profile, deploy);
|
||||||
const preFlushRuntimeReadyTasks = isRuntimeLane(settings.lane)
|
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"] }]
|
when: [{ input: "$(tasks.gitops-promote.results.runtime-ready-required)", operator: "in", values: ["true"] }]
|
||||||
}];
|
}];
|
||||||
return {
|
return {
|
||||||
@@ -671,9 +671,9 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function tektonPipelineRunTemplate({ source, args }) {
|
function tektonPipelineRunTemplate({ source, args, deploy = null }) {
|
||||||
const settings = ciLaneSettings(args);
|
const settings = ciLaneSettings(args, deploy);
|
||||||
const runtimePath = gitopsPathForProfile(args, settings.profile);
|
const runtimePath = gitopsPathForProfile(args, settings.profile, deploy);
|
||||||
return {
|
return {
|
||||||
apiVersion: "tekton.dev/v1",
|
apiVersion: "tekton.dev/v1",
|
||||||
kind: "PipelineRun",
|
kind: "PipelineRun",
|
||||||
@@ -720,7 +720,7 @@ function tektonPipelineRunTemplate({ source, args }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tektonPollerCronJob(args, deploy) {
|
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");
|
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 {
|
return {
|
||||||
apiVersion: "batch/v1",
|
apiVersion: "batch/v1",
|
||||||
@@ -771,7 +771,7 @@ function tektonPollerCronJob(args, deploy) {
|
|||||||
{ name: "LANE", value: settings.lane },
|
{ name: "LANE", value: settings.lane },
|
||||||
{ name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath },
|
{ name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath },
|
||||||
{ name: "IMAGE_TAG_MODE", value: args.imageTagMode || settings.imageTagMode },
|
{ 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_URL", value: args.sourceRepo },
|
||||||
{ name: "GIT_READ_URL", value: args.gitReadUrl },
|
{ name: "GIT_READ_URL", value: args.gitReadUrl },
|
||||||
{ name: "SOURCE_BRANCH", value: args.sourceBranch },
|
{ name: "SOURCE_BRANCH", value: args.sourceBranch },
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ for tool in node git timeout; do command -v "$tool" >/dev/null 2>&1 || { echo '{
|
|||||||
git_url_requires_ssh() { case "$1" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }
|
git_url_requires_ssh() { case "$1" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }
|
||||||
lane="$(params.lane)"
|
lane="$(params.lane)"
|
||||||
case "$lane" in
|
case "$lane" in
|
||||||
v[0-9][0-9]*) runtime_lane=true ;;
|
node) runtime_lane=false ;;
|
||||||
*) runtime_lane=false ;;
|
*) runtime_lane=true ;;
|
||||||
esac
|
esac
|
||||||
if [ "$runtime_lane" = "true" ]; then
|
if [ "$runtime_lane" = "true" ]; then
|
||||||
printf 'false' > /tekton/results/runtime-ready-required
|
printf 'false' > /tekton/results/runtime-ready-required
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyRuntimeLaneDeployConfig,
|
||||||
|
isNamedRuntimeLane,
|
||||||
|
isRuntimeLane,
|
||||||
|
isVersionRuntimeLane,
|
||||||
|
runtimeNamespaceForRuntimeLane,
|
||||||
|
runtimeRelativePathForRuntimeLane
|
||||||
|
} from "./runtime-lane.ts";
|
||||||
|
import {
|
||||||
|
gitopsPathForProfile,
|
||||||
|
namespaceNameForProfile,
|
||||||
|
parseArgs,
|
||||||
|
runtimeFrpConfigForProfile,
|
||||||
|
runtimePathForProfile,
|
||||||
|
transformServices
|
||||||
|
} from "./gitops-render/core.mjs";
|
||||||
|
import { nodeFrpcManifest } from "./gitops-render/runtime-manifests.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");
|
||||||
|
assert.equal(
|
||||||
|
gitopsPathForProfile(resolved, "production", namedLaneDeploy),
|
||||||
|
"deploy/gitops/node/nc01/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 frpDisabledDeploy = structuredClone(namedLaneDeploy);
|
||||||
|
frpDisabledDeploy.lanes.production.frp = { enabled: false };
|
||||||
|
assert.deepEqual(nodeFrpcManifest({ profile: "production", deploy: frpDisabledDeploy }).items, []);
|
||||||
|
|
||||||
|
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: 20, namedLane: "production", versionLane: "v03" }));
|
||||||
+76
-15
@@ -26,11 +26,15 @@ export type GitOpsRenderArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RuntimeLaneConfig = {
|
export type RuntimeLaneConfig = {
|
||||||
|
name?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
sourceBranch?: string;
|
sourceBranch?: string;
|
||||||
gitopsBranch?: string;
|
gitopsBranch?: string;
|
||||||
|
gitReadUrl?: string;
|
||||||
|
gitWriteUrl?: string;
|
||||||
artifactCatalog?: string;
|
artifactCatalog?: string;
|
||||||
runtimePath?: string;
|
runtimePath?: string;
|
||||||
|
namespace?: string;
|
||||||
imageTagMode?: string;
|
imageTagMode?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
externalPostgres?: {
|
externalPostgres?: {
|
||||||
@@ -75,11 +79,20 @@ const defaultGitopsRoot = "deploy/gitops/node";
|
|||||||
const defaultPublicHost = "74.48.78.17";
|
const defaultPublicHost = "74.48.78.17";
|
||||||
|
|
||||||
export function isRuntimeLane(lane: unknown): lane is string {
|
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 ?? ""));
|
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 {
|
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);
|
return Number.parseInt(lane.slice(1), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,12 +101,12 @@ export function versionNameForRuntimeLane(lane: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function namespaceNameForRuntimeLane(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}`;
|
return `hwlab-${lane}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runtimePathForRuntimeLane(lane: string): string {
|
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}`;
|
return `runtime-${lane}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +146,10 @@ export function defaultEndpointForRuntimeLane(lane: string, options: { host?: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function runtimeLaneConfig(deploy: DeployConfig, lane: string): RuntimeLaneConfig {
|
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];
|
const config = deploy.lanes?.[lane];
|
||||||
invariant(isObject(config), `deploy.lanes.${lane} is required`);
|
invariant(isObject(config), `deploy.lanes.${lane} is required`);
|
||||||
|
if (isNamedRuntimeLane(lane)) validateNamedRuntimeLaneConfig(config, lane);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +159,14 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep
|
|||||||
const laneConfig = runtimeLaneConfig(deploy, args.lane);
|
const laneConfig = runtimeLaneConfig(deploy, args.lane);
|
||||||
const configuredNodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults);
|
const configuredNodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults);
|
||||||
const rootNodeId = nodeIdFromGitopsRoot(result.gitopsRoot);
|
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 node = nodeConfig(deploy, nodeId);
|
||||||
const versionName = versionNameForRuntimeLane(args.lane);
|
const versionName = isVersionRuntimeLane(args.lane) ? versionNameForRuntimeLane(args.lane) : null;
|
||||||
const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {};
|
const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {};
|
||||||
if (node.publicHost) defaultEndpointOptions.host = node.publicHost;
|
if (node.publicHost) defaultEndpointOptions.host = node.publicHost;
|
||||||
if (defaults.defaultV02RuntimeEndpoint) defaultEndpointOptions.v02Endpoint = defaults.defaultV02RuntimeEndpoint;
|
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 configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint;
|
||||||
const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
|
const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
|
||||||
const configuredRuntimeEndpoint = laneConfig.publicEndpoints?.api ?? 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 defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git";
|
||||||
const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`;
|
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.nodeId || result.nodeId === defaultNodeId) result.nodeId = nodeId;
|
||||||
if (!result.gitopsRoot || result.gitopsRoot === (defaults.defaultGitopsRoot ?? defaultGitopsRoot)) result.gitopsRoot = gitopsRootForNode(deploy, nodeId, defaults);
|
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.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}-gitops`;
|
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.catalogPath === defaultCatalogPath || result.catalogPath === defaultCatalog) result.catalogPath = laneConfig.artifactCatalog ?? defaultCatalog;
|
||||||
if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode;
|
if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode;
|
||||||
if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint;
|
if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint ?? result.runtimeEndpoint;
|
||||||
if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint;
|
if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint ?? result.webEndpoint;
|
||||||
if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo;
|
if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -185,20 +218,48 @@ function nodeIdFromGitopsRoot(gitopsRoot: unknown): string | undefined {
|
|||||||
export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] {
|
export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] {
|
||||||
return Object.entries(deploy.lanes ?? {})
|
return Object.entries(deploy.lanes ?? {})
|
||||||
.filter(([lane, config]) => isRuntimeLane(lane) && isObject(config))
|
.filter(([lane, config]) => isRuntimeLane(lane) && isObject(config))
|
||||||
.sort(([left], [right]) => runtimeLaneMinor(left) - runtimeLaneMinor(right))
|
.sort(([left], [right]) => left.localeCompare(right, "en"))
|
||||||
.map(([lane, config]) => {
|
.map(([lane, config]) => {
|
||||||
const laneConfig = config as RuntimeLaneConfig;
|
const laneConfig = config as RuntimeLaneConfig;
|
||||||
const versionName = versionNameForRuntimeLane(lane);
|
const versionName = isVersionRuntimeLane(lane) ? versionNameForRuntimeLane(lane) : null;
|
||||||
return {
|
return {
|
||||||
lane,
|
lane,
|
||||||
sourceBranch: laneConfig.sourceBranch ?? versionName,
|
sourceBranch: laneConfig.sourceBranch ?? versionName ?? requiredString(laneConfig.sourceBranch, `deploy.lanes.${lane}.sourceBranch`),
|
||||||
gitopsBranch: laneConfig.gitopsBranch ?? `${versionName}-gitops`,
|
gitopsBranch: laneConfig.gitopsBranch ?? (versionName ? `${versionName}-gitops` : requiredString(laneConfig.gitopsBranch, `deploy.lanes.${lane}.gitopsBranch`)),
|
||||||
artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`,
|
artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`,
|
||||||
runtimePath: runtimeGitopsPathForRuntimeLane(deploy, lane, defaults),
|
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 {
|
function normalizeRepoPath(value: string): string {
|
||||||
return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user