feat: move D601 HWLAB GitOps to UniDesk YAML
This commit is contained in:
@@ -56,26 +56,34 @@ targets:
|
||||
repository: pikasTech/HWLAB
|
||||
branch: v0.3
|
||||
gitops:
|
||||
branch: v0.3-d601-gitops
|
||||
branch: v0.3-gitops
|
||||
path: deploy/gitops/node/d601/runtime-v03
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
serviceReadName: git-mirror-http
|
||||
serviceWriteName: git-mirror-write
|
||||
cachePvcName: hwlab-git-mirror-cache
|
||||
cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-d601-v03-git-mirror-cache
|
||||
cachePvcStorage: 20Gi
|
||||
servicePort: 8080
|
||||
deploymentReplicas: 0
|
||||
deploymentReplicas: 1
|
||||
secretName: git-mirror-github-ssh
|
||||
syncConfigMapName: git-mirror-sync-script
|
||||
syncJobPrefix: git-mirror-hwlab-d601-v03-sync-manual
|
||||
flushJobPrefix: git-mirror-hwlab-d601-v03-flush-manual
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
storage:
|
||||
localPath:
|
||||
namespace: kube-system
|
||||
configMapName: local-path-config
|
||||
provisionerDeployment: local-path-provisioner
|
||||
helperImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
imagePullPolicy: IfNotPresent
|
||||
tekton:
|
||||
pipelineName: hwlab-d601-v03-ci-image-publish
|
||||
serviceAccountName: hwlab-d601-v03-tekton-runner
|
||||
pipelineRunPrefix: hwlab-d601-v03-ci-poll
|
||||
pipelineName: hwlab-v03-ci-image-publish
|
||||
serviceAccountName: hwlab-v03-tekton-runner
|
||||
pipelineRunPrefix: hwlab-v03-ci-poll
|
||||
toolsImage:
|
||||
output: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
sourceKind: dockerfile
|
||||
@@ -124,9 +132,22 @@ targets:
|
||||
buildMode: node-local
|
||||
argo:
|
||||
namespace: argocd
|
||||
projectName: hwlab-d601
|
||||
applicationName: hwlab-d601-v03
|
||||
applicationFile: application-d601-v03.yaml
|
||||
projectName: hwlab-v03
|
||||
applicationName: hwlab-node-v03
|
||||
applicationFile: application-v03.yaml
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
obsoleteApplications:
|
||||
- hwlab-d601-v03
|
||||
resourceTracking:
|
||||
manageEndpointBridge: true
|
||||
repositoryCredential:
|
||||
enabled: true
|
||||
repoURL: git@github.com:pikasTech/HWLAB.git
|
||||
secretName: hwlab-node-v03-repository
|
||||
sourceSecret:
|
||||
namespace: hwlab-ci
|
||||
name: hwlab-git-ssh
|
||||
key: ssh-privatekey
|
||||
install:
|
||||
enabled: true
|
||||
sourceKind: url
|
||||
|
||||
@@ -10,6 +10,13 @@ nodes:
|
||||
gitopsRoot: deploy/gitops/node
|
||||
networkProfile: node-ci-egress
|
||||
downloadProfile: node-default
|
||||
D601:
|
||||
route: D601
|
||||
kubeRoute: D601:k3s
|
||||
sourceWorkspace: /home/ubuntu/workspace/hwlab-v03
|
||||
gitopsRoot: deploy/gitops/node
|
||||
networkProfile: d601-node-ci-egress
|
||||
downloadProfile: d601-node-default
|
||||
|
||||
lanes:
|
||||
v02:
|
||||
@@ -45,6 +52,8 @@ lanes:
|
||||
- hwlab-gateway
|
||||
- hwlab-edge-proxy
|
||||
- hwlab-agent-skills
|
||||
observability:
|
||||
prometheusOperator: true
|
||||
public:
|
||||
webUrl: http://74.48.78.17:19666
|
||||
apiUrl: http://74.48.78.17:19667
|
||||
@@ -81,9 +90,69 @@ lanes:
|
||||
- hwlab-gateway
|
||||
- hwlab-edge-proxy
|
||||
- hwlab-agent-skills
|
||||
observability:
|
||||
prometheusOperator: true
|
||||
public:
|
||||
webUrl: http://74.48.78.17:20666
|
||||
apiUrl: http://74.48.78.17:20667
|
||||
targets:
|
||||
D601:
|
||||
workspace: /home/ubuntu/workspace/hwlab-v03
|
||||
cicdRepo: /home/ubuntu/workspace/hwlab-v03-cicd.git
|
||||
cicdRepoLock: /tmp/hwlab-v03-cicd-repo.lock
|
||||
app: hwlab-node-v03
|
||||
pipeline: hwlab-v03-ci-image-publish
|
||||
pipelineRunPrefix: hwlab-v03-ci-poll
|
||||
serviceAccountName: hwlab-v03-tekton-runner
|
||||
controlPlaneFieldManager: unidesk-hwlab-d601-v03-control-plane
|
||||
git:
|
||||
url: git@github.com:pikasTech/HWLAB.git
|
||||
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
argo:
|
||||
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git
|
||||
gitopsBranch: v0.3-gitops
|
||||
catalogPath: deploy/artifact-catalog.d601-v03.json
|
||||
runtime:
|
||||
path: deploy/gitops/node/d601/runtime-v03
|
||||
namespace: hwlab-v03
|
||||
renderDir: runtime-v03
|
||||
tektonDir: tekton-v03
|
||||
argoApplicationFile: application-v03.yaml
|
||||
registryPrefix: 127.0.0.1:5000/hwlab
|
||||
baseImage: 127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim
|
||||
baseImageSource: node:20-bookworm-slim
|
||||
buildkit:
|
||||
sidecarImage: 127.0.0.1:5000/hwlab/buildkit:rootless
|
||||
stepEnv:
|
||||
HOME: /tekton/home
|
||||
XDG_CONFIG_HOME: /tekton/home/.config
|
||||
observability:
|
||||
prometheusOperator: false
|
||||
public:
|
||||
webUrl: https://v03.hwpod.com
|
||||
apiUrl: https://v03.hwpod.com
|
||||
externalPostgres:
|
||||
provider: PK01
|
||||
configRef: config/platform-db/postgres-pk01.yaml
|
||||
serviceName: pk01-platform-postgres
|
||||
endpointAddress: 82.156.23.220
|
||||
port: 5432
|
||||
sslmode: require
|
||||
database: hwlab_d601_v03
|
||||
cloudApi:
|
||||
secretName: hwlab-cloud-api-v03-db
|
||||
secretKey: database-url
|
||||
sourceRef: hwlab/d601-v03-cloud-api-db.env
|
||||
envKey: DATABASE_URL
|
||||
role: hwlab_d601_v03_app
|
||||
openfga:
|
||||
secretName: hwlab-v03-openfga
|
||||
secretKey: datastore-uri
|
||||
sourceRef: hwlab/d601-v03-openfga-db.env
|
||||
envKey: DATASTORE_URI
|
||||
authnKey: authn-preshared-key
|
||||
role: hwlab_d601_v03_app
|
||||
|
||||
networkProfiles:
|
||||
node-ci-egress:
|
||||
@@ -153,12 +222,70 @@ networkProfiles:
|
||||
- ::1
|
||||
- host.docker.internal
|
||||
- 127.0.0.1:5000
|
||||
d601-node-ci-egress:
|
||||
proxy:
|
||||
http: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
|
||||
https: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
|
||||
all: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- kubernetes
|
||||
- kubernetes.default
|
||||
- kubernetes.default.svc
|
||||
- 10.0.0.0/8
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 82.156.23.220
|
||||
- 74.48.78.17
|
||||
dockerBuildProxy:
|
||||
http: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
|
||||
https: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
|
||||
all: http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
|
||||
downloadProfiles:
|
||||
node-default:
|
||||
git:
|
||||
proxyMode: inherit
|
||||
retries: 3
|
||||
timeoutSeconds: 240
|
||||
npm:
|
||||
registry: https://registry.npmjs.org/
|
||||
retries: 3
|
||||
fetchTimeoutSeconds: 120
|
||||
pip:
|
||||
indexUrl: https://pypi.org/simple
|
||||
retries: 3
|
||||
timeoutSeconds: 120
|
||||
docker:
|
||||
registryMirrors: []
|
||||
pullRetries: 3
|
||||
curl:
|
||||
retries: 3
|
||||
connectTimeoutSeconds: 10
|
||||
maxTimeSeconds: 120
|
||||
d601-node-default:
|
||||
git:
|
||||
proxyMode: inherit
|
||||
retries: 3
|
||||
timeoutSeconds: 60
|
||||
npm:
|
||||
registry: https://registry.npmjs.org/
|
||||
retries: 3
|
||||
|
||||
@@ -8,6 +8,7 @@ metadata:
|
||||
relatedIssues:
|
||||
- 280
|
||||
- 281
|
||||
- 1119
|
||||
|
||||
cluster:
|
||||
role: primary
|
||||
@@ -84,7 +85,7 @@ postgres:
|
||||
purpose: admin-and-secret-sync
|
||||
- id: D601-public
|
||||
cidr: 36.49.29.73/32
|
||||
purpose: platform-infra-standby-app
|
||||
purpose: platform-infra-and-hwlab-v03-app
|
||||
tuning:
|
||||
maxConnections: 50
|
||||
sharedBuffers: 512MB
|
||||
@@ -135,6 +136,11 @@ postgres:
|
||||
user: sub2api
|
||||
address: 36.49.29.73/32
|
||||
method: scram-sha-256
|
||||
- type: hostssl
|
||||
database: hwlab_d601_v03
|
||||
user: hwlab_d601_v03_app
|
||||
address: 36.49.29.73/32
|
||||
method: scram-sha-256
|
||||
|
||||
secrets:
|
||||
source: master-local
|
||||
@@ -154,6 +160,20 @@ secrets:
|
||||
SUB2API_DB_NAME: sub2api
|
||||
randomHex:
|
||||
SUB2API_DB_PASSWORD: 32
|
||||
- name: hwlab-d601-v03-db-credentials
|
||||
sourceRef: platform-db/hwlab-d601-v03-db.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- HWLAB_D601_V03_DB_USER
|
||||
- HWLAB_D601_V03_DB_PASSWORD
|
||||
- HWLAB_D601_V03_DB_NAME
|
||||
createIfMissing:
|
||||
enabled: true
|
||||
values:
|
||||
HWLAB_D601_V03_DB_USER: hwlab_d601_v03_app
|
||||
HWLAB_D601_V03_DB_NAME: hwlab_d601_v03
|
||||
randomHex:
|
||||
HWLAB_D601_V03_DB_PASSWORD: 32
|
||||
|
||||
objects:
|
||||
roles:
|
||||
@@ -166,12 +186,26 @@ objects:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
- name: hwlab_d601_v03_app
|
||||
passwordRef:
|
||||
sourceRef: platform-db/hwlab-d601-v03-db.env
|
||||
key: HWLAB_D601_V03_DB_PASSWORD
|
||||
login: true
|
||||
attributes:
|
||||
createdb: false
|
||||
createrole: false
|
||||
superuser: false
|
||||
databases:
|
||||
- name: sub2api
|
||||
owner: sub2api
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
- name: hwlab_d601_v03
|
||||
owner: hwlab_d601_v03_app
|
||||
encoding: UTF8
|
||||
locale: C.UTF-8
|
||||
extensions: []
|
||||
|
||||
exports:
|
||||
connectionStrings:
|
||||
@@ -190,6 +224,36 @@ exports:
|
||||
- scope: platform-infra
|
||||
secret: sub2api-secrets
|
||||
key: DATABASE_URL
|
||||
- name: hwlab-d601-v03-cloud-api-database-url
|
||||
sourceSecretRef: platform-db/hwlab-d601-v03-db.env
|
||||
render:
|
||||
envKey: DATABASE_URL
|
||||
format: postgresql://$(HWLAB_D601_V03_DB_USER):$(HWLAB_D601_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_D601_V03_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 82.156.23.220
|
||||
writeToSecretSource:
|
||||
sourceRef: hwlab/d601-v03-cloud-api-db.env
|
||||
key: DATABASE_URL
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: hwlab-v03
|
||||
secret: hwlab-cloud-api-v03-db
|
||||
key: database-url
|
||||
- name: hwlab-d601-v03-openfga-datastore-uri
|
||||
sourceSecretRef: platform-db/hwlab-d601-v03-db.env
|
||||
render:
|
||||
envKey: DATASTORE_URI
|
||||
format: postgresql://$(HWLAB_D601_V03_DB_USER):$(HWLAB_D601_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_D601_V03_DB_NAME)?sslmode=require
|
||||
variables:
|
||||
PGHOST: 82.156.23.220
|
||||
writeToSecretSource:
|
||||
sourceRef: hwlab/d601-v03-openfga-db.env
|
||||
key: DATASTORE_URI
|
||||
mode: update-or-insert
|
||||
consumers:
|
||||
- scope: hwlab-v03
|
||||
secret: hwlab-v03-openfga
|
||||
key: datastore-uri
|
||||
|
||||
backup:
|
||||
phase: minimum-restoreable
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ export interface HwlabDownloadProfileSpec {
|
||||
readonly git: {
|
||||
readonly proxyMode: "inherit" | "direct" | "none";
|
||||
readonly retries: number;
|
||||
readonly timeoutSeconds: number;
|
||||
};
|
||||
readonly npm: {
|
||||
readonly registry: string;
|
||||
@@ -54,6 +55,36 @@ export interface HwlabDownloadProfileSpec {
|
||||
};
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeExternalPostgresComponentSpec {
|
||||
readonly secretName: string;
|
||||
readonly secretKey: string;
|
||||
readonly sourceRef: string;
|
||||
readonly envKey: string;
|
||||
readonly role: string;
|
||||
readonly authnKey?: string;
|
||||
readonly schema?: string;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeExternalPostgresSpec {
|
||||
readonly provider: string;
|
||||
readonly configRef: string;
|
||||
readonly serviceName: string;
|
||||
readonly endpointAddress: string;
|
||||
readonly port: number;
|
||||
readonly sslmode: "require";
|
||||
readonly database: string;
|
||||
readonly cloudApi: HwlabRuntimeExternalPostgresComponentSpec;
|
||||
readonly openfga: HwlabRuntimeExternalPostgresComponentSpec;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeBuildkitSpec {
|
||||
readonly sidecarImage: string;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeObservabilitySpec {
|
||||
readonly prometheusOperator: boolean;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeLaneSpec {
|
||||
readonly lane: HwlabRuntimeLane;
|
||||
readonly nodeId: string;
|
||||
@@ -74,6 +105,7 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly gitUrl: string;
|
||||
readonly gitReadUrl: string;
|
||||
readonly gitWriteUrl: string;
|
||||
readonly argoRepoUrl: string;
|
||||
readonly gitopsBranch: string;
|
||||
readonly catalogPath: string;
|
||||
readonly runtimePath: string;
|
||||
@@ -83,9 +115,14 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly argoApplicationFile: string;
|
||||
readonly registryPrefix: string;
|
||||
readonly baseImage: string;
|
||||
readonly baseImageSource?: string;
|
||||
readonly serviceIds: readonly string[];
|
||||
readonly publicWebUrl: string;
|
||||
readonly publicApiUrl: string;
|
||||
readonly stepEnv: Record<string, string>;
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly observability: HwlabRuntimeObservabilitySpec;
|
||||
readonly networkProfileId: string;
|
||||
readonly downloadProfileId: string;
|
||||
readonly networkProfile: HwlabNetworkProfileSpec;
|
||||
@@ -109,6 +146,7 @@ interface HwlabLaneConfig {
|
||||
readonly serviceAccountName: string;
|
||||
readonly controlPlaneFieldManager: string;
|
||||
readonly git: { readonly url: string; readonly readUrl: string; readonly writeUrl: string };
|
||||
readonly argo: { readonly repoURL?: string };
|
||||
readonly gitopsBranch: string;
|
||||
readonly catalogPath: string;
|
||||
readonly runtime: { readonly path: string; readonly namespace: string; readonly renderDir: string };
|
||||
@@ -116,14 +154,20 @@ interface HwlabLaneConfig {
|
||||
readonly argoApplicationFile: string;
|
||||
readonly registryPrefix: string;
|
||||
readonly baseImage: string;
|
||||
readonly baseImageSource?: string;
|
||||
readonly serviceIds: readonly string[];
|
||||
readonly public: { readonly webUrl: string; readonly apiUrl: string };
|
||||
readonly stepEnv: Record<string, string>;
|
||||
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly observability: HwlabRuntimeObservabilitySpec;
|
||||
}
|
||||
|
||||
interface HwlabNodeLaneConfig {
|
||||
readonly requiredNoProxy: readonly string[];
|
||||
readonly nodes: Record<string, HwlabRuntimeNodeSpec>;
|
||||
readonly lanes: Record<HwlabRuntimeLane, HwlabLaneConfig>;
|
||||
readonly laneTargets: Partial<Record<HwlabRuntimeLane, Record<string, HwlabLaneConfig>>>;
|
||||
readonly networkProfiles: Record<string, HwlabNetworkProfileSpec>;
|
||||
readonly downloadProfiles: Record<string, HwlabDownloadProfileSpec>;
|
||||
}
|
||||
@@ -160,6 +204,12 @@ function optionalStringField(obj: Record<string, unknown>, key: string, path: st
|
||||
return value;
|
||||
}
|
||||
|
||||
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function sortedRecordEntries(value: unknown, path: string): Array<[string, Record<string, unknown>]> {
|
||||
return Object.entries(asRecord(value, path)).map(([key, item]) => [key, asRecord(item, `${path}.${key}`)]);
|
||||
}
|
||||
@@ -168,6 +218,22 @@ function unique(values: readonly string[]): string[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function mergeOptionalRecord(base: unknown, override: unknown): Record<string, unknown> | undefined {
|
||||
if (base === undefined && override === undefined) return undefined;
|
||||
const baseRecord = base === undefined ? {} : asRecord(base, "base");
|
||||
const overrideRecord = override === undefined ? {} : asRecord(override, "override");
|
||||
return { ...baseRecord, ...overrideRecord };
|
||||
}
|
||||
|
||||
function optionalStringRecord(value: unknown, path: string): Record<string, string> {
|
||||
if (value === undefined) return {};
|
||||
const raw = asRecord(value, path);
|
||||
return Object.fromEntries(Object.entries(raw).map(([key, item]) => {
|
||||
if (typeof item !== "string" || item.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
|
||||
return [key, item];
|
||||
}));
|
||||
}
|
||||
|
||||
function proxyConfig(raw: Record<string, unknown>, id: string, key: "proxy" | "dockerBuildProxy", requiredNoProxy: readonly string[]): HwlabProxySpec {
|
||||
const proxy = asRecord(raw[key], `networkProfiles.${id}.${key}`);
|
||||
return {
|
||||
@@ -199,7 +265,11 @@ function downloadProfileConfig(id: string, raw: Record<string, unknown>): HwlabD
|
||||
}
|
||||
return {
|
||||
id,
|
||||
git: { proxyMode, retries: numberField(git, "retries", `downloadProfiles.${id}.git`) },
|
||||
git: {
|
||||
proxyMode,
|
||||
retries: numberField(git, "retries", `downloadProfiles.${id}.git`),
|
||||
timeoutSeconds: numberField(git, "timeoutSeconds", `downloadProfiles.${id}.git`),
|
||||
},
|
||||
npm: {
|
||||
registry: stringField(npm, "registry", `downloadProfiles.${id}.npm`),
|
||||
retries: numberField(npm, "retries", `downloadProfiles.${id}.npm`),
|
||||
@@ -240,6 +310,7 @@ function isSupportedLaneId(id: string): id is HwlabRuntimeLane {
|
||||
|
||||
function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLaneConfig {
|
||||
const git = asRecord(raw.git, `lanes.${id}.git`);
|
||||
const argo = raw.argo === undefined ? {} : asRecord(raw.argo, `lanes.${id}.argo`);
|
||||
const runtime = asRecord(raw.runtime, `lanes.${id}.runtime`);
|
||||
const publicUrls = asRecord(raw.public, `lanes.${id}.public`);
|
||||
const minor = numberField(raw, "minor", `lanes.${id}`);
|
||||
@@ -264,6 +335,9 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
readUrl: stringField(git, "readUrl", `lanes.${id}.git`),
|
||||
writeUrl: stringField(git, "writeUrl", `lanes.${id}.git`),
|
||||
},
|
||||
argo: {
|
||||
repoURL: optionalStringField(argo, "repoURL", `lanes.${id}.argo`),
|
||||
},
|
||||
gitopsBranch: stringField(raw, "gitopsBranch", `lanes.${id}`),
|
||||
catalogPath: stringField(raw, "catalogPath", `lanes.${id}`),
|
||||
runtime: {
|
||||
@@ -275,11 +349,80 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
argoApplicationFile: stringField(raw, "argoApplicationFile", `lanes.${id}`),
|
||||
registryPrefix: stringField(raw, "registryPrefix", `lanes.${id}`),
|
||||
baseImage: stringField(raw, "baseImage", `lanes.${id}`),
|
||||
baseImageSource: optionalStringField(raw, "baseImageSource", `lanes.${id}`),
|
||||
serviceIds: stringArrayField(raw, "serviceIds", `lanes.${id}`),
|
||||
public: {
|
||||
webUrl: stringField(publicUrls, "webUrl", `lanes.${id}.public`),
|
||||
apiUrl: stringField(publicUrls, "apiUrl", `lanes.${id}.public`),
|
||||
},
|
||||
stepEnv: optionalStringRecord(raw.stepEnv, `lanes.${id}.stepEnv`),
|
||||
buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`),
|
||||
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
|
||||
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
|
||||
};
|
||||
}
|
||||
|
||||
function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<string, unknown>, targetRaw: Record<string, unknown>): HwlabLaneConfig {
|
||||
const merged: Record<string, unknown> = {
|
||||
...baseRaw,
|
||||
...targetRaw,
|
||||
node: nodeId,
|
||||
git: mergeOptionalRecord(baseRaw.git, targetRaw.git),
|
||||
argo: mergeOptionalRecord(baseRaw.argo, targetRaw.argo),
|
||||
runtime: mergeOptionalRecord(baseRaw.runtime, targetRaw.runtime),
|
||||
public: mergeOptionalRecord(baseRaw.public, targetRaw.public),
|
||||
stepEnv: mergeOptionalRecord(baseRaw.stepEnv, targetRaw.stepEnv) ?? {},
|
||||
buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit),
|
||||
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
|
||||
observability: mergeOptionalRecord(baseRaw.observability, targetRaw.observability),
|
||||
};
|
||||
delete merged.targets;
|
||||
return laneConfig(id, merged);
|
||||
}
|
||||
|
||||
function buildkitConfig(value: unknown, path: string): HwlabRuntimeBuildkitSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
return {
|
||||
sidecarImage: stringField(raw, "sidecarImage", path),
|
||||
};
|
||||
}
|
||||
|
||||
function externalPostgresComponentConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresComponentSpec {
|
||||
const raw = asRecord(value, path);
|
||||
return {
|
||||
secretName: stringField(raw, "secretName", path),
|
||||
secretKey: stringField(raw, "secretKey", path),
|
||||
sourceRef: stringField(raw, "sourceRef", path),
|
||||
envKey: stringField(raw, "envKey", path),
|
||||
role: stringField(raw, "role", path),
|
||||
authnKey: optionalStringField(raw, "authnKey", path),
|
||||
schema: optionalStringField(raw, "schema", path),
|
||||
};
|
||||
}
|
||||
|
||||
function externalPostgresConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
const sslmode = stringField(raw, "sslmode", path);
|
||||
if (sslmode !== "require") throw new Error(`${path}.sslmode must be require`);
|
||||
return {
|
||||
provider: stringField(raw, "provider", path),
|
||||
configRef: stringField(raw, "configRef", path),
|
||||
serviceName: stringField(raw, "serviceName", path),
|
||||
endpointAddress: stringField(raw, "endpointAddress", path),
|
||||
port: numberField(raw, "port", path),
|
||||
sslmode,
|
||||
database: stringField(raw, "database", path),
|
||||
cloudApi: externalPostgresComponentConfig(raw.cloudApi, `${path}.cloudApi`),
|
||||
openfga: externalPostgresComponentConfig(raw.openfga, `${path}.openfga`),
|
||||
};
|
||||
}
|
||||
|
||||
function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservabilitySpec {
|
||||
const raw = asRecord(value, path);
|
||||
return {
|
||||
prometheusOperator: booleanField(raw, "prometheusOperator", path),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -298,18 +441,27 @@ function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig {
|
||||
const downloadProfiles = Object.fromEntries(
|
||||
sortedRecordEntries(parsed.downloadProfiles, "downloadProfiles").map(([id, item]) => [id, downloadProfileConfig(id, item)]),
|
||||
);
|
||||
const lanes = Object.fromEntries(sortedRecordEntries(parsed.lanes, "lanes").map(([id, item]) => {
|
||||
const laneEntries = sortedRecordEntries(parsed.lanes, "lanes");
|
||||
const lanes = Object.fromEntries(laneEntries.map(([id, item]) => {
|
||||
if (!isSupportedLaneId(id)) throw new Error(`lanes.${id} is not supported by this CLI build`);
|
||||
return [id, laneConfig(id, item)];
|
||||
})) as Record<HwlabRuntimeLane, HwlabLaneConfig>;
|
||||
const laneTargets: Partial<Record<HwlabRuntimeLane, Record<string, HwlabLaneConfig>>> = {};
|
||||
for (const [id, item] of laneEntries) {
|
||||
if (!isSupportedLaneId(id) || item.targets === undefined) continue;
|
||||
laneTargets[id] = Object.fromEntries(sortedRecordEntries(item.targets, `lanes.${id}.targets`).map(([nodeId, target]) => [
|
||||
nodeId,
|
||||
laneTargetConfig(id, nodeId, item, target),
|
||||
]));
|
||||
}
|
||||
for (const node of Object.values(nodes)) {
|
||||
if (networkProfiles[node.networkProfileId] === undefined) throw new Error(`nodes.${node.id}.networkProfile references missing profile ${node.networkProfileId}`);
|
||||
if (downloadProfiles[node.downloadProfileId] === undefined) throw new Error(`nodes.${node.id}.downloadProfile references missing profile ${node.downloadProfileId}`);
|
||||
}
|
||||
for (const lane of Object.values(lanes)) {
|
||||
for (const lane of [...Object.values(lanes), ...Object.values(laneTargets).flatMap((targets) => Object.values(targets))]) {
|
||||
if (nodes[lane.node] === undefined) throw new Error(`lanes.${lane.id}.node references missing node ${lane.node}`);
|
||||
}
|
||||
return { requiredNoProxy, nodes, lanes, networkProfiles, downloadProfiles };
|
||||
return { requiredNoProxy, nodes, lanes, laneTargets, networkProfiles, downloadProfiles };
|
||||
}
|
||||
|
||||
const HWLAB_NODE_LANE_CONFIG = readHwlabNodeLaneConfig();
|
||||
@@ -338,6 +490,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
gitUrl: config.git.url,
|
||||
gitReadUrl: config.git.readUrl,
|
||||
gitWriteUrl: config.git.writeUrl,
|
||||
argoRepoUrl: config.argo.repoURL ?? config.git.readUrl,
|
||||
gitopsBranch: config.gitopsBranch,
|
||||
catalogPath: config.catalogPath,
|
||||
runtimePath: config.runtime.path,
|
||||
@@ -347,9 +500,14 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
argoApplicationFile: config.argoApplicationFile,
|
||||
registryPrefix: config.registryPrefix,
|
||||
baseImage: config.baseImage,
|
||||
...(config.baseImageSource === undefined ? {} : { baseImageSource: config.baseImageSource }),
|
||||
serviceIds: config.serviceIds,
|
||||
publicWebUrl: config.public.webUrl,
|
||||
publicApiUrl: config.public.apiUrl,
|
||||
stepEnv: config.stepEnv,
|
||||
...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }),
|
||||
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
|
||||
observability: config.observability,
|
||||
networkProfileId: networkProfile.id,
|
||||
downloadProfileId: downloadProfile.id,
|
||||
networkProfile,
|
||||
@@ -369,6 +527,15 @@ export function hwlabRuntimeLaneSpec(lane: HwlabRuntimeLane): HwlabRuntimeLaneSp
|
||||
return RUNTIME_LANE_SPECS[lane];
|
||||
}
|
||||
|
||||
export function hwlabRuntimeLaneSpecForNode(lane: HwlabRuntimeLane, nodeId: string): HwlabRuntimeLaneSpec {
|
||||
const targetSpec = HWLAB_NODE_LANE_CONFIG.laneTargets[lane]?.[nodeId];
|
||||
if (targetSpec !== undefined) return buildRuntimeLaneSpec(targetSpec);
|
||||
const defaultSpec = RUNTIME_LANE_SPECS[lane];
|
||||
if (defaultSpec.nodeId === nodeId) return defaultSpec;
|
||||
const knownNodes = unique([defaultSpec.nodeId, ...Object.keys(HWLAB_NODE_LANE_CONFIG.laneTargets[lane] ?? {})]).join(", ");
|
||||
throw new Error(`lane ${lane} has no target for node ${nodeId}; known nodes: ${knownNodes}`);
|
||||
}
|
||||
|
||||
export function hwlabRuntimeLaneIds(): HwlabRuntimeLane[] {
|
||||
return Object.keys(RUNTIME_LANE_SPECS) as HwlabRuntimeLane[];
|
||||
}
|
||||
|
||||
+1976
-6
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -418,6 +418,8 @@ function summarizeRuntimeLaneTriggerJobProgress(job: JobRecord, stdoutTail: stri
|
||||
const stageStatus = stringField(lastEvent.status);
|
||||
const sourceCommit = stringField(lastEvent.sourceCommit) ?? firstMatch(stdoutTail, /"sourceCommit"\s*:\s*"([0-9a-f]{40})"/iu);
|
||||
const pipelineRun = stringField(lastEvent.pipelineRun) ?? firstMatch(stdoutTail, /"pipelineRun"\s*:\s*"([^"]+)"/u);
|
||||
const node = stringField(lastEvent.node) ?? commandOption(job.command, "--node") ?? "G14";
|
||||
const lane = stringField(lastEvent.lane) ?? commandOption(job.command, "--lane") ?? "v03";
|
||||
const pipelineCreated = /pipelinerun\.tekton\.dev\/[^ \n]+ created/u.test(stdoutTail)
|
||||
? true
|
||||
: stage === "create-pipelinerun" && stageStatus === "failed"
|
||||
@@ -465,13 +467,19 @@ function summarizeRuntimeLaneTriggerJobProgress(job: JobRecord, stdoutTail: stri
|
||||
slow ? "visibility-warning" : null,
|
||||
].filter(Boolean).join(" "),
|
||||
nextCommand: pipelineRun
|
||||
? `bun scripts/cli.ts hwlab nodes control-plane status --node ${stringField(lastEvent.node) ?? "G14"} --lane ${stringField(lastEvent.lane) ?? "v03"} --pipeline-run ${pipelineRun}`
|
||||
? `bun scripts/cli.ts hwlab nodes control-plane status --node ${node} --lane ${lane} --pipeline-run ${pipelineRun}`
|
||||
: job.status === "running"
|
||||
? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function commandOption(command: readonly string[], name: string): string | null {
|
||||
const index = command.indexOf(name);
|
||||
const value = index >= 0 ? command[index + 1] : undefined;
|
||||
return typeof value === "string" && value.trim().length > 0 && !value.startsWith("--") ? value.trim() : null;
|
||||
}
|
||||
|
||||
function genericJobProgress(job: JobRecord, stderrTailOverride?: string): JobProgressSummary {
|
||||
const nowMs = Date.now();
|
||||
const stderrTail = stderrTailOverride ?? tailFile(job.stderrFile, 96_000);
|
||||
|
||||
+96
-38
@@ -242,6 +242,8 @@ interface RemoteFacts {
|
||||
logDirExists: boolean;
|
||||
roleExists: boolean;
|
||||
databaseExists: boolean;
|
||||
roleExistsByName?: Record<string, boolean>;
|
||||
databaseExistsByName?: Record<string, boolean>;
|
||||
serverVersion: string | null;
|
||||
sslOn: boolean;
|
||||
sslCertFile: string | null;
|
||||
@@ -383,12 +385,14 @@ async function status(config: UniDeskConfig, options: PlatformDbOptions): Promis
|
||||
const facts = remote.parsed;
|
||||
const controllerConnection = controllerConnectionProbe(pg, secrets);
|
||||
const endpointHealthy = controllerConnection.ok === true && controllerConnection.ssl === true;
|
||||
const rolesReady = facts !== null && pg.objects.roles.every((role, index) => facts.postgres.roleExistsByName?.[role.name] ?? (index === 0 ? facts.postgres.roleExists : false));
|
||||
const databasesReady = facts !== null && pg.objects.databases.every((database, index) => facts.postgres.databaseExistsByName?.[database.name] ?? (index === 0 ? facts.postgres.databaseExists : false));
|
||||
const deploymentHealthy = facts !== null
|
||||
&& facts.postgres.packageInstalled
|
||||
&& facts.postgres.serviceActive
|
||||
&& facts.postgres.sslOn
|
||||
&& facts.postgres.roleExists
|
||||
&& facts.postgres.databaseExists
|
||||
&& rolesReady
|
||||
&& databasesReady
|
||||
&& facts.network.port5432Listening
|
||||
&& facts.postgres.appConnectionOk === true
|
||||
&& facts.postgres.appConnectionSsl === true;
|
||||
@@ -421,6 +425,10 @@ async function status(config: UniDeskConfig, options: PlatformDbOptions): Promis
|
||||
dnsAddresses: facts.network.dns.addresses,
|
||||
roleExists: facts.postgres.roleExists,
|
||||
databaseExists: facts.postgres.databaseExists,
|
||||
roleExistsByName: facts.postgres.roleExistsByName ?? {},
|
||||
databaseExistsByName: facts.postgres.databaseExistsByName ?? {},
|
||||
rolesReady,
|
||||
databasesReady,
|
||||
appConnectionOk: facts.postgres.appConnectionOk,
|
||||
appConnectionSsl: facts.postgres.appConnectionSsl,
|
||||
appConnectionHost: facts.postgres.appConnectionHost,
|
||||
@@ -548,7 +556,7 @@ function readPostgresHostConfig(pathArg: string): PostgresHostConfig {
|
||||
const entries = arrayOfRecords(secrets.entries, `${configPath}.secrets.entries`).map((entry, index) => secretEntry(entry, `${configPath}.secrets.entries[${index}]`));
|
||||
const roles = arrayOfRecords(objects.roles, `${configPath}.objects.roles`).map((role, index) => roleConfig(role, `${configPath}.objects.roles[${index}]`));
|
||||
const databases = arrayOfRecords(objects.databases, `${configPath}.objects.databases`).map((database, index) => databaseConfig(database, `${configPath}.objects.databases[${index}]`));
|
||||
if (roles.length !== 1 || databases.length !== 1) throw new Error(`${configPath}.objects must declare exactly one role and one database for the first PK01 Sub2API rollout`);
|
||||
if (roles.length === 0 || databases.length === 0) throw new Error(`${configPath}.objects.roles/databases must each contain at least one item`);
|
||||
const connectionStrings = arrayOfRecords(exportsConfig.connectionStrings, `${configPath}.exports.connectionStrings`).map((item, index) => connectionStringExport(item, `${configPath}.exports.connectionStrings[${index}]`));
|
||||
return {
|
||||
configPath: relativeConfigPath(configPath),
|
||||
@@ -849,7 +857,7 @@ function connectionStringExport(item: Record<string, unknown>, path: string): Co
|
||||
consumers: arrayOfRecords(item.consumers, `${path}.consumers`).map((consumer, index) => ({
|
||||
scope: stringField(consumer, "scope", `${path}.consumers[${index}]`),
|
||||
secret: stringField(consumer, "secret", `${path}.consumers[${index}]`),
|
||||
key: envKey(stringField(consumer, "key", `${path}.consumers[${index}]`), `${path}.consumers[${index}].key`),
|
||||
key: kubernetesSecretKey(stringField(consumer, "key", `${path}.consumers[${index}]`), `${path}.consumers[${index}].key`),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -880,6 +888,11 @@ function envKey(value: string, path: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function kubernetesSecretKey(value: string, path: string): string {
|
||||
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path} must be a Kubernetes Secret key`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function configSummary(pg: PostgresHostConfig): Record<string, unknown> {
|
||||
return {
|
||||
path: pg.configPath,
|
||||
@@ -994,8 +1007,8 @@ function validateSub2ApiSecretConsistency(pg: PostgresHostConfig, inspection: Se
|
||||
const database = pg.objects.databases[0];
|
||||
const material = inspection.materials.get(role.passwordRef.sourceRef);
|
||||
if (material === undefined) throw new Error(`secret source not found for role passwordRef: ${role.passwordRef.sourceRef}`);
|
||||
if (material.values.SUB2API_DB_USER !== role.name) throw new Error("SUB2API_DB_USER must match YAML objects.roles[0].name");
|
||||
if (material.values.SUB2API_DB_NAME !== database.name) throw new Error("SUB2API_DB_NAME must match YAML objects.databases[0].name");
|
||||
if (material.values.SUB2API_DB_USER !== undefined && material.values.SUB2API_DB_USER !== role.name) throw new Error("SUB2API_DB_USER must match YAML objects.roles[0].name");
|
||||
if (material.values.SUB2API_DB_NAME !== undefined && material.values.SUB2API_DB_NAME !== database.name) throw new Error("SUB2API_DB_NAME must match YAML objects.databases[0].name");
|
||||
}
|
||||
|
||||
function writeConnectionStringExport(pg: PostgresHostConfig, inspection: SecretInspection, item: ConnectionStringExportConfig): Record<string, unknown> {
|
||||
@@ -1093,8 +1106,8 @@ function secretSummary(secrets: SecretInspection): Record<string, unknown> {
|
||||
}
|
||||
|
||||
function desiredSummary(pg: PostgresHostConfig, secrets: SecretInspection, facts: RemoteFacts | null): Record<string, unknown> {
|
||||
const role = pg.objects.roles[0];
|
||||
const database = pg.objects.databases[0];
|
||||
const roleExistsByName = facts?.postgres.roleExistsByName ?? {};
|
||||
const databaseExistsByName = facts?.postgres.databaseExistsByName ?? {};
|
||||
return {
|
||||
package: {
|
||||
name: `postgresql-${pg.postgres.package.version}`,
|
||||
@@ -1131,8 +1144,15 @@ function desiredSummary(pg: PostgresHostConfig, secrets: SecretInspection, facts
|
||||
},
|
||||
pgHbaManagedBlock: { start: managedHbaStart, end: managedHbaEnd, rules: pg.postgres.auth.pgHba.length },
|
||||
pgHbaRemoteTransport: "hostssl",
|
||||
role: { name: role.name, action: facts?.postgres.roleExists ? "none" : "create-or-update" },
|
||||
database: { name: database.name, owner: database.owner, action: facts?.postgres.databaseExists ? "none" : "create" },
|
||||
roles: pg.objects.roles.map((role, index) => ({
|
||||
name: role.name,
|
||||
action: (facts === null ? false : roleExistsByName[role.name] ?? (index === 0 ? facts.postgres.roleExists : false)) ? "none" : "create-or-update",
|
||||
})),
|
||||
databases: pg.objects.databases.map((database, index) => ({
|
||||
name: database.name,
|
||||
owner: database.owner,
|
||||
action: (facts === null ? false : databaseExistsByName[database.name] ?? (index === 0 ? facts.postgres.databaseExists : false)) ? "none" : "create",
|
||||
})),
|
||||
},
|
||||
secrets: secretSummary(secrets),
|
||||
exports: pg.exports.connectionStrings.map((item) => ({
|
||||
@@ -1234,6 +1254,8 @@ function factsScript(pg: PostgresHostConfig, probe: { user: string; password: st
|
||||
const logDir = pg.postgres.paths.logDir;
|
||||
const role = pg.objects.roles[0].name;
|
||||
const database = pg.objects.databases[0].name;
|
||||
const roleSqlList = pg.objects.roles.map((item) => `'${item.name.replace(/'/gu, "''")}'`).join(",");
|
||||
const databaseSqlList = pg.objects.databases.map((item) => `'${item.name.replace(/'/gu, "''")}'`).join(",");
|
||||
const dnsHost = pg.postgres.network.publicDns;
|
||||
const appHost = pg.postgres.network.listenAddresses.find((item) => item !== "127.0.0.1" && item !== "0.0.0.0") ?? "127.0.0.1";
|
||||
const probeEnv = probe === null
|
||||
@@ -1285,6 +1307,8 @@ if command -v runuser >/dev/null 2>&1 && command -v psql >/dev/null 2>&1 && id p
|
||||
$root_prefix runuser -u postgres -- psql -Atqc "SHOW ssl_key_file;" >"$tmp/sslKeyFile" 2>/dev/null
|
||||
$root_prefix runuser -u postgres -- psql -Atqc "SELECT 1 FROM pg_roles WHERE rolname='${role}'" >"$tmp/roleExists" 2>/dev/null
|
||||
$root_prefix runuser -u postgres -- psql -Atqc "SELECT 1 FROM pg_database WHERE datname='${database}'" >"$tmp/databaseExists" 2>/dev/null
|
||||
$root_prefix runuser -u postgres -- psql -Atqc "SELECT rolname FROM pg_roles WHERE rolname IN (${roleSqlList})" >"$tmp/rolesExisting" 2>/dev/null
|
||||
$root_prefix runuser -u postgres -- psql -Atqc "SELECT datname FROM pg_database WHERE datname IN (${databaseSqlList})" >"$tmp/databasesExisting" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
if [ -n "\${APP_PROBE_PASSWORD:-}" ] && command -v psql >/dev/null 2>&1; then
|
||||
@@ -1308,6 +1332,12 @@ def int_or_none(name):
|
||||
return None
|
||||
def yes_file(name, expected="0"):
|
||||
return text(name) == expected
|
||||
def set_lines(name):
|
||||
return {line.strip() for line in text(name).splitlines() if line.strip()}
|
||||
expected_roles = ${JSON.stringify(pg.objects.roles.map((item) => item.name))}
|
||||
expected_databases = ${JSON.stringify(pg.objects.databases.map((item) => item.name))}
|
||||
existing_roles = set_lines("rolesExisting")
|
||||
existing_databases = set_lines("databasesExisting")
|
||||
release_rc = text("releaserc")
|
||||
payload = {
|
||||
"ok": True,
|
||||
@@ -1349,6 +1379,8 @@ payload = {
|
||||
"logDirExists": yes_file("logDirRc"),
|
||||
"roleExists": text("roleExists") == "1",
|
||||
"databaseExists": text("databaseExists") == "1",
|
||||
"roleExistsByName": {name: name in existing_roles for name in expected_roles},
|
||||
"databaseExistsByName": {name: name in existing_databases for name in expected_databases},
|
||||
"serverVersion": text("serverVersion") or None,
|
||||
"sslOn": text("sslOn").lower() in {"on", "true", "1"},
|
||||
"sslCertFile": text("sslCertFile") or None,
|
||||
@@ -1386,10 +1418,18 @@ async function startRemoteApplyJob(config: UniDeskConfig, pg: PostgresHostConfig
|
||||
function remoteApplyPayload(pg: PostgresHostConfig, secrets: SecretInspection): Record<string, unknown> {
|
||||
const role = pg.objects.roles[0];
|
||||
const database = pg.objects.databases[0];
|
||||
const material = secrets.materials.get(role.passwordRef.sourceRef);
|
||||
if (material === undefined) throw new Error(`missing material for ${role.passwordRef.sourceRef}`);
|
||||
const dbPassword = material.values[role.passwordRef.key];
|
||||
if (dbPassword === undefined || dbPassword.length === 0) throw new Error(`missing ${role.passwordRef.key}`);
|
||||
const roles = pg.objects.roles.map((item) => {
|
||||
const material = secrets.materials.get(item.passwordRef.sourceRef);
|
||||
if (material === undefined) throw new Error(`missing material for ${item.passwordRef.sourceRef}`);
|
||||
const password = material.values[item.passwordRef.key];
|
||||
if (password === undefined || password.length === 0) throw new Error(`missing ${item.passwordRef.key}`);
|
||||
return {
|
||||
name: item.name,
|
||||
password,
|
||||
login: item.login,
|
||||
attributes: item.attributes,
|
||||
};
|
||||
});
|
||||
return {
|
||||
clusterId: pg.metadata.id,
|
||||
pgVersion: pg.postgres.package.version,
|
||||
@@ -1404,11 +1444,13 @@ function remoteApplyPayload(pg: PostgresHostConfig, secrets: SecretInspection):
|
||||
passwordEncryption: pg.postgres.auth.passwordEncryption,
|
||||
role: {
|
||||
name: role.name,
|
||||
password: dbPassword,
|
||||
password: roles[0].password,
|
||||
login: role.login,
|
||||
attributes: role.attributes,
|
||||
},
|
||||
roles,
|
||||
database,
|
||||
databases: pg.objects.databases,
|
||||
backup: pg.backup.logicalDump,
|
||||
hbaMarkers: { start: managedHbaStart, end: managedHbaEnd },
|
||||
};
|
||||
@@ -1556,24 +1598,27 @@ if name == "pg_hba":
|
||||
print(f'{rule["type"]} {rule["database"]} {rule["user"]} {rule["address"]} {rule["method"]}')
|
||||
print(markers["end"])
|
||||
elif name == "sql":
|
||||
role = data["role"]
|
||||
name = role["name"]
|
||||
password = role["password"].replace("'", "''")
|
||||
attrs = []
|
||||
attrs.append("LOGIN" if role.get("login") else "NOLOGIN")
|
||||
attrs.append("CREATEDB" if role["attributes"].get("createdb") else "NOCREATEDB")
|
||||
attrs.append("CREATEROLE" if role["attributes"].get("createrole") else "NOCREATEROLE")
|
||||
attrs.append("SUPERUSER" if role["attributes"].get("superuser") else "NOSUPERUSER")
|
||||
attr_sql = " ".join(attrs)
|
||||
print("DO $unidesk$")
|
||||
print("BEGIN")
|
||||
print(f" IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{name}') THEN")
|
||||
print(f" CREATE ROLE {name} {attr_sql} PASSWORD '{password}';")
|
||||
print(" ELSE")
|
||||
print(f" ALTER ROLE {name} WITH {attr_sql} PASSWORD '{password}';")
|
||||
print(" END IF;")
|
||||
print("END")
|
||||
print("$unidesk$;")
|
||||
for role in data.get("roles", [data["role"]]):
|
||||
name = role["name"]
|
||||
password = role["password"].replace("'", "''")
|
||||
attrs = []
|
||||
attrs.append("LOGIN" if role.get("login") else "NOLOGIN")
|
||||
attrs.append("CREATEDB" if role["attributes"].get("createdb") else "NOCREATEDB")
|
||||
attrs.append("CREATEROLE" if role["attributes"].get("createrole") else "NOCREATEROLE")
|
||||
attrs.append("SUPERUSER" if role["attributes"].get("superuser") else "NOSUPERUSER")
|
||||
attr_sql = " ".join(attrs)
|
||||
print("DO $unidesk$")
|
||||
print("BEGIN")
|
||||
print(f" IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{name}') THEN")
|
||||
print(f" CREATE ROLE {name} {attr_sql} PASSWORD '{password}';")
|
||||
print(" ELSE")
|
||||
print(f" ALTER ROLE {name} WITH {attr_sql} PASSWORD '{password}';")
|
||||
print(" END IF;")
|
||||
print("END")
|
||||
print("$unidesk$;")
|
||||
elif name == "databases":
|
||||
for db in data.get("databases", [data["database"]]):
|
||||
print("\t".join([db["name"], db["owner"], db["encoding"], db["locale"]]))
|
||||
elif name == "backup_script":
|
||||
backup = data["backup"]
|
||||
role = data["role"]["name"]
|
||||
@@ -1704,9 +1749,14 @@ render_file sql > "$sql"
|
||||
runuser -u postgres -- psql -v ON_ERROR_STOP=1 < "$sql"
|
||||
|
||||
write_state running create-database
|
||||
if ! runuser -u postgres -- psql -Atqc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1; then
|
||||
runuser -u postgres -- createdb -O "$DB_OWNER" -E "$DB_ENCODING" --locale="$DB_LOCALE" --template=template0 "$DB_NAME"
|
||||
fi
|
||||
databases_tsv="$job_dir/databases.tsv"
|
||||
render_file databases > "$databases_tsv"
|
||||
while IFS=' ' read -r db_name db_owner db_encoding db_locale; do
|
||||
[ -n "$db_name" ] || continue
|
||||
if ! runuser -u postgres -- psql -Atqc "SELECT 1 FROM pg_database WHERE datname='$db_name'" | grep -q 1; then
|
||||
runuser -u postgres -- createdb -O "$db_owner" -E "$db_encoding" --locale="$db_locale" --template=template0 "$db_name"
|
||||
fi
|
||||
done < "$databases_tsv"
|
||||
|
||||
if [ "$BACKUP_ENABLED" = "true" ]; then
|
||||
write_state running configure-backup
|
||||
@@ -1739,8 +1789,16 @@ fi
|
||||
|
||||
write_state running final-check
|
||||
runuser -u postgres -- psql -Atqc "SHOW server_version;" >/dev/null
|
||||
runuser -u postgres -- psql -Atqc "SELECT 1 FROM pg_roles WHERE rolname='$DB_OWNER'" | grep -q 1
|
||||
runuser -u postgres -- psql -Atqc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1
|
||||
python3 - "$payload" <<'PY' > "$job_dir/final-check.sql"
|
||||
import json, sys
|
||||
data=json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
for role in data.get("roles", [data["role"]]):
|
||||
print("SELECT 'role', %r, EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %r);" % (role["name"], role["name"]))
|
||||
for db in data.get("databases", [data["database"]]):
|
||||
print("SELECT 'database', %r, EXISTS (SELECT 1 FROM pg_database WHERE datname = %r);" % (db["name"], db["name"]))
|
||||
PY
|
||||
chmod 0644 "$job_dir/final-check.sql"
|
||||
runuser -u postgres -- psql -Atq < "$job_dir/final-check.sql" | awk -F'|' '{ if ($3 != "t") exit 1 }'
|
||||
write_state succeeded complete 0
|
||||
trap - EXIT
|
||||
exit 0
|
||||
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo=${UNIDESK_TRAN_REPO_ROOT:-/root/unidesk}
|
||||
if [ ! -f "$repo/scripts/cli.ts" ]; then
|
||||
self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repo=$(CDPATH= cd -- "$self_dir/.." && pwd)
|
||||
self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
self_repo=$(CDPATH= cd -- "$self_dir/.." && pwd)
|
||||
repo=${UNIDESK_TRAN_REPO_ROOT:-$self_repo}
|
||||
if [ ! -f "$repo/scripts/cli.ts" ] && [ -f /root/unidesk/scripts/cli.ts ]; then
|
||||
repo=/root/unidesk
|
||||
fi
|
||||
|
||||
tran_timeout_seconds() {
|
||||
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo=${UNIDESK_TRANS_REPO_ROOT:-/root/unidesk}
|
||||
if [ ! -f "$repo/scripts/cli.ts" ]; then
|
||||
self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repo=$(CDPATH= cd -- "$self_dir/.." && pwd)
|
||||
self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
self_repo=$(CDPATH= cd -- "$self_dir/.." && pwd)
|
||||
repo=${UNIDESK_TRANS_REPO_ROOT:-$self_repo}
|
||||
if [ ! -f "$repo/scripts/cli.ts" ] && [ -f /root/unidesk/scripts/cli.ts ]; then
|
||||
repo=/root/unidesk
|
||||
fi
|
||||
|
||||
exec bun "$repo/scripts/cli.ts" ssh "$@"
|
||||
|
||||
Reference in New Issue
Block a user