feat: deploy decision center on nc01 k8s
This commit is contained in:
@@ -82,6 +82,28 @@ function envFromConfig(configName, keyName, envName) {
|
||||
].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");
|
||||
@@ -105,6 +127,7 @@ 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");
|
||||
@@ -128,7 +151,6 @@ 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 runtimeConfigFingerprint = sha(JSON.stringify(runtimeConfig));
|
||||
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");
|
||||
@@ -140,6 +162,74 @@ if (secrets[secretKeys.databaseUrl] !== `postgres://${secrets[secretKeys.postgre
|
||||
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")}`),
|
||||
};
|
||||
}
|
||||
|
||||
const decisionCenterDatabase = readDecisionCenterDatabase(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
|
||||
@@ -186,9 +276,25 @@ data:
|
||||
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(required(runtimeConfig.microservicesJson, "runtimeConfig.microservicesJson"))}
|
||||
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 (databaseMode === "k8s-postgres") {
|
||||
docs.push(`apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
@@ -409,6 +515,114 @@ ${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/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"))}
|
||||
volumeMounts:
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk
|
||||
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: {}`);
|
||||
}
|
||||
docs.push(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
|
||||
Reference in New Issue
Block a user