Merge pull request #2252 from pikasTech/feat/2250-pikaoa-nc01-host
PikaOA迁出PK01并切换NC01数据库
This commit is contained in:
@@ -73,7 +73,7 @@ testRuntime:
|
||||
name: PikaOA Test Fixture
|
||||
email: pikaoa-test-fixture@unidesk.local
|
||||
database:
|
||||
configRef: config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
||||
name: pikaoa_test
|
||||
username: pikaoa_test
|
||||
schema: pikaoa
|
||||
@@ -82,6 +82,15 @@ testRuntime:
|
||||
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
|
||||
|
||||
+7
-11
@@ -94,7 +94,7 @@ testRuntime:
|
||||
name: PikaOA Test NC01 CI
|
||||
email: pikaoa-test-nc01-ci@unidesk.local
|
||||
database:
|
||||
configRef: config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url
|
||||
name: pikaoa_test
|
||||
username: pikaoa_test
|
||||
schema: pikaoa
|
||||
@@ -253,7 +253,7 @@ releaseRuntime:
|
||||
name: PikaOA NC01 CI
|
||||
email: pikaoa-nc01-ci@unidesk.local
|
||||
database:
|
||||
configRef: config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-database-url
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-database-url
|
||||
name: pikaoa
|
||||
username: pikaoa
|
||||
schema: pikaoa
|
||||
@@ -435,7 +435,7 @@ delivery:
|
||||
configRef: config/secrets-distribution.yaml
|
||||
declarationId: pikaoa-runtime
|
||||
database:
|
||||
configRef: config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-database-url
|
||||
configRef: config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-database-url
|
||||
observability:
|
||||
configRef: config/platform-infra/observability.yaml
|
||||
otlpEndpoint: otel-collector.platform-infra.svc.cluster.local:4317
|
||||
@@ -459,12 +459,10 @@ delivery:
|
||||
excludes:
|
||||
- .tmp
|
||||
destination:
|
||||
type: sftp
|
||||
route: PK01
|
||||
host: 10.0.8.3
|
||||
port: 22
|
||||
username: pikaoa-restic
|
||||
directory: /var/backups/unidesk/pikaoa/attachments-restic
|
||||
type: local-directory
|
||||
route: NC01
|
||||
path: /var/backups/unidesk/pikaoa/attachments-restic
|
||||
mountPath: /repository
|
||||
secret:
|
||||
configRef: config/secrets-distribution.yaml
|
||||
declarationId: pikaoa-attachments-backup
|
||||
@@ -472,8 +470,6 @@ delivery:
|
||||
secretName: pikaoa-attachments-backup
|
||||
keys:
|
||||
password: RESTIC_PASSWORD
|
||||
privateKey: SFTP_PRIVATE_KEY
|
||||
knownHosts: SFTP_KNOWN_HOSTS
|
||||
retention:
|
||||
keepDaily: 14
|
||||
keepWeekly: 8
|
||||
|
||||
@@ -7,6 +7,7 @@ metadata:
|
||||
owner: unidesk
|
||||
relatedIssues:
|
||||
- 1875
|
||||
- 2250
|
||||
|
||||
cluster:
|
||||
role: primary
|
||||
@@ -119,6 +120,16 @@ postgres:
|
||||
user: web_probe_monitor
|
||||
address: 10.42.0.0/16
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa
|
||||
user: pikaoa
|
||||
address: 10.42.0.0/16
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa_test
|
||||
user: pikaoa_test
|
||||
address: 10.42.0.0/16
|
||||
method: scram-sha-256
|
||||
|
||||
secrets:
|
||||
source: master-local
|
||||
@@ -166,6 +177,34 @@ secrets:
|
||||
WEB_PROBE_MONITOR_NC01_DB_NAME: web_probe_monitor
|
||||
randomHex:
|
||||
WEB_PROBE_MONITOR_NC01_DB_PASSWORD: 32
|
||||
- name: pikaoa-nc01-db-credentials
|
||||
sourceRef: platform-db/pikaoa-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- PIKAOA_DB_USER
|
||||
- PIKAOA_DB_PASSWORD
|
||||
- PIKAOA_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
PIKAOA_DB_USER: pikaoa
|
||||
PIKAOA_DB_NAME: pikaoa
|
||||
randomHex:
|
||||
PIKAOA_DB_PASSWORD: 32
|
||||
- name: pikaoa-test-nc01-db-credentials
|
||||
sourceRef: platform-db/pikaoa-test-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- PIKAOA_TEST_DB_USER
|
||||
- PIKAOA_TEST_DB_PASSWORD
|
||||
- PIKAOA_TEST_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
PIKAOA_TEST_DB_USER: pikaoa_test
|
||||
PIKAOA_TEST_DB_NAME: pikaoa_test
|
||||
randomHex:
|
||||
PIKAOA_TEST_DB_PASSWORD: 32
|
||||
|
||||
objects:
|
||||
roles:
|
||||
@@ -196,6 +235,24 @@ objects:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: pikaoa
|
||||
passwordRef:
|
||||
sourceRef: platform-db/pikaoa-db.env
|
||||
key: PIKAOA_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: pikaoa_test
|
||||
passwordRef:
|
||||
sourceRef: platform-db/pikaoa-test-db.env
|
||||
key: PIKAOA_TEST_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
databases:
|
||||
- name: agentrun_v02
|
||||
owner: agentrun_v02
|
||||
@@ -212,6 +269,16 @@ objects:
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: pikaoa
|
||||
owner: pikaoa
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: pikaoa_test
|
||||
owner: pikaoa_test
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
|
||||
managedOperations:
|
||||
monitorReset:
|
||||
@@ -267,15 +334,68 @@ exports:
|
||||
- scope: hwlab-web-probe-monitor
|
||||
secret: hwlab-web-probe-monitor-db
|
||||
key: DATABASE_URL
|
||||
- name: pikaoa-database-url
|
||||
sourceSecretRef: platform-db/pikaoa-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(PIKAOA_DB_USER):$(PIKAOA_DB_PASSWORD)@$(PGHOST):5432/$(PIKAOA_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 10.42.0.1
|
||||
writeToSecretSource:
|
||||
sourceRef: platform-infra/pikaoa.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: pikaoa
|
||||
secret: pikaoa-runtime
|
||||
key: DATABASE_URL
|
||||
- name: pikaoa-test-database-url
|
||||
sourceSecretRef: platform-db/pikaoa-test-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(PIKAOA_TEST_DB_USER):$(PIKAOA_TEST_DB_PASSWORD)@$(PGHOST):5432/$(PIKAOA_TEST_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 10.42.0.1
|
||||
writeToSecretSource:
|
||||
sourceRef: platform-infra/pikaoa-test.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: pikaoa-test
|
||||
secret: pikaoa-test-runtime
|
||||
key: DATABASE_URL
|
||||
|
||||
backup:
|
||||
logicalDump:
|
||||
enabled: false
|
||||
schedule: "*-*-* 03:37:00"
|
||||
database: agentrun_v02
|
||||
retentionDays: 7
|
||||
destination:
|
||||
type: local-directory
|
||||
path: /var/backups/unidesk/platform-db/nc01/agentrun-v02
|
||||
encryption:
|
||||
enabled: false
|
||||
logicalDumps:
|
||||
- enabled: false
|
||||
database: agentrun_v02
|
||||
scriptPath: /usr/local/sbin/unidesk-nc01-agentrun-v02-pgdump.sh
|
||||
serviceName: unidesk-nc01-agentrun-v02-pgdump.service
|
||||
timerName: unidesk-nc01-agentrun-v02-pgdump.timer
|
||||
schedule: "*-*-* 03:37:00"
|
||||
retentionDays: 7
|
||||
warningAfterHours: 36
|
||||
destination:
|
||||
type: nc01-local
|
||||
path: /var/backups/unidesk/platform-db/nc01/agentrun-v02
|
||||
pgDump:
|
||||
host: 127.0.0.1
|
||||
format: custom
|
||||
encryption:
|
||||
enabled: false
|
||||
- enabled: true
|
||||
database: pikaoa
|
||||
scriptPath: /usr/local/sbin/unidesk-nc01-pikaoa-pgdump.sh
|
||||
serviceName: unidesk-nc01-pikaoa-pgdump.service
|
||||
timerName: unidesk-nc01-pikaoa-pgdump.timer
|
||||
schedule: "*-*-* 03:47:00"
|
||||
retentionDays: 14
|
||||
warningAfterHours: 36
|
||||
destination:
|
||||
type: nc01-local
|
||||
path: /var/backups/unidesk/platform-db/nc01/pikaoa
|
||||
pgDump:
|
||||
host: 127.0.0.1
|
||||
format: custom
|
||||
encryption:
|
||||
enabled: false
|
||||
|
||||
@@ -361,31 +361,6 @@ postgres:
|
||||
user: hwlab_jd01_v03_app
|
||||
address: 74.48.78.17/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa
|
||||
user: pikaoa
|
||||
address: 10.0.8.0/22
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa
|
||||
user: pikaoa
|
||||
address: 74.48.78.17/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa
|
||||
user: pikaoa
|
||||
address: 152.53.229.148/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa_test
|
||||
user: pikaoa_test
|
||||
address: 10.0.8.0/22
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: pikaoa_test
|
||||
user: pikaoa_test
|
||||
address: 152.53.229.148/32
|
||||
method: scram-sha-256
|
||||
|
||||
secrets:
|
||||
source: master-local
|
||||
@@ -489,34 +464,6 @@ secrets:
|
||||
HWLAB_JD01_V03_DB_NAME: hwlab_jd01_v03
|
||||
randomHex:
|
||||
HWLAB_JD01_V03_DB_PASSWORD: 32
|
||||
- name: pikaoa-db-credentials
|
||||
sourceRef: platform-db/pikaoa-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- PIKAOA_DB_USER
|
||||
- PIKAOA_DB_PASSWORD
|
||||
- PIKAOA_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
PIKAOA_DB_USER: pikaoa
|
||||
PIKAOA_DB_NAME: pikaoa
|
||||
randomHex:
|
||||
PIKAOA_DB_PASSWORD: 32
|
||||
- name: pikaoa-test-db-credentials
|
||||
sourceRef: platform-db/pikaoa-test-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- PIKAOA_TEST_DB_USER
|
||||
- PIKAOA_TEST_DB_PASSWORD
|
||||
- PIKAOA_TEST_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
PIKAOA_TEST_DB_USER: pikaoa_test
|
||||
PIKAOA_TEST_DB_NAME: pikaoa_test
|
||||
randomHex:
|
||||
PIKAOA_TEST_DB_PASSWORD: 32
|
||||
objects:
|
||||
roles:
|
||||
- name: sub2api
|
||||
@@ -582,24 +529,6 @@ objects:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: pikaoa
|
||||
passwordRef:
|
||||
sourceRef: platform-db/pikaoa-db.env
|
||||
key: PIKAOA_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: pikaoa_test
|
||||
passwordRef:
|
||||
sourceRef: platform-db/pikaoa-test-db.env
|
||||
key: PIKAOA_TEST_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
databases:
|
||||
- name: sub2api
|
||||
owner: sub2api
|
||||
@@ -636,16 +565,6 @@ objects:
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: pikaoa
|
||||
owner: pikaoa
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: pikaoa_test
|
||||
owner: pikaoa_test
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
|
||||
exports:
|
||||
connectionStrings:
|
||||
@@ -788,38 +707,6 @@ exports:
|
||||
- scope: hwlab-jd01-v03
|
||||
secret: hwlab-v03-openfga
|
||||
key: DATASTORE_URI
|
||||
- name: pikaoa-database-url
|
||||
sourceSecretRef: platform-db/pikaoa-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(PIKAOA_DB_USER):$(PIKAOA_DB_PASSWORD)@$(PGHOST):$(PGPORT)/$(PIKAOA_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 82.156.23.220
|
||||
PGPORT: "5432"
|
||||
writeToSecretSource:
|
||||
sourceRef: platform-infra/pikaoa.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: pikaoa
|
||||
secret: pikaoa-runtime
|
||||
key: DATABASE_URL
|
||||
- name: pikaoa-test-database-url
|
||||
sourceSecretRef: platform-db/pikaoa-test-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(PIKAOA_TEST_DB_USER):$(PIKAOA_TEST_DB_PASSWORD)@$(PGHOST):$(PGPORT)/$(PIKAOA_TEST_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 82.156.23.220
|
||||
PGPORT: "5432"
|
||||
writeToSecretSource:
|
||||
sourceRef: platform-infra/pikaoa-test.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: pikaoa-test
|
||||
secret: pikaoa-test-runtime
|
||||
key: DATABASE_URL
|
||||
|
||||
backup:
|
||||
phase: minimum-restoreable
|
||||
@@ -841,23 +728,6 @@ backup:
|
||||
encryption:
|
||||
enabled: false
|
||||
futureKeyRef: platform-db/backup-age-recipient.txt
|
||||
- enabled: true
|
||||
database: pikaoa
|
||||
scriptPath: /usr/local/sbin/unidesk-pk01-pikaoa-pgdump.sh
|
||||
serviceName: unidesk-pk01-pikaoa-pgdump.service
|
||||
timerName: unidesk-pk01-pikaoa-pgdump.timer
|
||||
schedule: "*-*-* 03:37:00"
|
||||
retentionDays: 14
|
||||
warningAfterHours: 36
|
||||
destination:
|
||||
type: pk01-local
|
||||
path: /var/backups/unidesk/platform-db/pk01/pikaoa
|
||||
pgDump:
|
||||
host: 127.0.0.1
|
||||
format: custom
|
||||
encryption:
|
||||
enabled: false
|
||||
futureKeyRef: platform-db/backup-age-recipient.txt
|
||||
physicalWalArchive:
|
||||
enabled: false
|
||||
futureTool: pgbackrest
|
||||
@@ -889,12 +759,6 @@ observability:
|
||||
- kind: psql-app-role
|
||||
database: hwlab_d601_v03
|
||||
user: hwlab_d601_v03_app
|
||||
- kind: psql-app-role
|
||||
database: pikaoa
|
||||
user: pikaoa
|
||||
- kind: psql-app-role
|
||||
database: pikaoa_test
|
||||
user: pikaoa_test
|
||||
- kind: disk-free
|
||||
path: /var/lib/postgresql/16/main
|
||||
minFreeGiB: 10
|
||||
|
||||
@@ -106,7 +106,11 @@ sources:
|
||||
requiredKeys:
|
||||
- RESTIC_PASSWORD
|
||||
createIfMissing:
|
||||
enabled: false
|
||||
enabled: true
|
||||
randomBase64Url:
|
||||
RESTIC_PASSWORD:
|
||||
bytes: 32
|
||||
prefix: poa_restic_
|
||||
- sourceRef: platform-infra/pikaoa-test.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
@@ -151,16 +155,6 @@ sources:
|
||||
createIfMissing:
|
||||
enabled: false
|
||||
externalFiles:
|
||||
- sourceRef: ~/.unidesk/.ssh/pikaoa-attachments-restic
|
||||
type: raw-file
|
||||
required: true
|
||||
createIfMissing:
|
||||
enabled: false
|
||||
- sourceRef: ~/.unidesk/.ssh/pikaoa-attachments-known_hosts
|
||||
type: raw-file
|
||||
required: true
|
||||
createIfMissing:
|
||||
enabled: false
|
||||
- sourceRef: ~/.unidesk/.env/pikaoa-admin-password.txt
|
||||
type: raw-file
|
||||
required: true
|
||||
@@ -278,12 +272,6 @@ kubernetesSecrets:
|
||||
- sourceRef: platform-infra/pikaoa-attachments-backup.env
|
||||
sourceKey: RESTIC_PASSWORD
|
||||
targetKey: RESTIC_PASSWORD
|
||||
- sourceRef: ~/.unidesk/.ssh/pikaoa-attachments-restic
|
||||
sourceKey: contents
|
||||
targetKey: SFTP_PRIVATE_KEY
|
||||
- sourceRef: ~/.unidesk/.ssh/pikaoa-attachments-known_hosts
|
||||
sourceKey: contents
|
||||
targetKey: SFTP_KNOWN_HOSTS
|
||||
- name: pikaoa-runtime
|
||||
targetId: pikaoa-nc01
|
||||
secretName: pikaoa-runtime
|
||||
@@ -301,9 +289,6 @@ kubernetesSecrets:
|
||||
- sourceRef: ~/.unidesk/.env/pikaoa-employee-password.txt
|
||||
sourceKey: contents
|
||||
targetKey: PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD
|
||||
- sourceRef: platform-infra/pk01-frp.env
|
||||
sourceKey: FRP_TOKEN
|
||||
targetKey: FRP_TOKEN
|
||||
- name: pikaoa-test-runtime
|
||||
targetId: pikaoa-test-nc01
|
||||
secretName: pikaoa-test-runtime
|
||||
|
||||
@@ -25,15 +25,18 @@
|
||||
|
||||
## 数据库边界
|
||||
|
||||
- 测试数据库固定使用 PK01 host PostgreSQL。
|
||||
- `config/platform-db/postgres-pk01.yaml` 声明独立的:
|
||||
- 生产与测试数据库固定使用 NC01 host PostgreSQL,不创建 namespace-local PostgreSQL。
|
||||
- `config/platform-db/postgres-nc01.yaml` 声明独立的:
|
||||
- 生产 role/database:`pikaoa`;
|
||||
- role:`pikaoa_test`;
|
||||
- database:`pikaoa_test`;
|
||||
- schema:`pikaoa`;
|
||||
- `DATABASE_URL` export:`platform-infra/pikaoa-test.env`。
|
||||
- 生产 `DATABASE_URL` export:`platform-infra/pikaoa.env`;
|
||||
- 测试 `DATABASE_URL` export:`platform-infra/pikaoa-test.env`。
|
||||
- `pikaoa-test` namespace 不创建 PostgreSQL StatefulSet、Service 或临时数据库。
|
||||
- 数据库生命周期独立于 namespace 和业务 workload。
|
||||
- 测试数据清理由 PK01 PostgreSQL 受控入口负责,不依赖删除 namespace。
|
||||
- 迁移采用全新 role/database,不复制旧数据。
|
||||
- 数据库准备、Secret export 与后续状态查询只走 `platform-db postgres` 受控入口。
|
||||
|
||||
## Secret 初始化
|
||||
|
||||
@@ -66,7 +69,7 @@
|
||||
|
||||
## 初始化顺序
|
||||
|
||||
1. 使用 `platform-db postgres` 受控入口准备 PK01 `pikaoa_test` role/database,并 export `DATABASE_URL`。
|
||||
1. 使用 `platform-db postgres` 受控入口和 `config/platform-db/postgres-nc01.yaml` 准备 `pikaoa_test` role/database,并 export `DATABASE_URL`。
|
||||
2. 执行 foundation:
|
||||
|
||||
```bash
|
||||
@@ -139,7 +142,14 @@
|
||||
- API、Worker、Web 并行构建并以 digest 发布到生产 GitOps branch;
|
||||
- Argo Application `pikaoa-nc01` 自动同步 `pikaoa` namespace;
|
||||
- 禁止人工补建 PipelineRun 代替 source merge。
|
||||
- 生产使用 PK01 host PostgreSQL 独立 `pikaoa` database/role,不使用 namespace-local PostgreSQL。
|
||||
- 生产使用 NC01 host PostgreSQL 独立 `pikaoa` database/role,不使用 namespace-local PostgreSQL。
|
||||
- 生产库逻辑备份由 `config/platform-db/postgres-nc01.yaml#backup.logicalDumps` 声明,写入 NC01 host 本地目录。
|
||||
- 生产附件主存储保持 NC01 k3s PVC,独立备份由 `config/pikaoa.yaml#delivery.targets.NC01.attachmentsBackup` 声明:
|
||||
- source PVC 只读挂载;
|
||||
- Restic repository 使用 NC01 hostPath `/var/backups/unidesk/pikaoa/attachments-restic`;
|
||||
- `backup`、`check`、`forget/prune` 与 `restore-smoke` 均由 YAML 渲染;
|
||||
- `restore-smoke` 只恢复到临时 `emptyDir`;
|
||||
- Secret 只包含 `RESTIC_PASSWORD`,不需要 SFTP key 或 known_hosts。
|
||||
- PikaOA 公网 exposure 由 `config/pikaoa.yaml` 的产品 target 唯一声明:
|
||||
- `testRuntime.targets.NC01.exposure` 拥有 `oa-dev.hwpod.com` 与 NodePort `32080`;
|
||||
- `releaseRuntime.targets.NC01.exposure` 拥有 `oa.hwpod.com` 与 NodePort `32096`;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { readPikaoaAttachmentsBackupSpec, renderPikaoaAttachmentsBackupManifest, renderPikaoaAttachmentsInitJob, renderPikaoaAttachmentsRestoreSmokeJob, runPikaoaAttachmentsBackupCommand } from "./pikaoa-attachments-backup";
|
||||
import { projectPikaoaAttachmentsRepositoryPrerequisite, readPikaoaAttachmentsBackupSpec, renderPikaoaAttachmentsBackupManifest, renderPikaoaAttachmentsInitJob, renderPikaoaAttachmentsRestoreSmokeJob, runPikaoaAttachmentsBackupCommand } from "./pikaoa-attachments-backup";
|
||||
import { runSecretsCommand } from "./secrets";
|
||||
|
||||
describe("PikaOA attachments backup", () => {
|
||||
test("owning YAML renders separate read-only backup and maintenance CronJobs", () => {
|
||||
@@ -9,23 +12,95 @@ describe("PikaOA attachments backup", () => {
|
||||
|
||||
expect(spec.schedule).toBe("17 4 * * *");
|
||||
expect(spec.source.excludes).toContain(".tmp");
|
||||
expect(spec.destination.directory).toBe("/var/backups/unidesk/pikaoa/attachments-restic");
|
||||
expect(spec.destination).toMatchObject({
|
||||
type: "local-directory",
|
||||
route: "NC01",
|
||||
path: "/var/backups/unidesk/pikaoa/attachments-restic",
|
||||
mountPath: "/repository",
|
||||
});
|
||||
expect(manifest).toContain("name: pikaoa-attachments-backup");
|
||||
expect(manifest).toContain("name: pikaoa-attachments-restic-check");
|
||||
expect(manifest).toContain("name: pikaoa-attachments-restic-forget-prune");
|
||||
expect(manifest).toContain("readOnly: true");
|
||||
expect(manifest).toContain("hostPath:");
|
||||
expect(manifest).toContain("path: /var/backups/unidesk/pikaoa/attachments-restic");
|
||||
expect(manifest).toContain("type: DirectoryOrCreate");
|
||||
expect(manifest).toContain("mountPath: /repository");
|
||||
expect(manifest).toContain("--exclude=.tmp");
|
||||
expect(manifest).toContain("forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune");
|
||||
expect(manifest).not.toContain("RESTIC_PASSWORD=");
|
||||
expect(manifest).not.toContain("RESTIC_SFTP_COMMAND");
|
||||
expect(manifest).not.toContain("SFTP_PRIVATE_KEY");
|
||||
expect(manifest.split("---\n").map((document) => Bun.YAML.parse(document))).toHaveLength(4);
|
||||
});
|
||||
|
||||
test("local directory is created by Kubernetes while missing SFTP directories remain blocking", () => {
|
||||
expect(projectPikaoaAttachmentsRepositoryPrerequisite("local-directory", false)).toEqual({
|
||||
exists: false,
|
||||
disposition: "will-create",
|
||||
blocking: false,
|
||||
});
|
||||
expect(projectPikaoaAttachmentsRepositoryPrerequisite("sftp", false)).toEqual({
|
||||
exists: false,
|
||||
disposition: "waiting",
|
||||
blocking: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("missing RESTIC_PASSWORD is pending generation instead of blocking secrets plan", async () => {
|
||||
const fixtureRoot = mkdtempSync(join(process.cwd(), ".pikaoa-attachments-secret-plan-"));
|
||||
try {
|
||||
const sourceConfig = Bun.YAML.parse(readFileSync("config/secrets-distribution.yaml", "utf8")) as {
|
||||
version: unknown;
|
||||
kind: unknown;
|
||||
metadata: unknown;
|
||||
sources: { files: Array<Record<string, unknown>> };
|
||||
targets: Array<Record<string, unknown>>;
|
||||
kubernetesSecrets: Array<Record<string, unknown>>;
|
||||
};
|
||||
const source = sourceConfig.sources.files.find((item: Record<string, unknown>) => item.sourceRef === "platform-infra/pikaoa-attachments-backup.env");
|
||||
const target = sourceConfig.targets.find((item: Record<string, unknown>) => item.id === "pikaoa-nc01");
|
||||
const secret = sourceConfig.kubernetesSecrets.find((item: Record<string, unknown>) => item.name === "pikaoa-attachments-backup");
|
||||
const fixtureConfig = {
|
||||
version: sourceConfig.version,
|
||||
kind: sourceConfig.kind,
|
||||
metadata: sourceConfig.metadata,
|
||||
sources: { root: join(fixtureRoot, "secrets"), files: [source], externalFiles: [] },
|
||||
targets: [target],
|
||||
kubernetesSecrets: [secret],
|
||||
};
|
||||
const fixturePath = join(fixtureRoot, "secrets-distribution.yaml");
|
||||
writeFileSync(fixturePath, Bun.YAML.stringify(fixtureConfig));
|
||||
|
||||
const planned = await runSecretsCommand({} as UniDeskConfig, ["plan", "--config", relative(process.cwd(), fixturePath), "--scope", "pikaoa", "--target", "pikaoa-nc01"]);
|
||||
const localSources = planned.localSources as { entries: Array<Record<string, unknown>> };
|
||||
const desiredSecrets = planned.desiredSecrets as Array<Record<string, unknown>>;
|
||||
|
||||
expect(planned.ok).toBeTrue();
|
||||
expect(localSources.entries[0]).toMatchObject({
|
||||
action: "create",
|
||||
generatedKeys: ["RESTIC_PASSWORD"],
|
||||
unmaterializedGeneratedKeys: ["RESTIC_PASSWORD"],
|
||||
missingKeys: [],
|
||||
valuesPrinted: false,
|
||||
});
|
||||
expect(desiredSecrets[0]).toMatchObject({
|
||||
missingKeys: [],
|
||||
pendingGeneratedKeys: ["RESTIC_PASSWORD"],
|
||||
valuesPrinted: false,
|
||||
});
|
||||
} finally {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("restore smoke only uses a temporary emptyDir", () => {
|
||||
const spec = readPikaoaAttachmentsBackupSpec("config/pikaoa.yaml");
|
||||
const manifest = renderPikaoaAttachmentsRestoreSmokeJob(spec, "pikaoa-attachments-restore-smoke-test");
|
||||
|
||||
expect(manifest).toContain("--target /restore");
|
||||
expect(manifest).toContain("emptyDir:");
|
||||
expect(manifest).toContain("mountPath: /repository");
|
||||
expect(manifest).not.toContain(`claimName: ${spec.source.claimName}`);
|
||||
expect(manifest).not.toContain(`mountPath: ${spec.source.mountPath}`);
|
||||
expect(Bun.YAML.parse(manifest)).toBeTruthy();
|
||||
@@ -39,7 +114,8 @@ describe("PikaOA attachments backup", () => {
|
||||
expect(manifest).toContain('if [ "$cat_status" -eq 0 ]');
|
||||
expect(manifest).toContain("result=already-initialized mutation=false");
|
||||
expect(manifest).toContain('if [ "$cat_status" -ne 10 ]');
|
||||
expect(manifest).toContain('restic -o sftp.command="$RESTIC_SFTP_COMMAND" init');
|
||||
expect(manifest).toContain("restic init");
|
||||
expect(manifest).toContain("mountPath: /repository");
|
||||
expect(manifest).not.toContain("persistentVolumeClaim:");
|
||||
expect(manifest).not.toContain(`claimName: ${spec.source.claimName}`);
|
||||
expect(manifest).not.toContain(`mountPath: ${spec.source.mountPath}`);
|
||||
|
||||
@@ -12,21 +12,35 @@ interface Options { action: Action; configPath: string; targetId: string | null;
|
||||
export interface PikaoaAttachmentsBackupSpec {
|
||||
targetId: string; route: string; namespace: string; enabled: boolean; nonBlocking: true; image: string; schedule: string;
|
||||
source: { claimName: string; mountPath: string; readOnly: true; excludes: string[] };
|
||||
destination: { type: "sftp"; route: string; host: string; port: number; username: string; directory: string };
|
||||
secret: { configRef: string; declarationId: string; targetId: string; secretName: string; keys: { password: string; privateKey: string; knownHosts: string } };
|
||||
destination:
|
||||
| { type: "sftp"; route: string; host: string; port: number; username: string; directory: string }
|
||||
| { type: "local-directory"; route: string; path: string; mountPath: string };
|
||||
secret: { configRef: string; declarationId: string; targetId: string; secretName: string; keys: { password: string; privateKey?: string; knownHosts?: string } };
|
||||
retention: { keepDaily: number; keepWeekly: number; keepMonthly: number };
|
||||
maintenance: { checkSchedule: string; forgetPruneSchedule: string };
|
||||
restoreSmoke: { jobPrefix: string; emptyDirSizeLimit: string; ttlSecondsAfterFinished: number };
|
||||
jobs: { initJobPrefix: string; backupCronJobName: string; checkCronJobName: string; forgetPruneCronJobName: string; serviceAccountName: string; fieldManager: string };
|
||||
}
|
||||
interface Prerequisites { namespace: boolean; sourcePvc: boolean; secret: boolean; destinationDirectory: boolean; ready: boolean; waiting: string[] }
|
||||
interface Prerequisites {
|
||||
namespace: boolean;
|
||||
sourcePvc: boolean;
|
||||
secret: boolean;
|
||||
repositoryDirectory: PikaoaAttachmentsRepositoryPrerequisite;
|
||||
ready: boolean;
|
||||
waiting: string[];
|
||||
}
|
||||
export interface PikaoaAttachmentsRepositoryPrerequisite {
|
||||
exists: boolean;
|
||||
disposition: "ready" | "will-create" | "waiting";
|
||||
blocking: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG_PATH = rootPath("config", "pikaoa.yaml");
|
||||
|
||||
export function pikaoaAttachmentsBackupHelp(): Record<string, unknown> {
|
||||
return {
|
||||
command: "pikaoa attachments-backup",
|
||||
description: "管理 PikaOA 生产附件的 YAML-first、只读 Restic SFTP 离机备份。",
|
||||
description: "管理 PikaOA 生产附件的 YAML-first、只读 Restic 备份。",
|
||||
usage: [
|
||||
"bun scripts/cli.ts pikaoa attachments-backup plan [--target id] [--output json]",
|
||||
"bun scripts/cli.ts pikaoa attachments-backup status [--target id] [--output json]",
|
||||
@@ -41,6 +55,7 @@ export function pikaoaAttachmentsBackupHelp(): Record<string, unknown> {
|
||||
"日常备份、check 与 forget/prune 使用独立 CronJob。",
|
||||
"restore-smoke 只恢复到临时 emptyDir,绝不挂载生产 PVC。",
|
||||
"Secret 只经 YAML sourceRef/targetKey 分发,输出不打印值。",
|
||||
"repository 可由 YAML 选择本机 hostPath 目录或兼容 SFTP 目标。",
|
||||
"init 仅在 restic cat config 精确报告 repository-not-found 时创建仓库;已初始化时结构化 no-op,认证或网络错误保持失败。",
|
||||
"前置条件未就绪时返回 prerequisite-waiting,且不阻塞业务、PaC 或 Argo。",
|
||||
],
|
||||
@@ -76,6 +91,15 @@ export function readPikaoaAttachmentsBackupSpec(configPath = DEFAULT_CONFIG_PATH
|
||||
const maintenance = object(backup.maintenance, "attachmentsBackup.maintenance");
|
||||
const smoke = object(backup.restoreSmoke, "attachmentsBackup.restoreSmoke");
|
||||
const jobs = object(backup.jobs, "attachmentsBackup.jobs");
|
||||
const destinationType = backupDestinationType(destination.type);
|
||||
const parsedDestination: PikaoaAttachmentsBackupSpec["destination"] = destinationType === "local-directory"
|
||||
? { type: destinationType, route: text(destination.route, "destination.route"), path: path(destination.path, "destination.path"), mountPath: path(destination.mountPath, "destination.mountPath") }
|
||||
: { type: destinationType, route: text(destination.route, "destination.route"), host: text(destination.host, "destination.host"), port: integer(destination.port, "destination.port"), username: name(destination.username, "destination.username"), directory: path(destination.directory, "destination.directory") };
|
||||
const parsedKeys: PikaoaAttachmentsBackupSpec["secret"]["keys"] = { password: env(keys.password, "keys.password") };
|
||||
if (destinationType === "sftp") {
|
||||
parsedKeys.privateKey = env(keys.privateKey, "keys.privateKey");
|
||||
parsedKeys.knownHosts = env(keys.knownHosts, "keys.knownHosts");
|
||||
}
|
||||
const spec: PikaoaAttachmentsBackupSpec = {
|
||||
targetId: selectedId,
|
||||
route: text(target.route, "target.route"),
|
||||
@@ -85,8 +109,8 @@ export function readPikaoaAttachmentsBackupSpec(configPath = DEFAULT_CONFIG_PATH
|
||||
image: text(backup.image, "image"),
|
||||
schedule: cron(backup.schedule, "schedule"),
|
||||
source: { claimName: dns(source.claimName, "source.claimName"), mountPath: path(source.mountPath, "source.mountPath"), readOnly: yes(source.readOnly, "source.readOnly"), excludes: texts(source.excludes, "source.excludes") },
|
||||
destination: { type: sftp(destination.type), route: text(destination.route, "destination.route"), host: text(destination.host, "destination.host"), port: integer(destination.port, "destination.port"), username: name(destination.username, "destination.username"), directory: path(destination.directory, "destination.directory") },
|
||||
secret: { configRef: text(secret.configRef, "secret.configRef"), declarationId: name(secret.declarationId, "secret.declarationId"), targetId: name(secret.targetId, "secret.targetId"), secretName: dns(secret.secretName, "secret.secretName"), keys: { password: env(keys.password, "keys.password"), privateKey: env(keys.privateKey, "keys.privateKey"), knownHosts: env(keys.knownHosts, "keys.knownHosts") } },
|
||||
destination: parsedDestination,
|
||||
secret: { configRef: text(secret.configRef, "secret.configRef"), declarationId: name(secret.declarationId, "secret.declarationId"), targetId: name(secret.targetId, "secret.targetId"), secretName: dns(secret.secretName, "secret.secretName"), keys: parsedKeys },
|
||||
retention: { keepDaily: integer(retention.keepDaily, "retention.keepDaily"), keepWeekly: integer(retention.keepWeekly, "retention.keepWeekly"), keepMonthly: integer(retention.keepMonthly, "retention.keepMonthly") },
|
||||
maintenance: { checkSchedule: cron(maintenance.checkSchedule, "maintenance.checkSchedule"), forgetPruneSchedule: cron(maintenance.forgetPruneSchedule, "maintenance.forgetPruneSchedule") },
|
||||
restoreSmoke: { jobPrefix: dns(smoke.jobPrefix, "restoreSmoke.jobPrefix"), emptyDirSizeLimit: text(smoke.emptyDirSizeLimit, "restoreSmoke.emptyDirSizeLimit"), ttlSecondsAfterFinished: integer(smoke.ttlSecondsAfterFinished, "restoreSmoke.ttlSecondsAfterFinished") },
|
||||
@@ -126,7 +150,7 @@ spec:
|
||||
args:
|
||||
- |
|
||||
${indentBlock(resticPrelude(spec), 14)}
|
||||
restic -o sftp.command="$RESTIC_SFTP_COMMAND" restore latest --target /restore
|
||||
${resticInvocation(spec, "restore latest --target /restore")}
|
||||
objects="$(find /restore -mindepth 1 -type f | wc -l | tr -d ' ')"
|
||||
printf 'event=pikaoa.attachments.restore-smoke objects=%s target=temporary-empty-dir\\n' "$objects"
|
||||
volumeMounts:
|
||||
@@ -135,6 +159,7 @@ ${indentBlock(resticPrelude(spec), 14)}
|
||||
readOnly: true
|
||||
- name: restore-target
|
||||
mountPath: /restore
|
||||
${repositoryVolumeMount(spec, 12)}
|
||||
volumes:
|
||||
- name: backup-secret
|
||||
secret:
|
||||
@@ -143,6 +168,7 @@ ${indentBlock(resticPrelude(spec), 14)}
|
||||
- name: restore-target
|
||||
emptyDir:
|
||||
sizeLimit: ${spec.restoreSmoke.emptyDirSizeLimit}
|
||||
${repositoryVolume(spec, 8)}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -173,7 +199,7 @@ spec:
|
||||
- |
|
||||
${indentBlock(resticPrelude(spec), 14)}
|
||||
set +e
|
||||
cat_output="$(restic -o sftp.command="$RESTIC_SFTP_COMMAND" cat config 2>&1)"
|
||||
cat_output="$(${resticInvocation(spec, "cat config")} 2>&1)"
|
||||
cat_status="$?"
|
||||
set -e
|
||||
if [ "$cat_status" -eq 0 ]; then
|
||||
@@ -185,17 +211,19 @@ ${indentBlock(resticPrelude(spec), 14)}
|
||||
printf '%s\\n' "$cat_output" >&2
|
||||
exit "$cat_status"
|
||||
fi
|
||||
restic -o sftp.command="$RESTIC_SFTP_COMMAND" init
|
||||
${resticInvocation(spec, "init")}
|
||||
printf 'event=pikaoa.attachments.repository-init result=initialized mutation=true\\n'
|
||||
volumeMounts:
|
||||
- name: backup-secret
|
||||
mountPath: /backup-secret
|
||||
readOnly: true
|
||||
${repositoryVolumeMount(spec, 12)}
|
||||
volumes:
|
||||
- name: backup-secret
|
||||
secret:
|
||||
secretName: ${spec.secret.secretName}
|
||||
defaultMode: 0400
|
||||
${repositoryVolume(spec, 8)}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -203,12 +231,12 @@ function planPayload(spec: PikaoaAttachmentsBackupSpec): Record<string, unknown>
|
||||
const manifest = renderPikaoaAttachmentsBackupManifest(spec);
|
||||
return {
|
||||
ok: true, action: "pikaoa-attachments-backup-plan", mode: "local-render", mutation: false,
|
||||
target: targetSummary(spec), schedule: { backup: spec.schedule, databaseReference: "03:37", orderedAfterDatabase: Number(spec.schedule.split(/\s+/u)[1]) > 3 },
|
||||
target: targetSummary(spec), schedule: { backup: spec.schedule, check: spec.maintenance.checkSchedule, forgetPrune: spec.maintenance.forgetPruneSchedule },
|
||||
retention: spec.retention, source: spec.source,
|
||||
destination: { ...spec.destination, repositoryCredentialsPrinted: false }, secret: secretSummary(spec), maintenance: spec.maintenance,
|
||||
manifests: { objects: 4, fingerprint: sha256Fingerprint(manifest), yaml: manifest },
|
||||
policy: { nonBlocking: true, sourcePvcReadOnly: true, restoreTarget: "temporary-emptyDir", valuesPrinted: false },
|
||||
prerequisites: ["production namespace", "production attachments PVC", "YAML-first Secret sync", "least-privilege PK01 SFTP directory"], next: next(spec),
|
||||
requirements: ["production namespace", "production attachments PVC", "YAML-first Secret sync", "YAML-declared repository directory"], next: next(spec),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,13 +297,20 @@ async function inspectPrerequisites(config: UniDeskConfig, spec: PikaoaAttachmen
|
||||
const namespace = await remoteExists(config, spec.route, ["kubectl", "get", "namespace", spec.namespace, "-o", "name"]);
|
||||
const sourcePvc = namespace && await remoteExists(config, spec.route, ["kubectl", "-n", spec.namespace, "get", "pvc", spec.source.claimName, "-o", "name"]);
|
||||
const secret = namespace && await remoteExists(config, spec.route, ["kubectl", "-n", spec.namespace, "get", "secret", spec.secret.secretName, "-o", "name"]);
|
||||
const destinationDirectory = await remoteExists(config, spec.destination.route, ["sh"], `test -d ${shQuote(spec.destination.directory)}\n`);
|
||||
const destinationDirectory = await remoteExists(config, spec.destination.route, ["sh"], `test -d ${shQuote(destinationPath(spec))}\n`);
|
||||
const repositoryDirectory = projectPikaoaAttachmentsRepositoryPrerequisite(spec.destination.type, destinationDirectory);
|
||||
const waiting: string[] = [];
|
||||
if (!namespace) waiting.push("production-namespace");
|
||||
if (!sourcePvc) waiting.push("production-attachments-pvc");
|
||||
if (!secret) waiting.push("yaml-first-secret-sync");
|
||||
if (!destinationDirectory) waiting.push("pk01-sftp-directory");
|
||||
return { namespace, sourcePvc, secret, destinationDirectory, ready: waiting.length === 0, waiting };
|
||||
if (repositoryDirectory.blocking) waiting.push("repository-directory");
|
||||
return { namespace, sourcePvc, secret, repositoryDirectory, ready: waiting.length === 0, waiting };
|
||||
}
|
||||
|
||||
export function projectPikaoaAttachmentsRepositoryPrerequisite(type: PikaoaAttachmentsBackupSpec["destination"]["type"], exists: boolean): PikaoaAttachmentsRepositoryPrerequisite {
|
||||
if (exists) return { exists: true, disposition: "ready", blocking: false };
|
||||
if (type === "local-directory") return { exists: false, disposition: "will-create", blocking: false };
|
||||
return { exists: false, disposition: "waiting", blocking: true };
|
||||
}
|
||||
|
||||
async function remoteExists(config: UniDeskConfig, route: string, args: string[], stdin = ""): Promise<boolean> {
|
||||
@@ -327,30 +362,51 @@ ${indentBlock(resticPrelude(spec), 18)}
|
||||
- name: backup-secret
|
||||
mountPath: /backup-secret
|
||||
readOnly: true
|
||||
${attachmentMount} volumes:
|
||||
${repositoryVolumeMount(spec, 16)}${attachmentMount} volumes:
|
||||
- name: backup-secret
|
||||
secret:
|
||||
secretName: ${spec.secret.secretName}
|
||||
defaultMode: 0400
|
||||
${attachmentVolume}`;
|
||||
${repositoryVolume(spec, 12)}${attachmentVolume}`;
|
||||
}
|
||||
|
||||
function resticPrelude(spec: PikaoaAttachmentsBackupSpec): string {
|
||||
if (spec.destination.type === "local-directory") {
|
||||
return `export RESTIC_REPOSITORY=${shQuote(spec.destination.mountPath)}\nexport RESTIC_PASSWORD_FILE=/backup-secret/${spec.secret.keys.password}`;
|
||||
}
|
||||
const repository = `sftp:${spec.destination.username}@${spec.destination.host}:${spec.destination.directory}`;
|
||||
const sshCommand = `ssh -p ${spec.destination.port} -i /backup-secret/${spec.secret.keys.privateKey} -o UserKnownHostsFile=/backup-secret/${spec.secret.keys.knownHosts} -o StrictHostKeyChecking=yes`;
|
||||
const sshCommand = `ssh -p ${spec.destination.port} -i /backup-secret/${spec.secret.keys.privateKey!} -o UserKnownHostsFile=/backup-secret/${spec.secret.keys.knownHosts!} -o StrictHostKeyChecking=yes`;
|
||||
return `export RESTIC_REPOSITORY=${shQuote(repository)}\nexport RESTIC_PASSWORD_FILE=/backup-secret/${spec.secret.keys.password}\nexport RESTIC_SFTP_COMMAND=${shQuote(sshCommand)}`;
|
||||
}
|
||||
|
||||
function resticInvocation(spec: PikaoaAttachmentsBackupSpec, args: string): string {
|
||||
return spec.destination.type === "sftp" ? `restic -o sftp.command="$RESTIC_SFTP_COMMAND" ${args}` : `restic ${args}`;
|
||||
}
|
||||
|
||||
function destinationPath(spec: PikaoaAttachmentsBackupSpec): string {
|
||||
return spec.destination.type === "sftp" ? spec.destination.directory : spec.destination.path;
|
||||
}
|
||||
|
||||
function repositoryVolumeMount(spec: PikaoaAttachmentsBackupSpec, spaces: number): string {
|
||||
if (spec.destination.type !== "local-directory") return "";
|
||||
return `${" ".repeat(spaces)}- name: repository\n${" ".repeat(spaces + 2)}mountPath: ${spec.destination.mountPath}\n`;
|
||||
}
|
||||
|
||||
function repositoryVolume(spec: PikaoaAttachmentsBackupSpec, spaces: number): string {
|
||||
if (spec.destination.type !== "local-directory") return "";
|
||||
return `${" ".repeat(spaces)}- name: repository\n${" ".repeat(spaces + 2)}hostPath:\n${" ".repeat(spaces + 4)}path: ${spec.destination.path}\n${" ".repeat(spaces + 4)}type: DirectoryOrCreate\n`;
|
||||
}
|
||||
|
||||
function indentBlock(value: string, spaces: number): string {
|
||||
const indentation = " ".repeat(spaces);
|
||||
return value.split("\n").map((line) => `${indentation}${line}`).join("\n");
|
||||
}
|
||||
|
||||
function resticCommand(spec: PikaoaAttachmentsBackupSpec, operation: "backup" | "check" | "forget-prune"): string {
|
||||
if (operation === "check") return "restic -o sftp.command=\"$RESTIC_SFTP_COMMAND\" check --read-data-subset=5%";
|
||||
if (operation === "forget-prune") return `restic -o sftp.command="$RESTIC_SFTP_COMMAND" forget --keep-daily ${spec.retention.keepDaily} --keep-weekly ${spec.retention.keepWeekly} --keep-monthly ${spec.retention.keepMonthly} --prune`;
|
||||
if (operation === "check") return resticInvocation(spec, "check --read-data-subset=5%");
|
||||
if (operation === "forget-prune") return resticInvocation(spec, `forget --keep-daily ${spec.retention.keepDaily} --keep-weekly ${spec.retention.keepWeekly} --keep-monthly ${spec.retention.keepMonthly} --prune`);
|
||||
const excludes = spec.source.excludes.flatMap((value) => [`--exclude=${value}`, `--exclude=${spec.source.mountPath}/${value}`]).map(shQuote).join(" ");
|
||||
return `restic -o sftp.command="$RESTIC_SFTP_COMMAND" backup ${shQuote(spec.source.mountPath)} ${excludes} --tag pikaoa-attachments`;
|
||||
return resticInvocation(spec, `backup ${shQuote(spec.source.mountPath)} ${excludes} --tag pikaoa-attachments`);
|
||||
}
|
||||
|
||||
function waiting(action: string, prerequisites: Prerequisites, spec: PikaoaAttachmentsBackupSpec): Record<string, unknown> {
|
||||
@@ -358,7 +414,7 @@ function waiting(action: string, prerequisites: Prerequisites, spec: PikaoaAttac
|
||||
}
|
||||
|
||||
function targetSummary(spec: PikaoaAttachmentsBackupSpec): Record<string, unknown> {
|
||||
return { id: spec.targetId, route: spec.route, namespace: spec.namespace, enabled: spec.enabled, nonBlocking: spec.nonBlocking };
|
||||
return { id: spec.targetId, route: spec.route, namespace: spec.namespace, enabled: spec.enabled, nonBlocking: spec.nonBlocking, repositoryType: spec.destination.type };
|
||||
}
|
||||
|
||||
function secretSummary(spec: PikaoaAttachmentsBackupSpec): Record<string, unknown> {
|
||||
@@ -430,4 +486,4 @@ function name(value: unknown, label: string): string { const parsed = text(value
|
||||
function dns(value: unknown, label: string): string { const parsed = text(value, label); if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(parsed) || parsed.length > 63) throw new Error(`${label} must be a DNS label`); return parsed }
|
||||
function env(value: unknown, label: string): string { const parsed = text(value, label); if (!/^[A-Z_][A-Z0-9_]*$/u.test(parsed)) throw new Error(`${label} must be an environment key`); return parsed }
|
||||
function cron(value: unknown, label: string): string { const parsed = text(value, label); if (parsed.trim().split(/\s+/u).length !== 5) throw new Error(`${label} must use five-field cron syntax`); return parsed }
|
||||
function sftp(value: unknown): "sftp" { if (value !== "sftp") throw new Error("destination.type must be sftp"); return "sftp" }
|
||||
function backupDestinationType(value: unknown): "sftp" | "local-directory" { if (value !== "sftp" && value !== "local-directory") throw new Error("destination.type must be sftp or local-directory"); return value }
|
||||
|
||||
@@ -138,6 +138,8 @@ describe("PikaOA test PaC renderer", () => {
|
||||
expect(manifest).toContain("name: initializer");
|
||||
expect(manifest).not.toContain("kind: Secret");
|
||||
expect(manifest).toContain("secretName: pikaoa-test-runtime");
|
||||
expect(manifest.match(/name: DATABASE_URL/gu)).toHaveLength(3);
|
||||
expect(manifest.match(/key: DATABASE_URL/gu)).toHaveLength(3);
|
||||
expect(manifest).toContain("image: __PIKAOA_API_IMAGE__");
|
||||
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
|
||||
expect(script).toContain('s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g');
|
||||
|
||||
@@ -96,7 +96,10 @@ export function renderPikaoaTestRuntimeManifest(
|
||||
name: "initializer", image: context.initializerImage, imagePullPolicy: target.source.pullPolicy,
|
||||
command: target.initializer.command,
|
||||
resources: target.runtime.resources.initializer,
|
||||
env: [{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" }],
|
||||
env: [
|
||||
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
|
||||
{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.database.connection.targetKey } } },
|
||||
],
|
||||
volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }],
|
||||
}],
|
||||
volumes: [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
|
||||
@@ -106,6 +109,7 @@ export function renderPikaoaTestRuntimeManifest(
|
||||
},
|
||||
deployment(target, context, "api", context.apiImage, apiSelector, target.runtime.apiPort, prometheusPodLabels, commonPodAnnotations(target.observability.apiMetricsPath, target.runtime.apiPort), [
|
||||
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
|
||||
{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.database.connection.targetKey } } },
|
||||
{ name: "PIKAOA_SESSION_SECRET", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.sessionSecret.targetKey } } },
|
||||
{ name: "PIKAOA_BOOTSTRAP_ADMIN_USERNAME", value: target.runtime.administrator.username },
|
||||
{ name: "PIKAOA_BOOTSTRAP_ADMIN_PASSWORD", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.administrator.password.targetKey } } },
|
||||
@@ -114,6 +118,7 @@ export function renderPikaoaTestRuntimeManifest(
|
||||
], target.probes.apiLivenessPath, target.probes.apiReadinessPath),
|
||||
deployment(target, context, "worker", context.workerImage, workerSelector, target.runtime.workerMetricsPort, prometheusPodLabels, commonPodAnnotations(target.observability.workerMetricsPath, target.runtime.workerMetricsPort), [
|
||||
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
|
||||
{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.database.connection.targetKey } } },
|
||||
], target.probes.workerLivenessPath, target.probes.workerReadinessPath),
|
||||
deployment(target, context, "web", context.webImage, webSelector, target.runtime.webPort, {}, {}, [
|
||||
{ name: target.runtime.webApiUpstreamEnv, value: `http://${target.runtime.apiServiceName}.${context.namespace}.svc.cluster.local:${target.runtime.apiPort}` },
|
||||
|
||||
@@ -380,7 +380,7 @@ test("validation-only fixture never invokes remote capture", async () => {
|
||||
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-pk01.yaml#exports.connectionStrings.pikaoa-test-database-url",
|
||||
configRef: "config/platform-db/postgres-nc01.yaml#exports.connectionStrings.pikaoa-test-database-url",
|
||||
name: "pikaoa_test",
|
||||
username: "pikaoa_test",
|
||||
schema: "pikaoa",
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { projectLogicalDumps } from "./platform-db-postgres-backups";
|
||||
import { readPostgresLogicalDumps, renderPostgresRemoteRunnerForTest } from "./platform-db";
|
||||
|
||||
describe("PK01 PostgreSQL logical dumps", () => {
|
||||
test("parses independent Sub2API and PikaOA declarations", () => {
|
||||
const logicalDumps = readPostgresLogicalDumps("config/platform-db/postgres-pk01.yaml");
|
||||
describe("PostgreSQL logical dumps", () => {
|
||||
test("keeps Sub2API on PK01 and declares PikaOA on NC01", () => {
|
||||
const pk01LogicalDumps = readPostgresLogicalDumps("config/platform-db/postgres-pk01.yaml");
|
||||
const nc01LogicalDumps = readPostgresLogicalDumps("config/platform-db/postgres-nc01.yaml");
|
||||
|
||||
expect(logicalDumps).toHaveLength(2);
|
||||
expect(logicalDumps.map((item) => item.database)).toEqual(["sub2api", "pikaoa"]);
|
||||
expect(logicalDumps[0]).toMatchObject({
|
||||
expect(pk01LogicalDumps).toHaveLength(1);
|
||||
expect(pk01LogicalDumps[0]).toMatchObject({
|
||||
database: "sub2api",
|
||||
scriptPath: "/usr/local/sbin/unidesk-pk01-sub2api-pgdump.sh",
|
||||
serviceName: "unidesk-pk01-sub2api-pgdump.service",
|
||||
timerName: "unidesk-pk01-sub2api-pgdump.timer",
|
||||
schedule: "*-*-* 03:17:00",
|
||||
retentionDays: 14,
|
||||
});
|
||||
expect(logicalDumps[1]).toMatchObject({
|
||||
destination: { path: "/var/backups/unidesk/platform-db/pk01/pikaoa" },
|
||||
schedule: "*-*-* 03:37:00",
|
||||
expect(nc01LogicalDumps.map((item) => item.database)).toEqual(["agentrun_v02", "pikaoa"]);
|
||||
expect(nc01LogicalDumps[1]).toMatchObject({
|
||||
destination: { path: "/var/backups/unidesk/platform-db/nc01/pikaoa" },
|
||||
schedule: "*-*-* 03:47:00",
|
||||
retentionDays: 14,
|
||||
});
|
||||
});
|
||||
@@ -32,6 +35,13 @@ describe("PK01 PostgreSQL logical dumps", () => {
|
||||
expect(runner).not.toContain("OnCalendar=*-*-* 03:17:00");
|
||||
});
|
||||
|
||||
test("PK01 backup YAML has no duplicate future encryption key", () => {
|
||||
const source = readFileSync("config/platform-db/postgres-pk01.yaml", "utf8");
|
||||
|
||||
expect(source.match(/^\s+futureKeyRef:/gmu)).toHaveLength(1);
|
||||
expect(() => readPostgresLogicalDumps("config/platform-db/postgres-pk01.yaml")).not.toThrow();
|
||||
});
|
||||
|
||||
test("reports stale or missing evidence as warnings", () => {
|
||||
const [declaration] = readPostgresLogicalDumps("config/platform-db/postgres-pk01.yaml");
|
||||
const [projection] = projectLogicalDumps([declaration], [{
|
||||
|
||||
Reference in New Issue
Block a user