feat: move D601 HWLAB GitOps to UniDesk YAML

This commit is contained in:
Codex
2026-06-12 21:06:57 +00:00
parent 17b54c685a
commit d1448cf583
10 changed files with 3175 additions and 100 deletions
+31 -10
View File
@@ -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
+127
View File
@@ -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
+65 -1
View File
@@ -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
+171 -4
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 "$@"