feat: 建立 PikaOA 稳定开发自动交付运行面
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
version: 1
|
||||
kind: pikaoa-platform-delivery
|
||||
|
||||
metadata:
|
||||
id: pikaoa-enterprise-fixture
|
||||
owner: unidesk
|
||||
repository: pikainc/pikaoa
|
||||
spec: PJ2026-03
|
||||
|
||||
testRuntime:
|
||||
defaultTargetId: TEST01
|
||||
targets:
|
||||
TEST01:
|
||||
enabled: true
|
||||
validationOnly: true
|
||||
node: TEST01
|
||||
route: TEST01:k3s
|
||||
namespace: pikaoa-test
|
||||
ttlSeconds: 7200
|
||||
waitTimeoutSeconds: 180
|
||||
fieldManager: unidesk-pikaoa-test-target
|
||||
taskState:
|
||||
rootPath: /tmp/unidesk-pikaoa-test-target-fixture
|
||||
statusRequestTimeoutSeconds: 3
|
||||
logTailLines: 8
|
||||
cleanup:
|
||||
mode: delete-namespace
|
||||
requireOwnershipLabels: true
|
||||
source:
|
||||
mode: prebuilt-images
|
||||
repository: pikainc/pikaoa
|
||||
commitPolicy: cli-required
|
||||
branch: master
|
||||
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
||||
readUrl: https://gitea.example.invalid/mirrors/pikainc-pikaoa.git
|
||||
snapshotPrefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-test01
|
||||
images:
|
||||
api: registry.invalid/pikaoa/api:${commit}
|
||||
worker: registry.invalid/pikaoa/worker:${commit}
|
||||
web: registry.invalid/pikaoa/web:${commit}
|
||||
initializer: registry.invalid/pikaoa/api:${commit}
|
||||
pullPolicy: IfNotPresent
|
||||
ci:
|
||||
lane: pikaoa-test
|
||||
namespace: pikaoa-ci
|
||||
pipeline: pikaoa-test-test01-pac
|
||||
pipelineRunPrefix: pikaoa-test-test01
|
||||
serviceAccountName: pikaoa-test-test01-tekton-runner
|
||||
workspaceSize: 1Gi
|
||||
pipelineTimeout: 20m0s
|
||||
toolsImage: registry.invalid/tools:latest
|
||||
buildkitImage: registry.invalid/buildkit:rootless
|
||||
build:
|
||||
dockerfiles:
|
||||
api: deploy/api.Dockerfile
|
||||
worker: deploy/worker.Dockerfile
|
||||
web: deploy/web.Dockerfile
|
||||
images:
|
||||
api: registry.invalid/pikaoa/api
|
||||
worker: registry.invalid/pikaoa/worker
|
||||
web: registry.invalid/pikaoa/web
|
||||
networkMode: host
|
||||
gitops:
|
||||
readUrl: https://gitea.example.invalid/mirrors/pikainc-pikaoa.git
|
||||
writeUrl: https://gitea.example.invalid/mirrors/pikainc-pikaoa.git
|
||||
branch: test01-pikaoa-test-gitops
|
||||
manifestPath: deploy/gitops/test01-test/resources.yaml
|
||||
credential:
|
||||
secretName: pac-gitea-pikaoa-test-test01
|
||||
tokenKey: token
|
||||
username: unidesk-admin
|
||||
author:
|
||||
name: PikaOA Test Fixture
|
||||
email: pikaoa-test-fixture@unidesk.local
|
||||
database:
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
||||
name: pikaoa_test
|
||||
username: pikaoa_test
|
||||
schema: pikaoa
|
||||
connection:
|
||||
sourceRef: ./secrets/pikaoa-test.env
|
||||
sourceKey: DATABASE_URL
|
||||
targetKey: DATABASE_URL
|
||||
runtime:
|
||||
replicas:
|
||||
api: 1
|
||||
worker: 1
|
||||
web: 1
|
||||
resources:
|
||||
api: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } }
|
||||
worker: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } }
|
||||
web: { requests: { cpu: 50m, memory: 64Mi }, limits: { cpu: 500m, memory: 256Mi } }
|
||||
initializer: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } }
|
||||
secretName: pikaoa-test-runtime
|
||||
apiServiceName: pikaoa-api
|
||||
apiPort: 8080
|
||||
workerMetricsPort: 8081
|
||||
webPort: 8080
|
||||
webApiUpstreamEnv: PIKAOA_API_UPSTREAM
|
||||
attachment:
|
||||
storageRoot: /var/lib/pikaoa/attachments
|
||||
maxBytes: 104857600
|
||||
claimName: pikaoa-attachments
|
||||
storageRequest: 1Gi
|
||||
storageClassName: null
|
||||
fsGroup: 65532
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
workerInterval: 2s
|
||||
outbox:
|
||||
batchSize: 50
|
||||
maxAttempts: 4
|
||||
retryDelay: 2s
|
||||
sessionTTL: 8h
|
||||
shutdownTimeout: 10s
|
||||
administrator:
|
||||
username: admin
|
||||
password:
|
||||
sourceRef: ./secrets/pikaoa-test-admin-password.txt
|
||||
targetKey: PIKAOA_BOOTSTRAP_ADMIN_PASSWORD
|
||||
employee:
|
||||
username: employee
|
||||
password:
|
||||
sourceRef: ./secrets/pikaoa-test-employee-password.txt
|
||||
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
||||
sessionSecret:
|
||||
sourceRef: ./secrets/pikaoa-test.env
|
||||
sourceKey: PIKAOA_SESSION_SECRET
|
||||
targetKey: PIKAOA_SESSION_SECRET
|
||||
adminToken:
|
||||
sourceRef: ./secrets/pikaoa-test.env
|
||||
sourceKey: OA_ADMIN_TOKEN
|
||||
targetKey: OA_ADMIN_TOKEN
|
||||
initializer:
|
||||
mode: fresh-database-only
|
||||
jobName: pikaoa-init
|
||||
backoffLimit: 2
|
||||
command:
|
||||
- /usr/local/bin/pikaoa
|
||||
- migrate
|
||||
- up
|
||||
- --config
|
||||
- /etc/pikaoa/pikaoa.yaml
|
||||
exposure:
|
||||
serviceName: pikaoa-web
|
||||
serviceType: NodePort
|
||||
hostIP: 192.0.2.10
|
||||
port: 32080
|
||||
observability:
|
||||
otlpEndpoint: otel-collector.observability.svc.cluster.local:4317
|
||||
prometheus:
|
||||
configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector
|
||||
scrape: true
|
||||
apiPath: /metrics
|
||||
workerPath: /metrics
|
||||
exporterFailure:
|
||||
warning: true
|
||||
blocking: false
|
||||
probes:
|
||||
api:
|
||||
livenessPath: /healthz
|
||||
readinessPath: /readyz
|
||||
worker:
|
||||
livenessPath: /healthz
|
||||
readinessPath: /readyz
|
||||
web:
|
||||
livenessPath: /healthz
|
||||
readinessPath: /healthz
|
||||
|
||||
delivery:
|
||||
targets:
|
||||
PROD:
|
||||
namespace: pikaoa
|
||||
ci:
|
||||
namespace: pikaoa-ci
|
||||
+26
-53
@@ -11,6 +11,7 @@ metadata:
|
||||
- 2110
|
||||
- 2128
|
||||
- 2263
|
||||
- 2274
|
||||
|
||||
defaults:
|
||||
targetId: NC01
|
||||
@@ -29,45 +30,30 @@ modules:
|
||||
events: transactional-outbox
|
||||
workers: named-consumer
|
||||
|
||||
testRuntime:
|
||||
developmentRuntime:
|
||||
defaultTargetId: NC01
|
||||
targets:
|
||||
NC01:
|
||||
enabled: true
|
||||
validationOnly: false
|
||||
node: NC01
|
||||
route: NC01:k3s
|
||||
namespace: pikaoa-test
|
||||
ttlSeconds: 86400
|
||||
waitTimeoutSeconds: 300
|
||||
fieldManager: unidesk-pikaoa-test-target
|
||||
taskState:
|
||||
rootPath: /var/lib/unidesk/pikaoa-test-target
|
||||
statusRequestTimeoutSeconds: 10
|
||||
logTailLines: 80
|
||||
cleanup:
|
||||
mode: delete-namespace
|
||||
requireOwnershipLabels: true
|
||||
namespace: pikaoa-dev
|
||||
fieldManager: unidesk-pikaoa-development-runtime
|
||||
source:
|
||||
mode: prebuilt-images
|
||||
mode: pac-source-build
|
||||
repository: pikainc/pikaoa
|
||||
commitPolicy: cli-required
|
||||
commitPolicy: pac-revision-required
|
||||
branch: master
|
||||
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
snapshotPrefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-nc01
|
||||
images:
|
||||
api: 127.0.0.1:5000/pikaoa/api:${commit}
|
||||
worker: 127.0.0.1:5000/pikaoa/worker:${commit}
|
||||
web: 127.0.0.1:5000/pikaoa/web:${commit}
|
||||
initializer: 127.0.0.1:5000/pikaoa/api:${commit}
|
||||
pullPolicy: IfNotPresent
|
||||
ci:
|
||||
lane: pikaoa-test
|
||||
lane: pikaoa-dev
|
||||
namespace: pikaoa-ci
|
||||
pipeline: pikaoa-test-nc01-pac
|
||||
pipelineRunPrefix: pikaoa-test-nc01
|
||||
serviceAccountName: pikaoa-test-nc01-tekton-runner
|
||||
pipeline: pikaoa-dev-nc01-pac
|
||||
pipelineRunPrefix: pikaoa-dev-nc01
|
||||
serviceAccountName: pikaoa-dev-nc01-tekton-runner
|
||||
workspaceSize: 8Gi
|
||||
pipelineTimeout: 20m0s
|
||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
@@ -85,22 +71,22 @@ testRuntime:
|
||||
gitops:
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
writeUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
branch: nc01-pikaoa-test-gitops
|
||||
manifestPath: deploy/gitops/nc01-test/resources.yaml
|
||||
branch: nc01-pikaoa-dev-gitops
|
||||
manifestPath: deploy/gitops/nc01-dev/resources.yaml
|
||||
credential:
|
||||
secretName: pac-gitea-pikaoa-test-nc01
|
||||
secretName: pac-gitea-pikaoa-dev-nc01
|
||||
tokenKey: token
|
||||
username: unidesk-admin
|
||||
author:
|
||||
name: PikaOA Test NC01 CI
|
||||
email: pikaoa-test-nc01-ci@unidesk.local
|
||||
name: PikaOA Development NC01 CI
|
||||
email: pikaoa-dev-nc01-ci@unidesk.local
|
||||
database:
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
||||
name: pikaoa_test
|
||||
username: pikaoa_test
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-dev-database-url
|
||||
name: pikaoa_dev
|
||||
username: pikaoa_dev
|
||||
schema: pikaoa
|
||||
connection:
|
||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
|
||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
|
||||
sourceKey: DATABASE_URL
|
||||
targetKey: DATABASE_URL
|
||||
runtime:
|
||||
@@ -113,7 +99,8 @@ testRuntime:
|
||||
worker: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } }
|
||||
web: { requests: { cpu: 50m, memory: 64Mi }, limits: { cpu: 500m, memory: 256Mi } }
|
||||
initializer: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } }
|
||||
secretName: pikaoa-test-runtime
|
||||
secretName: pikaoa-dev-runtime
|
||||
configMapName: pikaoa-runtime-config
|
||||
apiServiceName: pikaoa-api
|
||||
apiPort: 8080
|
||||
workerMetricsPort: 8081
|
||||
@@ -146,11 +133,11 @@ testRuntime:
|
||||
sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
||||
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
||||
sessionSecret:
|
||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
|
||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
|
||||
sourceKey: PIKAOA_SESSION_SECRET
|
||||
targetKey: PIKAOA_SESSION_SECRET
|
||||
adminToken:
|
||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
|
||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
|
||||
sourceKey: OA_ADMIN_TOKEN
|
||||
targetKey: OA_ADMIN_TOKEN
|
||||
initializer:
|
||||
@@ -197,33 +184,18 @@ releaseRuntime:
|
||||
targets:
|
||||
NC01:
|
||||
enabled: true
|
||||
validationOnly: false
|
||||
node: NC01
|
||||
route: NC01:k3s
|
||||
namespace: pikaoa
|
||||
ttlSeconds: 86400
|
||||
waitTimeoutSeconds: 300
|
||||
fieldManager: unidesk-pikaoa-release-runtime
|
||||
taskState:
|
||||
rootPath: /var/lib/unidesk/pikaoa-release-runtime
|
||||
statusRequestTimeoutSeconds: 10
|
||||
logTailLines: 80
|
||||
cleanup:
|
||||
mode: delete-namespace
|
||||
requireOwnershipLabels: true
|
||||
source:
|
||||
mode: prebuilt-images
|
||||
mode: pac-source-build
|
||||
repository: pikainc/pikaoa
|
||||
commitPolicy: cli-required
|
||||
commitPolicy: pac-revision-required
|
||||
branch: release
|
||||
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
snapshotPrefix: refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01
|
||||
images:
|
||||
api: 127.0.0.1:5000/pikaoa/api:${commit}
|
||||
worker: 127.0.0.1:5000/pikaoa/worker:${commit}
|
||||
web: 127.0.0.1:5000/pikaoa/web:${commit}
|
||||
initializer: 127.0.0.1:5000/pikaoa/api:${commit}
|
||||
pullPolicy: IfNotPresent
|
||||
ci:
|
||||
lane: pikaoa
|
||||
@@ -277,6 +249,7 @@ releaseRuntime:
|
||||
web: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 500m, memory: 512Mi } }
|
||||
initializer: { requests: { cpu: 250m, memory: 256Mi }, limits: { cpu: 1, memory: 1Gi } }
|
||||
secretName: pikaoa-runtime
|
||||
configMapName: pikaoa-runtime-config
|
||||
apiServiceName: pikaoa-api
|
||||
apiPort: 8080
|
||||
workerMetricsPort: 8081
|
||||
|
||||
@@ -8,6 +8,7 @@ metadata:
|
||||
relatedIssues:
|
||||
- 1875
|
||||
- 2250
|
||||
- 2274
|
||||
|
||||
cluster:
|
||||
role: primary
|
||||
@@ -126,8 +127,8 @@ postgres:
|
||||
address: 10.42.0.0/16
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa_test
|
||||
user: pikaoa_test
|
||||
database: pikaoa_dev
|
||||
user: pikaoa_dev
|
||||
address: 10.42.0.0/16
|
||||
method: scram-sha-256
|
||||
|
||||
@@ -191,20 +192,20 @@ secrets:
|
||||
PIKAOA_DB_NAME: pikaoa
|
||||
randomHex:
|
||||
PIKAOA_DB_PASSWORD: 32
|
||||
- name: pikaoa-test-nc01-db-credentials
|
||||
sourceRef: platform-db/pikaoa-test-db.env
|
||||
- name: pikaoa-dev-nc01-db-credentials
|
||||
sourceRef: platform-db/pikaoa-dev-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- PIKAOA_TEST_DB_USER
|
||||
- PIKAOA_TEST_DB_PASSWORD
|
||||
- PIKAOA_TEST_DB_NAME
|
||||
- PIKAOA_DEV_DB_USER
|
||||
- PIKAOA_DEV_DB_PASSWORD
|
||||
- PIKAOA_DEV_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
PIKAOA_TEST_DB_USER: pikaoa_test
|
||||
PIKAOA_TEST_DB_NAME: pikaoa_test
|
||||
PIKAOA_DEV_DB_USER: pikaoa_dev
|
||||
PIKAOA_DEV_DB_NAME: pikaoa_dev
|
||||
randomHex:
|
||||
PIKAOA_TEST_DB_PASSWORD: 32
|
||||
PIKAOA_DEV_DB_PASSWORD: 32
|
||||
|
||||
objects:
|
||||
roles:
|
||||
@@ -244,10 +245,10 @@ objects:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: pikaoa_test
|
||||
- name: pikaoa_dev
|
||||
passwordRef:
|
||||
sourceRef: platform-db/pikaoa-test-db.env
|
||||
key: PIKAOA_TEST_DB_PASSWORD
|
||||
sourceRef: platform-db/pikaoa-dev-db.env
|
||||
key: PIKAOA_DEV_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
@@ -274,8 +275,8 @@ objects:
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: pikaoa_test
|
||||
owner: pikaoa_test
|
||||
- name: pikaoa_dev
|
||||
owner: pikaoa_dev
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
@@ -349,20 +350,20 @@ exports:
|
||||
- scope: pikaoa
|
||||
secret: pikaoa-runtime
|
||||
key: DATABASE_URL
|
||||
- name: pikaoa-test-database-url
|
||||
sourceSecretRef: platform-db/pikaoa-test-db.env
|
||||
- name: pikaoa-dev-database-url
|
||||
sourceSecretRef: platform-db/pikaoa-dev-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(PIKAOA_TEST_DB_USER):$(PIKAOA_TEST_DB_PASSWORD)@$(PGHOST):5432/$(PIKAOA_TEST_DB_NAME)?sslmode=require
|
||||
format: postgresql://$(PIKAOA_DEV_DB_USER):$(PIKAOA_DEV_DB_PASSWORD)@$(PGHOST):5432/$(PIKAOA_DEV_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 10.42.0.1
|
||||
writeToSecretSource:
|
||||
sourceRef: platform-infra/pikaoa-test.env
|
||||
sourceRef: platform-infra/pikaoa-dev.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: pikaoa-test
|
||||
secret: pikaoa-test-runtime
|
||||
- scope: pikaoa-dev
|
||||
secret: pikaoa-dev-runtime
|
||||
key: DATABASE_URL
|
||||
|
||||
backup:
|
||||
|
||||
@@ -13,6 +13,7 @@ metadata:
|
||||
- 1560
|
||||
- 1760
|
||||
- 1768
|
||||
- 2274
|
||||
|
||||
defaults:
|
||||
targetId: NC01
|
||||
@@ -489,7 +490,7 @@ sourceAuthority:
|
||||
naming: gitea-actions-immutable-source
|
||||
prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
|
||||
legacyGitMirror: null
|
||||
- key: pikaoa-test-nc01
|
||||
- key: pikaoa-dev-nc01
|
||||
targetId: NC01
|
||||
credentialOverride:
|
||||
github:
|
||||
@@ -529,7 +530,7 @@ sourceAuthority:
|
||||
publicRead: false
|
||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
gitops:
|
||||
branch: nc01-pikaoa-test-gitops
|
||||
branch: nc01-pikaoa-dev-gitops
|
||||
flushDisposition: gitea-writeback
|
||||
snapshot:
|
||||
naming: gitea-actions-immutable-source
|
||||
|
||||
@@ -14,6 +14,7 @@ metadata:
|
||||
- 1769
|
||||
- 1802
|
||||
- 1811
|
||||
- 2274
|
||||
defaults:
|
||||
targetId: NC01
|
||||
consumerId: hwlab-nc01-v03
|
||||
@@ -247,15 +248,15 @@ repositories:
|
||||
runtime_service_port: "4317"
|
||||
health_path: /healthz
|
||||
health_url: http://selfmedia.selfmedia.svc.cluster.local:4317/healthz
|
||||
- id: pikaoa-test-nc01
|
||||
name: pikaoa-test-nc01
|
||||
- id: pikaoa-dev-nc01
|
||||
name: pikaoa-dev-nc01
|
||||
namespace: pikaoa-ci
|
||||
providerType: gitea
|
||||
url: https://gitea.pikapython.com/mirrors/pikainc-pikaoa
|
||||
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
owner: mirrors
|
||||
repo: pikainc-pikaoa
|
||||
secretName: pac-gitea-pikaoa-test-nc01
|
||||
secretName: pac-gitea-pikaoa-dev-nc01
|
||||
tokenKey: token
|
||||
webhookSecretKey: webhook.secret
|
||||
concurrencyLimit: 1
|
||||
@@ -264,9 +265,9 @@ repositories:
|
||||
source_branch: master
|
||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-nc01
|
||||
node: NC01
|
||||
pipeline_name: pikaoa-test-nc01-pac
|
||||
pipeline_run_prefix: pikaoa-test-nc01
|
||||
service_account: pikaoa-test-nc01-tekton-runner
|
||||
pipeline_name: pikaoa-dev-nc01-pac
|
||||
pipeline_run_prefix: pikaoa-dev-nc01
|
||||
service_account: pikaoa-dev-nc01-tekton-runner
|
||||
pipeline_timeout: 20m0s
|
||||
workspace_pvc_size: 8Gi
|
||||
image_repository: 127.0.0.1:5000/pikaoa/api
|
||||
@@ -276,16 +277,16 @@ repositories:
|
||||
registry_probe_base: http://127.0.0.1:5000
|
||||
gitops_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
gitops_write_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
gitops_branch: nc01-pikaoa-test-gitops
|
||||
gitops_branch: nc01-pikaoa-dev-gitops
|
||||
gitops_username: unidesk-admin
|
||||
gitops_secret_name: pac-gitea-pikaoa-test-nc01
|
||||
gitops_manifest_path: deploy/gitops/nc01-test/resources.yaml
|
||||
runtime_namespace: pikaoa-test
|
||||
gitops_secret_name: pac-gitea-pikaoa-dev-nc01
|
||||
gitops_manifest_path: deploy/gitops/nc01-dev/resources.yaml
|
||||
runtime_namespace: pikaoa-dev
|
||||
runtime_deployment: pikaoa-api
|
||||
runtime_service: pikaoa-web
|
||||
runtime_service_port: "8080"
|
||||
health_path: /healthz
|
||||
health_url: http://pikaoa-web.pikaoa-test.svc.cluster.local:8080/healthz
|
||||
health_url: http://pikaoa-web.pikaoa-dev.svc.cluster.local:8080/healthz
|
||||
consumers:
|
||||
- extends: templates.consumers.agentrunV02
|
||||
variables:
|
||||
@@ -560,49 +561,49 @@ consumers:
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
fsGroup: 1000
|
||||
- id: pikaoa-test-nc01
|
||||
repositoryRef: pikaoa-test-nc01
|
||||
- id: pikaoa-dev-nc01
|
||||
repositoryRef: pikaoa-dev-nc01
|
||||
node: NC01
|
||||
lane: pikaoa-test
|
||||
lane: pikaoa-dev
|
||||
namespace: pikaoa-ci
|
||||
pipeline: pikaoa-test-nc01-pac
|
||||
pipelineRunPrefix: pikaoa-test-nc01
|
||||
pipeline: pikaoa-dev-nc01-pac
|
||||
pipelineRunPrefix: pikaoa-dev-nc01
|
||||
argoNamespace: argocd
|
||||
argoApplication: pikaoa-test-nc01
|
||||
argoApplication: pikaoa-dev-nc01
|
||||
closeoutGitOpsMirrorFlush: false
|
||||
argoBootstrap:
|
||||
project: default
|
||||
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
targetRevision: nc01-pikaoa-test-gitops
|
||||
path: deploy/gitops/nc01-test
|
||||
destinationNamespace: pikaoa-test
|
||||
targetRevision: nc01-pikaoa-dev-gitops
|
||||
path: deploy/gitops/nc01-dev
|
||||
destinationNamespace: pikaoa-dev
|
||||
automated: true
|
||||
repositoryCredential:
|
||||
secretName: argocd-repo-pikaoa-test-nc01
|
||||
secretName: argocd-repo-pikaoa-dev-nc01
|
||||
username: unidesk-admin
|
||||
deliveryProvenance:
|
||||
required: true
|
||||
markerValue: admission-pac-v2:pikaoa-test-nc01
|
||||
executionServiceAccountName: pikaoa-test-nc01-tekton-runner
|
||||
markerValue: admission-pac-v2:pikaoa-dev-nc01
|
||||
executionServiceAccountName: pikaoa-dev-nc01-tekton-runner
|
||||
gitOps:
|
||||
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||
targetRevision: nc01-pikaoa-test-gitops
|
||||
targetRevision: nc01-pikaoa-dev-gitops
|
||||
runnerServiceAccount:
|
||||
name: pikaoa-test-nc01-tekton-runner
|
||||
name: pikaoa-dev-nc01-tekton-runner
|
||||
automountServiceAccountToken: false
|
||||
roleBindingName: pikaoa-test-nc01-tekton-runner
|
||||
roleBindingName: pikaoa-dev-nc01-tekton-runner
|
||||
sourceArtifact:
|
||||
mode: embedded-pipeline-spec
|
||||
renderer: pikaoa-test-runtime
|
||||
configRef: config/pikaoa.yaml#testRuntime.targets.NC01
|
||||
pipelineRunPath: .tekton/pikaoa-test-nc01-pac.yaml
|
||||
renderer: pikaoa-development-runtime
|
||||
configRef: config/pikaoa.yaml#developmentRuntime.targets.NC01
|
||||
pipelineRunPath: .tekton/pikaoa-dev-nc01-pac.yaml
|
||||
maxKeepRuns: 8
|
||||
taskRunTemplate:
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
fsGroup: 1000
|
||||
- id: pikaoa-nc01
|
||||
repositoryRef: pikaoa-test-nc01
|
||||
repositoryRef: pikaoa-dev-nc01
|
||||
node: NC01
|
||||
lane: pikaoa
|
||||
namespace: pikaoa-ci
|
||||
|
||||
@@ -6,6 +6,7 @@ metadata:
|
||||
owner: platform-infra
|
||||
relatedIssues:
|
||||
- 2181
|
||||
- 2274
|
||||
|
||||
defaults:
|
||||
targetId: NC01
|
||||
@@ -37,7 +38,7 @@ targets:
|
||||
sites:
|
||||
- id: pikaoa-development
|
||||
configRef: config/pikaoa.yaml
|
||||
path: testRuntime.targets.NC01
|
||||
path: developmentRuntime.targets.NC01
|
||||
- id: pikaoa-production
|
||||
configRef: config/pikaoa.yaml
|
||||
path: releaseRuntime.targets.NC01
|
||||
|
||||
@@ -13,6 +13,7 @@ metadata:
|
||||
- 2110
|
||||
- 2128
|
||||
- 2263
|
||||
- 2274
|
||||
|
||||
sources:
|
||||
root: /root/.unidesk/.state/secrets
|
||||
@@ -116,7 +117,7 @@ sources:
|
||||
RESTIC_PASSWORD:
|
||||
bytes: 32
|
||||
prefix: poa_restic_
|
||||
- sourceRef: platform-infra/pikaoa-test.env
|
||||
- sourceRef: platform-infra/pikaoa-dev.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- DATABASE_URL
|
||||
@@ -266,10 +267,10 @@ targets:
|
||||
namespace: pikaoa
|
||||
scope: pikaoa
|
||||
enabled: true
|
||||
- id: pikaoa-test-nc01
|
||||
- id: pikaoa-dev-nc01
|
||||
route: NC01:k3s
|
||||
namespace: pikaoa-test
|
||||
scope: pikaoa-test
|
||||
namespace: pikaoa-dev
|
||||
scope: pikaoa-dev
|
||||
enabled: true
|
||||
|
||||
kubernetesSecrets:
|
||||
@@ -301,18 +302,18 @@ kubernetesSecrets:
|
||||
- sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
||||
sourceKey: contents
|
||||
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
||||
- name: pikaoa-test-runtime
|
||||
targetId: pikaoa-test-nc01
|
||||
secretName: pikaoa-test-runtime
|
||||
- name: pikaoa-dev-runtime
|
||||
targetId: pikaoa-dev-nc01
|
||||
secretName: pikaoa-dev-runtime
|
||||
type: Opaque
|
||||
data:
|
||||
- sourceRef: platform-infra/pikaoa-test.env
|
||||
- sourceRef: platform-infra/pikaoa-dev.env
|
||||
sourceKey: DATABASE_URL
|
||||
targetKey: DATABASE_URL
|
||||
- sourceRef: platform-infra/pikaoa-test.env
|
||||
- sourceRef: platform-infra/pikaoa-dev.env
|
||||
sourceKey: PIKAOA_SESSION_SECRET
|
||||
targetKey: PIKAOA_SESSION_SECRET
|
||||
- sourceRef: platform-infra/pikaoa-test.env
|
||||
- sourceRef: platform-infra/pikaoa-dev.env
|
||||
sourceKey: OA_ADMIN_TOKEN
|
||||
targetKey: OA_ADMIN_TOKEN
|
||||
- sourceRef: ~/.unidesk/.env/pikaoa-admin-password.txt
|
||||
|
||||
+11
-2
@@ -1,5 +1,6 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
// UniDesk CLI dispatcher with bounded server lifecycle and job drill-down output.
|
||||
import { readConfig } from "./src/config";
|
||||
import { debugDispatch, debugEgressProxy, debugHealth, debugProviderRootfs, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
|
||||
@@ -429,8 +430,16 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
if (top === "pikaoa") {
|
||||
const { runPikaoaCommand } = await import("./src/pikaoa-test-target");
|
||||
const result = await runPikaoaCommand(readConfig(), args.slice(1));
|
||||
if (args[1] !== "attachments-backup") {
|
||||
throw new CliInputError("pikaoa 只支持 attachments-backup;运行面由 PaC 自动交付", {
|
||||
code: "unsupported-pikaoa-command",
|
||||
argument: args[1] ?? "<missing>",
|
||||
supported: ["attachments-backup"],
|
||||
usage: "bun scripts/cli.ts pikaoa attachments-backup --help",
|
||||
});
|
||||
}
|
||||
const { runPikaoaAttachmentsBackupCommand } = await import("./src/pikaoa-attachments-backup");
|
||||
const result = await runPikaoaAttachmentsBackupCommand(readConfig(), args.slice(2));
|
||||
if (isRenderedCliResult(result)) {
|
||||
emitText(result.renderedText, result.command || commandName);
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
|
||||
+3
-3
@@ -1,5 +1,6 @@
|
||||
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
// Static CLI help for job aliases and server lifecycle progressive disclosure.
|
||||
import { join } from "node:path";
|
||||
import { ghHelp, ghScopedHelp } from "./gh";
|
||||
@@ -75,7 +76,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|secret|test-accounts --node NC01 --lane v03", description: "Operate the NC01 HWLAB v0.3 development and deployment lane from YAML source truth." },
|
||||
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
|
||||
{ command: "platform-infra sub2api|langbot|n8n|webterm|wechat-archive ...", description: "Deploy platform-infra services such as Sub2API, LangBot, n8n and Web Terminal, manage YAML-controlled public Caddy/FRP exposure and WeChat archive workflows, and inspect status/logs without printing secrets." },
|
||||
{ command: "pikaoa test-target ... | pikaoa attachments-backup ...", description: "管理 PikaOA YAML-first 测试运行面和生产附件离机备份控制面。" },
|
||||
{ command: "pikaoa attachments-backup ...", description: "管理 PikaOA 生产附件离机备份控制面;开发与生产运行面由 PaC 自动交付。" },
|
||||
{ command: "secrets plan|sync|status", description: "Plan, push and inspect YAML-declared local secret source keys to Kubernetes Secrets without printing or reverse-engineering values." },
|
||||
{ command: "platform-db postgres plan|status|export-secrets|apply", description: "Manage YAML-declared host-native PostgreSQL 16 on PK01 for Sub2API/platform state, with PostgreSQL native TLS and redacted secret exports." },
|
||||
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Archived HWLAB DEV CD diagnostics only; current HWLAB rollout uses NC01 GitOps." },
|
||||
@@ -1065,9 +1066,8 @@ export async function staticNamespaceHelp(args: string[]): Promise<unknown | nul
|
||||
);
|
||||
}
|
||||
if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary());
|
||||
if (top === "pikaoa" && sub === "test-target") return loadHelp(async () => (await import("./pikaoa-test-target")).pikaoaTestTargetHelp(), { command: "pikaoa test-target", source: "config/pikaoa.yaml#testRuntime" });
|
||||
if (top === "pikaoa" && sub === "attachments-backup") return loadHelp(async () => (await import("./pikaoa-attachments-backup")).pikaoaAttachmentsBackupHelp(), { command: "pikaoa attachments-backup", source: "config/pikaoa.yaml#delivery.targets.*.attachmentsBackup" });
|
||||
if (top === "pikaoa") return loadHelp(async () => (await import("./pikaoa-test-target")).pikaoaHelp(), { command: "pikaoa", commands: ["test-target", "attachments-backup"] });
|
||||
if (top === "pikaoa" && (sub === undefined || isHelpToken(sub))) return loadHelp(async () => ({ ok: true, commands: [(await import("./pikaoa-attachments-backup")).pikaoaAttachmentsBackupHelp()] }), { command: "pikaoa", commands: ["attachments-backup"] });
|
||||
if (top === "platform-db") return platformDbHelp();
|
||||
if (top === "secrets") return secretsHelp();
|
||||
if (top === "web-probe") return loadHelp(async () => (await import("./web-probe")).webProbeHelp(), webProbeHelpSummary());
|
||||
|
||||
+22
-14
@@ -3,13 +3,13 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { renderPikaoaTestDesiredPipeline } from "./pikaoa-test-delivery-renderer";
|
||||
import { renderPikaoaDesiredPipeline } from "./pikaoa-delivery-renderer";
|
||||
|
||||
function pacDocument(): Record<string, unknown> {
|
||||
return Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function rendererBinding(id = "pikaoa-test-nc01"): Parameters<typeof renderPikaoaTestDesiredPipeline>[0] {
|
||||
function rendererBinding(id = "pikaoa-dev-nc01"): Parameters<typeof renderPikaoaDesiredPipeline>[0] {
|
||||
const document = pacDocument();
|
||||
const repositories = document.repositories as Array<Record<string, unknown>>;
|
||||
const consumers = document.consumers as Array<Record<string, unknown>>;
|
||||
@@ -25,31 +25,31 @@ function rendererBinding(id = "pikaoa-test-nc01"): Parameters<typeof renderPikao
|
||||
...((consumer.params as Record<string, unknown> | undefined) ?? {}),
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof renderPikaoaTestDesiredPipeline>[0];
|
||||
} as Parameters<typeof renderPikaoaDesiredPipeline>[0];
|
||||
}
|
||||
|
||||
function sourceFixture(): string {
|
||||
const root = mkdtempSync(resolve(tmpdir(), "pikaoa-test-renderer-"));
|
||||
const root = mkdtempSync(resolve(tmpdir(), "pikaoa-development-renderer-"));
|
||||
mkdirSync(resolve(root, "deploy"), { recursive: true });
|
||||
for (const file of ["api.Dockerfile", "worker.Dockerfile", "web.Dockerfile"]) writeFileSync(resolve(root, "deploy", file), "FROM scratch\n");
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("PikaOA test PaC renderer", () => {
|
||||
describe("PikaOA development PaC renderer", () => {
|
||||
test("shares one PaC repository while isolating master and release consumers", () => {
|
||||
const document = pacDocument();
|
||||
const repositories = document.repositories as Array<Record<string, unknown>>;
|
||||
const consumers = document.consumers as Array<Record<string, unknown>>;
|
||||
const repository = repositories.find((item) => item.id === "pikaoa-test-nc01");
|
||||
const testConsumer = consumers.find((item) => item.id === "pikaoa-test-nc01");
|
||||
const repository = repositories.find((item) => item.id === "pikaoa-dev-nc01");
|
||||
const developmentConsumer = consumers.find((item) => item.id === "pikaoa-dev-nc01");
|
||||
const productionConsumer = consumers.find((item) => item.id === "pikaoa-nc01");
|
||||
const duplicateProductionRepository = repositories.find((item) => item.id === "pikaoa-nc01");
|
||||
expect((repository?.params as Record<string, unknown>).source_branch).toBe("master");
|
||||
expect((testConsumer?.sourceArtifact as Record<string, unknown>).pipelineRunPath).toBe(".tekton/pikaoa-test-nc01-pac.yaml");
|
||||
expect((testConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-test-runtime");
|
||||
expect((developmentConsumer?.sourceArtifact as Record<string, unknown>).pipelineRunPath).toBe(".tekton/pikaoa-dev-nc01-pac.yaml");
|
||||
expect((developmentConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-development-runtime");
|
||||
expect((productionConsumer?.params as Record<string, unknown>).source_branch).toBe("release");
|
||||
expect((productionConsumer?.params as Record<string, unknown>).source_snapshot_prefix).toBe("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
|
||||
expect(productionConsumer?.repositoryRef).toBe("pikaoa-test-nc01");
|
||||
expect(productionConsumer?.repositoryRef).toBe("pikaoa-dev-nc01");
|
||||
expect(duplicateProductionRepository).toBeUndefined();
|
||||
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-release-runtime");
|
||||
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01");
|
||||
@@ -58,13 +58,13 @@ describe("PikaOA test PaC renderer", () => {
|
||||
test("renders release runtime from its isolated owning selector", () => {
|
||||
const source = sourceFixture();
|
||||
try {
|
||||
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding("pikaoa-nc01"), source);
|
||||
const rendered = renderPikaoaDesiredPipeline(rendererBinding("pikaoa-nc01"), source);
|
||||
expect(rendered.configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01");
|
||||
const serialized = JSON.stringify(rendered.pipeline);
|
||||
expect(serialized).toContain("pikaoa-nc01-pac");
|
||||
expect(serialized).toContain("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
|
||||
expect(serialized).toContain("pac-gitea-pikaoa-nc01");
|
||||
expect(serialized).not.toContain("pikaoa-test-nc01-pac");
|
||||
expect(serialized).not.toContain("pikaoa-dev-nc01-pac");
|
||||
} finally {
|
||||
rmSync(source, { recursive: true, force: true });
|
||||
}
|
||||
@@ -73,7 +73,7 @@ describe("PikaOA test PaC renderer", () => {
|
||||
test("renders parallel digest builds and a PreSync fresh database initializer", () => {
|
||||
const source = sourceFixture();
|
||||
try {
|
||||
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding(), source);
|
||||
const rendered = renderPikaoaDesiredPipeline(rendererBinding(), source);
|
||||
const spec = rendered.pipeline.spec as Record<string, unknown>;
|
||||
const tasks = spec.tasks as Array<Record<string, unknown>>;
|
||||
for (const task of tasks) {
|
||||
@@ -97,6 +97,8 @@ describe("PikaOA test PaC renderer", () => {
|
||||
expect(buildScript).not.toContain("\u0001");
|
||||
expect(buildScript).toContain("Docker-Content-Digest:");
|
||||
expect(buildScript).toContain("build_status=$?");
|
||||
expect(buildScript).toContain('if [ -f "$metadata" ]');
|
||||
expect(buildScript).toContain("grep -Eq '^sha256:[0-9a-f]{64}$'");
|
||||
expect(buildScript).toContain('"code":"buildctl-nonzero-artifact-verified"');
|
||||
expect(buildScript).toContain('"artifactVerified":true');
|
||||
}
|
||||
@@ -137,11 +139,17 @@ describe("PikaOA test PaC renderer", () => {
|
||||
expect(manifest).toContain("name: pikaoa-init");
|
||||
expect(manifest).toContain("name: initializer");
|
||||
expect(manifest).not.toContain("kind: Secret");
|
||||
expect(manifest).toContain("secretName: pikaoa-test-runtime");
|
||||
expect(manifest).toContain("kind: ConfigMap");
|
||||
expect(manifest).toContain("name: pikaoa-runtime-config");
|
||||
expect(manifest).toContain("configMap:");
|
||||
expect(manifest).toContain("secretKeyRef:");
|
||||
expect(manifest.match(/name: pikaoa-dev-runtime/gu)).toHaveLength(7);
|
||||
expect(manifest).not.toContain("secretName:");
|
||||
expect(manifest.match(/name: DATABASE_URL/gu)).toHaveLength(3);
|
||||
expect(manifest.match(/key: DATABASE_URL/gu)).toHaveLength(3);
|
||||
expect(manifest.match(/name: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
|
||||
expect(manifest.match(/key: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
|
||||
expect(script).toContain("grep -Eq '@sha256:[0-9a-f]{64}$'");
|
||||
expect(manifest).toContain("image: __PIKAOA_API_IMAGE__");
|
||||
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
|
||||
expect(script).toContain('s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g');
|
||||
+34
-28
@@ -1,17 +1,18 @@
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
pikaoaTestRuntimeConfigRef,
|
||||
pikaoaDevelopmentRuntimeConfigRef,
|
||||
pikaoaReleaseRuntimeConfigRef,
|
||||
resolvePikaoaReleaseRuntimeTargetByConfigRef,
|
||||
resolvePikaoaTestRuntimeTargetByConfigRef,
|
||||
type TestTargetSpec,
|
||||
} from "./pikaoa-test-target";
|
||||
import { renderPikaoaTestRuntimeManifest, type PikaoaTestRenderContext } from "./pikaoa-test-runtime-manifest";
|
||||
resolvePikaoaDevelopmentRuntimeTargetByConfigRef,
|
||||
type PikaoaRuntimeSpec,
|
||||
} from "./pikaoa-runtime";
|
||||
import { renderPikaoaRuntimeManifest, type PikaoaRuntimeRenderContext } from "./pikaoa-runtime-manifest";
|
||||
import { stableJsonSha256 } from "./stable-json";
|
||||
|
||||
export interface PikaoaTestRendererBinding {
|
||||
export interface PikaoaRendererBinding {
|
||||
readonly consumer: {
|
||||
readonly id: string;
|
||||
readonly node: string;
|
||||
@@ -31,20 +32,20 @@ export interface PikaoaTestRendererBinding {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PikaoaTestRenderedPipeline {
|
||||
export interface PikaoaRenderedPipeline {
|
||||
readonly pipeline: Record<string, unknown>;
|
||||
readonly configRef: string;
|
||||
readonly effectiveConfigSha256: string;
|
||||
readonly sourceWorktreeRemote: string;
|
||||
}
|
||||
|
||||
export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBinding, sourceWorktree: string): PikaoaTestRenderedPipeline {
|
||||
export function renderPikaoaDesiredPipeline(binding: PikaoaRendererBinding, sourceWorktree: string): PikaoaRenderedPipeline {
|
||||
const release = binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime";
|
||||
const target = release
|
||||
? resolvePikaoaReleaseRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef)
|
||||
: resolvePikaoaTestRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef);
|
||||
const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaTestRuntimeConfigRef(target.id);
|
||||
if ((!release && binding.consumer.sourceArtifact.renderer !== "pikaoa-test-runtime") || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") {
|
||||
: resolvePikaoaDevelopmentRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef);
|
||||
const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaDevelopmentRuntimeConfigRef(target.id);
|
||||
if ((!release && binding.consumer.sourceArtifact.renderer !== "pikaoa-development-runtime") || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") {
|
||||
throw new Error(`${binding.consumer.sourceArtifact.renderer} requires embedded-pipeline-spec`);
|
||||
}
|
||||
if (binding.consumer.sourceArtifact.configRef !== configRef) {
|
||||
@@ -60,15 +61,15 @@ export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBindi
|
||||
};
|
||||
}
|
||||
|
||||
export function pikaoaTestSourceWorktreeRemote(targetId: string): string {
|
||||
return resolvePikaoaTestRuntimeTargetByConfigRef(pikaoaTestRuntimeConfigRef(targetId)).source.worktreeRemote;
|
||||
export function pikaoaDevelopmentSourceWorktreeRemote(targetId: string): string {
|
||||
return resolvePikaoaDevelopmentRuntimeTargetByConfigRef(pikaoaDevelopmentRuntimeConfigRef(targetId)).source.worktreeRemote;
|
||||
}
|
||||
|
||||
export function pikaoaReleaseSourceWorktreeRemote(targetId: string): string {
|
||||
return resolvePikaoaReleaseRuntimeTargetByConfigRef(pikaoaReleaseRuntimeConfigRef(targetId)).source.worktreeRemote;
|
||||
}
|
||||
|
||||
function pipeline(target: TestTargetSpec): Record<string, unknown> {
|
||||
function pipeline(target: PikaoaRuntimeSpec): Record<string, unknown> {
|
||||
const runtimeManifest = renderGitOpsManifest(target);
|
||||
return {
|
||||
apiVersion: "tekton.dev/v1",
|
||||
@@ -96,7 +97,7 @@ function pipeline(target: TestTargetSpec): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function sourceTask(target: TestTargetSpec): Record<string, unknown> {
|
||||
function sourceTask(target: PikaoaRuntimeSpec): Record<string, unknown> {
|
||||
return {
|
||||
name: "source",
|
||||
taskSpec: {
|
||||
@@ -157,7 +158,7 @@ printf '{"ok":true,"phase":"source","sourceCommit":"%s","valuesPrinted":false}\n
|
||||
};
|
||||
}
|
||||
|
||||
function imageBuildTask(target: TestTargetSpec, component: "api" | "worker" | "web"): Record<string, unknown> {
|
||||
function imageBuildTask(target: PikaoaRuntimeSpec, component: "api" | "worker" | "web"): Record<string, unknown> {
|
||||
const taskName = `build-${component}`;
|
||||
const imageRepository = target.build.images[component];
|
||||
const dockerfile = target.build.dockerfiles[component];
|
||||
@@ -190,7 +191,10 @@ buildctl-daemonless.sh build \\
|
||||
--metadata-file "$metadata"
|
||||
build_status=$?
|
||||
set -e
|
||||
metadata_compact=
|
||||
if [ -f "$metadata" ]; then
|
||||
metadata_compact=$(tr -d '\\n\\r' < "$metadata")
|
||||
fi
|
||||
digest=$(printf '%s' "$metadata_compact" | sed -n 's/.*"containerimage.digest"[[:space:]]*:[[:space:]]*"\\(sha256:[0-9a-f]\\{64\\}\\)".*/\\1/p' | head -n1)
|
||||
if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
|
||||
registry_host=${shellSingleQuote(imageRepository.split("/")[0] ?? "")}
|
||||
@@ -198,7 +202,10 @@ if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
|
||||
headers=$(wget --server-response --spider --header='Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' "http://$registry_host/v2/$registry_path/manifests/$revision" 2>&1 || true)
|
||||
digest=$(printf '%s\\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^[[:space:]]*Docker-Content-Digest:/ {gsub(/\\r/, "", $2); print $2; exit}')
|
||||
fi
|
||||
case "$digest" in sha256:[0-9a-f][0-9a-f]*) test "\${#digest}" -eq 71 ;; *) echo 'build digest missing from metadata and registry manifest' >&2; exit 3;; esac
|
||||
if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
|
||||
echo 'build digest missing from metadata and registry manifest' >&2
|
||||
exit 3
|
||||
fi
|
||||
if [ "$build_status" -ne 0 ]; then
|
||||
printf '{"level":"warning","code":"buildctl-nonzero-artifact-verified","component":"${component}","buildctlExitCode":%s,"artifactVerified":true,"valuesPrinted":false}\\n' "$build_status" >&2
|
||||
fi
|
||||
@@ -214,7 +221,7 @@ printf '{"ok":true,"phase":"${taskName}","sourceCommit":"%s","imageTag":"%s","di
|
||||
};
|
||||
}
|
||||
|
||||
function gitOpsTask(target: TestTargetSpec, runtimeManifest: string): Record<string, unknown> {
|
||||
function gitOpsTask(target: PikaoaRuntimeSpec, runtimeManifest: string): Record<string, unknown> {
|
||||
const manifestB64 = Buffer.from(runtimeManifest, "utf8").toString("base64");
|
||||
return {
|
||||
name: "gitops-publish",
|
||||
@@ -257,7 +264,10 @@ api_image="$(params.api-image)"
|
||||
worker_image="$(params.worker-image)"
|
||||
web_image="$(params.web-image)"
|
||||
for image in "$api_image" "$worker_image" "$web_image"; do
|
||||
case "$image" in *@sha256:[0-9a-f]*) ;; *) echo 'required digestRef missing' >&2; exit 4;; esac
|
||||
if ! printf '%s' "$image" | grep -Eq '@sha256:[0-9a-f]{64}$'; then
|
||||
echo 'required digestRef missing' >&2
|
||||
exit 4
|
||||
fi
|
||||
done
|
||||
rm -rf /workspace/gitops
|
||||
mkdir -p /workspace/gitops
|
||||
@@ -320,25 +330,22 @@ printf '{"ok":true,"status":"published","phase":"gitops-publish","sourceCommitId
|
||||
};
|
||||
}
|
||||
|
||||
function renderGitOpsManifest(target: TestTargetSpec): string {
|
||||
const context: PikaoaTestRenderContext = {
|
||||
function renderGitOpsManifest(target: PikaoaRuntimeSpec): string {
|
||||
const context: PikaoaRuntimeRenderContext = {
|
||||
instanceId: "default",
|
||||
commit: "__PIKAOA_SOURCE_COMMIT__",
|
||||
step: "all",
|
||||
namespace: target.namespace,
|
||||
expiresAt: "gitops-managed",
|
||||
apiImage: "__PIKAOA_API_IMAGE__",
|
||||
workerImage: "__PIKAOA_WORKER_IMAGE__",
|
||||
webImage: "__PIKAOA_WEB_IMAGE__",
|
||||
initializerImage: "__PIKAOA_API_IMAGE__",
|
||||
};
|
||||
const manifest = renderPikaoaTestRuntimeManifest(target, context, { secretMode: "existing", initializerHook: "argo-presync" });
|
||||
const manifest = renderPikaoaRuntimeManifest(target, context, { initializerHook: "argo-presync" });
|
||||
return manifest.map((object) => Bun.YAML.stringify(object, null, 2).trim()).join("\n---\n") + "\n";
|
||||
}
|
||||
|
||||
function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBinding): void {
|
||||
function assertBinding(target: PikaoaRuntimeSpec, binding: PikaoaRendererBinding): void {
|
||||
const expected: Readonly<Record<string, string>> = {
|
||||
id: target.source.branch === "release" ? "pikaoa-nc01" : "pikaoa-test-nc01",
|
||||
node: target.node,
|
||||
lane: target.ci.lane,
|
||||
namespace: target.ci.namespace,
|
||||
@@ -349,7 +356,6 @@ function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBindin
|
||||
serviceAccount: target.ci.serviceAccountName,
|
||||
};
|
||||
const actual: Readonly<Record<string, string | undefined>> = {
|
||||
id: binding.consumer.id,
|
||||
node: binding.consumer.node,
|
||||
lane: binding.consumer.lane,
|
||||
namespace: binding.consumer.namespace,
|
||||
@@ -364,7 +370,7 @@ function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBindin
|
||||
}
|
||||
}
|
||||
|
||||
function assertSourceRepositoryContract(target: TestTargetSpec, sourceWorktree: string): void {
|
||||
function assertSourceRepositoryContract(target: PikaoaRuntimeSpec, sourceWorktree: string): void {
|
||||
for (const path of Object.values(target.build.dockerfiles)) {
|
||||
if (!existsSync(resolve(sourceWorktree, path))) throw new Error(`PikaOA source worktree is missing ${path}`);
|
||||
}
|
||||
+26
-84
@@ -1,44 +1,28 @@
|
||||
import type { TestTargetSpec } from "./pikaoa-test-target";
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
import type { PikaoaRuntimeSpec } from "./pikaoa-runtime";
|
||||
|
||||
export type PikaoaTestTargetStep = "all" | "foundation" | "init" | "api" | "worker" | "web";
|
||||
|
||||
export interface PikaoaTestRenderContext {
|
||||
export interface PikaoaRuntimeRenderContext {
|
||||
instanceId: string;
|
||||
commit: string;
|
||||
step: PikaoaTestTargetStep;
|
||||
namespace: string;
|
||||
expiresAt: string;
|
||||
apiImage: string;
|
||||
workerImage: string;
|
||||
webImage: string;
|
||||
initializerImage: string;
|
||||
}
|
||||
|
||||
export interface PikaoaTestSecretMaterial {
|
||||
databaseURL: string;
|
||||
sessionSecret: string;
|
||||
adminToken: string;
|
||||
administratorPassword: string;
|
||||
employeePassword: string;
|
||||
}
|
||||
|
||||
export interface PikaoaTestManifestOptions {
|
||||
secretMode: "materialized" | "existing";
|
||||
secrets?: PikaoaTestSecretMaterial;
|
||||
export interface PikaoaRuntimeManifestOptions {
|
||||
initializerHook?: "argo-presync";
|
||||
}
|
||||
|
||||
const TARGET_LABEL = "pikaoa.unidesk.io/target";
|
||||
const INSTANCE_LABEL = "pikaoa.unidesk.io/instance";
|
||||
|
||||
export function renderPikaoaTestRuntimeManifest(
|
||||
target: TestTargetSpec,
|
||||
context: PikaoaTestRenderContext,
|
||||
options: PikaoaTestManifestOptions,
|
||||
export function renderPikaoaRuntimeManifest(
|
||||
target: PikaoaRuntimeSpec,
|
||||
context: PikaoaRuntimeRenderContext,
|
||||
options: PikaoaRuntimeManifestOptions,
|
||||
): Record<string, unknown>[] {
|
||||
if (options.secretMode === "materialized" && options.secrets === undefined) {
|
||||
throw new Error("materialized PikaOA test manifest requires secret material");
|
||||
}
|
||||
const labels = ownershipLabels(target, context);
|
||||
const apiSelector = componentSelector("api", context.instanceId);
|
||||
const workerSelector = componentSelector("worker", context.instanceId);
|
||||
@@ -60,10 +44,10 @@ export function renderPikaoaTestRuntimeManifest(
|
||||
{
|
||||
apiVersion: "v1", kind: "Namespace", metadata: { name: context.namespace, labels, annotations: namespaceAnnotations(target, context) },
|
||||
},
|
||||
...(options.secretMode === "materialized" ? [{
|
||||
apiVersion: "v1", kind: "Secret", metadata: { name: target.runtime.secretName, namespace: context.namespace, labels }, type: "Opaque",
|
||||
stringData: runtimeSecretData(target, context, options.secrets!),
|
||||
}] : []),
|
||||
{
|
||||
apiVersion: "v1", kind: "ConfigMap", metadata: { name: target.runtime.configMapName, namespace: context.namespace, labels },
|
||||
data: { "pikaoa.yaml": runtimeConfigData(target, context) },
|
||||
},
|
||||
{
|
||||
apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: target.runtime.attachment.claimName, namespace: context.namespace, labels },
|
||||
spec: {
|
||||
@@ -103,7 +87,7 @@ export function renderPikaoaTestRuntimeManifest(
|
||||
],
|
||||
volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }],
|
||||
}],
|
||||
volumes: [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
|
||||
volumes: [{ name: "runtime", configMap: { name: target.runtime.configMapName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -128,26 +112,13 @@ export function renderPikaoaTestRuntimeManifest(
|
||||
];
|
||||
}
|
||||
|
||||
export function pikaoaManifestForStep(manifest: Record<string, unknown>[], step: PikaoaTestTargetStep): Record<string, unknown>[] {
|
||||
if (step === "all") return manifest;
|
||||
return manifest.filter((object) => {
|
||||
if (object.kind === "Namespace" || object.kind === "PersistentVolumeClaim") return true;
|
||||
if (step === "foundation") return false;
|
||||
if (object.kind === "Secret") return true;
|
||||
if (step === "init") return object.kind === "Job";
|
||||
const metadata = optionalRecord(object.metadata);
|
||||
const labels = optionalRecord(metadata?.labels);
|
||||
return labels?.["app.kubernetes.io/component"] === step;
|
||||
});
|
||||
}
|
||||
|
||||
export function ownershipLabels(target: TestTargetSpec, context: PikaoaTestRenderContext): Record<string, string> {
|
||||
export function ownershipLabels(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): Record<string, string> {
|
||||
const release = target.source.branch === "release";
|
||||
return {
|
||||
"app.kubernetes.io/name": "pikaoa",
|
||||
"app.kubernetes.io/part-of": release ? "pikaoa-release-runtime" : "pikaoa-test-runtime",
|
||||
"app.kubernetes.io/part-of": release ? "pikaoa-release-runtime" : "pikaoa-development-runtime",
|
||||
"app.kubernetes.io/managed-by": target.fieldManager,
|
||||
[release ? "pikaoa.unidesk.io/release-runtime" : "pikaoa.unidesk.io/test-runtime"]: "true",
|
||||
[release ? "pikaoa.unidesk.io/release-runtime" : "pikaoa.unidesk.io/development-runtime"]: "true",
|
||||
[TARGET_LABEL]: target.id,
|
||||
[INSTANCE_LABEL]: context.instanceId,
|
||||
};
|
||||
@@ -157,34 +128,17 @@ export function componentSelector(component: string, instanceId: string): Record
|
||||
return { "app.kubernetes.io/name": "pikaoa", "app.kubernetes.io/component": component, [INSTANCE_LABEL]: instanceId };
|
||||
}
|
||||
|
||||
export function namespaceAnnotations(target: TestTargetSpec, context: PikaoaTestRenderContext): Record<string, string> {
|
||||
export function namespaceAnnotations(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): Record<string, string> {
|
||||
return {
|
||||
"pikaoa.unidesk.io/source-repository": target.source.repository,
|
||||
"pikaoa.unidesk.io/source-commit": context.commit,
|
||||
"pikaoa.unidesk.io/expires-at": context.expiresAt,
|
||||
"pikaoa.unidesk.io/ttl-seconds": String(target.ttlSeconds),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderDatabaseURL(target: TestTargetSpec, sourceURL: string): string {
|
||||
const databaseURL = new URL(sourceURL);
|
||||
databaseURL.searchParams.set("search_path", target.database.schema);
|
||||
return databaseURL.toString();
|
||||
}
|
||||
|
||||
export function placeholderPikaoaSecrets(): PikaoaTestSecretMaterial {
|
||||
return {
|
||||
databaseURL: "postgresql://redacted@db.invalid/pikaoa_test?sslmode=require",
|
||||
sessionSecret: "<redacted>",
|
||||
adminToken: "<redacted>",
|
||||
administratorPassword: "<redacted>",
|
||||
employeePassword: "<redacted>",
|
||||
"pikaoa.unidesk.io/delivery-managed": "true",
|
||||
};
|
||||
}
|
||||
|
||||
function deployment(
|
||||
target: TestTargetSpec,
|
||||
context: PikaoaTestRenderContext,
|
||||
target: PikaoaRuntimeSpec,
|
||||
context: PikaoaRuntimeRenderContext,
|
||||
component: "api" | "worker" | "web",
|
||||
image: string,
|
||||
selector: Record<string, string>,
|
||||
@@ -215,7 +169,7 @@ function deployment(
|
||||
readinessProbe: { httpGet: { path: readinessPath, port }, periodSeconds: 3, failureThreshold: 20 },
|
||||
}],
|
||||
volumes: [
|
||||
...(component === "web" ? [] : [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }]),
|
||||
...(component === "web" ? [] : [{ name: "runtime", configMap: { name: target.runtime.configMapName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }]),
|
||||
...(component === "api" ? [{ name: target.runtime.attachment.claimName, persistentVolumeClaim: { claimName: target.runtime.attachment.claimName } }] : []),
|
||||
],
|
||||
},
|
||||
@@ -224,15 +178,8 @@ function deployment(
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderContext, secrets: PikaoaTestSecretMaterial): Record<string, string> {
|
||||
const databaseURL = renderDatabaseURL(target, secrets.databaseURL);
|
||||
return {
|
||||
[target.database.connection.targetKey]: databaseURL,
|
||||
[target.runtime.sessionSecret.targetKey]: secrets.sessionSecret,
|
||||
[target.runtime.adminToken.targetKey]: secrets.adminToken,
|
||||
[target.runtime.administrator.password.targetKey]: secrets.administratorPassword,
|
||||
[target.runtime.employee.password.targetKey]: secrets.employeePassword,
|
||||
"pikaoa.yaml": [
|
||||
function runtimeConfigData(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): string {
|
||||
return [
|
||||
"api:",
|
||||
` listen: 0.0.0.0:${target.runtime.apiPort}`,
|
||||
"worker:",
|
||||
@@ -243,21 +190,16 @@ function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderCont
|
||||
` maxAttempts: ${target.runtime.outboxMaxAttempts}`,
|
||||
` retryDelay: ${target.runtime.outboxRetryDelay}`,
|
||||
"database:",
|
||||
` url: ${JSON.stringify(databaseURL)}`,
|
||||
' url: ""',
|
||||
"identity:",
|
||||
` sessionTTL: ${target.runtime.sessionTTL}`,
|
||||
"attachment:",
|
||||
` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`,
|
||||
` maxBytes: ${target.runtime.attachment.maxBytes}`,
|
||||
"observability:",
|
||||
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-test-${context.instanceId}`}`,
|
||||
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-dev-${context.instanceId}`}`,
|
||||
` otlpEndpoint: ${JSON.stringify(target.observability.otlpEndpoint)}`,
|
||||
`shutdownTimeout: ${target.runtime.shutdownTimeout}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function optionalRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
].join("\n");
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
import { resolveConfigRef } from "./ops/config-refs";
|
||||
|
||||
interface SecretSourceSpec {
|
||||
purpose: string;
|
||||
sourceRef: string;
|
||||
sourceKey: string | null;
|
||||
targetKey: string;
|
||||
}
|
||||
|
||||
interface PrometheusSelectorSpec {
|
||||
sourceRef: string;
|
||||
key: string;
|
||||
value: string;
|
||||
presence: true;
|
||||
fingerprint: string;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface PikaoaRuntimeSpec {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
node: string;
|
||||
route: string;
|
||||
namespace: string;
|
||||
fieldManager: string;
|
||||
source: {
|
||||
mode: string;
|
||||
repository: string;
|
||||
commitPolicy: string;
|
||||
branch: string;
|
||||
worktreeRemote: string;
|
||||
readUrl: string;
|
||||
snapshotPrefix: string;
|
||||
pullPolicy: string;
|
||||
};
|
||||
ci: {
|
||||
lane: string;
|
||||
namespace: string;
|
||||
pipeline: string;
|
||||
pipelineRunPrefix: string;
|
||||
serviceAccountName: string;
|
||||
workspaceSize: string;
|
||||
pipelineTimeout: string;
|
||||
toolsImage: string;
|
||||
buildkitImage: string;
|
||||
};
|
||||
build: {
|
||||
dockerfiles: { api: string; worker: string; web: string };
|
||||
images: { api: string; worker: string; web: string };
|
||||
networkMode: string;
|
||||
};
|
||||
gitops: {
|
||||
readUrl: string;
|
||||
writeUrl: string;
|
||||
branch: string;
|
||||
manifestPath: string;
|
||||
credentialSecretName: string;
|
||||
credentialTokenKey: string;
|
||||
credentialUsername: string;
|
||||
authorName: string;
|
||||
authorEmail: string;
|
||||
};
|
||||
database: {
|
||||
configRef: string;
|
||||
name: string;
|
||||
username: string;
|
||||
schema: string;
|
||||
connection: SecretSourceSpec;
|
||||
};
|
||||
runtime: {
|
||||
replicas: { api: number; worker: number; web: number };
|
||||
resources: Record<"api" | "worker" | "web" | "initializer", Record<string, unknown>>;
|
||||
secretName: string;
|
||||
configMapName: string;
|
||||
apiServiceName: string;
|
||||
apiPort: number;
|
||||
workerMetricsPort: number;
|
||||
webPort: number;
|
||||
webApiUpstreamEnv: string;
|
||||
attachment: {
|
||||
storageRoot: string;
|
||||
maxBytes: number;
|
||||
claimName: string;
|
||||
storageRequest: string;
|
||||
storageClassName: string | null;
|
||||
fsGroup: number;
|
||||
accessModes: string[];
|
||||
};
|
||||
workerInterval: string;
|
||||
outboxBatchSize: number;
|
||||
outboxMaxAttempts: number;
|
||||
outboxRetryDelay: string;
|
||||
sessionTTL: string;
|
||||
shutdownTimeout: string;
|
||||
administrator: { username: string; password: SecretSourceSpec };
|
||||
employee: { username: string; password: SecretSourceSpec };
|
||||
sessionSecret: SecretSourceSpec;
|
||||
adminToken: SecretSourceSpec;
|
||||
};
|
||||
initializer: {
|
||||
mode: "fresh-database-only";
|
||||
jobName: string;
|
||||
backoffLimit: number;
|
||||
command: string[];
|
||||
};
|
||||
exposure: {
|
||||
serviceName: string;
|
||||
serviceType: "NodePort";
|
||||
hostIP: string;
|
||||
port: number;
|
||||
publicBaseUrl: string;
|
||||
expectedA: string;
|
||||
};
|
||||
observability: {
|
||||
otlpEndpoint: string;
|
||||
prometheusScrape: boolean;
|
||||
prometheusSelector: PrometheusSelectorSpec;
|
||||
apiMetricsPath: string;
|
||||
workerMetricsPath: string;
|
||||
exporterWarning: boolean;
|
||||
exporterBlocking: false;
|
||||
};
|
||||
probes: {
|
||||
apiLivenessPath: string;
|
||||
apiReadinessPath: string;
|
||||
workerLivenessPath: string;
|
||||
workerReadinessPath: string;
|
||||
webLivenessPath: string;
|
||||
webReadinessPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function pikaoaDevelopmentRuntimeConfigRef(targetId: string): string {
|
||||
simpleId(targetId, "PikaOA development runtime target id");
|
||||
return `config/pikaoa.yaml#developmentRuntime.targets.${targetId}`;
|
||||
}
|
||||
|
||||
export function resolvePikaoaDevelopmentRuntimeTargetByConfigRef(configRef: string): PikaoaRuntimeSpec {
|
||||
return resolvePikaoaRuntimeTarget(configRef, "developmentRuntime");
|
||||
}
|
||||
|
||||
export function pikaoaReleaseRuntimeConfigRef(targetId: string): string {
|
||||
simpleId(targetId, "PikaOA release runtime target id");
|
||||
return `config/pikaoa.yaml#releaseRuntime.targets.${targetId}`;
|
||||
}
|
||||
|
||||
export function resolvePikaoaReleaseRuntimeTargetByConfigRef(configRef: string): PikaoaRuntimeSpec {
|
||||
return resolvePikaoaRuntimeTarget(configRef, "releaseRuntime");
|
||||
}
|
||||
|
||||
function resolvePikaoaRuntimeTarget(configRef: string, runtimeSection: "developmentRuntime" | "releaseRuntime"): PikaoaRuntimeSpec {
|
||||
const resolved = resolveConfigRef(configRef, `PikaOA ${runtimeSection} configRef`);
|
||||
const match = new RegExp(`^${runtimeSection}\\.targets\\.([A-Za-z0-9._-]+)$`, "u").exec(resolved.fragment);
|
||||
if (resolved.file !== "config/pikaoa.yaml" || match === null) {
|
||||
throw new Error(`PikaOA ${runtimeSection} configRef must select config/pikaoa.yaml#${runtimeSection}.targets.<id>`);
|
||||
}
|
||||
return parseRuntime(match[1]!, record(resolved.value, configRef), resolved.file, runtimeSection);
|
||||
}
|
||||
|
||||
function parseRuntime(id: string, root: Record<string, unknown>, configLabel: string, runtimeSection: string): PikaoaRuntimeSpec {
|
||||
const path = `${configLabel}.${runtimeSection}.targets.${id}`;
|
||||
simpleId(id, "target id");
|
||||
const source = record(root.source, `${path}.source`);
|
||||
const ci = record(root.ci, `${path}.ci`);
|
||||
const build = record(root.build, `${path}.build`);
|
||||
const dockerfiles = record(build.dockerfiles, `${path}.build.dockerfiles`);
|
||||
const buildImages = record(build.images, `${path}.build.images`);
|
||||
const gitops = record(root.gitops, `${path}.gitops`);
|
||||
const gitopsCredential = record(gitops.credential, `${path}.gitops.credential`);
|
||||
const gitopsAuthor = record(gitops.author, `${path}.gitops.author`);
|
||||
const database = record(root.database, `${path}.database`);
|
||||
const runtime = record(root.runtime, `${path}.runtime`);
|
||||
const replicas = record(runtime.replicas, `${path}.runtime.replicas`);
|
||||
const resources = record(runtime.resources, `${path}.runtime.resources`);
|
||||
const attachment = record(runtime.attachment, `${path}.runtime.attachment`);
|
||||
const outbox = record(runtime.outbox, `${path}.runtime.outbox`);
|
||||
const administrator = record(runtime.administrator, `${path}.runtime.administrator`);
|
||||
const employee = record(runtime.employee, `${path}.runtime.employee`);
|
||||
const initializer = record(root.initializer, `${path}.initializer`);
|
||||
const exposure = record(root.exposure, `${path}.exposure`);
|
||||
const observability = record(root.observability, `${path}.observability`);
|
||||
const prometheus = record(observability.prometheus, `${path}.observability.prometheus`);
|
||||
const exporterFailure = record(observability.exporterFailure, `${path}.observability.exporterFailure`);
|
||||
const probes = record(root.probes, `${path}.probes`);
|
||||
const apiProbes = record(probes.api, `${path}.probes.api`);
|
||||
const workerProbes = record(probes.worker, `${path}.probes.worker`);
|
||||
const webProbes = record(probes.web, `${path}.probes.web`);
|
||||
if (exporterFailure.blocking !== false || exporterFailure.warning !== true) {
|
||||
throw new Error(`${path}.observability.exporterFailure 必须为 warning=true、blocking=false`);
|
||||
}
|
||||
const target: PikaoaRuntimeSpec = {
|
||||
id,
|
||||
enabled: bool(root.enabled, `${path}.enabled`),
|
||||
node: text(root.node, `${path}.node`),
|
||||
route: text(root.route, `${path}.route`),
|
||||
namespace: kubernetesName(root.namespace, `${path}.namespace`),
|
||||
fieldManager: kubernetesName(root.fieldManager, `${path}.fieldManager`),
|
||||
source: {
|
||||
mode: exact(source.mode, `${path}.source.mode`, "pac-source-build"),
|
||||
repository: text(source.repository, `${path}.source.repository`),
|
||||
commitPolicy: exact(source.commitPolicy, `${path}.source.commitPolicy`, "pac-revision-required"),
|
||||
branch: text(source.branch, `${path}.source.branch`),
|
||||
worktreeRemote: text(source.worktreeRemote, `${path}.source.worktreeRemote`),
|
||||
readUrl: text(source.readUrl, `${path}.source.readUrl`),
|
||||
snapshotPrefix: text(source.snapshotPrefix, `${path}.source.snapshotPrefix`),
|
||||
pullPolicy: oneOf(source.pullPolicy, `${path}.source.pullPolicy`, ["Always", "IfNotPresent", "Never"]),
|
||||
},
|
||||
ci: {
|
||||
lane: simpleId(text(ci.lane, `${path}.ci.lane`), `${path}.ci.lane`),
|
||||
namespace: kubernetesName(ci.namespace, `${path}.ci.namespace`),
|
||||
pipeline: kubernetesName(ci.pipeline, `${path}.ci.pipeline`),
|
||||
pipelineRunPrefix: kubernetesName(ci.pipelineRunPrefix, `${path}.ci.pipelineRunPrefix`),
|
||||
serviceAccountName: kubernetesName(ci.serviceAccountName, `${path}.ci.serviceAccountName`),
|
||||
workspaceSize: text(ci.workspaceSize, `${path}.ci.workspaceSize`),
|
||||
pipelineTimeout: text(ci.pipelineTimeout, `${path}.ci.pipelineTimeout`),
|
||||
toolsImage: imageReference(ci.toolsImage, `${path}.ci.toolsImage`),
|
||||
buildkitImage: imageReference(ci.buildkitImage, `${path}.ci.buildkitImage`),
|
||||
},
|
||||
build: {
|
||||
dockerfiles: {
|
||||
api: relativePath(dockerfiles.api, `${path}.build.dockerfiles.api`),
|
||||
worker: relativePath(dockerfiles.worker, `${path}.build.dockerfiles.worker`),
|
||||
web: relativePath(dockerfiles.web, `${path}.build.dockerfiles.web`),
|
||||
},
|
||||
images: {
|
||||
api: imageRepository(buildImages.api, `${path}.build.images.api`),
|
||||
worker: imageRepository(buildImages.worker, `${path}.build.images.worker`),
|
||||
web: imageRepository(buildImages.web, `${path}.build.images.web`),
|
||||
},
|
||||
networkMode: exact(build.networkMode, `${path}.build.networkMode`, "host"),
|
||||
},
|
||||
gitops: {
|
||||
readUrl: text(gitops.readUrl, `${path}.gitops.readUrl`),
|
||||
writeUrl: text(gitops.writeUrl, `${path}.gitops.writeUrl`),
|
||||
branch: text(gitops.branch, `${path}.gitops.branch`),
|
||||
manifestPath: relativePath(gitops.manifestPath, `${path}.gitops.manifestPath`),
|
||||
credentialSecretName: kubernetesName(gitopsCredential.secretName, `${path}.gitops.credential.secretName`),
|
||||
credentialTokenKey: text(gitopsCredential.tokenKey, `${path}.gitops.credential.tokenKey`),
|
||||
credentialUsername: text(gitopsCredential.username, `${path}.gitops.credential.username`),
|
||||
authorName: text(gitopsAuthor.name, `${path}.gitops.author.name`),
|
||||
authorEmail: text(gitopsAuthor.email, `${path}.gitops.author.email`),
|
||||
},
|
||||
database: {
|
||||
configRef: text(database.configRef, `${path}.database.configRef`),
|
||||
name: postgresIdentifier(database.name, `${path}.database.name`),
|
||||
username: postgresIdentifier(database.username, `${path}.database.username`),
|
||||
schema: postgresIdentifier(database.schema, `${path}.database.schema`),
|
||||
connection: secretSource(database.connection, "database.connection", `${path}.database.connection`, true),
|
||||
},
|
||||
runtime: {
|
||||
replicas: {
|
||||
api: positiveInteger(replicas.api, `${path}.runtime.replicas.api`, Number.MAX_SAFE_INTEGER),
|
||||
worker: positiveInteger(replicas.worker, `${path}.runtime.replicas.worker`, Number.MAX_SAFE_INTEGER),
|
||||
web: positiveInteger(replicas.web, `${path}.runtime.replicas.web`, Number.MAX_SAFE_INTEGER),
|
||||
},
|
||||
resources: {
|
||||
api: record(resources.api, `${path}.runtime.resources.api`),
|
||||
worker: record(resources.worker, `${path}.runtime.resources.worker`),
|
||||
web: record(resources.web, `${path}.runtime.resources.web`),
|
||||
initializer: record(resources.initializer, `${path}.runtime.resources.initializer`),
|
||||
},
|
||||
secretName: kubernetesName(runtime.secretName, `${path}.runtime.secretName`),
|
||||
configMapName: kubernetesName(runtime.configMapName, `${path}.runtime.configMapName`),
|
||||
apiServiceName: kubernetesName(runtime.apiServiceName, `${path}.runtime.apiServiceName`),
|
||||
apiPort: positiveInteger(runtime.apiPort, `${path}.runtime.apiPort`, 65_535),
|
||||
workerMetricsPort: positiveInteger(runtime.workerMetricsPort, `${path}.runtime.workerMetricsPort`, 65_535),
|
||||
webPort: positiveInteger(runtime.webPort, `${path}.runtime.webPort`, 65_535),
|
||||
webApiUpstreamEnv: envName(runtime.webApiUpstreamEnv, `${path}.runtime.webApiUpstreamEnv`),
|
||||
attachment: {
|
||||
storageRoot: absolutePath(attachment.storageRoot, `${path}.runtime.attachment.storageRoot`),
|
||||
maxBytes: positiveInteger(attachment.maxBytes, `${path}.runtime.attachment.maxBytes`, Number.MAX_SAFE_INTEGER),
|
||||
claimName: kubernetesName(attachment.claimName, `${path}.runtime.attachment.claimName`),
|
||||
storageRequest: text(attachment.storageRequest, `${path}.runtime.attachment.storageRequest`),
|
||||
storageClassName: nullableText(attachment.storageClassName, `${path}.runtime.attachment.storageClassName`),
|
||||
fsGroup: positiveInteger(attachment.fsGroup, `${path}.runtime.attachment.fsGroup`, 2_147_483_647),
|
||||
accessModes: stringList(attachment.accessModes, `${path}.runtime.attachment.accessModes`, ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"]),
|
||||
},
|
||||
workerInterval: duration(runtime.workerInterval, `${path}.runtime.workerInterval`),
|
||||
outboxBatchSize: positiveInteger(outbox.batchSize, `${path}.runtime.outbox.batchSize`, Number.MAX_SAFE_INTEGER),
|
||||
outboxMaxAttempts: positiveInteger(outbox.maxAttempts, `${path}.runtime.outbox.maxAttempts`, Number.MAX_SAFE_INTEGER),
|
||||
outboxRetryDelay: duration(outbox.retryDelay, `${path}.runtime.outbox.retryDelay`),
|
||||
sessionTTL: duration(runtime.sessionTTL, `${path}.runtime.sessionTTL`),
|
||||
shutdownTimeout: duration(runtime.shutdownTimeout, `${path}.runtime.shutdownTimeout`),
|
||||
administrator: {
|
||||
username: text(administrator.username, `${path}.runtime.administrator.username`),
|
||||
password: secretSource(administrator.password, "runtime.administrator.password", `${path}.runtime.administrator.password`),
|
||||
},
|
||||
employee: {
|
||||
username: text(employee.username, `${path}.runtime.employee.username`),
|
||||
password: secretSource(employee.password, "runtime.employee.password", `${path}.runtime.employee.password`),
|
||||
},
|
||||
sessionSecret: secretSource(runtime.sessionSecret, "runtime.sessionSecret", `${path}.runtime.sessionSecret`),
|
||||
adminToken: secretSource(runtime.adminToken, "runtime.adminToken", `${path}.runtime.adminToken`, true),
|
||||
},
|
||||
initializer: {
|
||||
mode: exact(initializer.mode, `${path}.initializer.mode`, "fresh-database-only") as "fresh-database-only",
|
||||
jobName: kubernetesName(initializer.jobName, `${path}.initializer.jobName`),
|
||||
backoffLimit: nonNegativeInteger(initializer.backoffLimit, `${path}.initializer.backoffLimit`),
|
||||
command: nonEmptyStringList(initializer.command, `${path}.initializer.command`),
|
||||
},
|
||||
exposure: {
|
||||
serviceName: kubernetesName(exposure.serviceName, `${path}.exposure.serviceName`),
|
||||
serviceType: exact(exposure.serviceType, `${path}.exposure.serviceType`, "NodePort") as "NodePort",
|
||||
hostIP: ipv4Address(exposure.hostIP, `${path}.exposure.hostIP`),
|
||||
port: positiveInteger(exposure.port, `${path}.exposure.port`, 65_535),
|
||||
publicBaseUrl: httpsUrl(exposure.publicBaseUrl, `${path}.exposure.publicBaseUrl`),
|
||||
expectedA: ipv4Address(exposure.expectedA, `${path}.exposure.expectedA`),
|
||||
},
|
||||
observability: {
|
||||
otlpEndpoint: otlpGrpcEndpoint(observability.otlpEndpoint, `${path}.observability.otlpEndpoint`),
|
||||
prometheusScrape: bool(prometheus.scrape, `${path}.observability.prometheus.scrape`),
|
||||
prometheusSelector: prometheusSelector(prometheus.configRef, `${path}.observability.prometheus`),
|
||||
apiMetricsPath: httpPath(prometheus.apiPath, `${path}.observability.prometheus.apiPath`),
|
||||
workerMetricsPath: httpPath(prometheus.workerPath, `${path}.observability.prometheus.workerPath`),
|
||||
exporterWarning: true,
|
||||
exporterBlocking: false,
|
||||
},
|
||||
probes: {
|
||||
apiLivenessPath: httpPath(apiProbes.livenessPath, `${path}.probes.api.livenessPath`),
|
||||
apiReadinessPath: httpPath(apiProbes.readinessPath, `${path}.probes.api.readinessPath`),
|
||||
workerLivenessPath: httpPath(workerProbes.livenessPath, `${path}.probes.worker.livenessPath`),
|
||||
workerReadinessPath: httpPath(workerProbes.readinessPath, `${path}.probes.worker.readinessPath`),
|
||||
webLivenessPath: httpPath(webProbes.livenessPath, `${path}.probes.web.livenessPath`),
|
||||
webReadinessPath: httpPath(webProbes.readinessPath, `${path}.probes.web.readinessPath`),
|
||||
},
|
||||
};
|
||||
const secretKeys = [target.database.connection, target.runtime.sessionSecret, target.runtime.adminToken, target.runtime.administrator.password, target.runtime.employee.password]
|
||||
.map((item) => item.targetKey);
|
||||
if (new Set(secretKeys).size !== secretKeys.length || secretKeys.includes("pikaoa.yaml")) {
|
||||
throw new Error(`${path} 的 Secret targetKey 必须唯一且不能使用 pikaoa.yaml`);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function secretSource(value: unknown, purpose: string, path: string, requireSourceKey = false): SecretSourceSpec {
|
||||
const source = record(value, path);
|
||||
const targetKey = envName(source.targetKey, `${path}.targetKey`);
|
||||
const sourceKey = nullableText(source.sourceKey, `${path}.sourceKey`);
|
||||
if (requireSourceKey && sourceKey === null) throw new Error(`${path}.sourceKey 必须显式声明`);
|
||||
if (sourceKey !== null) envName(sourceKey, `${path}.sourceKey`);
|
||||
return { purpose, sourceRef: text(source.sourceRef, `${path}.sourceRef`), sourceKey, targetKey };
|
||||
}
|
||||
|
||||
function prometheusSelector(value: unknown, path: string): PrometheusSelectorSpec {
|
||||
const sourceRef = text(value, `${path}.configRef`);
|
||||
const resolved = resolveConfigRef(sourceRef, `${path}.configRef`);
|
||||
const selector = record(resolved.value, sourceRef);
|
||||
return {
|
||||
sourceRef,
|
||||
key: kubernetesLabelKey(selector.key, `${path}.configRef.key`),
|
||||
value: kubernetesLabelValue(selector.value, `${path}.configRef.value`),
|
||||
presence: true,
|
||||
fingerprint: `sha256:${resolved.sha256}`,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} 必须是 YAML object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function text(value: unknown, path: string): string {
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} 必须是非空字符串`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function nullableText(value: unknown, path: string): string | null {
|
||||
return value === undefined || value === null ? null : text(value, path);
|
||||
}
|
||||
|
||||
function bool(value: unknown, path: string): boolean {
|
||||
if (typeof value !== "boolean") throw new Error(`${path} 必须是 boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, path: string, max: number): number {
|
||||
if (!Number.isInteger(value) || Number(value) <= 0 || Number(value) > max) throw new Error(`${path} 必须是 1-${max} 的整数`);
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: unknown, path: string): number {
|
||||
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new Error(`${path} 必须是非负安全整数`);
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function simpleId(value: string, path: string): string {
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path} 必须是简单 ID`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function kubernetesName(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (result.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(result)) throw new Error(`${path} 必须是 Kubernetes DNS label`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function postgresIdentifier(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (!/^[a-z_][a-z0-9_]{0,62}$/u.test(result)) throw new Error(`${path} 必须是 PostgreSQL identifier`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function imageReference(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (/\s/u.test(result) || !result.includes(":")) throw new Error(`${path} 必须是带 tag/template 的镜像引用`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function imageRepository(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (/\s/u.test(result) || result.includes("@") || /:[^/]+$/u.test(result)) throw new Error(`${path} 必须是不带 tag/digest 的镜像仓库`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function relativePath(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (result.startsWith("/") || result.split(/[\\/]/u).includes("..")) throw new Error(`${path} 必须是仓库相对路径`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function absolutePath(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (!result.startsWith("/") || result.includes("\0")) throw new Error(`${path} 必须是绝对路径`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function envName(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(result)) throw new Error(`${path} 必须是合法环境变量名`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function duration(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (!/^[1-9][0-9]*(?:ms|s|m|h)$/u.test(result)) throw new Error(`${path} 必须是正 duration`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function httpPath(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (!result.startsWith("/") || /[\s?#]/u.test(result)) throw new Error(`${path} 必须是绝对 HTTP path`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function httpsUrl(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
const parsed = new URL(result);
|
||||
if (parsed.protocol !== "https:") throw new Error(`${path} 必须是 HTTPS URL`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function ipv4Address(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
const octets = result.split(".");
|
||||
if (octets.length !== 4 || octets.some((item) => !/^(?:0|[1-9][0-9]{0,2})$/u.test(item) || Number(item) > 255)) throw new Error(`${path} 必须是 IPv4 地址`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function otlpGrpcEndpoint(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (result.includes("://") || !/^[A-Za-z0-9.-]+:[1-9][0-9]{0,4}$/u.test(result)) throw new Error(`${path} 必须是 OTLP gRPC host:port`);
|
||||
const port = Number(result.slice(result.lastIndexOf(":") + 1));
|
||||
if (port > 65_535) throw new Error(`${path} 端口必须是 1-65535`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function kubernetesLabelKey(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
const parts = result.split("/");
|
||||
const name = parts.pop() ?? "";
|
||||
const prefix = parts.length === 1 ? parts[0] : null;
|
||||
const namePattern = /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u;
|
||||
const prefixPattern = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/u;
|
||||
if (parts.length > 1 || !namePattern.test(name) || (prefix !== null && !prefixPattern.test(prefix))) throw new Error(`${path} 必须是 Kubernetes label key`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function kubernetesLabelValue(value: unknown, path: string): string {
|
||||
const result = text(value, path);
|
||||
if (!/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(result)) throw new Error(`${path} 必须是 Kubernetes label value`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function nonEmptyStringList(value: unknown, path: string): string[] {
|
||||
if (!Array.isArray(value) || value.length === 0) throw new Error(`${path} 必须是非空字符串数组`);
|
||||
return value.map((item, index) => text(item, `${path}[${index}]`));
|
||||
}
|
||||
|
||||
function stringList<T extends string>(value: unknown, path: string, allowed: readonly T[]): T[] {
|
||||
const items = nonEmptyStringList(value, path).map((item) => oneOf(item, path, allowed));
|
||||
if (new Set(items).size !== items.length) throw new Error(`${path} 不能包含重复值`);
|
||||
return items;
|
||||
}
|
||||
|
||||
function oneOf<T extends string>(value: unknown, path: string, allowed: readonly T[]): T {
|
||||
if (typeof value !== "string" || !allowed.includes(value as T)) throw new Error(`${path} 必须是 ${allowed.join("、")}`);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function exact(value: unknown, path: string, expected: string): string {
|
||||
if (value !== expected) throw new Error(`${path} 必须是 ${expected}`);
|
||||
return expected;
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { rootPath } from "./config";
|
||||
import { runPikaoaCommand } from "./pikaoa-test-target";
|
||||
import type { SshCaptureResult } from "./ssh";
|
||||
|
||||
test("PikaOA test target submits once and exposes bounded typed status", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "pikaoa-test-target-"));
|
||||
const stateRoot = join(root, "state");
|
||||
const fakeRoot = join(root, "fake-k8s");
|
||||
const binDir = join(root, "bin");
|
||||
const secretsDir = join(root, "secrets");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
mkdirSync(secretsDir, { recursive: true });
|
||||
mkdirSync(join(fakeRoot, "namespaces"), { recursive: true });
|
||||
|
||||
const secretValues = ["fixture-db-password", "fixture-admin-password", "fixture-employee-password", "fixture-session-secret", "fixture-oa-admin-token"];
|
||||
for (const [name, value] of [
|
||||
["pikaoa-test.env", `DATABASE_URL=postgresql://pikaoa_test:${secretValues[0]}@db.example.invalid:5432/pikaoa_test?sslmode=require\nPIKAOA_SESSION_SECRET=${secretValues[3]}\nOA_ADMIN_TOKEN=${secretValues[4]}`],
|
||||
["pikaoa-test-admin-password.txt", secretValues[1]],
|
||||
["pikaoa-test-employee-password.txt", secretValues[2]],
|
||||
]) writeFileSync(join(secretsDir, name), `${value}\n`, { mode: 0o600 });
|
||||
|
||||
const fakeKubectl = join(binDir, "kubectl");
|
||||
writeFileSync(fakeKubectl, `#!/bin/sh
|
||||
set -u
|
||||
root="$FAKE_K8S_ROOT"
|
||||
args="$*"
|
||||
namespace=""
|
||||
previous=""
|
||||
for argument in "$@"; do
|
||||
if [ "$previous" = "-n" ]; then namespace="$argument"; fi
|
||||
previous="$argument"
|
||||
done
|
||||
namespace_after_resource() {
|
||||
previous=""
|
||||
for argument in "$@"; do
|
||||
if [ "$previous" = "namespace" ]; then printf '%s' "$argument"; return; fi
|
||||
previous="$argument"
|
||||
done
|
||||
}
|
||||
if printf '%s' "$args" | grep -Eq '(^| )apply( |$)'; then
|
||||
file=""
|
||||
previous=""
|
||||
for argument in "$@"; do
|
||||
if [ "$previous" = "-f" ]; then file="$argument"; fi
|
||||
previous="$argument"
|
||||
done
|
||||
if grep -q '"kind":"Namespace"' "$file"; then
|
||||
applied_identity="$(python3 - "$file" <<'PY'
|
||||
import json, pathlib, sys
|
||||
for line in pathlib.Path(sys.argv[1]).read_text().splitlines():
|
||||
if not line or line == "---":
|
||||
continue
|
||||
item = json.loads(line)
|
||||
if item.get("kind") == "Namespace":
|
||||
print(item["metadata"]["name"] + "|" + item["metadata"]["labels"]["pikaoa.unidesk.io/instance"])
|
||||
break
|
||||
PY
|
||||
)"
|
||||
applied_namespace="\${applied_identity%%|*}"
|
||||
applied_instance="\${applied_identity##*|}"
|
||||
printf '%s' "$applied_instance" >"$root/namespaces/$applied_namespace"
|
||||
fi
|
||||
[ ! -f "$root/slow" ] || sleep 0.2
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$args" | grep -q ' rollout status '; then
|
||||
[ ! -f "$root/slow" ] || sleep 0.4
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$args" | grep -q ' get job/pikaoa-init '; then
|
||||
if [ -f "$root/fail-initializer-query" ]; then printf '%s\n' 'job query failed' >&2; exit 1; fi
|
||||
if [ -f "$root/failure-target-initializer" ]; then
|
||||
printf '%s\n' '{"status":{"conditions":[{"type":"FailureTarget","status":"True","reason":"BackoffLimitExceeded"}]}}'
|
||||
elif [ -f "$root/fail-initializer" ]; then
|
||||
printf '%s\n' '{"status":{"conditions":[{"type":"Failed","status":"True","reason":"BackoffLimitExceeded"}]}}'
|
||||
else
|
||||
printf '%s\n' '{"status":{"conditions":[{"type":"Complete","status":"True"}]}}'
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$args" | grep -Eq '(^| )get namespace '; then
|
||||
name="$(namespace_after_resource "$@")"
|
||||
if [ -f "$root/fail-runtime-query" ]; then
|
||||
printf '%s\n' 'unable to connect to the server' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$root/namespaces/$name" ]; then
|
||||
printf 'Error from server (NotFound): namespaces "%s" not found\n' "$name" >&2
|
||||
exit 1
|
||||
fi
|
||||
instance="$(cat "$root/namespaces/$name")"
|
||||
if printf '%s' "$args" | grep -q 'managed-by'; then printf '%s' 'unidesk-pikaoa-test-target'; exit 0; fi
|
||||
if printf '%s' "$args" | grep -q 'unidesk.*target'; then printf '%s' 'TEST01'; exit 0; fi
|
||||
if printf '%s' "$args" | grep -q 'unidesk.*instance'; then printf '%s' "$instance"; exit 0; fi
|
||||
printf '{"metadata":{"name":"%s","labels":{"app.kubernetes.io/managed-by":"unidesk-pikaoa-test-target","pikaoa.unidesk.io/target":"TEST01","pikaoa.unidesk.io/instance":"%s"}}}\n' "$name" "$instance"
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$args" | grep -q ' get deployment,job,service,persistentvolumeclaim,pod '; then
|
||||
if [ -f "$root/fail-runtime-resource-query" ]; then
|
||||
printf '%s\n' 'resource query failed' >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' '{"items":[]}'
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$args" | grep -Eq '(^| )delete namespace '; then
|
||||
name="$(namespace_after_resource "$@")"
|
||||
rm -f "$root/namespaces/$name"
|
||||
exit 0
|
||||
fi
|
||||
printf 'unsupported fake kubectl call: %s\n' "$args" >&2
|
||||
exit 64
|
||||
`, { mode: 0o700 });
|
||||
chmodSync(fakeKubectl, 0o700);
|
||||
|
||||
const fixturePath = join(root, "pikaoa.yaml");
|
||||
const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8")
|
||||
.replace("validationOnly: true", "validationOnly: false")
|
||||
.replace("statusRequestTimeoutSeconds: 3", "statusRequestTimeoutSeconds: 31")
|
||||
.replace("logTailLines: 8", "logTailLines: 101")
|
||||
.replace("/tmp/unidesk-pikaoa-test-target-fixture", stateRoot);
|
||||
writeFileSync(fixturePath, fixture);
|
||||
|
||||
const config = {} as UniDeskConfig;
|
||||
let lastCapture: SshCaptureResult | null = null;
|
||||
const localCapture = async (_config: UniDeskConfig, _route: string, args: string[], stdin: string): Promise<SshCaptureResult> => {
|
||||
assert.deepEqual(args, ["sh"]);
|
||||
const child = Bun.spawn(["sh"], {
|
||||
env: { ...process.env, PATH: `${binDir}:${process.env.PATH ?? ""}`, FAKE_K8S_ROOT: fakeRoot },
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
child.stdin.write(stdin);
|
||||
child.stdin.end();
|
||||
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
||||
lastCapture = { stdout, stderr, exitCode };
|
||||
return lastCapture;
|
||||
};
|
||||
const args = (action: string, instance: string, withCommit = false, step = "all"): string[] => [
|
||||
"test-target", action, "--config", fixturePath, "--target", "TEST01", "--instance", instance,
|
||||
...(withCommit ? ["--commit", "0123456789abcdef"] : []),
|
||||
"--step", step,
|
||||
...((action === "start" || action === "stop") ? ["--confirm"] : []),
|
||||
"--output", "json",
|
||||
];
|
||||
const projection = async (action: string, instance: string, withCommit = false, step = "all"): Promise<Record<string, unknown>> => {
|
||||
const result = await runPikaoaCommand(config, args(action, instance, withCommit, step), localCapture);
|
||||
return JSON.parse(result.renderedText) as Record<string, unknown>;
|
||||
};
|
||||
const projectionWithCommit = async (action: string, instance: string, commit: string, step = "all"): Promise<Record<string, unknown>> => {
|
||||
const commandArgs = args(action, instance, true, step);
|
||||
commandArgs[commandArgs.indexOf("--commit") + 1] = commit;
|
||||
const result = await runPikaoaCommand(config, commandArgs, localCapture);
|
||||
return JSON.parse(result.renderedText) as Record<string, unknown>;
|
||||
};
|
||||
const waitFor = async (instance: string, expectedCode: string, step = "all"): Promise<Record<string, unknown>> => {
|
||||
let lastStatus: Record<string, unknown> | null = null;
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
const value = await projection("status", instance, false, step);
|
||||
const status = value.status as Record<string, unknown>;
|
||||
if (status.code === expectedCode) return value;
|
||||
lastStatus = status;
|
||||
await Bun.sleep(25);
|
||||
}
|
||||
assert.fail(`status did not reach ${expectedCode}: ${JSON.stringify(lastStatus)}`);
|
||||
};
|
||||
|
||||
try {
|
||||
const planned = await projection("plan", "runtime", true);
|
||||
const plan = planned.plan as Record<string, unknown>;
|
||||
const secret = plan.secret as Record<string, unknown>;
|
||||
const sources = secret.sources as Array<Record<string, unknown>>;
|
||||
assert.equal(sources.find((source) => source.purpose === "database.connection")?.presence, true);
|
||||
assert.equal(sources.find((source) => source.purpose === "runtime.adminToken")?.targetKey, "OA_ADMIN_TOKEN");
|
||||
assert.equal(sources.find((source) => source.purpose === "runtime.adminToken")?.valuesPrinted, false);
|
||||
const objects = plan.objects as Array<Record<string, unknown>>;
|
||||
const observability = plan.observability as Record<string, unknown>;
|
||||
const prometheus = observability.prometheus as Record<string, unknown>;
|
||||
const selector = prometheus.selector as Record<string, unknown>;
|
||||
assert.equal(objects.some((object) => object.kind === "StatefulSet" || object.name === "pikaoa-postgres"), false);
|
||||
assert.equal(objects.some((object) => object.kind === "Deployment" && object.name === "pikaoa-web"), true);
|
||||
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-api")?.podFsGroup, 65532);
|
||||
assert.equal(observability.otlpEndpoint, "otel-collector.observability.svc.cluster.local:4317");
|
||||
assert.equal(selector.sourceRef, "config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector");
|
||||
assert.equal(selector.presence, true);
|
||||
assert.match(String(selector.fingerprint), /^sha256:[0-9a-f]{64}$/u);
|
||||
assert.equal(selector.valuesPrinted, false);
|
||||
for (const component of ["pikaoa-api", "pikaoa-worker"]) {
|
||||
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === component)?.prometheusSelectorLabel, selector.value);
|
||||
}
|
||||
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-web")?.prometheusSelectorLabel, undefined);
|
||||
assert.deepEqual(plan.exposure, { serviceType: "NodePort", serviceName: "pikaoa-web", hostIP: "192.0.2.10", port: 32080 });
|
||||
assert.deepEqual(plan.webApiUpstream, { envName: "PIKAOA_API_UPSTREAM", value: "http://pikaoa-api:8080", scope: "same-namespace" });
|
||||
|
||||
const missing = await projection("status", "runtime", false, "api");
|
||||
assert.equal((missing.status as Record<string, unknown>).code, "start-not-found");
|
||||
|
||||
writeFileSync(join(fakeRoot, "slow"), "1\n");
|
||||
const submitted = await projection("start", "runtime", true);
|
||||
assert.equal(submitted.code, "start-submitted", JSON.stringify(lastCapture));
|
||||
assert.equal(submitted.mutation, true);
|
||||
assert.deepEqual((submitted.target as Record<string, unknown>).taskState, {
|
||||
rootPath: stateRoot,
|
||||
statusRequestTimeoutSeconds: 31,
|
||||
logTailLines: 101,
|
||||
});
|
||||
const running = await projection("status", "runtime");
|
||||
assert.equal((running.status as Record<string, unknown>).code, "start-running");
|
||||
const concurrentCommit = "fedcba9876543210";
|
||||
const concurrent = await projectionWithCommit("start", "runtime", concurrentCommit);
|
||||
assert.equal(concurrent.code, "start-instance-conflict");
|
||||
assert.equal(concurrent.mutation, false);
|
||||
assert.equal(concurrent.previousTaskReset, false);
|
||||
rmSync(join(fakeRoot, "slow"));
|
||||
const succeeded = await waitFor("runtime", "start-succeeded");
|
||||
const succeededStatus = succeeded.status as Record<string, unknown>;
|
||||
assert.equal((succeededStatus.task as Record<string, unknown>).exitCode, 0);
|
||||
assert.ok((succeededStatus.logTail as unknown[]).length <= 101);
|
||||
|
||||
writeFileSync(join(fakeRoot, "fail-runtime-query"), "1\n");
|
||||
const runtimeQueryFailed = await projection("status", "runtime");
|
||||
const runtimeQueryFailedStatus = runtimeQueryFailed.status as Record<string, unknown>;
|
||||
assert.equal(runtimeQueryFailed.ok, false);
|
||||
assert.equal(runtimeQueryFailedStatus.code, "runtime-query-failed");
|
||||
assert.equal(runtimeQueryFailedStatus.taskCode, "start-succeeded");
|
||||
assert.equal((runtimeQueryFailedStatus.runtime as Record<string, unknown>).code, "runtime-query-failed");
|
||||
const runtimeQueryFailedText = await runPikaoaCommand(config, args("status", "runtime").slice(0, -2), localCapture);
|
||||
assert.match(runtimeQueryFailedText.renderedText, /status=runtime-query-failed/);
|
||||
rmSync(join(fakeRoot, "fail-runtime-query"));
|
||||
|
||||
writeFileSync(join(fakeRoot, "fail-runtime-resource-query"), "1\n");
|
||||
const runtimeResourceQueryFailed = await projection("status", "runtime");
|
||||
const runtimeResourceQueryFailedStatus = runtimeResourceQueryFailed.status as Record<string, unknown>;
|
||||
assert.equal(runtimeResourceQueryFailed.ok, false);
|
||||
assert.equal(runtimeResourceQueryFailedStatus.code, "runtime-resource-query-failed");
|
||||
assert.equal(runtimeResourceQueryFailedStatus.taskCode, "start-succeeded");
|
||||
assert.equal((runtimeResourceQueryFailedStatus.runtime as Record<string, unknown>).code, "runtime-resource-query-failed");
|
||||
rmSync(join(fakeRoot, "fail-runtime-resource-query"));
|
||||
|
||||
const repeated = await projection("start", "runtime", true);
|
||||
assert.equal(repeated.code, "start-already-succeeded");
|
||||
assert.equal(repeated.mutation, false);
|
||||
const rerunSubmitted = await projectionWithCommit("start", "runtime", concurrentCommit);
|
||||
assert.equal(rerunSubmitted.code, "start-submitted");
|
||||
assert.equal(rerunSubmitted.previousTaskReset, true);
|
||||
const rerunSucceeded = await waitFor("runtime", "start-succeeded");
|
||||
assert.equal(((rerunSucceeded.status as Record<string, unknown>).task as Record<string, unknown>).sourceCommit, concurrentCommit);
|
||||
writeFileSync(fixturePath, readFileSync(fixturePath, "utf8").replace(" livenessPath: /healthz\n", " livenessPath: /healthz-v2\n"));
|
||||
const manifestRerunSubmitted = await projectionWithCommit("start", "runtime", concurrentCommit);
|
||||
assert.equal(manifestRerunSubmitted.code, "start-submitted");
|
||||
assert.equal(manifestRerunSubmitted.previousTaskReset, true);
|
||||
const manifestRerunSucceeded = await waitFor("runtime", "start-succeeded");
|
||||
await Bun.sleep(5);
|
||||
const sameRequestAfterRenderTimeChanged = await projectionWithCommit("start", "runtime", concurrentCommit);
|
||||
assert.equal(sameRequestAfterRenderTimeChanged.code, "start-already-succeeded");
|
||||
assert.equal(sameRequestAfterRenderTimeChanged.previousTaskReset, false);
|
||||
|
||||
writeFileSync(join(fakeRoot, "slow"), "1\n");
|
||||
const workerMissingSubmitted = await projection("start", "runtime", true, "api");
|
||||
const workerPid = Number((workerMissingSubmitted.remote as Record<string, unknown>).pid);
|
||||
assert.equal(Number.isSafeInteger(workerPid) && workerPid > 0, true);
|
||||
process.kill(workerPid, "SIGKILL");
|
||||
await Bun.sleep(25);
|
||||
const workerMissingReset = await projectionWithCommit("start", "runtime", concurrentCommit, "api");
|
||||
assert.equal(workerMissingReset.code, "start-submitted");
|
||||
assert.equal(workerMissingReset.previousTaskReset, true);
|
||||
rmSync(join(fakeRoot, "slow"));
|
||||
const workerMissingSucceeded = await waitFor("runtime", "start-succeeded", "api");
|
||||
|
||||
writeFileSync(join(fakeRoot, "fail-initializer"), "1\n");
|
||||
await projection("start", "runtime", true, "init");
|
||||
const failed = await waitFor("runtime", "start-failed", "init");
|
||||
const failedTask = ((failed.status as Record<string, unknown>).task as Record<string, unknown>);
|
||||
assert.equal(failedTask.code, "initializer-job-failed");
|
||||
assert.equal(failedTask.exitCode, 46);
|
||||
rmSync(join(fakeRoot, "fail-initializer"));
|
||||
|
||||
writeFileSync(join(fakeRoot, "failure-target-initializer"), "1\n");
|
||||
await projectionWithCommit("start", "runtime", concurrentCommit, "init");
|
||||
const failureTarget = await waitFor("runtime", "start-failed", "init");
|
||||
const failureTargetTask = ((failureTarget.status as Record<string, unknown>).task as Record<string, unknown>);
|
||||
assert.equal(failureTargetTask.code, "initializer-job-failure-target");
|
||||
assert.equal(failureTargetTask.exitCode, 46);
|
||||
rmSync(join(fakeRoot, "failure-target-initializer"));
|
||||
|
||||
writeFileSync(join(fakeRoot, "fail-initializer-query"), "1\n");
|
||||
await projectionWithCommit("start", "runtime", "abcdef0123456789", "init");
|
||||
const initializerQueryFailed = await waitFor("runtime", "start-failed", "init");
|
||||
const initializerQueryFailedTask = ((initializerQueryFailed.status as Record<string, unknown>).task as Record<string, unknown>);
|
||||
assert.equal(initializerQueryFailedTask.code, "initializer-job-query-failed");
|
||||
assert.equal(initializerQueryFailedTask.exitCode, 46);
|
||||
rmSync(join(fakeRoot, "fail-initializer-query"));
|
||||
|
||||
await projectionWithCommit("start", "runtime", "1234567890abcdef", "init");
|
||||
const initializerSucceeded = await waitFor("runtime", "start-succeeded", "init");
|
||||
assert.equal((((initializerSucceeded.status as Record<string, unknown>).task as Record<string, unknown>).exitCode), 0);
|
||||
|
||||
writeFileSync(join(fakeRoot, "slow"), "1\n");
|
||||
await projection("start", "runtime", true, "web");
|
||||
const stopped = await projection("stop", "runtime", false, "web");
|
||||
assert.equal(stopped.startTaskDisposition, "cancel-requested");
|
||||
assert.equal(stopped.mutation, true);
|
||||
await waitFor("runtime", "start-canceled", "web");
|
||||
|
||||
for (const name of [
|
||||
"pikaoa-test.env",
|
||||
"pikaoa-test-admin-password.txt",
|
||||
"pikaoa-test-employee-password.txt",
|
||||
]) rmSync(join(secretsDir, name));
|
||||
const foundationPlan = await projection("plan", "foundation", false, "foundation");
|
||||
assert.equal(foundationPlan.readyForMutation, true);
|
||||
assert.deepEqual(foundationPlan.missingFields, []);
|
||||
assert.deepEqual((foundationPlan.blockers as unknown[]), []);
|
||||
const foundationObjects = ((foundationPlan.plan as Record<string, unknown>).objects as Array<Record<string, unknown>>);
|
||||
assert.deepEqual(foundationObjects.map((object) => object.kind), ["Namespace", "PersistentVolumeClaim"]);
|
||||
assert.equal(((foundationPlan.plan as Record<string, unknown>).secret as Record<string, unknown>).requiredForStep, false);
|
||||
const foundationSubmitted = await projection("start", "foundation", false, "foundation");
|
||||
assert.equal(foundationSubmitted.code, "start-submitted", JSON.stringify(lastCapture));
|
||||
const foundationSucceeded = await waitFor("foundation", "start-succeeded", "foundation");
|
||||
const foundationRepeated = await projection("start", "foundation", false, "foundation");
|
||||
assert.equal(foundationRepeated.code, "start-already-succeeded");
|
||||
const foundationRerun = await projectionWithCommit("start", "foundation", concurrentCommit, "foundation");
|
||||
assert.equal(foundationRerun.code, "start-submitted");
|
||||
assert.equal(foundationRerun.previousTaskReset, true);
|
||||
const foundationRerunSucceeded = await waitFor("foundation", "start-succeeded", "foundation");
|
||||
|
||||
const serialized = JSON.stringify({
|
||||
submitted,
|
||||
succeeded,
|
||||
concurrent,
|
||||
rerunSubmitted,
|
||||
rerunSucceeded,
|
||||
manifestRerunSubmitted,
|
||||
manifestRerunSucceeded,
|
||||
sameRequestAfterRenderTimeChanged,
|
||||
workerMissingSubmitted,
|
||||
workerMissingReset,
|
||||
workerMissingSucceeded,
|
||||
failed,
|
||||
failureTarget,
|
||||
initializerQueryFailed,
|
||||
initializerSucceeded,
|
||||
stopped,
|
||||
foundationSubmitted,
|
||||
foundationSucceeded,
|
||||
foundationRepeated,
|
||||
foundationRerun,
|
||||
foundationRerunSucceeded,
|
||||
});
|
||||
for (const value of secretValues) assert.equal(serialized.includes(value), false);
|
||||
assert.equal(serialized.includes("postgresql://pikaoa_test:"), false);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
test("validation-only fixture never invokes remote capture", async () => {
|
||||
let calls = 0;
|
||||
const forbiddenCapture = async (): Promise<SshCaptureResult> => {
|
||||
calls += 1;
|
||||
throw new Error("validation-only fixture attempted remote capture");
|
||||
};
|
||||
const result = await runPikaoaCommand({} as UniDeskConfig, [
|
||||
"test-target", "status", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--output", "json",
|
||||
], forbiddenCapture);
|
||||
assert.equal(calls, 0);
|
||||
const payload = JSON.parse(result.renderedText) as Record<string, unknown>;
|
||||
assert.equal(payload.remoteQueried, false);
|
||||
assert.equal((payload.status as Record<string, unknown>).remoteQueried, false);
|
||||
|
||||
const planResult = await runPikaoaCommand({} as UniDeskConfig, [
|
||||
"test-target", "plan", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--commit", "0123456789abcdef", "--output", "json",
|
||||
], forbiddenCapture);
|
||||
const planPayload = JSON.parse(planResult.renderedText) as Record<string, unknown>;
|
||||
const plan = planPayload.plan as Record<string, unknown>;
|
||||
assert.deepEqual(plan.database, {
|
||||
configRef: "config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url",
|
||||
name: "pikaoa_test",
|
||||
username: "pikaoa_test",
|
||||
schema: "pikaoa",
|
||||
connection: {
|
||||
sourceRef: "./secrets/pikaoa-test.env",
|
||||
sourceKey: "DATABASE_URL",
|
||||
targetKey: "DATABASE_URL",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
dsnParameterPresence: { sslmode: true, search_path: true },
|
||||
valuesPrinted: false,
|
||||
});
|
||||
assert.deepEqual(plan.runtimeConfig, {
|
||||
secretName: "pikaoa-test-runtime",
|
||||
key: "pikaoa.yaml",
|
||||
mountPath: "/etc/pikaoa/pikaoa.yaml",
|
||||
consumers: ["initializer", "api", "worker"],
|
||||
valuesPrinted: false,
|
||||
});
|
||||
assert.equal(planPayload.valuesPrinted, false);
|
||||
assert.equal(((planPayload.observability as Record<string, unknown>).prometheus as Record<string, unknown>).selector !== undefined, true);
|
||||
assert.equal(JSON.stringify(planPayload).includes("fixture-db-password"), false);
|
||||
assert.equal(JSON.stringify(planPayload).includes("postgres://"), false);
|
||||
|
||||
const missingSchemaRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-schema-"));
|
||||
const missingSchemaPath = join(missingSchemaRoot, "pikaoa.yaml");
|
||||
const missingSchemaFixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8")
|
||||
.replace(" schema: pikaoa\n", "");
|
||||
writeFileSync(missingSchemaPath, missingSchemaFixture);
|
||||
try {
|
||||
await assert.rejects(
|
||||
runPikaoaCommand({} as UniDeskConfig, [
|
||||
"test-target", "status", "--config", missingSchemaPath, "--target", "TEST01", "--instance", "missing-schema", "--output", "json",
|
||||
], forbiddenCapture),
|
||||
/testRuntime\.targets\.TEST01\.database\.schema 必须是非空字符串/u,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
} finally {
|
||||
rmSync(missingSchemaRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const missingSelectorRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-selector-"));
|
||||
const missingSelectorPath = join(missingSelectorRoot, "pikaoa.yaml");
|
||||
const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8");
|
||||
writeFileSync(missingSelectorPath, fixture.replace(" configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector\n", ""));
|
||||
try {
|
||||
await assert.rejects(
|
||||
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", missingSelectorPath, "--target", "TEST01", "--instance", "missing-selector", "--output", "json"], forbiddenCapture),
|
||||
/observability\.prometheus\.configRef 必须是非空字符串/u,
|
||||
);
|
||||
} finally {
|
||||
rmSync(missingSelectorRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const invalidSelectorRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-invalid-selector-"));
|
||||
const invalidSelectorPath = join(invalidSelectorRoot, "pikaoa.yaml");
|
||||
writeFileSync(invalidSelectorPath, fixture.replace(
|
||||
"config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector",
|
||||
"config/fixtures/pikaoa-observability-invalid-label.yaml#metricsBackend.discovery.labelSelector",
|
||||
));
|
||||
try {
|
||||
await assert.rejects(
|
||||
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", invalidSelectorPath, "--target", "TEST01", "--instance", "invalid-selector", "--output", "json"], forbiddenCapture),
|
||||
/observability\.prometheus\.configRef\.key 必须是合法 Kubernetes label key/u,
|
||||
);
|
||||
} finally {
|
||||
rmSync(invalidSelectorRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const missingSelectorPathRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-selector-path-"));
|
||||
const missingSelectorConfigPath = join(missingSelectorPathRoot, "pikaoa.yaml");
|
||||
writeFileSync(missingSelectorConfigPath, fixture.replace("metricsBackend.discovery.labelSelector", "metricsBackend.discovery.missingSelector"));
|
||||
try {
|
||||
await assert.rejects(
|
||||
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", missingSelectorConfigPath, "--target", "TEST01", "--instance", "missing-selector-path", "--output", "json"], forbiddenCapture),
|
||||
/observability\.prometheus\.configRef 解析失败.*missingSelector/u,
|
||||
);
|
||||
} finally {
|
||||
rmSync(missingSelectorPathRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const owningConfig = Bun.YAML.parse(readFileSync(rootPath("config", "pikaoa.yaml"), "utf8")) as Record<string, unknown>;
|
||||
const testTarget = (((owningConfig.testRuntime as Record<string, unknown>).targets as Record<string, unknown>).NC01 as Record<string, unknown>);
|
||||
assert.equal((testTarget.observability as Record<string, unknown>).otlpEndpoint, "otel-collector.platform-infra.svc.cluster.local:4317");
|
||||
const production = (((owningConfig.delivery as Record<string, unknown>).targets as Record<string, unknown>).NC01 as Record<string, unknown>);
|
||||
const productionObservability = (production.runtime as Record<string, unknown>).observability as Record<string, unknown>;
|
||||
assert.equal(productionObservability.otlpEndpoint, "otel-collector.platform-infra.svc.cluster.local:4317");
|
||||
assert.equal("otlpHttpEndpoint" in productionObservability, false);
|
||||
|
||||
const unconfiguredRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-unconfigured-"));
|
||||
const unconfiguredPath = join(unconfiguredRoot, "pikaoa.yaml");
|
||||
writeFileSync(unconfiguredPath, "version: 1\nkind: pikaoa-platform-delivery\ntestRuntime:\n defaultTargetId: null\n targets: {}\n");
|
||||
try {
|
||||
const unconfigured = await runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", unconfiguredPath, "--output", "json"], forbiddenCapture);
|
||||
const unconfiguredPayload = JSON.parse(unconfigured.renderedText) as Record<string, unknown>;
|
||||
assert.equal(calls, 0);
|
||||
assert.equal(unconfiguredPayload.configured, false);
|
||||
assert.equal(unconfiguredPayload.remoteQueried, false);
|
||||
} finally {
|
||||
rmSync(unconfiguredRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,18 +49,18 @@ describe("platform-infra Gitea repository GitHub credentials", () => {
|
||||
|
||||
test("keeps PikaOA master and release authorities on distinct Secret keys", () => {
|
||||
const config = readGiteaConfig();
|
||||
const testAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-test-nc01");
|
||||
const developmentAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-dev-nc01");
|
||||
const productionAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-nc01");
|
||||
const testKey = testAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
|
||||
const developmentKey = developmentAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
|
||||
const productionKey = productionAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
|
||||
|
||||
expect(testAuthority?.upstream.branch).toBe("master");
|
||||
expect(developmentAuthority?.upstream.branch).toBe("master");
|
||||
expect(productionAuthority?.upstream.branch).toBe("release");
|
||||
expect(testKey).toBe("github-token-pikainc-pikaoa");
|
||||
expect(developmentKey).toBe("github-token-pikainc-pikaoa");
|
||||
expect(productionKey).toBe("github-token-pikainc-pikaoa-release");
|
||||
expect(productionKey).not.toBe(testKey);
|
||||
expect(productionKey).not.toBe(developmentKey);
|
||||
expect(githubTokenEnvNameForSecretKey(productionKey ?? ""))
|
||||
.not.toBe(githubTokenEnvNameForSecretKey(testKey ?? ""));
|
||||
.not.toBe(githubTokenEnvNameForSecretKey(developmentKey ?? ""));
|
||||
});
|
||||
|
||||
test("normalizes Secret keys to bounded environment variable names", () => {
|
||||
|
||||
@@ -74,7 +74,15 @@ test("versioned admission desired state reports controller proof and candidate d
|
||||
toState: "started",
|
||||
}]);
|
||||
expect(contract.child.enabled).toBe(false);
|
||||
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02", "platform-infra-sub2rank-nc01", "selfmedia-nc01", "selfmedia-production-nc01", "pikaoa-test-nc01"]);
|
||||
expect(contract.consumers.map((item) => item.id)).toEqual([
|
||||
"agentrun-nc01-v02",
|
||||
"agentrun-nc01-release",
|
||||
"platform-infra-sub2rank-nc01",
|
||||
"selfmedia-nc01",
|
||||
"selfmedia-production-nc01",
|
||||
"pikaoa-dev-nc01",
|
||||
"pikaoa-nc01",
|
||||
]);
|
||||
const items = documents(renderPacAdmissionDesiredFragment("NC01"));
|
||||
expect(items.map((item) => item.kind)).toEqual(["ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
|
||||
const policy = items[0];
|
||||
|
||||
@@ -159,19 +159,19 @@ test("unselected parse and repository errors remain warnings while selected obje
|
||||
expect(() => parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-production-nc01" })).toThrow("namespace");
|
||||
|
||||
const invalidRepositoryDocument = pacConfigDocument();
|
||||
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-test-nc01");
|
||||
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-dev-nc01");
|
||||
if (invalidRepository === undefined) throw new Error("pikaoa repository fixture is required");
|
||||
invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
|
||||
const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
|
||||
expect(isolated.validationWarnings).toContainEqual({
|
||||
code: "pac-unselected-repository-invalid",
|
||||
object: { kind: "repository", id: "pikaoa-test-nc01" },
|
||||
object: { kind: "repository", id: "pikaoa-dev-nc01" },
|
||||
consumer: null,
|
||||
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-test-nc01",
|
||||
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-dev-nc01",
|
||||
blocking: false,
|
||||
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
|
||||
});
|
||||
expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-test-nc01")).toBe(false);
|
||||
expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-dev-nc01")).toBe(false);
|
||||
expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl");
|
||||
|
||||
const duplicateSelectedRepository = pacConfigDocument();
|
||||
|
||||
@@ -99,11 +99,11 @@ function runtimeInput(commit: string | null = sourceCommit): Record<string, unkn
|
||||
}
|
||||
|
||||
describe("PaC source artifact CLI contract", () => {
|
||||
test("accepts PikaOA test runtime provenance", () => {
|
||||
test("accepts PikaOA development runtime provenance", () => {
|
||||
const expected = {
|
||||
configRef: "config/pikaoa.yaml#testRuntime.targets.NC01",
|
||||
configRef: "config/pikaoa.yaml#developmentRuntime.targets.NC01",
|
||||
effectiveConfigSha256: `sha256:${"a".repeat(64)}`,
|
||||
renderer: "pikaoa-test-runtime",
|
||||
renderer: "pikaoa-development-runtime",
|
||||
mode: "embedded-pipeline-spec",
|
||||
} as const;
|
||||
const manifest = {
|
||||
@@ -113,9 +113,9 @@ describe("PaC source artifact CLI contract", () => {
|
||||
});
|
||||
|
||||
test("uses PikaOA-specific PipelineRun labels", () => {
|
||||
const binding = { consumer: { id: "pikaoa-test-nc01", node: "NC01" } } as Parameters<typeof pipelineRunLabels>[0];
|
||||
const binding = { consumer: { id: "pikaoa-dev-nc01", node: "NC01" } } as Parameters<typeof pipelineRunLabels>[0];
|
||||
expect(pipelineRunLabels(binding, "pikaoa")).toEqual({
|
||||
"app.kubernetes.io/name": "pikaoa-test-nc01-pac",
|
||||
"app.kubernetes.io/name": "pikaoa-dev-nc01-pac",
|
||||
"app.kubernetes.io/part-of": "pikaoa",
|
||||
"unidesk.ai/source-commit": "{{ revision }}",
|
||||
"pikaoa.unidesk.io/source-commit": "{{ revision }}",
|
||||
@@ -125,7 +125,7 @@ describe("PaC source artifact CLI contract", () => {
|
||||
|
||||
test("maps the PikaOA shared workspace to the declared RWO PVC size", () => {
|
||||
const binding = {
|
||||
consumer: { sourceArtifact: { renderer: "pikaoa-test-runtime" } },
|
||||
consumer: { sourceArtifact: { renderer: "pikaoa-development-runtime" } },
|
||||
repository: { params: { workspace_pvc_size: "8Gi" } },
|
||||
} as Parameters<typeof pipelineRunWorkspaces>[0];
|
||||
expect(pipelineRunWorkspaces(binding, { workspaces: [{ name: "workspace" }] })).toEqual([{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
import { createHash } from "node:crypto";
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -19,10 +20,10 @@ import { stableJsonSha256, stableJsonValue } from "./stable-json";
|
||||
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
|
||||
import { renderSub2RankDesiredPipeline, sub2RankOwningSourceRemote } from "./platform-infra-sub2rank-pipeline";
|
||||
import { renderSelfMediaDesiredPipeline, selfMediaSourceWorktreeRemote } from "./selfmedia-delivery-renderer";
|
||||
import { renderPikaoaTestDesiredPipeline, pikaoaReleaseSourceWorktreeRemote, pikaoaTestSourceWorktreeRemote } from "./pikaoa-test-delivery-renderer";
|
||||
import { renderPikaoaDesiredPipeline, pikaoaDevelopmentSourceWorktreeRemote, pikaoaReleaseSourceWorktreeRemote } from "./pikaoa-delivery-renderer";
|
||||
|
||||
export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation";
|
||||
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime" | "pikaoa-test-runtime" | "pikaoa-release-runtime";
|
||||
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime" | "pikaoa-development-runtime" | "pikaoa-release-runtime";
|
||||
export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
|
||||
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
|
||||
|
||||
@@ -502,7 +503,7 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
|
||||
? renderSub2RankDesiredPipeline(binding)
|
||||
: sourceArtifact.renderer === "selfmedia-runtime"
|
||||
? renderSelfMediaPipeline(binding, sourceWorktree)
|
||||
: renderPikaoaTestPipeline(binding, sourceWorktree);
|
||||
: renderPikaoaPipeline(binding, sourceWorktree);
|
||||
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
|
||||
? rendered.pipeline
|
||||
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
|
||||
@@ -586,8 +587,8 @@ function renderSelfMediaPipeline(binding: PacSourceArtifactBinding, sourceWorktr
|
||||
};
|
||||
}
|
||||
|
||||
function renderPikaoaTestPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
|
||||
const rendered = renderPikaoaTestDesiredPipeline(binding, sourceWorktree);
|
||||
function renderPikaoaPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
|
||||
const rendered = renderPikaoaDesiredPipeline(binding, sourceWorktree);
|
||||
return {
|
||||
pipeline: rendered.pipeline,
|
||||
provenance: {
|
||||
@@ -675,7 +676,7 @@ function embeddedPipelineRun(binding: PacSourceArtifactBinding, desiredSpec: Rec
|
||||
? "sub2rank"
|
||||
: provenance.renderer === "selfmedia-runtime"
|
||||
? "selfmedia"
|
||||
: provenance.renderer === "pikaoa-test-runtime"
|
||||
: provenance.renderer === "pikaoa-development-runtime" || provenance.renderer === "pikaoa-release-runtime"
|
||||
? "pikaoa"
|
||||
: "agentrun",
|
||||
),
|
||||
@@ -857,7 +858,7 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
|
||||
}
|
||||
if (name === "git-url") return { name, value: "{{ repo_url }}" };
|
||||
const repositoryParam = name.replaceAll("-", "_");
|
||||
if ((binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") && Object.prototype.hasOwnProperty.call(param, "default")) {
|
||||
if ((binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-development-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") && Object.prototype.hasOwnProperty.call(param, "default")) {
|
||||
return { name, value: param.default };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) {
|
||||
@@ -875,7 +876,7 @@ export function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desired
|
||||
if (desiredSpec.workspaces === undefined) return [];
|
||||
return arrayRecords(desiredSpec.workspaces, "Pipeline.spec.workspaces").map((workspace) => {
|
||||
const name = requiredString(workspace.name, "Pipeline.spec.workspaces[].name");
|
||||
if (name === "source" || (name === "workspace" && (binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"))) {
|
||||
if (name === "source" || (name === "workspace" && (binding.consumer.sourceArtifact.renderer === "pikaoa-development-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"))) {
|
||||
return {
|
||||
name,
|
||||
volumeClaimTemplate: {
|
||||
@@ -953,7 +954,7 @@ function owningSourceRemote(binding: PacSourceArtifactBinding): string {
|
||||
}
|
||||
if (binding.consumer.sourceArtifact.renderer === "sub2rank-platform-service") return sub2RankOwningSourceRemote();
|
||||
if (binding.consumer.sourceArtifact.renderer === "selfmedia-runtime") return selfMediaSourceWorktreeRemote(binding.consumer.node);
|
||||
if (binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime") return pikaoaTestSourceWorktreeRemote(binding.consumer.node);
|
||||
if (binding.consumer.sourceArtifact.renderer === "pikaoa-development-runtime") return pikaoaDevelopmentSourceWorktreeRemote(binding.consumer.node);
|
||||
if (binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") return pikaoaReleaseSourceWorktreeRemote(binding.consumer.node);
|
||||
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
|
||||
return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl;
|
||||
@@ -1074,7 +1075,7 @@ export function pacSourceArtifactProvenanceFromManifest(manifest: Record<string,
|
||||
&& provenance.renderer !== "hwlab-runtime-lane"
|
||||
&& provenance.renderer !== "sub2rank-platform-service"
|
||||
&& provenance.renderer !== "selfmedia-runtime"
|
||||
&& provenance.renderer !== "pikaoa-test-runtime"
|
||||
&& provenance.renderer !== "pikaoa-development-runtime"
|
||||
&& provenance.renderer !== "pikaoa-release-runtime") return null;
|
||||
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
|
||||
return provenance as PacSourceArtifactProvenance;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
@@ -869,7 +870,7 @@ function parseConsumerDeliveryProvenance(value: Record<string, unknown>, path: s
|
||||
|
||||
function parseSourceArtifact(value: Record<string, unknown>, path: string): PacSourceArtifactSpec {
|
||||
const mode = y.enumField(value, "mode", path, ["embedded-pipeline-spec", "remote-pipeline-annotation"] as const);
|
||||
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime", "pikaoa-test-runtime", "pikaoa-release-runtime"] as const);
|
||||
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime", "pikaoa-development-runtime", "pikaoa-release-runtime"] as const);
|
||||
const pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`);
|
||||
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
|
||||
const taskRunTemplate = y.objectField(value, "taskRunTemplate", path);
|
||||
@@ -879,7 +880,7 @@ function parseSourceArtifact(value: Record<string, unknown>, path: string): PacS
|
||||
if (renderer === "hwlab-runtime-lane" && mode !== "remote-pipeline-annotation") throw new Error(`${path}.renderer hwlab-runtime-lane requires remote-pipeline-annotation`);
|
||||
if (renderer === "sub2rank-platform-service" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer sub2rank-platform-service requires embedded-pipeline-spec`);
|
||||
if (renderer === "selfmedia-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer selfmedia-runtime requires embedded-pipeline-spec`);
|
||||
if (renderer === "pikaoa-test-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-test-runtime requires embedded-pipeline-spec`);
|
||||
if (renderer === "pikaoa-development-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-development-runtime requires embedded-pipeline-spec`);
|
||||
if (renderer === "pikaoa-release-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-release-runtime requires embedded-pipeline-spec`);
|
||||
return {
|
||||
mode,
|
||||
@@ -1221,7 +1222,7 @@ function validateConsumerConfig(
|
||||
if (consumer.sourceArtifact?.renderer === "sub2rank-platform-service" && consumer.runnerServiceAccount === null) {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for sub2rank-platform-service`);
|
||||
}
|
||||
if (consumer.sourceArtifact?.renderer === "selfmedia-runtime" || consumer.sourceArtifact?.renderer === "pikaoa-test-runtime") {
|
||||
if (consumer.sourceArtifact?.renderer === "selfmedia-runtime" || consumer.sourceArtifact?.renderer === "pikaoa-development-runtime") {
|
||||
if (consumer.runnerServiceAccount === null) throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for private GitOps runtime renderers`);
|
||||
if (consumer.argoBootstrap?.repositoryCredential === null || consumer.argoBootstrap?.repositoryCredential === undefined) {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private repository`);
|
||||
|
||||
Reference in New Issue
Block a user