From 465f4a626b4aebb79b59e40c09f064c568128536 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 17 May 2026 18:16:30 +0000 Subject: [PATCH] feat: add d601 dev core manifests --- AGENTS.md | 2 +- docs/reference/cli.md | 2 +- docs/reference/deploy.md | 15 ++ docs/reference/microservices.md | 10 + scripts/src/dev-env.ts | 17 +- src/components/backend-core/src/config.ts | 25 +- src/components/backend-core/src/egress-tcp.ts | 8 - src/components/backend-core/src/index.ts | 26 +- src/components/backend-core/src/types.ts | 12 + src/components/frontend/public/style.css | 30 +++ src/components/frontend/src/app.tsx | 30 ++- src/components/frontend/src/index.ts | 42 ++- .../k3s/dev/unidesk-dev-core.k8s.yaml | 248 ++++++++++++++++++ 13 files changed, 448 insertions(+), 19 deletions(-) create mode 100644 src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml diff --git a/AGENTS.md b/AGENTS.md index 56833185..33d3c371 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `bun scripts/cli.ts microservice list/status/health/diagnostics/tunnel-self-test/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,`proxy` 支持受控 JSON body,OA Event Flow/Todo Note/Baidu Netdisk/Code Queue Manager on main-server、k3s Control/Code Queue 执行面/MDTODO/Decision Center/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。 - `bun scripts/cli.ts decision upload/list/show/health`:通过 backend-core 用户服务代理上传会议记录/决议 Markdown、列出记录和查看详情;Decision Center 运行在 D601 k3s,规则见 `docs/reference/microservices.md`。 - `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json] [--service ]`:按根目录 `deploy.json` 的服务 repo 和 commit 期望状态校验或更新用户服务,目标侧自行 fetch、构建、部署和 live commit 验证;规则见 `docs/reference/deploy.md`。 -- `bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]`:离线校验 D601 `unidesk-dev` namespace 与 dev PostgreSQL 底座 manifest 的生产隔离护栏,规则见 `docs/reference/deploy.md` 与 `docs/reference/microservices.md`。 +- `bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]`:离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev backend/frontend manifest 的生产隔离护栏,规则见 `docs/reference/deploy.md` 与 `docs/reference/microservices.md`。 - `bun scripts/cli.ts ci install/status/run/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,只做每 commit 检查和 Code Queue 只读性能门禁,不部署 CD;规则见 `docs/reference/ci.md`。 - `bun scripts/cli.ts codex deploy `:Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build 与 live commit 验证路径;规则见 `docs/reference/codex-deploy.md`。 - `bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue ]`:通过 backend-core 私有代理提交 Code Queue 任务;控制面默认走主 server `code-queue-mgr` 写入 PostgreSQL,`--dry-run` 可只检查请求体不入队,规则见 `docs/reference/cli.md`。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 31c56693..90c055db 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -22,7 +22,7 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定 - `microservice list/status/health/diagnostics/tunnel-self-test/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 或 k3s 控制面中的用户服务(底层命令名仍为 microservice);`health`、`diagnostics`、`tunnel-self-test` 和 `proxy` 会走真实 backend-core -> provider-gateway 或 k3sctl-adapter -> 节点服务链路,`proxy` 支持受控 JSON 请求体并对超大响应 body 默认输出有界预览,规则见 `docs/reference/microservices.md`。 - `decision upload/list/show/health` 通过 backend-core 用户服务代理访问 D601 k3s Decision Center,用于上传会议记录/决议 Markdown、列出权威记录、查看详情和健康检查;它不得直连 D601 Service、NodePort 或 provider-gateway 业务 HTTP。 - `deploy check/plan/apply` 默认从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新直管服务与 k3s 代管服务;`deploy plan --env dev|prod` 在 Phase 0 只从固定 Git ref 读取 manifest 并输出 dry-run 环境计划,不使用本地 dirty worktree;规则见 `docs/reference/deploy.md`。 -- `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace 和 dev PostgreSQL 底座 manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml` 中所有 namespaced 对象都只落到 `unidesk-dev`,存在 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guard,且 dev `DATABASE_URL` 只能指向 `postgres-dev.unidesk-dev.../unidesk_dev`。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f `,仍不 apply 资源。 +- `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev backend/frontend manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml`;也可显式校验 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml`。所有 namespaced 对象必须只落到 `unidesk-dev`,foundation manifest 必须包含 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guard,core manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/Service。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f `,仍不 apply 资源。 - `codex deploy ` 是 Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build、k3s import、rollout 和 live commit 验证路径;详细规则见 `docs/reference/codex-deploy.md`。 - `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。提交确认和 dry-run 必须返回完整 prompt、字符数和 `truncated=false`,不能套用任务详情的预览截断策略,否则长任务 prompt 无法被人工验收。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQL;D601 scheduler 只轮询并执行已入库任务。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 diff --git a/docs/reference/deploy.md b/docs/reference/deploy.md index 339e0fc2..afa511b3 100644 --- a/docs/reference/deploy.md +++ b/docs/reference/deploy.md @@ -50,6 +50,21 @@ The manifest must not create, update, or delete production namespace resources, Phase 2 guardrails are deliberately limited to the dev manifest and CLI validator. Runtime startup guards for dev backend-core, Code Queue and Code Queue Manager must be reviewed and shipped as a separate change before dev workloads are exposed beyond dry-run or controlled apply. +On D601, dev/prod k3s verification must use the native k3s kubeconfig explicitly: `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`. The default `kubectl` context may point at Docker Desktop and is not an acceptable target for UniDesk k3s deploy validation. + +## D601 Dev Core + +Phase 3 introduces the dev backend/frontend manifest at `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml`. It may create only `backend-core-dev` and `frontend-dev` Deployment/Service objects in `unidesk-dev`. + +`backend-core-dev` must use `unidesk-dev-runtime-config` and `unidesk-dev-runtime-secrets`, connect to `postgres-dev.../unidesk_dev`, expose HTTP on 8080 and provider ingress on 8081, and write logs under `/var/log/unidesk-dev`. `frontend-dev` must set `CORE_INTERNAL_URL=http://backend-core-dev.unidesk-dev.svc.cluster.local:8080` and must not proxy to production backend-core. + +The manifest uses placeholder image tags and deploy commit values until `deploy apply --env dev` supports target-side dev builds. A controller or operator must replace those placeholders from `origin/deploy/dev:deploy.json` before real rollout. Client dry-run and static validation are the required checks before any controlled apply: + +- `bun scripts/cli.ts dev-env validate --manifest src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml` +- `KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl apply --dry-run=client --validate=false -f src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml` + +backend-core and frontend keep their production health payload shape by default. They add `environment`, `namespace`, `databaseName`, `serviceId`, `deployRef` and deploy commit metadata only when `UNIDESK_ENV=dev` or `UNIDESK_NAMESPACE=unidesk-dev` is set. The frontend shell shows a visible DEV ribbon only under the same dev identity. + ## CLI `bun scripts/cli.ts deploy check [--file deploy.json] [--service ]` checks the live runtime against the desired repo and commit without changing the system. diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index 2045c544..ded6ebf7 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -159,6 +159,16 @@ D601 开发环境底座只允许创建 `unidesk-dev` namespace 与 dev 专用对 验收入口:先运行 `bun scripts/cli.ts dev-env validate` 做静态资源与 DB URL 护栏检查;具备 D601 kubeconfig 时运行 `bun scripts/cli.ts dev-env validate --kubectl-dry-run` 做 Kubernetes client dry-run。若实际 apply,只能 apply 到 `unidesk-dev`,随后用 `kubectl -n unidesk-dev get pods,svc,pvc` 验证 dev DB ready,并对比 apply 前后的 `kubectl -n unidesk get deploy,sts,svc,secret,pvc -o name` 证明生产 workload 未变化。 +D601 上必须显式使用原生 k3s kubeconfig:`KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。默认 `kubectl` context 可能是 Docker Desktop,不能作为 UniDesk k3s deploy 或 dry-run 验收目标。 + +### D601 Dev Core Services + +`backend-core-dev` 与 `frontend-dev` 的第一版 manifest 固定为 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml`。该 manifest 只允许创建 `unidesk-dev` 内的 `backend-core-dev`、`frontend-dev` Deployment/Service;不得修改生产主 server Compose、生产 `unidesk` namespace 或生产 backend/frontend。 + +`backend-core-dev` 必须从 `unidesk-dev-runtime-config` 和 `unidesk-dev-runtime-secrets` 注入 dev-only 配置,使用 `postgres-dev.../unidesk_dev`、dev Provider token、dev log path 和 `UNIDESK_DEPLOY_REF=origin/deploy/dev`。`frontend-dev` 必须把 `CORE_INTERNAL_URL` 指向 `backend-core-dev.unidesk-dev.svc.cluster.local:8080`,页面在 dev identity 下显示 DEV 标记,`/health` 返回 dev namespace、database、service id、deploy ref 和 commit metadata。生产环境未设置 dev identity 时,backend-core 和 frontend health payload 保持生产兼容形状。 + +`unidesk-dev-core.k8s.yaml` 当前使用 placeholder image/commit;正式 rollout 需要后续 `deploy apply --env dev` executor 从 `origin/deploy/dev:deploy.json` 替换 commit 并构建镜像。当前验收只做静态校验和 Kubernetes client dry-run,不能把 placeholder manifest 当成已上线。 + ### Code Queue k3s-Managed 当前对外 `id=code-queue` 是稳定用户服务 ID,实际按 master 控制面与 D601 执行面拆分。队列管理、提交、历史摘要、已读状态和轻量 Trace 读取默认由主 server `code-queue-mgr` 直管 PostgreSQL;D601 k3s Code Queue 作为执行面代管,负责 scheduler/runner、dev-container、active run steer/interrupt、judge、输出/attempt/通知写回,并接入统一 `oa-event-flow` 发布 Trace/STEP 事实事件与读取统计中心: diff --git a/scripts/src/dev-env.ts b/scripts/src/dev-env.ts index d518070a..923fa410 100644 --- a/scripts/src/dev-env.ts +++ b/scripts/src/dev-env.ts @@ -5,7 +5,7 @@ import { repoRoot, rootPath } from "./config"; const defaultManifest = "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml"; const devNamespace = "unidesk-dev"; const prodNamespace = "unidesk"; -const requiredKinds = new Set([ +const foundationRequiredKinds = new Set([ "Namespace/unidesk-dev", "Secret/unidesk-dev-runtime-secrets", "ConfigMap/unidesk-dev-runtime-config", @@ -15,6 +15,12 @@ const requiredKinds = new Set([ "StatefulSet/postgres-dev", "Job/unidesk-dev-db-migrate", ]); +const coreRequiredKinds = new Set([ + "Service/backend-core-dev", + "Deployment/backend-core-dev", + "Service/frontend-dev", + "Deployment/frontend-dev", +]); interface ManifestDocument { index: number; @@ -81,6 +87,12 @@ function parseManifestDocuments(text: string): ManifestDocument[] { }); } +function requiredResourcesFor(resources: string[]): Set { + const resourceSet = new Set(resources); + if (resourceSet.has("Deployment/backend-core-dev") || resourceSet.has("Deployment/frontend-dev")) return coreRequiredKinds; + return foundationRequiredKinds; +} + function databaseUrls(text: string): string[] { const urls: string[] = []; const pattern = /postgres(?:ql)?:\/\/[^\s"']+/gu; @@ -131,7 +143,7 @@ function devEnvHelp(): Record { defaultManifest, checks: [ "all namespaced resources must target unidesk-dev", - "required dev namespace, postgres-dev, secret/config, guard, migration resources must exist", + "required foundation resources or backend-core-dev/frontend-dev resources must exist", "dev DATABASE_URL values must target postgres-dev/unidesk_dev and not production routes", "--kubectl-dry-run optionally asks kubectl to client-dry-run the manifest without applying it", ], @@ -148,6 +160,7 @@ export function runDevEnvCommand(args: string[]): unknown { const manifestText = readFileSync(manifestPath, "utf8"); const docs = parseManifestDocuments(manifestText); const resources = docs.map((doc) => `${doc.kind}/${doc.name}`); + const requiredKinds = requiredResourcesFor(resources); const namespacedViolations = docs .filter((doc) => doc.kind !== "Namespace") .filter((doc) => doc.namespace !== devNamespace) diff --git a/src/components/backend-core/src/config.ts b/src/components/backend-core/src/config.ts index 21662f05..b9d67d92 100644 --- a/src/components/backend-core/src/config.ts +++ b/src/components/backend-core/src/config.ts @@ -138,11 +138,34 @@ function readMicroservicesEnv(): MicroserviceConfig[] { return parsed.map(parseMicroserviceConfig); } +function optionalEnv(name: string): string { + return process.env[name]?.trim() ?? ""; +} + +function databaseNameFromUrl(databaseUrl: string): string { + try { + return new URL(databaseUrl).pathname.replace(/^\/+/u, ""); + } catch { + return ""; + } +} + export function readConfig(): RuntimeConfig { + const databaseUrl = requiredEnv("DATABASE_URL"); return { port: readNumberEnv("PORT"), providerPort: readNumberEnv("PROVIDER_PORT"), - databaseUrl: requiredEnv("DATABASE_URL"), + databaseUrl, + identity: { + environment: optionalEnv("UNIDESK_ENV") || "prod", + namespace: optionalEnv("UNIDESK_NAMESPACE"), + databaseName: optionalEnv("UNIDESK_DATABASE_NAME") || optionalEnv("UNIDESK_DEV_DATABASE_NAME") || databaseNameFromUrl(databaseUrl), + deployRef: optionalEnv("UNIDESK_DEPLOY_REF"), + serviceId: optionalEnv("UNIDESK_DEPLOY_SERVICE_ID") || "backend-core", + repo: optionalEnv("UNIDESK_DEPLOY_REPO"), + commit: optionalEnv("UNIDESK_DEPLOY_COMMIT"), + requestedCommit: optionalEnv("UNIDESK_DEPLOY_REQUESTED_COMMIT"), + }, providerToken: requiredEnv("PROVIDER_TOKEN"), heartbeatTimeoutMs: readNumberEnv("HEARTBEAT_TIMEOUT_MS"), taskPendingTimeoutMs: readOptionalNumberEnv("TASK_PENDING_TIMEOUT_MS", 10 * 60 * 1000), diff --git a/src/components/backend-core/src/egress-tcp.ts b/src/components/backend-core/src/egress-tcp.ts index 49269144..b9cad80d 100644 --- a/src/components/backend-core/src/egress-tcp.ts +++ b/src/components/backend-core/src/egress-tcp.ts @@ -146,11 +146,3 @@ export function closeEgressTcpConnectionsForSocket(provider: ProviderSocket): vo connection.socket.destroy(); } } - -export function closeEgressTcpConnectionsForSocket(provider: ProviderSocket): void { - for (const [key, connection] of ctx.activeEgressTcpConnections) { - if (connection.provider !== provider) continue; - ctx.activeEgressTcpConnections.delete(key); - connection.socket.destroy(); - } -} diff --git a/src/components/backend-core/src/index.ts b/src/components/backend-core/src/index.ts index 10080bfe..2728f775 100644 --- a/src/components/backend-core/src/index.ts +++ b/src/components/backend-core/src/index.ts @@ -42,6 +42,30 @@ ctx.sql = postgres(runtimeConfig.databaseUrl, { } })(); +function isDevIdentity(): boolean { + const identity = config().identity; + return identity.environment === "dev" || identity.namespace === "unidesk-dev"; +} + +function healthPayload(): Record { + const base = { ok: true, service: "unidesk-core", dbReady: ctx.dbReady, startedAt: ctx.serviceStartedAt.toISOString() }; + if (!isDevIdentity()) return base; + const identity = config().identity; + return { + ...base, + environment: identity.environment, + namespace: identity.namespace, + databaseName: identity.databaseName, + serviceId: identity.serviceId, + deployRef: identity.deployRef, + deploy: { + repo: identity.repo, + commit: identity.commit, + requestedCommit: identity.requestedCommit, + }, + }; +} + async function routeInner(req: Request, server: Server): Promise { const url = new URL(req.url); if (req.method === "OPTIONS") return jsonResponse({ ok: true }); @@ -49,7 +73,7 @@ async function routeInner(req: Request, server: Server): Promise= 7 ? text.slice(0, 7) : text || "unknown"; +} + function shellRefreshIntervalMs(moduleId: string, tabId: string): number { if (moduleId === "ops" && tabId === "status") return 5_000; if (moduleId === "nodes" && tabId === "monitor") return 5_000; @@ -515,8 +525,10 @@ function LoginScreen({ onLogin }: AnyRecord) { ); } -function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, activeStatusItems = [], onNotificationToggle, unreadCount = 0 }: AnyRecord) { +function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, activeStatusItems = [], onNotificationToggle, unreadCount = 0, environment = {} }: AnyRecord) { + const devMode = isDevEnvironment(environment); const statusItems = [ + ...(devMode ? [{ key: "environment", label: "环境", value: `${environment.namespace || "unidesk-dev"}`, tone: "warn" }] : []), { key: "core", label: "核心", value: connection.text, tone: connection.ok ? "ok" : "fail", testId: "conn-text" }, ...(Array.isArray(activeStatusItems) ? activeStatusItems : []), { key: "refresh", label: "刷新", value: lastRefresh ? fmtClock(lastRefresh) : "未刷新" }, @@ -524,7 +536,16 @@ function TopBar({ connection, lastRefresh, onRefresh, onLogout, session, clock, { key: "user", label: "用户", value: session?.user?.username || "--", tone: "user" }, ]; return h("header", { className: "topbar" }, - h("div", null, h("p", { className: "eyebrow" }, "Distributed Work Platform"), h("h1", null, "UniDesk 控制平面")), + h("div", null, + h("p", { className: "eyebrow" }, "Distributed Work Platform"), + h("h1", null, "UniDesk 控制平面"), + devMode ? h("div", { className: "dev-env-ribbon", "data-testid": "dev-environment-ribbon" }, + h("b", null, "DEV"), + h("span", null, environment.namespace || "unidesk-dev"), + h("span", null, environment.deployRef || "origin/deploy/dev"), + h("span", null, shortCommit(environment.commit || environment.requestedCommit)), + ) : null, + ), h(TopStatusBar, { className: "global-top-status", title: "状态", @@ -2350,10 +2371,11 @@ function Shell({ session, onLogout }: AnyRecord) { const { unreadCount, notifications } = useNotification(); const latestNotification = notifications.length > 0 ? notifications[notifications.length - 1] : null; - return h("div", { className: `shell ${railCollapsed ? "rail-collapsed" : ""}`, "data-testid": "app-shell" }, + const devMode = isDevEnvironment(environmentIdentity); + return h("div", { className: `shell ${railCollapsed ? "rail-collapsed" : ""} ${devMode ? "dev-shell" : ""}`, "data-testid": "app-shell" }, h(Sidebar, { activeModule, activeTabs, onNavigate: navigate, collapsed: railCollapsed, onToggle: () => setRailCollapsed((value: boolean) => !value) }), h("main", { className: "workspace" }, - h(TopBar, { connection, lastRefresh, onRefresh: refresh, onLogout: () => onLogout(true), session, clock, activeStatusItems, onNotificationToggle: () => setNotificationOpen((v: boolean) => !v), unreadCount }), + h(TopBar, { connection, lastRefresh, onRefresh: refresh, onLogout: () => onLogout(true), session, clock, activeStatusItems, onNotificationToggle: () => setNotificationOpen((v: boolean) => !v), unreadCount, environment: environmentIdentity }), h(TabBar, { module, activeTab, onNavigate: navigate }), h(LoadingContext.Provider, { value: refreshing }, h(WorkArea, { activeModule, activeTab, data: effectiveData, session, refresh, onRaw: openRaw, onNavigate: navigate }), diff --git a/src/components/frontend/src/index.ts b/src/components/frontend/src/index.ts index 82a528a4..bc2ddebf 100644 --- a/src/components/frontend/src/index.ts +++ b/src/components/frontend/src/index.ts @@ -14,6 +14,7 @@ interface RuntimeConfig { sessionSecret: string; sessionTtlSeconds: number; logFile: string; + environment: RuntimeIdentity; deploy: { serviceId: string; repo: string; @@ -22,6 +23,17 @@ interface RuntimeConfig { }; } +interface RuntimeIdentity { + environment: string; + namespace: string; + databaseName: string; + deployRef: string; + serviceId: string; + repo: string; + commit: string; + requestedCommit: string; +} + interface SessionPayload { username: string; expiresAt: number; @@ -62,6 +74,7 @@ const clientConfig = JSON.stringify({ authUsername: config.authUsername, sessionTtlSeconds: config.sessionTtlSeconds, apiBaseUrl: "/api", + environment: config.environment, }); const indexHtmlTemplate = readFileSync(join(publicDir, "index.html"), "utf8"); const indexHtmlRootMarker = '
'; @@ -149,6 +162,16 @@ function readConfig(): RuntimeConfig { sessionSecret: requiredEnv("SESSION_SECRET"), sessionTtlSeconds: readNumberEnv("SESSION_TTL_SECONDS"), logFile: requiredEnv("LOG_FILE"), + environment: { + environment: process.env.UNIDESK_ENV?.trim() || "prod", + namespace: process.env.UNIDESK_NAMESPACE?.trim() || "", + databaseName: process.env.UNIDESK_DATABASE_NAME?.trim() || process.env.UNIDESK_DEV_DATABASE_NAME?.trim() || "", + deployRef: process.env.UNIDESK_DEPLOY_REF?.trim() || "", + serviceId: process.env.UNIDESK_DEPLOY_SERVICE_ID?.trim() || "frontend", + repo: process.env.UNIDESK_DEPLOY_REPO?.trim() || "", + commit: process.env.UNIDESK_DEPLOY_COMMIT?.trim() || "", + requestedCommit: process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT?.trim() || "", + }, deploy: { serviceId: process.env.UNIDESK_DEPLOY_SERVICE_ID || "frontend", repo: process.env.UNIDESK_DEPLOY_REPO || "", @@ -158,6 +181,23 @@ function readConfig(): RuntimeConfig { }; } +function isDevIdentity(identity: RuntimeIdentity): boolean { + return identity.environment === "dev" || identity.namespace === "unidesk-dev"; +} + +function frontendHealthPayload(): Record { + const base = { ok: true, service: "unidesk-frontend", frontendPublicUrl: config.frontendPublicUrl, deploy: config.deploy }; + if (!isDevIdentity(config.environment)) return base; + return { + ...base, + environment: config.environment.environment, + namespace: config.environment.namespace, + databaseName: config.environment.databaseName, + serviceId: config.deploy.serviceId, + deployRef: config.environment.deployRef, + }; +} + function createLogger(service: string, logFile: string) { const writer = createHourlyJsonlWriter({ baseLogFile: logFile, @@ -723,7 +763,7 @@ async function handleRequest(req: Request): Promise { logger("debug", "request", { path: url.pathname }); try { if (url.pathname === "/health") { - return jsonResponse({ ok: true, service: "unidesk-frontend", frontendPublicUrl: config.frontendPublicUrl, deploy: config.deploy }); + return jsonResponse(frontendHealthPayload()); } if (url.pathname === "/login" && req.method === "POST") return login(req); if (url.pathname === "/logout" && req.method === "POST") return logout(); diff --git a/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml new file mode 100644 index 00000000..e2d96412 --- /dev/null +++ b/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml @@ -0,0 +1,248 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend-core-dev + namespace: unidesk-dev + labels: + app.kubernetes.io/name: backend-core + app.kubernetes.io/component: core + app.kubernetes.io/part-of: unidesk + unidesk.ai/environment: dev +spec: + selector: + app.kubernetes.io/name: backend-core + app.kubernetes.io/component: core + unidesk.ai/environment: dev + ports: + - name: http + port: 8080 + targetPort: http + - name: provider + port: 8081 + targetPort: provider +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-core-dev + namespace: unidesk-dev + labels: + app.kubernetes.io/name: backend-core + app.kubernetes.io/component: core + app.kubernetes.io/part-of: unidesk + unidesk.ai/environment: dev + annotations: + unidesk.ai/deploy-ref: origin/deploy/dev + unidesk.ai/image-source: deploy-dev-commit +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app.kubernetes.io/name: backend-core + app.kubernetes.io/component: core + unidesk.ai/environment: dev + template: + metadata: + labels: + app.kubernetes.io/name: backend-core + app.kubernetes.io/component: core + app.kubernetes.io/part-of: unidesk + unidesk.ai/environment: dev + unidesk.ai/node-id: D601 + spec: + nodeSelector: + unidesk.ai/node-id: D601 + terminationGracePeriodSeconds: 20 + containers: + - name: backend-core + image: unidesk-backend-core:dev-placeholder + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + - name: provider + containerPort: 8081 + envFrom: + - configMapRef: + name: unidesk-dev-runtime-config + - secretRef: + name: unidesk-dev-runtime-secrets + env: + - name: PORT + value: "8080" + - name: PROVIDER_PORT + value: "8081" + - name: UNIDESK_DATABASE_NAME + value: unidesk_dev + - name: UNIDESK_DEPLOY_SERVICE_ID + value: backend-core-dev + - name: UNIDESK_DEPLOY_REPO + value: https://github.com/pikasTech/unidesk + - name: UNIDESK_DEPLOY_COMMIT + value: replace-with-deploy-dev-commit + - name: UNIDESK_DEPLOY_REQUESTED_COMMIT + value: replace-with-deploy-dev-commit + - name: LOG_FILE + value: /var/log/unidesk-dev/backend-core-dev.jsonl + volumeMounts: + - name: logs + mountPath: /var/log/unidesk-dev + readinessProbe: + httpGet: + path: /health + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 20 + livenessProbe: + httpGet: + path: /health + port: http + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + startupProbe: + httpGet: + path: /health + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + resources: + requests: + cpu: 100m + memory: 192Mi + limits: + memory: 768Mi + volumes: + - name: logs + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend-dev + namespace: unidesk-dev + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: web + app.kubernetes.io/part-of: unidesk + unidesk.ai/environment: dev +spec: + selector: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: web + unidesk.ai/environment: dev + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-dev + namespace: unidesk-dev + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: web + app.kubernetes.io/part-of: unidesk + unidesk.ai/environment: dev + annotations: + unidesk.ai/deploy-ref: origin/deploy/dev + unidesk.ai/image-source: deploy-dev-commit +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: web + unidesk.ai/environment: dev + template: + metadata: + labels: + app.kubernetes.io/name: frontend + app.kubernetes.io/component: web + app.kubernetes.io/part-of: unidesk + unidesk.ai/environment: dev + unidesk.ai/node-id: D601 + spec: + nodeSelector: + unidesk.ai/node-id: D601 + terminationGracePeriodSeconds: 20 + containers: + - name: frontend + image: unidesk-frontend:dev-placeholder + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + envFrom: + - configMapRef: + name: unidesk-dev-runtime-config + - secretRef: + name: unidesk-dev-runtime-secrets + env: + - name: PORT + value: "8080" + - name: CORE_INTERNAL_URL + value: http://backend-core-dev.unidesk-dev.svc.cluster.local:8080 + - name: FRONTEND_PUBLIC_URL + value: http://frontend-dev.unidesk-dev.svc.cluster.local:8080 + - name: PROVIDER_INGRESS_PUBLIC_URL + value: ws://backend-core-dev.unidesk-dev.svc.cluster.local:8081/ws/provider + - name: UNIDESK_DATABASE_NAME + value: unidesk_dev + - name: UNIDESK_DEPLOY_SERVICE_ID + value: frontend-dev + - name: UNIDESK_DEPLOY_REPO + value: https://github.com/pikasTech/unidesk + - name: UNIDESK_DEPLOY_COMMIT + value: replace-with-deploy-dev-commit + - name: UNIDESK_DEPLOY_REQUESTED_COMMIT + value: replace-with-deploy-dev-commit + - name: LOG_FILE + value: /var/log/unidesk-dev/frontend-dev.jsonl + volumeMounts: + - name: logs + mountPath: /var/log/unidesk-dev + readinessProbe: + httpGet: + path: /health + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 20 + livenessProbe: + httpGet: + path: /health + port: http + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + startupProbe: + httpGet: + path: /health + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + resources: + requests: + cpu: 80m + memory: 128Mi + limits: + memory: 512Mi + volumes: + - name: logs + emptyDir: {}