feat: add JD01 YAML-first deployment support
This commit is contained in:
@@ -26,6 +26,7 @@ description: UniDesk YAML-first 运维正规化技能。用户提到 ymal-first/
|
||||
- 源码、配置、部署类正规化默认在独立 `.worktree/<task>` 中做;轻量 skill/docs/reference 收敛可按项目规则直接在主 worktree 做。
|
||||
- YAML 是 source of truth。不得新增隐藏代码默认值、schema 数值硬限制、合同测试或测试硬编码策略。
|
||||
- 代码校验只保证字段能被正确读取和渲染:类型、必填、枚举键名、引用存在性。版本号、namespace、endpoint、容量、冷却时间、回退窗口等数值以 YAML 为准。
|
||||
- YAML 文件名、YAML 解析函数名和 YAML 渲染函数名不得携带具体 node/lane 名称或版本实例(例如 `JD01`、`D601`、`v03`、`jd01-v03`)。node/lane/version 只能作为 YAML 变量、selector、target key、template value 或 CLI 参数参与渲染;可复用文件和代码入口必须按职责命名。
|
||||
- 避免“超级配置”。当一个能力同时涉及 target/lane、runtime、scenario、prompt、report、publicExposure、Secret、CI/CD 等不同职责时,按职责拆分到 owning YAML;root YAML 只保存归属和 `configRefs`/path 引用,不承载全部细节。
|
||||
- 跨 YAML 引用应使用稳定的 `path/to/file.yaml#object.path` 或当前 domain parser 明确支持的等价语法。parser 只解析引用、校验存在性/类型/形状和冲突,不生成隐藏默认值,也不把合并后的大对象写成新的 source of truth。
|
||||
- CLI `plan/status` 应输出 redacted 配置引用图:每个 ref 的文件、path、presence、摘要 hash、缺失字段和下一步 drill-down 命令。不要默认 dump 展开后的完整 YAML 或 Secret。
|
||||
|
||||
@@ -64,6 +64,9 @@ controlPlane:
|
||||
D518:
|
||||
route: D518
|
||||
kubeRoute: D518:k3s
|
||||
JD01:
|
||||
route: JD01
|
||||
kubeRoute: JD01:k3s
|
||||
|
||||
lanes:
|
||||
v01:
|
||||
@@ -212,6 +215,10 @@ controlPlane:
|
||||
image: postgres:16-alpine
|
||||
storage: 5Gi
|
||||
port: 5432
|
||||
database: agentrun_v01
|
||||
user: agentrun_v01
|
||||
passwordSourceRef: agentrun/v01-local-postgres.env
|
||||
passwordSourceKey: POSTGRES_PASSWORD
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
readService: git-mirror-http
|
||||
@@ -557,6 +564,261 @@ controlPlane:
|
||||
name: agentrun-v01-tool-unidesk-ssh
|
||||
key: UNIDESK_SSH_CLIENT_TOKEN
|
||||
|
||||
jd01-v02:
|
||||
node: JD01
|
||||
version: v0.2
|
||||
source:
|
||||
statusMode: k3s-git-mirror
|
||||
repository: pikasTech/agentrun
|
||||
branch: v0.2
|
||||
bootstrapFromBranch: v0.1
|
||||
bootstrapTimeoutSeconds: 900
|
||||
bootstrapPollSeconds: 15
|
||||
remote: git@github.com:pikasTech/agentrun.git
|
||||
workspace: /root/workspace/agentrun-v02
|
||||
runtime:
|
||||
namespace: agentrun-v02
|
||||
managerDeployment: agentrun-mgr
|
||||
managerService: agentrun-mgr
|
||||
managerPort: 8080
|
||||
internalBaseUrl: http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080
|
||||
ci:
|
||||
namespace: agentrun-ci
|
||||
pipeline: agentrun-jd01-v02-ci-image-publish
|
||||
pipelineRunPrefix: agentrun-jd01-v02-ci
|
||||
serviceAccountName: agentrun-jd01-v02-tekton-runner
|
||||
registryPrefix: 127.0.0.1:5000/agentrun
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
buildkitImage: 127.0.0.1:5000/hwlab/buildkit:rootless
|
||||
gitops:
|
||||
branch: jd01-v0.2-gitops
|
||||
path: deploy/gitops/node/jd01/runtime-v02
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-jd01-v02
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
deployment:
|
||||
format: unidesk-yaml-only
|
||||
gitopsRoot: deploy/gitops/node/jd01
|
||||
runtimeRenderDir: runtime-v02
|
||||
artifactCatalogPath: deploy/artifact-catalog.jd01-v02.json
|
||||
argocd:
|
||||
project: agentrun-jd01-v02
|
||||
applicationFile: application-v02.yaml
|
||||
manager:
|
||||
serviceAccount: agentrun-jd01-v02-mgr
|
||||
apiKeySecretRef:
|
||||
name: agentrun-v02-api-key
|
||||
key: HWLAB_API_KEY
|
||||
env:
|
||||
AGENTRUN_POSTGRES_POOL_MAX: "4"
|
||||
AGENTRUN_MANAGER_RECONCILER_BATCH_SIZE: "20"
|
||||
AGENTRUN_MANAGER_RECONCILER_ENABLED: "true"
|
||||
AGENTRUN_MANAGER_RECONCILER_INTERVAL_MS: "30000"
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
|
||||
OTEL_SERVICE_NAME: agentrun-manager
|
||||
UNIDESK_NODE_ID: JD01
|
||||
HWLAB_RUNTIME_LANE: v0.3
|
||||
unideskSshEndpointEnv:
|
||||
name: UNIDESK_MAIN_SERVER_IP
|
||||
value: 74.48.78.17
|
||||
bootRepoUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
imageBuild:
|
||||
context: .
|
||||
containerfile: deploy/container/Containerfile
|
||||
repository: agentrun-mgr-env
|
||||
network: host
|
||||
buildArgs:
|
||||
BUN_IMAGE: oven/bun:1-alpine
|
||||
httpProxy: http://127.0.0.1:10808
|
||||
httpsProxy: http://127.0.0.1:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
buildContainerProxy:
|
||||
httpProxy: http://127.0.0.1:10808
|
||||
httpsProxy: http://127.0.0.1:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
envIdentityFiles:
|
||||
- deploy/container/Containerfile
|
||||
- deploy/runtime/boot/agentrun-boot.sh
|
||||
- deploy/runtime/boot/agentrun-mgr.sh
|
||||
- deploy/runtime/boot/agentrun-runner.sh
|
||||
- src
|
||||
- scripts
|
||||
- package.json
|
||||
- bun.lock
|
||||
- tsconfig.json
|
||||
timeoutSeconds: 1800
|
||||
pollSeconds: 15
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 800m
|
||||
memory: 1Gi
|
||||
runner:
|
||||
serviceAccount: agentrun-jd01-v02-runner
|
||||
jobNamePrefix: agentrun-jd01-v02-runner
|
||||
idleTimeoutMs: 600000
|
||||
backendRetry:
|
||||
maxAttempts: 5
|
||||
initialBackoffMs: 1000
|
||||
maxBackoffMs: 30000
|
||||
apiKeySecretRef:
|
||||
name: agentrun-v02-api-key
|
||||
key: HWLAB_API_KEY
|
||||
egressProxyUrl: http://127.0.0.1:10808
|
||||
noProxyExtra:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
retention:
|
||||
maxRunners: 20
|
||||
cleanupOrder: oldest-inactive-last-active-first
|
||||
activeHeartbeatMaxAgeMs: 900000
|
||||
selectors:
|
||||
matchLabels:
|
||||
app.kubernetes.io/part-of: agentrun
|
||||
app.kubernetes.io/name: agentrun-runner
|
||||
app.kubernetes.io/component: runner
|
||||
jobNamePrefixes:
|
||||
- agentrun-jd01-v02-runner
|
||||
- agentrun-v02-runner
|
||||
- agentrun-v01-runner
|
||||
ageBasedCleanup:
|
||||
enabled: false
|
||||
maxAgeHours: 48
|
||||
cancelLifecycle:
|
||||
deliveryMode: manager-epoch
|
||||
gracefulAbortMs: 15000
|
||||
killEscalationMs: 30000
|
||||
staleHeartbeatFencingMs: 900000
|
||||
lateWriteFencing:
|
||||
enabled: true
|
||||
eventStages:
|
||||
- accepted
|
||||
- persisted
|
||||
- delivered
|
||||
- aborting
|
||||
- terminalized
|
||||
- fenced
|
||||
- late-write-rejected
|
||||
localPostgres:
|
||||
enabled: true
|
||||
serviceName: agentrun-v02-postgres
|
||||
image: postgres:16-alpine
|
||||
storage: 5Gi
|
||||
port: 5432
|
||||
database: agentrun_v02
|
||||
user: agentrun_v02
|
||||
passwordSourceRef: agentrun/jd01-v02-local-postgres.env
|
||||
passwordSourceKey: POSTGRES_PASSWORD
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
readService: git-mirror-http
|
||||
readDeployment: git-mirror-http
|
||||
writeService: git-mirror-write
|
||||
writeDeployment: git-mirror-write
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
cachePvc: hwlab-git-mirror-cache
|
||||
cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-jd01-v03-git-mirror-cache
|
||||
sshSecretName: git-mirror-github-ssh
|
||||
githubProxy:
|
||||
host: 127.0.0.1
|
||||
port: 10808
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
syncJobPrefix: git-mirror-agentrun-jd01-v02-sync-manual
|
||||
flushJobPrefix: git-mirror-agentrun-jd01-v02-flush-manual
|
||||
repositories:
|
||||
- key: agentrun
|
||||
repository: pikasTech/agentrun
|
||||
sourceBranch: v0.2
|
||||
gitopsBranch: jd01-v0.2-gitops
|
||||
- key: unidesk
|
||||
repository: pikasTech/unidesk
|
||||
sourceBranch: master
|
||||
- key: agent_skills
|
||||
repository: pikasTech/agent_skills
|
||||
sourceBranch: master
|
||||
database:
|
||||
mode: local-postgres
|
||||
secretRef:
|
||||
name: agentrun-v02-mgr-db
|
||||
key: DATABASE_URL
|
||||
localPostgresExpectedAbsent: false
|
||||
secrets:
|
||||
- id: manager-api-key
|
||||
sourceRef: /root/.config/hwlab-v03/master-server-admin-api-key.env
|
||||
sourceKey: HWLAB_API_KEY
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v02-api-key
|
||||
key: HWLAB_API_KEY
|
||||
- id: runner-api-key-legacy-name
|
||||
sourceRef: /root/.config/hwlab-v03/master-server-admin-api-key.env
|
||||
sourceKey: HWLAB_API_KEY
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-api-key
|
||||
key: HWLAB_API_KEY
|
||||
- id: provider-codex-auth-json
|
||||
sourceMode: file
|
||||
sourceRef: /root/.codex/auth.json
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-provider-codex
|
||||
key: auth.json
|
||||
providerCredential:
|
||||
profile: codex
|
||||
- id: provider-codex-config
|
||||
sourceMode: file
|
||||
sourceRef: agentrun/jd01-v02-provider-codex-config.toml
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-provider-codex
|
||||
key: config.toml
|
||||
providerCredential:
|
||||
profile: codex
|
||||
- id: tool-github-pr-token
|
||||
sourceRef: /root/.config/unidesk/github.env
|
||||
sourceKey: GH_TOKEN
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-tool-github-pr
|
||||
key: GH_TOKEN
|
||||
- id: tool-unidesk-ssh-token
|
||||
sourceRef: /root/unidesk/.state/docker-compose.env
|
||||
sourceKey: UNIDESK_SSH_CLIENT_TOKEN
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-tool-unidesk-ssh
|
||||
key: UNIDESK_SSH_CLIENT_TOKEN
|
||||
|
||||
d518-v02:
|
||||
node: D518
|
||||
version: v0.2
|
||||
|
||||
@@ -49,6 +49,14 @@ targets:
|
||||
mode: direct
|
||||
identities:
|
||||
- github.com
|
||||
JD01:
|
||||
providerId: JD01
|
||||
route: JD01
|
||||
homeDir: /root
|
||||
egress:
|
||||
mode: direct
|
||||
identities:
|
||||
- github.com
|
||||
G14:
|
||||
providerId: G14
|
||||
route: G14
|
||||
|
||||
@@ -8,6 +8,7 @@ metadata:
|
||||
- 1010
|
||||
- 1119
|
||||
- 1148
|
||||
- 1234
|
||||
imagePolicy:
|
||||
requireReproducibleBuildSource: true
|
||||
forbidPrivateOrNodeLocalImagesAsInputs: true
|
||||
@@ -152,6 +153,104 @@ nodes:
|
||||
- 74.48.78.17
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
JD01:
|
||||
route: JD01
|
||||
kubeRoute: JD01:k3s
|
||||
k3s:
|
||||
serviceName: k3s
|
||||
dropInPath: /etc/systemd/system/k3s.service.d/20-unidesk-node-config.conf
|
||||
nodeStatusName: jd01
|
||||
execStartPre: []
|
||||
install:
|
||||
enabled: true
|
||||
channel: stable
|
||||
version: v1.36.2+k3s1
|
||||
installScriptUrl: https://get.k3s.io
|
||||
binaryUrl: https://github.com/k3s-io/k3s/releases/download/v1.36.2%2Bk3s1/k3s
|
||||
sha256Url: https://github.com/k3s-io/k3s/releases/download/v1.36.2%2Bk3s1/sha256sum-amd64.txt
|
||||
expectedSha256: 65a55ec56c24eab44383086166ec620a491952b7e23941a49ddca6e8a4c4b4de
|
||||
hostProxyConfigRef: config/platform-infra/host-proxy.yaml#targets.JD01
|
||||
proxyEnvPath: /etc/unidesk/proxy.env
|
||||
registriesYamlPath: /etc/rancher/k3s/registries.yaml
|
||||
localRegistry:
|
||||
containerName: registry
|
||||
image: docker.m.daocloud.io/library/registry:2
|
||||
canonicalImage: registry:2
|
||||
bind: 127.0.0.1:5000:5000
|
||||
state:
|
||||
dir: /root/.unidesk/k3s-install
|
||||
logPath: /root/.unidesk/k3s-install/install.log
|
||||
statusPath: /root/.unidesk/k3s-install/status.json
|
||||
downloads:
|
||||
connectTimeoutSeconds: 15
|
||||
maxTimeSeconds: 1200
|
||||
retry: 8
|
||||
retryDelaySeconds: 5
|
||||
serverArgs:
|
||||
- server
|
||||
- --disable
|
||||
- traefik
|
||||
- --disable
|
||||
- servicelb
|
||||
- --disable
|
||||
- metrics-server
|
||||
- --node-name
|
||||
- JD01
|
||||
- --node-label
|
||||
- unidesk.ai/node-id=JD01
|
||||
- --node-label
|
||||
- unidesk.ai/provider-id=JD01
|
||||
- --tls-san
|
||||
- 127.0.0.1
|
||||
- --tls-san
|
||||
- host.docker.internal
|
||||
- --write-kubeconfig-mode
|
||||
- "644"
|
||||
- --kubelet-arg
|
||||
- image-gc-high-threshold=95
|
||||
- --kubelet-arg
|
||||
- image-gc-low-threshold=90
|
||||
- --kubelet-arg
|
||||
- max-pods=500
|
||||
kubelet:
|
||||
maxPods: 500
|
||||
registry:
|
||||
endpoint: 127.0.0.1:5000
|
||||
egressProxy:
|
||||
mode: host-route
|
||||
clientName: jd01-host-proxy
|
||||
hostProxyConfigRef: config/platform-infra/host-proxy.yaml#targets.JD01
|
||||
proxyEnvPath: /etc/unidesk/proxy.env
|
||||
proxyUrl: http://10.42.0.1:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- kubernetes
|
||||
- kubernetes.default
|
||||
- kubernetes.default.svc
|
||||
- argocd-repo-server
|
||||
- argocd-repo-server.argocd
|
||||
- argocd-redis
|
||||
- argocd-redis.argocd
|
||||
- git-mirror-http
|
||||
- git-mirror-http.devops-infra
|
||||
- git-mirror-write
|
||||
- git-mirror-write.devops-infra
|
||||
- 10.0.0.0/8
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 82.156.23.220
|
||||
- 74.48.78.17
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
|
||||
targets:
|
||||
- id: d601-v03
|
||||
@@ -174,6 +273,8 @@ targets:
|
||||
cachePvcStorage: 20Gi
|
||||
cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-d601-v03-git-mirror-cache
|
||||
servicePort: 8080
|
||||
readContainerPort: 8080
|
||||
writeContainerPort: 8080
|
||||
deploymentReplicas: 1
|
||||
secretName: git-mirror-github-ssh
|
||||
syncConfigMapName: git-mirror-sync-script
|
||||
@@ -184,6 +285,8 @@ targets:
|
||||
egressProxy:
|
||||
mode: node-global
|
||||
required: true
|
||||
podHostNetwork: false
|
||||
injectPodEnv: false
|
||||
githubTransport:
|
||||
mode: ssh
|
||||
privateKeySecretKey: ssh-privatekey
|
||||
@@ -195,6 +298,29 @@ targets:
|
||||
knownHostsSourceKey: GITHUB_KNOWN_HOSTS_B64
|
||||
knownHostsSourceEncoding: base64
|
||||
tekton:
|
||||
install:
|
||||
enabled: true
|
||||
sourceKind: url
|
||||
version: pipeline-v1.12.0-triggers-v0.34.0
|
||||
fieldManager: unidesk-hwlab-node-tekton
|
||||
manifests:
|
||||
- name: pipeline
|
||||
url: https://infra.tekton.dev/tekton-releases/pipeline/previous/v1.12.0/release.yaml
|
||||
- name: triggers
|
||||
url: https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/release.yaml
|
||||
- name: triggers-interceptors
|
||||
url: https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/interceptors.yaml
|
||||
requiredCrds:
|
||||
- pipelines.tekton.dev
|
||||
- pipelineruns.tekton.dev
|
||||
- tasks.tekton.dev
|
||||
- taskruns.tekton.dev
|
||||
expectedDeploymentNamespaces:
|
||||
- tekton-pipelines
|
||||
- tekton-pipelines-resolvers
|
||||
readinessTimeoutSeconds: 900
|
||||
runtimeProxy:
|
||||
enabled: false
|
||||
pipelineName: hwlab-d601-v03-ci-image-publish
|
||||
serviceAccountName: hwlab-d601-v03-tekton-runner
|
||||
pipelineRunPrefix: hwlab-d601-v03-ci-poll
|
||||
@@ -319,6 +445,218 @@ targets:
|
||||
expectedStatefulSets:
|
||||
- argocd-application-controller
|
||||
readinessTimeoutSeconds: 600
|
||||
runtimeProxy:
|
||||
enabled: false
|
||||
- id: jd01-v03
|
||||
node: JD01
|
||||
lane: v03
|
||||
enabled: true
|
||||
ciNamespace: hwlab-ci
|
||||
runtimeNamespace: hwlab-v03
|
||||
source:
|
||||
repository: pikasTech/HWLAB
|
||||
branch: v0.3
|
||||
gitops:
|
||||
branch: v0.3-gitops
|
||||
path: deploy/gitops/node/jd01/runtime-v03
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
serviceReadName: git-mirror-http
|
||||
serviceWriteName: git-mirror-write
|
||||
cachePvcName: hwlab-git-mirror-cache
|
||||
cachePvcStorage: 20Gi
|
||||
cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-jd01-v03-git-mirror-cache
|
||||
servicePort: 8080
|
||||
readContainerPort: 8080
|
||||
writeContainerPort: 8081
|
||||
deploymentReplicas: 1
|
||||
secretName: git-mirror-github-ssh
|
||||
syncConfigMapName: git-mirror-sync-script
|
||||
syncJobPrefix: git-mirror-hwlab-jd01-v03-sync-manual
|
||||
flushJobPrefix: git-mirror-hwlab-jd01-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
|
||||
egressProxy:
|
||||
mode: host-route
|
||||
required: true
|
||||
podHostNetwork: false
|
||||
injectPodEnv: true
|
||||
githubTransport:
|
||||
mode: ssh
|
||||
privateKeySecretKey: ssh-privatekey
|
||||
privateKeySourceRef: github/hwlab-git-mirror-ssh.env
|
||||
privateKeySourceKey: GITHUB_SSH_PRIVATE_KEY_B64
|
||||
privateKeySourceEncoding: base64
|
||||
knownHostsSecretKey: known_hosts
|
||||
knownHostsSourceRef: github/hwlab-git-mirror-ssh.env
|
||||
knownHostsSourceKey: GITHUB_KNOWN_HOSTS_B64
|
||||
knownHostsSourceEncoding: base64
|
||||
tekton:
|
||||
install:
|
||||
enabled: true
|
||||
sourceKind: url
|
||||
version: pipeline-v1.12.0-triggers-v0.34.0
|
||||
fieldManager: unidesk-hwlab-node-tekton
|
||||
manifests:
|
||||
- name: pipeline
|
||||
url: https://infra.tekton.dev/tekton-releases/pipeline/previous/v1.12.0/release.yaml
|
||||
- name: triggers
|
||||
url: https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/release.yaml
|
||||
- name: triggers-interceptors
|
||||
url: https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/interceptors.yaml
|
||||
requiredCrds:
|
||||
- pipelines.tekton.dev
|
||||
- pipelineruns.tekton.dev
|
||||
- tasks.tekton.dev
|
||||
- taskruns.tekton.dev
|
||||
expectedDeploymentNamespaces:
|
||||
- tekton-pipelines
|
||||
- tekton-pipelines-resolvers
|
||||
readinessTimeoutSeconds: 900
|
||||
runtimeProxy:
|
||||
enabled: false
|
||||
pipelineName: hwlab-jd01-v03-ci-image-publish
|
||||
serviceAccountName: hwlab-jd01-v03-tekton-runner
|
||||
pipelineRunPrefix: hwlab-jd01-v03-ci-poll
|
||||
gitWorkspaceSecret:
|
||||
name: hwlab-git-ssh
|
||||
namespace: hwlab-ci
|
||||
sourceRefFrom: gitMirror.githubTransport
|
||||
privateKeySecretKey: ssh-privatekey
|
||||
knownHostsSecretKey: known_hosts
|
||||
runtimeObserverRbac:
|
||||
namespace: hwlab-v03
|
||||
roleName: hwlab-jd01-v03-runtime-observer
|
||||
roleBindingName: hwlab-jd01-v03-runtime-observer
|
||||
argoObserverRbac:
|
||||
namespace: argocd
|
||||
roleName: hwlab-jd01-v03-argo-observer
|
||||
roleBindingName: hwlab-jd01-v03-argo-observer
|
||||
toolsImage:
|
||||
output: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
imagePullPolicy: Always
|
||||
sourceKind: dockerfile
|
||||
context: .
|
||||
dockerfileInline:
|
||||
filename: hwlab-ci-node-tools.public.Dockerfile
|
||||
lines:
|
||||
- FROM docker.io/oven/bun:1.3.13 AS bun-runtime
|
||||
- FROM docker.io/docker:29-cli AS docker-cli
|
||||
- FROM docker.io/library/golang:1.24-bookworm AS golang-toolchain
|
||||
- FROM docker.io/library/node:22-bookworm-slim AS node-runtime
|
||||
- FROM docker.io/library/python:3.12-bookworm
|
||||
- ARG HTTP_PROXY
|
||||
- ARG HTTPS_PROXY
|
||||
- ARG ALL_PROXY
|
||||
- ARG NO_PROXY
|
||||
- ARG http_proxy
|
||||
- ARG https_proxy
|
||||
- ARG all_proxy
|
||||
- ARG no_proxy
|
||||
- COPY --from=golang-toolchain /usr/local/go /usr/local/go
|
||||
- COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
|
||||
- COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
- COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun
|
||||
- COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
|
||||
- ENV PATH=/usr/local/go/bin:$PATH
|
||||
- RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||
- RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
|
||||
- ENV HWLAB_CI_NODE_DEPS=/opt/hwlab-ci-node-deps/node_modules
|
||||
- RUN set -eu; export HTTP_PROXY="${HTTP_PROXY:-${http_proxy:-}}"; export HTTPS_PROXY="${HTTPS_PROXY:-${https_proxy:-$HTTP_PROXY}}"; export ALL_PROXY="${ALL_PROXY:-${all_proxy:-}}"; export NO_PROXY="${NO_PROXY:-${no_proxy:-}}"; export http_proxy="$HTTP_PROXY"; export https_proxy="$HTTPS_PROXY"; export all_proxy="$ALL_PROXY"; export no_proxy="$NO_PROXY"; export npm_config_registry="https://registry.npmmirror.com/"; export BUN_CONFIG_REGISTRY="https://registry.npmmirror.com/"; export npm_config_noproxy="$NO_PROXY"; if [ -n "$HTTP_PROXY" ]; then export npm_config_proxy="$HTTP_PROXY"; fi; if [ -n "$HTTPS_PROXY" ]; then export npm_config_https_proxy="$HTTPS_PROXY"; fi; export npm_config_fetch_retries=2; export npm_config_fetch_retry_mintimeout=2000; export npm_config_fetch_retry_maxtimeout=16000; export npm_config_fetch_timeout=120000; proxy_label="${HTTP_PROXY:+HTTP_PROXY}"; proxy_label="${proxy_label:-none}"; mkdir -p /opt/hwlab-ci-node-deps; cd /opt/hwlab-ci-node-deps; printf '{"private":true,"dependencies":{}}\n' > package.json; ok=0; delay=2; for attempt in 1 2 3 4 5; do echo "{\"event\":\"tools-yaml-node-npm-install\",\"attempt\":\"$attempt/5\",\"registry\":\"$npm_config_registry\",\"proxy\":\"$proxy_label\"}" >&2; if timeout 180s npm install --package-lock=false --no-save --ignore-scripts --no-audit --no-fund --omit=dev yaml@2.8.3; then ok=1; break; fi; if [ "$attempt" = 5 ]; then break; fi; echo "{\"event\":\"tools-yaml-node-npm-install\",\"status\":\"retrying\",\"attempt\":\"$attempt/5\",\"sleepSeconds\":$delay}" >&2; sleep "$delay"; delay=$((delay * 2)); done; test "$ok" = 1; node --input-type=module -e 'import("/opt/hwlab-ci-node-deps/node_modules/yaml/browser/dist/index.js").then((yaml)=>console.log("yaml-ok", typeof yaml.parse))'
|
||||
- RUN node --version && npm --version && bun --version && git --version && python3 --version && docker --version && ssh -V && go version
|
||||
buildArgs: {}
|
||||
buildNetwork: host
|
||||
publicBaseImages:
|
||||
- docker.io/library/node:22-bookworm-slim
|
||||
- docker.io/library/golang:1.24-bookworm
|
||||
- docker.io/oven/bun:1.3.13
|
||||
- docker.io/buildpack-deps:bookworm-scm
|
||||
- docker.io/library/python:3.12-bookworm
|
||||
- docker.io/docker:29-cli
|
||||
buildOwner: JD01
|
||||
buildMode: node-local
|
||||
ciBuildBenchmarks:
|
||||
- profile: no-mirror-full
|
||||
runtimeLaneConfigRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01
|
||||
pipelineRunPrefix: hwlab-jd01-v03-ci-bench
|
||||
catalogPathTemplate: .unidesk/ci-build-benchmark/{profile}/{pipelineRun}/artifact-catalog.json
|
||||
imageTagMode: full
|
||||
pipelineTimeoutSeconds: 7200
|
||||
cachePolicy:
|
||||
noPipelineRunReuse: true
|
||||
forceFullBuild: true
|
||||
forbidGitopsCatalogReuse: true
|
||||
forbidDependencyCache: true
|
||||
forbidBuildkitCache: true
|
||||
forbidRegistryMirror: true
|
||||
forbidLocalPreheatedImages: true
|
||||
timings:
|
||||
requiredStages:
|
||||
- source-fetch
|
||||
- dependency-install
|
||||
- base-image-pull
|
||||
- service-image-build
|
||||
- registry-push
|
||||
- pipeline-total
|
||||
failureFamilies:
|
||||
- dns
|
||||
- proxy-connect
|
||||
- tls-timeout
|
||||
- rate-limit
|
||||
- auth
|
||||
- cache-hit-forbidden
|
||||
- image-policy
|
||||
- build-script
|
||||
- registry-push
|
||||
argo:
|
||||
namespace: argocd
|
||||
projectName: hwlab-jd01
|
||||
applicationName: hwlab-node-v03
|
||||
applicationFile: application-v03.yaml
|
||||
install:
|
||||
enabled: true
|
||||
sourceKind: url
|
||||
version: v3.4.2
|
||||
manifestUrl: https://raw.githubusercontent.com/argoproj/argo-cd/v3.4.2/manifests/install.yaml
|
||||
fieldManager: unidesk-hwlab-node-argocd
|
||||
imagePullPolicy: IfNotPresent
|
||||
preloadImages:
|
||||
- 127.0.0.1:5000/hwlab/argocd:v3.4.2
|
||||
- 127.0.0.1:5000/hwlab/dex:v2.45.0
|
||||
- 127.0.0.1:5000/hwlab/redis:8.2.3-alpine
|
||||
imageRewrites:
|
||||
- source: quay.io/argoproj/argocd:v3.4.2
|
||||
pullImage: quay.m.daocloud.io/argoproj/argocd:v3.4.2
|
||||
target: 127.0.0.1:5000/hwlab/argocd:v3.4.2
|
||||
- source: ghcr.io/dexidp/dex:v2.45.0
|
||||
pullImage: ghcr.m.daocloud.io/dexidp/dex:v2.45.0
|
||||
target: 127.0.0.1:5000/hwlab/dex:v2.45.0
|
||||
- source: public.ecr.aws/docker/library/redis:8.2.3-alpine
|
||||
pullImage: docker.m.daocloud.io/library/redis:8.2.3-alpine
|
||||
target: 127.0.0.1:5000/hwlab/redis:8.2.3-alpine
|
||||
requiredCrds:
|
||||
- applications.argoproj.io
|
||||
- appprojects.argoproj.io
|
||||
expectedDeployments:
|
||||
- argocd-applicationset-controller
|
||||
- argocd-dex-server
|
||||
- argocd-notifications-controller
|
||||
- argocd-redis
|
||||
- argocd-repo-server
|
||||
- argocd-server
|
||||
expectedStatefulSets:
|
||||
- argocd-application-controller
|
||||
readinessTimeoutSeconds: 600
|
||||
runtimeProxy:
|
||||
enabled: true
|
||||
mode: host-route
|
||||
configRef: nodes.JD01.egressProxy
|
||||
hostNetwork: false
|
||||
injectEnv: true
|
||||
deployments:
|
||||
- argocd-repo-server
|
||||
statefulSets:
|
||||
- argocd-application-controller
|
||||
- id: d518-v03
|
||||
node: D518
|
||||
lane: v03
|
||||
@@ -339,6 +677,8 @@ targets:
|
||||
cachePvcStorage: 20Gi
|
||||
cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-d518-v03-git-mirror-cache
|
||||
servicePort: 8080
|
||||
readContainerPort: 8080
|
||||
writeContainerPort: 8080
|
||||
deploymentReplicas: 1
|
||||
secretName: git-mirror-github-ssh
|
||||
syncConfigMapName: git-mirror-sync-script
|
||||
@@ -349,6 +689,8 @@ targets:
|
||||
egressProxy:
|
||||
mode: node-global
|
||||
required: true
|
||||
podHostNetwork: false
|
||||
injectPodEnv: false
|
||||
githubTransport:
|
||||
mode: ssh
|
||||
privateKeySecretKey: ssh-privatekey
|
||||
@@ -360,6 +702,29 @@ targets:
|
||||
knownHostsSourceKey: GITHUB_KNOWN_HOSTS_B64
|
||||
knownHostsSourceEncoding: base64
|
||||
tekton:
|
||||
install:
|
||||
enabled: true
|
||||
sourceKind: url
|
||||
version: pipeline-v1.12.0-triggers-v0.34.0
|
||||
fieldManager: unidesk-hwlab-node-tekton
|
||||
manifests:
|
||||
- name: pipeline
|
||||
url: https://infra.tekton.dev/tekton-releases/pipeline/previous/v1.12.0/release.yaml
|
||||
- name: triggers
|
||||
url: https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/release.yaml
|
||||
- name: triggers-interceptors
|
||||
url: https://infra.tekton.dev/tekton-releases/triggers/previous/v0.34.0/interceptors.yaml
|
||||
requiredCrds:
|
||||
- pipelines.tekton.dev
|
||||
- pipelineruns.tekton.dev
|
||||
- tasks.tekton.dev
|
||||
- taskruns.tekton.dev
|
||||
expectedDeploymentNamespaces:
|
||||
- tekton-pipelines
|
||||
- tekton-pipelines-resolvers
|
||||
readinessTimeoutSeconds: 900
|
||||
runtimeProxy:
|
||||
enabled: false
|
||||
pipelineName: hwlab-d518-v03-ci-image-publish
|
||||
serviceAccountName: hwlab-d518-v03-tekton-runner
|
||||
pipelineRunPrefix: hwlab-d518-v03-ci-poll
|
||||
@@ -492,3 +857,5 @@ targets:
|
||||
expectedStatefulSets:
|
||||
- argocd-application-controller
|
||||
readinessTimeoutSeconds: 600
|
||||
runtimeProxy:
|
||||
enabled: false
|
||||
|
||||
@@ -34,6 +34,13 @@ nodes:
|
||||
gitopsRoot: deploy/gitops/node
|
||||
networkProfile: d518-node-ci-egress
|
||||
downloadProfile: d518-node-default
|
||||
JD01:
|
||||
route: JD01
|
||||
kubeRoute: JD01:k3s
|
||||
sourceWorkspace: /root/workspace/hwlab-v03
|
||||
gitopsRoot: deploy/gitops/node
|
||||
networkProfile: jd01-node-ci-egress
|
||||
downloadProfile: jd01-node-default
|
||||
|
||||
lanes:
|
||||
v02:
|
||||
@@ -76,7 +83,7 @@ lanes:
|
||||
apiUrl: http://74.48.78.17:19667
|
||||
v03:
|
||||
node: G14
|
||||
activeTarget: D518
|
||||
activeTarget: JD01
|
||||
minor: 3
|
||||
version: v0.3
|
||||
sourceBranch: v0.3
|
||||
@@ -773,6 +780,198 @@ lanes:
|
||||
envKey: DATASTORE_URI
|
||||
authnKey: authn-preshared-key
|
||||
role: hwlab_d518_v03_app
|
||||
JD01:
|
||||
node: JD01
|
||||
workspace: /root/workspace/hwlab-v03
|
||||
sourceWorkspace:
|
||||
requiredCommands:
|
||||
- git
|
||||
- node
|
||||
- npm
|
||||
- npx
|
||||
requiredFiles:
|
||||
- AGENTS.md
|
||||
- package.json
|
||||
- package-lock.json
|
||||
- scripts/src/browser-launcher.mjs
|
||||
- scripts/web-live-dom-probe.mjs
|
||||
install:
|
||||
dependencyCommand: npm ci
|
||||
browserCommand: PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium
|
||||
timeoutSeconds: 900
|
||||
cicdRepo: /root/workspace/hwlab-v03-cicd.git
|
||||
cicdRepoLock: /tmp/hwlab-v03-cicd-repo.lock
|
||||
app: hwlab-node-v03
|
||||
pipeline: hwlab-jd01-v03-ci-image-publish
|
||||
pipelineRunPrefix: hwlab-jd01-v03-ci-poll
|
||||
serviceAccountName: hwlab-jd01-v03-tekton-runner
|
||||
controlPlaneFieldManager: unidesk-hwlab-jd01-v03-control-plane
|
||||
git:
|
||||
url: git@github.com:pikasTech/HWLAB.git
|
||||
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
|
||||
argo:
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
gitopsBranch: v0.3-gitops
|
||||
catalogPath: deploy/artifact-catalog.jd01-v03.json
|
||||
runtime:
|
||||
path: deploy/gitops/node/jd01/runtime-v03
|
||||
namespace: hwlab-v03
|
||||
renderDir: runtime-v03
|
||||
runtimeStore:
|
||||
postgres:
|
||||
mode: local-k3s
|
||||
secretName: hwlab-v03-postgres
|
||||
statefulSet: hwlab-v03-postgres
|
||||
serviceName: hwlab-v03-postgres
|
||||
adminUser: hwlab_v03
|
||||
adminPasswordSourceRef: hwlab/jd01-v03-postgres.env
|
||||
adminPasswordSourceKey: HWLAB_V03_POSTGRES_PASSWORD
|
||||
cloudApi:
|
||||
secretName: hwlab-cloud-api-v03-db
|
||||
secretKey: database-url
|
||||
database: hwlab_v03
|
||||
role: hwlab_v03
|
||||
openfga:
|
||||
secretName: hwlab-v03-openfga
|
||||
secretKey: datastore-uri
|
||||
authnKey: authn-preshared-key
|
||||
postgresPasswordKey: postgres-password
|
||||
database: hwlab_openfga
|
||||
role: hwlab_openfga
|
||||
poolMax: 16
|
||||
connectionTimeoutMs: 5000
|
||||
queryRetryMaxAttempts: 5
|
||||
queryRetryInitialDelayMs: 250
|
||||
queryRetryMaxDelayMs: 5000
|
||||
webProbe:
|
||||
browserProxyMode: direct
|
||||
defaultOrigin:
|
||||
mode: public
|
||||
baseUrl: https://hwlab.pikapython.com
|
||||
authLogin:
|
||||
maxAttempts: 6
|
||||
requestTimeoutMs: 30000
|
||||
initialDelayMs: 500
|
||||
maxDelayMs: 10000
|
||||
alertThresholds:
|
||||
sameOriginApiSlowMs: 10000
|
||||
partialApiSlowMs: 10000
|
||||
longLivedStreamOpenSlowMs: 10000
|
||||
visibleLoadingSlowMs: 10000
|
||||
turnTimingSampleSlackSeconds: 3
|
||||
turnElapsedSevereTimeoutSeconds: 120
|
||||
uncommandedStateChangeCommandWindowMs: 10000
|
||||
scrollJumpCommandWindowMs: 8000
|
||||
scrollJumpFromY: 250
|
||||
scrollJumpToY: 40
|
||||
sessionRailFallbackRatio: 0.5
|
||||
tektonDir: tekton-v03
|
||||
argoApplicationFile: application-v03.yaml
|
||||
registryPrefix: 127.0.0.1:5000/hwlab
|
||||
baseImage: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim
|
||||
baseImageSource: node:20-bookworm-slim
|
||||
serviceIds:
|
||||
- hwlab-cloud-api
|
||||
- hwlab-workbench-runtime
|
||||
- hwlab-user-billing
|
||||
- hwlab-project-management
|
||||
- hwlab-cloud-web
|
||||
- hwlab-gateway
|
||||
- hwlab-edge-proxy
|
||||
- hwlab-agent-skills
|
||||
buildkit:
|
||||
sidecarImage: 127.0.0.1:5000/hwlab/buildkit:rootless
|
||||
sourceImage: docker.io/moby/buildkit:rootless
|
||||
stepEnv:
|
||||
HOME: /tekton/home
|
||||
XDG_CONFIG_HOME: /tekton/home/.config
|
||||
observability:
|
||||
prometheusOperator: false
|
||||
webProbe:
|
||||
sentinels:
|
||||
- id: jd01-web-probe-sentinel
|
||||
enabled: true
|
||||
configRef: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.sentinel
|
||||
runtimeImageRewrites:
|
||||
- source: postgres:16-alpine
|
||||
target: 127.0.0.1:5000/hwlab/postgres:16-alpine
|
||||
- source: fatedier/frpc:v0.68.1
|
||||
target: 127.0.0.1:5000/hwlab/frpc:v0.68.1
|
||||
- source: openfga/openfga:v1.17.0
|
||||
target: 127.0.0.1:5000/hwlab/openfga:v1.17.0
|
||||
runtimeImageBuilds:
|
||||
- id: moonbridge
|
||||
kind: moonbridge
|
||||
target: 127.0.0.1:5000/hwlab/moonbridge:1b99888d3dae
|
||||
sourceRepo: https://github.com/ZhiYi-R/moon-bridge.git
|
||||
sourceRef: 1b99888d3dae889b79ee602cb875c7907f7e76f2
|
||||
builderImage: golang:1.25-bookworm
|
||||
goProxy: https://goproxy.cn,direct
|
||||
dockerNetworkMode: host
|
||||
public:
|
||||
webUrl: https://hwlab.pikapython.com
|
||||
apiUrl: https://hwlab.pikapython.com
|
||||
bootstrapAdmin:
|
||||
username: admin
|
||||
displayName: HWLAB v0.3 Admin
|
||||
usernameSourceRef: .env/HWLAB_admin.txt
|
||||
usernameSourceLine: 1
|
||||
passwordSourceRef: .env/HWLAB_admin.txt
|
||||
passwordSourceKey: HWLAB_ADMIN_PASSWORD
|
||||
passwordSourceLine: 2
|
||||
passwordHashTransform: hwlab-sha256
|
||||
secretName: hwlab-v03-bootstrap-admin
|
||||
secretKey: password-hash
|
||||
rollout:
|
||||
deployment: hwlab-cloud-api
|
||||
codeAgentProvider:
|
||||
secretName: hwlab-v03-code-agent-provider
|
||||
sourceRef: hwlab/jd01-v03-code-agent-provider.env
|
||||
openaiSourceKey: OPENAI_API_KEY
|
||||
opencodeSourceKey: OPENCODE_API_KEY
|
||||
codeAgentRuntime:
|
||||
enabled: true
|
||||
adapter: agentrun-v02
|
||||
managerUrl: http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080
|
||||
apiKeySecretName: hwlab-v03-master-server-admin-api-key
|
||||
apiKeySecretKey: api-key
|
||||
runnerNamespace: agentrun-v02
|
||||
secretNamespace: agentrun-v02
|
||||
repoUrlFrom: runtimeGitReadUrl
|
||||
providerIdFrom: runtimeNodeId
|
||||
defaultProviderProfile: codex
|
||||
codexStdioSupervisor: repo-owned
|
||||
publicExposure:
|
||||
mode: pk01-caddy-frp
|
||||
publicBaseUrl: https://hwlab.pikapython.com
|
||||
hostname: hwlab.pikapython.com
|
||||
expectedA: 82.156.23.220
|
||||
frpc:
|
||||
serverAddr: 82.156.23.220
|
||||
serverPort: 22000
|
||||
tokenSourceRef: platform-infra/pk01-frp.env
|
||||
tokenSourceKey: FRP_TOKEN
|
||||
secretName: hwlab-v03-frpc-secrets
|
||||
secretKey: frpc.toml
|
||||
tokenKey: token
|
||||
webProxy:
|
||||
name: hwlab-jd01-v03-cloud-web
|
||||
remotePort: 22090
|
||||
localIP: hwlab-cloud-web.hwlab-v03.svc.cluster.local
|
||||
localPort: 8080
|
||||
apiProxy:
|
||||
name: hwlab-jd01-v03-edge-proxy
|
||||
remotePort: 22089
|
||||
localIP: hwlab-edge-proxy.hwlab-v03.svc.cluster.local
|
||||
localPort: 6667
|
||||
caddy:
|
||||
route: PK01
|
||||
configPath: /etc/caddy/Caddyfile
|
||||
serviceName: caddy
|
||||
email: ops@pikapython.com
|
||||
tls: auto
|
||||
responseHeaderTimeoutSeconds: 600
|
||||
|
||||
networkProfiles:
|
||||
node-ci-egress:
|
||||
@@ -916,6 +1115,43 @@ networkProfiles:
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
jd01-node-ci-egress:
|
||||
proxy:
|
||||
http: http://127.0.0.1:10808
|
||||
https: http://127.0.0.1:10808
|
||||
all: http://127.0.0.1:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- kubernetes
|
||||
- kubernetes.default
|
||||
- kubernetes.default.svc
|
||||
- 10.0.0.0/8
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 82.156.23.220
|
||||
- 74.48.78.17
|
||||
dockerBuildProxy:
|
||||
http: http://127.0.0.1:10808
|
||||
https: http://127.0.0.1:10808
|
||||
all: http://127.0.0.1:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
|
||||
downloadProfiles:
|
||||
node-default:
|
||||
@@ -978,6 +1214,26 @@ downloadProfiles:
|
||||
retries: 3
|
||||
connectTimeoutSeconds: 10
|
||||
maxTimeSeconds: 120
|
||||
jd01-node-default:
|
||||
git:
|
||||
proxyMode: inherit
|
||||
retries: 3
|
||||
timeoutSeconds: 60
|
||||
npm:
|
||||
registry: https://registry.npmmirror.com/
|
||||
retries: 3
|
||||
fetchTimeoutSeconds: 120
|
||||
pip:
|
||||
indexUrl: https://pypi.org/simple
|
||||
retries: 3
|
||||
timeoutSeconds: 120
|
||||
docker:
|
||||
registryMirrors: []
|
||||
pullRetries: 3
|
||||
curl:
|
||||
retries: 3
|
||||
connectTimeoutSeconds: 10
|
||||
maxTimeSeconds: 120
|
||||
d601-node-no-mirror-benchmark:
|
||||
git:
|
||||
proxyMode: inherit
|
||||
|
||||
@@ -87,3 +87,79 @@ targets:
|
||||
targetKey: api-key
|
||||
scopes:
|
||||
- api
|
||||
|
||||
- id: jd01-v03
|
||||
node: JD01
|
||||
lane: v03
|
||||
namespace: hwlab-v03
|
||||
publicUrl: https://hwlab.pikapython.com
|
||||
userBilling:
|
||||
serviceId: hwlab-user-billing
|
||||
databaseUrlSource:
|
||||
sourceRef: hwlab/jd01-v03-postgres.env
|
||||
sourceKey: DATABASE_URL
|
||||
accounts:
|
||||
- logicalId: jd01-v03-admin
|
||||
kind: bootstrap-admin-api-key
|
||||
userId: usr_v03_admin
|
||||
username: admin
|
||||
displayName: HWLAB v0.3 Admin
|
||||
role: admin
|
||||
status: active
|
||||
permissions:
|
||||
- admin
|
||||
- system:hwlab
|
||||
- "tool:*"
|
||||
sourceRef: hwlab/jd01-v03-admin.env
|
||||
sourceKey: HWLAB_API_KEY
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
randomBase64Url:
|
||||
bytes: 32
|
||||
prefix: hwl_live_
|
||||
hostEnvTargets:
|
||||
- path: /root/.config/hwlab-v03/master-server-admin-api-key.env
|
||||
envKey: HWLAB_API_KEY
|
||||
mode: "0600"
|
||||
target:
|
||||
kind: kubernetes-secret
|
||||
namespace: hwlab-v03
|
||||
secretName: hwlab-v03-master-server-admin-api-key
|
||||
targetKey: api-key
|
||||
rolloutDeployment: hwlab-cloud-api
|
||||
- logicalId: jd01-v03-inner-test
|
||||
kind: user-billing-api-key
|
||||
userId: usr_jd01_v03_inner_test
|
||||
username: inner-test
|
||||
email: inner-test+jd01@hwlab.local
|
||||
displayName: HWLAB v0.3 JD01 Test User
|
||||
role: user
|
||||
status: active
|
||||
planId: default
|
||||
initialCredits: 100
|
||||
permissions:
|
||||
- code_agent
|
||||
- hwpod
|
||||
- aipod
|
||||
workbench:
|
||||
projectId: prj_hwpod_workbench
|
||||
lane: v03
|
||||
sourceRef: hwlab/jd01-v03-inner-test.env
|
||||
sourceKey: HWLAB_API_KEY
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
randomBase64Url:
|
||||
bytes: 32
|
||||
prefix: hwl_live_
|
||||
hostEnvTargets:
|
||||
- path: /root/.config/hwlab-v03/inner-test-api-key.env
|
||||
envKey: HWLAB_API_KEY
|
||||
mode: "0600"
|
||||
target:
|
||||
kind: user-billing-api-key
|
||||
serviceId: hwlab-user-billing
|
||||
keyId: key_jd01_v03_inner_test
|
||||
keyName: JD01 v0.3 inner test fixed key
|
||||
targetKey: api-key
|
||||
scopes:
|
||||
- api
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
version: 1
|
||||
kind: HwlabWebProbeSentinelProfiles
|
||||
metadata:
|
||||
id: hwlab-web-probe-sentinel-profiles
|
||||
owner: UniDesk
|
||||
specRef: PJ2026-01060508
|
||||
composition:
|
||||
mode: yaml-anchors-and-merge
|
||||
intent: node overlays inherit common web-probe sentinel baselines and render node/lane identity from variables.
|
||||
|
||||
baselines:
|
||||
sentinel: &sentinel-base
|
||||
enabled: true
|
||||
mode: web-probe-observe-wrapper
|
||||
|
||||
runtime:
|
||||
common: &runtime-common
|
||||
namespace: hwlab-${lane}
|
||||
listenHost: 0.0.0.0
|
||||
servicePort: 8080
|
||||
pvcStorage: 10Gi
|
||||
replicas: 1
|
||||
healthPath: /api/health
|
||||
metricsPath: /metrics
|
||||
scheduler: &scheduler-10m
|
||||
intervalMs: 600000
|
||||
heartbeatStaleSeconds: 900
|
||||
maxConcurrentRuns: 1
|
||||
scheduler15m: &scheduler-15m
|
||||
intervalMs: 900000
|
||||
heartbeatStaleSeconds: 900
|
||||
maxConcurrentRuns: 1
|
||||
sqlite: &sqlite-common
|
||||
busyTimeoutMs: 2000
|
||||
|
||||
cicd:
|
||||
source: &cicd-source
|
||||
repository: pikasTech/unidesk
|
||||
branch: master
|
||||
gitSshUrl: ssh://git@ssh.github.com:443/pikasTech/unidesk.git
|
||||
gitMirrorReadUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
|
||||
buildContext: .
|
||||
entrypoint: scripts/web-probe-sentinel-service.ts
|
||||
checkoutPaths:
|
||||
- scripts
|
||||
- config
|
||||
- config.json
|
||||
- src
|
||||
- package.json
|
||||
- bun.lock
|
||||
- bun.lockb
|
||||
builder: &cicd-builder
|
||||
namespace: devops-infra
|
||||
sourceMode: sparse-git-checkout
|
||||
gitSshSecretName: git-mirror-github-ssh
|
||||
dockerSocketPath: /var/run/docker.sock
|
||||
activeDeadlineSeconds: 900
|
||||
ttlSecondsAfterFinished: 3600
|
||||
monitorWeb: &monitor-web
|
||||
frontendStack: vue3-vendored-browser-build
|
||||
runtimeMode: runner-served-bridge
|
||||
assetRoot: scripts/assets/web-probe-sentinel-monitor-web
|
||||
envReuse:
|
||||
mode: docker-layer-and-ci-node-deps
|
||||
nodeDepsPath: /opt/hwlab-ci-node-deps/node_modules
|
||||
gitMirror:
|
||||
source: source.gitMirrorReadUrl
|
||||
preSync: required
|
||||
postFlush: required
|
||||
ciBudget:
|
||||
maxSeconds: 120
|
||||
maintenance: &maintenance
|
||||
startCommand: sentinel maintenance start
|
||||
stopCommand: sentinel maintenance stop
|
||||
confirmWait: &confirm-wait
|
||||
maxSeconds: 120
|
||||
|
||||
publicExposure:
|
||||
common: &public-exposure-common
|
||||
enabled: true
|
||||
mode: pk01-caddy-frp-path
|
||||
hostname: monitor.pikapython.com
|
||||
expectedA: 82.156.23.220
|
||||
frpc: &frpc-common
|
||||
image: 127.0.0.1:5000/hwlab/frpc:v0.68.1
|
||||
serverAddr: 82.156.23.220
|
||||
serverPort: 22000
|
||||
tokenSourceRef: platform-infra/pk01-frp.env
|
||||
tokenSourceKey: FRP_TOKEN
|
||||
secretKey: frpc.toml
|
||||
tokenKey: token
|
||||
caddy: &caddy-common
|
||||
route: PK01
|
||||
configPath: /etc/caddy/Caddyfile
|
||||
serviceName: caddy
|
||||
email: ops@pikapython.com
|
||||
tls: auto
|
||||
responseHeaderTimeoutSeconds: 600
|
||||
|
||||
secrets:
|
||||
jd01BootstrapSource: &jd01-bootstrap-source
|
||||
purpose: bootstrap-admin
|
||||
sourceRef: .env/HWLAB_admin.txt
|
||||
sourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
sourceLine: 2
|
||||
dsflashPromptSource: &dsflash-prompt-source
|
||||
purpose: prompt-set
|
||||
sourceRef: hwlab/web-probe-sentinel-dsflash-go.env
|
||||
sourceKey: DSFLASH_GO_TOOL_CALL_10X_PROMPTS_JSON
|
||||
frpTokenSource: &frp-token-source
|
||||
purpose: frp-token
|
||||
sourceRef: platform-infra/pk01-frp.env
|
||||
sourceKey: FRP_TOKEN
|
||||
|
||||
nodes:
|
||||
JD01:
|
||||
target: &jd01-target
|
||||
node: ${NODE}
|
||||
lane: ${LANE}
|
||||
publicOriginRef: config/hwlab-node-lanes.yaml#lanes.${LANE}.targets.${NODE}.public.webUrl
|
||||
|
||||
cicdCommon: &jd01-cicd-common
|
||||
controlPlaneConfigRef: config/hwlab-node-control-plane.yaml#targets[1]
|
||||
source:
|
||||
<<: *cicd-source
|
||||
argo: &jd01-argo
|
||||
namespace: argocd
|
||||
projectName: hwlab-jd01
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
targetRevision: v0.3-gitops
|
||||
maintenance:
|
||||
<<: *maintenance
|
||||
monitorWeb:
|
||||
<<: *monitor-web
|
||||
confirmWait:
|
||||
<<: *confirm-wait
|
||||
|
||||
sentinels:
|
||||
jd01-web-probe-sentinel:
|
||||
sentinel:
|
||||
<<: *sentinel-base
|
||||
id: jd01-web-probe-sentinel
|
||||
configRefs:
|
||||
runtime: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.runtime
|
||||
scenarios: config/hwlab-web-probe-sentinel/scenarios.multi-sentinel.yaml#sentinel.scenarios
|
||||
promptSet: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet
|
||||
reportViews: config/hwlab-web-probe-sentinel/report-views.multi-sentinel.yaml#sentinel.reportViews
|
||||
publicExposure: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.publicExposure
|
||||
cicd: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.cicd
|
||||
secrets: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.secrets
|
||||
runtime:
|
||||
<<: *runtime-common
|
||||
target:
|
||||
<<: *jd01-target
|
||||
observeWrapperRef: config/hwlab-node-lanes.yaml#lanes.${LANE}.targets.${NODE}.observability.webProbe.sentinels[0]
|
||||
serviceAccountName: hwlab-web-probe-sentinel-${nodeLower}
|
||||
deploymentName: hwlab-web-probe-sentinel-${nodeLower}
|
||||
serviceName: hwlab-web-probe-sentinel-${nodeLower}
|
||||
pvcName: hwlab-web-probe-sentinel-${nodeLower}-state
|
||||
stateRoot: /var/lib/web-probe-sentinel-${nodeLower}
|
||||
imageRef: 127.0.0.1:5000/hwlab/web-probe-sentinel-${nodeLower}:source-commit
|
||||
scheduler:
|
||||
<<: *scheduler-10m
|
||||
sqlite:
|
||||
<<: *sqlite-common
|
||||
path: /var/lib/web-probe-sentinel-${nodeLower}/index.sqlite
|
||||
publicExposure:
|
||||
<<: *public-exposure-common
|
||||
publicBaseUrl: https://monitor.pikapython.com/sentinels/${nodeLower}-web-probe-sentinel
|
||||
routePrefix: /sentinels/${nodeLower}-web-probe-sentinel
|
||||
frpc:
|
||||
<<: *frpc-common
|
||||
deploymentName: hwlab-web-probe-sentinel-${nodeLower}-frpc
|
||||
secretName: hwlab-web-probe-sentinel-${nodeLower}-frpc
|
||||
httpProxy:
|
||||
name: hwlab-${nodeLower}-${lane}-web-probe-sentinel
|
||||
remotePort: 22094
|
||||
localIP: hwlab-web-probe-sentinel-${nodeLower}.hwlab-${lane}.svc.cluster.local
|
||||
localPort: 8080
|
||||
caddy:
|
||||
<<: *caddy-common
|
||||
managedBlockOwner: hwlab-web-probe-sentinel-${nodeLower}-${lane}
|
||||
cicd:
|
||||
<<: *jd01-cicd-common
|
||||
builder:
|
||||
<<: *cicd-builder
|
||||
jobPrefix: web-probe-sentinel-${nodeLower}-publish
|
||||
gitopsPath: deploy/gitops/node/${nodeLower}/web-probe-sentinel
|
||||
argo:
|
||||
<<: *jd01-argo
|
||||
applicationName: hwlab-web-probe-sentinel-${nodeLower}
|
||||
image:
|
||||
repository: 127.0.0.1:5000/hwlab/web-probe-sentinel-${nodeLower}
|
||||
tagSource: source-commit
|
||||
baseImageRef: config/hwlab-node-control-plane.yaml#targets[1].tekton.toolsImage.output
|
||||
envRecipeRef: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.runtime
|
||||
targetValidation:
|
||||
scenarioId: workbench-dsflash-go-tool-call-10x
|
||||
maxSeconds: 300
|
||||
serviceUnavailablePolicy: structured-failure
|
||||
secrets:
|
||||
sources:
|
||||
- <<: *jd01-bootstrap-source
|
||||
- <<: *dsflash-prompt-source
|
||||
- purpose: account-a
|
||||
sourceRef: .env/HWLAB_admin.txt
|
||||
sourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
sourceLine: 2
|
||||
format: web-account-json
|
||||
usernameSourceRef: .env/HWLAB_admin.txt
|
||||
usernameSourceLine: 1
|
||||
- purpose: account-b
|
||||
sourceRef: hwlab/${nodeLower}-${lane}-preset-users.env
|
||||
sourceKey: ${NODE}_SECOND_USER_PASSWORD
|
||||
format: web-account-json
|
||||
username: ${nodeLower}-sentinel@hwlab.local
|
||||
- <<: *frp-token-source
|
||||
runtimeSecrets:
|
||||
- name: hwlab-web-probe-sentinel-${nodeLower}-bootstrap
|
||||
namespace: hwlab-${lane}
|
||||
data:
|
||||
- sourcePurpose: bootstrap-admin
|
||||
targetKey: bootstrap-admin-password
|
||||
- name: hwlab-web-probe-sentinel-${nodeLower}-prompt-set
|
||||
namespace: hwlab-${lane}
|
||||
data:
|
||||
- sourcePurpose: prompt-set
|
||||
targetKey: prompts.json
|
||||
- name: hwlab-web-probe-sentinel-${nodeLower}-accounts
|
||||
namespace: hwlab-${lane}
|
||||
data:
|
||||
- sourcePurpose: account-a
|
||||
targetKey: account-a.json
|
||||
- sourcePurpose: account-b
|
||||
targetKey: account-b.json
|
||||
- name: hwlab-web-probe-sentinel-${nodeLower}-frpc
|
||||
namespace: hwlab-${lane}
|
||||
data:
|
||||
- sourcePurpose: frp-token
|
||||
targetKey: token
|
||||
@@ -0,0 +1,24 @@
|
||||
version: 1
|
||||
kind: HwlabWebProbeSentinelReportViews
|
||||
metadata:
|
||||
id: web-probe-sentinel-multi-scenario-report-views
|
||||
owner: UniDesk
|
||||
specRef: PJ2026-01060508
|
||||
sentinel:
|
||||
reportViews:
|
||||
defaultView: summary
|
||||
views:
|
||||
- summary
|
||||
- turn-summary
|
||||
- auth-session-switch-summary
|
||||
- findings
|
||||
- trace-frame
|
||||
pageSize: 20
|
||||
maxPageSize: 100
|
||||
rawAccess: explicit-only
|
||||
checkCatalogRef: config/hwlab-web-probe-sentinel/check-catalog.yaml#sentinel.checkCatalog
|
||||
redaction:
|
||||
prompt: hash-and-byte-count
|
||||
assistantFinal: summary-and-hash
|
||||
providerPayload: denied
|
||||
secrets: denied
|
||||
@@ -0,0 +1,126 @@
|
||||
version: 1
|
||||
kind: HwlabWebProbeSentinelScenarios
|
||||
metadata:
|
||||
id: web-probe-sentinel-multi-scenario-suite
|
||||
owner: UniDesk
|
||||
specRef: PJ2026-01060508
|
||||
sentinel:
|
||||
scenarios:
|
||||
- id: workbench-dsflash-go-tool-call-10x
|
||||
enabled: true
|
||||
cadence: 10m
|
||||
observeTargetPath: /workbench
|
||||
sampleIntervalMs: 1000
|
||||
screenshotIntervalMs: 60000
|
||||
maxRunSeconds: 1200
|
||||
providerProfile: dsflash-go
|
||||
providerProfileMode: exact
|
||||
promptSetRef: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet
|
||||
reportViewRef: config/hwlab-web-probe-sentinel/report-views.multi-sentinel.yaml#sentinel.reportViews
|
||||
commandSequence:
|
||||
- type: newSession
|
||||
- type: selectProvider
|
||||
provider: dsflash-go
|
||||
- type: sendPrompt
|
||||
promptSource: promptSet
|
||||
repeat: 10
|
||||
sessionInvarianceChecks:
|
||||
- id: after-round-1-navigation-invariance
|
||||
afterRound: 1
|
||||
refreshCurrent: true
|
||||
switchAwayAndBack: true
|
||||
alternateSessionStrategy: existing-or-create
|
||||
assertSessionInvariant: true
|
||||
expectedSentinelRange: sentinel-01..sentinel-01
|
||||
findingId: workbench-message-order-user-clustered-after-navigation
|
||||
severity: amber
|
||||
blocking: false
|
||||
- id: after-round-5-navigation-invariance
|
||||
afterRound: 5
|
||||
refreshCurrent: true
|
||||
switchAwayAndBack: true
|
||||
alternateSessionStrategy: existing-or-create
|
||||
assertSessionInvariant: true
|
||||
requireComposerReady: true
|
||||
expectedSentinelRange: sentinel-01..sentinel-05
|
||||
findingId: workbench-message-order-user-clustered-after-navigation
|
||||
severity: amber
|
||||
blocking: false
|
||||
- id: after-round-10-refresh-invariance
|
||||
afterRound: 10
|
||||
refreshCurrent: true
|
||||
switchAwayAndBack: false
|
||||
assertSessionInvariant: true
|
||||
expectedSentinelRange: sentinel-01..sentinel-10
|
||||
findingId: workbench-message-order-user-clustered-after-navigation
|
||||
severity: amber
|
||||
blocking: false
|
||||
|
||||
- id: workbench-auth-session-switch-2users
|
||||
enabled: true
|
||||
cadence: 10m
|
||||
observeTargetPath: /workbench
|
||||
sampleIntervalMs: 1000
|
||||
screenshotIntervalMs: 60000
|
||||
maxRunSeconds: 900
|
||||
providerProfile: session-switch-sentinel
|
||||
providerProfileMode: exact
|
||||
promptSetRef: config/hwlab-web-probe-sentinel/prompt-set.auth-session-switch.yaml#sentinel.promptSet
|
||||
reportViewRef: config/hwlab-web-probe-sentinel/report-views.multi-sentinel.yaml#sentinel.reportViews
|
||||
accounts:
|
||||
- id: account-a
|
||||
sourcePurpose: account-a
|
||||
usernameKey: username
|
||||
passwordKey: password
|
||||
- id: account-b
|
||||
sourcePurpose: account-b
|
||||
usernameKey: username
|
||||
passwordKey: password
|
||||
commandSequence:
|
||||
- type: loginAccount
|
||||
accountId: account-a
|
||||
- type: listSessions
|
||||
- type: logout
|
||||
- type: loginAccount
|
||||
accountId: account-b
|
||||
- type: listSessions
|
||||
- type: switchSessions
|
||||
fromAccountId: account-b
|
||||
toAccountId: account-a
|
||||
- type: listSessions
|
||||
- type: logout
|
||||
|
||||
- id: mdtodo-visual-regression
|
||||
enabled: true
|
||||
cadence: 15m
|
||||
observeTargetPath: /projects/mdtodo
|
||||
viewport: 1440x900
|
||||
sampleIntervalMs: 1000
|
||||
screenshotIntervalMs: 60000
|
||||
maxRunSeconds: 360
|
||||
providerProfile: dsflash-go
|
||||
providerProfileMode: exact
|
||||
promptSetRef: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet
|
||||
reportViewRef: config/hwlab-web-probe-sentinel/report-views.multi-sentinel.yaml#sentinel.reportViews
|
||||
commandSequence:
|
||||
- type: goto
|
||||
path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_0db4dc4e46adf188/tasks/R1
|
||||
- type: screenshot
|
||||
label: mdtodo-desktop-few-task-gap
|
||||
waitProjectManagementReady: true
|
||||
- type: goto
|
||||
path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_5f9645ffe8774b92/tasks/R14
|
||||
- type: screenshot
|
||||
label: mdtodo-r14-selected
|
||||
waitProjectManagementReady: true
|
||||
- type: openMdtodoReportPreview
|
||||
task: R14
|
||||
link: R14
|
||||
- type: screenshot
|
||||
label: mdtodo-r14-report-preview
|
||||
waitProjectManagementReady: true
|
||||
- type: toggleMdtodoReportFullscreen
|
||||
text: toggle
|
||||
- type: screenshot
|
||||
label: mdtodo-r14-report-fullscreen
|
||||
waitProjectManagementReady: true
|
||||
@@ -0,0 +1,112 @@
|
||||
version: 1
|
||||
kind: platform-infra-host-proxy
|
||||
metadata:
|
||||
owner: unidesk
|
||||
relatedIssues:
|
||||
- 1148
|
||||
- 1110
|
||||
- 1117
|
||||
|
||||
defaults:
|
||||
targetId: JD01
|
||||
|
||||
server:
|
||||
id: benchmark-validated-master-shadowsocks-server
|
||||
enabled: true
|
||||
benchmarkRef: pikasTech/unidesk#1110
|
||||
implementationRef: pikasTech/unidesk#1117
|
||||
sourceConfigRef: config/platform-infra/egress-proxy-sources.yaml#sources.master-shadowsocks
|
||||
composeFile: config/platform-infra/sub2api-master-egress-proxy.compose.yaml
|
||||
serviceName: sub2api-master-egress-proxy
|
||||
containerName: unidesk-sub2api-master-egress-proxy
|
||||
image: ghcr.io/shadowsocks/ssserver-rust:latest
|
||||
configPath: /root/unidesk/.state/secrets/platform-infra/sub2api-master-egress-proxy.config.json
|
||||
listenHost: 0.0.0.0
|
||||
listenPort: 18792
|
||||
health:
|
||||
mode: tcp
|
||||
host: 127.0.0.1
|
||||
port: 18792
|
||||
|
||||
sources:
|
||||
jd01-real-deps-master-shadowsocks:
|
||||
sourceType: benchmark-validated-master-shadowsocks
|
||||
serverRef: server.benchmark-validated-master-shadowsocks-server
|
||||
benchmarkRef: pikasTech/unidesk#1110
|
||||
implementationRef: pikasTech/unidesk#1117
|
||||
sourceConfigRef: config/platform-infra/egress-proxy-sources.yaml#sources.master-shadowsocks
|
||||
client:
|
||||
mode: trans-static-binary
|
||||
upstreamUrl: https://github.com/SagerNet/sing-box/releases/download/v1.13.14/sing-box-1.13.14-linux-amd64.tar.gz
|
||||
version: v1.13.14
|
||||
archiveCachePath: .state/artifacts/platform-infra/sing-box-1.13.14-linux-amd64.tar.gz
|
||||
archiveSha256: f48703461a15476951ac4967cdad339d986f4b8096b4eb3ff0829a500502d697
|
||||
archiveInstallPath: /var/cache/unidesk/host-egress-proxy/sing-box-1.13.14-linux-amd64.tar.gz
|
||||
binaryMember: sing-box-1.13.14-linux-amd64/sing-box
|
||||
binaryCachePath: .state/artifacts/platform-infra/sing-box-1.13.14-linux-amd64
|
||||
binarySha256: 68aeab83cc4ab2659a5b92232261a20746ccdafc3b3d1e19b2d63247eec3bbf7
|
||||
installPath: /usr/local/bin/sing-box
|
||||
configPath: /etc/unidesk/host-egress-proxy/sing-box.json
|
||||
unitPath: /etc/systemd/system/unidesk-host-egress-proxy.service
|
||||
serviceName: unidesk-host-egress-proxy
|
||||
listenHost: 127.0.0.1
|
||||
listenPort: 10808
|
||||
podAccess:
|
||||
enabled: true
|
||||
listenHost: 10.42.0.1
|
||||
listenPort: 10808
|
||||
proxyUrl: http://10.42.0.1:10808
|
||||
clashApiListen: 127.0.0.1:19090
|
||||
healthUrl: http://127.0.0.1:19090/connections
|
||||
proxyUrl: http://127.0.0.1:10808
|
||||
externalProbeUrl: https://www.gstatic.com/generate_204
|
||||
|
||||
targets:
|
||||
JD01:
|
||||
route: JD01
|
||||
enabled: true
|
||||
sourceRef: sources.jd01-real-deps-master-shadowsocks
|
||||
env:
|
||||
httpProxy: http://127.0.0.1:10808
|
||||
httpsProxy: http://127.0.0.1:10808
|
||||
allProxy: http://127.0.0.1:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- host.docker.internal
|
||||
- 74.48.78.17
|
||||
- 82.156.23.220
|
||||
- 10.0.0.0/8
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- kubernetes
|
||||
- kubernetes.default
|
||||
- kubernetes.default.svc
|
||||
- argocd-repo-server
|
||||
- argocd-repo-server.argocd
|
||||
- argocd-redis
|
||||
- argocd-redis.argocd
|
||||
- git-mirror-http
|
||||
- git-mirror-http.devops-infra
|
||||
- git-mirror-write
|
||||
- git-mirror-write.devops-infra
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
files:
|
||||
envFile: /etc/unidesk/proxy.env
|
||||
profile: /etc/profile.d/unidesk-proxy.sh
|
||||
apt: /etc/apt/apt.conf.d/90unidesk-proxy
|
||||
dockerSystemdDropIn: /etc/systemd/system/docker.service.d/10-unidesk-proxy.conf
|
||||
k3sSystemdDropIn: /etc/systemd/system/k3s.service.d/10-unidesk-proxy.conf
|
||||
apply:
|
||||
reloadSystemd: true
|
||||
restartDocker: true
|
||||
restartK3s: false
|
||||
@@ -35,6 +35,12 @@ targets:
|
||||
role: active
|
||||
enabled: true
|
||||
createNamespace: true
|
||||
- id: JD01
|
||||
route: JD01:k3s
|
||||
namespace: platform-infra
|
||||
role: active
|
||||
enabled: true
|
||||
createNamespace: true
|
||||
|
||||
collector:
|
||||
deploymentName: otel-collector
|
||||
@@ -97,6 +103,20 @@ instrumentation:
|
||||
- projection_write
|
||||
- trace_events_read
|
||||
- turn_status_read
|
||||
- serviceName: hwlab-cloud-api
|
||||
owningRepo: pikasTech/HWLAB
|
||||
configRefs:
|
||||
targetNode: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.node
|
||||
lane: config/hwlab-node-lanes.yaml#lanes.v03.version
|
||||
namespace: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.runtime.namespace
|
||||
requiredSpans:
|
||||
- POST /v1/agent/chat
|
||||
- durable_admission
|
||||
- billing_preflight
|
||||
- agentrun_dispatch
|
||||
- projection_write
|
||||
- trace_events_read
|
||||
- turn_status_read
|
||||
- serviceName: user-billing
|
||||
owningRepo: pikasTech/HWLAB
|
||||
configRefs:
|
||||
@@ -113,6 +133,14 @@ instrumentation:
|
||||
namespace: config/hwlab-node-lanes.yaml#lanes.v03.targets.D518.runtime.namespace
|
||||
requiredSpans:
|
||||
- billing_preflight
|
||||
- serviceName: user-billing
|
||||
owningRepo: pikasTech/HWLAB
|
||||
configRefs:
|
||||
targetNode: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.node
|
||||
lane: config/hwlab-node-lanes.yaml#lanes.v03.version
|
||||
namespace: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.runtime.namespace
|
||||
requiredSpans:
|
||||
- billing_preflight
|
||||
- serviceName: agentrun-manager
|
||||
owningRepo: pikasTech/agentrun
|
||||
configRefs:
|
||||
@@ -124,6 +152,17 @@ instrumentation:
|
||||
- run_created
|
||||
- command_result
|
||||
- projection_sync
|
||||
- serviceName: agentrun-manager
|
||||
owningRepo: pikasTech/agentrun
|
||||
configRefs:
|
||||
targetNode: config/agentrun.yaml#controlPlane.lanes.jd01-v02.node
|
||||
lane: config/agentrun.yaml#controlPlane.lanes.jd01-v02.version
|
||||
namespace: config/agentrun.yaml#controlPlane.lanes.jd01-v02.runtime.namespace
|
||||
requiredSpans:
|
||||
- agentrun_dispatch
|
||||
- run_created
|
||||
- command_result
|
||||
- projection_sync
|
||||
|
||||
resourceAttributes:
|
||||
required:
|
||||
|
||||
@@ -233,6 +233,72 @@ targets:
|
||||
- 74.48.78.17
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
- id: JD01
|
||||
route: JD01:k3s
|
||||
namespace: platform-infra
|
||||
role: active
|
||||
enabled: true
|
||||
databaseMode: bundled
|
||||
redisMode: bundled-persistent
|
||||
appReplicas: 1
|
||||
redisReplicas: 1
|
||||
image:
|
||||
repository: weishaw/sub2api
|
||||
tag: 0.1.138
|
||||
pullPolicy: IfNotPresent
|
||||
dependencyImages:
|
||||
postgres: docker.m.daocloud.io/library/postgres:18-alpine
|
||||
redis: docker.m.daocloud.io/library/redis:8-alpine
|
||||
codexPool:
|
||||
sentinelImageBuild:
|
||||
baseImageCachePolicy: pull
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- host.docker.internal
|
||||
- 74.48.78.17
|
||||
- 192.168.0.0/16
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- kubernetes
|
||||
- kubernetes.default
|
||||
- kubernetes.default.svc
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
egressProxy:
|
||||
enabled: false
|
||||
deploymentName: sub2api-egress-proxy
|
||||
serviceName: sub2api-egress-proxy
|
||||
secretName: sub2api-egress-proxy-config
|
||||
secretKey: config.json
|
||||
image: ghcr.io/sagernet/sing-box:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
listenPort: 10808
|
||||
hostNetwork: true
|
||||
hostProxyConfigRef: config/platform-infra/host-proxy.yaml#targets.JD01
|
||||
proxyEnvPath: /etc/unidesk/proxy.env
|
||||
sourceConfigRef: config/platform-infra/egress-proxy-sources.yaml#sources.master-shadowsocks
|
||||
applyToSub2Api: false
|
||||
applyToSentinel: false
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- .svc
|
||||
- .cluster.local
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 82.156.23.220
|
||||
- 74.48.78.17
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
runtime:
|
||||
database:
|
||||
mode: external
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface AgentRunLaneSpec {
|
||||
readonly nodeKubeRoute: string;
|
||||
readonly version: string;
|
||||
readonly source: {
|
||||
readonly statusMode: "host-worktree" | "k3s-git-mirror";
|
||||
readonly repository: string;
|
||||
readonly branch: string;
|
||||
readonly bootstrapFromBranch: string | null;
|
||||
@@ -76,6 +77,7 @@ export interface AgentRunLaneSpec {
|
||||
readonly serviceAccountName: string;
|
||||
readonly registryPrefix: string;
|
||||
readonly toolsImage: string;
|
||||
readonly buildkitImage: string | null;
|
||||
};
|
||||
readonly gitops: {
|
||||
readonly branch: string;
|
||||
@@ -123,6 +125,10 @@ export interface AgentRunLaneSpec {
|
||||
readonly image: string | null;
|
||||
readonly storage: string | null;
|
||||
readonly port: number | null;
|
||||
readonly database: string | null;
|
||||
readonly user: string | null;
|
||||
readonly passwordSourceRef: string | null;
|
||||
readonly passwordSourceKey: string | null;
|
||||
};
|
||||
};
|
||||
readonly gitMirror: {
|
||||
@@ -271,6 +277,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
|
||||
source: {
|
||||
repository: spec.source.repository,
|
||||
branch: spec.source.branch,
|
||||
statusMode: spec.source.statusMode,
|
||||
bootstrapFromBranch: spec.source.bootstrapFromBranch,
|
||||
bootstrapTimeoutSeconds: spec.source.bootstrapTimeoutSeconds,
|
||||
bootstrapPollSeconds: spec.source.bootstrapPollSeconds,
|
||||
@@ -289,6 +296,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
|
||||
pipelineRunPrefix: spec.ci.pipelineRunPrefix,
|
||||
serviceAccountName: spec.ci.serviceAccountName,
|
||||
registryPrefix: spec.ci.registryPrefix,
|
||||
buildkitImage: spec.ci.buildkitImage,
|
||||
},
|
||||
gitops: {
|
||||
branch: spec.gitops.branch,
|
||||
@@ -491,6 +499,7 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
|
||||
nodeKubeRoute: node.kubeRoute,
|
||||
version: stringField(input, "version", path),
|
||||
source: {
|
||||
statusMode: sourceStatusModeField(optionalStringField(source, "statusMode", `${path}.source`) ?? "host-worktree", `${path}.source.statusMode`),
|
||||
repository: stringField(source, "repository", `${path}.source`),
|
||||
branch: stringField(source, "branch", `${path}.source`),
|
||||
bootstrapFromBranch: optionalStringField(source, "bootstrapFromBranch", `${path}.source`) ?? null,
|
||||
@@ -513,6 +522,7 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
|
||||
serviceAccountName: stringField(ci, "serviceAccountName", `${path}.ci`),
|
||||
registryPrefix: stringField(ci, "registryPrefix", `${path}.ci`),
|
||||
toolsImage: stringField(ci, "toolsImage", `${path}.ci`),
|
||||
buildkitImage: optionalStringField(ci, "buildkitImage", `${path}.ci`) ?? null,
|
||||
},
|
||||
gitops: {
|
||||
branch: stringField(gitops, "branch", `${path}.gitops`),
|
||||
@@ -544,6 +554,11 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
|
||||
};
|
||||
}
|
||||
|
||||
function sourceStatusModeField(value: string, path: string): "host-worktree" | "k3s-git-mirror" {
|
||||
if (value !== "host-worktree" && value !== "k3s-git-mirror") throw new Error(`${path} must be host-worktree or k3s-git-mirror`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseDeployment(input: Record<string, unknown>, path: string): AgentRunLaneSpec["deployment"] {
|
||||
const argocd = recordField(input, "argocd", path);
|
||||
const manager = recordField(input, "manager", path);
|
||||
@@ -660,6 +675,10 @@ function parseLocalPostgres(input: Record<string, unknown>, path: string): Agent
|
||||
image: optionalStringField(input, "image", path) ?? null,
|
||||
storage: optionalStringField(input, "storage", path) ?? null,
|
||||
port: optionalIntegerField(input, "port", path) ?? null,
|
||||
database: optionalStringField(input, "database", path) ?? null,
|
||||
user: optionalStringField(input, "user", path) ?? null,
|
||||
passwordSourceRef: optionalStringField(input, "passwordSourceRef", path) ?? null,
|
||||
passwordSourceKey: optionalStringField(input, "passwordSourceKey", path) ?? null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -668,6 +687,10 @@ function parseLocalPostgres(input: Record<string, unknown>, path: string): Agent
|
||||
image: stringField(input, "image", path),
|
||||
storage: stringField(input, "storage", path),
|
||||
port: integerField(input, "port", path),
|
||||
database: stringField(input, "database", path),
|
||||
user: stringField(input, "user", path),
|
||||
passwordSourceRef: secretSourceRefField(input, "passwordSourceRef", path),
|
||||
passwordSourceKey: stringField(input, "passwordSourceKey", path),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -300,10 +300,11 @@ function agentRunRuntimeNamespaceManifest(spec: AgentRunLaneSpec): Record<string
|
||||
|
||||
function agentRunPostgresManifest(spec: AgentRunLaneSpec): Record<string, unknown> {
|
||||
const localPostgres = spec.deployment.localPostgres;
|
||||
if (!localPostgres.enabled || localPostgres.serviceName === null || localPostgres.image === null || localPostgres.storage === null || localPostgres.port === null) {
|
||||
if (!localPostgres.enabled || localPostgres.serviceName === null || localPostgres.image === null || localPostgres.storage === null || localPostgres.port === null || localPostgres.database === null || localPostgres.user === null) {
|
||||
throw new Error(`localPostgres is enabled for ${spec.version} without renderable YAML fields`);
|
||||
}
|
||||
const name = localPostgres.serviceName;
|
||||
const secretName = spec.database.secretRef.name;
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
@@ -330,6 +331,13 @@ function agentRunPostgresManifest(spec: AgentRunLaneSpec): Record<string, unknow
|
||||
name: "postgres",
|
||||
image: localPostgres.image,
|
||||
ports: [{ name: "postgres", containerPort: localPostgres.port }],
|
||||
env: [
|
||||
{ name: "POSTGRES_DB", valueFrom: { secretKeyRef: { name: secretName, key: "POSTGRES_DB" } } },
|
||||
{ name: "POSTGRES_USER", valueFrom: { secretKeyRef: { name: secretName, key: "POSTGRES_USER" } } },
|
||||
{ name: "POSTGRES_PASSWORD", valueFrom: { secretKeyRef: { name: secretName, key: "POSTGRES_PASSWORD" } } },
|
||||
{ name: "PGDATA", value: "/var/lib/postgresql/data/pgdata" },
|
||||
],
|
||||
volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -35,10 +35,17 @@ import { sha256Fingerprint } from "../platform-infra-ops-library";
|
||||
|
||||
import { runYamlLaneGitMirrorFlushJob, runYamlLaneGitMirrorSyncJob, yamlLanePipelineRunCreateScript } from "./secrets";
|
||||
import { capture, captureJsonPayload, compactCapture, isGitSha, progressEvent, stringOrNull } from "./utils";
|
||||
import { runYamlLaneGitopsPublishJob, waitForYamlLaneBuildImage, waitForYamlLaneSourceBootstrap, yamlLaneBuildImageSubmitScript, yamlLaneSourceBootstrapSubmitScript, yamlLaneSourceRestoreScript } from "./yaml-lane";
|
||||
import { runYamlLaneGitopsPublishJob, runYamlLaneK3sBuildImageJob, waitForYamlLaneBuildImage, waitForYamlLaneSourceBootstrap, yamlLaneBuildImageSubmitScript, yamlLaneK3sSourceStatusScript, yamlLaneSourceBootstrapSubmitScript, yamlLaneSourceRestoreScript } from "./yaml-lane";
|
||||
|
||||
export async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown>> {
|
||||
const result = await triggerCurrentYamlLaneConfirmedSteps(config, spec, configPath, waited);
|
||||
if (spec.source.statusMode === "k3s-git-mirror") {
|
||||
return {
|
||||
...result,
|
||||
sourceWorkspaceRestore: { ok: true, status: "skipped", statusMode: spec.source.statusMode, reason: "k3s-git-mirror-does-not-use-host-worktree", valuesPrinted: false },
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const restore = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceRestoreScript(spec)]);
|
||||
const restorePayload = captureJsonPayload(restore);
|
||||
const restoreOk = restore.exitCode === 0 && restorePayload.ok === true;
|
||||
@@ -53,63 +60,27 @@ export async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spe
|
||||
}
|
||||
|
||||
export async function triggerCurrentYamlLaneConfirmedSteps(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown>> {
|
||||
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
|
||||
node: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
status: "submitting",
|
||||
});
|
||||
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
|
||||
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
|
||||
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
|
||||
const source = await resolveTriggerCurrentSource(config, spec, configPath, waited);
|
||||
if (source.ok !== true) return source;
|
||||
const sourceCommit = stringOrNull(source.sourceCommit);
|
||||
const bootstrapPayload = source.sourcePayload;
|
||||
if (sourceCommit === null || !isGitSha(sourceCommit)) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "source-bootstrap-submit",
|
||||
degradedReason: "yaml-lane-source-bootstrap-submit-failed",
|
||||
result: bootstrapSubmitPayload,
|
||||
capture: compactCapture(bootstrapSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const bootstrap = await waitForYamlLaneSourceBootstrap(config, spec, stringOrNull(bootstrapSubmitPayload.jobId));
|
||||
const bootstrapPayload = bootstrap.payload;
|
||||
const sourceCommit = stringOrNull(bootstrapPayload.sourceCommit);
|
||||
if (bootstrap.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "source-bootstrap",
|
||||
degradedReason: "yaml-lane-source-bootstrap-failed",
|
||||
phase: "source-resolve",
|
||||
degradedReason: "yaml-lane-source-commit-invalid",
|
||||
result: bootstrapPayload,
|
||||
bootstrapStatus: bootstrap,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
|
||||
const buildSubmitPayload = captureJsonPayload(buildSubmit);
|
||||
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "image-build-submit",
|
||||
sourceCommit,
|
||||
degradedReason: "yaml-lane-image-build-submit-failed",
|
||||
sourceBootstrap: bootstrapPayload,
|
||||
result: buildSubmitPayload,
|
||||
capture: compactCapture(buildSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const build = await waitForYamlLaneBuildImage(config, spec, sourceCommit, stringOrNull(buildSubmitPayload.jobId));
|
||||
const buildResult = await buildTriggerCurrentImage(config, spec, sourceCommit, bootstrapPayload, configPath, waited);
|
||||
if (buildResult.ok !== true) return buildResult;
|
||||
const build = buildResult.buildStatus;
|
||||
const buildSubmitPayload = buildResult.buildSubmitPayload;
|
||||
const buildPayload = build.payload;
|
||||
const digest = stringOrNull(buildPayload.digest);
|
||||
const envIdentity = stringOrNull(buildPayload.envIdentity);
|
||||
@@ -227,3 +198,124 @@ export async function triggerCurrentYamlLaneConfirmedSteps(config: UniDeskConfig
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveTriggerCurrentSource(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown> & { ok: boolean; sourceCommit?: string | null; sourcePayload?: Record<string, unknown> }> {
|
||||
if (spec.source.statusMode === "k3s-git-mirror") {
|
||||
progressEvent("agentrun.yaml-lane.source-status.progress", {
|
||||
node: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
statusMode: spec.source.statusMode,
|
||||
status: "probing",
|
||||
});
|
||||
const sourceStatus = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)]);
|
||||
const sourcePayload = captureJsonPayload(sourceStatus);
|
||||
const sourceCommit = stringOrNull(sourcePayload.sourceCommit) ?? stringOrNull(sourcePayload.remoteBranchCommit);
|
||||
if (sourceStatus.exitCode !== 0 || sourcePayload.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "source-status",
|
||||
degradedReason: "yaml-lane-k3s-source-status-failed",
|
||||
result: sourcePayload,
|
||||
capture: compactCapture(sourceStatus, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
return { ok: true, sourceCommit, sourcePayload, valuesPrinted: false };
|
||||
}
|
||||
|
||||
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
|
||||
node: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
status: "submitting",
|
||||
});
|
||||
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
|
||||
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
|
||||
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "source-bootstrap-submit",
|
||||
degradedReason: "yaml-lane-source-bootstrap-submit-failed",
|
||||
result: bootstrapSubmitPayload,
|
||||
capture: compactCapture(bootstrapSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const bootstrap = await waitForYamlLaneSourceBootstrap(config, spec, stringOrNull(bootstrapSubmitPayload.jobId));
|
||||
const sourcePayload = bootstrap.payload;
|
||||
const sourceCommit = stringOrNull(sourcePayload.sourceCommit);
|
||||
if (bootstrap.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "source-bootstrap",
|
||||
degradedReason: "yaml-lane-source-bootstrap-failed",
|
||||
result: sourcePayload,
|
||||
bootstrapStatus: bootstrap,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
return { ok: true, sourceCommit, sourcePayload, sourceBootstrapSubmit: bootstrapSubmitPayload, valuesPrinted: false };
|
||||
}
|
||||
|
||||
async function buildTriggerCurrentImage(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, sourcePayload: Record<string, unknown>, configPath: string, waited: boolean): Promise<Record<string, unknown> & { ok: boolean; buildStatus?: Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }; buildSubmitPayload?: Record<string, unknown> }> {
|
||||
if (spec.source.statusMode === "k3s-git-mirror") {
|
||||
const build = await runYamlLaneK3sBuildImageJob(config, spec, sourceCommit);
|
||||
const buildSubmitPayload = {
|
||||
ok: build.ok !== false,
|
||||
status: "submitted",
|
||||
mode: "k3s-buildkit-job",
|
||||
jobName: stringOrNull(build.jobName) ?? null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
if (build.ok !== true) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "image-build",
|
||||
sourceCommit,
|
||||
degradedReason: "yaml-lane-k3s-image-build-failed",
|
||||
sourceBootstrap: sourcePayload,
|
||||
buildSubmit: buildSubmitPayload,
|
||||
result: build.payload,
|
||||
buildStatus: build,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
return { ok: true, buildStatus: build, buildSubmitPayload, valuesPrinted: false };
|
||||
}
|
||||
|
||||
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
|
||||
const buildSubmitPayload = captureJsonPayload(buildSubmit);
|
||||
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane trigger-current",
|
||||
mode: waited ? "confirmed-waited" : "confirmed-trigger",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
phase: "image-build-submit",
|
||||
sourceCommit,
|
||||
degradedReason: "yaml-lane-image-build-submit-failed",
|
||||
sourceBootstrap: sourcePayload,
|
||||
result: buildSubmitPayload,
|
||||
capture: compactCapture(buildSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const build = await waitForYamlLaneBuildImage(config, spec, sourceCommit, stringOrNull(buildSubmitPayload.jobId));
|
||||
return { ok: true, buildStatus: build, buildSubmitPayload, valuesPrinted: false };
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import { agentRunControlPlaneStatusCommand } from "./public-exposure";
|
||||
import { applyYamlScript, manifestObjectRef, yamlLaneGitMirrorStatusScript } from "./secrets";
|
||||
import { compactAgentRunLaneStatusTarget, compactLaneSecretsStatus } from "./trigger";
|
||||
import { capture, captureJsonPayload, compactCapture, record, stringOrNull, timedStatusStage } from "./utils";
|
||||
import { yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
|
||||
import { yamlLaneK3sSourceStatusScript, yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
|
||||
|
||||
export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
@@ -315,7 +315,9 @@ export async function status(config: UniDeskConfig, options: StatusOptions): Pro
|
||||
|
||||
export async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourceProbe = await timedStatusStage("source", () => spec.source.statusMode === "k3s-git-mirror"
|
||||
? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)])
|
||||
: capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourcePayload = captureJsonPayload(sourceProbe.value);
|
||||
const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead);
|
||||
const initialSourceCommit = options.sourceCommit
|
||||
@@ -362,14 +364,15 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
|
||||
: null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const sourceWorkspaceRequired = spec.source.statusMode === "host-worktree";
|
||||
const warnings = [
|
||||
...(sourceWorktreeDetached ? ["source-worktree-detached"] : []),
|
||||
...(sourceBranchAdvanced ? ["source-branch-advanced-after-target"] : []),
|
||||
];
|
||||
const blockers = [
|
||||
...(sourcePayload.workspaceExists === true ? [] : ["source-worktree-missing"]),
|
||||
...(sourceWorkspaceRequired && sourcePayload.workspaceExists !== true ? ["source-worktree-missing"] : []),
|
||||
...(sourcePayload.remoteBranchExists === true ? [] : ["source-branch-missing"]),
|
||||
...(sourcePayload.workspaceClean === true || sourcePayload.workspaceExists !== true ? [] : ["source-worktree-dirty"]),
|
||||
...(sourceWorkspaceRequired && sourcePayload.workspaceClean !== true && sourcePayload.workspaceExists === true ? ["source-worktree-dirty"] : []),
|
||||
...(mirrorPayload.readReady === true ? [] : ["git-mirror-read-not-ready"]),
|
||||
...(mirrorPayload.writeReady === true ? [] : ["git-mirror-write-not-ready"]),
|
||||
...(mirrorPayload.cacheReady === true ? [] : ["git-mirror-cache-not-ready"]),
|
||||
@@ -422,6 +425,7 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
|
||||
? { code: "runtime-blocked", summary: `runtime alignment is blocked: ${runtimeBlockers.join(", ")}`, command: statusFullCommand }
|
||||
: { code: "inspect-full-status", summary: "alignment is inconclusive; inspect full status details", command: statusFullCommand };
|
||||
const compactSourceStatus = {
|
||||
statusMode: spec.source.statusMode,
|
||||
workspaceExists: sourcePayload.workspaceExists ?? false,
|
||||
workspaceClean: sourcePayload.workspaceClean ?? null,
|
||||
branch: sourcePayload.branch ?? null,
|
||||
|
||||
@@ -446,21 +446,27 @@ export function refreshYamlLaneScript(spec: AgentRunLaneSpec): string {
|
||||
`application=${shQuote(spec.gitops.argoApplication)}`,
|
||||
"kubectl -n \"$namespace\" annotate application \"$application\" argocd.argoproj.io/refresh=hard --overwrite >/tmp/agentrun-refresh-output.txt",
|
||||
"kubectl -n \"$namespace\" get application \"$application\" -o json >/tmp/agentrun-refresh-app.json",
|
||||
"NAMESPACE=\"$namespace\" APPLICATION=\"$application\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"let app = {};",
|
||||
"try { app = JSON.parse(fs.readFileSync('/tmp/agentrun-refresh-app.json', 'utf8')); } catch {}",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: true,",
|
||||
" namespace: process.env.NAMESPACE,",
|
||||
" application: process.env.APPLICATION,",
|
||||
" revision: app.status?.sync?.revision ?? null,",
|
||||
" syncStatus: app.status?.sync?.status ?? null,",
|
||||
" healthStatus: app.status?.health?.status ?? null,",
|
||||
" observedAt: new Date().toISOString(),",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
"NAMESPACE=\"$namespace\" APPLICATION=\"$application\" python3 - <<'PY'",
|
||||
"import datetime, json, os",
|
||||
"try:",
|
||||
" with open('/tmp/agentrun-refresh-app.json', 'r', encoding='utf-8') as fh:",
|
||||
" app = json.load(fh)",
|
||||
"except Exception:",
|
||||
" app = {}",
|
||||
"status = app.get('status') or {}",
|
||||
"sync = status.get('sync') or {}",
|
||||
"health = status.get('health') or {}",
|
||||
"print(json.dumps({",
|
||||
" 'ok': True,",
|
||||
" 'namespace': os.environ.get('NAMESPACE'),",
|
||||
" 'application': os.environ.get('APPLICATION'),",
|
||||
" 'revision': sync.get('revision'),",
|
||||
" 'syncStatus': sync.get('status'),",
|
||||
" 'healthStatus': health.get('status'),",
|
||||
" 'observedAt': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z'),",
|
||||
" 'valuesPrinted': False,",
|
||||
"}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
+182
-95
@@ -343,9 +343,10 @@ export function yamlLanePipelineRunCreateScript(spec: AgentRunLaneSpec, sourceCo
|
||||
"if [ \"$existing_status\" = False ]; then kubectl -n \"$namespace\" delete pipelinerun \"$pipeline_run\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true; fi",
|
||||
"if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then created=false; else kubectl create -f \"$tmp\"; created=true; fi",
|
||||
"status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
|
||||
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" node <<'NODE'",
|
||||
"console.log(JSON.stringify({ ok: true, pipelineRun: process.env.PIPELINE_RUN, created: process.env.CREATED === 'true', status: process.env.STATUS || null, valuesPrinted: false }));",
|
||||
"NODE",
|
||||
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" python3 - <<'PY'",
|
||||
"import json, os",
|
||||
"print(json.dumps({'ok': True, 'pipelineRun': os.environ.get('PIPELINE_RUN'), 'created': os.environ.get('CREATED') == 'true', 'status': os.environ.get('STATUS') or None, 'valuesPrinted': False}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -361,9 +362,10 @@ export function createYamlLaneJobScript(namespace: string, jobName: string, mani
|
||||
"printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
|
||||
"kubectl -n \"$namespace\" delete job \"$job\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true",
|
||||
"kubectl create -f \"$tmp\"",
|
||||
"JOB=\"$job\" node <<'NODE'",
|
||||
"console.log(JSON.stringify({ ok: true, jobName: process.env.JOB, valuesPrinted: false }));",
|
||||
"NODE",
|
||||
"JOB=\"$job\" python3 - <<'PY'",
|
||||
"import json, os",
|
||||
"print(json.dumps({'ok': True, 'jobName': os.environ.get('JOB'), 'valuesPrinted': False}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -375,14 +377,27 @@ export function yamlLaneJobProbeScript(namespace: string, jobName: string): stri
|
||||
"kubectl -n \"$namespace\" get job \"$job\" -o json > /tmp/agentrun-job.json 2>/dev/null",
|
||||
"job_exit=$?",
|
||||
"kubectl -n \"$namespace\" logs \"job/$job\" --tail=120 > /tmp/agentrun-job.log 2>/dev/null",
|
||||
"JOB_EXIT=\"$job_exit\" JOB=\"$job\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"let job = null; try { job = JSON.parse(fs.readFileSync('/tmp/agentrun-job.json', 'utf8')); } catch {}",
|
||||
"let log = ''; try { log = fs.readFileSync('/tmp/agentrun-job.log', 'utf8'); } catch {}",
|
||||
"const succeeded = Number(job?.status?.succeeded || 0) > 0;",
|
||||
"const failed = Number(job?.status?.failed || 0) > 0;",
|
||||
"console.log(JSON.stringify({ ok: process.env.JOB_EXIT === '0', jobName: process.env.JOB, succeeded, failed, active: job?.status?.active || 0, logsTail: log.slice(-4000), valuesPrinted: false }));",
|
||||
"NODE",
|
||||
"JOB_EXIT=\"$job_exit\" JOB=\"$job\" python3 - <<'PY'",
|
||||
"import json, os",
|
||||
"def read_json(path):",
|
||||
" try:",
|
||||
" with open(path, 'r', encoding='utf-8') as fh:",
|
||||
" return json.load(fh)",
|
||||
" except Exception:",
|
||||
" return None",
|
||||
"def read_text(path):",
|
||||
" try:",
|
||||
" with open(path, 'r', encoding='utf-8') as fh:",
|
||||
" return fh.read()",
|
||||
" except Exception:",
|
||||
" return ''",
|
||||
"job = read_json('/tmp/agentrun-job.json') or {}",
|
||||
"status = job.get('status') or {}",
|
||||
"log = read_text('/tmp/agentrun-job.log')",
|
||||
"succeeded = int(status.get('succeeded') or 0) > 0",
|
||||
"failed = int(status.get('failed') or 0) > 0",
|
||||
"print(json.dumps({'ok': os.environ.get('JOB_EXIT') == '0', 'jobName': os.environ.get('JOB'), 'succeeded': succeeded, 'failed': failed, 'active': status.get('active') or 0, 'logsTail': log[-4000:], 'valuesPrinted': False}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -619,38 +634,53 @@ export function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
|
||||
"repo_path=\"/cache/${repository}.git\"",
|
||||
"rm -f /tmp/agentrun-gitmirror-refs.txt",
|
||||
"if [ \"$read_exit\" -eq 0 ]; then",
|
||||
" kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; gitops_branch=\"$3\"; printf \"sourceCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$source_branch\" 2>/dev/null || true; printf \"gitopsCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$gitops_branch\" 2>/dev/null || true' sh \"$repo_path\" \"$source_branch\" \"$gitops_branch\" > /tmp/agentrun-gitmirror-refs.txt 2>/dev/null",
|
||||
" kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; gitops_branch=\"$3\"; source_commit=$(git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$source_branch^{commit}\" 2>/dev/null || true); gitops_commit=$(git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$gitops_branch^{commit}\" 2>/dev/null || true); printf \"sourceCommit=%s\\n\" \"$source_commit\"; printf \"gitopsCommit=%s\\n\" \"$gitops_commit\"' sh \"$repo_path\" \"$source_branch\" \"$gitops_branch\" > /tmp/agentrun-gitmirror-refs.txt 2>/dev/null",
|
||||
"fi",
|
||||
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" CACHE_MODE=\"$cache_mode\" CACHE_HOST_PATH=\"$cache_host_path\" REPOSITORY=\"$repository\" SOURCE_BRANCH=\"$source_branch\" GITOPS_BRANCH=\"$gitops_branch\" REPOSITORIES_JSON=\"$repositories_json\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');",
|
||||
"let names = ''; try { names = fs.readFileSync('/tmp/agentrun-gitmirror-names.txt', 'utf8'); } catch {}",
|
||||
"let refs = ''; try { refs = fs.readFileSync('/tmp/agentrun-gitmirror-refs.txt', 'utf8'); } catch {}",
|
||||
"const refValue = (key) => refs.split(/\\r?\\n/).find((line) => line.startsWith(`${key}=`))?.slice(key.length + 1).trim() || null;",
|
||||
"const readReady = process.env.READ_EXIT === '0';",
|
||||
"const writeReady = process.env.WRITE_EXIT === '0';",
|
||||
"const cachePvcExists = process.env.CACHE_EXIT === '0';",
|
||||
"const cacheMode = process.env.CACHE_MODE || 'pvc';",
|
||||
"const cacheReady = cacheMode === 'hostPath' ? true : cachePvcExists;",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: readReady && writeReady && cacheReady,",
|
||||
" namespace: process.env.NAMESPACE,",
|
||||
" readReady,",
|
||||
" writeReady,",
|
||||
" cacheMode,",
|
||||
" cacheReady,",
|
||||
" cachePvcExists,",
|
||||
" cacheHostPath: process.env.CACHE_HOST_PATH || null,",
|
||||
" resources: names.split(/\\r?\\n/).filter(Boolean).slice(0, 40),",
|
||||
" repository: process.env.REPOSITORY,",
|
||||
" sourceBranch: process.env.SOURCE_BRANCH,",
|
||||
" gitopsBranch: process.env.GITOPS_BRANCH,",
|
||||
" sourceCommit: refValue('sourceCommit'),",
|
||||
" gitopsCommit: refValue('gitopsCommit'),",
|
||||
" repositories,",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" CACHE_MODE=\"$cache_mode\" CACHE_HOST_PATH=\"$cache_host_path\" REPOSITORY=\"$repository\" SOURCE_BRANCH=\"$source_branch\" GITOPS_BRANCH=\"$gitops_branch\" REPOSITORIES_JSON=\"$repositories_json\" python3 - <<'PY'",
|
||||
"import json, os, re",
|
||||
"def read_text(path):",
|
||||
" try:",
|
||||
" with open(path, 'r', encoding='utf-8') as fh:",
|
||||
" return fh.read()",
|
||||
" except Exception:",
|
||||
" return ''",
|
||||
"def ref_value(text, key):",
|
||||
" prefix = key + '='",
|
||||
" for line in re.split(r'\\r?\\n', text):",
|
||||
" if line.startswith(prefix):",
|
||||
" value = line[len(prefix):].strip()",
|
||||
" return value or None",
|
||||
" return None",
|
||||
"try:",
|
||||
" repositories = json.loads(os.environ.get('REPOSITORIES_JSON') or '[]')",
|
||||
"except Exception:",
|
||||
" repositories = []",
|
||||
"names = read_text('/tmp/agentrun-gitmirror-names.txt')",
|
||||
"refs = read_text('/tmp/agentrun-gitmirror-refs.txt')",
|
||||
"read_ready = os.environ.get('READ_EXIT') == '0'",
|
||||
"write_ready = os.environ.get('WRITE_EXIT') == '0'",
|
||||
"cache_pvc_exists = os.environ.get('CACHE_EXIT') == '0'",
|
||||
"cache_mode = os.environ.get('CACHE_MODE') or 'pvc'",
|
||||
"cache_ready = True if cache_mode == 'hostPath' else cache_pvc_exists",
|
||||
"print(json.dumps({",
|
||||
" 'ok': read_ready and write_ready and cache_ready,",
|
||||
" 'namespace': os.environ.get('NAMESPACE'),",
|
||||
" 'readReady': read_ready,",
|
||||
" 'writeReady': write_ready,",
|
||||
" 'cacheMode': cache_mode,",
|
||||
" 'cacheReady': cache_ready,",
|
||||
" 'cachePvcExists': cache_pvc_exists,",
|
||||
" 'cacheHostPath': os.environ.get('CACHE_HOST_PATH') or None,",
|
||||
" 'resources': [line for line in re.split(r'\\r?\\n', names) if line][:40],",
|
||||
" 'repository': os.environ.get('REPOSITORY'),",
|
||||
" 'sourceBranch': os.environ.get('SOURCE_BRANCH'),",
|
||||
" 'gitopsBranch': os.environ.get('GITOPS_BRANCH'),",
|
||||
" 'sourceCommit': ref_value(refs, 'sourceCommit'),",
|
||||
" 'gitopsCommit': ref_value(refs, 'gitopsCommit'),",
|
||||
" 'repositories': repositories,",
|
||||
" 'valuesPrinted': False",
|
||||
"}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -660,6 +690,7 @@ export type LaneSecretSource = {
|
||||
sourceMode: "env" | "file";
|
||||
sourceKey: string | null;
|
||||
targetRef: { namespace: string; name: string; key: string };
|
||||
transform?: "local-postgres-database" | "local-postgres-user" | "local-postgres-database-url";
|
||||
};
|
||||
|
||||
export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource): { redactedPath: string; value: string; valueBytes: number; fingerprint: string } {
|
||||
@@ -680,6 +711,7 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
|
||||
value = envValue;
|
||||
}
|
||||
if (value.length === 0) throw new Error(`secret source ${sourceRef} is empty`);
|
||||
value = transformSecretSourceValue(spec, source, value);
|
||||
return {
|
||||
redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
|
||||
value,
|
||||
@@ -688,6 +720,20 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
|
||||
};
|
||||
}
|
||||
|
||||
function transformSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource, rawValue: string): string {
|
||||
if (source.transform === undefined) return rawValue;
|
||||
const localPostgres = spec.deployment.localPostgres;
|
||||
if (!localPostgres.enabled || localPostgres.serviceName === null || localPostgres.database === null || localPostgres.user === null || localPostgres.port === null) {
|
||||
throw new Error(`secret source ${source.id} requires enabled localPostgres with database/user/port`);
|
||||
}
|
||||
if (source.transform === "local-postgres-database") return localPostgres.database;
|
||||
if (source.transform === "local-postgres-user") return localPostgres.user;
|
||||
const user = encodeURIComponent(localPostgres.user);
|
||||
const password = encodeURIComponent(rawValue);
|
||||
const database = encodeURIComponent(localPostgres.database);
|
||||
return `postgresql://${user}:${password}@${localPostgres.serviceName}:${localPostgres.port}/${database}`;
|
||||
}
|
||||
|
||||
export function redactAbsoluteSecretPath(sourceRef: string): string {
|
||||
const parts = sourceRef.split("/").filter(Boolean);
|
||||
return parts.length === 0 ? "/" : `/${parts.slice(0, -1).join("/")}/<redacted>`;
|
||||
@@ -732,6 +778,45 @@ export function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSour
|
||||
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key },
|
||||
});
|
||||
}
|
||||
const localPostgres = spec.deployment.localPostgres;
|
||||
if (spec.database.mode === "local-postgres" && localPostgres.enabled) {
|
||||
if (localPostgres.passwordSourceRef === null || localPostgres.passwordSourceKey === null) {
|
||||
throw new Error(`config/agentrun.yaml controlPlane.lanes.${spec.lane}.deployment.localPostgres must declare passwordSourceRef/passwordSourceKey`);
|
||||
}
|
||||
result.push(
|
||||
{
|
||||
id: "local-postgres-password",
|
||||
sourceRef: localPostgres.passwordSourceRef,
|
||||
sourceMode: "env",
|
||||
sourceKey: localPostgres.passwordSourceKey,
|
||||
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: "POSTGRES_PASSWORD" },
|
||||
},
|
||||
{
|
||||
id: "local-postgres-user",
|
||||
sourceRef: localPostgres.passwordSourceRef,
|
||||
sourceMode: "env",
|
||||
sourceKey: localPostgres.passwordSourceKey,
|
||||
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: "POSTGRES_USER" },
|
||||
transform: "local-postgres-user",
|
||||
},
|
||||
{
|
||||
id: "local-postgres-database",
|
||||
sourceRef: localPostgres.passwordSourceRef,
|
||||
sourceMode: "env",
|
||||
sourceKey: localPostgres.passwordSourceKey,
|
||||
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: "POSTGRES_DB" },
|
||||
transform: "local-postgres-database",
|
||||
},
|
||||
{
|
||||
id: "database",
|
||||
sourceRef: localPostgres.passwordSourceRef,
|
||||
sourceMode: "env",
|
||||
sourceKey: localPostgres.passwordSourceKey,
|
||||
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key },
|
||||
transform: "local-postgres-database-url",
|
||||
},
|
||||
);
|
||||
}
|
||||
for (const secret of spec.secrets) {
|
||||
result.push({
|
||||
id: secret.id,
|
||||
@@ -762,47 +847,44 @@ export function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ target
|
||||
"trap 'rm -rf \"$tmp_dir\"' EXIT",
|
||||
"payload_json=\"$tmp_dir/payload.json\"",
|
||||
"printf '%s' \"$payload_b64\" | base64 -d > \"$payload_json\"",
|
||||
"PAYLOAD_JSON=\"$payload_json\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const cp = require('node:child_process');",
|
||||
"const crypto = require('node:crypto');",
|
||||
"const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));",
|
||||
"const results = [];",
|
||||
"const groups = new Map();",
|
||||
"function run(argv, input) {",
|
||||
" const out = cp.spawnSync(argv[0], argv.slice(1), { input, encoding: 'utf8' });",
|
||||
" if (out.status !== 0) throw new Error(`${argv.join(' ')} failed: ${out.stderr || out.stdout}`);",
|
||||
" return out;",
|
||||
"}",
|
||||
"function tryRun(argv) {",
|
||||
" return cp.spawnSync(argv[0], argv.slice(1), { encoding: 'utf8' });",
|
||||
"}",
|
||||
"for (let index = 0; index < items.length; index += 1) {",
|
||||
" const item = items[index];",
|
||||
" const ref = item.targetRef;",
|
||||
" const groupKey = `${ref.namespace}\\u0000${ref.name}`;",
|
||||
" if (!groups.has(groupKey)) groups.set(groupKey, { namespace: ref.namespace, name: ref.name, items: [] });",
|
||||
" groups.get(groupKey).items.push({ key: ref.key, valueBase64: item.valueBase64 });",
|
||||
"}",
|
||||
"for (const group of groups.values()) {",
|
||||
" const ns = run(['kubectl', 'create', 'namespace', group.namespace, '--dry-run=client', '-o', 'yaml']).stdout;",
|
||||
" run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns);",
|
||||
" const existing = tryRun(['kubectl', '-n', group.namespace, 'get', 'secret', group.name]);",
|
||||
" if (existing.status !== 0) run(['kubectl', '-n', group.namespace, 'create', 'secret', 'generic', group.name]);",
|
||||
" const patch = { data: {} };",
|
||||
" for (const entry of group.items) patch.data[entry.key] = entry.valueBase64;",
|
||||
" const patchFile = `${process.env.TMP_DIR}/${group.namespace}-${group.name}-patch.json`;",
|
||||
" fs.writeFileSync(patchFile, JSON.stringify(patch));",
|
||||
" run(['kubectl', '-n', group.namespace, 'patch', 'secret', group.name, '--type=merge', `--patch-file=${patchFile}`]);",
|
||||
" const fetched = JSON.parse(run(['kubectl', '-n', group.namespace, 'get', 'secret', group.name, '-o', 'json']).stdout);",
|
||||
" for (const entry of group.items) {",
|
||||
" const raw = fetched.data?.[entry.key] || '';",
|
||||
" const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);",
|
||||
" results.push({ namespace: group.namespace, secret: group.name, key: entry.key, ok: raw.length > 0, valueBytes: decoded.length, fingerprint: raw ? 'sha256:' + crypto.createHash('sha256').update(decoded).digest('hex') : null, valuesPrinted: false });",
|
||||
" }",
|
||||
"}",
|
||||
"console.log(JSON.stringify({ ok: results.every((item) => item.ok), secretCount: results.length, items: results, valuesPrinted: false }));",
|
||||
"NODE",
|
||||
"PAYLOAD_JSON=\"$payload_json\" TMP_DIR=\"$tmp_dir\" python3 - <<'PY'",
|
||||
"import base64, hashlib, json, os, subprocess",
|
||||
"def run(argv, input_text=None):",
|
||||
" out = subprocess.run(argv, input=input_text, text=True, capture_output=True)",
|
||||
" if out.returncode != 0:",
|
||||
" raise RuntimeError('%s failed: %s' % (' '.join(argv), (out.stderr or out.stdout).strip()))",
|
||||
" return out",
|
||||
"def try_run(argv):",
|
||||
" return subprocess.run(argv, text=True, capture_output=True)",
|
||||
"with open(os.environ['PAYLOAD_JSON'], 'r', encoding='utf-8') as fh:",
|
||||
" items = json.load(fh)",
|
||||
"groups = {}",
|
||||
"for item in items:",
|
||||
" ref = item.get('targetRef') or {}",
|
||||
" key = (ref.get('namespace'), ref.get('name'))",
|
||||
" groups.setdefault(key, {'namespace': ref.get('namespace'), 'name': ref.get('name'), 'items': []})['items'].append({'key': ref.get('key'), 'valueBase64': item.get('valueBase64') or ''})",
|
||||
"results = []",
|
||||
"for group in groups.values():",
|
||||
" namespace = group['namespace']",
|
||||
" name = group['name']",
|
||||
" ns_yaml = run(['kubectl', 'create', 'namespace', namespace, '--dry-run=client', '-o', 'yaml']).stdout",
|
||||
" run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns_yaml)",
|
||||
" existing = try_run(['kubectl', '-n', namespace, 'get', 'secret', name])",
|
||||
" if existing.returncode != 0:",
|
||||
" run(['kubectl', '-n', namespace, 'create', 'secret', 'generic', name])",
|
||||
" patch = {'data': {entry['key']: entry['valueBase64'] for entry in group['items']}}",
|
||||
" patch_file = os.path.join(os.environ['TMP_DIR'], '%s-%s-patch.json' % (namespace, name))",
|
||||
" with open(patch_file, 'w', encoding='utf-8') as fh:",
|
||||
" json.dump(patch, fh)",
|
||||
" run(['kubectl', '-n', namespace, 'patch', 'secret', name, '--type=merge', '--patch-file=%s' % patch_file])",
|
||||
" fetched = json.loads(run(['kubectl', '-n', namespace, 'get', 'secret', name, '-o', 'json']).stdout)",
|
||||
" data = fetched.get('data') or {}",
|
||||
" for entry in group['items']:",
|
||||
" raw = data.get(entry['key']) or ''",
|
||||
" decoded = base64.b64decode(raw.encode('utf-8')) if raw else b''",
|
||||
" results.append({'namespace': namespace, 'secret': name, 'key': entry['key'], 'ok': bool(raw), 'valueBytes': len(decoded), 'fingerprint': ('sha256:' + hashlib.sha256(decoded).hexdigest()) if raw else None, 'valuesPrinted': False})",
|
||||
"print(json.dumps({'ok': all(item['ok'] for item in results), 'secretCount': len(results), 'items': results, 'valuesPrinted': False}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -869,15 +951,20 @@ export function applyYamlScript(yaml: string, fieldManager: string, dryRun: bool
|
||||
"kubectl apply $args -f \"$manifest\" > \"$tmp_dir/apply.out\" 2> \"$tmp_dir/apply.err\"",
|
||||
"apply_exit=$?",
|
||||
"set -e",
|
||||
"APPLY_EXIT=\"$apply_exit\" APPLY_OUT=\"$tmp_dir/apply.out\" APPLY_ERR=\"$tmp_dir/apply.err\" MANIFEST=\"$manifest\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const crypto = require('node:crypto');",
|
||||
"const out = fs.readFileSync(process.env.APPLY_OUT, 'utf8');",
|
||||
"const err = fs.readFileSync(process.env.APPLY_ERR, 'utf8');",
|
||||
"const manifest = fs.readFileSync(process.env.MANIFEST, 'utf8');",
|
||||
"const resources = out.split(/\\r?\\n/).map((line) => line.trim()).filter(Boolean).slice(0, 80);",
|
||||
"console.log(JSON.stringify({ ok: process.env.APPLY_EXIT === '0', exitCode: Number(process.env.APPLY_EXIT), resourceCount: resources.length, resources, manifestBytes: Buffer.byteLength(manifest, 'utf8'), manifestDigest: 'sha256:' + crypto.createHash('sha256').update(manifest).digest('hex'), stderrTail: err.slice(-3000), valuesPrinted: false }));",
|
||||
"NODE",
|
||||
"APPLY_EXIT=\"$apply_exit\" APPLY_OUT=\"$tmp_dir/apply.out\" APPLY_ERR=\"$tmp_dir/apply.err\" MANIFEST=\"$manifest\" python3 - <<'PY'",
|
||||
"import hashlib, json, os, re",
|
||||
"def read_bytes(path):",
|
||||
" try:",
|
||||
" with open(path, 'rb') as fh:",
|
||||
" return fh.read()",
|
||||
" except Exception:",
|
||||
" return b''",
|
||||
"out = read_bytes(os.environ['APPLY_OUT']).decode('utf-8', errors='replace')",
|
||||
"err = read_bytes(os.environ['APPLY_ERR']).decode('utf-8', errors='replace')",
|
||||
"manifest = read_bytes(os.environ['MANIFEST'])",
|
||||
"resources = [line.strip() for line in re.split(r'\\r?\\n', out) if line.strip()][:80]",
|
||||
"print(json.dumps({'ok': os.environ.get('APPLY_EXIT') == '0', 'exitCode': int(os.environ.get('APPLY_EXIT') or '1'), 'resourceCount': len(resources), 'resources': resources, 'manifestBytes': len(manifest), 'manifestDigest': 'sha256:' + hashlib.sha256(manifest).hexdigest(), 'stderrTail': err[-3000:], 'valuesPrinted': False}, ensure_ascii=False))",
|
||||
"PY",
|
||||
"exit \"$apply_exit\"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -354,26 +354,78 @@ export function yamlLaneSourceStatusScript(spec: AgentRunLaneSpec): string {
|
||||
" actual_workspace=$(pwd)",
|
||||
"fi",
|
||||
"export expected_workspace source_branch workspace_exists workspace_clean local_head branch remote_url remote_branch_exists remote_branch_commit status_short actual_workspace",
|
||||
"node <<'NODE'",
|
||||
"function nullable(value) { return value && value !== 'null' ? value : null; }",
|
||||
"function booleanValue(value) { if (value === 'true') return true; if (value === 'false') return false; return null; }",
|
||||
"const env = process.env;",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: env.workspace_exists === 'true',",
|
||||
" expectedWorkspace: env.expected_workspace,",
|
||||
" actualWorkspace: env.actual_workspace || null,",
|
||||
" workspaceExists: env.workspace_exists === 'true',",
|
||||
" workspaceClean: booleanValue(env.workspace_clean),",
|
||||
" branch: nullable(env.branch),",
|
||||
" remoteUrl: nullable(env.remote_url),",
|
||||
" localHead: nullable(env.local_head),",
|
||||
" remoteBranch: env.source_branch,",
|
||||
" remoteBranchExists: env.remote_branch_exists === 'true',",
|
||||
" remoteBranchCommit: nullable(env.remote_branch_commit),",
|
||||
" statusShort: nullable(env.status_short),",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
"python3 - <<'PY'",
|
||||
"import json, os",
|
||||
"def nullable(value):",
|
||||
" return value if value and value != 'null' else None",
|
||||
"def boolean_value(value):",
|
||||
" if value == 'true': return True",
|
||||
" if value == 'false': return False",
|
||||
" return None",
|
||||
"env = os.environ",
|
||||
"print(json.dumps({",
|
||||
" 'ok': env.get('workspace_exists') == 'true',",
|
||||
" 'statusMode': 'host-worktree',",
|
||||
" 'expectedWorkspace': env.get('expected_workspace'),",
|
||||
" 'actualWorkspace': env.get('actual_workspace') or None,",
|
||||
" 'workspaceExists': env.get('workspace_exists') == 'true',",
|
||||
" 'workspaceClean': boolean_value(env.get('workspace_clean')),",
|
||||
" 'branch': nullable(env.get('branch')),",
|
||||
" 'remoteUrl': nullable(env.get('remote_url')),",
|
||||
" 'localHead': nullable(env.get('local_head')),",
|
||||
" 'remoteBranch': env.get('source_branch'),",
|
||||
" 'remoteBranchExists': env.get('remote_branch_exists') == 'true',",
|
||||
" 'remoteBranchCommit': nullable(env.get('remote_branch_commit')),",
|
||||
" 'statusShort': nullable(env.get('status_short')),",
|
||||
" 'valuesPrinted': False,",
|
||||
"}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function yamlLaneK3sSourceStatusScript(spec: AgentRunLaneSpec): string {
|
||||
return [
|
||||
"set +e",
|
||||
`namespace=${shQuote(spec.gitMirror.namespace)}`,
|
||||
`read_deployment=${shQuote(spec.gitMirror.readDeployment)}`,
|
||||
`repository=${shQuote(spec.source.repository)}`,
|
||||
`source_branch=${shQuote(spec.source.branch)}`,
|
||||
"repo_path=\"/cache/${repository}.git\"",
|
||||
"kubectl -n \"$namespace\" get deploy \"$read_deployment\" >/tmp/agentrun-source-read-deploy.txt 2>/dev/null",
|
||||
"read_exit=$?",
|
||||
"source_commit=''",
|
||||
"if [ \"$read_exit\" -eq 0 ]; then",
|
||||
" source_commit=$(kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$source_branch^{commit}\" 2>/dev/null || true' sh \"$repo_path\" \"$source_branch\" 2>/dev/null | tail -n 1 | tr -d '\\r')",
|
||||
"fi",
|
||||
"export namespace read_deployment repository source_branch read_exit source_commit",
|
||||
"python3 - <<'PY'",
|
||||
"import json, os",
|
||||
"source_commit = os.environ.get('source_commit') or None",
|
||||
"read_ready = os.environ.get('read_exit') == '0'",
|
||||
"print(json.dumps({",
|
||||
" 'ok': read_ready and source_commit is not None,",
|
||||
" 'statusMode': 'k3s-git-mirror',",
|
||||
" 'expectedWorkspace': None,",
|
||||
" 'actualWorkspace': None,",
|
||||
" 'workspaceExists': None,",
|
||||
" 'workspaceClean': None,",
|
||||
" 'branch': os.environ.get('source_branch'),",
|
||||
" 'remoteUrl': None,",
|
||||
" 'localHead': source_commit,",
|
||||
" 'remoteBranch': os.environ.get('source_branch'),",
|
||||
" 'remoteBranchExists': source_commit is not None,",
|
||||
" 'remoteBranchCommit': source_commit,",
|
||||
" 'sourceCommit': source_commit,",
|
||||
" 'gitMirror': {",
|
||||
" 'namespace': os.environ.get('namespace'),",
|
||||
" 'readDeployment': os.environ.get('read_deployment'),",
|
||||
" 'readReady': read_ready,",
|
||||
" 'repository': os.environ.get('repository'),",
|
||||
" },",
|
||||
" 'statusShort': None,",
|
||||
" 'valuesPrinted': False,",
|
||||
"}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -412,54 +464,70 @@ export function yamlLaneRuntimeStatusScript(spec: AgentRunLaneSpec, pipelineRun:
|
||||
"manager_svc_exit=$?",
|
||||
"kubectl -n \"$runtime_namespace\" get secret \"$database_secret\" -o json > \"$tmp_dir/db-secret.json\" 2>/dev/null",
|
||||
"db_secret_exit=$?",
|
||||
"SECRET_REFS_JSON=\"$secrets_json\" NODE_TMP=\"$tmp_dir\" node <<'NODE' > \"$tmp_dir/secrets.json\"",
|
||||
"const fs = require('node:fs');",
|
||||
"const cp = require('node:child_process');",
|
||||
"const refs = JSON.parse(process.env.SECRET_REFS_JSON || '[]');",
|
||||
"const result = [];",
|
||||
"for (const ref of refs) {",
|
||||
" const out = cp.spawnSync('kubectl', ['-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json'], { encoding: 'utf8' });",
|
||||
" let keyPresent = false;",
|
||||
" if (out.status === 0) {",
|
||||
" try { keyPresent = Object.prototype.hasOwnProperty.call((JSON.parse(out.stdout).data || {}), ref.key); } catch {}",
|
||||
" }",
|
||||
" result.push({ namespace: ref.namespace, name: ref.name, key: ref.key, present: out.status === 0, keyPresent, valuesPrinted: false });",
|
||||
"}",
|
||||
"console.log(JSON.stringify({ ready: result.every((item) => item.present && item.keyPresent), count: result.length, items: result, valuesPrinted: false }));",
|
||||
"NODE",
|
||||
"SECRET_REFS_JSON=\"$secrets_json\" python3 - <<'PY' > \"$tmp_dir/secrets.json\"",
|
||||
"import json, os, subprocess",
|
||||
"refs = json.loads(os.environ.get('SECRET_REFS_JSON') or '[]')",
|
||||
"result = []",
|
||||
"for ref in refs:",
|
||||
" out = subprocess.run(['kubectl', '-n', ref.get('namespace', ''), 'get', 'secret', ref.get('name', ''), '-o', 'json'], text=True, capture_output=True)",
|
||||
" key_present = False",
|
||||
" if out.returncode == 0:",
|
||||
" try:",
|
||||
" key_present = ref.get('key') in (json.loads(out.stdout).get('data') or {})",
|
||||
" except Exception:",
|
||||
" key_present = False",
|
||||
" result.append({'namespace': ref.get('namespace'), 'name': ref.get('name'), 'key': ref.get('key'), 'present': out.returncode == 0, 'keyPresent': key_present, 'valuesPrinted': False})",
|
||||
"print(json.dumps({'ready': all(item['present'] and item['keyPresent'] for item in result), 'count': len(result), 'items': result, 'valuesPrinted': False}, ensure_ascii=False))",
|
||||
"PY",
|
||||
"kubectl -n \"$runtime_namespace\" get deploy,sts,svc,secret -o name > \"$tmp_dir/runtime-names.txt\" 2>/dev/null",
|
||||
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const path = require('node:path');",
|
||||
"const dir = process.env.NODE_TMP;",
|
||||
"function readJson(name) { try { return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { return null; } }",
|
||||
"function exists(exitName) { return process.env[exitName] === '0'; }",
|
||||
"function condition(obj) { return obj?.status?.conditions?.[0] || null; }",
|
||||
"function envValue(deploy, name) { return deploy?.spec?.template?.spec?.containers?.[0]?.env?.find((entry) => entry.name === name)?.value || null; }",
|
||||
"function pipelineRunParam(obj, name) { return (obj?.spec?.params || []).find((entry) => entry.name === name)?.value || null; }",
|
||||
"const pipelineRun = readJson('pipelinerun.json');",
|
||||
"const argo = readJson('argo.json');",
|
||||
"const managerDeploy = readJson('manager-deploy.json');",
|
||||
"const managerSvc = readJson('manager-svc.json');",
|
||||
"const dbSecret = readJson('db-secret.json');",
|
||||
"const secrets = readJson('secrets.json') || { ready: true, count: 0, items: [], valuesPrinted: false };",
|
||||
"let names = ''; try { names = fs.readFileSync(path.join(dir, 'runtime-names.txt'), 'utf8'); } catch {}",
|
||||
"const c = condition(pipelineRun);",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: exists('RUNTIME_NS_EXIT') && exists('CI_NS_EXIT'),",
|
||||
" runtimeNamespaceExists: exists('RUNTIME_NS_EXIT'),",
|
||||
" ciNamespaceExists: exists('CI_NS_EXIT'),",
|
||||
" serviceAccountExists: exists('SERVICE_ACCOUNT_EXIT'),",
|
||||
" pipeline: { exists: exists('PIPELINE_EXIT'), name: process.env.pipeline_name },",
|
||||
" pipelineRun: { exists: exists('PIPELINERUN_EXIT'), name: process.env.pipeline_run || null, status: c?.status || null, reason: c?.reason || null, sourceCommit: pipelineRunParam(pipelineRun, 'revision'), startTime: pipelineRun?.status?.startTime || null, completionTime: pipelineRun?.status?.completionTime || null },",
|
||||
" argo: { exists: exists('ARGO_EXIT'), namespace: process.env.argo_namespace, application: process.env.argo_application, revision: argo?.status?.sync?.revision || null, syncStatus: argo?.status?.sync?.status || null, healthStatus: argo?.status?.health?.status || null },",
|
||||
" manager: { deploymentExists: exists('MANAGER_DEPLOY_EXIT'), serviceExists: exists('MANAGER_SVC_EXIT'), deployment: process.env.manager_deployment, service: process.env.manager_service, image: managerDeploy?.spec?.template?.spec?.containers?.[0]?.image || null, sourceCommit: envValue(managerDeploy, 'AGENTRUN_SOURCE_COMMIT'), servicePorts: Array.isArray(managerSvc?.spec?.ports) ? managerSvc.spec.ports.map((port) => ({ name: port.name || null, port: port.port || null, targetPort: port.targetPort || null })) : [] },",
|
||||
" database: { secretPresent: exists('DB_SECRET_EXIT'), secretName: process.env.database_secret, key: process.env.database_key, keyPresent: Boolean(dbSecret?.data && Object.prototype.hasOwnProperty.call(dbSecret.data, process.env.database_key || '')) , valuesPrinted: false },",
|
||||
" secrets,",
|
||||
" localPostgres: { absent: !/postgres/i.test(names), matchingObjects: names.split(/\\r?\\n/).filter((line) => /postgres/i.test(line)).slice(0, 20) },",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" python3 - <<'PY'",
|
||||
"import json, os, pathlib, re",
|
||||
"base = pathlib.Path(os.environ['NODE_TMP'])",
|
||||
"def read_json(name):",
|
||||
" try: return json.loads((base / name).read_text())",
|
||||
" except Exception: return None",
|
||||
"def exists(name): return os.environ.get(name) == '0'",
|
||||
"def condition(obj):",
|
||||
" conds = (((obj or {}).get('status') or {}).get('conditions') or [])",
|
||||
" return conds[0] if conds else {}",
|
||||
"def env_value(deploy, name):",
|
||||
" containers = (((((deploy or {}).get('spec') or {}).get('template') or {}).get('spec') or {}).get('containers') or [])",
|
||||
" envs = (containers[0].get('env') or []) if containers else []",
|
||||
" for entry in envs:",
|
||||
" if entry.get('name') == name: return entry.get('value')",
|
||||
" return None",
|
||||
"def pipeline_run_param(obj, name):",
|
||||
" for entry in (((obj or {}).get('spec') or {}).get('params') or []):",
|
||||
" if entry.get('name') == name: return entry.get('value')",
|
||||
" return None",
|
||||
"pipeline_run = read_json('pipelinerun.json')",
|
||||
"argo = read_json('argo.json')",
|
||||
"manager_deploy = read_json('manager-deploy.json')",
|
||||
"manager_svc = read_json('manager-svc.json')",
|
||||
"db_secret = read_json('db-secret.json')",
|
||||
"secrets = read_json('secrets.json') or {'ready': True, 'count': 0, 'items': [], 'valuesPrinted': False}",
|
||||
"try: names = (base / 'runtime-names.txt').read_text()",
|
||||
"except Exception: names = ''",
|
||||
"c = condition(pipeline_run)",
|
||||
"ports = []",
|
||||
"for port in ((((manager_svc or {}).get('spec') or {}).get('ports')) or []):",
|
||||
" ports.append({'name': port.get('name'), 'port': port.get('port'), 'targetPort': port.get('targetPort')})",
|
||||
"matching_postgres = [line for line in re.split(r'\\r?\\n', names) if re.search('postgres', line, re.I)][:20]",
|
||||
"print(json.dumps({",
|
||||
" 'ok': exists('RUNTIME_NS_EXIT') and exists('CI_NS_EXIT'),",
|
||||
" 'runtimeNamespaceExists': exists('RUNTIME_NS_EXIT'),",
|
||||
" 'ciNamespaceExists': exists('CI_NS_EXIT'),",
|
||||
" 'serviceAccountExists': exists('SERVICE_ACCOUNT_EXIT'),",
|
||||
" 'pipeline': {'exists': exists('PIPELINE_EXIT'), 'name': os.environ.get('pipeline_name')},",
|
||||
" 'pipelineRun': {'exists': exists('PIPELINERUN_EXIT'), 'name': os.environ.get('pipeline_run') or None, 'status': c.get('status'), 'reason': c.get('reason'), 'sourceCommit': pipeline_run_param(pipeline_run, 'revision'), 'startTime': ((pipeline_run or {}).get('status') or {}).get('startTime'), 'completionTime': ((pipeline_run or {}).get('status') or {}).get('completionTime')},",
|
||||
" 'argo': {'exists': exists('ARGO_EXIT'), 'namespace': os.environ.get('argo_namespace'), 'application': os.environ.get('argo_application'), 'revision': (((argo or {}).get('status') or {}).get('sync') or {}).get('revision'), 'syncStatus': (((argo or {}).get('status') or {}).get('sync') or {}).get('status'), 'healthStatus': (((argo or {}).get('status') or {}).get('health') or {}).get('status')},",
|
||||
" 'manager': {'deploymentExists': exists('MANAGER_DEPLOY_EXIT'), 'serviceExists': exists('MANAGER_SVC_EXIT'), 'deployment': os.environ.get('manager_deployment'), 'service': os.environ.get('manager_service'), 'image': ((((((manager_deploy or {}).get('spec') or {}).get('template') or {}).get('spec') or {}).get('containers') or [{}])[0]).get('image'), 'sourceCommit': env_value(manager_deploy, 'AGENTRUN_SOURCE_COMMIT'), 'servicePorts': ports},",
|
||||
" 'database': {'secretPresent': exists('DB_SECRET_EXIT'), 'secretName': os.environ.get('database_secret'), 'key': os.environ.get('database_key'), 'keyPresent': os.environ.get('database_key') in (((db_secret or {}).get('data') or {})), 'valuesPrinted': False},",
|
||||
" 'secrets': secrets,",
|
||||
" 'localPostgres': {'absent': len(matching_postgres) == 0, 'matchingObjects': matching_postgres},",
|
||||
" 'valuesPrinted': False,",
|
||||
"}, ensure_ascii=False))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -930,6 +998,234 @@ export function yamlLaneBuildImageStatusScript(spec: AgentRunLaneSpec, jobId: st
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function yamlLaneK3sBuildImageJobManifest(spec: AgentRunLaneSpec, sourceCommit: string, jobName: string): Record<string, unknown> {
|
||||
const build = spec.deployment.manager.imageBuild;
|
||||
if (spec.ci.buildkitImage === null) throw new Error(`config/agentrun.yaml controlPlane.lanes.${spec.lane}.ci.buildkitImage is required when source.statusMode=k3s-git-mirror`);
|
||||
const imageRepository = `${spec.ci.registryPrefix}/${build.repository}`;
|
||||
const envIdentity = `source-${sourceCommit.slice(0, 12)}`;
|
||||
const image = `${imageRepository}:${envIdentity}`;
|
||||
const proxyEnv = yamlLaneK3sBuildProxyEnv(spec);
|
||||
return {
|
||||
apiVersion: "batch/v1",
|
||||
kind: "Job",
|
||||
metadata: {
|
||||
name: jobName,
|
||||
namespace: spec.ci.namespace,
|
||||
labels: {
|
||||
"app.kubernetes.io/name": "agentrun-manager-image-build",
|
||||
"app.kubernetes.io/part-of": "agentrun",
|
||||
"agentrun.pikastech.local/lane": spec.version,
|
||||
"agentrun.pikastech.local/node": spec.nodeId,
|
||||
"agentrun.pikastech.local/source-commit": sourceCommit,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
backoffLimit: 0,
|
||||
activeDeadlineSeconds: Math.max(60, build.timeoutSeconds),
|
||||
ttlSecondsAfterFinished: 3600,
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
"app.kubernetes.io/name": "agentrun-manager-image-build",
|
||||
"app.kubernetes.io/part-of": "agentrun",
|
||||
"agentrun.pikastech.local/lane": spec.version,
|
||||
"agentrun.pikastech.local/node": spec.nodeId,
|
||||
"agentrun.pikastech.local/source-commit": sourceCommit,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
serviceAccountName: spec.ci.serviceAccountName,
|
||||
hostNetwork: true,
|
||||
dnsPolicy: "ClusterFirstWithHostNet",
|
||||
securityContext: { fsGroup: 1000 },
|
||||
volumes: [
|
||||
{ name: "workspace", emptyDir: { sizeLimit: "8Gi" } },
|
||||
{ name: "buildkit-state", emptyDir: { sizeLimit: "8Gi" } },
|
||||
{ name: "tmp", emptyDir: {} },
|
||||
],
|
||||
initContainers: [{
|
||||
name: "source",
|
||||
image: spec.ci.toolsImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
env: proxyEnv,
|
||||
command: ["/bin/sh", "-ec", yamlLaneK3sBuildSourceShell(spec, sourceCommit)],
|
||||
volumeMounts: [{ name: "workspace", mountPath: "/workspace" }],
|
||||
}],
|
||||
containers: [{
|
||||
name: "buildkit",
|
||||
image: spec.ci.buildkitImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
env: [
|
||||
...proxyEnv,
|
||||
{ name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" },
|
||||
],
|
||||
command: ["/bin/sh", "-ec", yamlLaneK3sBuildImageShell(spec, sourceCommit, image, envIdentity)],
|
||||
securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 },
|
||||
volumeMounts: [
|
||||
{ name: "workspace", mountPath: "/workspace" },
|
||||
{ name: "buildkit-state", mountPath: "/home/user/.local/share/buildkit" },
|
||||
{ name: "tmp", mountPath: "/tmp" },
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runYamlLaneK3sBuildImageJob(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
|
||||
const jobName = `agentrun-build-${spec.nodeId.toLowerCase()}-${spec.lane}-${sourceCommit.slice(0, 12)}`.slice(0, 63);
|
||||
const manifest = yamlLaneK3sBuildImageJobManifest(spec, sourceCommit, jobName);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.ci.namespace, jobName, manifest)]);
|
||||
const createPayload = captureJsonPayload(created);
|
||||
if (created.exitCode !== 0 || createPayload.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
payload: { ok: false, status: "create-failed", jobName, degradedReason: "k3s-buildkit-job-create-failed", valuesPrinted: false },
|
||||
create: createPayload,
|
||||
capture: compactCapture(created, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
const timeoutMs = Math.max(60, spec.deployment.manager.imageBuild.timeoutSeconds) * 1000;
|
||||
const pollMs = Math.max(1, spec.deployment.manager.imageBuild.pollSeconds) * 1000;
|
||||
let polls = 0;
|
||||
let lastPayload: Record<string, unknown> = {};
|
||||
let lastProbe: SshCaptureResult | null = null;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
polls += 1;
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.ci.namespace, jobName)]);
|
||||
const probePayload = captureJsonPayload(lastProbe);
|
||||
const buildPayload = yamlLaneGitopsPublishPayloadFromProbe(probePayload);
|
||||
if (Object.keys(buildPayload).length > 0) lastPayload = buildPayload;
|
||||
progressEvent("agentrun.yaml-lane.k3s-image-build.progress", {
|
||||
node: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
sourceCommit,
|
||||
jobName,
|
||||
polls,
|
||||
status: stringOrNull(buildPayload.status) ?? (probePayload.succeeded === true ? "succeeded" : probePayload.failed === true ? "failed" : "running"),
|
||||
digest: stringOrNull(buildPayload.digest),
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
});
|
||||
if (probePayload.succeeded === true) {
|
||||
if (buildPayload.ok === true && stringOrNull(buildPayload.digest) !== null && stringOrNull(buildPayload.envIdentity) !== null) {
|
||||
return { ok: true, payload: buildPayload, jobName, create: createPayload, polls, elapsedMs: Date.now() - startedAt, probe: compactCapture(lastProbe), valuesPrinted: false };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
payload: { ...buildPayload, ok: false, status: stringOrNull(buildPayload.status) ?? "succeeded-without-result", degradedReason: "k3s-buildkit-result-missing", jobName, valuesPrinted: false },
|
||||
jobName,
|
||||
create: createPayload,
|
||||
polls,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
probe: compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
if (probePayload.failed === true) {
|
||||
const payload = Object.keys(buildPayload).length > 0 ? buildPayload : { ok: false, status: "failed", degradedReason: "k3s-buildkit-job-failed", jobName, valuesPrinted: false };
|
||||
return {
|
||||
ok: false,
|
||||
payload,
|
||||
jobName,
|
||||
create: createPayload,
|
||||
polls,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
probe: compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
await sleep(pollMs);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
payload: { ...lastPayload, ok: false, status: "timeout", degradedReason: "k3s-buildkit-job-timeout", jobName, valuesPrinted: false },
|
||||
jobName,
|
||||
create: createPayload,
|
||||
polls,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
probe: lastProbe === null ? null : compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function yamlLaneK3sBuildProxyEnv(spec: AgentRunLaneSpec): Array<{ name: string; value: string }> {
|
||||
const build = spec.deployment.manager.imageBuild;
|
||||
const result: Array<{ name: string; value: string }> = [];
|
||||
const noProxy = build.noProxy.join(",");
|
||||
const allProxy = build.httpsProxy ?? build.httpProxy;
|
||||
if (build.httpProxy !== null) result.push({ name: "HTTP_PROXY", value: build.httpProxy }, { name: "http_proxy", value: build.httpProxy });
|
||||
if (build.httpsProxy !== null) result.push({ name: "HTTPS_PROXY", value: build.httpsProxy }, { name: "https_proxy", value: build.httpsProxy });
|
||||
if (allProxy !== null) result.push({ name: "ALL_PROXY", value: allProxy }, { name: "all_proxy", value: allProxy });
|
||||
result.push({ name: "NO_PROXY", value: noProxy }, { name: "no_proxy", value: noProxy });
|
||||
return result;
|
||||
}
|
||||
|
||||
function yamlLaneK3sBuildSourceShell(spec: AgentRunLaneSpec, sourceCommit: string): string {
|
||||
return [
|
||||
"set -eu",
|
||||
`read_url=${shQuote(spec.gitMirror.readUrl)}`,
|
||||
`source_branch=${shQuote(spec.source.branch)}`,
|
||||
`source_commit=${shQuote(sourceCommit)}`,
|
||||
"rm -rf /workspace/repo",
|
||||
"git clone --no-checkout \"$read_url\" /workspace/repo",
|
||||
"cd /workspace/repo",
|
||||
"git fetch origin \"refs/heads/$source_branch:refs/remotes/origin/$source_branch\"",
|
||||
"git checkout --detach \"$source_commit\"",
|
||||
"actual=$(git rev-parse HEAD)",
|
||||
"test \"$actual\" = \"$source_commit\"",
|
||||
"chmod -R a+rwX /workspace",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function yamlLaneK3sBuildImageShell(spec: AgentRunLaneSpec, sourceCommit: string, image: string, envIdentity: string): string {
|
||||
const build = spec.deployment.manager.imageBuild;
|
||||
const contextPath = build.context === "." ? "/workspace/repo" : `/workspace/repo/${build.context.replace(/^\.\//u, "")}`;
|
||||
const buildContainerNoProxy = build.buildContainerProxy.noProxy.join(",");
|
||||
const buildArgs = [
|
||||
...Object.entries(build.buildArgs).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `build-arg:${key}=${value}`),
|
||||
...yamlLaneK3sBuildProxyBuildArgs(build.buildContainerProxy.httpProxy, build.buildContainerProxy.httpsProxy, buildContainerNoProxy),
|
||||
];
|
||||
const buildctlArgs = [
|
||||
"build",
|
||||
"--allow", "network.host",
|
||||
"--frontend", "dockerfile.v0",
|
||||
"--local", `context=${contextPath}`,
|
||||
"--local", "dockerfile=/workspace/repo",
|
||||
"--opt", `filename=${build.containerfile}`,
|
||||
"--opt", `network=${build.network}`,
|
||||
...buildArgs.flatMap((arg) => ["--opt", arg]),
|
||||
"--metadata-file", "/workspace/build-metadata.json",
|
||||
"--output", `type=image,name=${image},push=true,registry.insecure=true`,
|
||||
];
|
||||
const repositoryDigestBase = image.slice(0, image.lastIndexOf(":"));
|
||||
return [
|
||||
"set -eu",
|
||||
"cd /workspace/repo",
|
||||
`buildctl-daemonless.sh ${buildctlArgs.map((arg) => shQuote(arg)).join(" ")}`,
|
||||
"metadata_compact=$(tr -d '\\n' < /workspace/build-metadata.json)",
|
||||
"digest=$(printf '%s' \"$metadata_compact\" | sed -n 's/.*\"containerimage.digest\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n 1)",
|
||||
"if [ -z \"$digest\" ]; then",
|
||||
` printf '{"ok":false,"status":"failed","failureKind":"image-digest-missing","sourceCommit":"%s","envIdentity":"%s","image":"%s","valuesPrinted":false}\\n' ${shQuote(sourceCommit)} ${shQuote(envIdentity)} ${shQuote(image)}`,
|
||||
" exit 1",
|
||||
"fi",
|
||||
`printf '{"ok":true,"status":"built","sourceCommit":"%s","envIdentity":"%s","image":"%s","digest":"%s","repositoryDigest":"%s@%s","valuesPrinted":false}\\n' ${shQuote(sourceCommit)} ${shQuote(envIdentity)} ${shQuote(image)} "$digest" ${shQuote(repositoryDigestBase)} "$digest"`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function yamlLaneK3sBuildProxyBuildArgs(httpProxy: string | null, httpsProxy: string | null, noProxy: string): string[] {
|
||||
const result: string[] = [];
|
||||
const allProxy = httpsProxy ?? httpProxy;
|
||||
if (httpProxy !== null) result.push(`build-arg:HTTP_PROXY=${httpProxy}`, `build-arg:http_proxy=${httpProxy}`);
|
||||
if (httpsProxy !== null) result.push(`build-arg:HTTPS_PROXY=${httpsProxy}`, `build-arg:https_proxy=${httpsProxy}`);
|
||||
if (allProxy !== null) result.push(`build-arg:ALL_PROXY=${allProxy}`, `build-arg:all_proxy=${allProxy}`);
|
||||
if (noProxy.length > 0) result.push(`build-arg:NO_PROXY=${noProxy}`, `build-arg:no_proxy=${noProxy}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runYamlLaneGitopsPublishJob(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, files: readonly { path: string; content: string }[]): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
|
||||
const jobName = `gitops-publish-${spec.nodeId.toLowerCase()}-${spec.lane}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const manifest = yamlLaneGitopsPublishJobManifest(spec, files, jobName);
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
export const HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH = "config/hwlab-node-control-plane.yaml";
|
||||
|
||||
export type InfraAction = "plan" | "status" | "apply";
|
||||
export type ToolsImageAction = "status" | "build" | "logs";
|
||||
export type TektonInstallAction = "status" | "apply" | "logs";
|
||||
export type ArgoAction = "status" | "apply" | "logs";
|
||||
export type EgressBenchmarkAction = "benchmark" | "status" | "logs";
|
||||
export type CiBuildBenchmarkAction = "benchmark" | "status" | "logs";
|
||||
export type K3sInstallAction = "plan" | "install" | "status";
|
||||
|
||||
export interface InfraOptions {
|
||||
action: InfraAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
export interface ToolsImageOptions {
|
||||
action: ToolsImageAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export interface TektonInstallOptions {
|
||||
action: TektonInstallAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export interface ArgoOptions {
|
||||
action: ArgoAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export interface EgressBenchmarkOptions {
|
||||
action: EgressBenchmarkAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
profile: "no-mirror";
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
samples: number;
|
||||
sampleTimeoutSeconds: number;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export interface CiBuildBenchmarkOptions {
|
||||
action: CiBuildBenchmarkAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
profile: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export interface K3sInstallOptions {
|
||||
action: K3sInstallAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
export interface CiBuildBenchmarkCachePolicy {
|
||||
noPipelineRunReuse: boolean;
|
||||
forceFullBuild: boolean;
|
||||
forbidGitopsCatalogReuse: boolean;
|
||||
forbidDependencyCache: boolean;
|
||||
forbidBuildkitCache: boolean;
|
||||
forbidRegistryMirror: boolean;
|
||||
forbidLocalPreheatedImages: boolean;
|
||||
}
|
||||
|
||||
export interface CiBuildBenchmarkProfileSpec {
|
||||
profile: string;
|
||||
runtimeLaneConfigRef: string;
|
||||
pipelineRunPrefix: string;
|
||||
catalogPathTemplate: string;
|
||||
imageTagMode: "full";
|
||||
pipelineTimeoutSeconds: number;
|
||||
cachePolicy: CiBuildBenchmarkCachePolicy;
|
||||
requiredTimings: readonly string[];
|
||||
failureFamilies: readonly string[];
|
||||
}
|
||||
|
||||
export type ControlPlaneEgressProxySpec = ControlPlaneK8sServiceEgressProxySpec | ControlPlaneHostRouteEgressProxySpec;
|
||||
|
||||
export interface ControlPlaneK8sServiceEgressProxySpec {
|
||||
mode: "k8s-service-cluster-ip";
|
||||
clientName: string;
|
||||
namespace: string;
|
||||
serviceName: string;
|
||||
port: number;
|
||||
sourceConfigRef: string | null;
|
||||
sourceFingerprint: string | null;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
sourceType: "subscription-url" | "master-shadowsocks";
|
||||
preferredOutbound: "vless-reality" | "hysteria2" | null;
|
||||
noProxy: readonly string[];
|
||||
}
|
||||
|
||||
export interface ControlPlaneHostRouteEgressProxySpec {
|
||||
mode: "host-route";
|
||||
clientName: string;
|
||||
hostProxyConfigRef: string;
|
||||
proxyEnvPath: string;
|
||||
proxyUrl: string;
|
||||
noProxy: readonly string[];
|
||||
}
|
||||
|
||||
export interface ControlPlaneGitMirrorEgressProxySpec {
|
||||
mode: "node-global" | "host-route" | "direct";
|
||||
required: boolean;
|
||||
podHostNetwork: boolean;
|
||||
injectPodEnv: boolean;
|
||||
}
|
||||
|
||||
export interface ControlPlaneRuntimeProxySpec {
|
||||
enabled: boolean;
|
||||
mode: "host-route";
|
||||
configRef: string | null;
|
||||
hostNetwork: boolean;
|
||||
injectEnv: boolean;
|
||||
deployments: readonly string[];
|
||||
statefulSets: readonly string[];
|
||||
}
|
||||
|
||||
export type ControlPlaneGitMirrorGithubTransportSpec =
|
||||
| {
|
||||
mode: "ssh";
|
||||
privateKeySecretKey: string;
|
||||
privateKeySourceRef: string;
|
||||
privateKeySourceKey: string;
|
||||
privateKeySourceEncoding: "plain" | "base64";
|
||||
knownHostsSecretKey: string | null;
|
||||
knownHostsSourceRef: string | null;
|
||||
knownHostsSourceKey: string | null;
|
||||
knownHostsSourceEncoding: "plain" | "base64" | null;
|
||||
}
|
||||
| {
|
||||
mode: "https";
|
||||
username: string;
|
||||
tokenSecretName: string;
|
||||
tokenSecretKey: string;
|
||||
tokenSourceRef: string;
|
||||
tokenSourceKey: string;
|
||||
};
|
||||
|
||||
export interface ControlPlaneTektonGitWorkspaceSecretSpec {
|
||||
name: string;
|
||||
namespace: string;
|
||||
sourceRefFrom: "gitMirror.githubTransport";
|
||||
privateKeySecretKey: string;
|
||||
knownHostsSecretKey: string;
|
||||
}
|
||||
|
||||
export interface ControlPlaneTektonRuntimeObserverRbacSpec {
|
||||
namespace: string;
|
||||
roleName: string;
|
||||
roleBindingName: string;
|
||||
}
|
||||
|
||||
export interface ControlPlaneTektonArgoObserverRbacSpec {
|
||||
namespace: string;
|
||||
roleName: string;
|
||||
roleBindingName: string;
|
||||
}
|
||||
|
||||
export interface ControlPlaneTektonInstallManifestSpec {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ControlPlaneTektonInstallSpec {
|
||||
enabled: boolean;
|
||||
sourceKind: "url";
|
||||
version: string;
|
||||
fieldManager: string;
|
||||
manifests: readonly ControlPlaneTektonInstallManifestSpec[];
|
||||
requiredCrds: readonly string[];
|
||||
expectedDeploymentNamespaces: readonly string[];
|
||||
readinessTimeoutSeconds: number;
|
||||
runtimeProxy: ControlPlaneRuntimeProxySpec;
|
||||
}
|
||||
|
||||
export interface ControlPlaneNodeSpec {
|
||||
id: string;
|
||||
route: string;
|
||||
kubeRoute: string;
|
||||
k3s: ControlPlaneK3sNodeSpec | null;
|
||||
registry: { endpoint: string };
|
||||
egressProxy: ControlPlaneEgressProxySpec | null;
|
||||
}
|
||||
|
||||
export interface ControlPlaneK3sNodeSpec {
|
||||
serviceName: string;
|
||||
dropInPath: string;
|
||||
nodeStatusName: string;
|
||||
execStartPre: readonly (readonly string[])[];
|
||||
install: ControlPlaneK3sInstallSpec | null;
|
||||
serverArgs: readonly string[];
|
||||
kubelet: { maxPods: number };
|
||||
}
|
||||
|
||||
export interface ControlPlaneK3sInstallSpec {
|
||||
enabled: boolean;
|
||||
channel: string;
|
||||
version: string;
|
||||
installScriptUrl: string;
|
||||
binaryUrl: string;
|
||||
sha256Url: string;
|
||||
expectedSha256: string;
|
||||
hostProxyConfigRef: string;
|
||||
proxyEnvPath: string;
|
||||
registriesYamlPath: string;
|
||||
localRegistry: {
|
||||
containerName: string;
|
||||
image: string;
|
||||
canonicalImage: string;
|
||||
bind: string;
|
||||
};
|
||||
state: {
|
||||
dir: string;
|
||||
logPath: string;
|
||||
statusPath: string;
|
||||
};
|
||||
downloads: {
|
||||
connectTimeoutSeconds: number;
|
||||
maxTimeSeconds: number;
|
||||
retry: number;
|
||||
retryDelaySeconds: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DockerfileInlineSpec {
|
||||
filename: string;
|
||||
lines: readonly string[];
|
||||
}
|
||||
|
||||
export interface ImageRewriteSpec {
|
||||
source: string;
|
||||
pullImage: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface ControlPlaneTargetSpec {
|
||||
id: string;
|
||||
node: string;
|
||||
lane: string;
|
||||
enabled: boolean;
|
||||
ciNamespace: string;
|
||||
runtimeNamespace: string;
|
||||
source: { repository: string; branch: string };
|
||||
gitops: { branch: string; path: string };
|
||||
gitMirror: {
|
||||
namespace: string;
|
||||
serviceReadName: string;
|
||||
serviceWriteName: string;
|
||||
cachePvcName: string;
|
||||
cachePvcStorage: string;
|
||||
cacheHostPath: string | null;
|
||||
servicePort: number;
|
||||
readContainerPort: number;
|
||||
writeContainerPort: number;
|
||||
deploymentReplicas: number;
|
||||
secretName: string;
|
||||
syncConfigMapName: string;
|
||||
syncJobPrefix: string;
|
||||
flushJobPrefix: string;
|
||||
readUrl: string;
|
||||
writeUrl: string;
|
||||
egressProxy: ControlPlaneGitMirrorEgressProxySpec | null;
|
||||
githubTransport: ControlPlaneGitMirrorGithubTransportSpec;
|
||||
};
|
||||
tekton: {
|
||||
install: ControlPlaneTektonInstallSpec;
|
||||
pipelineName: string;
|
||||
serviceAccountName: string;
|
||||
pipelineRunPrefix: string;
|
||||
gitWorkspaceSecret: ControlPlaneTektonGitWorkspaceSecretSpec;
|
||||
runtimeObserverRbac: ControlPlaneTektonRuntimeObserverRbacSpec;
|
||||
argoObserverRbac: ControlPlaneTektonArgoObserverRbacSpec;
|
||||
toolsImage: {
|
||||
output: string;
|
||||
imagePullPolicy: "Always" | "IfNotPresent" | "Never";
|
||||
sourceKind: "dockerfile" | "docker-compose";
|
||||
context: string;
|
||||
dockerfile?: string;
|
||||
dockerfileInline?: DockerfileInlineSpec;
|
||||
composeFile?: string;
|
||||
buildArgs: Readonly<Record<string, string>>;
|
||||
buildNetwork: string | null;
|
||||
publicBaseImages: readonly string[];
|
||||
buildOwner: string;
|
||||
buildMode: string;
|
||||
};
|
||||
};
|
||||
ciBuildBenchmarks: readonly CiBuildBenchmarkProfileSpec[];
|
||||
argo: {
|
||||
namespace: string;
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
applicationFile: string;
|
||||
install: {
|
||||
enabled: boolean;
|
||||
sourceKind: "url";
|
||||
version: string;
|
||||
manifestUrl: string;
|
||||
fieldManager: string;
|
||||
imagePullPolicy: "Always" | "IfNotPresent" | "Never";
|
||||
preloadImages: readonly string[];
|
||||
imageRewrites: readonly ImageRewriteSpec[];
|
||||
requiredCrds: readonly string[];
|
||||
expectedDeployments: readonly string[];
|
||||
expectedStatefulSets: readonly string[];
|
||||
readinessTimeoutSeconds: number;
|
||||
runtimeProxy: ControlPlaneRuntimeProxySpec;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ControlPlaneImagePolicy {
|
||||
requireReproducibleBuildSource: boolean;
|
||||
forbidPrivateOrNodeLocalImagesAsInputs: boolean;
|
||||
allowNodeLocalRegistryAsBuildOutput: boolean;
|
||||
requiredSourceKinds: readonly ("dockerfile" | "docker-compose")[];
|
||||
}
|
||||
|
||||
export interface ControlPlaneConfig {
|
||||
version: number;
|
||||
kind: string;
|
||||
metadata: { owner: string; relatedIssues: readonly number[] };
|
||||
imagePolicy: ControlPlaneImagePolicy;
|
||||
nodes: Record<string, ControlPlaneNodeSpec>;
|
||||
targets: readonly ControlPlaneTargetSpec[];
|
||||
}
|
||||
|
||||
export interface HwlabNodeControlPlaneSourceWorkspaceBootstrapSpec {
|
||||
readonly configPath: string;
|
||||
readonly targetId: string;
|
||||
readonly node: string;
|
||||
readonly lane: string;
|
||||
readonly ciNamespace: string;
|
||||
readonly serviceAccountName: string;
|
||||
readonly toolsImage: string;
|
||||
readonly imagePullPolicy: "Always" | "IfNotPresent" | "Never";
|
||||
readonly gitReadUrl: string;
|
||||
readonly gitWriteUrl: string;
|
||||
readonly gitMirrorNamespace: string;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+552
-1869
File diff suppressed because it is too large
Load Diff
@@ -307,8 +307,12 @@ export interface HwlabRuntimePublicExposureSpec {
|
||||
export interface HwlabRuntimeBootstrapAdminSpec {
|
||||
readonly username: string;
|
||||
readonly displayName: string;
|
||||
readonly usernameSourceRef?: string;
|
||||
readonly usernameSourceKey?: string;
|
||||
readonly usernameSourceLine?: number;
|
||||
readonly passwordSourceRef: string;
|
||||
readonly passwordSourceKey: string;
|
||||
readonly passwordSourceLine?: number;
|
||||
readonly passwordHashTransform: "hwlab-sha256";
|
||||
readonly secretName: string;
|
||||
readonly secretKey: string;
|
||||
@@ -756,11 +760,19 @@ function bootstrapAdminConfig(value: unknown, path: string): HwlabRuntimeBootstr
|
||||
const transform = stringField(raw, "passwordHashTransform", path);
|
||||
if (transform !== "hwlab-sha256") throw new Error(`${path}.passwordHashTransform must be hwlab-sha256`);
|
||||
const rollout = asRecord(raw.rollout, `${path}.rollout`);
|
||||
const usernameSourceRef = raw.usernameSourceRef === undefined ? undefined : sourceRefField(raw, "usernameSourceRef", path);
|
||||
const usernameSourceKey = raw.usernameSourceKey === undefined ? undefined : secretKeyField(raw, "usernameSourceKey", path);
|
||||
const usernameSourceLine = raw.usernameSourceLine === undefined ? undefined : numberField(raw, "usernameSourceLine", path);
|
||||
const passwordSourceLine = raw.passwordSourceLine === undefined ? undefined : numberField(raw, "passwordSourceLine", path);
|
||||
return {
|
||||
username: stringField(raw, "username", path),
|
||||
displayName: stringField(raw, "displayName", path),
|
||||
...(usernameSourceRef === undefined ? {} : { usernameSourceRef }),
|
||||
...(usernameSourceKey === undefined ? {} : { usernameSourceKey }),
|
||||
...(usernameSourceLine === undefined ? {} : { usernameSourceLine }),
|
||||
passwordSourceRef: sourceRefField(raw, "passwordSourceRef", path),
|
||||
passwordSourceKey: secretKeyField(raw, "passwordSourceKey", path),
|
||||
...(passwordSourceLine === undefined ? {} : { passwordSourceLine }),
|
||||
passwordHashTransform: transform,
|
||||
secretName: stringField(raw, "secretName", path),
|
||||
secretKey: secretKeyField(raw, "secretKey", path),
|
||||
@@ -1044,7 +1056,7 @@ function validateConfigRef(ref: string, path: string): void {
|
||||
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
|
||||
throw new Error(`${path} must reference a repo-relative config/*.yaml file without ..`);
|
||||
}
|
||||
if (!/^[A-Za-z0-9_.\-[\]]+$/u.test(fragment)) {
|
||||
if (!/^[A-Za-z0-9_.\-[\]${}]+$/u.test(fragment)) {
|
||||
throw new Error(`${path} has an unsupported YAML path fragment`);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-29-p14-yaml-template-refs.
|
||||
// Responsibility: Render YAML-first web-probe sentinel configRefs with node/lane variables and selector paths.
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
|
||||
export interface WebProbeSentinelTemplateContext {
|
||||
readonly nodeId: string;
|
||||
readonly lane: string;
|
||||
readonly runtimeNamespace?: string;
|
||||
}
|
||||
|
||||
type TemplateVarValue = string | number | boolean;
|
||||
|
||||
export interface WebProbeSentinelRenderedConfigRef {
|
||||
readonly originalRef: string;
|
||||
readonly ref: string;
|
||||
readonly file: string;
|
||||
readonly path: string;
|
||||
readonly target: unknown;
|
||||
readonly sha256: string;
|
||||
readonly byteCount: number;
|
||||
readonly variableNames: readonly string[];
|
||||
}
|
||||
|
||||
export function readWebProbeSentinelConfigRefTarget(context: WebProbeSentinelTemplateContext, ref: string): unknown {
|
||||
return readWebProbeSentinelConfigRef(context, ref).target;
|
||||
}
|
||||
|
||||
export function readWebProbeSentinelConfigRef(context: WebProbeSentinelTemplateContext, ref: string): WebProbeSentinelRenderedConfigRef {
|
||||
const builtinVars = templateContextVars(context);
|
||||
const renderedRef = renderTemplateString(ref, builtinVars, "configRef");
|
||||
const parsed = parseRenderedConfigRef(renderedRef);
|
||||
const absPath = rootPath(parsed.file);
|
||||
if (!existsSync(absPath)) throw new Error(`${parsed.file} does not exist`);
|
||||
const text = readFileSync(absPath, "utf8");
|
||||
const doc = Bun.YAML.parse(text) as unknown;
|
||||
const vars = { ...builtinVars, ...documentVars(doc, builtinVars) };
|
||||
const path = renderTemplateString(parsed.path, vars, `${parsed.file}#path`);
|
||||
const target = valueAtConfigPath(doc, path);
|
||||
if (target === undefined) throw new Error(`${parsed.file}#${path} is missing`);
|
||||
return {
|
||||
originalRef: ref,
|
||||
ref: `${parsed.file}#${path}`,
|
||||
file: parsed.file,
|
||||
path,
|
||||
target: renderTemplateValue(target, vars),
|
||||
sha256: `sha256:${createHash("sha256").update(text).digest("hex")}`,
|
||||
byteCount: Buffer.byteLength(text),
|
||||
variableNames: Object.keys(vars).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderWebProbeSentinelConfigRefString(context: WebProbeSentinelTemplateContext, ref: string): string {
|
||||
return renderTemplateString(ref, templateContextVars(context), "configRef");
|
||||
}
|
||||
|
||||
function templateContextVars(context: WebProbeSentinelTemplateContext): Record<string, TemplateVarValue> {
|
||||
const node = context.nodeId;
|
||||
const nodeLower = node.toLowerCase();
|
||||
const lane = context.lane;
|
||||
const laneMinor = lane.replace(/^v/u, "");
|
||||
return {
|
||||
NODE: node,
|
||||
node,
|
||||
nodeLower,
|
||||
NODE_LOWER: nodeLower,
|
||||
LANE: lane,
|
||||
lane,
|
||||
laneMinor,
|
||||
LANE_MINOR: laneMinor,
|
||||
nodeLane: `${nodeLower}-${lane}`,
|
||||
NODE_LANE: `${node}-${lane}`,
|
||||
runtimeNamespace: context.runtimeNamespace ?? "",
|
||||
...contextTemplateVars(context),
|
||||
};
|
||||
}
|
||||
|
||||
function contextTemplateVars(context: WebProbeSentinelTemplateContext): Record<string, TemplateVarValue> {
|
||||
const observability = isRecord((context as { readonly observability?: unknown }).observability)
|
||||
? (context as { readonly observability: Record<string, unknown> }).observability
|
||||
: {};
|
||||
const webProbe = isRecord(observability.webProbe) ? observability.webProbe : {};
|
||||
const templateVars = isRecord(webProbe.templateVars) ? webProbe.templateVars : {};
|
||||
const result: Record<string, TemplateVarValue> = {};
|
||||
for (const [key, value] of Object.entries(templateVars)) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`observability.webProbe.templateVars.${key} is not a supported variable name`);
|
||||
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
||||
throw new Error(`observability.webProbe.templateVars.${key} must be a scalar`);
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function documentVars(doc: unknown, vars: Record<string, TemplateVarValue>): Record<string, TemplateVarValue> {
|
||||
if (!isRecord(doc) || !isRecord(doc.vars)) return {};
|
||||
const result: Record<string, TemplateVarValue> = {};
|
||||
for (const [key, value] of Object.entries(doc.vars)) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`vars.${key} is not a supported variable name`);
|
||||
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
||||
throw new Error(`vars.${key} must be a scalar`);
|
||||
}
|
||||
result[key] = typeof value === "string"
|
||||
? renderTemplateString(value, { ...vars, ...result }, `vars.${key}`)
|
||||
: value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderTemplateValue(value: unknown, vars: Record<string, TemplateVarValue>): unknown {
|
||||
if (typeof value === "string") return renderTemplateScalar(value, vars, "YAML string");
|
||||
if (Array.isArray(value)) return value.map((item) => renderTemplateValue(item, vars));
|
||||
if (!isRecord(value)) return value;
|
||||
const localVars = localTemplateVars(value, vars);
|
||||
const rendered: Record<string, unknown> = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (key === "vars") continue;
|
||||
rendered[key] = renderTemplateValue(item, localVars);
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
function localTemplateVars(value: Record<string, unknown>, parentVars: Record<string, TemplateVarValue>): Record<string, TemplateVarValue> {
|
||||
if (!isRecord(value.vars)) return parentVars;
|
||||
const local: Record<string, TemplateVarValue> = {};
|
||||
for (const [key, item] of Object.entries(value.vars)) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`vars.${key} is not a supported variable name`);
|
||||
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") throw new Error(`vars.${key} must be a scalar`);
|
||||
local[key] = typeof item === "string"
|
||||
? renderTemplateString(item, { ...parentVars, ...local }, `vars.${key}`)
|
||||
: item;
|
||||
}
|
||||
return { ...parentVars, ...local };
|
||||
}
|
||||
|
||||
function renderTemplateScalar(value: string, vars: Record<string, TemplateVarValue>, label: string): TemplateVarValue {
|
||||
const whole = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/u.exec(value);
|
||||
if (whole !== null) {
|
||||
const rendered = vars[whole[1]];
|
||||
if (rendered === undefined) throw new Error(`${label} references undefined variable ${whole[1]}`);
|
||||
return rendered;
|
||||
}
|
||||
return renderTemplateString(value, vars, label);
|
||||
}
|
||||
|
||||
function renderTemplateString(value: string, vars: Record<string, TemplateVarValue>, label: string): string {
|
||||
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_match, key: string) => {
|
||||
const rendered = vars[key];
|
||||
if (rendered === undefined) throw new Error(`${label} references undefined variable ${key}`);
|
||||
return String(rendered);
|
||||
});
|
||||
}
|
||||
|
||||
function parseRenderedConfigRef(ref: string): { readonly file: string; readonly path: string } {
|
||||
const [file, path, extra] = ref.split("#");
|
||||
if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
|
||||
throw new Error(`${ref} must use path/to/file.yaml#object.path syntax`);
|
||||
}
|
||||
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
|
||||
throw new Error(`${ref} must reference a repo-relative config/*.yaml file without ..`);
|
||||
}
|
||||
return { file, path };
|
||||
}
|
||||
|
||||
function valueAtConfigPath(value: unknown, path: string): unknown {
|
||||
let current = value;
|
||||
for (const segment of path.split(".")) {
|
||||
if (segment.length === 0) return undefined;
|
||||
current = valueAtSegment(current, segment);
|
||||
if (current === undefined) return undefined;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function valueAtSegment(value: unknown, segment: string): unknown {
|
||||
let rest = segment;
|
||||
let current = value;
|
||||
const key = /^[A-Za-z0-9_-]+/u.exec(rest)?.[0] ?? null;
|
||||
if (key !== null) {
|
||||
if (!isRecord(current)) return undefined;
|
||||
current = current[key];
|
||||
rest = rest.slice(key.length);
|
||||
}
|
||||
if (key === null && !rest.startsWith("[")) return undefined;
|
||||
while (rest.length > 0) {
|
||||
const match = /^\[([^\]]+)\]/u.exec(rest);
|
||||
if (match === null) return undefined;
|
||||
current = valueAtBracket(current, match[1]);
|
||||
if (current === undefined) return undefined;
|
||||
rest = rest.slice(match[0].length);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function valueAtBracket(value: unknown, selector: string): unknown {
|
||||
if (/^\d+$/u.test(selector)) {
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
return value[Number(selector)];
|
||||
}
|
||||
const match = /^([A-Za-z0-9_-]+)=([A-Za-z0-9_.:/@+-]+)$/u.exec(selector);
|
||||
if (match === null || !Array.isArray(value)) return undefined;
|
||||
const [, key, expected] = match;
|
||||
return value.find((item) => isRecord(item) && item[key] === expected);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
|
||||
// Responsibility: Redacted YAML configRef graph for web-probe sentinel plan/status.
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
import { readWebProbeSentinelConfigRef } from "./hwlab-node-web-sentinel-config-ref";
|
||||
import { HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeSentinelConfigRefKey } from "./hwlab-node-lanes";
|
||||
import { effectiveWebProbeSentinelPublicExposure, resolveWebProbeSentinel, webProbeSentinelRegistryRows, type WebProbeSentinelRegistryRow } from "./hwlab-node-web-sentinel-resolver";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
@@ -212,7 +210,7 @@ export function webProbeSentinelConfigPlan(spec: HwlabRuntimeLaneSpec, action: W
|
||||
|
||||
const selected = resolveWebProbeSentinel(spec, sentinelId);
|
||||
const refs = HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS
|
||||
.map((key) => readSentinelConfigRef(key, selected.configRefs[key]))
|
||||
.map((key) => readSentinelConfigRef(spec, key, selected.configRefs[key]))
|
||||
.map((ref) => effectiveConfigRefStatus(spec, selected.id, ref));
|
||||
const conflicts = selected.enabled ? crossReferenceConflicts(spec, refs) : [];
|
||||
const refBlocked = refs.some((ref) => !ref.present || !ref.targetPresent || ref.missingFields.length > 0 || ref.conflicts.length > 0 || ref.error !== null);
|
||||
@@ -255,28 +253,22 @@ export function withWebProbeSentinelConfigRendered(value: WebProbeSentinelConfig
|
||||
};
|
||||
}
|
||||
|
||||
function readSentinelConfigRef(key: HwlabRuntimeWebProbeSentinelConfigRefKey, ref: string): InternalConfigRefStatus {
|
||||
const parsed = parseConfigRef(ref);
|
||||
if (parsed.error !== null) return emptyRefStatus(key, ref, parsed.file, parsed.path, parsed.error);
|
||||
const absPath = rootPath(parsed.file);
|
||||
if (!existsSync(absPath)) return emptyRefStatus(key, ref, parsed.file, parsed.path, `${parsed.file} does not exist`);
|
||||
function readSentinelConfigRef(spec: HwlabRuntimeLaneSpec, key: HwlabRuntimeWebProbeSentinelConfigRefKey, ref: string): InternalConfigRefStatus {
|
||||
try {
|
||||
const text = readFileSync(absPath, "utf8");
|
||||
const sha256 = `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
||||
const doc = Bun.YAML.parse(text) as unknown;
|
||||
const target = valueAtPath(doc, parsed.path);
|
||||
const resolved = readWebProbeSentinelConfigRef(spec, ref);
|
||||
const target = resolved.target;
|
||||
const targetKind = target === undefined ? "missing" : targetKindOf(target);
|
||||
const missingFields = target === undefined ? ["target"] : missingFieldsForTarget(key, target);
|
||||
return {
|
||||
key,
|
||||
ref,
|
||||
file: parsed.file,
|
||||
path: parsed.path,
|
||||
file: resolved.file,
|
||||
path: resolved.path,
|
||||
present: true,
|
||||
targetPresent: target !== undefined,
|
||||
targetKind,
|
||||
sha256,
|
||||
byteCount: Buffer.byteLength(text),
|
||||
sha256: resolved.sha256,
|
||||
byteCount: resolved.byteCount,
|
||||
missingFields,
|
||||
conflicts: [],
|
||||
summary: summarizeTarget(key, target),
|
||||
@@ -285,21 +277,10 @@ function readSentinelConfigRef(key: HwlabRuntimeWebProbeSentinelConfigRefKey, re
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return emptyRefStatus(key, ref, parsed.file, parsed.path, message);
|
||||
return emptyRefStatus(key, ref, "", "", message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigRef(ref: string): { readonly file: string; readonly path: string; readonly error: string | null } {
|
||||
const [file, path, extra] = ref.split("#");
|
||||
if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
|
||||
return { file: file ?? "", path: path ?? "", error: "configRef must use path/to/file.yaml#object.path" };
|
||||
}
|
||||
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
|
||||
return { file, path, error: "configRef file must be repo-relative config/*.yaml without .." };
|
||||
}
|
||||
return { file, path, error: null };
|
||||
}
|
||||
|
||||
function emptyRefStatus(key: HwlabRuntimeWebProbeSentinelConfigRefKey, ref: string, file: string, path: string, error: string): InternalConfigRefStatus {
|
||||
return {
|
||||
key,
|
||||
@@ -348,8 +329,6 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
requireEquals(conflicts, byKey.get("runtime"), "namespace", spec.runtimeNamespace, `selected namespace ${spec.runtimeNamespace}`);
|
||||
}
|
||||
|
||||
const promptSetRef = byKey.get("promptSet")?.ref ?? null;
|
||||
const reportViewsRef = byKey.get("reportViews")?.ref ?? null;
|
||||
const scenarioIds = new Set<string>();
|
||||
const scenarioProviders = new Set<string>();
|
||||
for (const [index, scenario] of scenarios.entries()) {
|
||||
@@ -357,8 +336,8 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
if (id !== null) scenarioIds.add(id);
|
||||
const provider = stringAt(scenario, "providerProfile");
|
||||
if (provider !== null) scenarioProviders.add(provider);
|
||||
if (promptSetRef !== null) requireEquals(conflicts, byKey.get("scenarios"), `[${index}].promptSetRef`, promptSetRef, "promptSet configRef");
|
||||
if (reportViewsRef !== null) requireEquals(conflicts, byKey.get("scenarios"), `[${index}].reportViewRef`, reportViewsRef, "reportViews configRef");
|
||||
requireReadableConfigRef(spec, conflicts, byKey.get("scenarios"), `[${index}].promptSetRef`, scenario.promptSetRef);
|
||||
requireReadableConfigRef(spec, conflicts, byKey.get("scenarios"), `[${index}].reportViewRef`, scenario.reportViewRef);
|
||||
}
|
||||
|
||||
const promptProvider = promptSet === null ? null : stringAt(promptSet, "providerProfile");
|
||||
@@ -384,6 +363,15 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
function requireReadableConfigRef(spec: HwlabRuntimeLaneSpec, conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, value: unknown): void {
|
||||
if (typeof value !== "string" || value.length === 0) return;
|
||||
try {
|
||||
readWebProbeSentinelConfigRef(spec, value);
|
||||
} catch (error) {
|
||||
conflicts.push(`${ref?.file ?? "-"}#${ref?.path ?? "-"}.${path}=${value} is not readable: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireEquals(conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, expected: string, expectedLabel: string): void {
|
||||
if (ref === undefined) return;
|
||||
const actual = stringAt(ref.target, path);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
// Responsibility: Resolve YAML-first web-probe sentinel registry entries into one selected sentinel config graph.
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
import { readWebProbeSentinelConfigRefTarget, type WebProbeSentinelTemplateContext } from "./hwlab-node-web-sentinel-config-ref";
|
||||
import { HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeSentinelConfigRefKey, type HwlabRuntimeWebProbeSentinelRegistryItemSpec } from "./hwlab-node-lanes";
|
||||
|
||||
export interface ResolvedWebProbeSentinel {
|
||||
@@ -110,7 +111,7 @@ function resolveRegistrySentinel(spec: HwlabRuntimeLaneSpec, registry: readonly
|
||||
const ids = registry.map((item) => item.id).join(", ");
|
||||
throw new Error(`unknown web-probe sentinel ${sentinelId ?? "-"} for ${spec.nodeId}/${spec.lane}; available: ${ids}`);
|
||||
}
|
||||
const target = readConfigRefRecord(selected.configRef);
|
||||
const target = readConfigRefRecord(selected.configRef, spec);
|
||||
const targetId = optionalStringAt(target, "id") ?? selected.id;
|
||||
if (targetId !== selected.id) {
|
||||
throw new Error(`${selected.configRef}.id=${targetId} does not match registry id ${selected.id}`);
|
||||
@@ -151,13 +152,14 @@ function normalizeSentinelConfigRefs(target: Record<string, unknown>, ref: strin
|
||||
return Object.fromEntries(HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS.map((key) => [key, normalized[key]])) as Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
|
||||
}
|
||||
|
||||
function readConfigRefRecord(ref: string): Record<string, unknown> {
|
||||
const target = readConfigRefTarget(ref);
|
||||
function readConfigRefRecord(ref: string, context: WebProbeSentinelTemplateContext): Record<string, unknown> {
|
||||
const target = readConfigRefTarget(ref, context);
|
||||
if (!isRecord(target)) throw new Error(`${ref} must point to a YAML object`);
|
||||
return target;
|
||||
}
|
||||
|
||||
export function readConfigRefTarget(ref: string): unknown {
|
||||
export function readConfigRefTarget(ref: string, context?: WebProbeSentinelTemplateContext): unknown {
|
||||
if (context !== undefined) return readWebProbeSentinelConfigRefTarget(context, ref);
|
||||
const [file, path, extra] = ref.split("#");
|
||||
if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
|
||||
throw new Error(`${ref} must use path/to/file.yaml#object.path syntax`);
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
// Responsibility: Persistent HTTP wrapper service for web-probe observe scheduling, index, health, metrics, maintenance, and dashboard.
|
||||
import { Buffer } from "node:buffer";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { rootPath } from "./config";
|
||||
import { renderWebProbeSentinelDashboardHtml, webProbeSentinelDashboardAssetResponse } from "./hwlab-node-web-sentinel-dashboard-assets";
|
||||
import { webProbeSentinelConfigPlan, type WebProbeSentinelConfigPlan } from "./hwlab-node-web-sentinel-config";
|
||||
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||
@@ -91,12 +90,12 @@ export interface WebProbeSentinelService {
|
||||
export function loadWebProbeSentinelServiceConfig(spec: HwlabRuntimeLaneSpec, options: Omit<WebProbeSentinelServiceOptions, "spec"> = {}): WebProbeSentinelServiceConfig {
|
||||
const sentinel = resolveWebProbeSentinel(spec, options.sentinelId ?? null);
|
||||
const plan = webProbeSentinelConfigPlan(spec, "status", sentinel.id);
|
||||
const runtime = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.runtime));
|
||||
const scenarios = scenarioArrayTarget(readSentinelConfigRefTarget(sentinel.configRefs.scenarios));
|
||||
const reportViews = resolveReportViewsWithCheckCatalog(recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.reportViews)));
|
||||
const rawPublicExposure = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.publicExposure));
|
||||
const runtime = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.runtime, spec));
|
||||
const scenarios = scenarioArrayTarget(readSentinelConfigRefTarget(sentinel.configRefs.scenarios, spec));
|
||||
const reportViews = resolveReportViewsWithCheckCatalog(recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.reportViews, spec)), spec);
|
||||
const rawPublicExposure = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.publicExposure, spec));
|
||||
const publicExposure = effectiveWebProbeSentinelPublicExposure(spec, sentinel.id, rawPublicExposure);
|
||||
const cicd = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.cicd));
|
||||
const cicd = recordTarget(readSentinelConfigRefTarget(sentinel.configRefs.cicd, spec));
|
||||
const stateRoot = options.stateRootOverride ?? stringAt(runtime, "stateRoot");
|
||||
const yamlSqlitePath = stringAt(runtime, "sqlite.path");
|
||||
return {
|
||||
@@ -269,12 +268,12 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
|
||||
return service;
|
||||
}
|
||||
|
||||
function resolveReportViewsWithCheckCatalog(reportViews: Record<string, unknown>): Record<string, unknown> {
|
||||
function resolveReportViewsWithCheckCatalog(reportViews: Record<string, unknown>, spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
||||
const ref = stringOrNull(reportViews.checkCatalogRef);
|
||||
if (ref === null) return reportViews;
|
||||
return {
|
||||
...reportViews,
|
||||
checkCatalog: recordTarget(readSentinelConfigRefTarget(ref)),
|
||||
checkCatalog: recordTarget(readSentinelConfigRefTarget(ref, spec)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -694,13 +693,6 @@ function plannedRunBacklog(config: WebProbeSentinelServiceConfig, db: Database):
|
||||
};
|
||||
}
|
||||
|
||||
function readConfigRefTarget(ref: string): unknown {
|
||||
const [file, path] = ref.split("#");
|
||||
if (file === undefined || path === undefined) throw new Error(`invalid configRef: ${ref}`);
|
||||
const text = readFileSync(rootPath(file), "utf8");
|
||||
return valueAtPath(Bun.YAML.parse(text) as unknown, path);
|
||||
}
|
||||
|
||||
function writeMetadata(db: Database, key: string, value: unknown): void {
|
||||
db.query("INSERT INTO metadata (key, value_json, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at")
|
||||
.run(key, JSON.stringify(value), nowIso());
|
||||
|
||||
@@ -468,6 +468,15 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
|
||||
},
|
||||
};
|
||||
const patchB64 = Buffer.from(JSON.stringify(operation), "utf8").toString("base64");
|
||||
const staleOperationStatusPatch = {
|
||||
status: {
|
||||
operationState: {
|
||||
phase: "Failed",
|
||||
message: "terminated by unidesk control-plane sync stale-operation recovery",
|
||||
},
|
||||
},
|
||||
};
|
||||
const staleOperationStatusPatchB64 = Buffer.from(JSON.stringify(staleOperationStatusPatch), "utf8").toString("base64");
|
||||
const script = [
|
||||
"set +e",
|
||||
`app=${shellQuote(spec.app)}`,
|
||||
@@ -475,23 +484,38 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
|
||||
`runtime_namespace=${shellQuote(spec.runtimeNamespace)}`,
|
||||
`dry_run=${shellQuote(scoped.dryRun ? "true" : "false")}`,
|
||||
`patch_b64=${shellQuote(patchB64)}`,
|
||||
`stale_operation_status_patch_b64=${shellQuote(staleOperationStatusPatchB64)}`,
|
||||
"terminate_patch_file=$(mktemp /tmp/hwlab-node-argocd-terminate.XXXXXX.json)",
|
||||
"stale_operation_status_patch_file=$(mktemp /tmp/hwlab-node-argocd-stale-operation-status.XXXXXX.json)",
|
||||
"patch_file=$(mktemp /tmp/hwlab-node-argocd-sync.XXXXXX.json)",
|
||||
"printf '{\"operation\":null}' > \"$terminate_patch_file\"",
|
||||
"printf '%s' \"$stale_operation_status_patch_b64\" | base64 -d > \"$stale_operation_status_patch_file\"",
|
||||
"printf '%s' \"$patch_b64\" | base64 -d > \"$patch_file\"",
|
||||
"before=$(kubectl -n \"$argo_namespace\" get application \"$app\" -o 'jsonpath={.status.sync.status}{\"\\t\"}{.status.health.status}{\"\\t\"}{.status.sync.revision}{\"\\t\"}{.status.operationState.phase}{\"\\t\"}{.status.operationState.message}' 2>/dev/null || true)",
|
||||
"operation_phase=$(kubectl -n \"$argo_namespace\" get application \"$app\" -o 'jsonpath={.status.operationState.phase}' 2>/dev/null || true)",
|
||||
"terminate_code=0",
|
||||
"terminate_output=",
|
||||
"terminated_operation=false",
|
||||
"post_terminate_phase=",
|
||||
"stale_status_patch_code=0",
|
||||
"stale_status_patch_output=",
|
||||
"patched_stale_operation_status=false",
|
||||
"if [ \"$operation_phase\" = Running ]; then",
|
||||
" if [ \"$dry_run\" = true ]; then",
|
||||
" terminated_operation=would-terminate",
|
||||
" patched_stale_operation_status=would-patch",
|
||||
" else",
|
||||
" terminate_output=$(kubectl -n \"$argo_namespace\" patch application \"$app\" --type merge --patch-file \"$terminate_patch_file\" -o name 2>&1)",
|
||||
" terminate_code=$?",
|
||||
" if [ \"$terminate_code\" -eq 0 ]; then terminated_operation=true; else terminated_operation=failed; fi",
|
||||
" sleep 2",
|
||||
" post_terminate_phase=$(kubectl -n \"$argo_namespace\" get application \"$app\" -o 'jsonpath={.status.operationState.phase}' 2>/dev/null || true)",
|
||||
" if [ \"$terminate_code\" -eq 0 ] && [ \"$post_terminate_phase\" = Running ]; then",
|
||||
" stale_status_patch_output=$(kubectl -n \"$argo_namespace\" patch application \"$app\" --type merge --patch-file \"$stale_operation_status_patch_file\" -o name 2>&1)",
|
||||
" stale_status_patch_code=$?",
|
||||
" if [ \"$stale_status_patch_code\" -eq 0 ]; then patched_stale_operation_status=true; else patched_stale_operation_status=failed; fi",
|
||||
" sleep 1",
|
||||
" fi",
|
||||
" fi",
|
||||
"fi",
|
||||
"failed_hook_count=0",
|
||||
@@ -570,6 +594,10 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
|
||||
"printf 'terminatedOperation\\t%s\\n' \"$terminated_operation\"",
|
||||
"printf 'terminateExitCode\\t%s\\n' \"$terminate_code\"",
|
||||
"printf 'terminateOutput\\t%s\\n' \"$(printf '%s' \"$terminate_output\" | tr '\\n\\t' ' ' | cut -c1-500)\"",
|
||||
"printf 'postTerminatePhase\\t%s\\n' \"$post_terminate_phase\"",
|
||||
"printf 'patchedStaleOperationStatus\\t%s\\n' \"$patched_stale_operation_status\"",
|
||||
"printf 'staleStatusPatchExitCode\\t%s\\n' \"$stale_status_patch_code\"",
|
||||
"printf 'staleStatusPatchOutput\\t%s\\n' \"$(printf '%s' \"$stale_status_patch_output\" | tr '\\n\\t' ' ' | cut -c1-500)\"",
|
||||
"printf 'failedHookCount\\t%s\\n' \"$failed_hook_count\"",
|
||||
"printf 'failedHooks\\t%s\\n' \"$failed_hooks\"",
|
||||
"printf 'deletedHookCount\\t%s\\n' \"$deleted_hook_count\"",
|
||||
@@ -582,7 +610,7 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
|
||||
"printf 'staleStatefulSetPodDeleteErrors\\t%s\\n' \"$stale_statefulset_pod_delete_errors\"",
|
||||
"printf 'patchExitCode\\t%s\\n' \"$code\"",
|
||||
"printf 'patchOutput\\t%s\\n' \"$(printf '%s' \"$output\" | tr '\\n\\t' ' ' | cut -c1-500)\"",
|
||||
"rm -f \"$patch_file\" \"$terminate_patch_file\"",
|
||||
"rm -f \"$patch_file\" \"$terminate_patch_file\" \"$stale_operation_status_patch_file\"",
|
||||
"exit \"$code\"",
|
||||
].join("\n");
|
||||
const result = runNodeK3sScript(spec, script, scoped.timeoutSeconds);
|
||||
@@ -608,6 +636,10 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
|
||||
terminatedOperation: fields.terminatedOperation || "false",
|
||||
terminateExitCode: numericField(fields.terminateExitCode),
|
||||
terminateOutput: fields.terminateOutput || null,
|
||||
postTerminatePhase: fields.postTerminatePhase || null,
|
||||
patchedStaleOperationStatus: fields.patchedStaleOperationStatus || "false",
|
||||
staleStatusPatchExitCode: numericField(fields.staleStatusPatchExitCode),
|
||||
staleStatusPatchOutput: fields.staleStatusPatchOutput || null,
|
||||
failedHookCount: numericField(fields.failedHookCount),
|
||||
failedHooks: commaListField(fields.failedHooks),
|
||||
deletedHookCount: numericField(fields.deletedHookCount),
|
||||
|
||||
@@ -306,8 +306,14 @@ export interface NodeSecretOptions {
|
||||
|
||||
export interface BootstrapAdminSecretMaterial {
|
||||
ok: boolean;
|
||||
username: string | null;
|
||||
usernameSourceRef: string | null;
|
||||
usernameSourceKey: string | null;
|
||||
usernameSourceLine: number | null;
|
||||
usernameFingerprint: string | null;
|
||||
sourceRef: string | null;
|
||||
sourceKey: string | null;
|
||||
sourceLine: number | null;
|
||||
sourcePath: string | null;
|
||||
sourcePresent: boolean;
|
||||
sourceFingerprint: string | null;
|
||||
@@ -317,8 +323,14 @@ export interface BootstrapAdminSecretMaterial {
|
||||
|
||||
export interface BootstrapAdminPasswordMaterial {
|
||||
ok: boolean;
|
||||
username: string | null;
|
||||
usernameSourceRef: string | null;
|
||||
usernameSourceKey: string | null;
|
||||
usernameSourceLine: number | null;
|
||||
usernameFingerprint: string | null;
|
||||
sourceRef: string | null;
|
||||
sourceKey: string | null;
|
||||
sourceLine: number | null;
|
||||
sourcePath: string | null;
|
||||
sourcePresent: boolean;
|
||||
sourceFingerprint: string | null;
|
||||
@@ -372,8 +384,12 @@ export interface RuntimeSecretSpec {
|
||||
bootstrapAdminPasswordHashKey: string;
|
||||
bootstrapAdminUsername: string;
|
||||
bootstrapAdminDisplayName: string;
|
||||
bootstrapAdminUsernameSourceRef?: string;
|
||||
bootstrapAdminUsernameSourceKey?: string;
|
||||
bootstrapAdminUsernameSourceLine?: number;
|
||||
bootstrapAdminPasswordSourceRef?: string;
|
||||
bootstrapAdminPasswordSourceKey?: string;
|
||||
bootstrapAdminPasswordSourceLine?: number;
|
||||
bootstrapAdminPasswordHashTransform?: "hwlab-sha256";
|
||||
bootstrapAdminSourceNamespace: string;
|
||||
bootstrapAdminSourceSecret: string;
|
||||
@@ -442,6 +458,14 @@ export type NodeRuntimeGitMirrorGithubTransportSpec =
|
||||
|
||||
export type NodeRuntimeGitMirrorEgressProxySpec =
|
||||
| { mode: "direct"; required: false }
|
||||
| {
|
||||
mode: "host-route";
|
||||
clientName: string;
|
||||
hostProxyConfigRef: string;
|
||||
proxyEnvPath: string;
|
||||
proxyUrl: string;
|
||||
noProxy: string[];
|
||||
}
|
||||
| {
|
||||
mode: "k8s-service-cluster-ip";
|
||||
clientName: string;
|
||||
|
||||
@@ -256,7 +256,7 @@ export function nodeObservabilityPerformanceSummary(options: NodeObservabilityOp
|
||||
};
|
||||
}
|
||||
const timeoutSeconds = Math.max(10, Math.min(options.timeoutSeconds, 55));
|
||||
const script = nodePerformanceSummaryRemoteScript(options.spec.publicWebUrl, summaryPath, secretSpec.bootstrapAdminUsername, material.password);
|
||||
const script = nodePerformanceSummaryRemoteScript(options.spec.publicWebUrl, summaryPath, material.username ?? secretSpec.bootstrapAdminUsername, material.password);
|
||||
const result = runTransWorkspaceStdinScript(options.node, options.spec.workspace, script, timeoutSeconds);
|
||||
const report = parseJsonRecordFromText(result.stdout);
|
||||
const performance = record(report.performance);
|
||||
|
||||
@@ -43,6 +43,7 @@ export function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, arg
|
||||
dryRun: boolean;
|
||||
wait: boolean;
|
||||
rerun: boolean;
|
||||
discardStaleGitops: boolean;
|
||||
allowLiveDbRead: boolean;
|
||||
timeoutSeconds: number;
|
||||
originalArgs: string[];
|
||||
@@ -69,6 +70,7 @@ export function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, arg
|
||||
dryRun,
|
||||
wait: args.includes("--wait"),
|
||||
rerun: args.includes("--rerun"),
|
||||
discardStaleGitops: args.includes("--discard-stale-gitops"),
|
||||
allowLiveDbRead: args.includes("--allow-live-db-read"),
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS, 3600),
|
||||
originalArgs: [...args],
|
||||
|
||||
@@ -228,8 +228,12 @@ export function runtimeSecretSpec(input: { node: string; lane: string }): Runtim
|
||||
bootstrapAdminPasswordHashKey: bootstrapAdmin?.secretKey ?? BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
|
||||
bootstrapAdminUsername: bootstrapAdmin?.username ?? "admin",
|
||||
bootstrapAdminDisplayName: bootstrapAdmin?.displayName ?? `HWLAB ${input.lane} Admin`,
|
||||
...(bootstrapAdmin?.usernameSourceRef === undefined ? {} : { bootstrapAdminUsernameSourceRef: bootstrapAdmin.usernameSourceRef }),
|
||||
...(bootstrapAdmin?.usernameSourceKey === undefined ? {} : { bootstrapAdminUsernameSourceKey: bootstrapAdmin.usernameSourceKey }),
|
||||
...(bootstrapAdmin?.usernameSourceLine === undefined ? {} : { bootstrapAdminUsernameSourceLine: bootstrapAdmin.usernameSourceLine }),
|
||||
...(bootstrapAdmin?.passwordSourceRef === undefined ? {} : { bootstrapAdminPasswordSourceRef: bootstrapAdmin.passwordSourceRef }),
|
||||
...(bootstrapAdmin?.passwordSourceKey === undefined ? {} : { bootstrapAdminPasswordSourceKey: bootstrapAdmin.passwordSourceKey }),
|
||||
...(bootstrapAdmin?.passwordSourceLine === undefined ? {} : { bootstrapAdminPasswordSourceLine: bootstrapAdmin.passwordSourceLine }),
|
||||
...(bootstrapAdmin?.passwordHashTransform === undefined ? {} : { bootstrapAdminPasswordHashTransform: bootstrapAdmin.passwordHashTransform }),
|
||||
bootstrapAdminSourceNamespace: BOOTSTRAP_ADMIN_SOURCE_NAMESPACE,
|
||||
bootstrapAdminSourceSecret: BOOTSTRAP_ADMIN_SOURCE_SECRET,
|
||||
|
||||
@@ -45,7 +45,12 @@ export function nodeRuntimeGitMirrorJobName(mirror: NodeRuntimeGitMirrorTargetSp
|
||||
return `${prefix}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTargetSpec, action: "sync" | "flush", jobName: string): Record<string, unknown> {
|
||||
export function nodeRuntimeGitMirrorJobManifest(
|
||||
mirror: NodeRuntimeGitMirrorTargetSpec,
|
||||
action: "sync" | "flush",
|
||||
jobName: string,
|
||||
options: { discardStaleGitops?: boolean } = {},
|
||||
): Record<string, unknown> {
|
||||
const volumes: Record<string, unknown>[] = [
|
||||
{ name: "cache", ...(mirror.cacheHostPath === null ? { persistentVolumeClaim: { claimName: mirror.cachePvcName } } : { hostPath: { path: mirror.cacheHostPath, type: "DirectoryOrCreate" } }) },
|
||||
{ name: "script", configMap: { name: mirror.syncConfigMapName, defaultMode: 0o755 } },
|
||||
@@ -88,6 +93,7 @@ export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTarg
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
...(mirror.egressProxy.mode === "host-route" ? { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet" } : {}),
|
||||
restartPolicy: "Never",
|
||||
volumes,
|
||||
containers: [{
|
||||
@@ -95,7 +101,11 @@ export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTarg
|
||||
image: mirror.toolsImage,
|
||||
imagePullPolicy: mirror.toolsImagePullPolicy,
|
||||
command: [action === "sync" ? "/script/sync.sh" : "/script/flush.sh"],
|
||||
env: [...nodeRuntimeGitMirrorProxyEnv(mirror), ...nodeRuntimeGitMirrorGithubTransportEnv(mirror)],
|
||||
env: [
|
||||
...nodeRuntimeGitMirrorProxyEnv(mirror),
|
||||
...nodeRuntimeGitMirrorGithubTransportEnv(mirror),
|
||||
...(options.discardStaleGitops === true ? [{ name: "UNIDESK_GIT_MIRROR_DISCARD_STALE_GITOPS", value: "true" }] : []),
|
||||
],
|
||||
volumeMounts,
|
||||
}],
|
||||
},
|
||||
@@ -107,7 +117,7 @@ export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTarg
|
||||
export function nodeRuntimeGitMirrorProxyEnv(mirror: NodeRuntimeGitMirrorTargetSpec): Record<string, unknown>[] {
|
||||
const proxy = mirror.egressProxy;
|
||||
if (proxy.mode === "direct") return [];
|
||||
const proxyUrl = `http://${proxy.serviceName}.${proxy.namespace}.svc.cluster.local:${proxy.port}`;
|
||||
const proxyUrl = proxy.mode === "host-route" ? proxy.proxyUrl : `http://${proxy.serviceName}.${proxy.namespace}.svc.cluster.local:${proxy.port}`;
|
||||
const noProxy = proxy.noProxy.join(",");
|
||||
return [
|
||||
{ name: "HTTP_PROXY", value: proxyUrl },
|
||||
@@ -1101,7 +1111,7 @@ export function renderNodeRuntimeControlPlaneOnNode(spec: HwlabRuntimeLaneSpec,
|
||||
"};",
|
||||
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
|
||||
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
"if (overlay.gitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.gitMirror;",
|
||||
"if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;",
|
||||
"fs.writeFileSync(path, YAML.stringify(doc));",
|
||||
"NODE",
|
||||
"if [ -f scripts/gitops-render.mjs ]; then render_script=scripts/gitops-render.mjs; else echo 'render script missing: scripts/gitops-render.mjs' >&2; exit 43; fi",
|
||||
@@ -1186,7 +1196,7 @@ export function renderNodeRuntimeControlPlaneLocal(spec: HwlabRuntimeLaneSpec, s
|
||||
"};",
|
||||
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
|
||||
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
"if (overlay.gitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.gitMirror;",
|
||||
"if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;",
|
||||
"fs.writeFileSync(path, YAML.stringify(doc));",
|
||||
"NODE",
|
||||
"if [ -f scripts/gitops-render.mjs ]; then render_script=scripts/gitops-render.mjs; else echo 'render script missing: scripts/gitops-render.mjs' >&2; exit 43; fi",
|
||||
@@ -1283,6 +1293,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" runtimeStore: overlay.runtimeStore,",
|
||||
" codeAgentRuntime: overlay.codeAgentRuntime,",
|
||||
" observability: overlay.observability,",
|
||||
" deployYamlGitMirror: overlay.deployYamlGitMirror,",
|
||||
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
|
||||
" dockerProxyHttp: overlay.dockerProxyHttp,",
|
||||
" dockerProxyHttps: overlay.dockerProxyHttps,",
|
||||
@@ -1332,6 +1343,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
"};",
|
||||
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
|
||||
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
|
||||
"if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;",
|
||||
"fs.writeFileSync(file, YAML.stringify(doc));",
|
||||
"console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));",
|
||||
"NODE_UNIDESK_DEPLOY_YAML_OVERLAY`;",
|
||||
@@ -2200,6 +2212,117 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (!changed) throw new Error(`generated git-mirror ConfigMap ${configMapName} was not found in ${gitMirrorFile}`);",
|
||||
" fs.writeFileSync(gitMirrorFile, docs.map((doc) => YAML.stringify(doc).trimEnd()).join('\\n---\\n') + '\\n');",
|
||||
"}",
|
||||
"function patchGitMirrorHostRouteYaml() {",
|
||||
" const mirror = overlay.gitMirror || {};",
|
||||
" const proxy = mirror.egressProxy || {};",
|
||||
" if (proxy.mode !== 'host-route') return;",
|
||||
" if (!YAML) throw new Error('yaml module is required to patch git-mirror host-route proxy');",
|
||||
" const requireString = (pathName, value) => {",
|
||||
" if (typeof value !== 'string' || value.length === 0) throw new Error('overlay.' + pathName + ' is required for git-mirror host-route proxy');",
|
||||
" return value;",
|
||||
" };",
|
||||
" const proxyUrl = requireString('gitMirror.egressProxy.proxyUrl', proxy.proxyUrl);",
|
||||
" const configMapName = requireString('gitMirror.syncConfigMapName', mirror.syncConfigMapName);",
|
||||
" let proxyEndpoint;",
|
||||
" try { proxyEndpoint = new URL(proxyUrl); } catch (error) { throw new Error(`overlay.gitMirror.egressProxy.proxyUrl is invalid: ${error.message}`); }",
|
||||
" if (proxyEndpoint.protocol !== 'http:') throw new Error('overlay.gitMirror.egressProxy.proxyUrl must use http:// for host-route');",
|
||||
" const proxyHost = proxyEndpoint.hostname;",
|
||||
" const proxyPort = Number(proxyEndpoint.port || '80');",
|
||||
" if (!proxyHost || !Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65535) throw new Error('overlay.gitMirror.egressProxy.proxyUrl must include a valid host and port');",
|
||||
" const noProxy = Array.isArray(proxy.noProxy) ? proxy.noProxy.join(',') : '';",
|
||||
" const envEntries = [",
|
||||
" ['HTTP_PROXY', proxyUrl],",
|
||||
" ['HTTPS_PROXY', proxyUrl],",
|
||||
" ['ALL_PROXY', proxyUrl],",
|
||||
" ['http_proxy', proxyUrl],",
|
||||
" ['https_proxy', proxyUrl],",
|
||||
" ['all_proxy', proxyUrl],",
|
||||
" ['NO_PROXY', noProxy],",
|
||||
" ['no_proxy', noProxy],",
|
||||
" ];",
|
||||
" function readDocs(file) { return YAML.parseAllDocuments(fs.readFileSync(file, 'utf8')).map((document) => document.toJS()).filter((doc) => doc !== null); }",
|
||||
" function flattenDocs(docs) {",
|
||||
" const manifests = [];",
|
||||
" for (const doc of docs) {",
|
||||
" if (doc && typeof doc === 'object' && doc.kind === 'List' && Array.isArray(doc.items)) manifests.push(...doc.items);",
|
||||
" else manifests.push(doc);",
|
||||
" }",
|
||||
" return manifests;",
|
||||
" }",
|
||||
" function setContainerEnv(container, name, value) {",
|
||||
" if (!container || typeof container !== 'object') return false;",
|
||||
" container.env = Array.isArray(container.env) ? container.env : [];",
|
||||
" let item = container.env.find((env) => env && env.name === name);",
|
||||
" if (!item) { item = { name }; container.env.push(item); }",
|
||||
" const changed = item.value !== value || item.valueFrom !== undefined;",
|
||||
" item.value = value;",
|
||||
" delete item.valueFrom;",
|
||||
" return changed;",
|
||||
" }",
|
||||
" function patchProxyScript(script, key) {",
|
||||
" let next = String(script || '');",
|
||||
" if (next.length === 0) throw new Error(`generated git-mirror ConfigMap ${configMapName} missing ${key}`);",
|
||||
" next = next.replace(/node \\/tmp\\/hwlab-github-proxy-connect\\.(?:mjs|cjs) \\S+ \\d+/g, `node /tmp/hwlab-github-proxy-connect.mjs ${proxyHost} ${proxyPort}`);",
|
||||
" const missing = [];",
|
||||
" for (const [name, value] of envEntries) {",
|
||||
" let replaced = false;",
|
||||
" const exportPattern = new RegExp(`(^|\\\\n)([ \\\\t]*export ${escapeRegExp(name)}=)[^\\\\n]*`, 'g');",
|
||||
" next = next.replace(exportPattern, (_match, prefix, left) => { replaced = true; return `${prefix}${left}${shellSingle(value)}`; });",
|
||||
" if (!replaced) missing.push([name, value]);",
|
||||
" }",
|
||||
" if (missing.length > 0) {",
|
||||
" const block = missing.map(([name, value]) => `export ${name}=${shellSingle(value)}`).join('\\\\n');",
|
||||
" if (next.includes('\\\\nset -eu\\\\n')) next = next.replace('\\\\nset -eu\\\\n', `\\\\nset -eu\\\\n${block}\\\\n`);",
|
||||
" else next = `${block}\\\\n${next}`;",
|
||||
" }",
|
||||
" next = next.replace(/mode=node-global/g, 'mode=host-route');",
|
||||
" return next;",
|
||||
" }",
|
||||
" const gitMirrorFile = path.join(renderDir, 'devops-infra', 'git-mirror.yaml');",
|
||||
" if (!fs.existsSync(gitMirrorFile)) throw new Error(`generated git-mirror manifest missing: ${gitMirrorFile}`);",
|
||||
" const docs = readDocs(gitMirrorFile);",
|
||||
" const manifests = flattenDocs(docs);",
|
||||
" let configMapChanged = false;",
|
||||
" let workloadChanged = false;",
|
||||
" const workloadNames = new Set([mirror.serviceReadName, mirror.serviceWriteName].filter((value) => typeof value === 'string' && value.length > 0));",
|
||||
" for (const doc of manifests) {",
|
||||
" if (!doc || typeof doc !== 'object') continue;",
|
||||
" if (doc.kind === 'ConfigMap' && doc.metadata && doc.metadata.name === configMapName) {",
|
||||
" doc.data = doc.data || {};",
|
||||
" doc.data['sync.sh'] = patchProxyScript(doc.data['sync.sh'], 'sync.sh');",
|
||||
" doc.data['flush.sh'] = patchProxyScript(doc.data['flush.sh'], 'flush.sh');",
|
||||
" configMapChanged = true;",
|
||||
" continue;",
|
||||
" }",
|
||||
" if (!['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'ReplicationController'].includes(doc.kind)) continue;",
|
||||
" const name = doc.metadata && doc.metadata.name ? String(doc.metadata.name) : '';",
|
||||
" const labels = doc.metadata && doc.metadata.labels && typeof doc.metadata.labels === 'object' ? doc.metadata.labels : {};",
|
||||
" if (!workloadNames.has(name) && labels['app.kubernetes.io/name'] !== 'git-mirror' && !name.includes('git-mirror')) continue;",
|
||||
" doc.spec = doc.spec || {};",
|
||||
" doc.spec.template = doc.spec.template || {};",
|
||||
" doc.spec.template.metadata = doc.spec.template.metadata || {};",
|
||||
" doc.spec.template.metadata.annotations = doc.spec.template.metadata.annotations || {};",
|
||||
" doc.spec.template.metadata.annotations['unidesk.ai/git-mirror-egress-proxy'] = 'host-route';",
|
||||
" doc.spec.template.metadata.annotations['unidesk.ai/git-mirror-host-proxy-config-ref'] = String(proxy.hostProxyConfigRef || '');",
|
||||
" doc.spec.template.spec = doc.spec.template.spec || {};",
|
||||
" if (proxy.podHostNetwork) {",
|
||||
" doc.spec.template.spec.hostNetwork = true;",
|
||||
" doc.spec.template.spec.dnsPolicy = 'ClusterFirstWithHostNet';",
|
||||
" } else {",
|
||||
" delete doc.spec.template.spec.hostNetwork;",
|
||||
" delete doc.spec.template.spec.dnsPolicy;",
|
||||
" }",
|
||||
" for (const group of ['containers', 'initContainers']) {",
|
||||
" for (const container of Array.isArray(doc.spec.template.spec[group]) ? doc.spec.template.spec[group] : []) {",
|
||||
" for (const [envName, envValue] of envEntries) setContainerEnv(container, envName, envValue);",
|
||||
" }",
|
||||
" }",
|
||||
" workloadChanged = true;",
|
||||
" }",
|
||||
" if (!configMapChanged) throw new Error(`generated git-mirror ConfigMap ${configMapName} was not found in ${gitMirrorFile}`);",
|
||||
" if (!workloadChanged) throw new Error(`generated git-mirror workload for host-route proxy was not found in ${gitMirrorFile}`);",
|
||||
" fs.writeFileSync(gitMirrorFile, docs.map((doc) => YAML.stringify(doc).trimEnd()).join('\\n---\\n') + '\\n');",
|
||||
"}",
|
||||
"const structured = patchStructuredPipeline();",
|
||||
"function replaceParamDefault(name, value) {",
|
||||
" const namePattern = escapeRegExp(name);",
|
||||
@@ -2253,6 +2376,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
"if (text.includes('/yaml/-/yaml-') || text.includes('bun add --no-save --ignore-scripts') || text.includes('npm install --package-lock=false --no-save')) { throw new Error(`generated pipeline still downloads yaml during prepare-source in ${pipelinePath}`); }",
|
||||
"if (text.includes('npm run gitops:ts:check')) { throw new Error(`generated pipeline still uses npm gitops:ts:check gate in ${pipelinePath}`); }",
|
||||
"fs.writeFileSync(pipelinePath, text);",
|
||||
"patchGitMirrorHostRouteYaml();",
|
||||
"patchGitMirrorTransportYaml();",
|
||||
"function patchArgoYaml(filePath) {",
|
||||
" if (!YAML || !fs.existsSync(filePath)) return;",
|
||||
|
||||
@@ -292,28 +292,48 @@ export function runNodeHostScriptAsync(spec: HwlabRuntimeLaneSpec, script: strin
|
||||
return commandResultFromAsync(spec, payload, statusPath, false);
|
||||
}
|
||||
}
|
||||
const kill = runNodeHostScript(spec, [
|
||||
const pending = runNodeHostScript(spec, [
|
||||
"set +e",
|
||||
`status_path=${shellQuote(statusPath)}`,
|
||||
"pid=$(python3 - \"$status_path\" <<'PY' 2>/dev/null",
|
||||
"import json, sys",
|
||||
"try: print(json.load(open(sys.argv[1])).get('pid') or '')",
|
||||
"except Exception: print('')",
|
||||
`stdout_path=${shellQuote(stdoutPath)}`,
|
||||
`stderr_path=${shellQuote(stderrPath)}`,
|
||||
`timeout_seconds=${shellQuote(String(timeoutSeconds))}`,
|
||||
"python3 - \"$status_path\" \"$stdout_path\" \"$stderr_path\" \"$timeout_seconds\" <<'PY'",
|
||||
"import datetime, json, pathlib, subprocess, sys",
|
||||
"status_path, stdout_path, stderr_path, timeout_seconds = sys.argv[1:5]",
|
||||
"try:",
|
||||
" payload = json.load(open(status_path))",
|
||||
"except Exception:",
|
||||
" payload = {'state': 'missing', 'ok': False, 'exitCode': 127}",
|
||||
"def tail(path):",
|
||||
" try:",
|
||||
" return pathlib.Path(path).read_text(errors='replace')[-12000:]",
|
||||
" except FileNotFoundError:",
|
||||
" return ''",
|
||||
"pid = payload.get('pid')",
|
||||
"pid_alive = None",
|
||||
"if isinstance(pid, int) and pid > 0:",
|
||||
" pid_alive = subprocess.run(['kill', '-0', str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0",
|
||||
"payload['stdout'] = tail(stdout_path)",
|
||||
"stderr = tail(stderr_path)",
|
||||
"pending_note = f\"remote async command still pending after {timeout_seconds}s; statusPath={status_path}; pidAlive={pid_alive}\"",
|
||||
"payload['stderr'] = '\\n'.join([item for item in [pending_note, stderr] if item])",
|
||||
"payload['timedOutAt'] = datetime.datetime.utcnow().isoformat() + 'Z'",
|
||||
"payload['pidAlive'] = pid_alive",
|
||||
"json.dump(payload, sys.stdout, indent=2)",
|
||||
"PY",
|
||||
")",
|
||||
"if [ -n \"$pid\" ]; then kill \"$pid\" 2>/dev/null || true; fi",
|
||||
"cat \"$status_path\" 2>/dev/null || true",
|
||||
].join("\n"), 55);
|
||||
payload = parseJsonObject(pending.stdout.trim());
|
||||
return {
|
||||
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: 124,
|
||||
stdout: typeof payload.stdout === "string" ? payload.stdout : "",
|
||||
stdout: typeof payload.stdout === "string" ? payload.stdout : pending.stdout,
|
||||
stderr: [
|
||||
`remote async command timed out after ${timeoutSeconds}s; statusPath=${statusPath}`,
|
||||
`remote async command pending after ${timeoutSeconds}s; statusPath=${statusPath}`,
|
||||
typeof payload.stderr === "string" ? payload.stderr : "",
|
||||
lastStatus?.stderr.trim() ?? "",
|
||||
kill.stderr.trim(),
|
||||
pending.stderr.trim(),
|
||||
].filter(Boolean).join("\n"),
|
||||
signal: null,
|
||||
timedOut: true,
|
||||
|
||||
@@ -1051,10 +1051,15 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
`namespace=${shellQuote(spec.namespace)}`,
|
||||
`name=${shellQuote(spec.bootstrapAdminSecret)}`,
|
||||
`password_hash_key=${shellQuote(spec.bootstrapAdminPasswordHashKey)}`,
|
||||
`username=${shellQuote(spec.bootstrapAdminUsername)}`,
|
||||
`username=${shellQuote(material?.username ?? spec.bootstrapAdminUsername)}`,
|
||||
`display_name=${shellQuote(spec.bootstrapAdminDisplayName)}`,
|
||||
`username_source_ref=${shellQuote(material?.usernameSourceRef ?? "")}`,
|
||||
`username_source_key=${shellQuote(material?.usernameSourceKey ?? "")}`,
|
||||
`username_source_line=${shellQuote(material?.usernameSourceLine === null || material?.usernameSourceLine === undefined ? "" : String(material.usernameSourceLine))}`,
|
||||
`username_source_fingerprint=${shellQuote(material?.usernameFingerprint ?? "")}`,
|
||||
`source_ref=${shellQuote(spec.bootstrapAdminPasswordSourceRef ?? "")}`,
|
||||
`source_key=${shellQuote(spec.bootstrapAdminPasswordSourceKey ?? "")}`,
|
||||
`source_line=${shellQuote(material?.sourceLine === null || material?.sourceLine === undefined ? "" : String(material.sourceLine))}`,
|
||||
`source_path=${shellQuote(material?.sourcePath === null || material?.sourcePath === undefined ? "" : displayRepoPath(material.sourcePath))}`,
|
||||
`source_present=${shellQuote(material?.sourcePresent === true ? "yes" : "no")}`,
|
||||
`source_fingerprint=${shellQuote(material?.sourceFingerprint ?? "")}`,
|
||||
@@ -1075,8 +1080,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
"before_hash_b64=$(secret_b64_key \"$password_hash_key\")",
|
||||
"before_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-ref)",
|
||||
"before_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-key)",
|
||||
"before_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-line)",
|
||||
"before_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-fingerprint)",
|
||||
"before_username=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username)",
|
||||
"before_username_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-ref)",
|
||||
"before_username_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-key)",
|
||||
"before_username_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-line)",
|
||||
"before_username_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-fingerprint)",
|
||||
"before_hash_present=$([ -n \"$before_hash_b64\" ] && printf yes || printf no)",
|
||||
"before_hash_bytes=$(decoded_length \"$before_hash_b64\")",
|
||||
"action=observed",
|
||||
@@ -1087,7 +1097,8 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
"if [ \"$action_request\" = ensure ]; then",
|
||||
" needs_sync=false",
|
||||
" [ \"$before_exists\" = yes ] && [ \"$before_hash_bytes\" -gt 0 ] || needs_sync=true",
|
||||
" [ \"$before_source_ref\" = \"$source_ref\" ] && [ \"$before_source_key\" = \"$source_key\" ] && [ \"$before_source_fingerprint\" = \"$source_fingerprint\" ] && [ \"$before_username\" = \"$username\" ] || needs_sync=true",
|
||||
" [ \"$before_source_ref\" = \"$source_ref\" ] && [ \"$before_source_key\" = \"$source_key\" ] && [ \"$before_source_line\" = \"$source_line\" ] && [ \"$before_source_fingerprint\" = \"$source_fingerprint\" ] && [ \"$before_username\" = \"$username\" ] || needs_sync=true",
|
||||
" [ \"$before_username_source_ref\" = \"$username_source_ref\" ] && [ \"$before_username_source_key\" = \"$username_source_key\" ] && [ \"$before_username_source_line\" = \"$username_source_line\" ] && [ \"$before_username_source_fingerprint\" = \"$username_source_fingerprint\" ] || needs_sync=true",
|
||||
" [ \"$force_sync\" = true ] && needs_sync=true",
|
||||
" if [ \"$material_ok\" != true ]; then",
|
||||
" action=${source_error:-secret-source-invalid}",
|
||||
@@ -1109,8 +1120,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
" annotations:",
|
||||
" hwlab.pikastech.local/bootstrap-admin-username: \"$username\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-display-name: \"$display_name\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-username-source-ref: \"$username_source_ref\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-username-source-key: \"$username_source_key\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-username-source-line: \"$username_source_line\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-username-source-fingerprint: \"$username_source_fingerprint\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-ref: \"$source_ref\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-key: \"$source_key\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-line: \"$source_line\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-source-fingerprint: \"$source_fingerprint\"",
|
||||
" hwlab.pikastech.local/bootstrap-admin-password-transform: \"$transform\"",
|
||||
" labels:",
|
||||
@@ -1140,8 +1156,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
"after_hash_b64=$(secret_b64_key \"$password_hash_key\")",
|
||||
"after_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-ref)",
|
||||
"after_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-key)",
|
||||
"after_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-line)",
|
||||
"after_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-fingerprint)",
|
||||
"after_username=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username)",
|
||||
"after_username_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-ref)",
|
||||
"after_username_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-key)",
|
||||
"after_username_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-line)",
|
||||
"after_username_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-fingerprint)",
|
||||
"after_hash_present=$([ -n \"$after_hash_b64\" ] && printf yes || printf no)",
|
||||
"after_hash_bytes=$(decoded_length \"$after_hash_b64\")",
|
||||
"printf 'namespace\\t%s\\n' \"$namespace\"",
|
||||
@@ -1150,8 +1171,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
"printf 'preset\\t%s\\n' \"$preset\"",
|
||||
"printf 'username\\t%s\\n' \"$username\"",
|
||||
"printf 'displayName\\t%s\\n' \"$display_name\"",
|
||||
"printf 'usernameSourceRef\\t%s\\n' \"$username_source_ref\"",
|
||||
"printf 'usernameSourceKey\\t%s\\n' \"$username_source_key\"",
|
||||
"printf 'usernameSourceLine\\t%s\\n' \"$username_source_line\"",
|
||||
"printf 'usernameSourceFingerprint\\t%s\\n' \"$username_source_fingerprint\"",
|
||||
"printf 'sourceRef\\t%s\\n' \"$source_ref\"",
|
||||
"printf 'sourceKey\\t%s\\n' \"$source_key\"",
|
||||
"printf 'sourceLine\\t%s\\n' \"$source_line\"",
|
||||
"printf 'sourcePath\\t%s\\n' \"$source_path\"",
|
||||
"printf 'sourceExists\\t%s\\n' \"$source_present\"",
|
||||
"printf 'sourceFingerprint\\t%s\\n' \"$source_fingerprint\"",
|
||||
@@ -1165,15 +1191,25 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
|
||||
"printf 'beforePasswordHashBytes\\t%s\\n' \"$before_hash_bytes\"",
|
||||
"printf 'beforeSourceRef\\t%s\\n' \"$before_source_ref\"",
|
||||
"printf 'beforeSourceKey\\t%s\\n' \"$before_source_key\"",
|
||||
"printf 'beforeSourceLine\\t%s\\n' \"$before_source_line\"",
|
||||
"printf 'beforeSourceFingerprint\\t%s\\n' \"$before_source_fingerprint\"",
|
||||
"printf 'beforeUsername\\t%s\\n' \"$before_username\"",
|
||||
"printf 'beforeUsernameSourceRef\\t%s\\n' \"$before_username_source_ref\"",
|
||||
"printf 'beforeUsernameSourceKey\\t%s\\n' \"$before_username_source_key\"",
|
||||
"printf 'beforeUsernameSourceLine\\t%s\\n' \"$before_username_source_line\"",
|
||||
"printf 'beforeUsernameSourceFingerprint\\t%s\\n' \"$before_username_source_fingerprint\"",
|
||||
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
|
||||
"printf 'afterPasswordHashPresent\\t%s\\n' \"$after_hash_present\"",
|
||||
"printf 'afterPasswordHashBytes\\t%s\\n' \"$after_hash_bytes\"",
|
||||
"printf 'afterSourceRef\\t%s\\n' \"$after_source_ref\"",
|
||||
"printf 'afterSourceKey\\t%s\\n' \"$after_source_key\"",
|
||||
"printf 'afterSourceLine\\t%s\\n' \"$after_source_line\"",
|
||||
"printf 'afterSourceFingerprint\\t%s\\n' \"$after_source_fingerprint\"",
|
||||
"printf 'afterUsername\\t%s\\n' \"$after_username\"",
|
||||
"printf 'afterUsernameSourceRef\\t%s\\n' \"$after_username_source_ref\"",
|
||||
"printf 'afterUsernameSourceKey\\t%s\\n' \"$after_username_source_key\"",
|
||||
"printf 'afterUsernameSourceLine\\t%s\\n' \"$after_username_source_line\"",
|
||||
"printf 'afterUsernameSourceFingerprint\\t%s\\n' \"$after_username_source_fingerprint\"",
|
||||
"printf 'cloudApiDeployment\\t%s\\n' \"$cloud_api_deployment\"",
|
||||
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
|
||||
"printf 'rolloutRestartExitCode\\t%s\\n' \"$rollout_restart_exit\"",
|
||||
@@ -1759,8 +1795,13 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
|
||||
fields.sourceFingerprint.length > 0 &&
|
||||
fields.afterSourceRef === fields.sourceRef &&
|
||||
fields.afterSourceKey === fields.sourceKey &&
|
||||
(fields.afterSourceLine || "") === (fields.sourceLine || "") &&
|
||||
fields.afterSourceFingerprint === fields.sourceFingerprint &&
|
||||
fields.afterUsername === fields.username
|
||||
fields.afterUsername === fields.username &&
|
||||
(fields.afterUsernameSourceRef || "") === (fields.usernameSourceRef || "") &&
|
||||
(fields.afterUsernameSourceKey || "") === (fields.usernameSourceKey || "") &&
|
||||
(fields.afterUsernameSourceLine || "") === (fields.usernameSourceLine || "") &&
|
||||
(fields.afterUsernameSourceFingerprint || "") === (fields.usernameSourceFingerprint || "")
|
||||
);
|
||||
const healthy = targetHashReady && yamlSourceReady;
|
||||
return {
|
||||
@@ -1777,6 +1818,7 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
|
||||
? {
|
||||
sourceRef: fields.sourceRef,
|
||||
sourceKey: fields.sourceKey || null,
|
||||
sourceLine: numericField(fields.sourceLine),
|
||||
sourcePath: fields.sourcePath || null,
|
||||
exists: fields.sourceExists === "yes",
|
||||
fingerprint: fields.sourceFingerprint || null,
|
||||
@@ -1799,8 +1841,13 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
|
||||
...(yamlSourceMode ? {
|
||||
sourceRef: fields.beforeSourceRef || null,
|
||||
sourceKey: fields.beforeSourceKey || null,
|
||||
sourceLine: numericField(fields.beforeSourceLine),
|
||||
sourceFingerprint: fields.beforeSourceFingerprint || null,
|
||||
username: fields.beforeUsername || null,
|
||||
usernameSourceRef: fields.beforeUsernameSourceRef || null,
|
||||
usernameSourceKey: fields.beforeUsernameSourceKey || null,
|
||||
usernameSourceLine: numericField(fields.beforeUsernameSourceLine),
|
||||
usernameSourceFingerprint: fields.beforeUsernameSourceFingerprint || null,
|
||||
} : {}),
|
||||
},
|
||||
after: {
|
||||
@@ -1809,8 +1856,13 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
|
||||
...(yamlSourceMode ? {
|
||||
sourceRef: fields.afterSourceRef || null,
|
||||
sourceKey: fields.afterSourceKey || null,
|
||||
sourceLine: numericField(fields.afterSourceLine),
|
||||
sourceFingerprint: fields.afterSourceFingerprint || null,
|
||||
username: fields.afterUsername || null,
|
||||
usernameSourceRef: fields.afterUsernameSourceRef || null,
|
||||
usernameSourceKey: fields.afterUsernameSourceKey || null,
|
||||
usernameSourceLine: numericField(fields.afterUsernameSourceLine),
|
||||
usernameSourceFingerprint: fields.afterUsernameSourceFingerprint || null,
|
||||
} : {}),
|
||||
},
|
||||
cloudApiDeployment: fields.cloudApiDeployment || spec.cloudApiDeployment,
|
||||
|
||||
@@ -117,7 +117,14 @@ export function nodeRuntimeGitMirrorStatus(scoped: ReturnType<typeof parseNodeSc
|
||||
"PY",
|
||||
")",
|
||||
"summary_json=$(kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc '/etc/git-mirror/status.sh' 2>/tmp/hwlab-node-gitmirror-status.err || true)",
|
||||
"if ! printf '%s' \"$summary_json\" | node -e 'let s=\"\"; process.stdin.on(\"data\", c => s += c); process.stdin.on(\"end\", () => { try { const o = JSON.parse(s || \"{}\"); process.exit(o && o.localSource ? 0 : 1); } catch { process.exit(1); } });'; then",
|
||||
"if ! SUMMARY_JSON=\"$summary_json\" python3 - <<'PY'; then",
|
||||
"import json, os, sys",
|
||||
"try:",
|
||||
" value = json.loads(os.environ.get('SUMMARY_JSON') or '{}')",
|
||||
"except Exception:",
|
||||
" value = {}",
|
||||
"sys.exit(0 if isinstance(value, dict) and value.get('localSource') else 1)",
|
||||
"PY",
|
||||
" summary_json=$(kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc \"source_repository=\\$1 source_branch=\\$2 gitops_branch=\\$3 node <<'NODE'",
|
||||
"const { execFileSync } = require('node:child_process');",
|
||||
"const { readFileSync, existsSync } = require('node:fs');",
|
||||
@@ -161,29 +168,37 @@ export function nodeRuntimeGitMirrorStatus(scoped: ReturnType<typeof parseNodeSc
|
||||
"cache_pvc_exists=$(exists_res \"$namespace\" pvc \"$cache_pvc\")",
|
||||
"cache_host_path_exists=false",
|
||||
"if [ -n \"$cache_host_path\" ] && kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc 'test -d /cache' >/dev/null 2>&1; then cache_host_path_exists=true; fi",
|
||||
"SUMMARY_JSON=\"$summary_json\" GITHUB_TRANSPORT_JSON=\"$github_transport_json\" read_deployment_ready=\"$read_deployment_ready\" write_deployment_ready=\"$write_deployment_ready\" read_service_exists=\"$read_service_exists\" write_service_exists=\"$write_service_exists\" read_endpoints_ready=\"$read_endpoints_ready\" write_endpoints_ready=\"$write_endpoints_ready\" cache_pvc_exists=\"$cache_pvc_exists\" cache_host_path=\"$cache_host_path\" cache_host_path_exists=\"$cache_host_path_exists\" node <<'NODE'",
|
||||
"const summary = (() => { try { return JSON.parse(process.env.SUMMARY_JSON || '{}'); } catch { return {}; } })();",
|
||||
"const githubTransport = (() => { try { return JSON.parse(process.env.GITHUB_TRANSPORT_JSON || '{}'); } catch { return {}; } })();",
|
||||
"const env = process.env;",
|
||||
"const ok = env.read_deployment_ready === 'true' && env.write_deployment_ready === 'true' && env.read_service_exists === 'true' && env.write_service_exists === 'true' && env.read_endpoints_ready === 'true' && env.write_endpoints_ready === 'true' && (env.cache_pvc_exists === 'true' || env.cache_host_path_exists === 'true') && githubTransport.ready !== false && summary.localSource;",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: Boolean(ok),",
|
||||
" resources: {",
|
||||
" readDeploymentReady: env.read_deployment_ready === 'true',",
|
||||
" writeDeploymentReady: env.write_deployment_ready === 'true',",
|
||||
" readServiceExists: env.read_service_exists === 'true',",
|
||||
" writeServiceExists: env.write_service_exists === 'true',",
|
||||
" readEndpointsReady: env.read_endpoints_ready === 'true',",
|
||||
" writeEndpointsReady: env.write_endpoints_ready === 'true',",
|
||||
" cachePvcExists: env.cache_pvc_exists === 'true',",
|
||||
" cacheHostPathConfigured: Boolean(env.cache_host_path),",
|
||||
" cacheHostPathExists: env.cache_host_path_exists === 'true'",
|
||||
" },",
|
||||
" githubTransport,",
|
||||
" summary,",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
"SUMMARY_JSON=\"$summary_json\" GITHUB_TRANSPORT_JSON=\"$github_transport_json\" read_deployment_ready=\"$read_deployment_ready\" write_deployment_ready=\"$write_deployment_ready\" read_service_exists=\"$read_service_exists\" write_service_exists=\"$write_service_exists\" read_endpoints_ready=\"$read_endpoints_ready\" write_endpoints_ready=\"$write_endpoints_ready\" cache_pvc_exists=\"$cache_pvc_exists\" cache_host_path=\"$cache_host_path\" cache_host_path_exists=\"$cache_host_path_exists\" python3 - <<'PY'",
|
||||
"import json, os",
|
||||
"def load_env_json(name):",
|
||||
" try:",
|
||||
" value = json.loads(os.environ.get(name) or '{}')",
|
||||
" return value if isinstance(value, dict) else {}",
|
||||
" except Exception:",
|
||||
" return {}",
|
||||
"def truth(name):",
|
||||
" return os.environ.get(name) == 'true'",
|
||||
"summary = load_env_json('SUMMARY_JSON')",
|
||||
"github_transport = load_env_json('GITHUB_TRANSPORT_JSON')",
|
||||
"ok = truth('read_deployment_ready') and truth('write_deployment_ready') and truth('read_service_exists') and truth('write_service_exists') and truth('read_endpoints_ready') and truth('write_endpoints_ready') and (truth('cache_pvc_exists') or truth('cache_host_path_exists')) and github_transport.get('ready') is not False and bool(summary.get('localSource'))",
|
||||
"print(json.dumps({",
|
||||
" 'ok': bool(ok),",
|
||||
" 'resources': {",
|
||||
" 'readDeploymentReady': truth('read_deployment_ready'),",
|
||||
" 'writeDeploymentReady': truth('write_deployment_ready'),",
|
||||
" 'readServiceExists': truth('read_service_exists'),",
|
||||
" 'writeServiceExists': truth('write_service_exists'),",
|
||||
" 'readEndpointsReady': truth('read_endpoints_ready'),",
|
||||
" 'writeEndpointsReady': truth('write_endpoints_ready'),",
|
||||
" 'cachePvcExists': truth('cache_pvc_exists'),",
|
||||
" 'cacheHostPathConfigured': bool(os.environ.get('cache_host_path')),",
|
||||
" 'cacheHostPathExists': truth('cache_host_path_exists'),",
|
||||
" },",
|
||||
" 'githubTransport': github_transport,",
|
||||
" 'summary': summary,",
|
||||
" 'valuesPrinted': False,",
|
||||
"}))",
|
||||
"PY",
|
||||
].join("\n");
|
||||
const result = runNodeK3sScript(spec, script, scoped.timeoutSeconds);
|
||||
const parsed = record(parseJsonObject(statusText(result)));
|
||||
@@ -240,7 +255,9 @@ export function nodeRuntimeGitMirrorRun(scoped: ReturnType<typeof parseNodeScope
|
||||
for (let attempt = 1; attempt <= retryMaxAttempts; attempt += 1) {
|
||||
const retryLabel = `${attempt}/${retryMaxAttempts}`;
|
||||
const jobName = nodeRuntimeGitMirrorJobName(mirror, scoped.action);
|
||||
const manifest = nodeRuntimeGitMirrorJobManifest(mirror, scoped.action, jobName);
|
||||
const manifest = nodeRuntimeGitMirrorJobManifest(mirror, scoped.action, jobName, {
|
||||
discardStaleGitops: scoped.discardStaleGitops === true,
|
||||
});
|
||||
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
||||
const waitTimeoutSeconds = Math.max(5, Math.min(45, Math.max(5, scoped.timeoutSeconds - 10)));
|
||||
const script = [
|
||||
@@ -429,7 +446,7 @@ export function nodeRuntimeGitMirrorRetryableFailure(
|
||||
: waitTimeoutFailure
|
||||
? "Git mirror job wait exceeded the controlled short-connection budget. Standard git-mirror retries with exponential backoff and stops when retry budget is exhausted."
|
||||
: proxyConnectFailure || sshProxyBannerFailure
|
||||
? "Git mirror job hit a retryable YAML-first SSH-over-proxy failure. Standard git-mirror keeps using the configured node-global proxy and stops when retry budget is exhausted."
|
||||
? "Git mirror job hit a retryable YAML-first SSH-over-proxy failure. Standard git-mirror keeps using the configured proxy and stops when retry budget is exhausted."
|
||||
: `Git mirror job hit a retryable upstream GitHub ${mirror.githubTransport.mode} transport/fetch failure. Standard git-mirror stops without host workspace fallback.`,
|
||||
refSources: nodeRuntimeGitMirrorRefSources(scoped, mirror),
|
||||
githubTransport: nodeRuntimeGitMirrorGithubTransportSummary(mirror),
|
||||
@@ -489,7 +506,15 @@ export function nodeRuntimeEnsureGitMirrorSourceCurrent(scoped: ReturnType<typeo
|
||||
degradedReason: flush.ok === true ? undefined : "node-runtime-git-mirror-pre-flush-failed",
|
||||
};
|
||||
}
|
||||
const sync = nodeRuntimeGitMirrorRun({ ...scoped, domain: "git-mirror", action: "sync", confirm: true, dryRun: false, wait: true });
|
||||
const sync = nodeRuntimeGitMirrorRun({
|
||||
...scoped,
|
||||
domain: "git-mirror",
|
||||
action: "sync",
|
||||
confirm: true,
|
||||
dryRun: false,
|
||||
wait: true,
|
||||
discardStaleGitops: scoped.discardStaleGitops === true || scoped.rerun === true,
|
||||
});
|
||||
const after = record(sync.status);
|
||||
const afterSummary = Object.keys(after).length > 0 ? compactNodeRuntimeGitMirrorStatus(after) : {};
|
||||
const sourceOk = sync.ok === true && afterSummary.localSource === sourceCommit && afterSummary.githubSource === sourceCommit;
|
||||
@@ -633,7 +658,6 @@ export function nodeRuntimeGitMirrorNeedsFlush(status: Record<string, unknown>):
|
||||
const githubGitops = typeof summary.githubGitops === "string" ? summary.githubGitops : null;
|
||||
return summary.pendingFlush === true
|
||||
|| summary.flushNeeded === true
|
||||
|| summary.githubInSync === false
|
||||
|| (localGitops !== null && githubGitops !== null && localGitops !== githubGitops);
|
||||
}
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ export function runNodeWebProbeScript(
|
||||
credential: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
|
||||
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.password ?? "", webProbeProxy);
|
||||
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.username ?? secretSpec.bootstrapAdminUsername, material.password ?? "", webProbeProxy);
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
const commandTimedOut = result.timedOut || result.exitCode === 124;
|
||||
const stdoutReport = parseJsonObject(result.stdout);
|
||||
@@ -606,7 +606,7 @@ function webProbeScriptPreferredCommands(options: NodeWebProbeScriptOptions): Re
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, password: string, webProbeProxy: NodeWebProbeHostProxyEnv): string {
|
||||
export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, username: string, password: string, webProbeProxy: NodeWebProbeHostProxyEnv): string {
|
||||
const userScriptB64 = Buffer.from(options.scriptText, "utf8").toString("base64");
|
||||
const runnerB64 = Buffer.from(nodeWebProbeScriptRunnerSource(), "utf8").toString("base64");
|
||||
return [
|
||||
@@ -625,7 +625,7 @@ export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions
|
||||
[
|
||||
...webProbeProxy.envAssignments,
|
||||
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
|
||||
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
|
||||
`HWLAB_WEB_USER=${shellQuote(username)}`,
|
||||
`HWLAB_WEB_PASS=${shellQuote(password)}`,
|
||||
"UNIDESK_WEB_PROBE_RUN_DIR=\"$run_dir\"",
|
||||
"UNIDESK_WEB_PROBE_USER_SCRIPT=\"$user_script\"",
|
||||
|
||||
@@ -582,7 +582,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
|
||||
const script = [
|
||||
"set -eu",
|
||||
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
|
||||
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
|
||||
].join("\n");
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
const probe = compactWebProbeResult(parseJsonObject(result.stdout));
|
||||
@@ -922,7 +922,7 @@ export function runNodeWebProbeAsync(
|
||||
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
|
||||
const startScript = [
|
||||
"set -eu",
|
||||
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
|
||||
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
|
||||
].join("\n");
|
||||
const startResult = runTransWorkspaceStdinScript(options.node, spec.workspace, startScript, 55);
|
||||
const start = parseJsonObject(startResult.stdout);
|
||||
@@ -1114,7 +1114,11 @@ export function webProbeCommandTimeoutSummary(options: NodeWebProbeRunOptions, t
|
||||
|
||||
export function webProbeCredential(secretSpec: RuntimeSecretSpec, material: BootstrapAdminPasswordMaterial): Record<string, unknown> {
|
||||
return {
|
||||
username: secretSpec.bootstrapAdminUsername,
|
||||
username: material.username === null ? secretSpec.bootstrapAdminUsername : "<source-ref>",
|
||||
usernameSourceRef: material.usernameSourceRef,
|
||||
usernameSourceKey: material.usernameSourceKey,
|
||||
usernameSourceLine: material.usernameSourceLine,
|
||||
usernameFingerprint: material.usernameFingerprint,
|
||||
sourceRef: material.sourceRef,
|
||||
sourceKey: material.sourceKey,
|
||||
sourcePath: material.sourcePath === null ? null : displayRepoPath(material.sourcePath),
|
||||
@@ -1310,7 +1314,7 @@ export function runNodeWebProbeObserveStart(
|
||||
...webProbeProxy.envAssignments,
|
||||
...webProbeAccountEnvAssignments(),
|
||||
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
|
||||
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
|
||||
`HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`,
|
||||
`HWLAB_WEB_PASS=${shellQuote(material.password)}`,
|
||||
`UNIDESK_WEB_OBSERVE_STATE_DIR=${shellQuote(stateDir)}`,
|
||||
`UNIDESK_WEB_OBSERVE_JOB_ID=${shellQuote(jobId)}`,
|
||||
|
||||
@@ -49,12 +49,28 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
|
||||
egressProxy: gitMirror.egressProxy.mode === "direct" ? {
|
||||
mode: "direct",
|
||||
required: false,
|
||||
} : gitMirror.egressProxy.mode === "host-route" ? {
|
||||
...gitMirror.egressProxy,
|
||||
mode: "host-route",
|
||||
required: true,
|
||||
} : {
|
||||
...gitMirror.egressProxy,
|
||||
mode: "node-global",
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
const deployYamlGitMirror = {
|
||||
...renderGitMirror,
|
||||
egressProxy: renderGitMirror.egressProxy.mode !== "host-route" ? renderGitMirror.egressProxy : {
|
||||
mode: "node-global",
|
||||
required: true,
|
||||
clientName: renderGitMirror.egressProxy.clientName,
|
||||
namespace: "platform-infra",
|
||||
serviceName: renderGitMirror.egressProxy.clientName,
|
||||
port: httpProxyEndpoint(renderGitMirror.egressProxy.proxyUrl)?.port ?? 10808,
|
||||
noProxy: renderGitMirror.egressProxy.noProxy,
|
||||
},
|
||||
};
|
||||
return {
|
||||
nodeId: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
@@ -75,6 +91,7 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
|
||||
toolsImage: gitMirror.toolsImage,
|
||||
toolsImagePullPolicy: gitMirror.toolsImagePullPolicy,
|
||||
gitMirror: renderGitMirror,
|
||||
deployYamlGitMirror,
|
||||
networkProfileId: spec.networkProfileId,
|
||||
downloadProfileId: spec.downloadProfileId,
|
||||
gitSshProxyHost: gitSshProxy?.host,
|
||||
@@ -704,6 +721,7 @@ export function readLocalPostgresPasswordMaterial(input: { sourceRef: string; so
|
||||
}
|
||||
|
||||
export function localSecretSourcePaths(sourceRef: string): string[] {
|
||||
if (sourceRef.startsWith(".env/")) return ownerFileSourcePaths(sourceRef);
|
||||
const marker = "/.worktree/";
|
||||
const index = repoRoot.indexOf(marker);
|
||||
const paths = index >= 0
|
||||
@@ -1034,6 +1052,16 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
|
||||
" if [ \"$mode\" = build-moonbridge ]; then",
|
||||
" action=build",
|
||||
" source_present_before=build-source",
|
||||
" effective_build_container_http_proxy=\"$build_container_http_proxy\"",
|
||||
" effective_build_container_https_proxy=\"$build_container_https_proxy\"",
|
||||
" effective_build_container_all_proxy=\"$build_container_all_proxy\"",
|
||||
" effective_docker_build_add_host_args=\"$docker_build_add_host_args\"",
|
||||
" if [ \"$docker_network_mode\" = host ]; then",
|
||||
" effective_build_container_http_proxy=\"$build_http_proxy\"",
|
||||
" effective_build_container_https_proxy=\"$build_https_proxy\"",
|
||||
" effective_build_container_all_proxy=\"$build_all_proxy\"",
|
||||
" effective_docker_build_add_host_args=\"\"",
|
||||
" fi",
|
||||
" tmpdir=$(mktemp -d /tmp/hwlab-node-runtime-image-$id.XXXXXX)",
|
||||
" dockerfile=\"$tmpdir/Dockerfile\"",
|
||||
" cat > \"$dockerfile\" <<'DOCKERFILE'",
|
||||
@@ -1069,7 +1097,7 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
|
||||
"USER 65532:65532",
|
||||
"ENTRYPOINT [\"/app/moonbridge\"]",
|
||||
"DOCKERFILE",
|
||||
" if env HTTP_PROXY=\"$build_http_proxy\" HTTPS_PROXY=\"$build_https_proxy\" ALL_PROXY=\"$build_all_proxy\" NO_PROXY=\"$build_no_proxy\" http_proxy=\"$build_http_proxy\" https_proxy=\"$build_https_proxy\" all_proxy=\"$build_all_proxy\" no_proxy=\"$build_no_proxy\" docker build $docker_build_add_host_args --network \"$docker_network_mode\" --build-arg BUILDER_IMAGE=\"$builder_image\" --build-arg MOONBRIDGE_REPO=\"$source_repo\" --build-arg MOONBRIDGE_REF=\"$source_ref\" --build-arg GOPROXY_VALUE=\"$go_proxy\" --build-arg HTTP_PROXY=\"$build_container_http_proxy\" --build-arg HTTPS_PROXY=\"$build_container_https_proxy\" --build-arg ALL_PROXY=\"$build_container_all_proxy\" --build-arg NO_PROXY=\"$build_no_proxy\" --build-arg http_proxy=\"$build_container_http_proxy\" --build-arg https_proxy=\"$build_container_https_proxy\" --build-arg all_proxy=\"$build_container_all_proxy\" --build-arg no_proxy=\"$build_no_proxy\" -t \"$target\" -f \"$dockerfile\" \"$tmpdir\" >/tmp/hwlab-node-runtime-image-$id-build.out 2>&1; then",
|
||||
" if env HTTP_PROXY=\"$build_http_proxy\" HTTPS_PROXY=\"$build_https_proxy\" ALL_PROXY=\"$build_all_proxy\" NO_PROXY=\"$build_no_proxy\" http_proxy=\"$build_http_proxy\" https_proxy=\"$build_https_proxy\" all_proxy=\"$build_all_proxy\" no_proxy=\"$build_no_proxy\" docker build $effective_docker_build_add_host_args --network \"$docker_network_mode\" --build-arg BUILDER_IMAGE=\"$builder_image\" --build-arg MOONBRIDGE_REPO=\"$source_repo\" --build-arg MOONBRIDGE_REF=\"$source_ref\" --build-arg GOPROXY_VALUE=\"$go_proxy\" --build-arg HTTP_PROXY=\"$effective_build_container_http_proxy\" --build-arg HTTPS_PROXY=\"$effective_build_container_https_proxy\" --build-arg ALL_PROXY=\"$effective_build_container_all_proxy\" --build-arg NO_PROXY=\"$build_no_proxy\" --build-arg http_proxy=\"$effective_build_container_http_proxy\" --build-arg https_proxy=\"$effective_build_container_https_proxy\" --build-arg all_proxy=\"$effective_build_container_all_proxy\" --build-arg no_proxy=\"$build_no_proxy\" -t \"$target\" -f \"$dockerfile\" \"$tmpdir\" >/tmp/hwlab-node-runtime-image-$id-build.out 2>&1; then",
|
||||
" docker push \"$target\" >/tmp/hwlab-node-runtime-image-$id-push.out 2>&1 || { cat /tmp/hwlab-node-runtime-image-$id-push.out >&2 2>/dev/null || true; failed=true; }",
|
||||
" else",
|
||||
" cat /tmp/hwlab-node-runtime-image-$id-build.out >&2 2>/dev/null || true",
|
||||
@@ -1122,9 +1150,12 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
|
||||
? runNodeHostScriptAsync(spec, script, Math.min(timeoutSeconds, 600), "runtime-image-build")
|
||||
: runNodeHostScript(spec, script, Math.min(timeoutSeconds, 300));
|
||||
const imageRows = parseNodeRuntimeImageRows(statusText(result));
|
||||
const dependencyCount = nodeRuntimeImageDependencies(spec).length;
|
||||
const imageRowsComplete = imageRows.length === dependencyCount
|
||||
&& imageRows.every((image) => image.presentAfter === true || image.registryTagPresent === true);
|
||||
const base = imageRows.find((image) => image.id === "base") ?? {};
|
||||
return {
|
||||
ok: isCommandSuccess(result),
|
||||
ok: isCommandSuccess(result) || imageRowsComplete,
|
||||
dryRun,
|
||||
target: base.target ?? spec.baseImage,
|
||||
source: base.source ?? spec.baseImageSource,
|
||||
@@ -1196,6 +1227,7 @@ export function readSecretSourceValue(secretRoot: string, sourceRef: string, key
|
||||
}
|
||||
|
||||
export function secretSourcePaths(sourceRef: string): string[] {
|
||||
if (sourceRef.startsWith(".env/")) return ownerFileSourcePaths(sourceRef);
|
||||
const paths = [join(repoRoot, ".state", "secrets", sourceRef)];
|
||||
const marker = "/.worktree/";
|
||||
const index = repoRoot.indexOf(marker);
|
||||
@@ -1203,6 +1235,14 @@ export function secretSourcePaths(sourceRef: string): string[] {
|
||||
return [...new Set(paths)];
|
||||
}
|
||||
|
||||
function ownerFileSourcePaths(sourceRef: string): string[] {
|
||||
if (sourceRef.includes("..") || sourceRef.includes("\0")) return [];
|
||||
const marker = "/.worktree/";
|
||||
const index = repoRoot.indexOf(marker);
|
||||
const roots = index >= 0 ? [repoRoot.slice(0, index), repoRoot] : [repoRoot];
|
||||
return [...new Set(roots.map((root) => join(root, sourceRef)))];
|
||||
}
|
||||
|
||||
export function displayRepoPath(path: string): string {
|
||||
const normalizedRoot = repoRoot.replace(/\/+$/u, "");
|
||||
if (path === normalizedRoot) return ".";
|
||||
@@ -1225,27 +1265,28 @@ export function hwlabPasswordHash(password: string): string {
|
||||
export function readBootstrapAdminSecretMaterial(spec: RuntimeSecretSpec): BootstrapAdminSecretMaterial {
|
||||
const sourceRef = spec.bootstrapAdminPasswordSourceRef;
|
||||
const sourceKey = spec.bootstrapAdminPasswordSourceKey;
|
||||
const sourceLine = spec.bootstrapAdminPasswordSourceLine ?? null;
|
||||
if (sourceRef === undefined || sourceKey === undefined || spec.bootstrapAdminPasswordHashTransform === undefined) {
|
||||
return { ok: false, sourceRef: sourceRef ?? null, sourceKey: sourceKey ?? null, sourcePath: null, sourcePresent: false, sourceFingerprint: null, passwordHash: null, error: "bootstrap-admin-yaml-source-missing" };
|
||||
}
|
||||
const paths = secretSourcePaths(sourceRef);
|
||||
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
|
||||
if (!existsSync(sourcePath)) {
|
||||
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: false, sourceFingerprint: null, passwordHash: null, error: "secret-source-missing" };
|
||||
}
|
||||
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
|
||||
const password = values[sourceKey];
|
||||
if (password === undefined || password.length === 0) {
|
||||
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: true, sourceFingerprint: null, passwordHash: null, error: "secret-key-missing" };
|
||||
return bootstrapAdminSecretMaterialError(spec, sourceRef ?? null, sourceKey ?? null, sourceLine, null, null, "bootstrap-admin-yaml-source-missing");
|
||||
}
|
||||
const password = readSecretSourceScalar(sourceRef, sourceKey, sourceLine);
|
||||
if (!password.ok) return bootstrapAdminSecretMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, password.error);
|
||||
const username = readBootstrapAdminUsername(spec);
|
||||
if (!username.ok) return bootstrapAdminSecretMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, username.error);
|
||||
return {
|
||||
ok: true,
|
||||
username: username.value,
|
||||
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
|
||||
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
|
||||
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
|
||||
usernameFingerprint: shortSecretFingerprint(username.value),
|
||||
sourceRef,
|
||||
sourceKey,
|
||||
sourcePath,
|
||||
sourceLine,
|
||||
sourcePath: password.sourcePath,
|
||||
sourcePresent: true,
|
||||
sourceFingerprint: `sha256:${createHash("sha256").update(password).digest("hex").slice(0, 16)}`,
|
||||
passwordHash: hwlabPasswordHash(password),
|
||||
sourceFingerprint: shortSecretFingerprint(password.value),
|
||||
passwordHash: hwlabPasswordHash(password.value),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
@@ -1253,32 +1294,96 @@ export function readBootstrapAdminSecretMaterial(spec: RuntimeSecretSpec): Boots
|
||||
export function readBootstrapAdminPasswordMaterial(spec: RuntimeSecretSpec): BootstrapAdminPasswordMaterial {
|
||||
const sourceRef = spec.bootstrapAdminPasswordSourceRef;
|
||||
const sourceKey = spec.bootstrapAdminPasswordSourceKey;
|
||||
const sourceLine = spec.bootstrapAdminPasswordSourceLine ?? null;
|
||||
if (sourceRef === undefined || sourceKey === undefined || spec.bootstrapAdminPasswordHashTransform === undefined) {
|
||||
return { ok: false, sourceRef: sourceRef ?? null, sourceKey: sourceKey ?? null, sourcePath: null, sourcePresent: false, sourceFingerprint: null, password: null, error: "bootstrap-admin-yaml-source-missing" };
|
||||
}
|
||||
const paths = secretSourcePaths(sourceRef);
|
||||
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
|
||||
const runtimePassword = process.env[sourceKey];
|
||||
if (!existsSync(sourcePath) && (runtimePassword === undefined || runtimePassword.length === 0)) {
|
||||
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: false, sourceFingerprint: null, password: null, error: "secret-source-missing" };
|
||||
}
|
||||
const values = existsSync(sourcePath) ? parseEnvFile(readFileSync(sourcePath, "utf8")) : {};
|
||||
const password = values[sourceKey] ?? runtimePassword;
|
||||
if (password === undefined || password.length === 0) {
|
||||
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: true, sourceFingerprint: null, password: null, error: "secret-key-missing" };
|
||||
return bootstrapAdminPasswordMaterialError(spec, sourceRef ?? null, sourceKey ?? null, sourceLine, null, null, "bootstrap-admin-yaml-source-missing");
|
||||
}
|
||||
const password = readSecretSourceScalar(sourceRef, sourceKey, sourceLine);
|
||||
if (!password.ok) return bootstrapAdminPasswordMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, password.error);
|
||||
const username = readBootstrapAdminUsername(spec);
|
||||
if (!username.ok) return bootstrapAdminPasswordMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, username.error);
|
||||
return {
|
||||
ok: true,
|
||||
username: username.value,
|
||||
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
|
||||
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
|
||||
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
|
||||
usernameFingerprint: shortSecretFingerprint(username.value),
|
||||
sourceRef,
|
||||
sourceKey,
|
||||
sourcePath,
|
||||
sourceLine,
|
||||
sourcePath: password.sourcePath,
|
||||
sourcePresent: true,
|
||||
sourceFingerprint: `sha256:${createHash("sha256").update(password).digest("hex").slice(0, 16)}`,
|
||||
password,
|
||||
sourceFingerprint: shortSecretFingerprint(password.value),
|
||||
password: password.value,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function readBootstrapAdminUsername(spec: RuntimeSecretSpec): { ok: true; value: string } | { ok: false; error: string } {
|
||||
const ref = spec.bootstrapAdminUsernameSourceRef;
|
||||
if (ref === undefined) return { ok: true, value: spec.bootstrapAdminUsername };
|
||||
const key = spec.bootstrapAdminUsernameSourceKey ?? "username";
|
||||
const material = readSecretSourceScalar(ref, key, spec.bootstrapAdminUsernameSourceLine ?? null);
|
||||
if (!material.ok) return { ok: false, error: `username-${material.error}` };
|
||||
return { ok: true, value: material.value };
|
||||
}
|
||||
|
||||
function readSecretSourceScalar(sourceRef: string, sourceKey: string, sourceLine: number | null): { ok: true; value: string; sourcePath: string; sourcePresent: true } | { ok: false; sourcePath: string; sourcePresent: boolean; error: string } {
|
||||
const paths = secretSourcePaths(sourceRef);
|
||||
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
|
||||
if (!existsSync(sourcePath)) return { ok: false, sourcePath, sourcePresent: false, error: "secret-source-missing" };
|
||||
const text = readFileSync(sourcePath, "utf8");
|
||||
if (sourceLine !== null) {
|
||||
const line = text.split(/\r?\n/u)[sourceLine - 1]?.replace(/\r$/u, "") ?? "";
|
||||
if (line.length === 0) return { ok: false, sourcePath, sourcePresent: true, error: "secret-line-missing" };
|
||||
return { ok: true, value: line, sourcePath, sourcePresent: true };
|
||||
}
|
||||
const values = parseEnvFile(text);
|
||||
const runtimeValue = process.env[sourceKey];
|
||||
const value = values[sourceKey] ?? runtimeValue;
|
||||
if (value === undefined || value.length === 0) return { ok: false, sourcePath, sourcePresent: true, error: "secret-key-missing" };
|
||||
return { ok: true, value, sourcePath, sourcePresent: true };
|
||||
}
|
||||
|
||||
function bootstrapAdminSecretMaterialError(spec: RuntimeSecretSpec, sourceRef: string | null, sourceKey: string | null, sourceLine: number | null, sourcePath: string | null, sourcePresent: boolean | null, error: string): BootstrapAdminSecretMaterial {
|
||||
return {
|
||||
ok: false,
|
||||
username: null,
|
||||
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
|
||||
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
|
||||
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
|
||||
usernameFingerprint: null,
|
||||
sourceRef,
|
||||
sourceKey,
|
||||
sourceLine,
|
||||
sourcePath,
|
||||
sourcePresent: sourcePresent === true,
|
||||
sourceFingerprint: null,
|
||||
passwordHash: null,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
function bootstrapAdminPasswordMaterialError(spec: RuntimeSecretSpec, sourceRef: string | null, sourceKey: string | null, sourceLine: number | null, sourcePath: string | null, sourcePresent: boolean | null, error: string): BootstrapAdminPasswordMaterial {
|
||||
return {
|
||||
ok: false,
|
||||
username: null,
|
||||
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
|
||||
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
|
||||
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
|
||||
usernameFingerprint: null,
|
||||
sourceRef,
|
||||
sourceKey,
|
||||
sourceLine,
|
||||
sourcePath,
|
||||
sourcePresent: sourcePresent === true,
|
||||
sourceFingerprint: null,
|
||||
password: null,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseEnvFile(text: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const rawLine of text.split(/\r?\n/u)) {
|
||||
@@ -1547,11 +1652,13 @@ export function nodeRuntimeGitMirrorTarget(spec: HwlabRuntimeLaneSpec): NodeRunt
|
||||
const gitMirror = record(target.gitMirror);
|
||||
const gitMirrorEgressProxy = record(gitMirror.egressProxy);
|
||||
const gitMirrorEgressMode = stringValue(gitMirrorEgressProxy.mode, "gitMirror.egressProxy.mode");
|
||||
if (gitMirrorEgressMode !== "node-global" && gitMirrorEgressMode !== "direct") throw new Error(`gitMirror.egressProxy.mode must be node-global or direct for node=${spec.nodeId} lane=${spec.lane}`);
|
||||
if (gitMirrorEgressMode !== "node-global" && gitMirrorEgressMode !== "host-route" && gitMirrorEgressMode !== "direct") throw new Error(`gitMirror.egressProxy.mode must be node-global, host-route, or direct for node=${spec.nodeId} lane=${spec.lane}`);
|
||||
const nodeEgressProxy = gitMirrorEgressMode === "direct"
|
||||
? { mode: "direct" as const, required: false as const }
|
||||
: nodeRuntimeGitMirrorEgressProxySpec(record(node.egressProxy), `nodes.${spec.nodeId}.egressProxy`);
|
||||
if (gitMirrorEgressMode === "node-global" && gitMirrorEgressProxy.required !== true) throw new Error(`gitMirror.egressProxy.required must be true for node=${spec.nodeId} lane=${spec.lane}`);
|
||||
if ((gitMirrorEgressMode === "node-global" || gitMirrorEgressMode === "host-route") && gitMirrorEgressProxy.required !== true) throw new Error(`gitMirror.egressProxy.required must be true for node=${spec.nodeId} lane=${spec.lane}`);
|
||||
if (gitMirrorEgressMode === "node-global" && nodeEgressProxy.mode !== "k8s-service-cluster-ip") throw new Error(`gitMirror.egressProxy.mode=node-global requires nodes.${spec.nodeId}.egressProxy.mode=k8s-service-cluster-ip`);
|
||||
if (gitMirrorEgressMode === "host-route" && nodeEgressProxy.mode !== "host-route") throw new Error(`gitMirror.egressProxy.mode=host-route requires nodes.${spec.nodeId}.egressProxy.mode=host-route`);
|
||||
const githubTransport = nodeRuntimeGitMirrorGithubTransportSpec(record(gitMirror.githubTransport), "gitMirror.githubTransport");
|
||||
const source = record(target.source);
|
||||
const gitops = record(target.gitops);
|
||||
@@ -1626,7 +1733,19 @@ function gitMirrorSecretSourceEncoding(raw: unknown, path: string): "plain" | "b
|
||||
|
||||
export function nodeRuntimeGitMirrorEgressProxySpec(raw: Record<string, unknown>, path: string): NodeRuntimeGitMirrorEgressProxySpec {
|
||||
const mode = stringValue(raw.mode, `${path}.mode`);
|
||||
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be k8s-service-cluster-ip`);
|
||||
if (mode === "host-route") {
|
||||
const noProxyRaw = raw.noProxy;
|
||||
if (!Array.isArray(noProxyRaw)) throw new Error(`${path}.noProxy must be an array`);
|
||||
return {
|
||||
mode,
|
||||
clientName: stringValue(raw.clientName, `${path}.clientName`),
|
||||
hostProxyConfigRef: stringValue(raw.hostProxyConfigRef, `${path}.hostProxyConfigRef`),
|
||||
proxyEnvPath: stringValue(raw.proxyEnvPath, `${path}.proxyEnvPath`),
|
||||
proxyUrl: stringValue(raw.proxyUrl, `${path}.proxyUrl`),
|
||||
noProxy: noProxyRaw.map((item, index) => stringValue(item, `${path}.noProxy[${index}]`)),
|
||||
};
|
||||
}
|
||||
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be k8s-service-cluster-ip or host-route`);
|
||||
const port = Number(raw.port);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${path}.port must be a TCP port`);
|
||||
const sourceConfigRef = optionalStringValue(raw.sourceConfigRef, `${path}.sourceConfigRef`) ?? null;
|
||||
|
||||
@@ -34,6 +34,10 @@ interface TrafficOptions {
|
||||
type EgressProxyOptions = BenchmarkOptions | TrafficOptions;
|
||||
|
||||
export async function runPlatformInfraEgressProxyCommand(_config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
if (args[0] === "host") {
|
||||
const { runPlatformInfraHostProxyCommand } = await import("./platform-infra-host-proxy");
|
||||
return await runPlatformInfraHostProxyCommand(_config, args.slice(1));
|
||||
}
|
||||
if (args[0] === "k3s-build-benchmark") return runK3sBuildBenchmarkCommand(args.slice(1));
|
||||
const options = parseEgressProxyOptions(args);
|
||||
if (options.action === "traffic") {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,9 @@ export function manifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): s
|
||||
const configHash = configHashFor(sub2api, target);
|
||||
const image = targetImage(sub2api, target);
|
||||
const dependencyImages = targetDependencyImages(sub2api, target);
|
||||
const proxyEnv = sub2ApiProxyEnv(target);
|
||||
return template
|
||||
.replaceAll("unidesk.ai/runtime-node: G14", `unidesk.ai/runtime-node: ${target.id}`)
|
||||
.replaceAll("__SUB2API_IMAGE__", imageRef(sub2api, target))
|
||||
.replaceAll("__SUB2API_IMAGE_PULL_POLICY__", image.pullPolicy)
|
||||
.replaceAll("postgres:18-alpine", dependencyImages.postgres)
|
||||
@@ -126,7 +128,22 @@ export function manifest(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): s
|
||||
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ENABLED__", String(urlAllowlist.enabled))
|
||||
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP__", String(urlAllowlist.allowInsecureHttp))
|
||||
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS__", String(urlAllowlist.allowPrivateHosts))
|
||||
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS__", urlAllowlist.upstreamHosts.join(","));
|
||||
.replaceAll("__SUB2API_SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS__", urlAllowlist.upstreamHosts.join(","))
|
||||
.replaceAll(
|
||||
'UPDATE_PROXY_URL: ""',
|
||||
[
|
||||
`UPDATE_PROXY_URL: "${proxyEnv.httpProxy}"`,
|
||||
` HTTP_PROXY: "${proxyEnv.httpProxy}"`,
|
||||
` HTTPS_PROXY: "${proxyEnv.httpProxy}"`,
|
||||
` ALL_PROXY: "${proxyEnv.httpProxy}"`,
|
||||
` http_proxy: "${proxyEnv.httpProxy}"`,
|
||||
` https_proxy: "${proxyEnv.httpProxy}"`,
|
||||
` all_proxy: "${proxyEnv.httpProxy}"`,
|
||||
` NO_PROXY: "${proxyEnv.noProxy}"`,
|
||||
` no_proxy: "${proxyEnv.noProxy}"`,
|
||||
].join("\n"),
|
||||
)
|
||||
.concat("\n", renderPublicExposureManifest(target), renderEgressProxyManifest(target));
|
||||
}
|
||||
|
||||
export function configHashFor(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
|
||||
|
||||
@@ -45,6 +45,16 @@ function safeProviderSlug(providerId: string): string {
|
||||
return providerId.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "provider";
|
||||
}
|
||||
|
||||
function defaultHostSshUser(hostProjectRoot: string): string {
|
||||
const homeMatch = hostProjectRoot.match(/^\/home\/([^/]+)/u);
|
||||
if (homeMatch?.[1]) return homeMatch[1];
|
||||
return "root";
|
||||
}
|
||||
|
||||
function defaultHostRemoteCwd(user: string): string {
|
||||
return user === "root" ? "/root" : `/home/${user}`;
|
||||
}
|
||||
|
||||
function defaultMasterServer(config: UniDeskConfig): string {
|
||||
return `http://${config.network.publicHost}/`;
|
||||
}
|
||||
@@ -88,11 +98,27 @@ function parseAttachOptions(config: UniDeskConfig, args: string[]): ProviderAtta
|
||||
}
|
||||
|
||||
function envContent(options: ProviderAttachOptions): string {
|
||||
const slug = safeProviderSlug(options.providerId);
|
||||
const hostSshUser = defaultHostSshUser(repoRoot);
|
||||
const lines = [
|
||||
"# Generated by: bun scripts/cli.ts provider attach",
|
||||
"# Required attach inputs are intentionally limited to master server and provider id.",
|
||||
"# Required attach inputs are intentionally limited to master server, provider id, and optional provider token.",
|
||||
`UNIDESK_MASTER_SERVER=${options.masterServer}`,
|
||||
`PROVIDER_ID=${options.providerId}`,
|
||||
`PROVIDER_NAME=${options.providerId}`,
|
||||
`PROVIDER_UPGRADE_HOST_PROJECT_ROOT=${repoRoot}`,
|
||||
"PROVIDER_UPGRADE_WORKSPACE_PATH=/workspace",
|
||||
`PROVIDER_UPGRADE_COMPOSE_FILE=provider-${options.providerId}.yml`,
|
||||
`PROVIDER_UPGRADE_ENV_FILE=.state/provider-${options.providerId}.env`,
|
||||
`PROVIDER_UPGRADE_COMPOSE_PROJECT=unidesk-${slug}`,
|
||||
"PROVIDER_UPGRADE_SERVICE=provider-gateway",
|
||||
`PROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:${slug}`,
|
||||
"HOST_SSH_HOST=host.docker.internal",
|
||||
"HOST_SSH_PORT=22",
|
||||
`HOST_SSH_USER=${hostSshUser}`,
|
||||
"HOST_SSH_KEY=/run/host-ssh/id_ed25519",
|
||||
`HOST_REMOTE_CWD=${defaultHostRemoteCwd(hostSshUser)}`,
|
||||
"HOST_LOGIN_SHELL=/bin/bash",
|
||||
];
|
||||
if (options.token !== null) lines.push(`PROVIDER_TOKEN=${options.token}`);
|
||||
return `${lines.join("\n")}\n`;
|
||||
@@ -109,6 +135,8 @@ function composeContent(options: ProviderAttachOptions): string {
|
||||
" build:",
|
||||
" context: .",
|
||||
" dockerfile: src/components/provider-gateway/Dockerfile",
|
||||
" args:",
|
||||
' ALPINE_REPOSITORY: "${PROVIDER_GATEWAY_ALPINE_REPOSITORY:-}"',
|
||||
` container_name: unidesk-provider-gateway-${slug}`,
|
||||
" restart: always",
|
||||
' pid: "host"',
|
||||
|
||||
@@ -91,7 +91,7 @@ class SshFileTransferError extends Error {
|
||||
}
|
||||
|
||||
const fileTransferWriteB64ArgvLimit = 48_000;
|
||||
const fileTransferWriteRawChunkBytes = 1_048_576;
|
||||
const fileTransferWriteRawChunkBytes = 131_072;
|
||||
const fileTransferWriteB64ChunkChars = 1_398_104;
|
||||
const fileTransferProgressEveryChunks = 16;
|
||||
|
||||
@@ -247,7 +247,7 @@ async function writeRemoteFileVerified(
|
||||
const encoded = content.length <= fileTransferWriteB64ArgvLimit
|
||||
? content.toString("base64")
|
||||
: "";
|
||||
if (invocation.route.plane !== "win" && encoded.length <= fileTransferWriteB64ArgvLimit) {
|
||||
if (invocation.route.plane !== "win" && encoded.length > 0 && encoded.length <= fileTransferWriteB64ArgvLimit) {
|
||||
await checkedFileTransfer(invocation, executor, builders, "write-b64-argv", [remotePath, expectedBytes, expectedSha256, ...chunkString(encoded, fileTransferWriteB64ChunkChars)]);
|
||||
return { strategy: "argv", chunks: encoded.length === 0 ? 0 : Math.ceil(encoded.length / fileTransferWriteB64ChunkChars) };
|
||||
}
|
||||
@@ -260,14 +260,18 @@ async function writeRemoteFileVerified(
|
||||
}
|
||||
}
|
||||
const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`;
|
||||
const startedAtMs = Date.now();
|
||||
let chunks = 0;
|
||||
await checkedFileTransfer(invocation, executor, builders, "write-b64-begin", [remotePath, token]);
|
||||
for (let offset = 0; offset < content.length; offset += fileTransferWriteRawChunkBytes) {
|
||||
const encodedChunk = content.subarray(offset, Math.min(content.length, offset + fileTransferWriteRawChunkBytes)).toString("base64");
|
||||
const nextOffset = Math.min(content.length, offset + fileTransferWriteRawChunkBytes);
|
||||
const encodedChunk = content.subarray(offset, nextOffset).toString("base64");
|
||||
await checkedFileTransfer(invocation, executor, builders, "write-b64-append-stdin", [remotePath, token], encodedChunk);
|
||||
chunks += 1;
|
||||
emitUploadProgress(invocation, remotePath, chunks, nextOffset, content.length, nextOffset - offset, startedAtMs);
|
||||
}
|
||||
await checkedFileTransfer(invocation, executor, builders, "write-b64-commit", [remotePath, token, expectedBytes, expectedSha256]);
|
||||
emitUploadProgress(invocation, remotePath, Math.max(1, chunks), content.length, content.length, 0, startedAtMs, true);
|
||||
return { strategy: "chunked-stdin", chunks };
|
||||
}
|
||||
|
||||
@@ -383,6 +387,37 @@ function emitDownloadProgress(
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
function emitUploadProgress(
|
||||
invocation: ParsedSshInvocation,
|
||||
remotePath: string,
|
||||
chunkCount: number,
|
||||
actualBytes: number,
|
||||
expectedBytes: number,
|
||||
lastChunkBytes: number,
|
||||
startedAtMs: number,
|
||||
force = false,
|
||||
): void {
|
||||
if (!force && chunkCount !== 1 && actualBytes < expectedBytes && chunkCount % fileTransferProgressEveryChunks !== 0) return;
|
||||
const elapsedMs = Math.max(1, Date.now() - startedAtMs);
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
event: "unidesk.ssh.upload.progress",
|
||||
at: new Date().toISOString(),
|
||||
route: invocation.route.raw,
|
||||
providerId: invocation.providerId,
|
||||
remotePath,
|
||||
strategy: "chunked-stdin",
|
||||
chunks: chunkCount,
|
||||
bytes: actualBytes,
|
||||
totalBytes: expectedBytes,
|
||||
actualBytes,
|
||||
expectedBytes,
|
||||
lastChunkBytes,
|
||||
remainingBytes: Math.max(0, expectedBytes - actualBytes),
|
||||
elapsedMs,
|
||||
throughputBytesPerSecond: Math.round((actualBytes * 1000) / elapsedMs),
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
async function statRemoteFile(
|
||||
invocation: ParsedSshInvocation,
|
||||
executor: SshRemoteCommandExecutor,
|
||||
|
||||
@@ -157,10 +157,10 @@ function sentinelSchedules(spec: ReturnType<typeof hwlabRuntimeLaneSpecForNode>,
|
||||
}
|
||||
return selectedRows.map((row) => {
|
||||
const sentinel = resolveWebProbeSentinel(spec, row.id);
|
||||
const publicExposure = record(readConfigRefTarget(sentinel.configRefs.publicExposure), sentinel.configRefs.publicExposure);
|
||||
const runtime = record(readConfigRefTarget(sentinel.configRefs.runtime), sentinel.configRefs.runtime);
|
||||
const cicd = record(readConfigRefTarget(sentinel.configRefs.cicd), sentinel.configRefs.cicd);
|
||||
const scenarios = scenarioRows(readConfigRefTarget(sentinel.configRefs.scenarios));
|
||||
const publicExposure = record(readConfigRefTarget(sentinel.configRefs.publicExposure, spec), sentinel.configRefs.publicExposure);
|
||||
const runtime = record(readConfigRefTarget(sentinel.configRefs.runtime, spec), sentinel.configRefs.runtime);
|
||||
const cicd = record(readConfigRefTarget(sentinel.configRefs.cicd, spec), sentinel.configRefs.cicd);
|
||||
const scenarios = scenarioRows(readConfigRefTarget(sentinel.configRefs.scenarios, spec));
|
||||
const enabledScenarios = scenarios.filter((scenario) => scenario.enabled !== false);
|
||||
const scenarioCadences = enabledScenarios
|
||||
.map((scenario) => typeof scenario.cadence === "string" ? parseDurationSeconds(scenario.cadence) : null)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
FROM oven/bun:1-alpine
|
||||
ARG ALPINE_REPOSITORY=
|
||||
RUN if [ -n "$ALPINE_REPOSITORY" ]; then printf '%s\n' "$ALPINE_REPOSITORY/main" "$ALPINE_REPOSITORY/community" > /etc/apk/repositories; fi
|
||||
RUN apk add --no-cache bash docker-cli docker-cli-compose openssh-client
|
||||
WORKDIR /app/src/components/provider-gateway
|
||||
COPY src/components/provider-gateway/package.json ./package.json
|
||||
|
||||
Reference in New Issue
Block a user