810 lines
34 KiB
JavaScript
Executable File
810 lines
34 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
import { readFileSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { resolve } from "node:path";
|
|
|
|
function required(value, path) {
|
|
if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string`);
|
|
return value;
|
|
}
|
|
|
|
function record(value, path) {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
|
return value;
|
|
}
|
|
|
|
function positiveInteger(value, path) {
|
|
if (!Number.isInteger(value) || value < 1) throw new Error(`${path} must be a positive integer`);
|
|
return value;
|
|
}
|
|
|
|
function readEnvFile(path) {
|
|
const values = {};
|
|
const text = readFileSync(path, "utf8");
|
|
for (const [index, rawLine] of text.split(/\r?\n/u).entries()) {
|
|
const line = rawLine.trim();
|
|
if (line.length === 0 || line.startsWith("#")) continue;
|
|
const eq = line.indexOf("=");
|
|
if (eq <= 0) throw new Error(`${path}:${index + 1} must be KEY=value`);
|
|
values[line.slice(0, eq)] = line.slice(eq + 1);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function readLinePairCredential(path, usernameLine, passwordLine) {
|
|
const lines = readFileSync(path, "utf8").split(/\r?\n/u);
|
|
const username = (lines[usernameLine - 1] || "").trim();
|
|
const password = (lines[passwordLine - 1] || "").trim();
|
|
if (!username) throw new Error(`missing username line ${usernameLine} in ${path}`);
|
|
if (!password) throw new Error(`missing password line ${passwordLine} in ${path}`);
|
|
return { username, password };
|
|
}
|
|
|
|
function sha(value) {
|
|
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
}
|
|
|
|
function yamlScalar(value) {
|
|
return JSON.stringify(String(value));
|
|
}
|
|
|
|
function indent(text, spaces) {
|
|
const pad = " ".repeat(spaces);
|
|
return text.split("\n").map((line) => line.length > 0 ? `${pad}${line}` : line).join("\n");
|
|
}
|
|
|
|
function labels(config) {
|
|
const name = required(record(config.metadata, "metadata").name, "metadata.name");
|
|
return [
|
|
"app.kubernetes.io/part-of: unidesk",
|
|
`unidesk.ai/deployment: ${name}`,
|
|
`unidesk.ai/target: ${required(record(config.target, "target").id, "target.id")}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function envFromSecret(secretName, keyName, envName) {
|
|
return [
|
|
`- name: ${envName}`,
|
|
" valueFrom:",
|
|
" secretKeyRef:",
|
|
` name: ${secretName}`,
|
|
` key: ${keyName}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function envFromConfig(configName, keyName, envName) {
|
|
return [
|
|
`- name: ${envName}`,
|
|
" valueFrom:",
|
|
" configMapKeyRef:",
|
|
` name: ${configName}`,
|
|
` key: ${keyName}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function optionalRecord(value, path) {
|
|
if (value === undefined || value === null) return null;
|
|
return record(value, path);
|
|
}
|
|
|
|
function optionalString(value, fallback) {
|
|
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
}
|
|
|
|
function removeUrlSearchParam(value, paramName) {
|
|
if (typeof value !== "string" || value.length === 0) return value;
|
|
try {
|
|
const url = new URL(value);
|
|
url.searchParams.delete(paramName);
|
|
return url.toString();
|
|
} catch {
|
|
return value
|
|
.replace(new RegExp(`([?&])${paramName}=true(&|$)`, "g"), (_match, prefix, suffix) => (prefix === "?" && suffix ? "?" : ""))
|
|
.replace(/[?&]$/, "");
|
|
}
|
|
}
|
|
|
|
function resourceBlock(resources) {
|
|
const requests = record(record(resources, "resources").requests, "resources.requests");
|
|
const limits = record(resources.limits, "resources.limits");
|
|
return [
|
|
"resources:",
|
|
" requests:",
|
|
` cpu: ${yamlScalar(required(requests.cpu, "resources.requests.cpu"))}`,
|
|
` memory: ${yamlScalar(required(requests.memory, "resources.requests.memory"))}`,
|
|
" limits:",
|
|
...(limits.cpu ? [` cpu: ${yamlScalar(limits.cpu)}`] : []),
|
|
` memory: ${yamlScalar(required(limits.memory, "resources.limits.memory"))}`,
|
|
].join("\n");
|
|
}
|
|
|
|
const configPath = process.argv[2] ?? "config/unidesk-host-k8s.yaml";
|
|
const config = Bun.YAML.parse(readFileSync(configPath, "utf8"));
|
|
const target = record(config.target, "target");
|
|
const runtime = record(config.runtime, "runtime");
|
|
const images = record(config.images, "images");
|
|
const database = record(config.database, "database");
|
|
const services = record(config.services, "services");
|
|
const backend = record(services.backendCore, "services.backendCore");
|
|
const frontend = record(services.frontend, "services.frontend");
|
|
const decisionCenter = optionalRecord(services.decisionCenter, "services.decisionCenter");
|
|
const runtimeConfig = record(config.runtimeConfig, "runtimeConfig");
|
|
const runtimeAuth = record(config.runtimeAuth, "runtimeAuth");
|
|
const runtimeAuthSourceRef = record(runtimeAuth.sourceRef, "runtimeAuth.sourceRef");
|
|
const runtimeSecrets = record(config.runtimeSecrets, "runtimeSecrets");
|
|
const secretTarget = record(runtimeSecrets.target, "runtimeSecrets.target");
|
|
const secretKeys = record(secretTarget.keys, "runtimeSecrets.target.keys");
|
|
const namespace = required(target.namespace, "target.namespace");
|
|
const secretName = required(secretTarget.name, "runtimeSecrets.target.name");
|
|
const authSourcePath = resolve(required(runtimeAuthSourceRef.path, "runtimeAuth.sourceRef.path"));
|
|
if (required(runtimeAuthSourceRef.format, "runtimeAuth.sourceRef.format") !== "linePair") throw new Error("runtimeAuth.sourceRef.format must be linePair");
|
|
const authCredential = readLinePairCredential(
|
|
authSourcePath,
|
|
positiveInteger(runtimeAuthSourceRef.usernameLine, "runtimeAuth.sourceRef.usernameLine"),
|
|
positiveInteger(runtimeAuthSourceRef.passwordLine, "runtimeAuth.sourceRef.passwordLine"),
|
|
);
|
|
const sourcePath = resolve(required(record(runtimeSecrets.sourceRef, "runtimeSecrets.sourceRef").path, "runtimeSecrets.sourceRef.path"));
|
|
const secrets = readEnvFile(sourcePath);
|
|
secrets[secretKeys.authPassword] = authCredential.password;
|
|
const requiredSecretKeys = Object.values(secretKeys);
|
|
for (const key of requiredSecretKeys) {
|
|
if (typeof secrets[key] !== "string" || secrets[key].length === 0) throw new Error(`missing ${key} in ${sourcePath}`);
|
|
}
|
|
const secretFingerprint = sha(requiredSecretKeys.map((key) => `${key}=${secrets[key]}`).join("\n"));
|
|
const runtimeConfigName = "unidesk-runtime-config";
|
|
const databaseMode = database.mode === undefined ? "k8s-postgres" : required(database.mode, "database.mode");
|
|
if (databaseMode !== "k8s-postgres" && databaseMode !== "host-native") throw new Error("database.mode must be k8s-postgres or host-native");
|
|
const dbService = required(database.serviceName, "database.serviceName");
|
|
const dbHost = databaseMode === "host-native" ? required(database.host, "database.host") : `${dbService}.${namespace}.svc.cluster.local`;
|
|
const dbPort = required(String(database.port), "database.port");
|
|
const dbName = secrets[secretKeys.postgresDb];
|
|
if (secrets[secretKeys.databaseUrl] !== `postgres://${secrets[secretKeys.postgresUser]}:${secrets[secretKeys.postgresPassword]}@${dbHost}:${dbPort}/${dbName}`) {
|
|
throw new Error(`DATABASE_URL in ${sourcePath} must target ${dbHost}:${dbPort}/${dbName}`);
|
|
}
|
|
|
|
function readDecisionCenterDatabase(decision) {
|
|
if (decision === null) return null;
|
|
const database = record(decision.database, "services.decisionCenter.database");
|
|
const sourceRef = record(database.sourceRef, "services.decisionCenter.database.sourceRef");
|
|
const sourcePath = resolve(required(sourceRef.path, "services.decisionCenter.database.sourceRef.path"));
|
|
const sourceKey = required(sourceRef.key, "services.decisionCenter.database.sourceRef.key");
|
|
const sourceValues = readEnvFile(sourcePath);
|
|
const value = sourceValues[sourceKey];
|
|
if (typeof value !== "string" || value.length === 0) throw new Error(`missing ${sourceKey} in ${sourcePath}`);
|
|
return {
|
|
sourcePath,
|
|
sourceRefPath: required(sourceRef.path, "services.decisionCenter.database.sourceRef.path"),
|
|
sourceKey,
|
|
secretName: required(database.secretName, "services.decisionCenter.database.secretName"),
|
|
secretKey: required(database.secretKey, "services.decisionCenter.database.secretKey"),
|
|
value: removeUrlSearchParam(value, "uselibpqcompat"),
|
|
fingerprint: sha(`${sourceKey}=${removeUrlSearchParam(value, "uselibpqcompat")}`),
|
|
};
|
|
}
|
|
|
|
function readFileSource(sourceRef, path) {
|
|
const sourcePath = resolve(required(sourceRef.path, `${path}.path`));
|
|
const value = readFileSync(sourcePath, "utf8");
|
|
if (value.length === 0) throw new Error(`missing file content in ${sourcePath}`);
|
|
return {
|
|
sourcePath,
|
|
sourceRefPath: required(sourceRef.path, `${path}.path`),
|
|
value,
|
|
fingerprint: sha(value),
|
|
};
|
|
}
|
|
|
|
function readDecisionCenterStorage(decision) {
|
|
if (decision === null) return null;
|
|
const storage = optionalRecord(decision.storage, "services.decisionCenter.storage");
|
|
if (storage === null) return null;
|
|
const primary = required(storage.primary, "services.decisionCenter.storage.primary");
|
|
if (primary !== "postgres" && primary !== "github-repo") throw new Error("services.decisionCenter.storage.primary must be postgres or github-repo");
|
|
const github = primary === "github-repo" ? record(storage.github, "services.decisionCenter.storage.github") : null;
|
|
if (github === null) return { primary };
|
|
const ssh = record(github.ssh, "services.decisionCenter.storage.github.ssh");
|
|
const privateKey = record(ssh.privateKey, "services.decisionCenter.storage.github.ssh.privateKey");
|
|
const knownHosts = record(ssh.knownHosts, "services.decisionCenter.storage.github.ssh.knownHosts");
|
|
const privateKeySource = readFileSource(record(privateKey.sourceRef, "services.decisionCenter.storage.github.ssh.privateKey.sourceRef"), "services.decisionCenter.storage.github.ssh.privateKey.sourceRef");
|
|
const knownHostsSource = readFileSource(record(knownHosts.sourceRef, "services.decisionCenter.storage.github.ssh.knownHosts.sourceRef"), "services.decisionCenter.storage.github.ssh.knownHosts.sourceRef");
|
|
return {
|
|
primary,
|
|
github: {
|
|
repo: required(github.repo, "services.decisionCenter.storage.github.repo"),
|
|
sshUrl: required(github.sshUrl, "services.decisionCenter.storage.github.sshUrl"),
|
|
branch: required(github.branch, "services.decisionCenter.storage.github.branch"),
|
|
basePath: required(github.basePath, "services.decisionCenter.storage.github.basePath"),
|
|
worktreePath: required(github.worktreePath, "services.decisionCenter.storage.github.worktreePath"),
|
|
commitMessagePrefix: required(github.commitMessagePrefix, "services.decisionCenter.storage.github.commitMessagePrefix"),
|
|
author: {
|
|
name: required(record(github.author, "services.decisionCenter.storage.github.author").name, "services.decisionCenter.storage.github.author.name"),
|
|
email: required(record(github.author, "services.decisionCenter.storage.github.author").email, "services.decisionCenter.storage.github.author.email"),
|
|
},
|
|
ssh: {
|
|
secretName: required(ssh.secretName, "services.decisionCenter.storage.github.ssh.secretName"),
|
|
mountPath: required(ssh.mountPath, "services.decisionCenter.storage.github.ssh.mountPath"),
|
|
privateKeyKey: required(privateKey.secretKey, "services.decisionCenter.storage.github.ssh.privateKey.secretKey"),
|
|
knownHostsKey: required(knownHosts.secretKey, "services.decisionCenter.storage.github.ssh.knownHosts.secretKey"),
|
|
privateKeySource,
|
|
knownHostsSource,
|
|
fingerprint: sha(`${privateKeySource.fingerprint}\n${knownHostsSource.fingerprint}`),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const decisionCenterDatabase = readDecisionCenterDatabase(decisionCenter);
|
|
const decisionCenterStorage = readDecisionCenterStorage(decisionCenter);
|
|
const effectiveMicroservicesJson = decisionCenter === null
|
|
? required(runtimeConfig.microservicesJson, "runtimeConfig.microservicesJson")
|
|
: JSON.stringify([
|
|
{
|
|
id: "decision-center",
|
|
name: "Decision Center",
|
|
providerId: required(target.id, "target.id"),
|
|
description: "Decision Center is managed by the NC01 YAML-first k8s runtime for UniDesk decisions, requirements, meetings, and work diaries.",
|
|
repository: {
|
|
url: required(record(decisionCenter.repository, "services.decisionCenter.repository").url, "services.decisionCenter.repository.url"),
|
|
commitId: required(runtime.commit, "runtime.commit"),
|
|
dockerfile: required(record(decisionCenter.repository, "services.decisionCenter.repository").dockerfile, "services.decisionCenter.repository.dockerfile"),
|
|
composeFile: required(runtime.deployRef, "runtime.deployRef"),
|
|
composeService: required(decisionCenter.deploymentName, "services.decisionCenter.deploymentName"),
|
|
containerName: required(decisionCenter.containerName, "services.decisionCenter.containerName"),
|
|
},
|
|
backend: {
|
|
nodeBaseUrl: `http://${required(decisionCenter.serviceName, "services.decisionCenter.serviceName")}.${namespace}.svc.cluster.local:${required(String(decisionCenter.containerPort), "services.decisionCenter.containerPort")}`,
|
|
nodeBindHost: `${required(decisionCenter.serviceName, "services.decisionCenter.serviceName")}.${namespace}.svc.cluster.local`,
|
|
nodePort: positiveInteger(decisionCenter.containerPort, "services.decisionCenter.containerPort"),
|
|
proxyMode: "cluster-service-http",
|
|
frontendOnly: true,
|
|
public: false,
|
|
allowedMethods: ["GET", "HEAD", "POST", "PUT", "DELETE"],
|
|
allowedPathPrefixes: ["/health", "/live", "/logs", "/api/"],
|
|
healthPath: required(decisionCenter.healthPath, "services.decisionCenter.healthPath"),
|
|
timeoutMs: 30000,
|
|
},
|
|
development: {
|
|
providerId: required(target.id, "target.id"),
|
|
sshPassthrough: true,
|
|
worktreePath: required(record(decisionCenter.repository, "services.decisionCenter.repository").worktreePath, "services.decisionCenter.repository.worktreePath"),
|
|
},
|
|
frontend: {
|
|
route: required(record(decisionCenter.frontend, "services.decisionCenter.frontend").route, "services.decisionCenter.frontend.route"),
|
|
integrated: record(decisionCenter.frontend, "services.decisionCenter.frontend").integrated !== false,
|
|
},
|
|
deployment: {
|
|
mode: "internal-sidecar",
|
|
namespace,
|
|
expectedNodeIds: [required(target.id, "target.id")],
|
|
activeNodeId: required(target.id, "target.id"),
|
|
},
|
|
},
|
|
]);
|
|
const runtimeConfigFingerprint = sha(JSON.stringify({ ...runtimeConfig, microservicesJson: effectiveMicroservicesJson }));
|
|
|
|
const docs = [];
|
|
docs.push(`apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}`);
|
|
docs.push(`apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: ${secretName}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
annotations:
|
|
unidesk.ai/source-ref: ${yamlScalar(required(record(runtimeSecrets.sourceRef, "runtimeSecrets.sourceRef").path, "runtimeSecrets.sourceRef.path"))}
|
|
unidesk.ai/fingerprint: ${yamlScalar(secretFingerprint)}
|
|
type: Opaque
|
|
stringData:
|
|
${indent(requiredSecretKeys.map((key) => `${key}: ${yamlScalar(secrets[key])}`).join("\n"), 2)}`);
|
|
docs.push(`apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: ${runtimeConfigName}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
data:
|
|
UNIDESK_ENV: ${yamlScalar(required(runtime.environment, "runtime.environment"))}
|
|
UNIDESK_NAMESPACE: ${yamlScalar(namespace)}
|
|
UNIDESK_DEPLOY_REF: ${yamlScalar(required(runtime.deployRef, "runtime.deployRef"))}
|
|
UNIDESK_DEPLOY_REPO: ${yamlScalar(required(runtime.repository, "runtime.repository"))}
|
|
UNIDESK_DEPLOY_COMMIT: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
|
|
UNIDESK_DEPLOY_REQUESTED_COMMIT: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
|
|
UNIDESK_DATABASE_NAME: ${yamlScalar(dbName)}
|
|
UNIDESK_DATABASE_MODE: ${yamlScalar(databaseMode)}
|
|
UNIDESK_DATABASE_HOST: ${yamlScalar(dbHost)}
|
|
UNIDESK_DATABASE_PORT: ${yamlScalar(dbPort)}
|
|
DATABASE_VOLUME_NAME: ${yamlScalar(required(database.pvcName, "database.pvcName"))}
|
|
DATABASE_VOLUME_SIZE: ${yamlScalar(required(database.storageSize, "database.storageSize"))}
|
|
HEARTBEAT_TIMEOUT_MS: ${yamlScalar(required(runtimeConfig.heartBeatTimeoutMs, "runtimeConfig.heartBeatTimeoutMs"))}
|
|
TASK_PENDING_TIMEOUT_MS: ${yamlScalar(required(runtimeConfig.taskPendingTimeoutMs, "runtimeConfig.taskPendingTimeoutMs"))}
|
|
DATABASE_POOL_MAX: ${yamlScalar(required(runtimeConfig.databasePoolMax, "runtimeConfig.databasePoolMax"))}
|
|
SESSION_TTL_SECONDS: ${yamlScalar(required(runtimeConfig.sessionTtlSeconds, "runtimeConfig.sessionTtlSeconds"))}
|
|
AUTH_USERNAME: ${yamlScalar(authCredential.username)}
|
|
UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: ${yamlScalar(required(runtimeConfig.sshClientRouteAllowlist, "runtimeConfig.sshClientRouteAllowlist"))}
|
|
MICROSERVICES_JSON: ${yamlScalar(effectiveMicroservicesJson)}
|
|
NO_PROXY: ${yamlScalar(required(runtime.noProxy, "runtime.noProxy"))}
|
|
no_proxy: ${yamlScalar(required(runtime.noProxy, "runtime.noProxy"))}`);
|
|
if (decisionCenter !== null && decisionCenterDatabase !== null) {
|
|
docs.push(`apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: ${decisionCenterDatabase.secretName}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
app.kubernetes.io/name: decision-center
|
|
annotations:
|
|
unidesk.ai/source-ref: ${yamlScalar(decisionCenterDatabase.sourceRefPath)}
|
|
unidesk.ai/fingerprint: ${yamlScalar(decisionCenterDatabase.fingerprint)}
|
|
type: Opaque
|
|
stringData:
|
|
${decisionCenterDatabase.secretKey}: ${yamlScalar(decisionCenterDatabase.value)}`);
|
|
}
|
|
if (decisionCenterStorage?.github !== undefined) {
|
|
docs.push(`apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: ${decisionCenterStorage.github.ssh.secretName}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
app.kubernetes.io/name: decision-center
|
|
annotations:
|
|
unidesk.ai/private-key-source-ref: ${yamlScalar(decisionCenterStorage.github.ssh.privateKeySource.sourceRefPath)}
|
|
unidesk.ai/known-hosts-source-ref: ${yamlScalar(decisionCenterStorage.github.ssh.knownHostsSource.sourceRefPath)}
|
|
unidesk.ai/fingerprint: ${yamlScalar(decisionCenterStorage.github.ssh.fingerprint)}
|
|
type: Opaque
|
|
stringData:
|
|
${decisionCenterStorage.github.ssh.privateKeyKey}: ${yamlScalar(decisionCenterStorage.github.ssh.privateKeySource.value)}
|
|
${decisionCenterStorage.github.ssh.knownHostsKey}: ${yamlScalar(decisionCenterStorage.github.ssh.knownHostsSource.value)}`);
|
|
}
|
|
if (databaseMode === "k8s-postgres") {
|
|
docs.push(`apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: ${required(database.pvcName, "database.pvcName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: ${required(database.storageClassName, "database.storageClassName")}
|
|
resources:
|
|
requests:
|
|
storage: ${required(database.storageSize, "database.storageSize")}`);
|
|
docs.push(`apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: unidesk-db-init
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
data:
|
|
001_unidesk_init.sql: |
|
|
${indent(readFileSync("src/components/database/init/001_unidesk_init.sql", "utf8"), 4)}`);
|
|
docs.push(`apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ${dbService}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app.kubernetes.io/name: database
|
|
ports:
|
|
- name: pg
|
|
port: ${database.port}
|
|
targetPort: pg`);
|
|
docs.push(`apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: database
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: database
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: database
|
|
app.kubernetes.io/part-of: unidesk
|
|
annotations:
|
|
unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
|
|
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
|
spec:
|
|
securityContext:
|
|
fsGroup: 70
|
|
containers:
|
|
- name: postgres
|
|
image: ${required(images.postgres, "images.postgres")}
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: pg
|
|
containerPort: ${database.port}
|
|
env:
|
|
${indent(envFromSecret(secretName, secretKeys.postgresUser, "POSTGRES_USER"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.postgresPassword, "POSTGRES_PASSWORD"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.postgresDb, "POSTGRES_DB"), 12)}
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /var/lib/postgresql/data
|
|
- name: init
|
|
mountPath: /docker-entrypoint-initdb.d
|
|
readOnly: true
|
|
readinessProbe:
|
|
exec:
|
|
command: ["sh", "-ec", "pg_isready -U \\"$POSTGRES_USER\\" -d \\"$POSTGRES_DB\\""]
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 18
|
|
startupProbe:
|
|
exec:
|
|
command: ["sh", "-ec", "pg_isready -U \\"$POSTGRES_USER\\" -d \\"$POSTGRES_DB\\""]
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 60
|
|
${indent(resourceBlock(database.resources), 10)}
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: ${required(database.pvcName, "database.pvcName")}
|
|
- name: init
|
|
configMap:
|
|
name: unidesk-db-init`);
|
|
}
|
|
docs.push(`apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ${required(backend.serviceName, "services.backendCore.serviceName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app.kubernetes.io/name: backend-core
|
|
ports:
|
|
- name: http
|
|
port: ${backend.containerPort}
|
|
targetPort: http
|
|
- name: provider
|
|
port: ${backend.providerPort}
|
|
targetPort: provider
|
|
- name: provider-data
|
|
port: ${backend.providerDataPort}
|
|
targetPort: provider-data`);
|
|
docs.push(`apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ${required(backend.providerPublicServiceName, "services.backendCore.providerPublicServiceName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
type: LoadBalancer
|
|
selector:
|
|
app.kubernetes.io/name: backend-core
|
|
ports:
|
|
- name: provider
|
|
port: ${backend.providerPublicPort}
|
|
targetPort: provider
|
|
- name: provider-data
|
|
port: ${backend.providerDataPublicPort}
|
|
targetPort: provider-data`);
|
|
docs.push(`apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ${required(backend.deploymentName, "services.backendCore.deploymentName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: backend-core
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: backend-core
|
|
app.kubernetes.io/part-of: unidesk
|
|
annotations:
|
|
unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
|
|
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
|
spec:
|
|
initContainers:
|
|
- name: wait-database
|
|
image: ${required(images.postgres, "images.postgres")}
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: PGHOST
|
|
value: ${yamlScalar(dbHost)}
|
|
- name: PGPORT
|
|
value: ${yamlScalar(dbPort)}
|
|
${indent(envFromSecret(secretName, secretKeys.postgresUser, "PGUSER"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.postgresPassword, "PGPASSWORD"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.postgresDb, "PGDATABASE"), 12)}
|
|
command: ["sh", "-ec", "until pg_isready -h \\"$PGHOST\\" -p \\"$PGPORT\\" -U \\"$PGUSER\\" -d \\"$PGDATABASE\\" >/dev/null 2>&1; do sleep 2; done"]
|
|
containers:
|
|
- name: backend-core
|
|
image: ${required(images.backendCore, "images.backendCore")}
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: http
|
|
containerPort: ${backend.containerPort}
|
|
- name: provider
|
|
containerPort: ${backend.providerPort}
|
|
- name: provider-data
|
|
containerPort: ${backend.providerDataPort}
|
|
envFrom:
|
|
- configMapRef:
|
|
name: ${runtimeConfigName}
|
|
env:
|
|
- name: PORT
|
|
value: ${yamlScalar(backend.containerPort)}
|
|
- name: PROVIDER_PORT
|
|
value: ${yamlScalar(backend.providerPort)}
|
|
- name: PROVIDER_DATA_PORT
|
|
value: ${yamlScalar(backend.providerDataPort)}
|
|
${indent(envFromSecret(secretName, secretKeys.databaseUrl, "DATABASE_URL"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.providerToken, "PROVIDER_TOKEN"), 12)}
|
|
- name: LOG_FILE
|
|
value: ${yamlScalar(required(backend.logFile, "services.backendCore.logFile"))}
|
|
volumeMounts:
|
|
- name: logs
|
|
mountPath: /var/log/unidesk
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 20
|
|
startupProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 60
|
|
${indent(resourceBlock(backend.resources), 10)}
|
|
volumes:
|
|
- name: logs
|
|
emptyDir: {}`);
|
|
if (decisionCenter !== null) {
|
|
docs.push(`apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ${required(decisionCenter.serviceName, "services.decisionCenter.serviceName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
app.kubernetes.io/name: decision-center
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app.kubernetes.io/name: decision-center
|
|
ports:
|
|
- name: http
|
|
port: ${positiveInteger(decisionCenter.containerPort, "services.decisionCenter.containerPort")}
|
|
targetPort: http`);
|
|
docs.push(`apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ${required(decisionCenter.deploymentName, "services.decisionCenter.deploymentName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
app.kubernetes.io/name: decision-center
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: decision-center
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: decision-center
|
|
app.kubernetes.io/part-of: unidesk
|
|
annotations:
|
|
unidesk.ai/secret-fingerprint: ${yamlScalar(decisionCenterDatabase?.fingerprint ?? "")}
|
|
unidesk.ai/storage-secret-fingerprint: ${yamlScalar(decisionCenterStorage?.github?.ssh.fingerprint ?? "")}
|
|
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
|
spec:
|
|
initContainers:
|
|
- name: wait-database
|
|
image: ${required(images.postgres, "images.postgres")}
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: ${required(record(decisionCenter.database, "services.decisionCenter.database").secretName, "services.decisionCenter.database.secretName")}
|
|
key: ${required(record(decisionCenter.database, "services.decisionCenter.database").secretKey, "services.decisionCenter.database.secretKey")}
|
|
command: ["sh", "-ec", "db_url=\\"$(printf '%s' \\"$DATABASE_URL\\" | sed 's/[?&]uselibpqcompat=true//g')\\"; until psql \\"$db_url\\" -Atqc 'SELECT 1' >/dev/null 2>&1; do sleep 2; done"]
|
|
containers:
|
|
- name: ${required(decisionCenter.containerName, "services.decisionCenter.containerName")}
|
|
image: ${required(images.decisionCenter, "images.decisionCenter")}
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: http
|
|
containerPort: ${positiveInteger(decisionCenter.containerPort, "services.decisionCenter.containerPort")}
|
|
env:
|
|
- name: HOST
|
|
value: "0.0.0.0"
|
|
- name: PORT
|
|
value: ${yamlScalar(decisionCenter.containerPort)}
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: ${required(record(decisionCenter.database, "services.decisionCenter.database").secretName, "services.decisionCenter.database.secretName")}
|
|
key: ${required(record(decisionCenter.database, "services.decisionCenter.database").secretKey, "services.decisionCenter.database.secretKey")}
|
|
- name: DATABASE_POOL_MAX
|
|
value: "2"
|
|
- name: UNIDESK_DEPLOY_SERVICE_ID
|
|
value: "decision-center"
|
|
- name: UNIDESK_DEPLOY_REPO
|
|
value: ${yamlScalar(required(record(decisionCenter.repository, "services.decisionCenter.repository").url, "services.decisionCenter.repository.url"))}
|
|
- name: UNIDESK_DEPLOY_COMMIT
|
|
value: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
|
|
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
|
|
value: ${yamlScalar(required(runtime.commit, "runtime.commit"))}
|
|
- name: LOG_FILE
|
|
value: ${yamlScalar(required(decisionCenter.logFile, "services.decisionCenter.logFile"))}
|
|
- name: DECISION_CENTER_STORAGE_PRIMARY
|
|
value: ${yamlScalar(decisionCenterStorage?.primary ?? "postgres")}
|
|
${decisionCenterStorage?.github !== undefined ? indent(`- name: DECISION_CENTER_GIT_REPO_SSH_URL
|
|
value: ${yamlScalar(decisionCenterStorage.github.sshUrl)}
|
|
- name: DECISION_CENTER_GIT_BRANCH
|
|
value: ${yamlScalar(decisionCenterStorage.github.branch)}
|
|
- name: DECISION_CENTER_GIT_BASE_PATH
|
|
value: ${yamlScalar(decisionCenterStorage.github.basePath)}
|
|
- name: DECISION_CENTER_GIT_WORKTREE
|
|
value: ${yamlScalar(decisionCenterStorage.github.worktreePath)}
|
|
- name: DECISION_CENTER_GIT_AUTHOR_NAME
|
|
value: ${yamlScalar(decisionCenterStorage.github.author.name)}
|
|
- name: DECISION_CENTER_GIT_AUTHOR_EMAIL
|
|
value: ${yamlScalar(decisionCenterStorage.github.author.email)}
|
|
- name: DECISION_CENTER_GIT_COMMIT_PREFIX
|
|
value: ${yamlScalar(decisionCenterStorage.github.commitMessagePrefix)}
|
|
- name: GIT_SSH_COMMAND
|
|
value: ${yamlScalar(`ssh -i ${decisionCenterStorage.github.ssh.mountPath}/${decisionCenterStorage.github.ssh.privateKeyKey} -o UserKnownHostsFile=${decisionCenterStorage.github.ssh.mountPath}/${decisionCenterStorage.github.ssh.knownHostsKey} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes`)}`, 12) : ""}
|
|
volumeMounts:
|
|
- name: logs
|
|
mountPath: /var/log/unidesk
|
|
${decisionCenterStorage?.github !== undefined ? indent(`- name: github-ssh
|
|
mountPath: ${yamlScalar(decisionCenterStorage.github.ssh.mountPath)}
|
|
readOnly: true`, 12) : ""}
|
|
readinessProbe:
|
|
httpGet:
|
|
path: ${required(decisionCenter.healthPath, "services.decisionCenter.healthPath")}
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 18
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /live
|
|
port: http
|
|
periodSeconds: 10
|
|
timeoutSeconds: 3
|
|
failureThreshold: 6
|
|
startupProbe:
|
|
httpGet:
|
|
path: /live
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 30
|
|
${indent(resourceBlock(decisionCenter.resources), 10)}
|
|
volumes:
|
|
- name: logs
|
|
emptyDir: {}`);
|
|
if (decisionCenterStorage?.github !== undefined) {
|
|
docs[docs.length - 1] += `
|
|
- name: github-ssh
|
|
secret:
|
|
secretName: ${decisionCenterStorage.github.ssh.secretName}
|
|
defaultMode: 0400`;
|
|
}
|
|
}
|
|
docs.push(`apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ${required(frontend.serviceName, "services.frontend.serviceName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
type: LoadBalancer
|
|
selector:
|
|
app.kubernetes.io/name: frontend
|
|
ports:
|
|
- name: http
|
|
port: ${frontend.publicPort}
|
|
targetPort: http`);
|
|
docs.push(`apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ${required(frontend.deploymentName, "services.frontend.deploymentName")}
|
|
namespace: ${namespace}
|
|
labels:
|
|
${indent(labels(config), 4)}
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: frontend
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: frontend
|
|
app.kubernetes.io/part-of: unidesk
|
|
annotations:
|
|
unidesk.ai/secret-fingerprint: ${yamlScalar(secretFingerprint)}
|
|
unidesk.ai/runtime-config-fingerprint: ${yamlScalar(runtimeConfigFingerprint)}
|
|
spec:
|
|
containers:
|
|
- name: frontend
|
|
image: ${required(images.frontend, "images.frontend")}
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: http
|
|
containerPort: ${frontend.containerPort}
|
|
envFrom:
|
|
- configMapRef:
|
|
name: ${runtimeConfigName}
|
|
env:
|
|
- name: PORT
|
|
value: ${yamlScalar(frontend.containerPort)}
|
|
- name: CORE_INTERNAL_URL
|
|
value: ${yamlScalar(`http://${backend.serviceName}.${namespace}.svc.cluster.local:${backend.containerPort}`)}
|
|
- name: FRONTEND_PUBLIC_URL
|
|
value: ${yamlScalar(`http://${runtime.publicHost}:${frontend.publicPort}`)}
|
|
- name: PROVIDER_INGRESS_PUBLIC_URL
|
|
value: ${yamlScalar(`ws://${runtime.publicHost}:${backend.providerPort}/ws/provider`)}
|
|
${indent(envFromConfig(runtimeConfigName, "AUTH_USERNAME", "AUTH_USERNAME"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.authPassword, "AUTH_PASSWORD"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.providerToken, "PROVIDER_TOKEN"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.sshClientToken, "UNIDESK_SSH_CLIENT_TOKEN"), 12)}
|
|
${indent(envFromSecret(secretName, secretKeys.sessionSecret, "SESSION_SECRET"), 12)}
|
|
- name: LOG_FILE
|
|
value: ${yamlScalar(required(frontend.logFile, "services.frontend.logFile"))}
|
|
volumeMounts:
|
|
- name: logs
|
|
mountPath: /var/log/unidesk
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 20
|
|
startupProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: http
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 60
|
|
${indent(resourceBlock(frontend.resources), 10)}
|
|
volumes:
|
|
- name: logs
|
|
emptyDir: {}`);
|
|
|
|
process.stdout.write(`${docs.join("\n---\n")}\n`);
|