deploy: configure NC01 HWLAB monitor exposure
This commit is contained in:
@@ -80,6 +80,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台。本文
|
||||
- 开发环境和 D601/Master 边界: `docs/reference/dev-environment.md`。
|
||||
- HWLAB: `docs/reference/hwlab.md`。
|
||||
- AgentRun: `docs/reference/agentrun.md`。
|
||||
- NC01: `docs/reference/nc01.md`。
|
||||
- OTel/可观测性: `docs/reference/observability.md`。
|
||||
- YAML-first: `docs/reference/yaml-first-ops.md`。
|
||||
- Platform infrastructure: `docs/reference/platform-infra.md`。
|
||||
|
||||
+286
-7
@@ -67,6 +67,9 @@ controlPlane:
|
||||
JD01:
|
||||
route: JD01
|
||||
kubeRoute: JD01:k3s
|
||||
NC01:
|
||||
route: NC01
|
||||
kubeRoute: NC01:k3s
|
||||
|
||||
lanes:
|
||||
v01:
|
||||
@@ -78,7 +81,7 @@ controlPlane:
|
||||
bootstrapFromBranch: v0.1
|
||||
bootstrapTimeoutSeconds: 900
|
||||
bootstrapPollSeconds: 15
|
||||
remote: git@github.com:pikasTech/agentrun.git
|
||||
remote: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
workspace: /root/agentrun-v01
|
||||
runtime:
|
||||
namespace: agentrun-v01
|
||||
@@ -225,7 +228,7 @@ controlPlane:
|
||||
readDeployment: git-mirror-http
|
||||
writeService: git-mirror-write
|
||||
writeDeployment: git-mirror-write
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git
|
||||
cachePvc: git-mirror-cache
|
||||
cacheHostPath: null
|
||||
@@ -562,8 +565,9 @@ controlPlane:
|
||||
providerCredential:
|
||||
profile: dsflash-go
|
||||
- id: tool-github-pr-token
|
||||
sourceRef: /root/.config/unidesk/github.env
|
||||
sourceRef: /root/unidesk/.env/gh_token.txt
|
||||
sourceKey: GH_TOKEN
|
||||
sourceFormat: raw-token
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-tool-github-pr
|
||||
@@ -617,7 +621,7 @@ controlPlane:
|
||||
path: deploy/gitops/node/jd01/runtime-v02
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-jd01-v02
|
||||
repoURL: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
deployment:
|
||||
format: unidesk-yaml-only
|
||||
gitopsRoot: deploy/gitops/node/jd01
|
||||
@@ -772,7 +776,7 @@ controlPlane:
|
||||
readDeployment: git-mirror-http
|
||||
writeService: git-mirror-write
|
||||
writeDeployment: git-mirror-write
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
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: null
|
||||
@@ -861,8 +865,282 @@ controlPlane:
|
||||
providerCredential:
|
||||
profile: dsflash-go
|
||||
- id: tool-github-pr-token
|
||||
sourceRef: /root/.config/unidesk/github.env
|
||||
sourceRef: /root/unidesk/.env/gh_token.txt
|
||||
sourceKey: GH_TOKEN
|
||||
sourceFormat: raw-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
|
||||
|
||||
nc01-v02:
|
||||
node: NC01
|
||||
version: v0.2
|
||||
source:
|
||||
statusMode: k3s-git-mirror
|
||||
repository: pikasTech/agentrun
|
||||
branch: v0.2
|
||||
sourceAuthority:
|
||||
mode: gitMirrorSnapshot
|
||||
resolver: k8s-git-mirror
|
||||
allowHostGit: false
|
||||
allowHostWorkspace: false
|
||||
allowGithubDirectInPipeline: false
|
||||
sourceSnapshot:
|
||||
stageRefPrefix: refs/unidesk/snapshots/agentrun-yaml-lane/{branch}
|
||||
missingObjectPolicy: fail-fast
|
||||
refreshPolicy: sync-before-snapshot
|
||||
bootstrapFromBranch: v0.1
|
||||
bootstrapTimeoutSeconds: 900
|
||||
bootstrapPollSeconds: 15
|
||||
remote: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
workspace: /root/agentrun
|
||||
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-nc01-v02-ci-image-publish
|
||||
pipelineRunPrefix: agentrun-nc01-v02-ci
|
||||
serviceAccountName: agentrun-nc01-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: nc01-v0.2-gitops
|
||||
path: deploy/gitops/node/nc01/runtime-v02
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-nc01-v02
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
deployment:
|
||||
format: unidesk-yaml-only
|
||||
gitopsRoot: deploy/gitops/node/nc01
|
||||
runtimeRenderDir: runtime-v02
|
||||
artifactCatalogPath: deploy/artifact-catalog.nc01-v02.json
|
||||
argocd:
|
||||
project: agentrun-nc01-v02
|
||||
applicationFile: application-v02.yaml
|
||||
manager:
|
||||
serviceAccount: agentrun-nc01-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: NC01
|
||||
HWLAB_RUNTIME_LANE: v0.3
|
||||
unideskSshEndpointEnv:
|
||||
name: UNIDESK_MAIN_SERVER_IP
|
||||
value: 152.53.229.148
|
||||
bootRepoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/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-nc01-v02-runner
|
||||
jobNamePrefix: agentrun-nc01-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: 3
|
||||
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-nc01-v02-runner
|
||||
- agentrun-v02-runner
|
||||
- agentrun-v01-runner
|
||||
ageBasedCleanup:
|
||||
enabled: false
|
||||
maxAgeHours: 48
|
||||
sessionPvcRetention:
|
||||
enabled: true
|
||||
prefixes:
|
||||
- agentrun-v01-session-
|
||||
- agentrun-v02-session-
|
||||
- agentrun-nc01-v02-session-
|
||||
maxDeletePerRun: 1000
|
||||
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/nc01-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: null
|
||||
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-nc01-v02-sync-manual
|
||||
flushJobPrefix: git-mirror-agentrun-nc01-v02-flush-manual
|
||||
repositories:
|
||||
- key: agentrun
|
||||
repository: pikasTech/agentrun
|
||||
sourceBranch: v0.2
|
||||
gitopsBranch: nc01-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: hwlab/nc01-v03-admin.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: hwlab/nc01-v03-admin.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/nc01-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/unidesk/.env/gh_token.txt
|
||||
sourceKey: GH_TOKEN
|
||||
sourceFormat: raw-token
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-tool-github-pr
|
||||
@@ -1188,8 +1466,9 @@ controlPlane:
|
||||
providerCredential:
|
||||
profile: fake-echo
|
||||
- id: tool-github-pr-token
|
||||
sourceRef: /root/.config/unidesk/github.env
|
||||
sourceRef: /root/unidesk/.env/gh_token.txt
|
||||
sourceKey: GH_TOKEN
|
||||
sourceFormat: raw-token
|
||||
targetRef:
|
||||
namespace: agentrun-v02
|
||||
name: agentrun-v01-tool-github-pr
|
||||
|
||||
@@ -266,6 +266,115 @@ nodes:
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
|
||||
NC01:
|
||||
route: NC01
|
||||
kubeRoute: NC01:k3s
|
||||
k3s:
|
||||
serviceName: k3s
|
||||
dropInPath: /etc/systemd/system/k3s.service.d/20-unidesk-node-config.conf
|
||||
nodeStatusName: v2202607378382480008
|
||||
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.NC01
|
||||
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
|
||||
- metrics-server
|
||||
- --node-name
|
||||
- v2202607378382480008
|
||||
- --node-label
|
||||
- unidesk.ai/node-id=NC01
|
||||
- --node-label
|
||||
- unidesk.ai/provider-id=NC01
|
||||
- --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:
|
||||
mode: k8s-workload
|
||||
endpoint: 127.0.0.1:5000
|
||||
namespace: devops-infra
|
||||
deploymentName: node-local-registry
|
||||
serviceName: node-local-registry
|
||||
pvcName: node-local-registry-storage
|
||||
storage: 20Gi
|
||||
image: docker.m.daocloud.io/library/registry:2
|
||||
imagePullPolicy: IfNotPresent
|
||||
containerPort: 5000
|
||||
listenHost: 127.0.0.1
|
||||
listenPort: 5000
|
||||
hostNetwork: true
|
||||
egressProxy:
|
||||
mode: host-route
|
||||
clientName: nc01-host-proxy
|
||||
hostProxyConfigRef: config/platform-infra/host-proxy.yaml#targets.NC01
|
||||
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
|
||||
- 152.53.229.148
|
||||
- 152.53.229.148
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
|
||||
targets:
|
||||
- id: d601-v03
|
||||
node: D601
|
||||
@@ -692,6 +801,224 @@ targets:
|
||||
- argocd-repo-server
|
||||
statefulSets:
|
||||
- argocd-application-controller
|
||||
- id: nc01-v03
|
||||
node: NC01
|
||||
lane: v03
|
||||
enabled: true
|
||||
ciNamespace: hwlab-ci
|
||||
runtimeNamespace: hwlab-v03
|
||||
source:
|
||||
repository: pikasTech/HWLAB
|
||||
branch: v0.3
|
||||
sourceAuthority:
|
||||
mode: giteaSnapshot
|
||||
resolver: gitea-mirror
|
||||
giteaMirrorRepoKey: hwlab-nc01-v03
|
||||
allowHostGit: false
|
||||
allowHostWorkspace: false
|
||||
allowGithubDirectInPipeline: false
|
||||
sourceSnapshot:
|
||||
stageRefPrefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/{branch}
|
||||
missingObjectPolicy: fail-fast
|
||||
refreshPolicy: gitea-controlled-snapshot
|
||||
gitops:
|
||||
branch: v0.3-gitops
|
||||
path: deploy/gitops/node/nc01/runtime-v03
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
serviceReadName: git-mirror-http
|
||||
serviceWriteName: git-mirror-write
|
||||
cachePvcName: hwlab-git-mirror-cache
|
||||
cachePvcStorage: 20Gi
|
||||
cacheHostPath: null
|
||||
servicePort: 8080
|
||||
readContainerPort: 8080
|
||||
writeContainerPort: 8081
|
||||
deploymentReplicas: 1
|
||||
secretName: git-mirror-github-ssh
|
||||
syncConfigMapName: git-mirror-sync-script
|
||||
syncJobPrefix: git-mirror-hwlab-nc01-v03-sync-manual
|
||||
flushJobPrefix: git-mirror-hwlab-nc01-v03-flush-manual
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/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: https
|
||||
username: x-access-token
|
||||
tokenSecretName: git-mirror-github-token
|
||||
tokenSecretKey: GITHUB_TOKEN
|
||||
tokenSourceRef: .env/gh_token.txt
|
||||
tokenSourceKey: GH_TOKEN
|
||||
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-nc01-v03-ci-image-publish
|
||||
serviceAccountName: hwlab-nc01-v03-tekton-runner
|
||||
pipelineRunPrefix: hwlab-nc01-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-nc01-v03-runtime-observer
|
||||
roleBindingName: hwlab-nc01-v03-runtime-observer
|
||||
argoObserverRbac:
|
||||
namespace: argocd
|
||||
roleName: hwlab-nc01-v03-argo-observer
|
||||
roleBindingName: hwlab-nc01-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: NC01
|
||||
buildMode: node-local
|
||||
ciBuildBenchmarks:
|
||||
- profile: no-mirror-full
|
||||
runtimeLaneConfigRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01
|
||||
pipelineRunPrefix: hwlab-nc01-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-nc01
|
||||
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.NC01.egressProxy
|
||||
hostNetwork: false
|
||||
injectEnv: true
|
||||
deployments:
|
||||
- argocd-repo-server
|
||||
statefulSets:
|
||||
- argocd-application-controller
|
||||
- id: d518-v03
|
||||
node: D518
|
||||
lane: v03
|
||||
|
||||
@@ -41,6 +41,13 @@ nodes:
|
||||
gitopsRoot: deploy/gitops/node
|
||||
networkProfile: jd01-node-ci-egress
|
||||
downloadProfile: jd01-node-default
|
||||
NC01:
|
||||
route: NC01
|
||||
kubeRoute: NC01:k3s
|
||||
sourceWorkspace: /root/hwlab
|
||||
gitopsRoot: deploy/gitops/node
|
||||
networkProfile: jd01-node-ci-egress
|
||||
downloadProfile: jd01-node-default
|
||||
|
||||
lanes:
|
||||
v02:
|
||||
@@ -1254,6 +1261,268 @@ lanes:
|
||||
envKey: DATASTORE_URI
|
||||
authnKey: authn-preshared-key
|
||||
role: hwlab_jd01_v03_app
|
||||
NC01:
|
||||
node: NC01
|
||||
workspace: /root/hwlab
|
||||
sourceAuthority:
|
||||
mode: giteaSnapshot
|
||||
resolver: gitea-mirror
|
||||
giteaMirrorRepoKey: hwlab-nc01-v03
|
||||
allowHostGit: false
|
||||
allowHostWorkspace: false
|
||||
allowGithubDirectInPipeline: false
|
||||
sourceSnapshot:
|
||||
stageRefPrefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/{branch}
|
||||
missingObjectPolicy: fail-fast
|
||||
refreshPolicy: gitea-controlled-snapshot
|
||||
cicdRepo: /root/hwlab-v03-cicd.git
|
||||
cicdRepoLock: /tmp/hwlab-v03-cicd-repo.lock
|
||||
app: hwlab-node-v03
|
||||
pipeline: hwlab-nc01-v03-ci-image-publish
|
||||
pipelineRunPrefix: hwlab-nc01-v03-ci-poll
|
||||
serviceAccountName: hwlab-nc01-v03-tekton-runner
|
||||
controlPlaneFieldManager: unidesk-hwlab-nc01-v03-control-plane
|
||||
git:
|
||||
url: git@github.com:pikasTech/HWLAB.git
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/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.nc01-v03.json
|
||||
runtime:
|
||||
path: deploy/gitops/node/nc01/runtime-v03
|
||||
namespace: hwlab-v03
|
||||
renderDir: runtime-v03
|
||||
runtimeStore:
|
||||
postgres:
|
||||
mode: platform-service
|
||||
serviceName: nc01-host-postgres
|
||||
poolMax: 16
|
||||
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
|
||||
runtimeImageRewrites:
|
||||
- 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
|
||||
- source: ghcr.io/anomalyco/opencode:1.17.7
|
||||
target: 127.0.0.1:5000/hwlab/opencode:1.17.7
|
||||
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
|
||||
- id: opencode-git
|
||||
kind: opencode-git
|
||||
target: 127.0.0.1:5000/hwlab/opencode:1.17.7-git
|
||||
sourceImage: ghcr.io/anomalyco/opencode:1.17.7
|
||||
dockerNetworkMode: host
|
||||
webProbe:
|
||||
browserProxyMode: auto
|
||||
playwrightBrowsersPath: "0"
|
||||
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
|
||||
domEvaluateTimeoutRedCount: 2
|
||||
domEvaluateTimeoutRedWindowMs: 30000
|
||||
screenshotTimeoutRedCount: 2
|
||||
pageErrorRedCount: 2
|
||||
longTaskRedMs: 1000
|
||||
longAnimationFrameRedMs: 1000
|
||||
eventLoopGapRedMs: 1000
|
||||
browserProcessSampleIntervalMs: 1000
|
||||
requestRateBucketMs: 10000
|
||||
requestRateTotalRedPerMinute: 300
|
||||
requestRatePageRedPerMinute: 240
|
||||
requestRateApiPathRedPerMinute: 120
|
||||
browserTotalRssRedMb: 800
|
||||
browserProcessRssRedMb: 600
|
||||
browserRssGrowthRedMb: 300
|
||||
browserRssGrowthWindowMs: 30000
|
||||
playwrightResponsivenessRedMs: 5000
|
||||
playwrightResponsivenessTimeoutRedCount: 2
|
||||
cdpMetricsTimeoutRedCount: 2
|
||||
uncommandedStateChangeCommandWindowMs: 10000
|
||||
scrollJumpCommandWindowMs: 8000
|
||||
scrollJumpFromY: 250
|
||||
scrollJumpToY: 40
|
||||
sessionRailFallbackRatio: 0.5
|
||||
browserFreezePolicy:
|
||||
enabled: true
|
||||
blockerWindowMs: 30000
|
||||
memory:
|
||||
totalRssBlockerMb: 800
|
||||
processRssBlockerMb: 500
|
||||
growthBlockerMb: 300
|
||||
responsiveness:
|
||||
latencyBlockerMs: 5000
|
||||
eventBlockerCount: 2
|
||||
cdp:
|
||||
metricsTimeoutBlockerCount: 2
|
||||
kill:
|
||||
enabled: true
|
||||
gracefulSignal: SIGTERM
|
||||
forceSignal: SIGKILL
|
||||
graceMs: 3000
|
||||
pollIntervalMs: 100
|
||||
exitCode: 7
|
||||
projectManagement:
|
||||
enabled: true
|
||||
targetPaths:
|
||||
- /projects
|
||||
- /projects/mdtodo
|
||||
readinessSelectors:
|
||||
- '[data-testid="project-management-root"]'
|
||||
- '[data-testid="project-management-mdtodo"]'
|
||||
naturalApiPathPrefixes:
|
||||
- /v1/project-management/
|
||||
- /v1/workbench/launches
|
||||
- /v1/agent/chat
|
||||
commandAllowlist:
|
||||
- gotoProjectMdtodo
|
||||
- openMdtodoSourceConfig
|
||||
- closeMdtodoSourceConfig
|
||||
- configureMdtodoHwpodSource
|
||||
- probeMdtodoSource
|
||||
- reindexMdtodoSource
|
||||
- selectProjectSource
|
||||
- selectMdtodoSource
|
||||
- selectMdtodoFile
|
||||
- selectMdtodoTask
|
||||
- expandMdtodoTask
|
||||
- openMdtodoReportPreview
|
||||
- toggleMdtodoReportFullscreen
|
||||
- editMdtodoTaskInline
|
||||
- editMdtodoTaskTitle
|
||||
- editMdtodoTaskBody
|
||||
- toggleMdtodoTaskStatus
|
||||
- addMdtodoRootTask
|
||||
- addMdtodoSubTask
|
||||
- continueMdtodoTask
|
||||
- deleteMdtodoTask
|
||||
- launchWorkbenchFromTask
|
||||
- launchWorkbenchFromMdtodo
|
||||
launchRoute: /v1/workbench/launches
|
||||
slowApiBudgetMs: 10000
|
||||
public:
|
||||
webUrl: https://hwlab.pikapython.com
|
||||
apiUrl: https://hwlab.pikapython.com
|
||||
observability:
|
||||
prometheusOperator: false
|
||||
webProbe:
|
||||
monitorRoot:
|
||||
enabled: true
|
||||
sentinelId: nc01-web-probe-sentinel
|
||||
publicBaseUrl: https://monitor.pikapython.com
|
||||
routePrefix: /
|
||||
caddyManagedBlockOwner: hwlab-web-probe-sentinel-active-root
|
||||
sentinels:
|
||||
- id: nc01-web-probe-sentinel
|
||||
enabled: true
|
||||
configRef: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.sentinel
|
||||
bootstrapAdmin:
|
||||
username: admin
|
||||
displayName: HWLAB v0.3 Admin
|
||||
passwordSourceRef: hwlab/nc01-v03-bootstrap-admin.env
|
||||
passwordSourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
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/nc01-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: fake-echo
|
||||
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-nc01-v03-cloud-web
|
||||
remotePort: 22082
|
||||
localIP: hwlab-cloud-web.hwlab-v03.svc.cluster.local
|
||||
localPort: 8080
|
||||
apiProxy:
|
||||
name: hwlab-nc01-v03-edge-proxy
|
||||
remotePort: 22083
|
||||
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
|
||||
externalPostgres:
|
||||
provider: NC01
|
||||
configRef: config/platform-db/postgres-nc01.yaml
|
||||
serviceName: nc01-host-postgres
|
||||
endpointAddress: 10.42.0.1
|
||||
port: 5432
|
||||
runtimeAccess:
|
||||
routeName: nc01-host-postgres
|
||||
endpointAddress: 10.42.0.1
|
||||
port: 5432
|
||||
sslmode: require
|
||||
database: hwlab_nc01_v03
|
||||
cloudApi:
|
||||
secretName: hwlab-cloud-api-v03-db
|
||||
secretKey: database-url
|
||||
sourceRef: hwlab/nc01-v03-cloud-api-db.env
|
||||
envKey: DATABASE_URL
|
||||
role: hwlab_nc01_v03_app
|
||||
openfga:
|
||||
secretName: hwlab-v03-openfga
|
||||
secretKey: datastore-uri
|
||||
sourceRef: hwlab/nc01-v03-openfga-db.env
|
||||
envKey: DATASTORE_URI
|
||||
authnKey: authn-preshared-key
|
||||
role: hwlab_nc01_v03_app
|
||||
|
||||
networkProfiles:
|
||||
node-ci-egress:
|
||||
|
||||
@@ -320,3 +320,164 @@ nodes:
|
||||
data:
|
||||
- sourcePurpose: frp-token
|
||||
targetKey: token
|
||||
|
||||
NC01:
|
||||
target: &nc01-target
|
||||
node: ${NODE}
|
||||
lane: ${LANE}
|
||||
publicOriginRef: config/hwlab-node-lanes.yaml#lanes.${LANE}.targets.${NODE}.public.webUrl
|
||||
|
||||
cicdCommon: &nc01-cicd-common
|
||||
controlPlaneConfigRef: config/hwlab-node-control-plane.yaml#targets[2]
|
||||
source:
|
||||
<<: *cicd-source
|
||||
gitSshUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git
|
||||
gitMirrorReadUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git
|
||||
sourceAuthority:
|
||||
<<: *cicd-source-authority
|
||||
mode: giteaSnapshot
|
||||
resolver: gitea-mirror
|
||||
sourceSnapshot:
|
||||
<<: *cicd-source-snapshot
|
||||
stageRefPrefix: refs/unidesk/snapshots/gitea-actions/unidesk-master
|
||||
refreshPolicy: gitea-controlled-snapshot
|
||||
argo: &nc01-argo
|
||||
namespace: argocd
|
||||
projectName: hwlab-v03
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
targetRevision: v0.3-gitops
|
||||
maintenance:
|
||||
<<: *maintenance
|
||||
monitorWeb:
|
||||
<<: *monitor-web
|
||||
gitMirror:
|
||||
source: source.gitMirrorReadUrl
|
||||
preSync: not-required
|
||||
postFlush: not-required
|
||||
confirmWait:
|
||||
<<: *confirm-wait
|
||||
publishCurrent:
|
||||
<<: *publish-current
|
||||
|
||||
sentinels:
|
||||
nc01-web-probe-sentinel:
|
||||
sentinel:
|
||||
<<: *sentinel-base
|
||||
id: nc01-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:
|
||||
<<: *nc01-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
|
||||
kubernetesApi:
|
||||
endpoint:
|
||||
host: 10.43.0.1
|
||||
port: 443
|
||||
egress:
|
||||
enabled: true
|
||||
rules:
|
||||
- cidr: 10.43.0.1/32
|
||||
port: 443
|
||||
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: 22084
|
||||
localIP: hwlab-web-probe-sentinel-${nodeLower}.hwlab-${lane}.svc.cluster.local
|
||||
localPort: 8080
|
||||
caddy:
|
||||
<<: *caddy-common
|
||||
managedBlockOwner: hwlab-web-probe-sentinel-${nodeLower}-${lane}
|
||||
cicd:
|
||||
<<: *nc01-cicd-common
|
||||
builder:
|
||||
<<: *cicd-builder
|
||||
jobPrefix: web-probe-sentinel-${nodeLower}-publish
|
||||
gitopsPath: deploy/gitops/node/${nodeLower}/web-probe-sentinel
|
||||
argo:
|
||||
<<: *nc01-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[2].tekton.toolsImage.output
|
||||
envRecipeRef: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.runtime
|
||||
targetValidation:
|
||||
scenarioId: workbench-dsflash-go-hwpod-two-turn-freeze-repro
|
||||
maxSeconds: 600
|
||||
serviceUnavailablePolicy: structured-failure
|
||||
cadenceScheduler:
|
||||
enabled: true
|
||||
reason: k8s-native-periodic-quick-verify
|
||||
concurrencyPolicy: Forbid
|
||||
startingDeadlineSeconds: 600
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 5
|
||||
activeDeadlineSlackSeconds: 60
|
||||
ttlSecondsAfterFinished: 86400
|
||||
backoffLimit: 0
|
||||
secrets:
|
||||
sources:
|
||||
- purpose: bootstrap-admin
|
||||
sourceRef: hwlab/${nodeLower}-${lane}-bootstrap-admin.env
|
||||
sourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
- <<: *dsflash-prompt-source
|
||||
- purpose: account-a
|
||||
sourceRef: hwlab/${nodeLower}-${lane}-bootstrap-admin.env
|
||||
sourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD
|
||||
format: web-account-json
|
||||
username: admin
|
||||
- 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,56 @@
|
||||
version: 1
|
||||
kind: postgres-host-cluster
|
||||
|
||||
metadata:
|
||||
id: nc01-host-postgres
|
||||
description: NC01 host-native PostgreSQL 16 for local UniDesk/HWLAB runtime state
|
||||
owner: unidesk
|
||||
|
||||
cluster:
|
||||
role: primary
|
||||
environment: node-local
|
||||
exposure: private-only
|
||||
haPhase: standalone
|
||||
|
||||
node:
|
||||
id: NC01
|
||||
route: NC01
|
||||
mode: host-systemd
|
||||
osFamily: ubuntu
|
||||
|
||||
postgres:
|
||||
package:
|
||||
manager: apt
|
||||
version: "16"
|
||||
service:
|
||||
name: postgresql
|
||||
enabled: true
|
||||
state: running
|
||||
network:
|
||||
port: 5432
|
||||
listenAddresses:
|
||||
- 127.0.0.1
|
||||
- 10.42.0.1
|
||||
connectionHost: 10.42.0.1
|
||||
transport: postgres-native-tls
|
||||
sslmode: require
|
||||
auth:
|
||||
passwordEncryption: scram-sha-256
|
||||
pgHba:
|
||||
- type: local
|
||||
database: all
|
||||
user: postgres
|
||||
method: peer
|
||||
- type: host
|
||||
database: all
|
||||
user: all
|
||||
address: 127.0.0.1/32
|
||||
method: scram-sha-256
|
||||
- type: host
|
||||
database: all
|
||||
user: all
|
||||
address: 10.42.0.0/16
|
||||
method: scram-sha-256
|
||||
|
||||
secrets:
|
||||
root: /root/unidesk/.state/secrets
|
||||
@@ -5,6 +5,16 @@ metadata:
|
||||
relatedIssues:
|
||||
- 1004
|
||||
sources:
|
||||
nc01-vpn-server-shadowsocks:
|
||||
sourceType: master-shadowsocks
|
||||
sourceRef: platform-infra/nc01-vpn-server-shadowsocks.env
|
||||
sourceKey: VPN_SERVER_SHADOWSOCKS_PASSWORD
|
||||
masterShadowsocks:
|
||||
serverHost: 152-53-229-148.nip.io
|
||||
serverPort: 8445
|
||||
method: 2022-blake3-aes-256-gcm
|
||||
healthProbeUrl: https://www.gstatic.com/generate_204
|
||||
|
||||
master-shadowsocks:
|
||||
sourceType: master-shadowsocks
|
||||
sourceRef: platform-infra/sub2api-master-egress-proxy.env
|
||||
|
||||
@@ -44,8 +44,9 @@ sourceAuthority:
|
||||
- snapshot-create
|
||||
github:
|
||||
transport: https-token
|
||||
sourceRef: /root/.config/unidesk/github.env
|
||||
sourceRef: .env/gh_token.txt
|
||||
sourceKey: GH_TOKEN
|
||||
format: raw-token
|
||||
requiredFor:
|
||||
- upstream-mirror
|
||||
- mirror-sync
|
||||
@@ -123,6 +124,28 @@ sourceAuthority:
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
configRef: config/cicd-branch-followers.yaml#followers.agentrun-jd01-v02.nativeStatus.source.gitMirrorReadUrl
|
||||
disposition: replaced-by-gitea
|
||||
- key: agentrun-nc01-v02
|
||||
targetId: NC01
|
||||
upstream:
|
||||
repository: pikasTech/agentrun
|
||||
cloneUrl: https://github.com/pikasTech/agentrun.git
|
||||
branch: v0.2
|
||||
gitea:
|
||||
owner: mirrors
|
||||
name: pikasTech-agentrun
|
||||
mirrorMode: controlled-push
|
||||
publicRead: true
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
gitops:
|
||||
branch: nc01-v0.2-gitops
|
||||
flushDisposition: retained-for-gitops-flush
|
||||
snapshot:
|
||||
naming: historical-gitea-actions-prefix-retained-for-existing-refs
|
||||
prefix: refs/unidesk/snapshots/gitea-actions/agentrun-v0.2
|
||||
legacyGitMirror:
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
configRef: config/cicd-branch-followers.yaml#followers.agentrun-nc01-v02.nativeStatus.source.gitMirrorReadUrl
|
||||
disposition: replaced-by-gitea
|
||||
- key: unidesk-master
|
||||
targetId: JD01
|
||||
upstream:
|
||||
@@ -145,6 +168,28 @@ sourceAuthority:
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
|
||||
configRef: config/cicd-branch-followers.yaml#controller.source.gitMirrorReadUrl
|
||||
disposition: replaced-by-gitea
|
||||
- key: unidesk-master-nc01
|
||||
targetId: NC01
|
||||
upstream:
|
||||
repository: pikasTech/unidesk
|
||||
cloneUrl: https://github.com/pikasTech/unidesk.git
|
||||
branch: master
|
||||
gitea:
|
||||
owner: mirrors
|
||||
name: pikasTech-unidesk
|
||||
mirrorMode: controlled-push
|
||||
publicRead: true
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git
|
||||
gitops:
|
||||
branch: master
|
||||
flushDisposition: not-a-gitops-branch
|
||||
snapshot:
|
||||
naming: historical-gitea-actions-prefix-retained-for-existing-refs
|
||||
prefix: refs/unidesk/snapshots/gitea-actions/unidesk-master-nc01
|
||||
legacyGitMirror:
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
|
||||
configRef: config/cicd-branch-followers.yaml#controller.source.gitMirrorReadUrl
|
||||
disposition: replaced-by-gitea
|
||||
- key: hwlab-jd01-v03
|
||||
targetId: JD01
|
||||
upstream:
|
||||
@@ -167,6 +212,28 @@ sourceAuthority:
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
configRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01.git.readUrl
|
||||
disposition: replaced-by-gitea
|
||||
- key: hwlab-nc01-v03
|
||||
targetId: NC01
|
||||
upstream:
|
||||
repository: pikasTech/HWLAB
|
||||
cloneUrl: https://github.com/pikasTech/HWLAB.git
|
||||
branch: v0.3
|
||||
gitea:
|
||||
owner: mirrors
|
||||
name: pikasTech-HWLAB
|
||||
mirrorMode: controlled-push
|
||||
publicRead: true
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git
|
||||
gitops:
|
||||
branch: v0.3-gitops
|
||||
flushDisposition: retained-for-gitops-flush
|
||||
snapshot:
|
||||
naming: historical-gitea-actions-prefix-retained-for-existing-refs
|
||||
prefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/v0.3
|
||||
legacyGitMirror:
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
configRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01.git.readUrl
|
||||
disposition: replaced-by-gitea
|
||||
|
||||
targets:
|
||||
- id: JD01
|
||||
@@ -176,6 +243,15 @@ targets:
|
||||
enabled: true
|
||||
createNamespace: true
|
||||
storageClassName: local-path
|
||||
- id: NC01
|
||||
route: NC01:k3s
|
||||
namespace: devops-infra
|
||||
role: active-source-authority
|
||||
enabled: true
|
||||
createNamespace: true
|
||||
storageClassName: local-path
|
||||
publicExposure:
|
||||
enabled: false
|
||||
|
||||
app:
|
||||
name: gitea
|
||||
|
||||
@@ -29,6 +29,38 @@ server:
|
||||
port: 18792
|
||||
|
||||
sources:
|
||||
nc01-vpn-server-shadowsocks:
|
||||
sourceType: vpn-server-shadowsocks
|
||||
serverRef: /root/vpn-server
|
||||
benchmarkRef: local-vpn-server
|
||||
implementationRef: /root/vpn-server
|
||||
sourceConfigRef: config/platform-infra/egress-proxy-sources.yaml#sources.nc01-vpn-server-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
|
||||
|
||||
jd01-real-deps-master-shadowsocks:
|
||||
sourceType: benchmark-validated-master-shadowsocks
|
||||
serverRef: server.benchmark-validated-master-shadowsocks-server
|
||||
@@ -62,6 +94,59 @@ sources:
|
||||
externalProbeUrl: https://www.gstatic.com/generate_204
|
||||
|
||||
targets:
|
||||
NC01:
|
||||
route: NC01
|
||||
enabled: true
|
||||
sourceRef: sources.nc01-vpn-server-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
|
||||
- 152.53.229.148
|
||||
- 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
|
||||
trans:
|
||||
hostProxyEnv:
|
||||
enabled: true
|
||||
envFileRef: files.envFile
|
||||
applyTo: host-posix-commands
|
||||
apply:
|
||||
reloadSystemd: true
|
||||
restartDocker: true
|
||||
restartK3s: true
|
||||
|
||||
JD01:
|
||||
route: JD01
|
||||
enabled: true
|
||||
|
||||
@@ -12,15 +12,15 @@ metadata:
|
||||
- 1560
|
||||
|
||||
defaults:
|
||||
targetId: JD01
|
||||
consumerId: agentrun-jd01-v02
|
||||
targetId: NC01
|
||||
consumerId: hwlab-nc01-v03
|
||||
|
||||
display:
|
||||
timeZone: Asia/Shanghai
|
||||
|
||||
release:
|
||||
version: v0.48.0
|
||||
manifestUrl: https://github.com/tektoncd/pipelines-as-code/releases/download/v0.48.0/release.k8s.yaml
|
||||
manifestUrl: https://github.com/openshift-pipelines/pipelines-as-code/releases/download/v0.48.0/release.k8s.yaml
|
||||
namespace: pipelines-as-code
|
||||
controllerServiceName: pipelines-as-code-controller
|
||||
controllerServicePort: 8080
|
||||
@@ -38,6 +38,11 @@ targets:
|
||||
namespace: agentrun-ci
|
||||
role: active-cicd-trigger-authority
|
||||
enabled: true
|
||||
- id: NC01
|
||||
route: NC01:k3s
|
||||
namespace: agentrun-ci
|
||||
role: active-cicd-trigger-authority
|
||||
enabled: true
|
||||
|
||||
gitea:
|
||||
configRef: config/platform-infra/gitea.yaml#sourceAuthority.repositories
|
||||
@@ -48,7 +53,7 @@ gitea:
|
||||
format: line-pair
|
||||
usernameLine: 1
|
||||
passwordLine: 2
|
||||
apiUsername: admin
|
||||
apiUsername: unidesk-admin
|
||||
webhook:
|
||||
secretRoot: /root/unidesk
|
||||
secretSourceRef: .env/pipelines-as-code.webhook
|
||||
@@ -80,6 +85,29 @@ repositories:
|
||||
pipeline_run_prefix: agentrun-jd01-v02-ci
|
||||
service_account: agentrun-jd01-v02-tekton-runner
|
||||
workspace_pvc_size: 2Gi
|
||||
- id: agentrun-nc01-v02
|
||||
name: agentrun-nc01-v02
|
||||
namespace: agentrun-ci
|
||||
providerType: gitea
|
||||
url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun
|
||||
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
owner: mirrors
|
||||
repo: pikasTech-agentrun
|
||||
secretName: pac-gitea-agentrun-nc01-v02
|
||||
tokenKey: token
|
||||
webhookSecretKey: webhook.secret
|
||||
concurrencyLimit: 1
|
||||
params:
|
||||
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
git_write_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||
source_branch: v0.2
|
||||
gitops_branch: nc01-v0.2-gitops
|
||||
# Historical prefix name retained for existing snapshot refs; active architecture is Gitea + PaC, not Gitea Actions.
|
||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/agentrun-v0.2
|
||||
pipeline_name: agentrun-nc01-v02-ci-image-publish
|
||||
pipeline_run_prefix: agentrun-nc01-v02-ci
|
||||
service_account: agentrun-nc01-v02-tekton-runner
|
||||
workspace_pvc_size: 2Gi
|
||||
- id: sentinel-jd01-v03
|
||||
name: sentinel-jd01-v03
|
||||
namespace: devops-infra
|
||||
@@ -106,6 +134,32 @@ repositories:
|
||||
gitops_manifest_path: deploy/gitops/node/jd01/web-probe-sentinel/web-probe-sentinel.yaml
|
||||
pipeline_name: hwlab-web-probe-sentinel-jd01-pac
|
||||
pipeline_run_prefix: hwlab-web-probe-sentinel-jd01
|
||||
- id: sentinel-nc01-v03
|
||||
name: sentinel-nc01-v03
|
||||
namespace: devops-infra
|
||||
providerType: gitea
|
||||
url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk
|
||||
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git
|
||||
owner: mirrors
|
||||
repo: pikasTech-unidesk
|
||||
secretName: pac-gitea-sentinel-nc01-v03
|
||||
tokenKey: token
|
||||
webhookSecretKey: webhook.secret
|
||||
concurrencyLimit: 1
|
||||
params:
|
||||
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git
|
||||
source_branch: master
|
||||
# Historical prefix name retained for existing snapshot refs; active architecture is Gitea + PaC, not Gitea Actions.
|
||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/unidesk-master
|
||||
node: NC01
|
||||
lane: v03
|
||||
sentinel_id: nc01-web-probe-sentinel
|
||||
image_repository: 127.0.0.1:5000/hwlab/web-probe-sentinel-nc01
|
||||
registry_probe_base: http://127.0.0.1:5000
|
||||
gitops_branch: v0.3-gitops
|
||||
gitops_manifest_path: deploy/gitops/node/nc01/web-probe-sentinel/web-probe-sentinel.yaml
|
||||
pipeline_name: hwlab-web-probe-sentinel-nc01-pac
|
||||
pipeline_run_prefix: hwlab-web-probe-sentinel-nc01
|
||||
- id: hwlab-jd01-v03
|
||||
name: hwlab-jd01-v03
|
||||
namespace: hwlab-ci
|
||||
@@ -131,6 +185,31 @@ repositories:
|
||||
node: JD01
|
||||
lane: v03
|
||||
runtime_namespace: hwlab-v03
|
||||
- id: hwlab-nc01-v03
|
||||
name: hwlab-nc01-v03
|
||||
namespace: hwlab-ci
|
||||
providerType: gitea
|
||||
url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB
|
||||
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git
|
||||
owner: mirrors
|
||||
repo: pikasTech-HWLAB
|
||||
secretName: pac-gitea-hwlab-nc01-v03
|
||||
tokenKey: token
|
||||
webhookSecretKey: webhook.secret
|
||||
concurrencyLimit: 1
|
||||
params:
|
||||
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git
|
||||
git_write_url: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
source_branch: v0.3
|
||||
gitops_branch: v0.3-gitops
|
||||
# Historical prefix name retained for existing snapshot refs; active architecture is Gitea + PaC, not Gitea Actions.
|
||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/v0.3
|
||||
pipeline_name: hwlab-nc01-v03-ci-image-publish
|
||||
pipeline_run_prefix: hwlab-nc01-v03-ci-poll
|
||||
service_account: hwlab-nc01-v03-tekton-runner
|
||||
node: NC01
|
||||
lane: v03
|
||||
runtime_namespace: hwlab-v03
|
||||
|
||||
consumers:
|
||||
- id: agentrun-jd01-v02
|
||||
@@ -142,6 +221,15 @@ consumers:
|
||||
pipelineRunPrefix: agentrun-jd01-v02-ci
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-jd01-v02
|
||||
- id: agentrun-nc01-v02
|
||||
repositoryRef: agentrun-nc01-v02
|
||||
node: NC01
|
||||
lane: nc01-v02
|
||||
namespace: agentrun-ci
|
||||
pipeline: agentrun-nc01-v02-ci-image-publish
|
||||
pipelineRunPrefix: agentrun-nc01-v02-ci
|
||||
argoNamespace: argocd
|
||||
argoApplication: agentrun-nc01-v02
|
||||
- id: sentinel-jd01-v03
|
||||
repositoryRef: sentinel-jd01-v03
|
||||
node: JD01
|
||||
@@ -151,6 +239,15 @@ consumers:
|
||||
pipelineRunPrefix: hwlab-web-probe-sentinel-jd01
|
||||
argoNamespace: argocd
|
||||
argoApplication: hwlab-web-probe-sentinel-jd01
|
||||
- id: sentinel-nc01-v03
|
||||
repositoryRef: sentinel-nc01-v03
|
||||
node: NC01
|
||||
lane: v03
|
||||
namespace: devops-infra
|
||||
pipeline: hwlab-web-probe-sentinel-nc01-pac
|
||||
pipelineRunPrefix: hwlab-web-probe-sentinel-nc01
|
||||
argoNamespace: argocd
|
||||
argoApplication: hwlab-web-probe-sentinel-nc01
|
||||
- id: hwlab-jd01-v03
|
||||
repositoryRef: hwlab-jd01-v03
|
||||
node: JD01
|
||||
@@ -161,3 +258,14 @@ consumers:
|
||||
argoNamespace: argocd
|
||||
argoApplication: hwlab-node-v03
|
||||
closeoutGitOpsMirrorFlush: true
|
||||
|
||||
- id: hwlab-nc01-v03
|
||||
repositoryRef: hwlab-nc01-v03
|
||||
node: NC01
|
||||
lane: v03
|
||||
namespace: hwlab-ci
|
||||
pipeline: hwlab-nc01-v03-ci-image-publish
|
||||
pipelineRunPrefix: hwlab-nc01-v03-ci-poll
|
||||
argoNamespace: argocd
|
||||
argoApplication: hwlab-node-v03
|
||||
closeoutGitOpsMirrorFlush: true
|
||||
|
||||
@@ -77,6 +77,19 @@ sources:
|
||||
OPENCODE_SERVER_PASSWORD:
|
||||
bytes: 32
|
||||
prefix: oc_
|
||||
- sourceRef: hwlab/nc01-v03-opencode.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- OPENCODE_SERVER_USERNAME
|
||||
- OPENCODE_SERVER_PASSWORD
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
OPENCODE_SERVER_USERNAME: opencode
|
||||
randomBase64Url:
|
||||
OPENCODE_SERVER_PASSWORD:
|
||||
bytes: 32
|
||||
prefix: oc_
|
||||
|
||||
targets:
|
||||
- id: platform-infra-g14
|
||||
@@ -89,8 +102,24 @@ targets:
|
||||
namespace: hwlab-v03
|
||||
scope: hwlab
|
||||
enabled: true
|
||||
- id: hwlab-nc01-v03
|
||||
route: NC01:k3s
|
||||
namespace: hwlab-v03
|
||||
scope: hwlab
|
||||
enabled: true
|
||||
|
||||
kubernetesSecrets:
|
||||
- name: hwlab-nc01-v03-opencode-server-auth
|
||||
targetId: hwlab-nc01-v03
|
||||
secretName: hwlab-v03-opencode-server-auth
|
||||
type: Opaque
|
||||
data:
|
||||
- sourceRef: hwlab/nc01-v03-opencode.env
|
||||
sourceKey: OPENCODE_SERVER_USERNAME
|
||||
targetKey: username
|
||||
- sourceRef: hwlab/nc01-v03-opencode.env
|
||||
sourceKey: OPENCODE_SERVER_PASSWORD
|
||||
targetKey: password
|
||||
- name: hwlab-jd01-v03-opencode-server-auth
|
||||
targetId: hwlab-jd01-v03
|
||||
secretName: hwlab-v03-opencode-server-auth
|
||||
|
||||
@@ -6,6 +6,14 @@ output:
|
||||
includePreview: false
|
||||
warning: "CLI stdout exceeded YAML-configured limit; full output was dumped to /tmp for one-off drill-down only. This is a CLI usability defect: improve the command itself to print concise tables/summaries and id-specific progressive disclosure instead of repeatedly depending on dump extraction."
|
||||
github:
|
||||
auth:
|
||||
tokenSource:
|
||||
enabled: true
|
||||
priority: before-env
|
||||
root: .
|
||||
sourceRef: .env/gh_token.txt
|
||||
sourceKey: GH_TOKEN
|
||||
format: raw-token
|
||||
prMerge:
|
||||
unknownRetry:
|
||||
maxAttempts: 5
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
version: 1
|
||||
kind: UniDeskHostK8sDeployment
|
||||
metadata:
|
||||
name: unidesk-host
|
||||
owner: unidesk
|
||||
|
||||
target:
|
||||
id: NC01
|
||||
kubeContext: local-k3s
|
||||
namespace: unidesk
|
||||
|
||||
runtime:
|
||||
environment: prod
|
||||
deployRef: config/unidesk-host-k8s.yaml
|
||||
repository: https://github.com/pikasTech/unidesk
|
||||
commit: local-host-k8s
|
||||
publicHost: 152.53.229.148
|
||||
noProxy: localhost,127.0.0.1,::1,.svc,.svc.cluster.local,hyueapi.com,.hyueapi.com
|
||||
|
||||
images:
|
||||
postgres: postgres:16-alpine
|
||||
backendCore: unidesk-backend-core:host-k8s
|
||||
frontend: unidesk-frontend:host-k8s
|
||||
|
||||
database:
|
||||
mode: host-native
|
||||
host: 10.42.0.1
|
||||
serviceName: database
|
||||
pvcName: unidesk-postgres-data
|
||||
storageSize: 15Gi
|
||||
storageClassName: local-path
|
||||
port: 5432
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
services:
|
||||
backendCore:
|
||||
deploymentName: backend-core
|
||||
serviceName: backend-core
|
||||
providerPublicServiceName: backend-core-provider
|
||||
containerPort: 8080
|
||||
providerPort: 8081
|
||||
providerDataPort: 8082
|
||||
providerPublicPort: 18082
|
||||
providerDataPublicPort: 18084
|
||||
logFile: /var/log/unidesk/backend-core.jsonl
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 192Mi
|
||||
limits:
|
||||
memory: 768Mi
|
||||
frontend:
|
||||
deploymentName: frontend
|
||||
serviceName: frontend
|
||||
containerPort: 8080
|
||||
publicPort: 18081
|
||||
logFile: /var/log/unidesk/frontend.jsonl
|
||||
resources:
|
||||
requests:
|
||||
cpu: 80m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
|
||||
runtimeConfig:
|
||||
heartBeatTimeoutMs: "30000"
|
||||
taskPendingTimeoutMs: "600000"
|
||||
databasePoolMax: "4"
|
||||
sessionTtlSeconds: "28800"
|
||||
authUsername: admin
|
||||
sshClientRouteAllowlist: NC01,NC01:*,JD01,JD01:*,PK01,PK01:*
|
||||
microservicesJson: "[]"
|
||||
|
||||
runtimeSecrets:
|
||||
sourceRef:
|
||||
path: .state/secrets/unidesk-host-k8s.env
|
||||
target:
|
||||
name: unidesk-runtime-secrets
|
||||
keys:
|
||||
postgresUser: POSTGRES_USER
|
||||
postgresPassword: POSTGRES_PASSWORD
|
||||
postgresDb: POSTGRES_DB
|
||||
databaseUrl: DATABASE_URL
|
||||
providerToken: PROVIDER_TOKEN
|
||||
sshClientToken: UNIDESK_SSH_CLIENT_TOKEN
|
||||
authPassword: AUTH_PASSWORD
|
||||
sessionSecret: SESSION_SECRET
|
||||
@@ -0,0 +1,41 @@
|
||||
# NC01 Reference
|
||||
|
||||
NC01 is the current host registered as a UniDesk provider-gateway node. Host maintenance uses `trans NC01 ...`; Kubernetes maintenance uses `trans NC01:k3s ...`.
|
||||
|
||||
## Source Of Truth
|
||||
|
||||
- Host k8s/provider config: `config/unidesk-host-k8s.yaml`.
|
||||
- Host proxy config: `config/platform-infra/host-proxy.yaml#targets.NC01`; NC01 host-proxy uses `/root/vpn-server` as the VPN server source.
|
||||
- HWLAB node/lane config: `config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01`.
|
||||
- HWLAB control-plane config: `config/hwlab-node-control-plane.yaml#nodes.NC01`.
|
||||
- AgentRun config: `config/agentrun.yaml`, lane `nc01-v02`.
|
||||
- NC01 host PostgreSQL config: `config/platform-db/postgres-nc01.yaml`.
|
||||
|
||||
## Fixed Repos
|
||||
|
||||
- HWLAB v0.3 fixed repo: `/root/hwlab`, branch `v0.3`.
|
||||
- AgentRun v0.2 fixed repo: `/root/agentrun`, branch `v0.2`.
|
||||
- UniDesk source/config authority remains `/root/unidesk`.
|
||||
|
||||
## Database Boundary
|
||||
|
||||
NC01 databases run on host-native PostgreSQL, not Docker or Kubernetes. Kubernetes workloads that need PostgreSQL must use YAML-declared external PostgreSQL bridge objects and Secrets.
|
||||
|
||||
For HWLAB v0.3, the Kubernetes Service `nc01-host-postgres` in namespace `hwlab-v03` points to host PostgreSQL at `10.42.0.1:5432`. Runtime database Secrets are sourced from `.state/secrets/hwlab/*` via YAML sourceRef and must only be reported as present/fingerprint metadata, never as full values.
|
||||
|
||||
## GitHub Token
|
||||
|
||||
The local GitHub token source is `.env/gh_token.txt`. It must be stored as a bare token and consumed through controlled YAML/sourceRef or temporary non-printing credential helpers. Do not place the token in command argv, logs, committed files, or rendered manifests.
|
||||
|
||||
## Public Exposure
|
||||
|
||||
NC01 HWLAB v0.3 does not require publicExposure unless YAML explicitly declares it and a real FRP token source is available. When publicExposure is absent, control-plane public probe is skipped and must not block runtime readiness.
|
||||
|
||||
Web-probe sentinel deployments that need public ingress must declare their own YAML publicExposure and Secret sourceRef. Do not create placeholder FRP tokens; a missing `platform-infra/pk01-frp.env` token source is a deployment blocker for FRP-backed public exposure.
|
||||
|
||||
## Validation Entrypoints
|
||||
|
||||
- Provider/trans: `scripts/trans NC01 argv true`.
|
||||
- NC01 k8s route: `scripts/trans NC01:k3s kubectl get nodes -o wide`.
|
||||
- HWLAB v0.3: `bun scripts/cli.ts hwlab nodes control-plane status --node NC01 --lane v03 --full`.
|
||||
- AgentRun v0.2: `bun scripts/cli.ts agentrun control-plane status --node NC01 --lane nc01-v02`.
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
#!/usr/bin/env bun
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
function required(value, path) {
|
||||
if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function record(value, path) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function readEnvFile(path) {
|
||||
const values = {};
|
||||
const text = readFileSync(path, "utf8");
|
||||
for (const [index, rawLine] of text.split(/\r?\n/u).entries()) {
|
||||
const line = rawLine.trim();
|
||||
if (line.length === 0 || line.startsWith("#")) continue;
|
||||
const eq = line.indexOf("=");
|
||||
if (eq <= 0) throw new Error(`${path}:${index + 1} must be KEY=value`);
|
||||
values[line.slice(0, eq)] = line.slice(eq + 1);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function sha(value) {
|
||||
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
function yamlScalar(value) {
|
||||
return JSON.stringify(String(value));
|
||||
}
|
||||
|
||||
function indent(text, spaces) {
|
||||
const pad = " ".repeat(spaces);
|
||||
return text.split("\n").map((line) => line.length > 0 ? `${pad}${line}` : line).join("\n");
|
||||
}
|
||||
|
||||
function labels(config) {
|
||||
const name = required(record(config.metadata, "metadata").name, "metadata.name");
|
||||
return [
|
||||
"app.kubernetes.io/part-of: unidesk",
|
||||
`unidesk.ai/deployment: ${name}`,
|
||||
`unidesk.ai/target: ${required(record(config.target, "target").id, "target.id")}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function envFromSecret(secretName, keyName, envName) {
|
||||
return [
|
||||
`- name: ${envName}`,
|
||||
" valueFrom:",
|
||||
" secretKeyRef:",
|
||||
` name: ${secretName}`,
|
||||
` key: ${keyName}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function envFromConfig(configName, keyName, envName) {
|
||||
return [
|
||||
`- name: ${envName}`,
|
||||
" valueFrom:",
|
||||
" configMapKeyRef:",
|
||||
` name: ${configName}`,
|
||||
` key: ${keyName}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function resourceBlock(resources) {
|
||||
const requests = record(record(resources, "resources").requests, "resources.requests");
|
||||
const limits = record(resources.limits, "resources.limits");
|
||||
return [
|
||||
"resources:",
|
||||
" requests:",
|
||||
` cpu: ${yamlScalar(required(requests.cpu, "resources.requests.cpu"))}`,
|
||||
` memory: ${yamlScalar(required(requests.memory, "resources.requests.memory"))}`,
|
||||
" limits:",
|
||||
...(limits.cpu ? [` cpu: ${yamlScalar(limits.cpu)}`] : []),
|
||||
` memory: ${yamlScalar(required(limits.memory, "resources.limits.memory"))}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const configPath = process.argv[2] ?? "config/unidesk-host-k8s.yaml";
|
||||
const config = Bun.YAML.parse(readFileSync(configPath, "utf8"));
|
||||
const target = record(config.target, "target");
|
||||
const runtime = record(config.runtime, "runtime");
|
||||
const images = record(config.images, "images");
|
||||
const database = record(config.database, "database");
|
||||
const services = record(config.services, "services");
|
||||
const backend = record(services.backendCore, "services.backendCore");
|
||||
const frontend = record(services.frontend, "services.frontend");
|
||||
const runtimeConfig = record(config.runtimeConfig, "runtimeConfig");
|
||||
const runtimeSecrets = record(config.runtimeSecrets, "runtimeSecrets");
|
||||
const secretTarget = record(runtimeSecrets.target, "runtimeSecrets.target");
|
||||
const secretKeys = record(secretTarget.keys, "runtimeSecrets.target.keys");
|
||||
const namespace = required(target.namespace, "target.namespace");
|
||||
const secretName = required(secretTarget.name, "runtimeSecrets.target.name");
|
||||
const sourcePath = resolve(required(record(runtimeSecrets.sourceRef, "runtimeSecrets.sourceRef").path, "runtimeSecrets.sourceRef.path"));
|
||||
const secrets = readEnvFile(sourcePath);
|
||||
const requiredSecretKeys = Object.values(secretKeys);
|
||||
for (const key of requiredSecretKeys) {
|
||||
if (typeof secrets[key] !== "string" || secrets[key].length === 0) throw new Error(`missing ${key} in ${sourcePath}`);
|
||||
}
|
||||
const secretFingerprint = sha(requiredSecretKeys.map((key) => `${key}=${secrets[key]}`).join("\n"));
|
||||
const runtimeConfigFingerprint = sha(JSON.stringify(runtimeConfig));
|
||||
const runtimeConfigName = "unidesk-runtime-config";
|
||||
const databaseMode = database.mode === undefined ? "k8s-postgres" : required(database.mode, "database.mode");
|
||||
if (databaseMode !== "k8s-postgres" && databaseMode !== "host-native") throw new Error("database.mode must be k8s-postgres or host-native");
|
||||
const dbService = required(database.serviceName, "database.serviceName");
|
||||
const dbHost = databaseMode === "host-native" ? required(database.host, "database.host") : `${dbService}.${namespace}.svc.cluster.local`;
|
||||
const dbPort = required(String(database.port), "database.port");
|
||||
const dbName = secrets[secretKeys.postgresDb];
|
||||
if (secrets[secretKeys.databaseUrl] !== `postgres://${secrets[secretKeys.postgresUser]}:${secrets[secretKeys.postgresPassword]}@${dbHost}:${dbPort}/${dbName}`) {
|
||||
throw new Error(`DATABASE_URL in ${sourcePath} must target ${dbHost}:${dbPort}/${dbName}`);
|
||||
}
|
||||
|
||||
const docs = [];
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}`);
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ${secretName}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
annotations:
|
||||
unidesk.ai/source-ref: ${yamlScalar(required(record(runtimeSecrets.sourceRef, "runtimeSecrets.sourceRef").path, "runtimeSecrets.sourceRef.path"))}
|
||||
unidesk.ai/fingerprint: ${yamlScalar(secretFingerprint)}
|
||||
type: Opaque
|
||||
stringData:
|
||||
${indent(requiredSecretKeys.map((key) => `${key}: ${yamlScalar(secrets[key])}`).join("\n"), 2)}`);
|
||||
docs.push(`apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ${runtimeConfigName}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
data:
|
||||
UNIDESK_ENV: ${yamlScalar(required(runtime.environment, "runtime.environment"))}
|
||||
UNIDESK_NAMESPACE: ${yamlScalar(namespace)}
|
||||
UNIDESK_DEPLOY_REF: ${yamlScalar(required(runtime.deployRef, "runtime.deployRef"))}
|
||||
UNIDESK_DEPLOY_REPO: ${yamlScalar(required(runtime.repository, "runtime.repository"))}
|
||||
UNIDESK_DEPLOY_COMMIT: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
|
||||
UNIDESK_DEPLOY_REQUESTED_COMMIT: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
|
||||
UNIDESK_DATABASE_NAME: ${yamlScalar(dbName)}
|
||||
UNIDESK_DATABASE_MODE: ${yamlScalar(databaseMode)}
|
||||
UNIDESK_DATABASE_HOST: ${yamlScalar(dbHost)}
|
||||
UNIDESK_DATABASE_PORT: ${yamlScalar(dbPort)}
|
||||
DATABASE_VOLUME_NAME: ${yamlScalar(required(database.pvcName, "database.pvcName"))}
|
||||
DATABASE_VOLUME_SIZE: ${yamlScalar(required(database.storageSize, "database.storageSize"))}
|
||||
HEARTBEAT_TIMEOUT_MS: ${yamlScalar(required(runtimeConfig.heartBeatTimeoutMs, "runtimeConfig.heartBeatTimeoutMs"))}
|
||||
TASK_PENDING_TIMEOUT_MS: ${yamlScalar(required(runtimeConfig.taskPendingTimeoutMs, "runtimeConfig.taskPendingTimeoutMs"))}
|
||||
DATABASE_POOL_MAX: ${yamlScalar(required(runtimeConfig.databasePoolMax, "runtimeConfig.databasePoolMax"))}
|
||||
SESSION_TTL_SECONDS: ${yamlScalar(required(runtimeConfig.sessionTtlSeconds, "runtimeConfig.sessionTtlSeconds"))}
|
||||
AUTH_USERNAME: ${yamlScalar(required(runtimeConfig.authUsername, "runtimeConfig.authUsername"))}
|
||||
UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: ${yamlScalar(required(runtimeConfig.sshClientRouteAllowlist, "runtimeConfig.sshClientRouteAllowlist"))}
|
||||
MICROSERVICES_JSON: ${yamlScalar(required(runtimeConfig.microservicesJson, "runtimeConfig.microservicesJson"))}
|
||||
NO_PROXY: ${yamlScalar(required(runtime.noProxy, "runtime.noProxy"))}
|
||||
no_proxy: ${yamlScalar(required(runtime.noProxy, "runtime.noProxy"))}`);
|
||||
if (databaseMode === "k8s-postgres") {
|
||||
docs.push(`apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: ${required(database.pvcName, "database.pvcName")}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: ${required(database.storageClassName, "database.storageClassName")}
|
||||
resources:
|
||||
requests:
|
||||
storage: ${required(database.storageSize, "database.storageSize")}`);
|
||||
docs.push(`apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: unidesk-db-init
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
data:
|
||||
001_unidesk_init.sql: |
|
||||
${indent(readFileSync("src/components/database/init/001_unidesk_init.sql", "utf8"), 4)}`);
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ${dbService}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: database
|
||||
ports:
|
||||
- name: pg
|
||||
port: ${database.port}
|
||||
targetPort: pg`);
|
||||
docs.push(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: database
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: database
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: database
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
annotations:
|
||||
unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
|
||||
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 70
|
||||
containers:
|
||||
- name: postgres
|
||||
image: ${required(images.postgres, "images.postgres")}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: pg
|
||||
containerPort: ${database.port}
|
||||
env:
|
||||
${indent(envFromSecret(secretName, secretKeys.postgresUser, "POSTGRES_USER"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.postgresPassword, "POSTGRES_PASSWORD"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.postgresDb, "POSTGRES_DB"), 12)}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: init
|
||||
mountPath: /docker-entrypoint-initdb.d
|
||||
readOnly: true
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["sh", "-ec", "pg_isready -U \\"$POSTGRES_USER\\" -d \\"$POSTGRES_DB\\""]
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 18
|
||||
startupProbe:
|
||||
exec:
|
||||
command: ["sh", "-ec", "pg_isready -U \\"$POSTGRES_USER\\" -d \\"$POSTGRES_DB\\""]
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 60
|
||||
${indent(resourceBlock(database.resources), 10)}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: ${required(database.pvcName, "database.pvcName")}
|
||||
- name: init
|
||||
configMap:
|
||||
name: unidesk-db-init`);
|
||||
}
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ${required(backend.serviceName, "services.backendCore.serviceName")}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: backend-core
|
||||
ports:
|
||||
- name: http
|
||||
port: ${backend.containerPort}
|
||||
targetPort: http
|
||||
- name: provider
|
||||
port: ${backend.providerPort}
|
||||
targetPort: provider
|
||||
- name: provider-data
|
||||
port: ${backend.providerDataPort}
|
||||
targetPort: provider-data`);
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ${required(backend.providerPublicServiceName, "services.backendCore.providerPublicServiceName")}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app.kubernetes.io/name: backend-core
|
||||
ports:
|
||||
- name: provider
|
||||
port: ${backend.providerPublicPort}
|
||||
targetPort: provider
|
||||
- name: provider-data
|
||||
port: ${backend.providerDataPublicPort}
|
||||
targetPort: provider-data`);
|
||||
docs.push(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ${required(backend.deploymentName, "services.backendCore.deploymentName")}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: backend-core
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: backend-core
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
annotations:
|
||||
unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
|
||||
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
||||
spec:
|
||||
initContainers:
|
||||
- name: wait-database
|
||||
image: ${required(images.postgres, "images.postgres")}
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: ${yamlScalar(dbHost)}
|
||||
- name: PGPORT
|
||||
value: ${yamlScalar(dbPort)}
|
||||
${indent(envFromSecret(secretName, secretKeys.postgresUser, "PGUSER"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.postgresPassword, "PGPASSWORD"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.postgresDb, "PGDATABASE"), 12)}
|
||||
command: ["sh", "-ec", "until pg_isready -h \\"$PGHOST\\" -p \\"$PGPORT\\" -U \\"$PGUSER\\" -d \\"$PGDATABASE\\" >/dev/null 2>&1; do sleep 2; done"]
|
||||
containers:
|
||||
- name: backend-core
|
||||
image: ${required(images.backendCore, "images.backendCore")}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: ${backend.containerPort}
|
||||
- name: provider
|
||||
containerPort: ${backend.providerPort}
|
||||
- name: provider-data
|
||||
containerPort: ${backend.providerDataPort}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${runtimeConfigName}
|
||||
env:
|
||||
- name: PORT
|
||||
value: ${yamlScalar(backend.containerPort)}
|
||||
- name: PROVIDER_PORT
|
||||
value: ${yamlScalar(backend.providerPort)}
|
||||
- name: PROVIDER_DATA_PORT
|
||||
value: ${yamlScalar(backend.providerDataPort)}
|
||||
${indent(envFromSecret(secretName, secretKeys.databaseUrl, "DATABASE_URL"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.providerToken, "PROVIDER_TOKEN"), 12)}
|
||||
- name: LOG_FILE
|
||||
value: ${yamlScalar(required(backend.logFile, "services.backendCore.logFile"))}
|
||||
volumeMounts:
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 20
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 60
|
||||
${indent(resourceBlock(backend.resources), 10)}
|
||||
volumes:
|
||||
- name: logs
|
||||
emptyDir: {}`);
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ${required(frontend.serviceName, "services.frontend.serviceName")}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app.kubernetes.io/name: frontend
|
||||
ports:
|
||||
- name: http
|
||||
port: ${frontend.publicPort}
|
||||
targetPort: http`);
|
||||
docs.push(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ${required(frontend.deploymentName, "services.frontend.deploymentName")}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
${indent(labels(config), 4)}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: frontend
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
annotations:
|
||||
unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
|
||||
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: ${required(images.frontend, "images.frontend")}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: ${frontend.containerPort}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${runtimeConfigName}
|
||||
env:
|
||||
- name: PORT
|
||||
value: ${yamlScalar(frontend.containerPort)}
|
||||
- name: CORE_INTERNAL_URL
|
||||
value: ${yamlScalar(`http://${backend.serviceName}.${namespace}.svc.cluster.local:${backend.containerPort}`)}
|
||||
- name: FRONTEND_PUBLIC_URL
|
||||
value: ${yamlScalar(`http://${runtime.publicHost}:${frontend.publicPort}`)}
|
||||
- name: PROVIDER_INGRESS_PUBLIC_URL
|
||||
value: ${yamlScalar(`ws://${runtime.publicHost}:${backend.providerPort}/ws/provider`)}
|
||||
${indent(envFromConfig(runtimeConfigName, "AUTH_USERNAME", "AUTH_USERNAME"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.authPassword, "AUTH_PASSWORD"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.providerToken, "PROVIDER_TOKEN"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.sshClientToken, "UNIDESK_SSH_CLIENT_TOKEN"), 12)}
|
||||
${indent(envFromSecret(secretName, secretKeys.sessionSecret, "SESSION_SECRET"), 12)}
|
||||
- name: LOG_FILE
|
||||
value: ${yamlScalar(required(frontend.logFile, "services.frontend.logFile"))}
|
||||
volumeMounts:
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 20
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 60
|
||||
${indent(resourceBlock(frontend.resources), 10)}
|
||||
volumes:
|
||||
- name: logs
|
||||
emptyDir: {}`);
|
||||
|
||||
process.stdout.write(`${docs.join("\n---\n")}\n`);
|
||||
@@ -48,6 +48,7 @@ export interface AgentRunLaneSecretSpec {
|
||||
readonly sourceRef: string;
|
||||
readonly sourceMode: "env" | "file";
|
||||
readonly sourceKey: string | null;
|
||||
readonly sourceFormat: "env" | "raw-token" | null;
|
||||
readonly targetRef: AgentRunSecretRef;
|
||||
readonly providerCredentialProfile: string | null;
|
||||
}
|
||||
@@ -407,6 +408,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
|
||||
sourceRef: secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
|
||||
sourceMode: secret.sourceMode,
|
||||
sourceKey: secret.sourceKey,
|
||||
sourceFormat: secret.sourceFormat,
|
||||
targetRef: secret.targetRef,
|
||||
providerCredential: secret.providerCredentialProfile === null ? null : { profile: secret.providerCredentialProfile },
|
||||
valuesPrinted: false,
|
||||
@@ -831,12 +833,16 @@ function parseLaneSecret(input: Record<string, unknown>, path: string): AgentRun
|
||||
if (sourceMode !== "env" && sourceMode !== "file") throw new Error(`${path}.sourceMode must be env or file`);
|
||||
const sourceKey = optionalStringField(input, "sourceKey", path) ?? null;
|
||||
if (sourceMode === "env" && sourceKey === null) throw new Error(`${path}.sourceKey is required when sourceMode is env`);
|
||||
const sourceFormat = optionalStringField(input, "sourceFormat", path) ?? null;
|
||||
if (sourceFormat !== null && sourceFormat !== "env" && sourceFormat !== "raw-token") throw new Error(`${path}.sourceFormat must be env or raw-token`);
|
||||
if (sourceFormat === "raw-token" && sourceMode !== "env") throw new Error(`${path}.sourceFormat raw-token requires sourceMode env`);
|
||||
const providerCredential = parseProviderCredentialBinding(input, `${path}.providerCredential`);
|
||||
return {
|
||||
id: stringField(input, "id", path),
|
||||
sourceRef: secretSourceRefField(input, "sourceRef", path),
|
||||
sourceMode,
|
||||
sourceKey,
|
||||
sourceFormat,
|
||||
targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
|
||||
providerCredentialProfile: providerCredential,
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
const secretIds: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm" || arg === "--dry-run") continue;
|
||||
@@ -62,9 +63,17 @@ export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--secret-id") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--secret-id requires a value");
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("--secret-id must be a simple YAML secret id");
|
||||
if (!secretIds.includes(value)) secretIds.push(value);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported secret-sync option: ${arg}`);
|
||||
}
|
||||
return { ...base, node, lane };
|
||||
return { ...base, node, lane, secretIds };
|
||||
}
|
||||
|
||||
export function parseLaneConfirmOptions(args: string[]): LaneConfirmOptions {
|
||||
|
||||
@@ -71,6 +71,7 @@ export function agentRunHelp(): unknown {
|
||||
"bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
|
||||
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run",
|
||||
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm",
|
||||
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --secret-id tool-github-pr-token --confirm",
|
||||
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --dry-run",
|
||||
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --confirm",
|
||||
"bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run",
|
||||
|
||||
@@ -219,6 +219,7 @@ export interface ConfirmOptions {
|
||||
export interface SecretSyncOptions extends ConfirmOptions {
|
||||
node: string | null;
|
||||
lane: string | null;
|
||||
secretIds: string[];
|
||||
}
|
||||
|
||||
export interface LaneConfirmOptions extends ConfirmOptions {
|
||||
|
||||
@@ -350,6 +350,7 @@ export function yamlLanePipelineRunManifest(spec: AgentRunLaneSpec, sourceCommit
|
||||
{ name: "source-branch", value: spec.source.branch },
|
||||
{ name: "gitops-branch", value: spec.gitops.branch },
|
||||
{ name: "revision", value: sourceCommit },
|
||||
...(sourceStageRef === null ? [] : [{ name: "source-stage-ref", value: sourceStageRef }]),
|
||||
{ name: "registry-prefix", value: spec.ci.registryPrefix },
|
||||
{ name: "tools-image", value: spec.ci.toolsImage },
|
||||
],
|
||||
@@ -476,6 +477,7 @@ export function yamlLaneGitMirrorSshSetupShellLines(spec: AgentRunLaneSpec): str
|
||||
const proxyHost = spec.gitMirror.githubProxy.host;
|
||||
const proxyPort = spec.gitMirror.githubProxy.port;
|
||||
const proxyUrl = `http://${proxyHost}:${proxyPort}`;
|
||||
const noProxy = "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,hyueapi.com,.hyueapi.com";
|
||||
const proxySummary = `agentrun git-mirror-egress-proxy node=${spec.nodeId} lane=${spec.lane} host=${proxyHost} port=${proxyPort} ssh=GIT_SSH-wrapper source=yaml`;
|
||||
const proxyCommand = `ProxyCommand=node /tmp/agentrun-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`;
|
||||
return [
|
||||
@@ -545,6 +547,8 @@ export function yamlLaneGitMirrorSshSetupShellLines(spec: AgentRunLaneSpec): str
|
||||
`export http_proxy=${shQuote(proxyUrl)}`,
|
||||
`export https_proxy=${shQuote(proxyUrl)}`,
|
||||
`export all_proxy=${shQuote(proxyUrl)}`,
|
||||
`export NO_PROXY=${shQuote(noProxy)}`,
|
||||
`export no_proxy=${shQuote(noProxy)}`,
|
||||
"export GIT_SSH=/tmp/agentrun-git-ssh-proxy.sh",
|
||||
"unset GIT_SSH_COMMAND",
|
||||
];
|
||||
@@ -558,8 +562,8 @@ export function yamlLaneGitMirrorSyncShell(spec: AgentRunLaneSpec): string {
|
||||
`source_branch=${shQuote(spec.source.branch)}`,
|
||||
`source_stage_ref_prefix=${shQuote(agentRunSourceSnapshotStageRefPrefix(spec))}`,
|
||||
`gitops_branch=${shQuote(spec.gitops.branch)}`,
|
||||
`remote=${shQuote(spec.source.remote)}`,
|
||||
"repo=\"/cache/${repository}.git\"",
|
||||
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
|
||||
"mkdir -p \"$(dirname \"$repo\")\"",
|
||||
"if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then",
|
||||
" git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
|
||||
@@ -606,8 +610,8 @@ export function yamlLaneGitMirrorFlushShell(spec: AgentRunLaneSpec): string {
|
||||
...yamlLaneGitMirrorSshSetupShellLines(spec),
|
||||
`repository=${shQuote(spec.source.repository)}`,
|
||||
`gitops_branch=${shQuote(spec.gitops.branch)}`,
|
||||
`remote=${shQuote(spec.gitMirror.writeUrl)}`,
|
||||
"repo=\"/cache/${repository}.git\"",
|
||||
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
|
||||
"test -d \"$repo\"",
|
||||
"git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
|
||||
"local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
|
||||
@@ -703,6 +707,7 @@ export type LaneSecretSource = {
|
||||
sourceRef: string;
|
||||
sourceMode: "env" | "file";
|
||||
sourceKey: string | null;
|
||||
sourceFormat?: "env" | "raw-token" | null;
|
||||
targetRef: { namespace: string; name: string; key: string };
|
||||
transform?: "local-postgres-database" | "local-postgres-user" | "local-postgres-database-url";
|
||||
};
|
||||
@@ -719,8 +724,8 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
|
||||
value = readFileSync(sourcePath, "utf8");
|
||||
} else {
|
||||
if (source.sourceKey === null) throw new Error(`secret source ${sourceRef} is missing sourceKey`);
|
||||
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
|
||||
const envValue = values.get(source.sourceKey);
|
||||
const text = readFileSync(sourcePath, "utf8");
|
||||
const envValue = source.sourceFormat === "raw-token" ? text.trim() : parseEnvFile(text).get(source.sourceKey);
|
||||
if (envValue === undefined || envValue.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${source.sourceKey}`);
|
||||
value = envValue;
|
||||
}
|
||||
@@ -837,6 +842,7 @@ export function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSour
|
||||
sourceRef: secret.sourceRef,
|
||||
sourceMode: secret.sourceMode,
|
||||
sourceKey: secret.sourceKey,
|
||||
sourceFormat: secret.sourceFormat,
|
||||
targetRef: secret.targetRef,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,15 +178,19 @@ export async function restartYamlLane(config: UniDeskConfig, options: LaneConfir
|
||||
|
||||
export async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
||||
const sources = collectLaneSecretSources(spec);
|
||||
const allSources = collectLaneSecretSources(spec);
|
||||
const selectedIds = new Set(options.secretIds);
|
||||
const sources = selectedIds.size === 0 ? allSources : allSources.filter((source) => selectedIds.has(source.id));
|
||||
if (sources.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane secret-sync",
|
||||
mode: "yaml-declared-node-lane",
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
target: compactAgentRunLaneStatusTarget(spec),
|
||||
degradedReason: "lane-secret-sources-not-declared",
|
||||
requestedSecretIds: options.secretIds,
|
||||
declaredSecretIds: allSources.map((source) => source.id),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
@@ -211,10 +215,11 @@ export async function secretSync(config: UniDeskConfig, options: SecretSyncOptio
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
target: compactAgentRunLaneStatusTarget(spec),
|
||||
filter: options.secretIds.length === 0 ? null : { secretIds: options.secretIds, selected: sources.map((source) => source.id) },
|
||||
plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
|
||||
next: {
|
||||
confirm: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
|
||||
confirm: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane}${options.secretIds.map((id) => ` --secret-id ${id}`).join("")} --confirm`,
|
||||
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
@@ -228,7 +233,8 @@ export async function secretSync(config: UniDeskConfig, options: SecretSyncOptio
|
||||
mode: "confirmed-sync",
|
||||
mutation: true,
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
target: compactAgentRunLaneStatusTarget(spec),
|
||||
filter: options.secretIds.length === 0 ? null : { secretIds: options.secretIds, selected: sources.map((source) => source.id) },
|
||||
plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
|
||||
result: payload,
|
||||
capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
|
||||
|
||||
@@ -128,22 +128,9 @@ export type AgentRunBridgeCaptureResult = SshCaptureResult & { bridgeExecution?:
|
||||
export let localBackendCoreStatusCache: LocalBackendCoreStatus | null = null;
|
||||
|
||||
export async function capture(config: UniDeskConfig, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
|
||||
const result = await runSshCommandCapture(config, target, args);
|
||||
const plan = agentRunBridgeCapturePlan(config, target);
|
||||
if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) {
|
||||
return await captureRemote(config, plan, target, args);
|
||||
}
|
||||
const local = attachBridgeExecution(await runSshCommandCapture(config, target, args), plan);
|
||||
const fallbackHost = agentRunBridgeRemoteHost(config);
|
||||
if (local.exitCode !== 0 && fallbackHost !== null && bridgeExecutionFailureKind(local)?.degradedReason === "capture-backend-unavailable") {
|
||||
const fallbackPlan: AgentRunBridgeCapturePlan = {
|
||||
...plan,
|
||||
backend: "remote-frontend-websocket",
|
||||
remoteHost: fallbackHost,
|
||||
reason: "local-capture-backend-unavailable",
|
||||
};
|
||||
return await captureRemote(config, fallbackPlan, target, args);
|
||||
}
|
||||
return local;
|
||||
return attachBridgeExecution(result, { ...plan, reason: `unified-ssh-capture:${plan.reason}` });
|
||||
}
|
||||
|
||||
export async function captureRemote(config: UniDeskConfig, plan: AgentRunBridgeCapturePlan, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
|
||||
|
||||
@@ -1242,15 +1242,18 @@ function yamlLaneK3sBuildProxyEnv(spec: AgentRunLaneSpec): Array<{ name: string;
|
||||
}
|
||||
|
||||
function yamlLaneK3sBuildSourceShell(spec: AgentRunLaneSpec, sourceCommit: string): string {
|
||||
const useDirectSourceRemote = spec.source.remote.startsWith("http://gitea-http.") || spec.source.remote.startsWith("https://gitea.");
|
||||
const readUrl = useDirectSourceRemote ? spec.source.remote : spec.gitMirror.readUrl;
|
||||
return [
|
||||
"set -eu",
|
||||
`read_url=${shQuote(spec.gitMirror.readUrl)}`,
|
||||
`read_url=${shQuote(readUrl)}`,
|
||||
`source_commit=${shQuote(sourceCommit)}`,
|
||||
`source_stage_ref=${shQuote(agentRunSourceSnapshotRef(spec, sourceCommit))}`,
|
||||
`use_direct_source_remote=${useDirectSourceRemote ? "true" : "false"}`,
|
||||
"rm -rf /workspace/repo",
|
||||
"git clone --no-checkout \"$read_url\" /workspace/repo",
|
||||
"cd /workspace/repo",
|
||||
"git fetch origin \"+$source_stage_ref:refs/remotes/origin/unidesk-source-snapshot\"",
|
||||
"if [ \"$use_direct_source_remote\" != true ]; then git fetch origin \"+$source_stage_ref:refs/remotes/origin/unidesk-source-snapshot\"; fi",
|
||||
"git checkout --detach \"$source_commit\"",
|
||||
"actual=$(git rev-parse HEAD)",
|
||||
"test \"$actual\" = \"$source_commit\"",
|
||||
|
||||
@@ -3,10 +3,25 @@
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
|
||||
import { rootPath } from "../config";
|
||||
import { parseEnvFile } from "../platform-infra-ops-library";
|
||||
import { readIssueCommentBody } from "./body-input";
|
||||
import { PREVIEW_CHARS } from "./types";
|
||||
import { PREVIEW_CHARS, UNIDESK_CLI_CONFIG_PATH } from "./types";
|
||||
import type { GitHubOptions, GitHubTokenProbe } from "./types";
|
||||
|
||||
const GITHUB_TOKEN_SOURCE_CONFIG_REF = `${UNIDESK_CLI_CONFIG_PATH}#github.auth.tokenSource`;
|
||||
|
||||
interface GitHubYamlTokenSourceConfig {
|
||||
enabled: boolean;
|
||||
priority: "before-env" | "after-env";
|
||||
root: string;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
format: "env" | "raw-token";
|
||||
}
|
||||
|
||||
export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
|
||||
if (options.comment === undefined && options.commentFile === undefined) return null;
|
||||
if (options.comment !== undefined) {
|
||||
@@ -34,6 +49,50 @@ export function tokenFromEnvironment(): GitHubTokenProbe {
|
||||
return { present: false, source: null, ghFallbackAttempted: false };
|
||||
}
|
||||
|
||||
export function tokenFromYamlSource(): { token: string | null; probe: GitHubTokenProbe } {
|
||||
const config = readGitHubYamlTokenSourceConfig();
|
||||
if (config === null || !config.enabled) {
|
||||
return {
|
||||
token: null,
|
||||
probe: {
|
||||
present: false,
|
||||
source: null,
|
||||
ghFallbackAttempted: false,
|
||||
yamlSourceAttempted: true,
|
||||
yamlSourceEnabled: config?.enabled ?? false,
|
||||
yamlSourcePriority: config?.priority,
|
||||
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
const sourceRoot = resolveYamlSourceRoot(config.root);
|
||||
const sourcePath = resolveYamlSourcePath(sourceRoot, config.sourceRef);
|
||||
const sourceExists = existsSync(sourcePath);
|
||||
const values = sourceExists ? parseYamlTokenSource(readFileSync(sourcePath, "utf8"), config) : {};
|
||||
const token = values[config.sourceKey] ?? null;
|
||||
const present = token !== null && token.length > 0;
|
||||
return {
|
||||
token: present ? token : null,
|
||||
probe: {
|
||||
present,
|
||||
source: present ? "yaml-token-source" : null,
|
||||
ghFallbackAttempted: false,
|
||||
yamlSourceAttempted: true,
|
||||
yamlSourceEnabled: true,
|
||||
yamlSourcePriority: config.priority,
|
||||
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
|
||||
sourceRef: config.sourceRef,
|
||||
sourceKey: config.sourceKey,
|
||||
sourcePath: redactRepoPath(sourcePath),
|
||||
sourceExists,
|
||||
sourceKeyPresent: present,
|
||||
tokenFingerprint: present ? fingerprintToken(token) : null,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ghBinaryPath(): string | null {
|
||||
try {
|
||||
const output = execFileSync("sh", ["-lc", "command -v gh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
||||
@@ -53,17 +112,103 @@ export function ghAuthToken(): string | null {
|
||||
}
|
||||
|
||||
export function resolveToken(allowGhFallback: boolean): { token: string | null; probe: GitHubTokenProbe } {
|
||||
const yamlToken = tokenFromYamlSource();
|
||||
if (yamlToken.probe.yamlSourcePriority === "before-env" && yamlToken.probe.present) return yamlToken;
|
||||
const envProbe = tokenFromEnvironment();
|
||||
if (envProbe.present) {
|
||||
const token = envProbe.source === "GH_TOKEN" ? process.env.GH_TOKEN ?? null : process.env.GITHUB_TOKEN ?? null;
|
||||
return { token, probe: envProbe };
|
||||
}
|
||||
if (!allowGhFallback) return { token: null, probe: envProbe };
|
||||
if (yamlToken.probe.present) return yamlToken;
|
||||
if (!allowGhFallback) return { token: null, probe: yamlToken.probe };
|
||||
const ghPath = ghBinaryPath();
|
||||
if (ghPath === null) return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
|
||||
if (ghPath === null) return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
|
||||
const token = ghAuthToken();
|
||||
if (token !== null) return { token, probe: { present: true, source: "gh-auth-token", ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: true } };
|
||||
return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
|
||||
return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
|
||||
}
|
||||
|
||||
function readGitHubYamlTokenSourceConfig(): GitHubYamlTokenSourceConfig | null {
|
||||
const configPath = rootPath(UNIDESK_CLI_CONFIG_PATH);
|
||||
if (!existsSync(configPath)) return null;
|
||||
const parsed = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, UNIDESK_CLI_CONFIG_PATH);
|
||||
const github = optionalRecord(parsed.github, `${UNIDESK_CLI_CONFIG_PATH}.github`);
|
||||
const auth = optionalRecord(github?.auth, `${UNIDESK_CLI_CONFIG_PATH}.github.auth`);
|
||||
const tokenSource = optionalRecord(auth?.tokenSource, `${GITHUB_TOKEN_SOURCE_CONFIG_REF}`);
|
||||
if (tokenSource === null) return null;
|
||||
return {
|
||||
enabled: booleanField(tokenSource, "enabled", GITHUB_TOKEN_SOURCE_CONFIG_REF),
|
||||
priority: tokenSourcePriorityField(tokenSource, "priority", GITHUB_TOKEN_SOURCE_CONFIG_REF),
|
||||
root: stringField(tokenSource, "root", GITHUB_TOKEN_SOURCE_CONFIG_REF),
|
||||
sourceRef: stringField(tokenSource, "sourceRef", GITHUB_TOKEN_SOURCE_CONFIG_REF),
|
||||
sourceKey: envKeyField(tokenSource, "sourceKey", GITHUB_TOKEN_SOURCE_CONFIG_REF),
|
||||
format: tokenSourceFormatField(tokenSource, "format", GITHUB_TOKEN_SOURCE_CONFIG_REF),
|
||||
};
|
||||
}
|
||||
|
||||
function parseYamlTokenSource(text: string, config: GitHubYamlTokenSourceConfig): Record<string, string> {
|
||||
if (config.format === "raw-token") return { [config.sourceKey]: text.trim() };
|
||||
return parseEnvFile(text);
|
||||
}
|
||||
|
||||
function resolveYamlSourceRoot(root: string): string {
|
||||
return isAbsolute(root) ? resolve(root) : rootPath(root);
|
||||
}
|
||||
|
||||
function resolveYamlSourcePath(root: string, sourceRef: string): string {
|
||||
if (isAbsolute(sourceRef) || sourceRef.includes("..")) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef must be a relative path without ..`);
|
||||
const resolved = normalize(join(root, sourceRef));
|
||||
if (resolved !== root && !resolved.startsWith(`${root}/`)) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef escapes configured root`);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function redactRepoPath(value: string): string {
|
||||
const root = rootPath();
|
||||
return value === root ? "." : value.startsWith(`${root}/`) ? relative(root, value) : value;
|
||||
}
|
||||
|
||||
function fingerprintToken(value: string): string {
|
||||
return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function optionalRecord(value: unknown, path: string): Record<string, unknown> | null {
|
||||
if (value === undefined || value === null) return null;
|
||||
return asRecord(value, path);
|
||||
}
|
||||
|
||||
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function envKeyField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = stringField(obj, key, path);
|
||||
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${path}.${key} must be an env key`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function tokenSourcePriorityField(obj: Record<string, unknown>, key: string, path: string): "before-env" | "after-env" {
|
||||
const value = stringField(obj, key, path);
|
||||
if (value !== "before-env" && value !== "after-env") throw new Error(`${path}.${key} must be before-env or after-env`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function tokenSourceFormatField(obj: Record<string, unknown>, key: string, path: string): "env" | "raw-token" {
|
||||
const value = obj[key] === undefined ? "env" : stringField(obj, key, path);
|
||||
if (value !== "env" && value !== "raw-token") throw new Error(`${path}.${key} must be env or raw-token`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function repoParts(repo: string): { owner: string; name: string } {
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function authStatus(repo: string): Promise<GitHubCommandResult> {
|
||||
};
|
||||
}
|
||||
degraded.push("missing-token");
|
||||
return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required", { details: probe }), {
|
||||
return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: probe }), {
|
||||
degraded,
|
||||
gh: { binaryFound: ghPath !== null, path: ghPath },
|
||||
token: probe,
|
||||
|
||||
@@ -335,12 +335,12 @@ export async function githubGraphqlRequest<T>(
|
||||
export function authRequired(repo: string, command: string, tokenProbe: GitHubTokenProbe): GitHubCommandResult | null {
|
||||
if (!tokenProbe.present) {
|
||||
if (tokenProbe.ghFallbackAttempted && tokenProbe.ghBinaryFound === false) {
|
||||
return commandError(command, repo, errorPayload("missing-binary", "gh binary is missing and no GH_TOKEN/GITHUB_TOKEN is available", { details: tokenProbe }));
|
||||
return commandError(command, repo, errorPayload("missing-binary", "gh binary is missing and no env or YAML GitHub token source is available", { details: tokenProbe }));
|
||||
}
|
||||
if (tokenProbe.ghFallbackAttempted && tokenProbe.ghBinaryFound === true && tokenProbe.ghAuthTokenAvailable === false) {
|
||||
return commandError(command, repo, errorPayload("auth-failed", "gh auth token failed and no GH_TOKEN/GITHUB_TOKEN is available", { details: tokenProbe }));
|
||||
return commandError(command, repo, errorPayload("auth-failed", "gh auth token failed and no env or YAML GitHub token source is available", { details: tokenProbe }));
|
||||
}
|
||||
return commandError(command, repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required", { details: tokenProbe }), {
|
||||
return commandError(command, repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: tokenProbe }), {
|
||||
degraded: ["missing-token"],
|
||||
token: tokenProbe,
|
||||
});
|
||||
|
||||
@@ -377,8 +377,17 @@ function renderAuthStatus(result: GitHubCommandResult): string {
|
||||
const token = record(result.token);
|
||||
const gh = record(result.gh);
|
||||
const probes = record(result.probes);
|
||||
const tokenDetail = [
|
||||
`source=${ghText(token.source)}`,
|
||||
`yaml=${ghText(token.yamlSourceEnabled)}`,
|
||||
`priority=${ghText(token.yamlSourcePriority)}`,
|
||||
`sourceRef=${ghText(token.sourceRef)}`,
|
||||
`key=${ghText(token.sourceKey)}`,
|
||||
`fingerprint=${ghText(token.tokenFingerprint)}`,
|
||||
`ghFallback=${ghText(token.ghFallbackAttempted)}`,
|
||||
].filter((item) => !item.endsWith("=-") && !item.endsWith("=undefined")).join(" ");
|
||||
const rows = [
|
||||
["token", token.present === true ? "ok" : "missing", `source=${ghText(token.source)} ghFallback=${ghText(token.ghFallbackAttempted)}`],
|
||||
["token", token.present === true ? "ok" : "missing", tokenDetail],
|
||||
["gh-binary", gh.binaryFound === true ? "ok" : "missing", `path=${ghText(gh.path)}`],
|
||||
["rest-api", probeStatus(probes.restApi), ghText(probes.restApi)],
|
||||
["repo", probeStatus(probes.repo), probeDetail(probes.repo)],
|
||||
|
||||
+12
-1
@@ -410,10 +410,21 @@ export type GitHubCommandFailure = GitHubCommandResult & { ok: false };
|
||||
|
||||
export interface GitHubTokenProbe {
|
||||
present: boolean;
|
||||
source: "GH_TOKEN" | "GITHUB_TOKEN" | "gh-auth-token" | null;
|
||||
source: "GH_TOKEN" | "GITHUB_TOKEN" | "yaml-token-source" | "gh-auth-token" | null;
|
||||
ghFallbackAttempted: boolean;
|
||||
ghBinaryFound?: boolean;
|
||||
ghAuthTokenAvailable?: boolean | null;
|
||||
yamlSourceAttempted?: boolean;
|
||||
yamlSourceEnabled?: boolean;
|
||||
yamlSourcePriority?: "before-env" | "after-env";
|
||||
configRef?: string;
|
||||
sourceRef?: string;
|
||||
sourceKey?: string;
|
||||
sourcePath?: string;
|
||||
sourceExists?: boolean;
|
||||
sourceKeyPresent?: boolean;
|
||||
tokenFingerprint?: string | null;
|
||||
valuesPrinted?: false;
|
||||
}
|
||||
|
||||
export interface GitHubOptions {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { rootPath } from "./config";
|
||||
|
||||
const hostK8sConfigPath = "config/unidesk-host-k8s.yaml";
|
||||
|
||||
function asRecord(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function readEnvFile(path: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
const text = readFileSync(path, "utf8");
|
||||
for (const [index, rawLine] of text.split(/\r?\n/u).entries()) {
|
||||
const line = rawLine.trim();
|
||||
if (line.length === 0 || line.startsWith("#")) continue;
|
||||
const eq = line.indexOf("=");
|
||||
if (eq <= 0) throw new Error(`${path}:${index + 1} must be KEY=value`);
|
||||
values[line.slice(0, eq)] = line.slice(eq + 1);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function readHostK8sConfig(): Record<string, unknown> | null {
|
||||
const path = rootPath(hostK8sConfigPath);
|
||||
if (!existsSync(path)) return null;
|
||||
return asRecord(Bun.YAML.parse(readFileSync(path, "utf8")) as unknown, hostK8sConfigPath);
|
||||
}
|
||||
|
||||
export function readHostK8sPublicHost(): string | null {
|
||||
const config = readHostK8sConfig();
|
||||
if (config === null) return null;
|
||||
const runtime = asRecord(config.runtime, `${hostK8sConfigPath}.runtime`);
|
||||
return stringField(runtime, "publicHost", `${hostK8sConfigPath}.runtime`);
|
||||
}
|
||||
|
||||
export function readHostK8sSshClientToken(): string | null {
|
||||
const config = readHostK8sConfig();
|
||||
if (config === null) return null;
|
||||
const runtimeSecrets = asRecord(config.runtimeSecrets, `${hostK8sConfigPath}.runtimeSecrets`);
|
||||
const sourceRef = asRecord(runtimeSecrets.sourceRef, `${hostK8sConfigPath}.runtimeSecrets.sourceRef`);
|
||||
const target = asRecord(runtimeSecrets.target, `${hostK8sConfigPath}.runtimeSecrets.target`);
|
||||
const keys = asRecord(target.keys, `${hostK8sConfigPath}.runtimeSecrets.target.keys`);
|
||||
const sourcePathRaw = stringField(sourceRef, "path", `${hostK8sConfigPath}.runtimeSecrets.sourceRef`);
|
||||
const tokenKey = stringField(keys, "sshClientToken", `${hostK8sConfigPath}.runtimeSecrets.target.keys`);
|
||||
const sourcePath = isAbsolute(sourcePathRaw) ? sourcePathRaw : join(rootPath(), sourcePathRaw);
|
||||
if (!existsSync(sourcePath)) return null;
|
||||
const token = readEnvFile(sourcePath)[tokenKey]?.trim() ?? "";
|
||||
return token.length > 0 ? token : null;
|
||||
}
|
||||
@@ -1817,10 +1817,7 @@ function tektonGitWorkspaceSecretSpec(
|
||||
if (sourceRefFrom !== "gitMirror.githubTransport") {
|
||||
throw new Error(`${path}.sourceRefFrom must be gitMirror.githubTransport`);
|
||||
}
|
||||
if (githubTransport.mode !== "ssh") {
|
||||
throw new Error(`${path}.sourceRefFrom=gitMirror.githubTransport requires gitMirror.githubTransport.mode=ssh`);
|
||||
}
|
||||
if (githubTransport.knownHostsSecretKey === null) {
|
||||
if (githubTransport.mode === "ssh" && githubTransport.knownHostsSecretKey === null) {
|
||||
throw new Error(`${path}.sourceRefFrom=gitMirror.githubTransport requires gitMirror.githubTransport.knownHostsSecretKey`);
|
||||
}
|
||||
const name = stringField(raw, "name", path);
|
||||
@@ -2301,6 +2298,7 @@ function gitMirrorGithubHttpsToken(transport: Extract<ControlPlaneGitMirrorGithu
|
||||
const source = readEnvSourceFile({
|
||||
root: absolute ? "/" : rootPath("."),
|
||||
sourceRef: absolute ? transport.tokenSourceRef.slice(1) : transport.tokenSourceRef,
|
||||
rawKey: transport.tokenSourceKey,
|
||||
missingMessage: () => `gitMirror.githubTransport token source ${transport.tokenSourceRef} is missing; create the YAML-declared sourceRef with ${transport.tokenSourceKey} before applying the control plane`,
|
||||
});
|
||||
const value = requiredEnvValue(source.values, transport.tokenSourceKey, transport.tokenSourceRef);
|
||||
|
||||
@@ -476,6 +476,23 @@ export function nodeRuntimeStatusDegradedReason(input: {
|
||||
}
|
||||
|
||||
export function nodeRuntimePublicProbeStatus(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
||||
if (spec.publicExposure?.enabled !== true) {
|
||||
return {
|
||||
ready: true,
|
||||
skipped: true,
|
||||
reason: "public-exposure-not-configured",
|
||||
web: { kind: "web", url: spec.publicWebUrl, ok: true, skipped: true, httpStatus: null },
|
||||
apiHealth: { kind: "apiHealth", url: joinUrlPath(spec.publicApiUrl, "/health/live"), ok: true, skipped: true, httpStatus: null },
|
||||
targetHost: { ready: true, skipped: true, probeAvailable: false },
|
||||
diagnostic: {
|
||||
kind: "public-entry-probe-skipped",
|
||||
affectsUserEntry: false,
|
||||
targetHostReady: true,
|
||||
message: "publicExposure is not configured for this node/lane; public endpoint readiness is not a deployment gate.",
|
||||
nextAction: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
const web = publicHttpProbe("web", spec.publicWebUrl);
|
||||
const apiHealth = publicHttpProbe("apiHealth", joinUrlPath(spec.publicApiUrl, "/health/live"));
|
||||
const ready = web.ok === true && apiHealth.ok === true;
|
||||
@@ -1896,10 +1913,11 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (!fs.existsSync(file)) return false;",
|
||||
" const doc = readYaml(file) || {};",
|
||||
" const resources = Array.isArray(doc.resources) ? doc.resources : [];",
|
||||
" const publicExposureEnabled = overlay.publicExposure && overlay.publicExposure.enabled;",
|
||||
" const next = resources.filter((item) => {",
|
||||
" if (!(overlay.observability && overlay.observability.prometheusOperator === false)) return true;",
|
||||
" const resource = String(item);",
|
||||
" if (resource === 'observability.yaml') return false;",
|
||||
" if (!publicExposureEnabled && resource === 'node-frpc.yaml') return false;",
|
||||
" if (overlay.observability && overlay.observability.prometheusOperator === false && resource === 'observability.yaml') return false;",
|
||||
" return !(/\\.ya?ml$/u.test(resource) && !fs.existsSync(path.join(runtimePath, resource)));",
|
||||
" });",
|
||||
" let changed = false;",
|
||||
@@ -1918,7 +1936,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" const items = listItems(doc).filter(Boolean);",
|
||||
" let changed = false;",
|
||||
" const endpointIsIpv4 = /^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$/.test(String(access.endpointAddress));",
|
||||
" const endpointSliceName = String(pg.serviceName) + '-host';",
|
||||
" for (const item of items) {",
|
||||
" if (!item || typeof item !== 'object') continue;",
|
||||
" item.metadata = item.metadata || {};",
|
||||
@@ -1946,11 +1963,14 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" }",
|
||||
" if (item.kind === 'EndpointSlice') {",
|
||||
" if (!endpointIsIpv4) { item.__delete = true; changed = true; continue; }",
|
||||
" item.metadata.name = endpointSliceName;",
|
||||
" item.apiVersion = 'v1';",
|
||||
" item.kind = 'Endpoints';",
|
||||
" item.metadata.name = pg.serviceName;",
|
||||
" item.metadata.labels['kubernetes.io/service-name'] = pg.serviceName;",
|
||||
" item.addressType = 'IPv4';",
|
||||
" item.ports = [{ name: 'postgres', port: access.port, protocol: 'TCP' }];",
|
||||
" item.endpoints = [{ addresses: [access.endpointAddress], conditions: { ready: true } }];",
|
||||
" delete item.addressType;",
|
||||
" delete item.ports;",
|
||||
" delete item.endpoints;",
|
||||
" item.subsets = [{ addresses: [{ ip: access.endpointAddress }], ports: [{ name: 'postgres', port: access.port, protocol: 'TCP' }] }];",
|
||||
" changed = true;",
|
||||
" }",
|
||||
" }",
|
||||
@@ -2015,8 +2035,12 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
"}",
|
||||
"function patchPublicExposure() {",
|
||||
" const exposure = overlay.publicExposure;",
|
||||
" if (!exposure || !exposure.enabled) return { configured: false, changed: false };",
|
||||
" const file = path.join(runtimePath, 'node-frpc.yaml');",
|
||||
" if (!exposure || !exposure.enabled) {",
|
||||
" const changed = fs.existsSync(file);",
|
||||
" if (changed) fs.rmSync(file, { force: true });",
|
||||
" return { configured: false, changed };",
|
||||
" }",
|
||||
" if (!fs.existsSync(file)) {",
|
||||
" console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: false, reason: 'node-frpc-yaml-missing', filePath: file, hostname: exposure.hostname }));",
|
||||
" process.exit(49);",
|
||||
@@ -2253,6 +2277,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (!fs.existsSync(file)) fail('external-postgres-missing');",
|
||||
" const items = listItems(readYaml(file)).filter(Boolean);",
|
||||
" const service = items.find((item) => item && item.kind === 'Service' && item.metadata && item.metadata.name === pg.serviceName);",
|
||||
" const endpoints = items.find((item) => item && item.kind === 'Endpoints' && item.metadata && item.metadata.name === String(pg.serviceName));",
|
||||
" const endpointSlice = items.find((item) => item && item.kind === 'EndpointSlice' && item.metadata && item.metadata.name === String(pg.serviceName) + '-host');",
|
||||
" if (!service) fail('external-postgres-service-missing', { expected: pg.serviceName });",
|
||||
" const endpointIsIpv4 = /^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$/.test(String(access.endpointAddress));",
|
||||
@@ -2262,11 +2287,12 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" if (!service.spec || String(service.spec.externalName) !== String(access.endpointAddress)) fail('external-postgres-external-name-mismatch', { value: service.spec && service.spec.externalName, expected: access.endpointAddress });",
|
||||
" if (Number(servicePort) !== Number(access.port)) fail('external-postgres-port-mismatch', { servicePort, expectedPort: access.port });",
|
||||
" if (endpointSlice) fail('external-postgres-endpointslice-unexpected', { name: String(pg.serviceName) + '-host' });",
|
||||
" if (endpoints) fail('external-postgres-endpoints-unexpected', { name: String(pg.serviceName) });",
|
||||
" checks.push('external-postgres-bridge');",
|
||||
" } else {",
|
||||
" if (!endpointSlice) fail('external-postgres-endpointslice-missing', { expected: String(pg.serviceName) + '-host' });",
|
||||
" const endpointPort = Array.isArray(endpointSlice.ports) && endpointSlice.ports[0] ? endpointSlice.ports[0].port : null;",
|
||||
" const endpointAddress = Array.isArray(endpointSlice.endpoints) && endpointSlice.endpoints[0] && Array.isArray(endpointSlice.endpoints[0].addresses) ? endpointSlice.endpoints[0].addresses[0] : null;",
|
||||
" if (!endpoints && !endpointSlice) fail('external-postgres-endpoints-missing', { expected: String(pg.serviceName) });",
|
||||
" const endpointPort = endpoints && Array.isArray(endpoints.subsets) && endpoints.subsets[0] && Array.isArray(endpoints.subsets[0].ports) && endpoints.subsets[0].ports[0] ? endpoints.subsets[0].ports[0].port : endpointSlice && Array.isArray(endpointSlice.ports) && endpointSlice.ports[0] ? endpointSlice.ports[0].port : null;",
|
||||
" const endpointAddress = endpoints && Array.isArray(endpoints.subsets) && endpoints.subsets[0] && Array.isArray(endpoints.subsets[0].addresses) && endpoints.subsets[0].addresses[0] ? endpoints.subsets[0].addresses[0].ip : endpointSlice && Array.isArray(endpointSlice.endpoints) && endpointSlice.endpoints[0] && Array.isArray(endpointSlice.endpoints[0].addresses) ? endpointSlice.endpoints[0].addresses[0] : null;",
|
||||
" if (Number(servicePort) !== Number(access.port) || Number(endpointPort) !== Number(access.port)) fail('external-postgres-port-mismatch', { servicePort, endpointPort, expectedPort: access.port });",
|
||||
" if (String(endpointAddress) !== String(access.endpointAddress)) fail('external-postgres-address-mismatch', { endpointAddress, expectedEndpointAddress: access.endpointAddress });",
|
||||
" checks.push('external-postgres-bridge');",
|
||||
|
||||
@@ -62,7 +62,13 @@ function shellUrlEncodeFunction(): string[] {
|
||||
export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
|
||||
if (options.dryRun && options.confirm) throw new Error("control-plane allow-endpoint-bridge accepts only one of --dry-run or --confirm");
|
||||
const dryRun = options.dryRun || !options.confirm;
|
||||
const result = runTransScript(options.node, endpointBridgeScript({ lane: options.lane, dryRun }), "", options.timeoutSeconds);
|
||||
const secretSpec = runtimeSecretSpec({ node: options.node, lane: options.lane });
|
||||
const result = runTransScript(options.node, endpointBridgeScript({
|
||||
lane: options.lane,
|
||||
dryRun,
|
||||
platformService: secretSpec.platformPostgresService,
|
||||
hostEndpointSlice: secretSpec.platformPostgresEndpointSlice,
|
||||
}), "", options.timeoutSeconds);
|
||||
const fields = keyValueLinesFromText(statusText(result));
|
||||
const beforeExcluded = fields.beforeEndpointResourcesExcluded === "yes";
|
||||
const beforeIgnored = fields.beforeEndpointsIgnoreUpdates === "yes" || fields.beforeEndpointSliceIgnoreUpdates === "yes";
|
||||
@@ -74,8 +80,8 @@ export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScoped
|
||||
const afterLegacyEndpoints = fields.afterLegacyEndpointsExists === "yes";
|
||||
const beforeHostEndpointSlice = fields.beforeHostEndpointSliceExists === "yes";
|
||||
const afterHostEndpointSlice = fields.afterHostEndpointSliceExists === "yes";
|
||||
const bridgeReady = !afterLegacyEndpoints && afterHostEndpointSlice && afterExtraEndpointSlices.length === 0;
|
||||
const ok = result.exitCode === 0 && !afterExcluded && !afterIgnored && bridgeReady;
|
||||
const bridgeReady = (afterLegacyEndpoints || afterHostEndpointSlice) && afterExtraEndpointSlices.length === 0;
|
||||
const ok = result.exitCode === 0 && !afterExcluded && !afterIgnored;
|
||||
return {
|
||||
ok: dryRun ? result.exitCode === 0 : ok,
|
||||
command: "hwlab nodes control-plane allow-endpoint-bridge",
|
||||
@@ -105,8 +111,8 @@ export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScoped
|
||||
extraEndpointSlices: afterExtraEndpointSlices,
|
||||
},
|
||||
runtimeNamespace: fields.runtimeNamespace || `hwlab-${options.lane}`,
|
||||
platformService: fields.platformService || "g14-platform-postgres",
|
||||
hostEndpointSlice: fields.hostEndpointSlice || "g14-platform-postgres-host",
|
||||
platformService: fields.platformService || secretSpec.platformPostgresService,
|
||||
hostEndpointSlice: fields.hostEndpointSlice || secretSpec.platformPostgresEndpointSlice,
|
||||
patchExitCode: numericField(fields.patchExitCode),
|
||||
rolloutRestartExitCode: numericField(fields.rolloutRestartExitCode),
|
||||
rolloutStatusExitCode: numericField(fields.rolloutStatusExitCode),
|
||||
@@ -115,15 +121,16 @@ export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScoped
|
||||
refreshExitCode: numericField(fields.refreshExitCode),
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.exitCode === 0 ? "" : result.stderr.trim().slice(0, 2000),
|
||||
summary: !afterExcluded && !afterIgnored && bridgeReady
|
||||
? "Argo tracks HWLAB external Postgres EndpointSlice and no legacy Endpoints remain"
|
||||
: "Argo endpoint bridge is not in final Service plus EndpointSlice shape",
|
||||
bridgeReady,
|
||||
summary: !afterExcluded && !afterIgnored
|
||||
? "Argo tracks HWLAB external Postgres bridge resource"
|
||||
: "Argo endpoint bridge resources are still excluded or ignored",
|
||||
},
|
||||
result: compactCommandResult(result),
|
||||
};
|
||||
}
|
||||
|
||||
export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun: boolean }): string {
|
||||
export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun: boolean; platformService: string; hostEndpointSlice: string }): string {
|
||||
const application = `hwlab-node-${options.lane}`;
|
||||
const runtimeNamespace = `hwlab-${options.lane}`;
|
||||
return [
|
||||
@@ -133,8 +140,8 @@ export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun:
|
||||
"configmap=argocd-cm",
|
||||
`application=${shellQuote(application)}`,
|
||||
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
|
||||
"platform_service=g14-platform-postgres",
|
||||
"host_endpointslice=g14-platform-postgres-host",
|
||||
`platform_service=${shellQuote(options.platformService)}`,
|
||||
`host_endpointslice=${shellQuote(options.hostEndpointSlice)}`,
|
||||
"preset=endpoint-bridge-resource-tracking",
|
||||
"cm_data() { kubectl -n \"$namespace\" get configmap \"$configmap\" -o \"go-template={{ index .data \\\"$1\\\" }}\" 2>/dev/null || true; }",
|
||||
"cm_has_key() { value=$(cm_data \"$1\"); [ -n \"$value\" ] && [ \"$value\" != \"<no value>\" ] && printf yes || printf no; }",
|
||||
@@ -146,7 +153,7 @@ export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun:
|
||||
" current_legacy=$(resource_exists endpoints \"$platform_service\")",
|
||||
" current_extra=$(extra_endpoint_slices)",
|
||||
" current_host=$(resource_exists endpointslice \"$host_endpointslice\")",
|
||||
" if [ \"$current_legacy\" != yes ] && [ -z \"$current_extra\" ] && [ \"$current_host\" = yes ]; then return 0; fi",
|
||||
" if { [ \"$current_legacy\" = yes ] || [ \"$current_host\" = yes ]; } && [ -z \"$current_extra\" ]; then return 0; fi",
|
||||
" sleep 2",
|
||||
" done",
|
||||
" return 1",
|
||||
@@ -310,7 +317,6 @@ export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun:
|
||||
"printf 'deleteExtraEndpointSlicesExitCode\\t%s\\n' \"$delete_extra_endpointslices_exit\"",
|
||||
"printf 'refreshExitCode\\t%s\\n' \"$refresh_exit\"",
|
||||
"if [ \"$dry_run\" != true ] && { [ \"$after_endpoint_resources_excluded\" = yes ] || [ \"$after_endpoints_ignore_updates\" = yes ] || [ \"$after_endpoint_slice_ignore_updates\" = yes ]; }; then exit 46; fi",
|
||||
"if [ \"$dry_run\" != true ] && { [ \"$after_legacy_endpoints_exists\" = yes ] || [ -n \"$after_extra_endpoint_slice_names\" ] || [ \"$after_host_endpointslice_exists\" != yes ]; }; then exit 47; fi",
|
||||
"if [ -n \"$patch_exit\" ] && [ \"$patch_exit\" != 0 ]; then exit \"$patch_exit\"; fi",
|
||||
"if [ -n \"$rollout_restart_exit\" ] && [ \"$rollout_restart_exit\" != 0 ]; then exit \"$rollout_restart_exit\"; fi",
|
||||
"if [ -n \"$rollout_status_exit\" ] && [ \"$rollout_status_exit\" != 0 ]; then exit \"$rollout_status_exit\"; fi",
|
||||
|
||||
@@ -1706,14 +1706,20 @@ export function externalPostgresBridgeStatus(spec: HwlabRuntimeLaneSpec, namespa
|
||||
service: compactRuntimeCommand(service),
|
||||
};
|
||||
}
|
||||
const endpointSlice = runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "endpointslice", `${pg.serviceName}-host`, "-o", "jsonpath={.addressType}{\"\\n\"}{.endpoints[0].addresses[0]}{\"\\n\"}{.ports[0].port}{\"\\n\"}"], 60);
|
||||
const [addressType = "", address = "", port = ""] = endpointSlice.stdout.split(/\r?\n/u);
|
||||
const endpoints = runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "endpoints", pg.serviceName, "-o", "jsonpath={.subsets[0].addresses[0].ip}{\"\\n\"}{.subsets[0].ports[0].port}{\"\\n\"}"], 60);
|
||||
const [endpointsAddress = "", endpointsPort = ""] = endpoints.stdout.split(/\r?\n/u);
|
||||
const endpointSlice = endpoints.exitCode === 0 && endpointsAddress.length > 0
|
||||
? { exitCode: 1, stdout: "", stderr: "", timedOut: false }
|
||||
: runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "endpointslice", `${pg.serviceName}-host`, "-o", "jsonpath={.addressType}{\"\\n\"}{.endpoints[0].addresses[0]}{\"\\n\"}{.ports[0].port}{\"\\n\"}"], 60);
|
||||
const [addressType = "", endpointSliceAddress = "", endpointSlicePort = ""] = endpointSlice.stdout.split(/\r?\n/u);
|
||||
const address = endpointsAddress || endpointSliceAddress;
|
||||
const port = endpointsPort || endpointSlicePort;
|
||||
return {
|
||||
required: true,
|
||||
ready: service.exitCode === 0 && endpointSlice.exitCode === 0 && serviceType !== "ExternalName" && address === runtimeAccess.endpointAddress && Number(port) === runtimeAccess.port,
|
||||
ready: service.exitCode === 0 && (endpoints.exitCode === 0 || endpointSlice.exitCode === 0) && serviceType !== "ExternalName" && address === runtimeAccess.endpointAddress && Number(port) === runtimeAccess.port,
|
||||
serviceName: pg.serviceName,
|
||||
serviceType: serviceType || null,
|
||||
addressType: addressType || null,
|
||||
addressType: endpoints.exitCode === 0 ? "IPv4" : addressType || null,
|
||||
endpointAddress: address || null,
|
||||
expectedEndpointAddress: runtimeAccess.endpointAddress,
|
||||
port: numericField(port),
|
||||
@@ -1722,6 +1728,7 @@ export function externalPostgresBridgeStatus(spec: HwlabRuntimeLaneSpec, namespa
|
||||
providerPort: pg.port,
|
||||
runtimeAccess: pg.runtimeAccess ?? null,
|
||||
service: compactRuntimeCommand(service),
|
||||
endpoints: compactRuntimeCommand(endpoints),
|
||||
endpointSlice: compactRuntimeCommand(endpointSlice),
|
||||
};
|
||||
}
|
||||
|
||||
+12
-4
@@ -49,8 +49,9 @@ process.stderr.on("error", (error) => {
|
||||
});
|
||||
|
||||
export function emitJson<T>(command: string, data: T, ok = true): void {
|
||||
const envelope: JsonEnvelope<T> = { ok, command, data };
|
||||
safeStdoutWrite(renderEnvelope(command, envelope));
|
||||
const safeCommand = redactSensitiveCommandArgs(command);
|
||||
const envelope: JsonEnvelope<T> = { ok, command: safeCommand, data };
|
||||
safeStdoutWrite(renderEnvelope(safeCommand, envelope));
|
||||
}
|
||||
|
||||
export function isRenderedCliResult(value: unknown): value is RenderedCliResult {
|
||||
@@ -67,8 +68,15 @@ export function emitText(text: string, command = "text"): void {
|
||||
|
||||
export function emitError(command: string, error: unknown): void {
|
||||
const payload = normalizeErrorPayload(command, error);
|
||||
const envelope: JsonEnvelope<never> = { ok: false, command, error: payload };
|
||||
safeStdoutWrite(renderEnvelope(command, envelope));
|
||||
const safeCommand = redactSensitiveCommandArgs(command);
|
||||
const envelope: JsonEnvelope<never> = { ok: false, command: safeCommand, error: payload };
|
||||
safeStdoutWrite(renderEnvelope(safeCommand, envelope));
|
||||
}
|
||||
|
||||
function redactSensitiveCommandArgs(command: string): string {
|
||||
return command
|
||||
.replace(/(--(?:provider-token|token|password|auth-password|session-secret|ssh-client-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/giu, "$1<redacted>")
|
||||
.replace(/\b((?:PROVIDER_TOKEN|UNIDESK_PROVIDER_TOKEN|UNIDESK_SSH_CLIENT_TOKEN|AUTH_PASSWORD|SESSION_SECRET)=)("[^"]*"|'[^']*'|\S+)/gu, "$1<redacted>");
|
||||
}
|
||||
|
||||
export function readCliOutputPolicy(): CliOutputPolicy {
|
||||
|
||||
@@ -30,6 +30,9 @@ capture_json() {
|
||||
run_apply() {
|
||||
manifest="$tmp/gitea.k8s.yaml"
|
||||
printf '%s' "$UNIDESK_GITEA_MANIFEST_B64" | base64 -d >"$manifest"
|
||||
if [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
||||
kubectl create namespace "$UNIDESK_GITEA_NAMESPACE" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/namespace.out" 2>"$tmp/namespace.err"
|
||||
fi
|
||||
if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
|
||||
printf '%s' "$UNIDESK_GITEA_FRPC_TOML_B64" | base64 -d >"$tmp/frpc.toml"
|
||||
kubectl -n "$UNIDESK_GITEA_NAMESPACE" create secret generic "$UNIDESK_GITEA_FRPC_SECRET_NAME" \
|
||||
@@ -63,11 +66,13 @@ run_apply() {
|
||||
fi
|
||||
fi
|
||||
dry_arg=""
|
||||
apply_args="--server-side --force-conflicts"
|
||||
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
|
||||
dry_arg="--dry-run=server"
|
||||
dry_arg="--dry-run=client"
|
||||
apply_args=""
|
||||
fi
|
||||
if [ "$frpc_rc" -eq 0 ] && [ "$webhook_secret_rc" -eq 0 ]; then
|
||||
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side $dry_arg --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
|
||||
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply $apply_args $dry_arg --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
|
||||
apply_rc=$?
|
||||
else
|
||||
apply_rc=1
|
||||
|
||||
@@ -33,6 +33,7 @@ interface GiteaTarget {
|
||||
enabled: boolean;
|
||||
createNamespace: boolean;
|
||||
storageClassName: string;
|
||||
publicExposureEnabled: boolean;
|
||||
}
|
||||
|
||||
interface GiteaConfig {
|
||||
@@ -150,6 +151,7 @@ interface GiteaGithubCredential {
|
||||
transport: "https-token";
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
format: "env" | "raw-token";
|
||||
requiredFor: string[];
|
||||
}
|
||||
|
||||
@@ -446,6 +448,7 @@ function parseGithubCredential(record: Record<string, unknown>, path: string): G
|
||||
transport: y.enumField(record, "transport", path, ["https-token"] as const),
|
||||
sourceRef: secretSourceRefField(record, "sourceRef", path),
|
||||
sourceKey: y.envKeyField(record, "sourceKey", path),
|
||||
format: optionalLiteralField(record, "format", path, ["env", "raw-token"] as const) ?? "env",
|
||||
requiredFor: y.stringArrayField(record, "requiredFor", path),
|
||||
};
|
||||
}
|
||||
@@ -569,6 +572,11 @@ function parseMirrorRepository(record: Record<string, unknown>, index: number):
|
||||
|
||||
function parseTarget(record: Record<string, unknown>, index: number): GiteaTarget {
|
||||
const path = `targets[${index}]`;
|
||||
const publicExposureRaw = record.publicExposure;
|
||||
if (publicExposureRaw !== undefined && (typeof publicExposureRaw !== "object" || publicExposureRaw === null || Array.isArray(publicExposureRaw))) {
|
||||
throw new Error(`${configLabel}.${path}.publicExposure must be an object`);
|
||||
}
|
||||
const publicExposure = publicExposureRaw as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: y.stringField(record, "id", path),
|
||||
route: y.stringField(record, "route", path),
|
||||
@@ -577,6 +585,7 @@ function parseTarget(record: Record<string, unknown>, index: number): GiteaTarge
|
||||
enabled: y.booleanField(record, "enabled", path),
|
||||
createNamespace: y.booleanField(record, "createNamespace", path),
|
||||
storageClassName: y.stringField(record, "storageClassName", path),
|
||||
publicExposureEnabled: publicExposure === undefined ? true : y.booleanField(publicExposure, "enabled", `${path}.publicExposure`),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -644,7 +653,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
const secrets = gitea.sourceAuthority.webhookSync.enabled && !options.dryRun ? ensureMirrorSecrets(gitea, true, true) : undefined;
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("apply", gitea, target, manifest, options, { frpcSecret, secrets }));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && gitea.app.publicExposure.enabled
|
||||
const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && publicExposureEnabled(gitea, target)
|
||||
? await applyGiteaCaddyBlock(config, gitea)
|
||||
: { ok: true, skipped: true, reason: options.dryRun ? "dry-run" : "apply-not-ready" };
|
||||
return {
|
||||
@@ -1265,7 +1274,7 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-bootstra
|
||||
UNIDESK_GITEA_GITHUB_PROXY_ENABLED: gitea.sourceAuthority.githubProxy.enabled ? "1" : "0",
|
||||
UNIDESK_GITEA_GITHUB_PROXY_URL: gitea.sourceAuthority.githubProxy.url,
|
||||
UNIDESK_GITEA_NO_PROXY: gitea.sourceAuthority.githubProxy.noProxy.join(","),
|
||||
UNIDESK_GITEA_FRPC_ENABLED: gitea.app.publicExposure.enabled ? "1" : "0",
|
||||
UNIDESK_GITEA_FRPC_ENABLED: publicExposureEnabled(gitea, target) ? "1" : "0",
|
||||
UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: gitea.app.publicExposure.frpc.deploymentName,
|
||||
UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED: gitea.sourceAuthority.webhookSync.enabled ? "1" : "0",
|
||||
UNIDESK_GITEA_WEBHOOK_PUBLIC_URL: githubWebhookPublicUrl(gitea),
|
||||
@@ -1337,6 +1346,7 @@ function targetSummary(target: GiteaTarget): Record<string, unknown> {
|
||||
role: target.role,
|
||||
createNamespace: target.createNamespace,
|
||||
storageClassName: target.storageClassName,
|
||||
publicExposureEnabled: target.publicExposureEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1363,11 +1373,20 @@ function publicServiceTarget(gitea: GiteaConfig, target: GiteaTarget): PublicSer
|
||||
route: target.route,
|
||||
namespace: target.namespace,
|
||||
replicas: 1,
|
||||
publicExposure: gitea.app.publicExposure,
|
||||
publicExposure: targetPublicExposure(gitea, target),
|
||||
};
|
||||
}
|
||||
|
||||
function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial {
|
||||
function publicExposureEnabled(gitea: GiteaConfig, target: GiteaTarget): boolean {
|
||||
return gitea.app.publicExposure.enabled && target.publicExposureEnabled;
|
||||
}
|
||||
|
||||
function targetPublicExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] {
|
||||
return publicExposureEnabled(gitea, target) ? gitea.app.publicExposure : { ...gitea.app.publicExposure, enabled: false };
|
||||
}
|
||||
|
||||
function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial | undefined {
|
||||
if (!publicExposureEnabled(gitea, target)) return undefined;
|
||||
const base = prepareFrpcSecret({
|
||||
secretRoot: gitea.app.publicExposure.secretRoot,
|
||||
exposure: gitea.app.publicExposure,
|
||||
@@ -1438,7 +1457,8 @@ function publicExposureSummary(gitea: GiteaConfig): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function frpcSecretSummary(secret: FrpcSecretMaterial): Record<string, unknown> {
|
||||
function frpcSecretSummary(secret: FrpcSecretMaterial | undefined): Record<string, unknown> {
|
||||
if (secret === undefined) return { enabled: false, skipped: true, reason: "target-public-exposure-disabled", valuesPrinted: false };
|
||||
return {
|
||||
sourceRef: secret.sourceRef,
|
||||
sourcePath: secret.sourcePath,
|
||||
@@ -1568,9 +1588,10 @@ function webhookSyncSummary(gitea: GiteaConfig): Record<string, unknown> {
|
||||
|
||||
function credentialSummaries(gitea: GiteaConfig): Array<Record<string, unknown>> {
|
||||
const adminPath = credentialPath(gitea, gitea.sourceAuthority.credentials.admin.sourceRef);
|
||||
const githubPath = credentialPath(gitea, gitea.sourceAuthority.credentials.github.sourceRef);
|
||||
const github = gitea.sourceAuthority.credentials.github;
|
||||
const githubPath = credentialPath(gitea, github.sourceRef);
|
||||
const adminLinePair = existsSync(adminPath) ? parseLinePairCredential(adminPath, gitea.sourceAuthority.credentials.admin) : { username: null, password: null };
|
||||
const githubEnv = existsSync(githubPath) ? parseEnvFileSafe(githubPath) : {};
|
||||
const githubEnv = existsSync(githubPath) ? parseGithubCredentialFile(githubPath, github) : {};
|
||||
return [
|
||||
{
|
||||
id: "gitea-admin",
|
||||
@@ -1584,12 +1605,12 @@ function credentialSummaries(gitea: GiteaConfig): Array<Record<string, unknown>>
|
||||
},
|
||||
{
|
||||
id: "github-upstream",
|
||||
transport: gitea.sourceAuthority.credentials.github.transport,
|
||||
sourceRef: gitea.sourceAuthority.credentials.github.sourceRef,
|
||||
transport: github.transport,
|
||||
sourceRef: github.sourceRef,
|
||||
sourcePath: githubPath,
|
||||
present: existsSync(githubPath),
|
||||
requiredKeysPresent: Boolean(githubEnv[gitea.sourceAuthority.credentials.github.sourceKey]),
|
||||
fingerprint: fingerprintKeys(githubEnv, [gitea.sourceAuthority.credentials.github.sourceKey]),
|
||||
requiredKeysPresent: Boolean(githubEnv[github.sourceKey]),
|
||||
fingerprint: fingerprintKeys(githubEnv, [github.sourceKey]),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
];
|
||||
@@ -1614,7 +1635,7 @@ function ensureMirrorSecrets(gitea: GiteaConfig, createAdmin: boolean, createWeb
|
||||
const github = gitea.sourceAuthority.credentials.github;
|
||||
const githubPath = credentialPath(gitea, github.sourceRef);
|
||||
if (!existsSync(githubPath)) throw new Error(`${github.sourceRef} is missing; cannot sync GitHub upstream`);
|
||||
const githubEnv = parseEnvFileSafe(githubPath);
|
||||
const githubEnv = parseGithubCredentialFile(githubPath, github);
|
||||
const webhookSecret = ensureWebhookSecret(gitea, createWebhookSecret);
|
||||
const adminUsername = adminLinePair.username;
|
||||
const adminPassword = adminLinePair.password;
|
||||
@@ -1667,6 +1688,27 @@ function parseEnvFileSafe(path: string): Record<string, string> {
|
||||
return values;
|
||||
}
|
||||
|
||||
function parseGithubCredentialFile(path: string, github: GiteaGithubCredential): Record<string, string> {
|
||||
const text = readFileSync(path, "utf8");
|
||||
if (github.format === "raw-token") return { [github.sourceKey]: text.trim() };
|
||||
return parseEnvTextSafe(text);
|
||||
}
|
||||
|
||||
function parseEnvTextSafe(text: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const rawLine of text.split(/\r?\n/u)) {
|
||||
let line = rawLine.trim();
|
||||
if (line.length === 0 || line.startsWith("#")) continue;
|
||||
if (line.startsWith("export ")) line = line.slice("export ".length).trim();
|
||||
const eq = line.indexOf("=");
|
||||
if (eq <= 0) continue;
|
||||
const key = line.slice(0, eq).trim();
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue;
|
||||
values[key] = unquote(line.slice(eq + 1).trim());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function unquote(value: string): string {
|
||||
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
||||
return value;
|
||||
@@ -2055,6 +2097,13 @@ function literalField<T extends string>(obj: Record<string, unknown>, key: strin
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function optionalLiteralField<T extends string>(obj: Record<string, unknown>, key: string, path: string, allowed: readonly T[]): T | null {
|
||||
if (obj[key] === undefined || obj[key] === null) return null;
|
||||
const value = y.stringField(obj, key, path);
|
||||
if (!allowed.includes(value as T)) throw new Error(`${configLabel}.${path}.${key} must be one of ${allowed.join(", ")}`);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function quantity(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = y.stringField(obj, key, path);
|
||||
if (!/^[0-9]+(?:m|Ki|Mi|Gi|Ti)?$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a Kubernetes quantity`);
|
||||
|
||||
@@ -42,7 +42,7 @@ interface HostProxyServer {
|
||||
|
||||
interface HostProxySource {
|
||||
id: string;
|
||||
sourceType: "benchmark-validated-master-shadowsocks";
|
||||
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks";
|
||||
serverRef: string;
|
||||
benchmarkRef: string;
|
||||
implementationRef: string;
|
||||
@@ -167,7 +167,7 @@ function readHostProxyConfig(): HostProxyConfig {
|
||||
const server = parseServer(record(root.server, `${configPath}.server`), `${configPath}.server`);
|
||||
const sources = Object.fromEntries(Object.entries(record(root.sources, `${configPath}.sources`)).map(([id, value]) => {
|
||||
const source = parseSource(id, record(value, `${configPath}.sources.${id}`));
|
||||
if (source.serverRef !== `server.${server.id}`) throw new Error(`${configPath}.sources.${id}.serverRef must be server.${server.id}`);
|
||||
if (sourceUsesManagedServer(source) && source.serverRef !== `server.${server.id}`) throw new Error(`${configPath}.sources.${id}.serverRef must be server.${server.id}`);
|
||||
return [id, source];
|
||||
}));
|
||||
const targets: Record<string, HostProxyTarget> = {};
|
||||
@@ -232,7 +232,7 @@ function parseServer(raw: Record<string, unknown>, label: string): HostProxyServ
|
||||
}
|
||||
|
||||
function parseSource(id: string, raw: Record<string, unknown>): HostProxySource {
|
||||
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks"] as const);
|
||||
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks"] as const);
|
||||
const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`);
|
||||
const resolved = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.sources.${id}.sourceConfigRef`);
|
||||
if (resolved.sourceType !== "master-shadowsocks" || resolved.masterShadowsocks === null) {
|
||||
@@ -389,7 +389,7 @@ function plan(server: HostProxyServer, target: HostProxyTarget): Record<string,
|
||||
|
||||
function apply(server: HostProxyServer, target: HostProxyTarget): Record<string, unknown> {
|
||||
const material = proxySecretMaterial(server, target.source);
|
||||
const serverApply = applyMasterProxyServer(server, material);
|
||||
const serverApply = sourceUsesManagedServer(target.source) ? applyMasterProxyServer(server, material) : skippedMasterProxyServer(target.source, "apply");
|
||||
const artifact = prepareClientArtifact(target.source.client);
|
||||
const remotePrepare = runTrans(target.route, remotePrepareScript(target.source.client), 30);
|
||||
const remoteArchive = remotePrepare.exitCode === 0
|
||||
@@ -429,7 +429,7 @@ function apply(server: HostProxyServer, target: HostProxyTarget): Record<string,
|
||||
}
|
||||
|
||||
function status(server: HostProxyServer, target: HostProxyTarget): Record<string, unknown> {
|
||||
const serverStatus = masterProxyServerStatus(server);
|
||||
const serverStatus = sourceUsesManagedServer(target.source) ? masterProxyServerStatus(server) : skippedMasterProxyServer(target.source, "status");
|
||||
const result = runTrans(target.route, remoteStatusScript(target), 45);
|
||||
const parsed = parseJson(result.stdout);
|
||||
return {
|
||||
@@ -446,6 +446,34 @@ function status(server: HostProxyServer, target: HostProxyTarget): Record<string
|
||||
};
|
||||
}
|
||||
|
||||
function sourceUsesManagedServer(source: HostProxySource): boolean {
|
||||
return source.sourceType === "benchmark-validated-master-shadowsocks";
|
||||
}
|
||||
|
||||
function skippedMasterProxyServer(source: HostProxySource, mode: "apply" | "status"): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
id: source.id,
|
||||
sourceType: source.sourceType,
|
||||
enabled: false,
|
||||
mode,
|
||||
skipped: true,
|
||||
reason: `source ${source.id} uses externally managed ${source.sourceType} server`,
|
||||
benchmarkRef: source.benchmarkRef,
|
||||
implementationRef: source.implementationRef,
|
||||
sourceConfigRef: source.sourceConfigRef,
|
||||
sourceFingerprint: source.sourceFingerprint,
|
||||
materialFingerprint: null,
|
||||
compose: null,
|
||||
existingHealthy: null,
|
||||
inspect: null,
|
||||
container: "external",
|
||||
listen: `${source.masterShadowsocks.serverHost}:${source.masterShadowsocks.serverPort}`,
|
||||
health: { ok: true, mode: "external", host: "", port: 0 },
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function proxySecretMaterial(server: HostProxyServer, source: HostProxySource): HostProxySecretMaterial {
|
||||
const envSource = readEnvSourceFile({
|
||||
root: rootPath(".state", "secrets"),
|
||||
@@ -951,9 +979,12 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
||||
const client = record(remote.client);
|
||||
const podProbe = record(remote.podAccessProbe);
|
||||
const probe = record(remote.externalProbe);
|
||||
const serverRows = serverApply.enabled === false
|
||||
? table(["SOURCE", "OK", "UPSTREAM", "TYPE"], [[text(serverApply.id), text(serverApply.ok), text(serverApply.listen), text(serverApply.sourceType)]])
|
||||
: table(["SERVER", "OK", "LISTEN", "BENCHMARK"], [[text(server.id), text(serverApply.ok), text(server.listen), text(server.benchmarkRef)]]);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA HOST PROXY APPLY",
|
||||
...table(["SERVER", "OK", "LISTEN", "BENCHMARK"], [[text(server.id), text(serverApply.ok), text(server.listen), text(server.benchmarkRef)]]),
|
||||
...serverRows,
|
||||
"",
|
||||
...table(["SERVER_HEALTH", "CLIENT_BINARY", "DISTRIBUTION"], [[text(serverHealth.ok), text(artifact.binaryAction), text(distribution.mode)]]),
|
||||
"",
|
||||
@@ -979,9 +1010,12 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
|
||||
const health = record(remote.health);
|
||||
const probe = record(remote.externalProbe);
|
||||
const podProbe = record(remote.podAccessProbe);
|
||||
const serverRows = serverStatus.enabled === false
|
||||
? table(["SOURCE", "OK", "UPSTREAM", "TYPE"], [[text(serverStatus.id), text(serverStatus.ok), text(serverStatus.listen), text(serverStatus.sourceType)]])
|
||||
: table(["SERVER", "OK", "LISTEN", "CONTAINER"], [[text(server.id), text(serverStatus.ok), text(server.listen), text(serverStatus.container)]]);
|
||||
const lines = [
|
||||
"PLATFORM-INFRA HOST PROXY STATUS",
|
||||
...table(["SERVER", "OK", "LISTEN", "CONTAINER"], [[text(server.id), text(serverStatus.ok), text(server.listen), text(serverStatus.container)]]),
|
||||
...serverRows,
|
||||
"",
|
||||
...table(["TARGET", "ROUTE", "OK", "CLIENT"], [[text(target.id), text(target.route), result.ok === false ? "false" : "true", text(client.service)]]),
|
||||
"",
|
||||
|
||||
@@ -70,6 +70,23 @@ prepare_gitea_api_base() {
|
||||
export UNIDESK_PAC_GITEA_API_BASE_URL
|
||||
}
|
||||
|
||||
resolve_service_url_to_cluster_ip() {
|
||||
value="$1"
|
||||
hostport=$(printf '%s' "$value" | sed -n 's#^http://\([^/]*\).*$#\1#p')
|
||||
if ! printf '%s' "$hostport" | grep -q '\.svc\.cluster\.local'; then
|
||||
printf '%s' "$value"
|
||||
return
|
||||
fi
|
||||
host=${hostport%%:*}
|
||||
port=${hostport##*:}
|
||||
if [ "$port" = "$hostport" ]; then port=80; fi
|
||||
service=${host%%.*}
|
||||
rest=${host#*.}
|
||||
namespace=${rest%%.*}
|
||||
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
|
||||
printf '%s' "$value" | sed "s#^http://$hostport#http://$cluster_ip:$port#"
|
||||
}
|
||||
|
||||
gitea_api() {
|
||||
method="$1"
|
||||
path="$2"
|
||||
@@ -118,6 +135,7 @@ NODE
|
||||
}
|
||||
|
||||
repository_manifest() {
|
||||
repository_url=$(resolve_service_url_to_cluster_ip "$UNIDESK_PAC_REPOSITORY_URL")
|
||||
cat <<EOF
|
||||
apiVersion: pipelinesascode.tekton.dev/v1alpha1
|
||||
kind: Repository
|
||||
@@ -129,7 +147,7 @@ metadata:
|
||||
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
|
||||
unidesk.ai/spec: $UNIDESK_PAC_SPEC
|
||||
spec:
|
||||
url: $UNIDESK_PAC_REPOSITORY_URL
|
||||
url: $repository_url
|
||||
git_provider:
|
||||
type: gitea
|
||||
url: $UNIDESK_PAC_GITEA_BASE_URL
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type SshRemoteCommandExecutor,
|
||||
type SshRemoteCommandStreamHandlers,
|
||||
} from "./ssh-file-transfer";
|
||||
import { readHostK8sSshClientToken } from "./host-k8s-config";
|
||||
|
||||
interface FrontendSession {
|
||||
baseUrl: string;
|
||||
@@ -282,7 +283,8 @@ async function loginFrontend(host: string, config: UniDeskConfig): Promise<Front
|
||||
|
||||
function sshClientTokenFromEnv(env: NodeJS.ProcessEnv = process.env): string | null {
|
||||
const token = env.UNIDESK_SSH_CLIENT_TOKEN?.trim() ?? "";
|
||||
return token.length > 0 ? token : null;
|
||||
if (token.length > 0) return token;
|
||||
return readHostK8sSshClientToken();
|
||||
}
|
||||
|
||||
function scopedSshFrontendSession(host: string, config: UniDeskConfig, token: string): FrontendSession {
|
||||
|
||||
@@ -716,16 +716,17 @@ export function readTextFile(path: string): string {
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
export function readEnvSourceFile(params: { root: string; sourceRef: string; missingMessage?: (sourcePath: string) => string }): EnvSourceFileMaterial {
|
||||
export function readEnvSourceFile(params: { root: string; sourceRef: string; rawKey?: string; missingMessage?: (sourcePath: string) => string }): EnvSourceFileMaterial {
|
||||
const sourcePath = join(params.root, params.sourceRef);
|
||||
if (!existsSync(sourcePath)) {
|
||||
throw new Error(params.missingMessage?.(sourcePath) ?? `required secret source ${redactRepoPath(sourcePath)} is missing`);
|
||||
}
|
||||
const text = readFileSync(sourcePath, "utf8");
|
||||
return {
|
||||
sourceRef: params.sourceRef,
|
||||
sourcePath,
|
||||
sourcePathRedacted: redactRepoPath(sourcePath),
|
||||
values: parseEnvFile(readFileSync(sourcePath, "utf8")),
|
||||
values: params.rawKey === undefined ? parseEnvFile(text) : { [params.rawKey]: text.trim() },
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { readTransHostProxyEnvRule, type TransHostProxyEnvRule } from "./trans-host-proxy";
|
||||
import { readTransSshBackendConfig, type TransSshBackendConfig } from "./trans-config";
|
||||
import { readCliOutputPolicy } from "./output";
|
||||
import { readHostK8sPublicHost } from "./host-k8s-config";
|
||||
|
||||
export interface ParsedSshArgs {
|
||||
remoteCommand: string | null;
|
||||
@@ -3743,6 +3744,7 @@ function sshCaptureRemoteHost(config: UniDeskConfig, env: NodeJS.ProcessEnv): st
|
||||
return normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_IP)
|
||||
?? normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_HOST)
|
||||
?? normalizeRemoteHost(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST)
|
||||
?? normalizeRemoteHost(readHostK8sPublicHost() ?? undefined)
|
||||
?? normalizeRemoteHost(config.network.publicHost);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user