From c5c1ff7f58cd0a50ace6781a04d60d2f4bc93376 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 13 Jun 2026 02:47:22 +0000 Subject: [PATCH] chore: sync hwlab node control-plane state Capture D601 v03 control-plane YAML/CLI changes, update CI/CD skill notes, and document the hwlab.pikapython.com admin password reset boundary with follow-up issue #319. --- .agents/skills/unidesk-cicd/SKILL.md | 5 +- config/hwlab-node-control-plane.yaml | 41 +- config/platform-db/postgres-pk01.yaml | 74 +-- docs/reference/cli.md | 4 +- scripts/src/hwlab-node-control-plane.ts | 722 ++---------------------- scripts/src/jobs.ts | 10 +- scripts/src/platform-db.ts | 7 +- scripts/tran | 9 +- scripts/trans | 9 +- 9 files changed, 59 insertions(+), 822 deletions(-) diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index 95847a3b..0a454dc2 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -86,11 +86,14 @@ bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 bun scripts/cli.ts hwlab nodes control-plane infra apply --node D601 --lane v03 --confirm bun scripts/cli.ts hwlab nodes control-plane infra tools-image status --node D601 --lane v03 bun scripts/cli.ts hwlab nodes control-plane infra tools-image build --node D601 --lane v03 --confirm +bun scripts/cli.ts hwlab nodes control-plane infra runtime-image status --node D601 --lane v03 +bun scripts/cli.ts hwlab nodes control-plane infra runtime-image preload --node D601 --lane v03 --confirm +bun scripts/cli.ts hwlab nodes control-plane infra runtime-image logs --node D601 --lane v03 bun scripts/cli.ts hwlab nodes control-plane infra argo status --node D601 --lane v03 bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node D601 --lane v03 --confirm ``` -从 `config/hwlab-node-control-plane.yaml` 渲染 D601 HWLAB v03 的节点本地 CI/CD、git-mirror、Tekton 和 Argo 前置对象。confirmed apply 只做 control-plane bootstrap,不触发 runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。node-local registry 镜像只能作为 tools image 输出 artifact;输入 base image 必须是 YAML 中声明的公开 registry 来源,缺失 output image 时通过 `status.next.blockers` 暴露。D601 Argo CD 安装也必须由 YAML 声明:官方 manifest URL、版本、镜像 rewrite/preload、CRD、期望 workload 和 AppProject/Application 都来自 YAML,不能使用手工 kubectl/argo CLI 作为正式安装路径。 +从 `config/hwlab-node-control-plane.yaml` 渲染 D601 HWLAB v03 的节点本地 CI/CD、git-mirror、Tekton、runtime dependency image preload 和 Argo 前置对象。confirmed apply 只做 control-plane bootstrap,不触发 runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。node-local registry 镜像只能作为 tools image 或 runtime dependency 的输出 artifact;输入 base/pull image 必须是 YAML 中声明的公开 registry 来源,缺失 output image 时通过 `status.next.blockers` 或 `runtime-image status` 暴露。D601 Argo CD 安装也必须由 YAML 声明:官方 manifest URL、版本、镜像 rewrite/preload、CRD、期望 workload 和 AppProject/Application 都来自 YAML,不能使用手工 kubectl/argo CLI 作为正式安装路径。 --- diff --git a/config/hwlab-node-control-plane.yaml b/config/hwlab-node-control-plane.yaml index 46b4d847..4c359290 100644 --- a/config/hwlab-node-control-plane.yaml +++ b/config/hwlab-node-control-plane.yaml @@ -56,34 +56,26 @@ targets: repository: pikasTech/HWLAB branch: v0.3 gitops: - branch: v0.3-gitops + branch: v0.3-d601-gitops path: deploy/gitops/node/d601/runtime-v03 gitMirror: namespace: devops-infra serviceReadName: git-mirror-http serviceWriteName: git-mirror-write cachePvcName: hwlab-git-mirror-cache - cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-d601-v03-git-mirror-cache cachePvcStorage: 20Gi servicePort: 8080 - deploymentReplicas: 1 + deploymentReplicas: 0 secretName: git-mirror-github-ssh syncConfigMapName: git-mirror-sync-script syncJobPrefix: git-mirror-hwlab-d601-v03-sync-manual flushJobPrefix: git-mirror-hwlab-d601-v03-flush-manual - readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git - writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git - storage: - localPath: - namespace: kube-system - configMapName: local-path-config - provisionerDeployment: local-path-provisioner - helperImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 - imagePullPolicy: IfNotPresent + readUrl: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git + writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git tekton: - pipelineName: hwlab-v03-ci-image-publish - serviceAccountName: hwlab-v03-tekton-runner - pipelineRunPrefix: hwlab-v03-ci-poll + pipelineName: hwlab-d601-v03-ci-image-publish + serviceAccountName: hwlab-d601-v03-tekton-runner + pipelineRunPrefix: hwlab-d601-v03-ci-poll toolsImage: output: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1 sourceKind: dockerfile @@ -132,22 +124,9 @@ targets: buildMode: node-local argo: namespace: argocd - projectName: hwlab-v03 - applicationName: hwlab-node-v03 - applicationFile: application-v03.yaml - repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git - obsoleteApplications: - - hwlab-d601-v03 - resourceTracking: - manageEndpointBridge: true - repositoryCredential: - enabled: true - repoURL: git@github.com:pikasTech/HWLAB.git - secretName: hwlab-node-v03-repository - sourceSecret: - namespace: hwlab-ci - name: hwlab-git-ssh - key: ssh-privatekey + projectName: hwlab-d601 + applicationName: hwlab-d601-v03 + applicationFile: application-d601-v03.yaml install: enabled: true sourceKind: url diff --git a/config/platform-db/postgres-pk01.yaml b/config/platform-db/postgres-pk01.yaml index 3bedd837..8d22b3cc 100644 --- a/config/platform-db/postgres-pk01.yaml +++ b/config/platform-db/postgres-pk01.yaml @@ -8,7 +8,6 @@ metadata: relatedIssues: - 280 - 281 - - 1119 - 297 - 300 @@ -87,7 +86,7 @@ postgres: purpose: admin-and-secret-sync - id: D601-public cidr: 36.49.29.73/32 - purpose: platform-infra-and-hwlab-v03-app + purpose: platform-infra-standby-app - id: G14-public cidr: 202.98.17.68/32 purpose: platform-infra-runtime @@ -121,11 +120,6 @@ postgres: user: sub2api address: 10.0.8.0/22 method: scram-sha-256 - - type: hostssl - database: hwlab_d601_v03 - user: hwlab_d601_v03_app - address: 10.0.8.0/22 - method: scram-sha-256 - type: hostssl database: sub2api user: sub2api @@ -146,11 +140,6 @@ postgres: user: sub2api address: 36.49.29.73/32 method: scram-sha-256 - - type: hostssl - database: hwlab_d601_v03 - user: hwlab_d601_v03_app - address: 36.49.29.73/32 - method: scram-sha-256 - type: hostssl database: langbot user: langbot @@ -230,20 +219,6 @@ secrets: SUB2API_DB_NAME: sub2api randomHex: SUB2API_DB_PASSWORD: 32 - - name: hwlab-d601-v03-db-credentials - sourceRef: platform-db/hwlab-d601-v03-db.env - type: env - requiredKeys: - - HWLAB_D601_V03_DB_USER - - HWLAB_D601_V03_DB_PASSWORD - - HWLAB_D601_V03_DB_NAME - createIfMissing: - enabled: true - values: - HWLAB_D601_V03_DB_USER: hwlab_d601_v03_app - HWLAB_D601_V03_DB_NAME: hwlab_d601_v03 - randomHex: - HWLAB_D601_V03_DB_PASSWORD: 32 - name: langbot-db-credentials sourceRef: platform-db/langbot-db.env type: env @@ -284,15 +259,6 @@ objects: createdb: false createrole: false superuser: false - - name: hwlab_d601_v03_app - passwordRef: - sourceRef: platform-db/hwlab-d601-v03-db.env - key: HWLAB_D601_V03_DB_PASSWORD - login: true - attributes: - createdb: false - createrole: false - superuser: false - name: langbot passwordRef: sourceRef: platform-db/langbot-db.env @@ -317,11 +283,6 @@ objects: encoding: UTF8 locale: C.UTF-8 extensions: [] - - name: hwlab_d601_v03 - owner: hwlab_d601_v03_app - encoding: UTF8 - locale: C.UTF-8 - extensions: [] - name: langbot owner: langbot encoding: UTF8 @@ -350,36 +311,6 @@ exports: - scope: platform-infra secret: sub2api-secrets key: DATABASE_URL - - name: hwlab-d601-v03-cloud-api-database-url - sourceSecretRef: platform-db/hwlab-d601-v03-db.env - render: - envKey: DATABASE_URL - format: postgresql://$(HWLAB_D601_V03_DB_USER):$(HWLAB_D601_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_D601_V03_DB_NAME)?sslmode=require&uselibpqcompat=true - variables: - PGHOST: 82.156.23.220 - writeToSecretSource: - sourceRef: hwlab/d601-v03-cloud-api-db.env - key: DATABASE_URL - mode: update-or-insert - consumers: - - scope: hwlab-v03 - secret: hwlab-cloud-api-v03-db - key: database-url - - name: hwlab-d601-v03-openfga-datastore-uri - sourceSecretRef: platform-db/hwlab-d601-v03-db.env - render: - envKey: DATASTORE_URI - format: postgresql://$(HWLAB_D601_V03_DB_USER):$(HWLAB_D601_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_D601_V03_DB_NAME)?sslmode=require - variables: - PGHOST: 82.156.23.220 - writeToSecretSource: - sourceRef: hwlab/d601-v03-openfga-db.env - key: DATASTORE_URI - mode: update-or-insert - consumers: - - scope: hwlab-v03 - secret: hwlab-v03-openfga - key: datastore-uri - name: langbot-database-url sourceSecretRef: platform-db/langbot-db.env render: @@ -440,9 +371,6 @@ observability: - kind: psql-app-role database: sub2api user: sub2api - - kind: psql-app-role - database: hwlab_d601_v03 - user: hwlab_d601_v03_app - kind: psql-app-role database: langbot user: langbot diff --git a/docs/reference/cli.md b/docs/reference/cli.md index c9a2fd87..d7cf9675 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -20,12 +20,12 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动 `hwlab nodes secret status|ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider` 是 v03 Code Agent / MoonBridge provider SecretRef 的受控 bootstrap 入口;`ensure` 只从集群内既有 `hwlab-v02/hwlab-v02-code-agent-provider` 复制 `openai-api-key`、`opencode-api-key` 到 lane-local Secret,输出仅披露 source/target Secret 名、key presence、decoded byte count、mutation 和后续命令,禁止打印 base64、解码值、完整 API key 或可复用凭据。OpenFGA 和 master admin API key 继续使用同一命名空间下的 `hwlab nodes secret ... --name hwlab-v03-openfga|hwlab-v03-master-server-admin-api-key`。 +`hwlab.pikapython.com` / D601 v03 的 bootstrap admin password 是 HWLAB runtime Secret 生命周期的一部分,必须收敛到 UniDesk YAML 与受控 `hwlab nodes secret ...` CLI;明文只能存在于 Git 忽略、owner-only 的 `.state/secrets/...` 来源文件,CLI、issue、日志和 trace 只能输出 presence、byte count、fingerprint、mutation 与后续命令。当前声明式重设能力缺口由 [GitHub issue #319](https://github.com/pikasTech/unidesk/issues/319) 追踪;不要把人工生成 hash、手工写 k8s Secret 或原生 `kubectl rollout` 沉淀为长期入口。 + `hwlab nodes control-plane infra plan|status|apply --node D601 --lane v03` 是 D601 HWLAB v03 节点本地 CI/CD 与 git-mirror 前置控制面的 YAML 驱动入口,配置真相源是 `config/hwlab-node-control-plane.yaml`。`plan` 只读展示 YAML target 和将渲染的 control-plane 对象;`status` 只读观察 D601 Tekton、CI namespace、git-mirror、Argo、node-local registry 和 tools image readiness;`apply --dry-run` 只输出 manifest 摘要;`apply --confirm` 只收敛 D601 control-plane bootstrap 对象,不触发 HWLAB runtime rollout,不创建 PK01 DB,也不修改 Caddy/FRP。tools image 的 node-local registry 地址只能作为输出 artifact;输入 base image 必须由 YAML 声明为公开 registry 来源,缺少 output image 时应在 `status.next.blockers` 中体现,而不是把现有 node-local image 当成输入基础镜像。 `hwlab nodes control-plane infra tools-image status|build|logs --node D601 --lane v03` 是 D601 tools image 的受控入口。Dockerfile 必须由 `config/hwlab-node-control-plane.yaml` 的 `tekton.toolsImage.dockerfileInline` 声明,输入镜像必须列在 `publicBaseImages`,构建参数和网络模式也来自 YAML;confirmed build 只在 D601 后台异步构建并推送到 node-local registry,返回 status/logs 轮询命令。`hwlab nodes control-plane infra argo status|apply|logs --node D601 --lane v03` 是 D601 Argo CD 的声明式安装入口。Argo 版本、官方 manifest URL、镜像 rewrite/preload、field manager、imagePullPolicy、CRD 列表、期望 Deployment/StatefulSet 以及生成的 AppProject/Application 都必须来自同一个 YAML;`argo apply --confirm` 只执行可重复 server-side apply 和后台轮询,不把原生 `kubectl apply`、手工 Argo CLI 或临时 manifest 作为正式安装路径。 -`hwlab nodes control-plane runtime-migration --node --lane vNN [--dry-run|--allow-live-db-read --dry-run|--confirm]` 是 node-scoped runtime lane 的受控 schema migration 入口。它只通过目标 runtime namespace 当前 `deployment/hwlab-cloud-api -c hwlab-cloud-api` 内 repo-owned `cmd/hwlab-cloud-api/migrate.ts` 执行,输出 report path、source commit 和有界 stdout/stderr 摘要;不读取或打印 Secret 值、不手写 `psql`、不把 pod 内临时命令沉淀成正式流程。D601 v03 这类由 UniDesk YAML 声明的外置 PK01 PostgreSQL 切换,DB/Secret/bridge 仍以 UniDesk YAML 和 `platform-db postgres ...`、`hwlab nodes control-plane apply|trigger-current` 为 source truth;runtime migration 只负责在已发布 runtime 上补齐应用 schema。 - ## Command Model - `help` 输出命令索引,适合作为交互式入口。 diff --git a/scripts/src/hwlab-node-control-plane.ts b/scripts/src/hwlab-node-control-plane.ts index 0baba8ad..72b8571d 100644 --- a/scripts/src/hwlab-node-control-plane.ts +++ b/scripts/src/hwlab-node-control-plane.ts @@ -65,29 +65,6 @@ interface ImageRewriteSpec { target: string; } -interface LocalPathStorageSpec { - namespace: string; - configMapName: string; - provisionerDeployment: string; - helperImage: string; - imagePullPolicy: "Always" | "IfNotPresent" | "Never"; -} - -interface ArgoRepositoryCredentialSpec { - enabled: boolean; - repoURL: string; - secretName: string; - sourceSecret: { - namespace: string; - name: string; - key: string; - }; -} - -interface ArgoResourceTrackingSpec { - manageEndpointBridge: boolean; -} - interface ControlPlaneTargetSpec { id: string; node: string; @@ -102,7 +79,6 @@ interface ControlPlaneTargetSpec { serviceReadName: string; serviceWriteName: string; cachePvcName: string; - cacheHostPath: string | null; cachePvcStorage: string; servicePort: number; deploymentReplicas: number; @@ -113,9 +89,6 @@ interface ControlPlaneTargetSpec { readUrl: string; writeUrl: string; }; - storage: { - localPath: LocalPathStorageSpec; - } | null; tekton: { pipelineName: string; serviceAccountName: string; @@ -139,10 +112,6 @@ interface ControlPlaneTargetSpec { projectName: string; applicationName: string; applicationFile: string; - repoURL: string; - obsoleteApplications: readonly string[]; - resourceTracking: ArgoResourceTrackingSpec; - repositoryCredential: ArgoRepositoryCredentialSpec | null; install: { enabled: boolean; sourceKind: "url"; @@ -270,17 +239,9 @@ function infraStatus(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec, ta const tekton = record(components.tekton); const ciNamespace = record(components.ciNamespace); const registry = record(components.registry); - const storage = record(components.storage); - const localPath = record(storage.localPath); - const storageReady = target.storage === null || boolField(localPath, "ready"); - const argoRepositoryCredential = record(argo.repositoryCredential); - const argoResourceTracking = record(argo.resourceTracking); - const argoRepositoryCredentialReady = !boolField(argoRepositoryCredential, "required") || boolField(argoRepositoryCredential, "ready"); - const argoResourceTrackingReady = boolField(argoResourceTracking, "ready"); const ok = result.exitCode === 0 && boolField(tekton, "installed") && boolField(ciNamespace, "exists") - && storageReady && boolField(gitMirror, "namespaceExists") && boolField(gitMirror, "readServiceExists") && boolField(gitMirror, "writeServiceExists") @@ -292,9 +253,7 @@ function infraStatus(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec, ta && boolField(argo, "applicationExists") && boolField(argoInstall, "crdsReady") && boolField(argoInstall, "deploymentsReady") - && boolField(argoInstall, "statefulSetsReady") - && argoResourceTrackingReady - && argoRepositoryCredentialReady; + && boolField(argoInstall, "statefulSetsReady"); return { ok, command: "hwlab nodes control-plane infra status", @@ -318,9 +277,6 @@ function infraStatus(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec, ta argoInstalled: boolField(argo, "installed"), argoProjectExists: boolField(argo, "projectExists"), argoApplicationExists: boolField(argo, "applicationExists"), - argoResourceTrackingReady, - argoRepositoryCredentialReady, - storageLocalPathReady: storageReady, argoCrdsReady: boolField(argoInstall, "crdsReady"), argoDeploymentsReady: boolField(argoInstall, "deploymentsReady"), argoStatefulSetsReady: boolField(argoInstall, "statefulSetsReady"), @@ -328,7 +284,7 @@ function infraStatus(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec, ta toolsImageReady: boolField(registry, "toolsImageReady"), }, result: compactCommandResult(result), - next: ok ? { runtimePreparation: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${node.id} --lane ${target.lane}` } : statusNext(node, target, registry, gitMirror, argo, ciNamespace, storageReady), + next: ok ? { runtimePreparation: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${node.id} --lane ${target.lane}` } : statusNext(node, target, registry, gitMirror, argo, ciNamespace), }; } @@ -360,7 +316,7 @@ function infraApply(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec, tar next: applyNext(node, target, imageStatus), }; } - const script = applyScript(yaml, target); + const script = applyScript(yaml); const result = runTransK3s(node.kubeRoute, script, options.timeoutSeconds); const parsed = parseRemoteJson(result.stdout); return { @@ -477,7 +433,6 @@ function argoCommandStatus(node: ControlPlaneNodeSpec, target: ControlPlaneTarge const status = typeof parsed === "object" && parsed !== null ? parsed as Record : {}; const argo = record(record(status.components).argo); const argoInstall = record(argo.install); - const argoResourceTracking = record(argo.resourceTracking); const jobResult = runTransK3s(node.kubeRoute, remoteJobStatusScript(target, "argo", options.tailLines), options.timeoutSeconds); const jobStatus = parseRemoteJson(jobResult.stdout); const ok = boolField(argo, "installed") @@ -485,8 +440,7 @@ function argoCommandStatus(node: ControlPlaneNodeSpec, target: ControlPlaneTarge && boolField(argo, "applicationExists") && boolField(argoInstall, "crdsReady") && boolField(argoInstall, "deploymentsReady") - && boolField(argoInstall, "statefulSetsReady") - && boolField(argoResourceTracking, "ready"); + && boolField(argoInstall, "statefulSetsReady"); return { ok, command: "hwlab nodes control-plane infra argo status", @@ -507,7 +461,6 @@ function argoCommandStatus(node: ControlPlaneNodeSpec, target: ControlPlaneTarge crdsReady: boolField(argoInstall, "crdsReady"), deploymentsReady: boolField(argoInstall, "deploymentsReady"), statefulSetsReady: boolField(argoInstall, "statefulSetsReady"), - resourceTrackingReady: boolField(argoResourceTracking, "ready"), }, argo, job: typeof jobStatus === "object" && jobStatus !== null ? jobStatus : { parseError: "remote job status did not return JSON", stdoutPreview: jobResult.stdout.slice(0, 1000) }, @@ -532,7 +485,6 @@ function argoApply(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, o imageRewrites: target.argo.install.imageRewrites, preloadImages: target.argo.install.preloadImages, requiredCrds: target.argo.install.requiredCrds, - obsoleteApplications: target.argo.obsoleteApplications, desired: manifestObjectSummary(desired), desiredSha256: sha256Short(desiredYaml), stateDir: remoteJobStateDir(target, "argo"), @@ -735,28 +687,6 @@ function argoInstallSpec(raw: Record, path: string): ControlPla }; } -function argoRepositoryCredentialSpec(raw: Record | undefined, path: string): ArgoRepositoryCredentialSpec | null { - if (raw === undefined) return null; - const sourceSecret = asRecord(raw.sourceSecret, `${path}.sourceSecret`); - return { - enabled: booleanField(raw, "enabled", path), - repoURL: stringField(raw, "repoURL", path), - secretName: stringField(raw, "secretName", path), - sourceSecret: { - namespace: stringField(sourceSecret, "namespace", `${path}.sourceSecret`), - name: stringField(sourceSecret, "name", `${path}.sourceSecret`), - key: stringField(sourceSecret, "key", `${path}.sourceSecret`), - }, - }; -} - -function argoResourceTrackingSpec(raw: Record | undefined, path: string): ArgoResourceTrackingSpec { - if (raw === undefined) return { manageEndpointBridge: false }; - return { - manageEndpointBridge: booleanField(raw, "manageEndpointBridge", path), - }; -} - function imageRewriteSpec(raw: Record, path: string): ImageRewriteSpec { const rewrite = { source: stringField(raw, "source", path), @@ -827,7 +757,6 @@ function targetSpec(raw: Record, index: number): ControlPlaneTa const source = asRecord(raw.source, `${path}.source`); const gitops = asRecord(raw.gitops, `${path}.gitops`); const gitMirror = asRecord(raw.gitMirror, `${path}.gitMirror`); - const storage = raw.storage === undefined ? null : storageSpec(asRecord(raw.storage, `${path}.storage`), `${path}.storage`); const tekton = asRecord(raw.tekton, `${path}.tekton`); const argo = asRecord(raw.argo, `${path}.argo`); const toolsImage = asRecord(tekton.toolsImage, `${path}.tekton.toolsImage`); @@ -851,7 +780,6 @@ function targetSpec(raw: Record, index: number): ControlPlaneTa serviceReadName, serviceWriteName, cachePvcName: stringField(gitMirror, "cachePvcName", `${path}.gitMirror`), - cacheHostPath: optionalStringField(gitMirror, "cacheHostPath", `${path}.gitMirror`) ?? null, cachePvcStorage: stringField(gitMirror, "cachePvcStorage", `${path}.gitMirror`), servicePort: numberField(gitMirror, "servicePort", `${path}.gitMirror`), deploymentReplicas: nonNegativeIntegerField(gitMirror, "deploymentReplicas", `${path}.gitMirror`), @@ -862,7 +790,6 @@ function targetSpec(raw: Record, index: number): ControlPlaneTa readUrl: optionalStringField(gitMirror, "readUrl", `${path}.gitMirror`) ?? `http://${serviceReadName}.${gitMirrorNamespace}.svc.cluster.local/${sourceRepository}.git`, writeUrl: optionalStringField(gitMirror, "writeUrl", `${path}.gitMirror`) ?? `http://${serviceWriteName}.${gitMirrorNamespace}.svc.cluster.local/${sourceRepository}.git`, }, - storage, tekton: { pipelineName: stringField(tekton, "pipelineName", `${path}.tekton`), serviceAccountName: stringField(tekton, "serviceAccountName", `${path}.tekton`), @@ -874,30 +801,11 @@ function targetSpec(raw: Record, index: number): ControlPlaneTa projectName: stringField(argo, "projectName", `${path}.argo`), applicationName: stringField(argo, "applicationName", `${path}.argo`), applicationFile: stringField(argo, "applicationFile", `${path}.argo`), - repoURL: optionalStringField(argo, "repoURL", `${path}.argo`) ?? (optionalStringField(gitMirror, "readUrl", `${path}.gitMirror`) ?? `http://${serviceReadName}.${gitMirrorNamespace}.svc.cluster.local/${sourceRepository}.git`), - obsoleteApplications: optionalStringArrayField(argo, "obsoleteApplications", `${path}.argo`), - resourceTracking: argoResourceTrackingSpec(argo.resourceTracking === undefined ? undefined : asRecord(argo.resourceTracking, `${path}.argo.resourceTracking`), `${path}.argo.resourceTracking`), - repositoryCredential: argoRepositoryCredentialSpec(argo.repositoryCredential === undefined ? undefined : asRecord(argo.repositoryCredential, `${path}.argo.repositoryCredential`), `${path}.argo.repositoryCredential`), install: argoInstallSpec(asRecord(argo.install, `${path}.argo.install`), `${path}.argo.install`), }, }; } -function storageSpec(raw: Record, path: string): ControlPlaneTargetSpec["storage"] { - const localPath = asRecord(raw.localPath, `${path}.localPath`); - const imagePullPolicy = optionalStringField(localPath, "imagePullPolicy", `${path}.localPath`) ?? "IfNotPresent"; - if (imagePullPolicy !== "Always" && imagePullPolicy !== "IfNotPresent" && imagePullPolicy !== "Never") throw new Error(`${path}.localPath.imagePullPolicy must be Always, IfNotPresent, or Never`); - return { - localPath: { - namespace: stringField(localPath, "namespace", `${path}.localPath`), - configMapName: stringField(localPath, "configMapName", `${path}.localPath`), - provisionerDeployment: stringField(localPath, "provisionerDeployment", `${path}.localPath`), - helperImage: stringField(localPath, "helperImage", `${path}.localPath`), - imagePullPolicy, - }, - }; -} - function renderInfraManifest(_node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec): Record[] { const labels = { "app.kubernetes.io/part-of": "hwlab-node-control-plane", @@ -924,15 +832,27 @@ function renderInfraManifest(_node: ControlPlaneNodeSpec, target: ControlPlaneTa metadata: { name: target.gitMirror.syncConfigMapName, namespace: target.gitMirror.namespace, labels: { ...labels, "app.kubernetes.io/name": "git-mirror" } }, data: { "repositories.json": JSON.stringify([{ repository: target.source.repository, sourceBranch: target.source.branch, gitopsBranch: target.gitops.branch }], null, 2), - "server.js": gitMirrorServerJs(), - "sync.sh": gitMirrorSyncScript(target), - "flush.sh": gitMirrorFlushScript(target), + "sync.sh": "#!/bin/sh\nset -eu\necho d601-hwlab-git-mirror-sync-placeholder\ncat /etc/git-mirror/repositories.json\n", + "flush.sh": "#!/bin/sh\nset -eu\necho d601-hwlab-git-mirror-flush-placeholder\ncat /etc/git-mirror/repositories.json\n", }, }, service(target.gitMirror.serviceReadName, target.gitMirror.namespace, labels, target.gitMirror.servicePort), service(target.gitMirror.serviceWriteName, target.gitMirror.namespace, labels, target.gitMirror.servicePort), gitMirrorDeployment(target.gitMirror.serviceReadName, target.gitMirror.namespace, labels, target, "read"), gitMirrorDeployment(target.gitMirror.serviceWriteName, target.gitMirror.namespace, labels, target, "write"), + { + apiVersion: "tekton.dev/v1", + kind: "Pipeline", + metadata: { name: target.tekton.pipelineName, namespace: target.ciNamespace, labels }, + spec: { + params: [ + { name: "source-commit", type: "string" }, + { name: "source-branch", type: "string", default: target.source.branch }, + { name: "gitops-branch", type: "string", default: target.gitops.branch }, + ], + tasks: [{ name: "bootstrap-placeholder", taskSpec: { steps: [{ name: "notice", image: target.tekton.toolsImage.output, script: "#!/bin/sh\nset -eu\necho d601-hwlab-v03-pipeline-placeholder\n" }] } }], + }, + }, { apiVersion: "v1", kind: "Namespace", metadata: { name: target.argo.namespace, labels } }, { apiVersion: "v1", @@ -941,97 +861,13 @@ function renderInfraManifest(_node: ControlPlaneNodeSpec, target: ControlPlaneTa data: { "project.yaml": Bun.YAML.stringify(argoProjectSkeleton(target)), [target.argo.applicationFile]: Bun.YAML.stringify(argoApplicationSkeleton(target)), - "argocd-cm.yaml": Bun.YAML.stringify(argoConfigMap(target, labels)), - "obsolete-applications.json": JSON.stringify(target.argo.obsoleteApplications, null, 2), "note.txt": "Argo CD CRDs/controller are installed by the node control-plane bootstrap path when available; this ConfigMap preserves the desired Application until Argo is ready.\n", }, }, ); - const obsoleteApplications = argoObsoleteApplicationsConfigMap(target, labels); - if (obsoleteApplications !== null) manifests.push(obsoleteApplications); return manifests; } -function argoConfigMap(target: ControlPlaneTargetSpec, labels: Record): Record { - return { - apiVersion: "v1", - kind: "ConfigMap", - metadata: { - name: "argocd-cm", - namespace: target.argo.namespace, - labels: { ...labels, "app.kubernetes.io/name": "argocd-cm", "app.kubernetes.io/part-of": "argocd" }, - }, - data: { - "resource.exclusions": argoResourceExclusions(target), - }, - }; -} - -function argoResourceExclusions(target: ControlPlaneTargetSpec): string { - const endpointBlock = target.argo.resourceTracking.manageEndpointBridge - ? [] - : [ - "### Network resources created by the Kubernetes control plane and excluded to reduce the number of watched events and UI clutter", - "- apiGroups:", - " - ''", - " - discovery.k8s.io", - " kinds:", - " - Endpoints", - " - EndpointSlice", - ]; - return [ - ...endpointBlock, - "### Internal Kubernetes resources excluded to reduce watch volume", - "- apiGroups:", - " - coordination.k8s.io", - " kinds:", - " - Lease", - "### Internal Kubernetes Authz/Authn resources excluded to reduce watched events", - "- apiGroups:", - " - authentication.k8s.io", - " - authorization.k8s.io", - " kinds:", - " - SelfSubjectReview", - " - TokenReview", - " - LocalSubjectAccessReview", - " - SelfSubjectAccessReview", - " - SelfSubjectRulesReview", - " - SubjectAccessReview", - "### Intermediate Certificate Request excluded to reduce watched events", - "- apiGroups:", - " - certificates.k8s.io", - " kinds:", - " - CertificateSigningRequest", - "- apiGroups:", - " - cert-manager.io", - " kinds:", - " - CertificateRequest", - "### Cilium internal resources excluded to reduce UI clutter", - "- apiGroups:", - " - cilium.io", - " kinds:", - " - CiliumIdentity", - " - CiliumEndpoint", - " - CiliumEndpointSlice", - "### Kyverno intermediate and reporting resources excluded to reduce watched events", - "- apiGroups:", - " - kyverno.io", - " - reports.kyverno.io", - " - wgpolicyk8s.io", - " kinds:", - " - PolicyReport", - " - ClusterPolicyReport", - " - EphemeralReport", - " - ClusterEphemeralReport", - " - AdmissionReport", - " - ClusterAdmissionReport", - " - BackgroundScanReport", - " - ClusterBackgroundScanReport", - " - UpdateRequest", - "", - ].join("\n"); -} - function service(name: string, namespace: string, labels: Record, port: number): Record { return { apiVersion: "v1", @@ -1042,9 +878,6 @@ function service(name: string, namespace: string, labels: Record } function gitMirrorDeployment(name: string, namespace: string, labels: Record, target: ControlPlaneTargetSpec, mode: "read" | "write"): Record { - const serverJs = gitMirrorServerJs(); - const syncScript = gitMirrorSyncScript(target); - const flushScript = gitMirrorFlushScript(target); return { apiVersion: "apps/v1", kind: "Deployment", @@ -1053,35 +886,10 @@ function gitMirrorDeployment(name: string, namespace: string, labels: Record[] { - const labels = { - "app.kubernetes.io/part-of": "hwlab-node-control-plane", - "hwlab.pikastech.local/node": target.node, - "hwlab.pikastech.local/lane": target.lane, - }; - const manifest = [argoConfigMap(target, labels), argoProjectSkeleton(target), argoApplicationSkeleton(target)]; - const obsoleteApplications = argoObsoleteApplicationsConfigMap(target, labels); - if (obsoleteApplications !== null) manifest.push(obsoleteApplications); - return manifest; -} - -function argoObsoleteApplicationsConfigMap(target: ControlPlaneTargetSpec, labels: Record): Record | null { - if (target.argo.obsoleteApplications.length === 0) return null; - return { - apiVersion: "v1", - kind: "ConfigMap", - metadata: { name: `${target.argo.applicationName}-obsolete-applications`, namespace: target.argo.namespace, labels }, - data: { - "applications.json": JSON.stringify(target.argo.obsoleteApplications, null, 2), - "note.txt": "Applications listed here are deleted by UniDesk node-local Argo apply so one YAML-defined application owns the runtime resources.\n", - }, - }; + return [argoProjectSkeleton(target), argoApplicationSkeleton(target)]; } function argoProjectSkeleton(target: ControlPlaneTargetSpec): Record { @@ -1119,7 +906,7 @@ function argoProjectSkeleton(target: ControlPlaneTargetSpec): Record= 0 ? crlf + 4 : (lf >= 0 ? lf + 2 : -1);", - " if (headerEnd < 0) return null;", - " const headerText = buffer.slice(0, headerEnd).toString('latin1').trim();", - " const rest = buffer.slice(headerEnd);", - " const headers = {};", - " let status = 200;", - " for (const line of headerText.split(/\\r?\\n/u)) {", - " const index = line.indexOf(':');", - " if (index < 0) continue;", - " const key = line.slice(0, index).trim();", - " const value = line.slice(index + 1).trim();", - " if (key.toLowerCase() === 'status') { const parsed = Number.parseInt(value.split(' ')[0] || '', 10); if (Number.isInteger(parsed)) status = parsed; }", - " else headers[key] = value;", - " }", - " return { status, headers, rest };", - "}", - "function handleGit(req, res) {", - " const url = new URL(req.url || '/', 'http://git-mirror.local');", - " const chunks = [];", - " req.on('data', (chunk) => chunks.push(chunk));", - " req.on('error', (error) => { res.writeHead(500, { 'content-type': 'text/plain' }); res.end(String(error && error.message || error)); });", - " req.on('end', () => {", - " let body = Buffer.concat(chunks);", - " const encoding = String(req.headers['content-encoding'] || '').toLowerCase();", - " try {", - " if (encoding === 'gzip' || encoding === 'x-gzip') body = zlib.gunzipSync(body);", - " else if (encoding === 'deflate') body = zlib.inflateSync(body);", - " else if (encoding === 'br') body = zlib.brotliDecompressSync(body);", - " else if (encoding && encoding !== 'identity') throw new Error(`unsupported content-encoding: ${encoding}`);", - " } catch (error) {", - " res.writeHead(415, { 'content-type': 'text/plain' });", - " res.end(String(error && error.message || error));", - " return;", - " }", - " const env = { ...process.env, GIT_PROJECT_ROOT: projectRoot, GIT_HTTP_EXPORT_ALL: '1', PATH_INFO: decodeURIComponent(url.pathname), REQUEST_METHOD: req.method || 'GET', QUERY_STRING: url.search.slice(1), CONTENT_TYPE: req.headers['content-type'] || '', CONTENT_LENGTH: String(body.length), REMOTE_USER: 'git' };", - " delete env.HTTP_CONTENT_ENCODING;", - " const child = spawn('git', ['http-backend'], { env });", - " let pending = Buffer.alloc(0);", - " let headersSent = false;", - " child.stderr.on('data', (chunk) => process.stderr.write(chunk));", - " child.on('error', (error) => { if (!headersSent) { res.writeHead(500, { 'content-type': 'text/plain' }); headersSent = true; } res.end(String(error && error.message || error)); });", - " child.stdout.on('data', (chunk) => {", - " if (headersSent) { res.write(chunk); return; }", - " pending = Buffer.concat([pending, chunk]);", - " const parsed = parseHeaders(pending);", - " if (!parsed) return;", - " headersSent = true;", - " res.writeHead(parsed.status, parsed.headers);", - " if (parsed.rest.length) res.write(parsed.rest);", - " });", - " child.on('close', (code) => {", - " if (!headersSent) { res.writeHead(code === 0 ? 200 : 500, { 'content-type': 'text/plain' }); headersSent = true; if (pending.length) res.write(pending); }", - " res.end();", - " });", - " child.stdin.end(body);", - " });", - "}", - "http.createServer((req, res) => { if ((req.url || '').startsWith('/healthz')) return sendHealth(res); return handleGit(req, res); }).listen(port, '0.0.0.0', () => { console.log(JSON.stringify({ event: 'git-mirror-http-started', port, projectRoot, mode: process.env.GIT_MIRROR_MODE || null })); });", - "", - ].join("\n"); -} - -function gitMirrorSshSetupScriptLines(): string[] { - return [ - "key_file=${GIT_SSH_KEY_FILE:-/etc/git-mirror/ssh-privatekey}", - "if [ -s \"$key_file\" ]; then", - " chmod 600 \"$key_file\"", - " export GIT_SSH_COMMAND=\"ssh -i $key_file -o StrictHostKeyChecking=accept-new -p 443\"", - "else", - " export GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=accept-new -p 443\"", - "fi", - ]; -} - -function gitMirrorSyncScript(target: ControlPlaneTargetSpec): string { - return [ - "#!/bin/sh", - "set -eu", - "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)", - "mkdir -p /cache/pikasTech", - ...gitMirrorSshSetupScriptLines(), - `repository=${shQuote(target.source.repository)}`, - `source_branch=${shQuote(target.source.branch)}`, - `gitops_branch=${shQuote(target.gitops.branch)}`, - "repo=\"/cache/${repository}.git\"", - "remote=\"ssh://git@ssh.github.com:443/${repository}.git\"", - "mkdir -p \"$(dirname \"$repo\")\"", - "if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then", - " git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"", - "else", - " rm -rf \"$repo\"", - " git init --bare \"$repo\"", - " git --git-dir=\"$repo\" remote add origin \"$remote\"", - "fi", - "git --git-dir=\"$repo\" config uploadpack.allowReachableSHA1InWant true", - "git --git-dir=\"$repo\" config uploadpack.allowAnySHA1InWant true", - "git --git-dir=\"$repo\" config http.receivepack true", - "timeout 180 git --git-dir=\"$repo\" fetch origin \"+refs/heads/${source_branch}:refs/mirror-stage/heads/${source_branch}\"", - "source_sha=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${source_branch}^{commit}\")", - "git --git-dir=\"$repo\" update-ref \"refs/heads/${source_branch}\" \"$source_sha\"", - "if timeout 180 git --git-dir=\"$repo\" fetch origin \"+refs/heads/${gitops_branch}:refs/mirror-stage/heads/${gitops_branch}\"; then", - " github_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)", - " local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)", - " if [ -z \"$local_gitops\" ] && [ -n \"$github_gitops\" ]; then", - " git --git-dir=\"$repo\" update-ref \"refs/heads/${gitops_branch}\" \"$github_gitops\"", - " elif [ -n \"$local_gitops\" ] && [ -n \"$github_gitops\" ] && [ \"$local_gitops\" != \"$github_gitops\" ] && git --git-dir=\"$repo\" merge-base --is-ancestor \"$local_gitops\" \"$github_gitops\"; then", - " git --git-dir=\"$repo\" update-ref \"refs/heads/${gitops_branch}\" \"$github_gitops\"", - " fi", - "fi", - "git --git-dir=\"$repo\" update-server-info", - "local_source=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${source_branch}^{commit}\" 2>/dev/null || true)", - "github_source=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${source_branch}^{commit}\" 2>/dev/null || true)", - "local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)", - "github_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)", - "pending=false; if [ -n \"$local_gitops\" ] && { [ -z \"$github_gitops\" ] || [ \"$local_gitops\" != \"$github_gitops\" ]; }; then pending=true; fi", - "json_ref() { if [ -n \"$1\" ]; then printf '\"%s\"' \"$1\"; else printf null; fi; }", - "cat > /cache/HWLAB.last-sync.json </dev/null || true)", - "if [ -n \"$local_gitops\" ]; then", - " git --git-dir=\"$repo\" -c remote.origin.mirror=false push origin \"refs/heads/${gitops_branch}:refs/heads/${gitops_branch}\"", - " git --git-dir=\"$repo\" fetch origin \"+refs/heads/${gitops_branch}:refs/mirror-stage/heads/${gitops_branch}\"", - "fi", - "git --git-dir=\"$repo\" update-server-info", - "github_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)", - "pending=false; if [ -n \"$local_gitops\" ] && { [ -z \"$github_gitops\" ] || [ \"$local_gitops\" != \"$github_gitops\" ]; }; then pending=true; fi", - "json_ref() { if [ -n \"$1\" ]; then printf '\"%s\"' \"$1\"; else printf null; fi; }", - "cat > /cache/HWLAB.last-flush.json </dev/null || printf '{}') -if [ "$local_path_enabled" = true ]; then - kubectl -n "$local_path_ns" get cm "$local_path_configmap" -o 'go-template={{ index .data "helperPod.yaml" }}' >/tmp/hwlab-local-path-helper.yaml 2>/tmp/hwlab-local-path-helper.err || true - python3 - "$local_path_ns" "$local_path_configmap" "$local_path_deployment" "$local_path_helper_image" "$local_path_pull_policy" /tmp/hwlab-local-path-helper.yaml <<'PY' >/tmp/hwlab-local-path-fragment.json -import json, pathlib, re, subprocess, sys -ns, configmap, deployment, expected_image, expected_policy, helper_path = sys.argv[1:7] -text = pathlib.Path(helper_path).read_text(errors="replace") if pathlib.Path(helper_path).exists() else "" -def run(args): - return subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) -def exists(args): - return run(args).returncode == 0 -def ready_deploy(): - data = run(["kubectl", "-n", ns, "get", "deployment", deployment, "-o", "json"]) - if data.returncode != 0: - return False - obj=json.loads(data.stdout) - desired=int(obj.get("spec", {}).get("replicas") or 0) - ready=int(obj.get("status", {}).get("readyReplicas") or 0) - return desired > 0 and ready == desired -image_match = re.search(r"(?m)^\\s*image:\\s*['\\\"]?([^'\\\"\\s]+)", text) -policy_match = re.search(r"(?m)^\\s*imagePullPolicy:\\s*['\\\"]?([^'\\\"\\s]+)", text) -current_image = image_match.group(1) if image_match else None -current_policy = policy_match.group(1) if policy_match else None -config_exists = exists(["kubectl", "-n", ns, "get", "configmap", configmap]) -deployment_exists = exists(["kubectl", "-n", ns, "get", "deployment", deployment]) -payload = { - "enabled": True, - "namespace": ns, - "configMap": configmap, - "provisionerDeployment": deployment, - "configMapExists": config_exists, - "deploymentExists": deployment_exists, - "deploymentReady": ready_deploy(), - "expectedHelperImage": expected_image, - "helperImage": current_image, - "expectedImagePullPolicy": expected_policy, - "imagePullPolicy": current_policy, - "ready": config_exists and deployment_exists and ready_deploy() and current_image == expected_image and current_policy == expected_policy, -} -print(json.dumps(payload)) -PY -else - printf '{"enabled":false,"ready":true}\\n' >/tmp/hwlab-local-path-fragment.json -fi -local_path_fragment=$(cat /tmp/hwlab-local-path-fragment.json 2>/dev/null || printf '{"enabled":false,"ready":true}') -if [ "$argo_repo_credential_enabled" = true ]; then - python3 - "$argo_ns" "$argo_repo_credential_secret" "$argo_repo_credential_url" "$argo_repo_credential_source_ns" "$argo_repo_credential_source_name" "$argo_repo_credential_source_key" <<'PY' >/tmp/hwlab-argo-repository-credential-fragment.json -import json, subprocess, sys -argo_ns, secret, repo_url, source_ns, source_name, source_key = sys.argv[1:7] -def run(args): - return subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) -repo = run(["kubectl", "-n", argo_ns, "get", "secret", secret, "-o", "json"]) -source = run(["kubectl", "-n", source_ns, "get", "secret", source_name, "-o", "json"]) -repo_payload = json.loads(repo.stdout) if repo.returncode == 0 and repo.stdout.strip() else {} -source_payload = json.loads(source.stdout) if source.returncode == 0 and source.stdout.strip() else {} -labels = repo_payload.get("metadata", {}).get("labels", {}) if repo_payload else {} -data = repo_payload.get("data", {}) if repo_payload else {} -source_data = source_payload.get("data", {}) if source_payload else {} -payload = { - "required": True, - "ready": repo.returncode == 0 and source.returncode == 0 and labels.get("argocd.argoproj.io/secret-type") == "repository" and all(key in data for key in ["type", "url", "sshPrivateKey"]) and source_key in source_data, - "secret": secret, - "repoURL": repo_url, - "sourceSecret": {"namespace": source_ns, "name": source_name, "key": source_key, "present": source.returncode == 0 and source_key in source_data}, - "valuesPrinted": False, -} -print(json.dumps(payload)) -PY -else - printf '{"required":false,"ready":true,"valuesPrinted":false}\\n' >/tmp/hwlab-argo-repository-credential-fragment.json -fi -argo_repository_credential_fragment=$(cat /tmp/hwlab-argo-repository-credential-fragment.json 2>/dev/null || printf '{"required":false,"ready":true,"valuesPrinted":false}') -python3 - "$argo_ns" "$argo_manage_endpoint_bridge" <<'PY' >/tmp/hwlab-argo-resource-tracking-fragment.json -import json, subprocess, sys -namespace, manage = sys.argv[1:3] -expect_manage = manage == "true" -result = subprocess.run(["kubectl", "-n", namespace, "get", "configmap", "argocd-cm", "-o", "json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) -payload = {"manageEndpointBridge": expect_manage, "configMap": "argocd-cm", "exists": result.returncode == 0} -def endpoint_resources_excluded(exclusions): - for item in exclusions.split("- apiGroups:"): - if "kinds:" not in item: - continue - kinds = [line.strip()[2:].strip() for line in item.splitlines() if line.strip().startswith("- ")] - if "Endpoints" in kinds or "EndpointSlice" in kinds: - return True - return False -if result.returncode == 0: - data = json.loads(result.stdout).get("data", {}) - exclusions = data.get("resource.exclusions", "") - payload["endpointResourcesExcluded"] = endpoint_resources_excluded(exclusions) - payload["endpointIgnoreUpdates"] = "resource.customizations.ignoreResourceUpdates.Endpoints" in data - payload["endpointSliceIgnoreUpdates"] = "resource.customizations.ignoreResourceUpdates.discovery.k8s.io_EndpointSlice" in data -else: - payload["endpointResourcesExcluded"] = None - payload["endpointIgnoreUpdates"] = None - payload["endpointSliceIgnoreUpdates"] = None -payload["ready"] = payload["exists"] and (not expect_manage or (payload["endpointResourcesExcluded"] is False and payload["endpointIgnoreUpdates"] is False and payload["endpointSliceIgnoreUpdates"] is False)) -print(json.dumps(payload)) -PY -argo_resource_tracking_fragment=$(cat /tmp/hwlab-argo-resource-tracking-fragment.json 2>/dev/null || printf '{"ready":false,"parseError":true}') cat </dev/null 2>&1 && printf true || printf false),"controllerReady":$(deploy_ready tekton-pipelines tekton-pipelines-controller),"webhookReady":$(deploy_ready tekton-pipelines tekton-pipelines-webhook)},"ciNamespace":{"name":"$ci_ns","exists":$(exists_ns "$ci_ns"),"serviceAccountExists":$(exists_res "$ci_ns" serviceaccount "$service_account"),"pipelineExists":$(exists_res "$ci_ns" pipeline "$pipeline")},"storage":{"localPath":$local_path_fragment},"gitMirror":{"namespace":"$gitmirror_ns","namespaceExists":$(exists_ns "$gitmirror_ns"),"readDeploymentReady":$(deploy_ready "$gitmirror_ns" "$read_deploy"),"writeDeploymentReady":$(deploy_ready "$gitmirror_ns" "$write_deploy"),"readServiceExists":$(exists_res "$gitmirror_ns" service "$read_svc"),"writeServiceExists":$(exists_res "$gitmirror_ns" service "$write_svc"),"readEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$read_svc"),"writeEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$write_svc"),"cachePvcExists":$(exists_res "$gitmirror_ns" pvc "$cache_pvc"),"summary":{"localSource":null,"githubSource":null,"localGitops":null,"githubGitops":null,"pendingFlush":null,"flushNeeded":null,"githubInSync":null}},"argo":{"namespace":"$argo_ns","namespaceExists":$(exists_ns "$argo_ns"),"installed":$(kubectl get crd applications.argoproj.io appprojects.argoproj.io >/dev/null 2>&1 && printf true || printf false),"projectExists":$(kubectl -n "$argo_ns" get appproject "$argo_project" >/dev/null 2>&1 && printf true || printf false),"applicationExists":$(kubectl -n "$argo_ns" get application "$argo_app" >/dev/null 2>&1 && printf true || printf false),"resourceTracking":$argo_resource_tracking_fragment,"repositoryCredential":$argo_repository_credential_fragment,"install":$argo_fragment},"registry":{"endpoint":"$registry","ready":$registry_ready,"toolsImage":"$tools_image","toolsImageReady":$tools_image_ready},"runtimeNamespace":{"name":"$runtime_ns","exists":$(exists_ns "$runtime_ns")}}} +{"observedAt":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","node":"$node","lane":"$lane","components":{"tekton":{"installed":$(kubectl get crd pipelines.tekton.dev pipelineruns.tekton.dev >/dev/null 2>&1 && printf true || printf false),"controllerReady":$(deploy_ready tekton-pipelines tekton-pipelines-controller),"webhookReady":$(deploy_ready tekton-pipelines tekton-pipelines-webhook)},"ciNamespace":{"name":"$ci_ns","exists":$(exists_ns "$ci_ns"),"serviceAccountExists":$(exists_res "$ci_ns" serviceaccount "$service_account"),"pipelineExists":$(exists_res "$ci_ns" pipeline "$pipeline")},"gitMirror":{"namespace":"$gitmirror_ns","namespaceExists":$(exists_ns "$gitmirror_ns"),"readDeploymentReady":$(deploy_ready "$gitmirror_ns" "$read_deploy"),"writeDeploymentReady":$(deploy_ready "$gitmirror_ns" "$write_deploy"),"readServiceExists":$(exists_res "$gitmirror_ns" service "$read_svc"),"writeServiceExists":$(exists_res "$gitmirror_ns" service "$write_svc"),"readEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$read_svc"),"writeEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$write_svc"),"cachePvcExists":$(exists_res "$gitmirror_ns" pvc "$cache_pvc"),"summary":{"localSource":null,"githubSource":null,"localGitops":null,"githubGitops":null,"pendingFlush":null,"flushNeeded":null,"githubInSync":null}},"argo":{"namespace":"$argo_ns","namespaceExists":$(exists_ns "$argo_ns"),"installed":$(kubectl get crd applications.argoproj.io appprojects.argoproj.io >/dev/null 2>&1 && printf true || printf false),"projectExists":$(kubectl -n "$argo_ns" get appproject "$argo_project" >/dev/null 2>&1 && printf true || printf false),"applicationExists":$(kubectl -n "$argo_ns" get application "$argo_app" >/dev/null 2>&1 && printf true || printf false),"install":$argo_fragment},"registry":{"endpoint":"$registry","ready":$registry_ready,"toolsImage":"$tools_image","toolsImageReady":$tools_image_ready},"runtimeNamespace":{"name":"$runtime_ns","exists":$(exists_ns "$runtime_ns")}}} JSON `; } -function applyScript(yaml: string, target: ControlPlaneTargetSpec): string { +function applyScript(yaml: string): string { const encoded = Buffer.from(yaml, "utf8").toString("base64"); - const localPath = target.storage?.localPath ?? null; - const helperPod = localPath === null ? "" : localPathHelperPodYaml(localPath); - const helperEncoded = Buffer.from(helperPod, "utf8").toString("base64"); - const argoRepositoryCredential = target.argo.repositoryCredential; return ` set +e manifest=$(mktemp /tmp/hwlab-node-infra.XXXXXX.yaml) printf %s ${shQuote(encoded)} | base64 -d >"$manifest" kubectl apply --server-side --field-manager=unidesk-hwlab-node-control-plane -f "$manifest" >/tmp/hwlab-node-infra-apply.out 2>/tmp/hwlab-node-infra-apply.err rc=$? -storage_rc=0 -repo_credential_rc=0 -if [ "$rc" -eq 0 ] && [ ${localPath === null ? "false" : "true"} = true ]; then - local_path_ns=${shQuote(localPath?.namespace ?? "")} - local_path_configmap=${shQuote(localPath?.configMapName ?? "")} - local_path_deployment=${shQuote(localPath?.provisionerDeployment ?? "")} - helper_path=$(mktemp /tmp/hwlab-local-path-helper.XXXXXX.yaml) - printf %s ${shQuote(helperEncoded)} | base64 -d >"$helper_path" - patch_path=$(mktemp /tmp/hwlab-local-path-helper.XXXXXX.json) - python3 - "$helper_path" "$patch_path" <<'PY' -import json, pathlib, sys -helper=pathlib.Path(sys.argv[1]).read_text() -pathlib.Path(sys.argv[2]).write_text(json.dumps({"data":{"helperPod.yaml": helper}}, ensure_ascii=False)) -PY - ( - kubectl -n "$local_path_ns" patch configmap "$local_path_configmap" --type merge -p "$(cat "$patch_path")" - patch_rc=$? - if [ "$patch_rc" -eq 0 ]; then - kubectl -n "$local_path_ns" rollout restart deployment "$local_path_deployment" || true - kubectl -n "$local_path_ns" get pod -o name | grep '^pod/helper-pod-create-pvc-' | xargs -r kubectl -n "$local_path_ns" delete --ignore-not-found=true - fi - exit "$patch_rc" - ) >/tmp/hwlab-node-local-path.out 2>/tmp/hwlab-node-local-path.err - storage_rc=$? - rm -f "$helper_path" "$patch_path" -fi -if [ "$rc" -eq 0 ] && [ ${argoRepositoryCredential?.enabled === true ? "true" : "false"} = true ]; then - argo_ns=${shQuote(target.argo.namespace)} - repo_secret=${shQuote(argoRepositoryCredential?.secretName ?? "")} - repo_url=${shQuote(argoRepositoryCredential?.repoURL ?? "")} - source_ns=${shQuote(argoRepositoryCredential?.sourceSecret.namespace ?? "")} - source_secret=${shQuote(argoRepositoryCredential?.sourceSecret.name ?? "")} - source_key=${shQuote(argoRepositoryCredential?.sourceSecret.key ?? "")} - repo_key_file=$(mktemp /tmp/hwlab-argo-repository-key.XXXXXX) - ( - set -eu - kubectl -n "$source_ns" get secret "$source_secret" -o "jsonpath={.data.$source_key}" | base64 -d >"$repo_key_file" - kubectl -n "$argo_ns" create secret generic "$repo_secret" --from-literal=type=git --from-literal=url="$repo_url" --from-file=sshPrivateKey="$repo_key_file" --dry-run=client -o yaml \\ - | kubectl label --local -f - argocd.argoproj.io/secret-type=repository app.kubernetes.io/part-of=hwlab-node-control-plane hwlab.pikastech.local/node=${shQuote(target.node)} hwlab.pikastech.local/lane=${shQuote(target.lane)} -o yaml \\ - | kubectl apply --server-side --force-conflicts --field-manager=unidesk-hwlab-node-argo-repository -f - - ) >/tmp/hwlab-node-argo-repository.out 2>/tmp/hwlab-node-argo-repository.err - repo_credential_rc=$? - rm -f "$repo_key_file" -fi -python3 - "$rc" "$storage_rc" "$repo_credential_rc" <<'PY' +python3 - "$rc" <<'PY' import json, pathlib, sys out=pathlib.Path('/tmp/hwlab-node-infra-apply.out').read_text(errors='replace') if pathlib.Path('/tmp/hwlab-node-infra-apply.out').exists() else '' err=pathlib.Path('/tmp/hwlab-node-infra-apply.err').read_text(errors='replace') if pathlib.Path('/tmp/hwlab-node-infra-apply.err').exists() else '' -storage_out=pathlib.Path('/tmp/hwlab-node-local-path.out').read_text(errors='replace') if pathlib.Path('/tmp/hwlab-node-local-path.out').exists() else '' -storage_err=pathlib.Path('/tmp/hwlab-node-local-path.err').read_text(errors='replace') if pathlib.Path('/tmp/hwlab-node-local-path.err').exists() else '' -repo_out=pathlib.Path('/tmp/hwlab-node-argo-repository.out').read_text(errors='replace') if pathlib.Path('/tmp/hwlab-node-argo-repository.out').exists() else '' -repo_err=pathlib.Path('/tmp/hwlab-node-argo-repository.err').read_text(errors='replace') if pathlib.Path('/tmp/hwlab-node-argo-repository.err').exists() else '' -print(json.dumps({'applyExitCode': int(sys.argv[1]), 'stdoutPreview': out[-2000:], 'stderrPreview': err[-2000:], 'storage': {'localPathApplyExitCode': int(sys.argv[2]), 'stdoutPreview': storage_out[-2000:], 'stderrPreview': storage_err[-2000:]}, 'argoRepositoryCredential': {'applyExitCode': int(sys.argv[3]), 'stdoutPreview': repo_out[-2000:], 'stderrPreview': repo_err[-2000:], 'valuesPrinted': False}, 'runtimeRolloutTriggered': False, 'pk01Touched': False}, ensure_ascii=False)) +print(json.dumps({'applyExitCode': int(sys.argv[1]), 'stdoutPreview': out[-2000:], 'stderrPreview': err[-2000:], 'runtimeRolloutTriggered': False, 'pk01Touched': False}, ensure_ascii=False)) PY rm -f "$manifest" -if [ "$rc" -ne 0 ]; then exit "$rc"; fi -if [ "$storage_rc" -ne 0 ]; then exit "$storage_rc"; fi -exit "$repo_credential_rc" +exit "$rc" `; } -function localPathHelperPodYaml(spec: LocalPathStorageSpec): string { - return [ - "apiVersion: v1", - "kind: Pod", - "metadata:", - " name: helper-pod", - "spec:", - " containers:", - " - name: helper-pod", - ` image: "${spec.helperImage}"`, - ` imagePullPolicy: ${spec.imagePullPolicy}`, - "", - ].join("\n"); -} - function toolsImageStatus(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, timeoutSeconds: number): { registryReady: boolean; toolsImageReady: boolean; @@ -1719,7 +1134,6 @@ function statusNext( gitMirror: Record, argo: Record, ciNamespace: Record, - storageReady: boolean, ): Record { const bootstrapMissing = !boolField(ciNamespace, "exists") || !boolField(gitMirror, "namespaceExists") @@ -1729,7 +1143,6 @@ function statusNext( const blockers: string[] = []; if (!boolField(registry, "ready")) blockers.push("node-local-registry-not-ready"); if (!boolField(registry, "toolsImageReady")) blockers.push("tools-image-missing"); - if (!storageReady) blockers.push("local-path-storage-not-ready"); if (bootstrapMissing) blockers.push("control-plane-bootstrap-missing"); const argoInstall = record(argo.install); if (!boolField(argo, "installed")) blockers.push("argocd-not-installed"); @@ -1749,9 +1162,6 @@ function statusNext( if (!boolField(registry, "toolsImageReady")) { next.buildToolsImage = "准备受控 D601 tools-image build/publish 入口后提升 control-plane readiness。"; } - if (!storageReady) { - next.fixLocalPathStorage = `bun scripts/cli.ts hwlab nodes control-plane infra apply --node ${node.id} --lane ${target.lane} --confirm`; - } if (!boolField(argo, "installed")) { next.installArgo = "准备受控 D601 Argo CD 安装入口后再进入 runtime rollout。"; } @@ -1857,7 +1267,6 @@ function argoApplyStartScript(node: ControlPlaneNodeSpec, target: ControlPlaneTa const desiredEncoded = Buffer.from(desiredYaml, "utf8").toString("base64"); const rewritesEncoded = Buffer.from(JSON.stringify(target.argo.install.imageRewrites), "utf8").toString("base64"); const preloadEncoded = Buffer.from(JSON.stringify(target.argo.install.preloadImages), "utf8").toString("base64"); - const obsoleteApplications = shellSingleQuotedLines(target.argo.obsoleteApplications); return ` set -eu state_dir=${shQuote(stateDir)} @@ -1874,19 +1283,11 @@ namespace=${shQuote(target.argo.namespace)} manifest_url=${shQuote(target.argo.install.manifestUrl)} field_manager=${shQuote(target.argo.install.fieldManager)} readiness_timeout=${shQuote(String(target.argo.install.readinessTimeoutSeconds))} -repo_credential_enabled=${target.argo.repositoryCredential?.enabled === true ? "true" : "false"} -repo_secret=${shQuote(target.argo.repositoryCredential?.secretName ?? "")} -repo_url=${shQuote(target.argo.repositoryCredential?.repoURL ?? "")} -source_ns=${shQuote(target.argo.repositoryCredential?.sourceSecret.namespace ?? "")} -source_secret=${shQuote(target.argo.repositoryCredential?.sourceSecret.name ?? "")} -source_key=${shQuote(target.argo.repositoryCredential?.sourceSecret.key ?? "")} -active_app=${shQuote(target.argo.applicationName)} log="$state_dir/job.log" status="$state_dir/status.json" install_yaml="$state_dir/install.yaml" rendered_yaml="$state_dir/install.rendered.yaml" desired_yaml="$state_dir/desired.yaml" -obsolete_apps_file="$state_dir/obsolete-applications.txt" write_status() { state="$1"; shift message="$1"; shift || true @@ -1902,23 +1303,11 @@ ${proxyExportBlock(node)} printf %s ${shQuote(desiredEncoded)} | base64 -d >"$desired_yaml" printf %s ${shQuote(rewritesEncoded)} | base64 -d >"$state_dir/image-rewrites.json" printf %s ${shQuote(preloadEncoded)} | base64 -d >"$state_dir/preload-images.json" - cat >"$obsolete_apps_file" <<'OBSOLETE_APPS' -${obsoleteApplications} -OBSOLETE_APPS kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply --server-side --field-manager="$field_manager" -f - || exit "$?" python3 - "$state_dir/preload-images.json" "$state_dir/image-rewrites.json" <<'PY' >"$state_dir/pull-images.sh" import json, pathlib, shlex, sys preload=json.loads(pathlib.Path(sys.argv[1]).read_text()) rewrites=json.loads(pathlib.Path(sys.argv[2]).read_text()) -def registry_probe(image): - prefix="127.0.0.1:5000/" - if not image.startswith(prefix): - return None - repo_tag=image[len(prefix):] - if ":" not in repo_tag: - return None - repo, tag=repo_tag.rsplit(":", 1) - return "http://127.0.0.1:5000/v2/" + repo + "/manifests/" + tag print("#!/bin/sh") print("set -eu") seen=set() @@ -1928,16 +1317,6 @@ for item in rewrites: if target in seen: continue seen.add(target) - probe=registry_probe(target) - if probe: - print("if curl -fsS --max-time 5 " + shlex.quote(probe) + " >/dev/null 2>&1; then") - print(" echo " + shlex.quote("image already present: " + target)) - print("else") - print(" docker pull " + shlex.quote(pull)) - print(" docker tag " + shlex.quote(pull) + " " + shlex.quote(target)) - print(" docker push " + shlex.quote(target)) - print("fi") - continue print("docker pull " + shlex.quote(pull)) print("docker tag " + shlex.quote(pull) + " " + shlex.quote(target)) print("docker push " + shlex.quote(target)) @@ -1965,30 +1344,7 @@ PY sleep 5 done kubectl get crd applications.argoproj.io appprojects.argoproj.io >/dev/null || exit "$?" - if [ "$repo_credential_enabled" = true ]; then - repo_key_file=$(mktemp /tmp/hwlab-argo-repository-key.XXXXXX) - kubectl -n "$source_ns" get secret "$source_secret" -o "jsonpath={.data.$source_key}" | base64 -d >"$repo_key_file" || exit "$?" - kubectl -n "$namespace" create secret generic "$repo_secret" --from-literal=type=git --from-literal=url="$repo_url" --from-file=sshPrivateKey="$repo_key_file" --dry-run=client -o yaml \\ - | kubectl label --local -f - argocd.argoproj.io/secret-type=repository app.kubernetes.io/part-of=hwlab-node-control-plane hwlab.pikastech.local/node=${shQuote(target.node)} hwlab.pikastech.local/lane=${shQuote(target.lane)} -o yaml \\ - | kubectl apply --server-side --force-conflicts --field-manager=unidesk-hwlab-node-argo-repository -f - || exit "$?" - rm -f "$repo_key_file" - fi - kubectl apply --server-side --force-conflicts --field-manager="$field_manager" -f "$desired_yaml" || exit "$?" - while IFS= read -r obsolete_app; do - [ -n "$obsolete_app" ] || continue - if [ "$obsolete_app" = "$active_app" ]; then - echo "refusing to delete active Argo application listed as obsolete: $obsolete_app" >&2 - exit 42 - fi - kubectl -n "$namespace" delete application "$obsolete_app" --ignore-not-found=true || exit "$?" - done <"$obsolete_apps_file" - kubectl -n "$namespace" rollout restart deployment argocd-repo-server || exit "$?" - kubectl -n "$namespace" rollout status deployment argocd-repo-server --timeout=180s || exit "$?" - kubectl -n "$namespace" rollout restart statefulset argocd-application-controller || exit "$?" - if ! kubectl -n "$namespace" rollout status statefulset argocd-application-controller --timeout=120s; then - kubectl -n "$namespace" delete pod -l app.kubernetes.io/name=argocd-application-controller --ignore-not-found=true --wait=false || exit "$?" - kubectl -n "$namespace" rollout status statefulset argocd-application-controller --timeout=180s || exit "$?" - fi + kubectl apply --server-side --field-manager="$field_manager" -f "$desired_yaml" || exit "$?" write_status succeeded argocd-install-applied } >>"$log" 2>&1 || { rc=$? @@ -2065,7 +1421,7 @@ function manifestObjectSummary(manifest: readonly Record[]): Re } function runTransK3s(kubeRoute: string, script: string, timeoutSeconds: number): CommandResult { - return runCommand([rootPath("scripts", "trans"), kubeRoute, "script", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 }); + return runCommand(["/root/.local/bin/trans", kubeRoute, "script", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 }); } function proxyExportBlock(node: ControlPlaneNodeSpec): string { @@ -2094,15 +1450,6 @@ function shellJsonArray(items: readonly string[]): string { return JSON.stringify([...items]); } -function shellSingleQuotedLines(items: readonly string[]): string { - for (const item of items) validateKubernetesName(item, "argo.obsoleteApplications"); - return items.join("\n"); -} - -function validateKubernetesName(value: string, path: string): void { - if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value) || value.length > 253) throw new Error(`${path} contains invalid Kubernetes metadata.name: ${value}`); -} - function parseRemoteJson(text: string): unknown { const trimmed = text.trim(); if (trimmed.length === 0) return null; @@ -2168,11 +1515,6 @@ function stringArrayField(obj: Record, key: string, path: strin return [...value] as string[]; } -function optionalStringArrayField(obj: Record, key: string, path: string): string[] { - if (obj[key] === undefined) return []; - return stringArrayField(obj, key, path); -} - function stringRecordField(obj: Record, path: string): Record { const result: Record = {}; for (const [key, value] of Object.entries(obj)) { diff --git a/scripts/src/jobs.ts b/scripts/src/jobs.ts index 464a35af..d916cf93 100644 --- a/scripts/src/jobs.ts +++ b/scripts/src/jobs.ts @@ -418,8 +418,6 @@ function summarizeRuntimeLaneTriggerJobProgress(job: JobRecord, stdoutTail: stri const stageStatus = stringField(lastEvent.status); const sourceCommit = stringField(lastEvent.sourceCommit) ?? firstMatch(stdoutTail, /"sourceCommit"\s*:\s*"([0-9a-f]{40})"/iu); const pipelineRun = stringField(lastEvent.pipelineRun) ?? firstMatch(stdoutTail, /"pipelineRun"\s*:\s*"([^"]+)"/u); - const node = stringField(lastEvent.node) ?? commandOption(job.command, "--node") ?? "G14"; - const lane = stringField(lastEvent.lane) ?? commandOption(job.command, "--lane") ?? "v03"; const pipelineCreated = /pipelinerun\.tekton\.dev\/[^ \n]+ created/u.test(stdoutTail) ? true : stage === "create-pipelinerun" && stageStatus === "failed" @@ -467,19 +465,13 @@ function summarizeRuntimeLaneTriggerJobProgress(job: JobRecord, stdoutTail: stri slow ? "visibility-warning" : null, ].filter(Boolean).join(" "), nextCommand: pipelineRun - ? `bun scripts/cli.ts hwlab nodes control-plane status --node ${node} --lane ${lane} --pipeline-run ${pipelineRun}` + ? `bun scripts/cli.ts hwlab nodes control-plane status --node ${stringField(lastEvent.node) ?? "G14"} --lane ${stringField(lastEvent.lane) ?? "v03"} --pipeline-run ${pipelineRun}` : job.status === "running" ? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000` : null, }; } -function commandOption(command: readonly string[], name: string): string | null { - const index = command.indexOf(name); - const value = index >= 0 ? command[index + 1] : undefined; - return typeof value === "string" && value.trim().length > 0 && !value.startsWith("--") ? value.trim() : null; -} - function genericJobProgress(job: JobRecord, stderrTailOverride?: string): JobProgressSummary { const nowMs = Date.now(); const stderrTail = stderrTailOverride ?? tailFile(job.stderrFile, 96_000); diff --git a/scripts/src/platform-db.ts b/scripts/src/platform-db.ts index dba3bbba..96b0249d 100644 --- a/scripts/src/platform-db.ts +++ b/scripts/src/platform-db.ts @@ -878,7 +878,7 @@ function connectionStringExport(item: Record, path: string): Co consumers: arrayOfRecords(item.consumers, `${path}.consumers`).map((consumer, index) => ({ scope: stringField(consumer, "scope", `${path}.consumers[${index}]`), secret: stringField(consumer, "secret", `${path}.consumers[${index}]`), - key: kubernetesSecretKey(stringField(consumer, "key", `${path}.consumers[${index}]`), `${path}.consumers[${index}].key`), + key: envKey(stringField(consumer, "key", `${path}.consumers[${index}]`), `${path}.consumers[${index}].key`), })), }; } @@ -909,11 +909,6 @@ function envKey(value: string, path: string): string { return value; } -function kubernetesSecretKey(value: string, path: string): string { - if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path} must be a Kubernetes Secret key`); - return value; -} - function configSummary(pg: PostgresHostConfig): Record { return { path: pg.configPath, diff --git a/scripts/tran b/scripts/tran index 992cef87..67c18135 100755 --- a/scripts/tran +++ b/scripts/tran @@ -1,11 +1,10 @@ #!/bin/sh set -eu -self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -self_repo=$(CDPATH= cd -- "$self_dir/.." && pwd) -repo=${UNIDESK_TRAN_REPO_ROOT:-$self_repo} -if [ ! -f "$repo/scripts/cli.ts" ] && [ -f /root/unidesk/scripts/cli.ts ]; then - repo=/root/unidesk +repo=${UNIDESK_TRAN_REPO_ROOT:-/root/unidesk} +if [ ! -f "$repo/scripts/cli.ts" ]; then + self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + repo=$(CDPATH= cd -- "$self_dir/.." && pwd) fi tran_timeout_seconds() { diff --git a/scripts/trans b/scripts/trans index 7c4a1f23..e0e68a83 100755 --- a/scripts/trans +++ b/scripts/trans @@ -1,11 +1,10 @@ #!/bin/sh set -eu -self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -self_repo=$(CDPATH= cd -- "$self_dir/.." && pwd) -repo=${UNIDESK_TRANS_REPO_ROOT:-$self_repo} -if [ ! -f "$repo/scripts/cli.ts" ] && [ -f /root/unidesk/scripts/cli.ts ]; then - repo=/root/unidesk +repo=${UNIDESK_TRANS_REPO_ROOT:-/root/unidesk} +if [ ! -f "$repo/scripts/cli.ts" ]; then + self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + repo=$(CDPATH= cd -- "$self_dir/.." && pwd) fi exec bun "$repo/scripts/cli.ts" ssh "$@"