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
|
- 2110
|
||||||
- 2128
|
- 2128
|
||||||
- 2263
|
- 2263
|
||||||
|
- 2274
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
targetId: NC01
|
targetId: NC01
|
||||||
@@ -29,45 +30,30 @@ modules:
|
|||||||
events: transactional-outbox
|
events: transactional-outbox
|
||||||
workers: named-consumer
|
workers: named-consumer
|
||||||
|
|
||||||
testRuntime:
|
developmentRuntime:
|
||||||
defaultTargetId: NC01
|
defaultTargetId: NC01
|
||||||
targets:
|
targets:
|
||||||
NC01:
|
NC01:
|
||||||
enabled: true
|
enabled: true
|
||||||
validationOnly: false
|
|
||||||
node: NC01
|
node: NC01
|
||||||
route: NC01:k3s
|
route: NC01:k3s
|
||||||
namespace: pikaoa-test
|
namespace: pikaoa-dev
|
||||||
ttlSeconds: 86400
|
fieldManager: unidesk-pikaoa-development-runtime
|
||||||
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
|
|
||||||
source:
|
source:
|
||||||
mode: prebuilt-images
|
mode: pac-source-build
|
||||||
repository: pikainc/pikaoa
|
repository: pikainc/pikaoa
|
||||||
commitPolicy: cli-required
|
commitPolicy: pac-revision-required
|
||||||
branch: master
|
branch: master
|
||||||
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
||||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/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
|
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
|
pullPolicy: IfNotPresent
|
||||||
ci:
|
ci:
|
||||||
lane: pikaoa-test
|
lane: pikaoa-dev
|
||||||
namespace: pikaoa-ci
|
namespace: pikaoa-ci
|
||||||
pipeline: pikaoa-test-nc01-pac
|
pipeline: pikaoa-dev-nc01-pac
|
||||||
pipelineRunPrefix: pikaoa-test-nc01
|
pipelineRunPrefix: pikaoa-dev-nc01
|
||||||
serviceAccountName: pikaoa-test-nc01-tekton-runner
|
serviceAccountName: pikaoa-dev-nc01-tekton-runner
|
||||||
workspaceSize: 8Gi
|
workspaceSize: 8Gi
|
||||||
pipelineTimeout: 20m0s
|
pipelineTimeout: 20m0s
|
||||||
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||||
@@ -85,22 +71,22 @@ testRuntime:
|
|||||||
gitops:
|
gitops:
|
||||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
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
|
writeUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||||
branch: nc01-pikaoa-test-gitops
|
branch: nc01-pikaoa-dev-gitops
|
||||||
manifestPath: deploy/gitops/nc01-test/resources.yaml
|
manifestPath: deploy/gitops/nc01-dev/resources.yaml
|
||||||
credential:
|
credential:
|
||||||
secretName: pac-gitea-pikaoa-test-nc01
|
secretName: pac-gitea-pikaoa-dev-nc01
|
||||||
tokenKey: token
|
tokenKey: token
|
||||||
username: unidesk-admin
|
username: unidesk-admin
|
||||||
author:
|
author:
|
||||||
name: PikaOA Test NC01 CI
|
name: PikaOA Development NC01 CI
|
||||||
email: pikaoa-test-nc01-ci@unidesk.local
|
email: pikaoa-dev-nc01-ci@unidesk.local
|
||||||
database:
|
database:
|
||||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-dev-database-url
|
||||||
name: pikaoa_test
|
name: pikaoa_dev
|
||||||
username: pikaoa_test
|
username: pikaoa_dev
|
||||||
schema: pikaoa
|
schema: pikaoa
|
||||||
connection:
|
connection:
|
||||||
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-test.env
|
sourceRef: /root/.unidesk/.state/secrets/platform-infra/pikaoa-dev.env
|
||||||
sourceKey: DATABASE_URL
|
sourceKey: DATABASE_URL
|
||||||
targetKey: DATABASE_URL
|
targetKey: DATABASE_URL
|
||||||
runtime:
|
runtime:
|
||||||
@@ -113,7 +99,8 @@ testRuntime:
|
|||||||
worker: { 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 } }
|
web: { requests: { cpu: 50m, memory: 64Mi }, limits: { cpu: 500m, memory: 256Mi } }
|
||||||
initializer: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 1, memory: 512Mi } }
|
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
|
apiServiceName: pikaoa-api
|
||||||
apiPort: 8080
|
apiPort: 8080
|
||||||
workerMetricsPort: 8081
|
workerMetricsPort: 8081
|
||||||
@@ -146,11 +133,11 @@ testRuntime:
|
|||||||
sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
||||||
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
||||||
sessionSecret:
|
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
|
sourceKey: PIKAOA_SESSION_SECRET
|
||||||
targetKey: PIKAOA_SESSION_SECRET
|
targetKey: PIKAOA_SESSION_SECRET
|
||||||
adminToken:
|
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
|
sourceKey: OA_ADMIN_TOKEN
|
||||||
targetKey: OA_ADMIN_TOKEN
|
targetKey: OA_ADMIN_TOKEN
|
||||||
initializer:
|
initializer:
|
||||||
@@ -197,33 +184,18 @@ releaseRuntime:
|
|||||||
targets:
|
targets:
|
||||||
NC01:
|
NC01:
|
||||||
enabled: true
|
enabled: true
|
||||||
validationOnly: false
|
|
||||||
node: NC01
|
node: NC01
|
||||||
route: NC01:k3s
|
route: NC01:k3s
|
||||||
namespace: pikaoa
|
namespace: pikaoa
|
||||||
ttlSeconds: 86400
|
|
||||||
waitTimeoutSeconds: 300
|
|
||||||
fieldManager: unidesk-pikaoa-release-runtime
|
fieldManager: unidesk-pikaoa-release-runtime
|
||||||
taskState:
|
|
||||||
rootPath: /var/lib/unidesk/pikaoa-release-runtime
|
|
||||||
statusRequestTimeoutSeconds: 10
|
|
||||||
logTailLines: 80
|
|
||||||
cleanup:
|
|
||||||
mode: delete-namespace
|
|
||||||
requireOwnershipLabels: true
|
|
||||||
source:
|
source:
|
||||||
mode: prebuilt-images
|
mode: pac-source-build
|
||||||
repository: pikainc/pikaoa
|
repository: pikainc/pikaoa
|
||||||
commitPolicy: cli-required
|
commitPolicy: pac-revision-required
|
||||||
branch: release
|
branch: release
|
||||||
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
worktreeRemote: https://github.com/pikainc/pikaoa.git
|
||||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/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
|
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
|
pullPolicy: IfNotPresent
|
||||||
ci:
|
ci:
|
||||||
lane: pikaoa
|
lane: pikaoa
|
||||||
@@ -277,6 +249,7 @@ releaseRuntime:
|
|||||||
web: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 500m, memory: 512Mi } }
|
web: { requests: { cpu: 100m, memory: 128Mi }, limits: { cpu: 500m, memory: 512Mi } }
|
||||||
initializer: { requests: { cpu: 250m, memory: 256Mi }, limits: { cpu: 1, memory: 1Gi } }
|
initializer: { requests: { cpu: 250m, memory: 256Mi }, limits: { cpu: 1, memory: 1Gi } }
|
||||||
secretName: pikaoa-runtime
|
secretName: pikaoa-runtime
|
||||||
|
configMapName: pikaoa-runtime-config
|
||||||
apiServiceName: pikaoa-api
|
apiServiceName: pikaoa-api
|
||||||
apiPort: 8080
|
apiPort: 8080
|
||||||
workerMetricsPort: 8081
|
workerMetricsPort: 8081
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ metadata:
|
|||||||
relatedIssues:
|
relatedIssues:
|
||||||
- 1875
|
- 1875
|
||||||
- 2250
|
- 2250
|
||||||
|
- 2274
|
||||||
|
|
||||||
cluster:
|
cluster:
|
||||||
role: primary
|
role: primary
|
||||||
@@ -126,8 +127,8 @@ postgres:
|
|||||||
address: 10.42.0.0/16
|
address: 10.42.0.0/16
|
||||||
method: scram-sha-256
|
method: scram-sha-256
|
||||||
- type: hostssl
|
- type: hostssl
|
||||||
database: pikaoa_test
|
database: pikaoa_dev
|
||||||
user: pikaoa_test
|
user: pikaoa_dev
|
||||||
address: 10.42.0.0/16
|
address: 10.42.0.0/16
|
||||||
method: scram-sha-256
|
method: scram-sha-256
|
||||||
|
|
||||||
@@ -191,20 +192,20 @@ secrets:
|
|||||||
PIKAOA_DB_NAME: pikaoa
|
PIKAOA_DB_NAME: pikaoa
|
||||||
randomHex:
|
randomHex:
|
||||||
PIKAOA_DB_PASSWORD: 32
|
PIKAOA_DB_PASSWORD: 32
|
||||||
- name: pikaoa-test-nc01-db-credentials
|
- name: pikaoa-dev-nc01-db-credentials
|
||||||
sourceRef: platform-db/pikaoa-test-db.env
|
sourceRef: platform-db/pikaoa-dev-db.env
|
||||||
type: env
|
type: env
|
||||||
requiredKeys:
|
requiredKeys:
|
||||||
- PIKAOA_TEST_DB_USER
|
- PIKAOA_DEV_DB_USER
|
||||||
- PIKAOA_TEST_DB_PASSWORD
|
- PIKAOA_DEV_DB_PASSWORD
|
||||||
- PIKAOA_TEST_DB_NAME
|
- PIKAOA_DEV_DB_NAME
|
||||||
createIfMissing:
|
createIfMissing:
|
||||||
enabled: true
|
enabled: true
|
||||||
values:
|
values:
|
||||||
PIKAOA_TEST_DB_USER: pikaoa_test
|
PIKAOA_DEV_DB_USER: pikaoa_dev
|
||||||
PIKAOA_TEST_DB_NAME: pikaoa_test
|
PIKAOA_DEV_DB_NAME: pikaoa_dev
|
||||||
randomHex:
|
randomHex:
|
||||||
PIKAOA_TEST_DB_PASSWORD: 32
|
PIKAOA_DEV_DB_PASSWORD: 32
|
||||||
|
|
||||||
objects:
|
objects:
|
||||||
roles:
|
roles:
|
||||||
@@ -244,10 +245,10 @@ objects:
|
|||||||
createdb: false
|
createdb: false
|
||||||
createrole: false
|
createrole: false
|
||||||
superuser: false
|
superuser: false
|
||||||
- name: pikaoa_test
|
- name: pikaoa_dev
|
||||||
passwordRef:
|
passwordRef:
|
||||||
sourceRef: platform-db/pikaoa-test-db.env
|
sourceRef: platform-db/pikaoa-dev-db.env
|
||||||
key: PIKAOA_TEST_DB_PASSWORD
|
key: PIKAOA_DEV_DB_PASSWORD
|
||||||
login: true
|
login: true
|
||||||
attributes:
|
attributes:
|
||||||
createdb: false
|
createdb: false
|
||||||
@@ -274,8 +275,8 @@ objects:
|
|||||||
encoding: UTF8
|
encoding: UTF8
|
||||||
locale: C.UTF-8
|
locale: C.UTF-8
|
||||||
extensions: []
|
extensions: []
|
||||||
- name: pikaoa_test
|
- name: pikaoa_dev
|
||||||
owner: pikaoa_test
|
owner: pikaoa_dev
|
||||||
encoding: UTF8
|
encoding: UTF8
|
||||||
locale: C.UTF-8
|
locale: C.UTF-8
|
||||||
extensions: []
|
extensions: []
|
||||||
@@ -349,20 +350,20 @@ exports:
|
|||||||
- scope: pikaoa
|
- scope: pikaoa
|
||||||
secret: pikaoa-runtime
|
secret: pikaoa-runtime
|
||||||
key: DATABASE_URL
|
key: DATABASE_URL
|
||||||
- name: pikaoa-test-database-url
|
- name: pikaoa-dev-database-url
|
||||||
sourceSecretRef: platform-db/pikaoa-test-db.env
|
sourceSecretRef: platform-db/pikaoa-dev-db.env
|
||||||
render:
|
render:
|
||||||
envKey: DATABASE_URL
|
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:
|
variables:
|
||||||
PGHOST: 10.42.0.1
|
PGHOST: 10.42.0.1
|
||||||
writeToSecretSource:
|
writeToSecretSource:
|
||||||
sourceRef: platform-infra/pikaoa-test.env
|
sourceRef: platform-infra/pikaoa-dev.env
|
||||||
key: DATABASE_URL
|
key: DATABASE_URL
|
||||||
mode: update-or-insert
|
mode: update-or-insert
|
||||||
consumers:
|
consumers:
|
||||||
- scope: pikaoa-test
|
- scope: pikaoa-dev
|
||||||
secret: pikaoa-test-runtime
|
secret: pikaoa-dev-runtime
|
||||||
key: DATABASE_URL
|
key: DATABASE_URL
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ metadata:
|
|||||||
- 1560
|
- 1560
|
||||||
- 1760
|
- 1760
|
||||||
- 1768
|
- 1768
|
||||||
|
- 2274
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
targetId: NC01
|
targetId: NC01
|
||||||
@@ -489,7 +490,7 @@ sourceAuthority:
|
|||||||
naming: gitea-actions-immutable-source
|
naming: gitea-actions-immutable-source
|
||||||
prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
|
prefix: refs/unidesk/snapshots/gitea-actions/selfmedia-release-nc01
|
||||||
legacyGitMirror: null
|
legacyGitMirror: null
|
||||||
- key: pikaoa-test-nc01
|
- key: pikaoa-dev-nc01
|
||||||
targetId: NC01
|
targetId: NC01
|
||||||
credentialOverride:
|
credentialOverride:
|
||||||
github:
|
github:
|
||||||
@@ -529,7 +530,7 @@ sourceAuthority:
|
|||||||
publicRead: false
|
publicRead: false
|
||||||
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||||
gitops:
|
gitops:
|
||||||
branch: nc01-pikaoa-test-gitops
|
branch: nc01-pikaoa-dev-gitops
|
||||||
flushDisposition: gitea-writeback
|
flushDisposition: gitea-writeback
|
||||||
snapshot:
|
snapshot:
|
||||||
naming: gitea-actions-immutable-source
|
naming: gitea-actions-immutable-source
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ metadata:
|
|||||||
- 1769
|
- 1769
|
||||||
- 1802
|
- 1802
|
||||||
- 1811
|
- 1811
|
||||||
|
- 2274
|
||||||
defaults:
|
defaults:
|
||||||
targetId: NC01
|
targetId: NC01
|
||||||
consumerId: hwlab-nc01-v03
|
consumerId: hwlab-nc01-v03
|
||||||
@@ -247,15 +248,15 @@ repositories:
|
|||||||
runtime_service_port: "4317"
|
runtime_service_port: "4317"
|
||||||
health_path: /healthz
|
health_path: /healthz
|
||||||
health_url: http://selfmedia.selfmedia.svc.cluster.local:4317/healthz
|
health_url: http://selfmedia.selfmedia.svc.cluster.local:4317/healthz
|
||||||
- id: pikaoa-test-nc01
|
- id: pikaoa-dev-nc01
|
||||||
name: pikaoa-test-nc01
|
name: pikaoa-dev-nc01
|
||||||
namespace: pikaoa-ci
|
namespace: pikaoa-ci
|
||||||
providerType: gitea
|
providerType: gitea
|
||||||
url: https://gitea.pikapython.com/mirrors/pikainc-pikaoa
|
url: https://gitea.pikapython.com/mirrors/pikainc-pikaoa
|
||||||
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||||
owner: mirrors
|
owner: mirrors
|
||||||
repo: pikainc-pikaoa
|
repo: pikainc-pikaoa
|
||||||
secretName: pac-gitea-pikaoa-test-nc01
|
secretName: pac-gitea-pikaoa-dev-nc01
|
||||||
tokenKey: token
|
tokenKey: token
|
||||||
webhookSecretKey: webhook.secret
|
webhookSecretKey: webhook.secret
|
||||||
concurrencyLimit: 1
|
concurrencyLimit: 1
|
||||||
@@ -264,9 +265,9 @@ repositories:
|
|||||||
source_branch: master
|
source_branch: master
|
||||||
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-nc01
|
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/pikaoa-master-nc01
|
||||||
node: NC01
|
node: NC01
|
||||||
pipeline_name: pikaoa-test-nc01-pac
|
pipeline_name: pikaoa-dev-nc01-pac
|
||||||
pipeline_run_prefix: pikaoa-test-nc01
|
pipeline_run_prefix: pikaoa-dev-nc01
|
||||||
service_account: pikaoa-test-nc01-tekton-runner
|
service_account: pikaoa-dev-nc01-tekton-runner
|
||||||
pipeline_timeout: 20m0s
|
pipeline_timeout: 20m0s
|
||||||
workspace_pvc_size: 8Gi
|
workspace_pvc_size: 8Gi
|
||||||
image_repository: 127.0.0.1:5000/pikaoa/api
|
image_repository: 127.0.0.1:5000/pikaoa/api
|
||||||
@@ -276,16 +277,16 @@ repositories:
|
|||||||
registry_probe_base: http://127.0.0.1:5000
|
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_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_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_username: unidesk-admin
|
||||||
gitops_secret_name: pac-gitea-pikaoa-test-nc01
|
gitops_secret_name: pac-gitea-pikaoa-dev-nc01
|
||||||
gitops_manifest_path: deploy/gitops/nc01-test/resources.yaml
|
gitops_manifest_path: deploy/gitops/nc01-dev/resources.yaml
|
||||||
runtime_namespace: pikaoa-test
|
runtime_namespace: pikaoa-dev
|
||||||
runtime_deployment: pikaoa-api
|
runtime_deployment: pikaoa-api
|
||||||
runtime_service: pikaoa-web
|
runtime_service: pikaoa-web
|
||||||
runtime_service_port: "8080"
|
runtime_service_port: "8080"
|
||||||
health_path: /healthz
|
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:
|
consumers:
|
||||||
- extends: templates.consumers.agentrunV02
|
- extends: templates.consumers.agentrunV02
|
||||||
variables:
|
variables:
|
||||||
@@ -560,49 +561,49 @@ consumers:
|
|||||||
hostNetwork: true
|
hostNetwork: true
|
||||||
dnsPolicy: ClusterFirstWithHostNet
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
- id: pikaoa-test-nc01
|
- id: pikaoa-dev-nc01
|
||||||
repositoryRef: pikaoa-test-nc01
|
repositoryRef: pikaoa-dev-nc01
|
||||||
node: NC01
|
node: NC01
|
||||||
lane: pikaoa-test
|
lane: pikaoa-dev
|
||||||
namespace: pikaoa-ci
|
namespace: pikaoa-ci
|
||||||
pipeline: pikaoa-test-nc01-pac
|
pipeline: pikaoa-dev-nc01-pac
|
||||||
pipelineRunPrefix: pikaoa-test-nc01
|
pipelineRunPrefix: pikaoa-dev-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: pikaoa-test-nc01
|
argoApplication: pikaoa-dev-nc01
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
project: default
|
project: default
|
||||||
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
||||||
targetRevision: nc01-pikaoa-test-gitops
|
targetRevision: nc01-pikaoa-dev-gitops
|
||||||
path: deploy/gitops/nc01-test
|
path: deploy/gitops/nc01-dev
|
||||||
destinationNamespace: pikaoa-test
|
destinationNamespace: pikaoa-dev
|
||||||
automated: true
|
automated: true
|
||||||
repositoryCredential:
|
repositoryCredential:
|
||||||
secretName: argocd-repo-pikaoa-test-nc01
|
secretName: argocd-repo-pikaoa-dev-nc01
|
||||||
username: unidesk-admin
|
username: unidesk-admin
|
||||||
deliveryProvenance:
|
deliveryProvenance:
|
||||||
required: true
|
required: true
|
||||||
markerValue: admission-pac-v2:pikaoa-test-nc01
|
markerValue: admission-pac-v2:pikaoa-dev-nc01
|
||||||
executionServiceAccountName: pikaoa-test-nc01-tekton-runner
|
executionServiceAccountName: pikaoa-dev-nc01-tekton-runner
|
||||||
gitOps:
|
gitOps:
|
||||||
repoUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikainc-pikaoa.git
|
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:
|
runnerServiceAccount:
|
||||||
name: pikaoa-test-nc01-tekton-runner
|
name: pikaoa-dev-nc01-tekton-runner
|
||||||
automountServiceAccountToken: false
|
automountServiceAccountToken: false
|
||||||
roleBindingName: pikaoa-test-nc01-tekton-runner
|
roleBindingName: pikaoa-dev-nc01-tekton-runner
|
||||||
sourceArtifact:
|
sourceArtifact:
|
||||||
mode: embedded-pipeline-spec
|
mode: embedded-pipeline-spec
|
||||||
renderer: pikaoa-test-runtime
|
renderer: pikaoa-development-runtime
|
||||||
configRef: config/pikaoa.yaml#testRuntime.targets.NC01
|
configRef: config/pikaoa.yaml#developmentRuntime.targets.NC01
|
||||||
pipelineRunPath: .tekton/pikaoa-test-nc01-pac.yaml
|
pipelineRunPath: .tekton/pikaoa-dev-nc01-pac.yaml
|
||||||
maxKeepRuns: 8
|
maxKeepRuns: 8
|
||||||
taskRunTemplate:
|
taskRunTemplate:
|
||||||
hostNetwork: true
|
hostNetwork: true
|
||||||
dnsPolicy: ClusterFirstWithHostNet
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
- id: pikaoa-nc01
|
- id: pikaoa-nc01
|
||||||
repositoryRef: pikaoa-test-nc01
|
repositoryRef: pikaoa-dev-nc01
|
||||||
node: NC01
|
node: NC01
|
||||||
lane: pikaoa
|
lane: pikaoa
|
||||||
namespace: pikaoa-ci
|
namespace: pikaoa-ci
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ metadata:
|
|||||||
owner: platform-infra
|
owner: platform-infra
|
||||||
relatedIssues:
|
relatedIssues:
|
||||||
- 2181
|
- 2181
|
||||||
|
- 2274
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
targetId: NC01
|
targetId: NC01
|
||||||
@@ -37,7 +38,7 @@ targets:
|
|||||||
sites:
|
sites:
|
||||||
- id: pikaoa-development
|
- id: pikaoa-development
|
||||||
configRef: config/pikaoa.yaml
|
configRef: config/pikaoa.yaml
|
||||||
path: testRuntime.targets.NC01
|
path: developmentRuntime.targets.NC01
|
||||||
- id: pikaoa-production
|
- id: pikaoa-production
|
||||||
configRef: config/pikaoa.yaml
|
configRef: config/pikaoa.yaml
|
||||||
path: releaseRuntime.targets.NC01
|
path: releaseRuntime.targets.NC01
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ metadata:
|
|||||||
- 2110
|
- 2110
|
||||||
- 2128
|
- 2128
|
||||||
- 2263
|
- 2263
|
||||||
|
- 2274
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
root: /root/.unidesk/.state/secrets
|
root: /root/.unidesk/.state/secrets
|
||||||
@@ -116,7 +117,7 @@ sources:
|
|||||||
RESTIC_PASSWORD:
|
RESTIC_PASSWORD:
|
||||||
bytes: 32
|
bytes: 32
|
||||||
prefix: poa_restic_
|
prefix: poa_restic_
|
||||||
- sourceRef: platform-infra/pikaoa-test.env
|
- sourceRef: platform-infra/pikaoa-dev.env
|
||||||
type: env
|
type: env
|
||||||
requiredKeys:
|
requiredKeys:
|
||||||
- DATABASE_URL
|
- DATABASE_URL
|
||||||
@@ -266,10 +267,10 @@ targets:
|
|||||||
namespace: pikaoa
|
namespace: pikaoa
|
||||||
scope: pikaoa
|
scope: pikaoa
|
||||||
enabled: true
|
enabled: true
|
||||||
- id: pikaoa-test-nc01
|
- id: pikaoa-dev-nc01
|
||||||
route: NC01:k3s
|
route: NC01:k3s
|
||||||
namespace: pikaoa-test
|
namespace: pikaoa-dev
|
||||||
scope: pikaoa-test
|
scope: pikaoa-dev
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
kubernetesSecrets:
|
kubernetesSecrets:
|
||||||
@@ -301,18 +302,18 @@ kubernetesSecrets:
|
|||||||
- sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
- sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
||||||
sourceKey: contents
|
sourceKey: contents
|
||||||
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
||||||
- name: pikaoa-test-runtime
|
- name: pikaoa-dev-runtime
|
||||||
targetId: pikaoa-test-nc01
|
targetId: pikaoa-dev-nc01
|
||||||
secretName: pikaoa-test-runtime
|
secretName: pikaoa-dev-runtime
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
- sourceRef: platform-infra/pikaoa-test.env
|
- sourceRef: platform-infra/pikaoa-dev.env
|
||||||
sourceKey: DATABASE_URL
|
sourceKey: DATABASE_URL
|
||||||
targetKey: DATABASE_URL
|
targetKey: DATABASE_URL
|
||||||
- sourceRef: platform-infra/pikaoa-test.env
|
- sourceRef: platform-infra/pikaoa-dev.env
|
||||||
sourceKey: PIKAOA_SESSION_SECRET
|
sourceKey: PIKAOA_SESSION_SECRET
|
||||||
targetKey: PIKAOA_SESSION_SECRET
|
targetKey: PIKAOA_SESSION_SECRET
|
||||||
- sourceRef: platform-infra/pikaoa-test.env
|
- sourceRef: platform-infra/pikaoa-dev.env
|
||||||
sourceKey: OA_ADMIN_TOKEN
|
sourceKey: OA_ADMIN_TOKEN
|
||||||
targetKey: OA_ADMIN_TOKEN
|
targetKey: OA_ADMIN_TOKEN
|
||||||
- sourceRef: ~/.unidesk/.env/pikaoa-admin-password.txt
|
- 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-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-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.
|
// UniDesk CLI dispatcher with bounded server lifecycle and job drill-down output.
|
||||||
import { readConfig } from "./src/config";
|
import { readConfig } from "./src/config";
|
||||||
import { debugDispatch, debugEgressProxy, debugHealth, debugProviderRootfs, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
|
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") {
|
if (top === "pikaoa") {
|
||||||
const { runPikaoaCommand } = await import("./src/pikaoa-test-target");
|
if (args[1] !== "attachments-backup") {
|
||||||
const result = await runPikaoaCommand(readConfig(), args.slice(1));
|
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)) {
|
if (isRenderedCliResult(result)) {
|
||||||
emitText(result.renderedText, result.command || commandName);
|
emitText(result.renderedText, result.command || commandName);
|
||||||
if (!result.ok) process.exitCode = 1;
|
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-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-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.
|
// Static CLI help for job aliases and server lifecycle progressive disclosure.
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { ghHelp, ghScopedHelp } from "./gh";
|
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: "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: "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: "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: "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: "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." },
|
{ 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 === "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" && 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 === "platform-db") return platformDbHelp();
|
||||||
if (top === "secrets") return secretsHelp();
|
if (top === "secrets") return secretsHelp();
|
||||||
if (top === "web-probe") return loadHelp(async () => (await import("./web-probe")).webProbeHelp(), webProbeHelpSummary());
|
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 { tmpdir } from "node:os";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
import { renderPikaoaTestDesiredPipeline } from "./pikaoa-test-delivery-renderer";
|
import { renderPikaoaDesiredPipeline } from "./pikaoa-delivery-renderer";
|
||||||
|
|
||||||
function pacDocument(): Record<string, unknown> {
|
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>;
|
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 document = pacDocument();
|
||||||
const repositories = document.repositories as Array<Record<string, unknown>>;
|
const repositories = document.repositories as Array<Record<string, unknown>>;
|
||||||
const consumers = document.consumers 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) ?? {}),
|
...((consumer.params as Record<string, unknown> | undefined) ?? {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Parameters<typeof renderPikaoaTestDesiredPipeline>[0];
|
} as Parameters<typeof renderPikaoaDesiredPipeline>[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function sourceFixture(): string {
|
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 });
|
mkdirSync(resolve(root, "deploy"), { recursive: true });
|
||||||
for (const file of ["api.Dockerfile", "worker.Dockerfile", "web.Dockerfile"]) writeFileSync(resolve(root, "deploy", file), "FROM scratch\n");
|
for (const file of ["api.Dockerfile", "worker.Dockerfile", "web.Dockerfile"]) writeFileSync(resolve(root, "deploy", file), "FROM scratch\n");
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("PikaOA test PaC renderer", () => {
|
describe("PikaOA development PaC renderer", () => {
|
||||||
test("shares one PaC repository while isolating master and release consumers", () => {
|
test("shares one PaC repository while isolating master and release consumers", () => {
|
||||||
const document = pacDocument();
|
const document = pacDocument();
|
||||||
const repositories = document.repositories as Array<Record<string, unknown>>;
|
const repositories = document.repositories as Array<Record<string, unknown>>;
|
||||||
const consumers = document.consumers 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 repository = repositories.find((item) => item.id === "pikaoa-dev-nc01");
|
||||||
const testConsumer = consumers.find((item) => item.id === "pikaoa-test-nc01");
|
const developmentConsumer = consumers.find((item) => item.id === "pikaoa-dev-nc01");
|
||||||
const productionConsumer = consumers.find((item) => item.id === "pikaoa-nc01");
|
const productionConsumer = consumers.find((item) => item.id === "pikaoa-nc01");
|
||||||
const duplicateProductionRepository = repositories.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((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((developmentConsumer?.sourceArtifact as Record<string, unknown>).pipelineRunPath).toBe(".tekton/pikaoa-dev-nc01-pac.yaml");
|
||||||
expect((testConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-test-runtime");
|
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_branch).toBe("release");
|
||||||
expect((productionConsumer?.params as Record<string, unknown>).source_snapshot_prefix).toBe("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
|
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(duplicateProductionRepository).toBeUndefined();
|
||||||
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-release-runtime");
|
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");
|
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", () => {
|
test("renders release runtime from its isolated owning selector", () => {
|
||||||
const source = sourceFixture();
|
const source = sourceFixture();
|
||||||
try {
|
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");
|
expect(rendered.configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01");
|
||||||
const serialized = JSON.stringify(rendered.pipeline);
|
const serialized = JSON.stringify(rendered.pipeline);
|
||||||
expect(serialized).toContain("pikaoa-nc01-pac");
|
expect(serialized).toContain("pikaoa-nc01-pac");
|
||||||
expect(serialized).toContain("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
|
expect(serialized).toContain("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
|
||||||
expect(serialized).toContain("pac-gitea-pikaoa-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 {
|
} finally {
|
||||||
rmSync(source, { recursive: true, force: true });
|
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", () => {
|
test("renders parallel digest builds and a PreSync fresh database initializer", () => {
|
||||||
const source = sourceFixture();
|
const source = sourceFixture();
|
||||||
try {
|
try {
|
||||||
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding(), source);
|
const rendered = renderPikaoaDesiredPipeline(rendererBinding(), source);
|
||||||
const spec = rendered.pipeline.spec as Record<string, unknown>;
|
const spec = rendered.pipeline.spec as Record<string, unknown>;
|
||||||
const tasks = spec.tasks as Array<Record<string, unknown>>;
|
const tasks = spec.tasks as Array<Record<string, unknown>>;
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
@@ -97,6 +97,8 @@ describe("PikaOA test PaC renderer", () => {
|
|||||||
expect(buildScript).not.toContain("\u0001");
|
expect(buildScript).not.toContain("\u0001");
|
||||||
expect(buildScript).toContain("Docker-Content-Digest:");
|
expect(buildScript).toContain("Docker-Content-Digest:");
|
||||||
expect(buildScript).toContain("build_status=$?");
|
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('"code":"buildctl-nonzero-artifact-verified"');
|
||||||
expect(buildScript).toContain('"artifactVerified":true');
|
expect(buildScript).toContain('"artifactVerified":true');
|
||||||
}
|
}
|
||||||
@@ -137,11 +139,17 @@ describe("PikaOA test PaC renderer", () => {
|
|||||||
expect(manifest).toContain("name: pikaoa-init");
|
expect(manifest).toContain("name: pikaoa-init");
|
||||||
expect(manifest).toContain("name: initializer");
|
expect(manifest).toContain("name: initializer");
|
||||||
expect(manifest).not.toContain("kind: Secret");
|
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(/name: DATABASE_URL/gu)).toHaveLength(3);
|
||||||
expect(manifest.match(/key: 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(/name: OA_ADMIN_TOKEN/gu)).toHaveLength(1);
|
||||||
expect(manifest.match(/key: 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("image: __PIKAOA_API_IMAGE__");
|
||||||
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
|
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
|
||||||
expect(script).toContain('s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g');
|
expect(script).toContain('s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g');
|
||||||
+35
-29
@@ -1,17 +1,18 @@
|
|||||||
|
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
pikaoaTestRuntimeConfigRef,
|
pikaoaDevelopmentRuntimeConfigRef,
|
||||||
pikaoaReleaseRuntimeConfigRef,
|
pikaoaReleaseRuntimeConfigRef,
|
||||||
resolvePikaoaReleaseRuntimeTargetByConfigRef,
|
resolvePikaoaReleaseRuntimeTargetByConfigRef,
|
||||||
resolvePikaoaTestRuntimeTargetByConfigRef,
|
resolvePikaoaDevelopmentRuntimeTargetByConfigRef,
|
||||||
type TestTargetSpec,
|
type PikaoaRuntimeSpec,
|
||||||
} from "./pikaoa-test-target";
|
} from "./pikaoa-runtime";
|
||||||
import { renderPikaoaTestRuntimeManifest, type PikaoaTestRenderContext } from "./pikaoa-test-runtime-manifest";
|
import { renderPikaoaRuntimeManifest, type PikaoaRuntimeRenderContext } from "./pikaoa-runtime-manifest";
|
||||||
import { stableJsonSha256 } from "./stable-json";
|
import { stableJsonSha256 } from "./stable-json";
|
||||||
|
|
||||||
export interface PikaoaTestRendererBinding {
|
export interface PikaoaRendererBinding {
|
||||||
readonly consumer: {
|
readonly consumer: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly node: string;
|
readonly node: string;
|
||||||
@@ -31,20 +32,20 @@ export interface PikaoaTestRendererBinding {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PikaoaTestRenderedPipeline {
|
export interface PikaoaRenderedPipeline {
|
||||||
readonly pipeline: Record<string, unknown>;
|
readonly pipeline: Record<string, unknown>;
|
||||||
readonly configRef: string;
|
readonly configRef: string;
|
||||||
readonly effectiveConfigSha256: string;
|
readonly effectiveConfigSha256: string;
|
||||||
readonly sourceWorktreeRemote: 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 release = binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime";
|
||||||
const target = release
|
const target = release
|
||||||
? resolvePikaoaReleaseRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef)
|
? resolvePikaoaReleaseRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef)
|
||||||
: resolvePikaoaTestRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef);
|
: resolvePikaoaDevelopmentRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef);
|
||||||
const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaTestRuntimeConfigRef(target.id);
|
const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaDevelopmentRuntimeConfigRef(target.id);
|
||||||
if ((!release && binding.consumer.sourceArtifact.renderer !== "pikaoa-test-runtime") || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") {
|
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`);
|
throw new Error(`${binding.consumer.sourceArtifact.renderer} requires embedded-pipeline-spec`);
|
||||||
}
|
}
|
||||||
if (binding.consumer.sourceArtifact.configRef !== configRef) {
|
if (binding.consumer.sourceArtifact.configRef !== configRef) {
|
||||||
@@ -60,15 +61,15 @@ export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBindi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pikaoaTestSourceWorktreeRemote(targetId: string): string {
|
export function pikaoaDevelopmentSourceWorktreeRemote(targetId: string): string {
|
||||||
return resolvePikaoaTestRuntimeTargetByConfigRef(pikaoaTestRuntimeConfigRef(targetId)).source.worktreeRemote;
|
return resolvePikaoaDevelopmentRuntimeTargetByConfigRef(pikaoaDevelopmentRuntimeConfigRef(targetId)).source.worktreeRemote;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pikaoaReleaseSourceWorktreeRemote(targetId: string): string {
|
export function pikaoaReleaseSourceWorktreeRemote(targetId: string): string {
|
||||||
return resolvePikaoaReleaseRuntimeTargetByConfigRef(pikaoaReleaseRuntimeConfigRef(targetId)).source.worktreeRemote;
|
return resolvePikaoaReleaseRuntimeTargetByConfigRef(pikaoaReleaseRuntimeConfigRef(targetId)).source.worktreeRemote;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pipeline(target: TestTargetSpec): Record<string, unknown> {
|
function pipeline(target: PikaoaRuntimeSpec): Record<string, unknown> {
|
||||||
const runtimeManifest = renderGitOpsManifest(target);
|
const runtimeManifest = renderGitOpsManifest(target);
|
||||||
return {
|
return {
|
||||||
apiVersion: "tekton.dev/v1",
|
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 {
|
return {
|
||||||
name: "source",
|
name: "source",
|
||||||
taskSpec: {
|
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 taskName = `build-${component}`;
|
||||||
const imageRepository = target.build.images[component];
|
const imageRepository = target.build.images[component];
|
||||||
const dockerfile = target.build.dockerfiles[component];
|
const dockerfile = target.build.dockerfiles[component];
|
||||||
@@ -190,7 +191,10 @@ buildctl-daemonless.sh build \\
|
|||||||
--metadata-file "$metadata"
|
--metadata-file "$metadata"
|
||||||
build_status=$?
|
build_status=$?
|
||||||
set -e
|
set -e
|
||||||
metadata_compact=$(tr -d '\\n\\r' < "$metadata")
|
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)
|
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
|
if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
|
||||||
registry_host=${shellSingleQuote(imageRepository.split("/")[0] ?? "")}
|
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)
|
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}')
|
digest=$(printf '%s\\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^[[:space:]]*Docker-Content-Digest:/ {gsub(/\\r/, "", $2); print $2; exit}')
|
||||||
fi
|
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
|
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
|
printf '{"level":"warning","code":"buildctl-nonzero-artifact-verified","component":"${component}","buildctlExitCode":%s,"artifactVerified":true,"valuesPrinted":false}\\n' "$build_status" >&2
|
||||||
fi
|
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");
|
const manifestB64 = Buffer.from(runtimeManifest, "utf8").toString("base64");
|
||||||
return {
|
return {
|
||||||
name: "gitops-publish",
|
name: "gitops-publish",
|
||||||
@@ -257,7 +264,10 @@ api_image="$(params.api-image)"
|
|||||||
worker_image="$(params.worker-image)"
|
worker_image="$(params.worker-image)"
|
||||||
web_image="$(params.web-image)"
|
web_image="$(params.web-image)"
|
||||||
for image in "$api_image" "$worker_image" "$web_image"; do
|
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
|
done
|
||||||
rm -rf /workspace/gitops
|
rm -rf /workspace/gitops
|
||||||
mkdir -p /workspace/gitops
|
mkdir -p /workspace/gitops
|
||||||
@@ -320,25 +330,22 @@ printf '{"ok":true,"status":"published","phase":"gitops-publish","sourceCommitId
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGitOpsManifest(target: TestTargetSpec): string {
|
function renderGitOpsManifest(target: PikaoaRuntimeSpec): string {
|
||||||
const context: PikaoaTestRenderContext = {
|
const context: PikaoaRuntimeRenderContext = {
|
||||||
instanceId: "default",
|
instanceId: "default",
|
||||||
commit: "__PIKAOA_SOURCE_COMMIT__",
|
commit: "__PIKAOA_SOURCE_COMMIT__",
|
||||||
step: "all",
|
|
||||||
namespace: target.namespace,
|
namespace: target.namespace,
|
||||||
expiresAt: "gitops-managed",
|
|
||||||
apiImage: "__PIKAOA_API_IMAGE__",
|
apiImage: "__PIKAOA_API_IMAGE__",
|
||||||
workerImage: "__PIKAOA_WORKER_IMAGE__",
|
workerImage: "__PIKAOA_WORKER_IMAGE__",
|
||||||
webImage: "__PIKAOA_WEB_IMAGE__",
|
webImage: "__PIKAOA_WEB_IMAGE__",
|
||||||
initializerImage: "__PIKAOA_API_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";
|
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>> = {
|
const expected: Readonly<Record<string, string>> = {
|
||||||
id: target.source.branch === "release" ? "pikaoa-nc01" : "pikaoa-test-nc01",
|
|
||||||
node: target.node,
|
node: target.node,
|
||||||
lane: target.ci.lane,
|
lane: target.ci.lane,
|
||||||
namespace: target.ci.namespace,
|
namespace: target.ci.namespace,
|
||||||
@@ -349,7 +356,6 @@ function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBindin
|
|||||||
serviceAccount: target.ci.serviceAccountName,
|
serviceAccount: target.ci.serviceAccountName,
|
||||||
};
|
};
|
||||||
const actual: Readonly<Record<string, string | undefined>> = {
|
const actual: Readonly<Record<string, string | undefined>> = {
|
||||||
id: binding.consumer.id,
|
|
||||||
node: binding.consumer.node,
|
node: binding.consumer.node,
|
||||||
lane: binding.consumer.lane,
|
lane: binding.consumer.lane,
|
||||||
namespace: binding.consumer.namespace,
|
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)) {
|
for (const path of Object.values(target.build.dockerfiles)) {
|
||||||
if (!existsSync(resolve(sourceWorktree, path))) throw new Error(`PikaOA source worktree is missing ${path}`);
|
if (!existsSync(resolve(sourceWorktree, path))) throw new Error(`PikaOA source worktree is missing ${path}`);
|
||||||
}
|
}
|
||||||
+45
-103
@@ -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 PikaoaRuntimeRenderContext {
|
||||||
|
|
||||||
export interface PikaoaTestRenderContext {
|
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
commit: string;
|
commit: string;
|
||||||
step: PikaoaTestTargetStep;
|
|
||||||
namespace: string;
|
namespace: string;
|
||||||
expiresAt: string;
|
|
||||||
apiImage: string;
|
apiImage: string;
|
||||||
workerImage: string;
|
workerImage: string;
|
||||||
webImage: string;
|
webImage: string;
|
||||||
initializerImage: string;
|
initializerImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PikaoaTestSecretMaterial {
|
export interface PikaoaRuntimeManifestOptions {
|
||||||
databaseURL: string;
|
|
||||||
sessionSecret: string;
|
|
||||||
adminToken: string;
|
|
||||||
administratorPassword: string;
|
|
||||||
employeePassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PikaoaTestManifestOptions {
|
|
||||||
secretMode: "materialized" | "existing";
|
|
||||||
secrets?: PikaoaTestSecretMaterial;
|
|
||||||
initializerHook?: "argo-presync";
|
initializerHook?: "argo-presync";
|
||||||
}
|
}
|
||||||
|
|
||||||
const TARGET_LABEL = "pikaoa.unidesk.io/target";
|
const TARGET_LABEL = "pikaoa.unidesk.io/target";
|
||||||
const INSTANCE_LABEL = "pikaoa.unidesk.io/instance";
|
const INSTANCE_LABEL = "pikaoa.unidesk.io/instance";
|
||||||
|
|
||||||
export function renderPikaoaTestRuntimeManifest(
|
export function renderPikaoaRuntimeManifest(
|
||||||
target: TestTargetSpec,
|
target: PikaoaRuntimeSpec,
|
||||||
context: PikaoaTestRenderContext,
|
context: PikaoaRuntimeRenderContext,
|
||||||
options: PikaoaTestManifestOptions,
|
options: PikaoaRuntimeManifestOptions,
|
||||||
): Record<string, unknown>[] {
|
): 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 labels = ownershipLabels(target, context);
|
||||||
const apiSelector = componentSelector("api", context.instanceId);
|
const apiSelector = componentSelector("api", context.instanceId);
|
||||||
const workerSelector = componentSelector("worker", 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) },
|
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",
|
apiVersion: "v1", kind: "ConfigMap", metadata: { name: target.runtime.configMapName, namespace: context.namespace, labels },
|
||||||
stringData: runtimeSecretData(target, context, options.secrets!),
|
data: { "pikaoa.yaml": runtimeConfigData(target, context) },
|
||||||
}] : []),
|
},
|
||||||
{
|
{
|
||||||
apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: target.runtime.attachment.claimName, namespace: context.namespace, labels },
|
apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: target.runtime.attachment.claimName, namespace: context.namespace, labels },
|
||||||
spec: {
|
spec: {
|
||||||
@@ -103,7 +87,7 @@ export function renderPikaoaTestRuntimeManifest(
|
|||||||
],
|
],
|
||||||
volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }],
|
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>[] {
|
export function ownershipLabels(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): Record<string, string> {
|
||||||
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> {
|
|
||||||
const release = target.source.branch === "release";
|
const release = target.source.branch === "release";
|
||||||
return {
|
return {
|
||||||
"app.kubernetes.io/name": "pikaoa",
|
"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,
|
"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,
|
[TARGET_LABEL]: target.id,
|
||||||
[INSTANCE_LABEL]: context.instanceId,
|
[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 };
|
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 {
|
return {
|
||||||
"pikaoa.unidesk.io/source-repository": target.source.repository,
|
"pikaoa.unidesk.io/source-repository": target.source.repository,
|
||||||
"pikaoa.unidesk.io/source-commit": context.commit,
|
"pikaoa.unidesk.io/source-commit": context.commit,
|
||||||
"pikaoa.unidesk.io/expires-at": context.expiresAt,
|
"pikaoa.unidesk.io/delivery-managed": "true",
|
||||||
"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>",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function deployment(
|
function deployment(
|
||||||
target: TestTargetSpec,
|
target: PikaoaRuntimeSpec,
|
||||||
context: PikaoaTestRenderContext,
|
context: PikaoaRuntimeRenderContext,
|
||||||
component: "api" | "worker" | "web",
|
component: "api" | "worker" | "web",
|
||||||
image: string,
|
image: string,
|
||||||
selector: Record<string, string>,
|
selector: Record<string, string>,
|
||||||
@@ -215,7 +169,7 @@ function deployment(
|
|||||||
readinessProbe: { httpGet: { path: readinessPath, port }, periodSeconds: 3, failureThreshold: 20 },
|
readinessProbe: { httpGet: { path: readinessPath, port }, periodSeconds: 3, failureThreshold: 20 },
|
||||||
}],
|
}],
|
||||||
volumes: [
|
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 } }] : []),
|
...(component === "api" ? [{ name: target.runtime.attachment.claimName, persistentVolumeClaim: { claimName: target.runtime.attachment.claimName } }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -224,40 +178,28 @@ function deployment(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderContext, secrets: PikaoaTestSecretMaterial): Record<string, string> {
|
function runtimeConfigData(target: PikaoaRuntimeSpec, context: PikaoaRuntimeRenderContext): string {
|
||||||
const databaseURL = renderDatabaseURL(target, secrets.databaseURL);
|
return [
|
||||||
return {
|
"api:",
|
||||||
[target.database.connection.targetKey]: databaseURL,
|
` listen: 0.0.0.0:${target.runtime.apiPort}`,
|
||||||
[target.runtime.sessionSecret.targetKey]: secrets.sessionSecret,
|
"worker:",
|
||||||
[target.runtime.adminToken.targetKey]: secrets.adminToken,
|
` interval: ${target.runtime.workerInterval}`,
|
||||||
[target.runtime.administrator.password.targetKey]: secrets.administratorPassword,
|
` metricsListen: 0.0.0.0:${target.runtime.workerMetricsPort}`,
|
||||||
[target.runtime.employee.password.targetKey]: secrets.employeePassword,
|
" outbox:",
|
||||||
"pikaoa.yaml": [
|
` batchSize: ${target.runtime.outboxBatchSize}`,
|
||||||
"api:",
|
` maxAttempts: ${target.runtime.outboxMaxAttempts}`,
|
||||||
` listen: 0.0.0.0:${target.runtime.apiPort}`,
|
` retryDelay: ${target.runtime.outboxRetryDelay}`,
|
||||||
"worker:",
|
"database:",
|
||||||
` interval: ${target.runtime.workerInterval}`,
|
' url: ""',
|
||||||
` metricsListen: 0.0.0.0:${target.runtime.workerMetricsPort}`,
|
"identity:",
|
||||||
" outbox:",
|
` sessionTTL: ${target.runtime.sessionTTL}`,
|
||||||
` batchSize: ${target.runtime.outboxBatchSize}`,
|
"attachment:",
|
||||||
` maxAttempts: ${target.runtime.outboxMaxAttempts}`,
|
` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`,
|
||||||
` retryDelay: ${target.runtime.outboxRetryDelay}`,
|
` maxBytes: ${target.runtime.attachment.maxBytes}`,
|
||||||
"database:",
|
"observability:",
|
||||||
` url: ${JSON.stringify(databaseURL)}`,
|
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-dev-${context.instanceId}`}`,
|
||||||
"identity:",
|
` otlpEndpoint: ${JSON.stringify(target.observability.otlpEndpoint)}`,
|
||||||
` sessionTTL: ${target.runtime.sessionTTL}`,
|
`shutdownTimeout: ${target.runtime.shutdownTimeout}`,
|
||||||
"attachment:",
|
"",
|
||||||
` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`,
|
].join("\n");
|
||||||
` maxBytes: ${target.runtime.attachment.maxBytes}`,
|
|
||||||
"observability:",
|
|
||||||
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-test-${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;
|
|
||||||
}
|
}
|
||||||
@@ -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", () => {
|
test("keeps PikaOA master and release authorities on distinct Secret keys", () => {
|
||||||
const config = readGiteaConfig();
|
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 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;
|
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(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).toBe("github-token-pikainc-pikaoa-release");
|
||||||
expect(productionKey).not.toBe(testKey);
|
expect(productionKey).not.toBe(developmentKey);
|
||||||
expect(githubTokenEnvNameForSecretKey(productionKey ?? ""))
|
expect(githubTokenEnvNameForSecretKey(productionKey ?? ""))
|
||||||
.not.toBe(githubTokenEnvNameForSecretKey(testKey ?? ""));
|
.not.toBe(githubTokenEnvNameForSecretKey(developmentKey ?? ""));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("normalizes Secret keys to bounded environment variable names", () => {
|
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",
|
toState: "started",
|
||||||
}]);
|
}]);
|
||||||
expect(contract.child.enabled).toBe(false);
|
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"));
|
const items = documents(renderPacAdmissionDesiredFragment("NC01"));
|
||||||
expect(items.map((item) => item.kind)).toEqual(["ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
|
expect(items.map((item) => item.kind)).toEqual(["ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
|
||||||
const policy = items[0];
|
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");
|
expect(() => parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-production-nc01" })).toThrow("namespace");
|
||||||
|
|
||||||
const invalidRepositoryDocument = pacConfigDocument();
|
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");
|
if (invalidRepository === undefined) throw new Error("pikaoa repository fixture is required");
|
||||||
invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
|
invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
|
||||||
const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
|
const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
|
||||||
expect(isolated.validationWarnings).toContainEqual({
|
expect(isolated.validationWarnings).toContainEqual({
|
||||||
code: "pac-unselected-repository-invalid",
|
code: "pac-unselected-repository-invalid",
|
||||||
object: { kind: "repository", id: "pikaoa-test-nc01" },
|
object: { kind: "repository", id: "pikaoa-dev-nc01" },
|
||||||
consumer: null,
|
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,
|
blocking: false,
|
||||||
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
|
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");
|
expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl");
|
||||||
|
|
||||||
const duplicateSelectedRepository = pacConfigDocument();
|
const duplicateSelectedRepository = pacConfigDocument();
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ function runtimeInput(commit: string | null = sourceCommit): Record<string, unkn
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("PaC source artifact CLI contract", () => {
|
describe("PaC source artifact CLI contract", () => {
|
||||||
test("accepts PikaOA test runtime provenance", () => {
|
test("accepts PikaOA development runtime provenance", () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
configRef: "config/pikaoa.yaml#testRuntime.targets.NC01",
|
configRef: "config/pikaoa.yaml#developmentRuntime.targets.NC01",
|
||||||
effectiveConfigSha256: `sha256:${"a".repeat(64)}`,
|
effectiveConfigSha256: `sha256:${"a".repeat(64)}`,
|
||||||
renderer: "pikaoa-test-runtime",
|
renderer: "pikaoa-development-runtime",
|
||||||
mode: "embedded-pipeline-spec",
|
mode: "embedded-pipeline-spec",
|
||||||
} as const;
|
} as const;
|
||||||
const manifest = {
|
const manifest = {
|
||||||
@@ -113,9 +113,9 @@ describe("PaC source artifact CLI contract", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("uses PikaOA-specific PipelineRun labels", () => {
|
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({
|
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",
|
"app.kubernetes.io/part-of": "pikaoa",
|
||||||
"unidesk.ai/source-commit": "{{ revision }}",
|
"unidesk.ai/source-commit": "{{ revision }}",
|
||||||
"pikaoa.unidesk.io/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", () => {
|
test("maps the PikaOA shared workspace to the declared RWO PVC size", () => {
|
||||||
const binding = {
|
const binding = {
|
||||||
consumer: { sourceArtifact: { renderer: "pikaoa-test-runtime" } },
|
consumer: { sourceArtifact: { renderer: "pikaoa-development-runtime" } },
|
||||||
repository: { params: { workspace_pvc_size: "8Gi" } },
|
repository: { params: { workspace_pvc_size: "8Gi" } },
|
||||||
} as Parameters<typeof pipelineRunWorkspaces>[0];
|
} as Parameters<typeof pipelineRunWorkspaces>[0];
|
||||||
expect(pipelineRunWorkspaces(binding, { workspaces: [{ name: "workspace" }] })).toEqual([{
|
expect(pipelineRunWorkspaces(binding, { workspaces: [{ name: "workspace" }] })).toEqual([{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
@@ -19,10 +20,10 @@ import { stableJsonSha256, stableJsonValue } from "./stable-json";
|
|||||||
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
|
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
|
||||||
import { renderSub2RankDesiredPipeline, sub2RankOwningSourceRemote } from "./platform-infra-sub2rank-pipeline";
|
import { renderSub2RankDesiredPipeline, sub2RankOwningSourceRemote } from "./platform-infra-sub2rank-pipeline";
|
||||||
import { renderSelfMediaDesiredPipeline, selfMediaSourceWorktreeRemote } from "./selfmedia-delivery-renderer";
|
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 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 PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
|
||||||
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
|
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
|
||||||
|
|
||||||
@@ -502,7 +503,7 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
|
|||||||
? renderSub2RankDesiredPipeline(binding)
|
? renderSub2RankDesiredPipeline(binding)
|
||||||
: sourceArtifact.renderer === "selfmedia-runtime"
|
: sourceArtifact.renderer === "selfmedia-runtime"
|
||||||
? renderSelfMediaPipeline(binding, sourceWorktree)
|
? renderSelfMediaPipeline(binding, sourceWorktree)
|
||||||
: renderPikaoaTestPipeline(binding, sourceWorktree);
|
: renderPikaoaPipeline(binding, sourceWorktree);
|
||||||
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
|
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
|
||||||
? rendered.pipeline
|
? rendered.pipeline
|
||||||
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
|
: 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 } {
|
function renderPikaoaPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
|
||||||
const rendered = renderPikaoaTestDesiredPipeline(binding, sourceWorktree);
|
const rendered = renderPikaoaDesiredPipeline(binding, sourceWorktree);
|
||||||
return {
|
return {
|
||||||
pipeline: rendered.pipeline,
|
pipeline: rendered.pipeline,
|
||||||
provenance: {
|
provenance: {
|
||||||
@@ -675,7 +676,7 @@ function embeddedPipelineRun(binding: PacSourceArtifactBinding, desiredSpec: Rec
|
|||||||
? "sub2rank"
|
? "sub2rank"
|
||||||
: provenance.renderer === "selfmedia-runtime"
|
: provenance.renderer === "selfmedia-runtime"
|
||||||
? "selfmedia"
|
? "selfmedia"
|
||||||
: provenance.renderer === "pikaoa-test-runtime"
|
: provenance.renderer === "pikaoa-development-runtime" || provenance.renderer === "pikaoa-release-runtime"
|
||||||
? "pikaoa"
|
? "pikaoa"
|
||||||
: "agentrun",
|
: "agentrun",
|
||||||
),
|
),
|
||||||
@@ -857,7 +858,7 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
|
|||||||
}
|
}
|
||||||
if (name === "git-url") return { name, value: "{{ repo_url }}" };
|
if (name === "git-url") return { name, value: "{{ repo_url }}" };
|
||||||
const repositoryParam = name.replaceAll("-", "_");
|
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 };
|
return { name, value: param.default };
|
||||||
}
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) {
|
if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) {
|
||||||
@@ -875,7 +876,7 @@ export function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desired
|
|||||||
if (desiredSpec.workspaces === undefined) return [];
|
if (desiredSpec.workspaces === undefined) return [];
|
||||||
return arrayRecords(desiredSpec.workspaces, "Pipeline.spec.workspaces").map((workspace) => {
|
return arrayRecords(desiredSpec.workspaces, "Pipeline.spec.workspaces").map((workspace) => {
|
||||||
const name = requiredString(workspace.name, "Pipeline.spec.workspaces[].name");
|
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 {
|
return {
|
||||||
name,
|
name,
|
||||||
volumeClaimTemplate: {
|
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 === "sub2rank-platform-service") return sub2RankOwningSourceRemote();
|
||||||
if (binding.consumer.sourceArtifact.renderer === "selfmedia-runtime") return selfMediaSourceWorktreeRemote(binding.consumer.node);
|
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 (binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") return pikaoaReleaseSourceWorktreeRemote(binding.consumer.node);
|
||||||
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
|
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
|
||||||
return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl;
|
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 !== "hwlab-runtime-lane"
|
||||||
&& provenance.renderer !== "sub2rank-platform-service"
|
&& provenance.renderer !== "sub2rank-platform-service"
|
||||||
&& provenance.renderer !== "selfmedia-runtime"
|
&& provenance.renderer !== "selfmedia-runtime"
|
||||||
&& provenance.renderer !== "pikaoa-test-runtime"
|
&& provenance.renderer !== "pikaoa-development-runtime"
|
||||||
&& provenance.renderer !== "pikaoa-release-runtime") return null;
|
&& provenance.renderer !== "pikaoa-release-runtime") return null;
|
||||||
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
|
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
|
||||||
return provenance as PacSourceArtifactProvenance;
|
return provenance as PacSourceArtifactProvenance;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPEC: PJ2026-03 PikaOA v0.8.
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
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 {
|
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 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 pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`);
|
||||||
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
|
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
|
||||||
const taskRunTemplate = y.objectField(value, "taskRunTemplate", path);
|
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 === "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 === "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 === "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`);
|
if (renderer === "pikaoa-release-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-release-runtime requires embedded-pipeline-spec`);
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
@@ -1221,7 +1222,7 @@ function validateConsumerConfig(
|
|||||||
if (consumer.sourceArtifact?.renderer === "sub2rank-platform-service" && consumer.runnerServiceAccount === null) {
|
if (consumer.sourceArtifact?.renderer === "sub2rank-platform-service" && consumer.runnerServiceAccount === null) {
|
||||||
throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for sub2rank-platform-service`);
|
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.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) {
|
if (consumer.argoBootstrap?.repositoryCredential === null || consumer.argoBootstrap?.repositoryCredential === undefined) {
|
||||||
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private repository`);
|
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private repository`);
|
||||||
|
|||||||
Reference in New Issue
Block a user