#!/usr/bin/env bun import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; function option(name) { const index = process.argv.indexOf(name); if (index === -1) return null; const value = process.argv[index + 1]; if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`); 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 required(value, path) { if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string`); return value; } function positiveInteger(value, path) { if (!Number.isInteger(value) || value < 1) throw new Error(`${path} must be a positive integer`); return value; } function booleanValue(value, path) { if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`); return value; } function stringValue(value, path) { if (typeof value !== "string") throw new Error(`${path} must be a string`); return value; } function valueAt(root, reference) { return reference.split(".").reduce((value, key) => record(value, reference)[key], root); } function sha(value) { return createHash("sha256").update(value).digest("hex").slice(0, 16); } function secretEnv(name, secretName, secretKey) { return { name, valueFrom: { secretKeyRef: { name: secretName, key: secretKey } } }; } function main() { const configPath = option("--config") ?? "config/unidesk-host-k8s.yaml"; const serviceRef = required(option("--service-ref"), "--service-ref"); const image = required(option("--image"), "--image"); const sourceCommit = required(option("--source-commit"), "--source-commit"); if (!/^services\.[A-Za-z][A-Za-z0-9]*$/u.test(serviceRef)) throw new Error("--service-ref must identify one services. entry"); if (!/@sha256:[0-9a-f]{64}$/u.test(image)) throw new Error("--image must be digest pinned"); if (!/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error("--source-commit must be a full Git commit SHA"); const config = record(Bun.YAML.parse(readFileSync(configPath, "utf8")), configPath); const target = record(config.target, "target"); const runtime = record(config.runtime, "runtime"); const images = record(config.images, "images"); const service = record(valueAt(config, serviceRef), serviceRef); const database = record(service.database, `${serviceRef}.database`); const storage = record(service.storage, `${serviceRef}.storage`); const repository = record(service.repository, `${serviceRef}.repository`); const reminder = record(service.reminder, `${serviceRef}.reminder`); const reminderTransport = record(reminder.transport, `${serviceRef}.reminder.transport`); const resources = record(service.resources, `${serviceRef}.resources`); const github = storage.primary === "github-repo" ? record(storage.github, `${serviceRef}.storage.github`) : null; const githubSsh = github === null ? null : record(github.ssh, `${serviceRef}.storage.github.ssh`); const privateKey = githubSsh === null ? null : record(githubSsh.privateKey, `${serviceRef}.storage.github.ssh.privateKey`); const knownHosts = githubSsh === null ? null : record(githubSsh.knownHosts, `${serviceRef}.storage.github.ssh.knownHosts`); const namespace = required(target.namespace, "target.namespace"); const serviceId = required(service.serviceId, `${serviceRef}.serviceId`); const serviceName = required(service.serviceName, `${serviceRef}.serviceName`); const deploymentName = required(service.deploymentName, `${serviceRef}.deploymentName`); const containerName = required(service.containerName, `${serviceRef}.containerName`); const containerPort = positiveInteger(service.containerPort, `${serviceRef}.containerPort`); const databaseSecretName = required(database.secretName, `${serviceRef}.database.secretName`); const databaseSecretKey = required(database.secretKey, `${serviceRef}.database.secretKey`); const storageEnvPrefix = required(service.storageEnvPrefix, `${serviceRef}.storageEnvPrefix`); const reminderTransportType = required(reminderTransport.type, `${serviceRef}.reminder.transport.type`); if (reminderTransportType !== "claudeqq") throw new Error(`${serviceRef}.reminder.transport.type must be claudeqq`); const reminderTargetType = required(reminderTransport.targetType, `${serviceRef}.reminder.transport.targetType`); if (reminderTargetType !== "private" && reminderTargetType !== "group") throw new Error(`${serviceRef}.reminder.transport.targetType must be private or group`); const reminderUserId = stringValue(reminderTransport.userId, `${serviceRef}.reminder.transport.userId`); const reminderGroupId = stringValue(reminderTransport.groupId, `${serviceRef}.reminder.transport.groupId`); if (booleanValue(reminder.enabled, `${serviceRef}.reminder.enabled`) && reminderTargetType === "private" && reminderUserId.length === 0) throw new Error(`${serviceRef}.reminder.transport.userId is required for private reminders`); if (reminder.enabled && reminderTargetType === "group" && reminderGroupId.length === 0) throw new Error(`${serviceRef}.reminder.transport.groupId is required for group reminders`); const configuredImage = required(valueAt(config, required(service.imageRef, `${serviceRef}.imageRef`)), service.imageRef); const configFingerprint = sha(JSON.stringify({ target, runtime, images, serviceRef, service, configuredImage })); const commonLabels = { "app.kubernetes.io/name": serviceId, "app.kubernetes.io/part-of": "unidesk", "app.kubernetes.io/managed-by": "argocd", "unidesk.ai/deployment": required(record(config.metadata, "metadata").name, "metadata.name"), "unidesk.ai/target": required(target.id, "target.id"), }; const env = [ { name: "HOST", value: "0.0.0.0" }, { name: "PORT", value: String(containerPort) }, secretEnv("DATABASE_URL", databaseSecretName, databaseSecretKey), { name: "DATABASE_POOL_MAX", value: required(record(config.runtimeConfig, "runtimeConfig").databasePoolMax, "runtimeConfig.databasePoolMax") }, { name: "UNIDESK_DEPLOY_SERVICE_ID", value: serviceId }, { name: "UNIDESK_DEPLOY_REPO", value: required(repository.url, `${serviceRef}.repository.url`) }, { name: "UNIDESK_DEPLOY_COMMIT", value: sourceCommit }, { name: "UNIDESK_DEPLOY_REQUESTED_COMMIT", value: sourceCommit }, { name: "LOG_FILE", value: required(service.logFile, `${serviceRef}.logFile`) }, { name: `${storageEnvPrefix}_STORAGE_PRIMARY`, value: required(storage.primary, `${serviceRef}.storage.primary`) }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED", value: String(reminder.enabled) }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL", value: required(reminderTransport.baseUrl, `${serviceRef}.reminder.transport.baseUrl`) }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE", value: reminderTargetType }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_USER_ID", value: reminderUserId }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_GROUP_ID", value: reminderGroupId }, { name: "TODO_NOTE_REMINDER_LEAD_MINUTES", value: String(positiveInteger(reminder.leadMinutes, `${serviceRef}.reminder.leadMinutes`)) }, { name: "TODO_NOTE_REMINDER_SCAN_INTERVAL_MS", value: String(positiveInteger(reminder.scanIntervalMs, `${serviceRef}.reminder.scanIntervalMs`)) }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS", value: String(positiveInteger(reminderTransport.timeoutMs, `${serviceRef}.reminder.transport.timeoutMs`)) }, { name: "TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS", value: String(positiveInteger(reminderTransport.sendAttempts, `${serviceRef}.reminder.transport.sendAttempts`)) }, ]; const volumeMounts = [{ name: "logs", mountPath: "/var/log/unidesk" }]; const volumes = [{ name: "logs", emptyDir: {} }]; if (github !== null && githubSsh !== null && privateKey !== null && knownHosts !== null) { const mountPath = required(githubSsh.mountPath, `${serviceRef}.storage.github.ssh.mountPath`); const privateKeyName = required(privateKey.secretKey, `${serviceRef}.storage.github.ssh.privateKey.secretKey`); const knownHostsName = required(knownHosts.secretKey, `${serviceRef}.storage.github.ssh.knownHosts.secretKey`); env.push( { name: `${storageEnvPrefix}_GIT_REPO`, value: required(github.repo, `${serviceRef}.storage.github.repo`) }, { name: `${storageEnvPrefix}_GIT_REPO_SSH_URL`, value: required(github.sshUrl, `${serviceRef}.storage.github.sshUrl`) }, { name: `${storageEnvPrefix}_GIT_BRANCH`, value: required(github.branch, `${serviceRef}.storage.github.branch`) }, { name: `${storageEnvPrefix}_GIT_BASE_PATH`, value: required(github.basePath, `${serviceRef}.storage.github.basePath`) }, { name: `${storageEnvPrefix}_GIT_WORKTREE`, value: required(github.worktreePath, `${serviceRef}.storage.github.worktreePath`) }, { name: `${storageEnvPrefix}_GIT_AUTHOR_NAME`, value: required(record(github.author, `${serviceRef}.storage.github.author`).name, `${serviceRef}.storage.github.author.name`) }, { name: `${storageEnvPrefix}_GIT_AUTHOR_EMAIL`, value: required(record(github.author, `${serviceRef}.storage.github.author`).email, `${serviceRef}.storage.github.author.email`) }, { name: `${storageEnvPrefix}_GIT_COMMIT_PREFIX`, value: required(github.commitMessagePrefix, `${serviceRef}.storage.github.commitMessagePrefix`) }, { name: "GIT_SSH_COMMAND", value: `ssh -i ${mountPath}/${privateKeyName} -o UserKnownHostsFile=${mountPath}/${knownHostsName} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes` }, ); volumeMounts.push({ name: "github-ssh", mountPath, readOnly: true }); volumes.push({ name: "github-ssh", secret: { secretName: required(githubSsh.secretName, `${serviceRef}.storage.github.ssh.secretName`), defaultMode: 256 } }); } const objects = [ { apiVersion: "v1", kind: "Service", metadata: { name: serviceName, namespace, labels: commonLabels }, spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": serviceId }, ports: [{ name: "http", port: containerPort, targetPort: "http" }], }, }, { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: deploymentName, namespace, labels: commonLabels }, spec: { replicas: positiveInteger(service.replicas, `${serviceRef}.replicas`), selector: { matchLabels: { "app.kubernetes.io/name": serviceId } }, template: { metadata: { labels: commonLabels, annotations: { "unidesk.ai/source-commit": sourceCommit, "unidesk.ai/config-fingerprint": `sha256:${configFingerprint}`, }, }, spec: { initContainers: [{ name: "wait-database", image: required(images.postgres, "images.postgres"), imagePullPolicy: "IfNotPresent", env: [secretEnv("DATABASE_URL", databaseSecretName, databaseSecretKey)], 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: containerName, image, imagePullPolicy: "IfNotPresent", ports: [{ name: "http", containerPort }], env, volumeMounts, readinessProbe: { httpGet: { path: required(service.healthPath, `${serviceRef}.healthPath`), port: "http" }, periodSeconds: 5, timeoutSeconds: 3, failureThreshold: 18 }, livenessProbe: { httpGet: { path: required(service.livePath, `${serviceRef}.livePath`), port: "http" }, periodSeconds: 10, timeoutSeconds: 3, failureThreshold: 6 }, startupProbe: { httpGet: { path: required(service.livePath, `${serviceRef}.livePath`), port: "http" }, periodSeconds: 5, timeoutSeconds: 3, failureThreshold: 30 }, resources, }], volumes, }, }, }, }, ]; process.stdout.write(`${objects.map((object) => Bun.YAML.stringify(object).trim()).join("\n---\n")}\n`); } if (!process.execArgv.includes("--check")) main();