diff --git a/config.json b/config.json index 42b314a2..c1f0c5a6 100644 --- a/config.json +++ b/config.json @@ -610,16 +610,65 @@ "integrated": true } }, + { + "id": "k3sctl-adapter-g14", + "name": "k3s Control Plane G14", + "providerId": "G14", + "description": "k3sctl-adapter-g14 是 UniDesk 直管的 G14 k3s 控制平面适配微服务,专用于承载迁移后的 Code Queue 执行面和基础设施 CI/CD,不接管仍保留在 D601 的用户服务。", + "repository": { + "url": "https://github.com/pikasTech/unidesk", + "commitId": "local", + "dockerfile": "src/components/microservices/k3sctl-adapter/Dockerfile", + "composeFile": "src/components/microservices/k3sctl-adapter/docker-compose.g14.yml", + "composeService": "k3sctl-adapter-g14", + "containerName": "k3sctl-adapter-g14" + }, + "backend": { + "nodeBaseUrl": "http://host.docker.internal:4266", + "nodeBindHost": "127.0.0.1", + "nodePort": 4266, + "proxyMode": "provider-gateway-http", + "frontendOnly": true, + "public": false, + "allowedMethods": [ + "GET", + "HEAD", + "POST", + "DELETE", + "PUT", + "PATCH" + ], + "allowedPathPrefixes": [ + "/health", + "/logs", + "/api/" + ], + "healthPath": "/health", + "timeoutMs": 30000 + }, + "deployment": { + "mode": "unidesk-direct" + }, + "development": { + "providerId": "G14", + "sshPassthrough": true, + "worktreePath": "/root/unidesk" + }, + "frontend": { + "route": "/apps/k3sctl-g14", + "integrated": false + } + }, { "id": "code-queue", "name": "Code Queue", - "providerId": "D601", - "description": "Code Queue 的用户服务 ID 保持稳定;队列管理、提交、历史和轻量 Trace 读取默认由主 server code-queue-mgr 直管 PostgreSQL,D601 k3s Code Queue 只负责 scheduler/runner、active run steer/interrupt 和执行态写回。", + "providerId": "G14", + "description": "Code Queue 的用户服务 ID 保持稳定;队列管理、提交、历史和轻量 Trace 读取默认由主 server code-queue-mgr 直管 PostgreSQL,G14 k3s Code Queue 负责 scheduler/runner、active run steer/interrupt 和执行态写回。D601 仍保留未迁移用户服务的 k3s adapter。", "repository": { "url": "https://github.com/pikasTech/unidesk", "commitId": "2a9f60d57401bf9d6165e44af30c2f21ada79320", "dockerfile": "src/components/microservices/code-queue/Dockerfile", - "composeFile": "src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json", + "composeFile": "src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k3s.json", "composeService": "code-queue", "containerName": "k3s:code-queue" }, @@ -647,9 +696,9 @@ "timeoutMs": 30000 }, "development": { - "providerId": "D601", + "providerId": "G14", "sshPassthrough": true, - "worktreePath": "/home/ubuntu/unidesk-code-queue-deploy" + "worktreePath": "/root/unidesk" }, "frontend": { "route": "/apps/code-queue", @@ -657,13 +706,13 @@ }, "deployment": { "mode": "k3sctl-managed", - "adapterServiceId": "k3sctl-adapter", + "adapterServiceId": "k3sctl-adapter-g14", "k3sServiceId": "code-queue", "namespace": "unidesk", "expectedNodeIds": [ - "D601" + "G14" ], - "activeNodeId": "D601" + "activeNodeId": "G14" } }, { diff --git a/scripts/src/check.ts b/scripts/src/check.ts index ac46b4e8..159bd825 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -115,11 +115,11 @@ export function checkHelp(): Record { { name: "--compose", description: "Render Docker Compose config." }, { name: "--logs", description: "Check unified log rotation policy." }, { name: "--recovery-guardrails", description: "Run D601 k3s/Code Queue reboot recovery diagnostics in read-only mode." }, - { name: "--rust", description: "Run cargo check only when UNIDESK_D601_RUST_CHECK=1 is set inside D601 CI/dev execution." }, + { name: "--rust", description: "Run cargo check only when UNIDESK_D601_RUST_CHECK=1 or UNIDESK_NATIVE_K3S_RUST_CHECK=1 is set inside an approved native k3s CI/dev execution." }, ], rustBoundary: { masterServer: "do not run cargo check/build here", - d601: "use deploy apply --env dev --service backend-core and CI with UNIDESK_D601_RUST_CHECK=1", + nativeK3sCi: "use deploy apply --env dev --service backend-core and CI with UNIDESK_NATIVE_K3S_RUST_CHECK=1", }, recoveryGuardrailsBoundary: { command: "bun scripts/cli.ts check recovery-guardrails", @@ -282,14 +282,16 @@ function codeQueueMgrHealthcheckItem(): CheckItem { } function rustCheckItem(): CheckItem { - if (process.env.UNIDESK_D601_RUST_CHECK !== "1") { + const rustCheckAllowed = process.env.UNIDESK_D601_RUST_CHECK === "1" || process.env.UNIDESK_NATIVE_K3S_RUST_CHECK === "1"; + if (!rustCheckAllowed) { return { name: "rust:backend-core", ok: false, detail: { skipped: true, - reason: "Rust compilation is intentionally not allowed on the master server; run it from D601 CI/dev deploy.", - enableOnD601: "UNIDESK_D601_RUST_CHECK=1 bun scripts/cli.ts check --rust", + reason: "Rust compilation is intentionally not allowed on the master server; run it from an approved native k3s CI/dev execution plane.", + enableOnNativeK3s: "UNIDESK_NATIVE_K3S_RUST_CHECK=1 bun scripts/cli.ts check --rust", + legacyEnableOnD601: "UNIDESK_D601_RUST_CHECK=1 bun scripts/cli.ts check --rust", deployPath: "bun scripts/cli.ts deploy apply --env dev --service backend-core", }, }; @@ -397,6 +399,9 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default fileItem("src/components/microservices/auth-broker/src/main.rs"), fileItem("scripts/artifact-consumer-dry-run-matrix-test.ts"), fileItem("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"), + fileItem("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml"), + fileItem("src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k3s.json"), + fileItem("src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml"), ); } else { items.push(skippedItem("files:required-entrypoints", "required file presence scan is opt-in", "--files or --full")); @@ -531,7 +536,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default if (options.rust) { items.push(rustCheckItem()); } else { - items.push(skippedItem("rust:backend-core", "Rust check/build must run through D601 CI artifact publication, not on the master server or CD runtime target", "--rust inside D601 CI with UNIDESK_D601_RUST_CHECK=1")); + items.push(skippedItem("rust:backend-core", "Rust check/build must run through an approved native k3s CI artifact publication, not on the master server or CD runtime target", "--rust inside native k3s CI with UNIDESK_NATIVE_K3S_RUST_CHECK=1")); } return { ok: items.every((item) => item.ok), mode: options.full ? "full" : "basic", options, items }; } diff --git a/scripts/src/ci.ts b/scripts/src/ci.ts index 010b837b..26e23947 100644 --- a/scripts/src/ci.ts +++ b/scripts/src/ci.ts @@ -27,6 +27,8 @@ const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789"; const ciCodeQueueImage = "unidesk-code-queue:dev"; const codeQueueDirectDockerBaseImage = "unidesk-code-queue:d601"; const providerDispatchCompletionLagMs = 45_000; +const defaultCiPipelineManifest = "src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"; +const g14CiPipelineManifest = "src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml"; const ciRuntimeImages = [ "rancher/mirrored-pause:3.6", "rancher/mirrored-library-busybox:1.36.1", @@ -40,10 +42,56 @@ const ciRuntimeImages = [ "alpine/git:2.45.2", ciCodeQueueImage, ]; + +function ciTarget(providerId: string | null): CiTarget { + const normalized = providerId ?? d601ProviderId; + if (normalized === d601ProviderId) { + return { + providerId: d601ProviderId, + kubeconfig: d601Kubeconfig, + hostCwd: "/home/ubuntu", + homeDir: "/home/ubuntu", + pipelineManifest: defaultCiPipelineManifest, + codeQueueImage: ciCodeQueueImage, + guardName: "d601_native_k3s_guard", + requiredNodeName: "d601", + }; + } + if (normalized === "G14") { + return { + providerId: "G14", + kubeconfig: d601Kubeconfig, + hostCwd: "/root", + homeDir: "/root", + pipelineManifest: g14CiPipelineManifest, + codeQueueImage: ciCodeQueueImage, + guardName: "g14_native_k3s_guard", + requiredNodeLabel: { key: "unidesk.ai/node-id", value: "G14" }, + }; + } + throw new Error(`ci --provider-id currently supports D601 or G14, got ${normalized}`); +} + +function providerIdOption(args: string[]): string | null { + return stringOption(args, "--provider-id") ?? stringOption(args, "--provider"); +} interface CiOptions { repoUrl: string; revision: string; waitMs: number; + target: CiTarget; +} + +interface CiTarget { + providerId: string; + kubeconfig: string; + hostCwd: string; + homeDir: string; + pipelineManifest: string; + codeQueueImage: string; + guardName: string; + requiredNodeName?: string; + requiredNodeLabel?: { key: string; value: string }; } interface CiPublishBackendCoreOptions { @@ -251,6 +299,70 @@ function shellQuote(value: string): string { return `'${value.replace(/'/gu, "'\\''")}'`; } +function ciTargetGuardShellLines(target: CiTarget, options: { passOutput?: "stdout" | "stderr" | "quiet" } = {}): string[] { + if (target.providerId === d601ProviderId) { + return d601K3sGuardShellLines(target.kubeconfig, options); + } + const passOutput = options.passOutput ?? "stdout"; + const labelSelector = target.requiredNodeLabel === undefined ? "" : `${target.requiredNodeLabel.key}=${target.requiredNodeLabel.value}`; + const passLine = passOutput === "quiet" + ? ":" + : passOutput === "stderr" + ? `printf '${target.guardName}=pass kubeconfig=%s context=%s server=%s nodes=%s\\n' "$required_kubeconfig" "$context" "$server" "$(printf '%s' "$nodes" | tr '\\n' ',')" >&2` + : `printf '${target.guardName}=pass kubeconfig=%s context=%s server=%s nodes=%s\\n' "$required_kubeconfig" "$context" "$server" "$(printf '%s' "$nodes" | tr '\\n' ',')"`; + return [ + `export KUBECONFIG=${shellQuote(target.kubeconfig)}`, + "unidesk_ci_k3s_guard() {", + ` required_kubeconfig=${shellQuote(target.kubeconfig)}`, + ` required_node=${shellQuote(target.requiredNodeName ?? "")}`, + ` required_selector=${shellQuote(labelSelector)}`, + ` guard_name=${shellQuote(target.guardName)}`, + " if [ \"${KUBECONFIG:-}\" != \"$required_kubeconfig\" ]; then", + " printf '%s=blocked reason=wrong-kubeconfig expected=%s actual=%s\\n' \"$guard_name\" \"$required_kubeconfig\" \"${KUBECONFIG:-}\" >&2", + " return 1", + " fi", + " if ! command -v kubectl >/dev/null 2>&1; then", + " printf '%s=blocked reason=kubectl-missing\\n' \"$guard_name\" >&2", + " return 1", + " fi", + " if ! context=$(kubectl config current-context 2>&1); then", + " printf '%s=blocked reason=context-read-failed detail=%s\\n' \"$guard_name\" \"$context\" >&2", + " return 1", + " fi", + " if ! server=$(kubectl config view --minify -o 'jsonpath={.clusters[0].cluster.server}' 2>&1); then", + " printf '%s=blocked reason=server-read-failed detail=%s\\n' \"$guard_name\" \"$server\" >&2", + " return 1", + " fi", + " if ! nodes=$(kubectl get nodes -o 'jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}' 2>&1); then", + " printf '%s=blocked reason=nodes-read-failed detail=%s\\n' \"$guard_name\" \"$nodes\" >&2", + " return 1", + " fi", + " combined=$(printf '%s\\n%s\\n%s\\n' \"$context\" \"$server\" \"$nodes\")", + " if printf '%s\\n' \"$combined\" | grep -Eiq 'docker-desktop|desktop-control-plane|127\\.0\\.0\\.1:11700'; then", + " printf '%s=refused reason=forbidden-control-plane context=%s server=%s nodes=%s\\n' \"$guard_name\" \"$context\" \"$server\" \"$(printf '%s' \"$nodes\" | tr '\\n' ',')\" >&2", + " return 1", + " fi", + " if [ -n \"$required_node\" ] && ! printf '%s\\n' \"$nodes\" | grep -Fx \"$required_node\" >/dev/null; then", + " printf '%s=blocked reason=missing-required-node expected=%s nodes=%s\\n' \"$guard_name\" \"$required_node\" \"$(printf '%s' \"$nodes\" | tr '\\n' ',')\" >&2", + " return 1", + " fi", + " if [ -n \"$required_selector\" ]; then", + " if ! labeled_nodes=$(kubectl get nodes -l \"$required_selector\" -o 'jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}' 2>&1); then", + " printf '%s=blocked reason=label-query-failed selector=%s detail=%s\\n' \"$guard_name\" \"$required_selector\" \"$labeled_nodes\" >&2", + " return 1", + " fi", + " if [ -z \"$(printf '%s' \"$labeled_nodes\" | tr -d '\\n')\" ]; then", + " printf '%s=blocked reason=missing-required-label selector=%s nodes=%s\\n' \"$guard_name\" \"$required_selector\" \"$(printf '%s' \"$nodes\" | tr '\\n' ',')\" >&2", + " return 1", + " fi", + " nodes=\"$labeled_nodes\"", + " fi", + ` ${passLine}`, + "}", + "unidesk_ci_k3s_guard", + ]; +} + function safePathToken(value: string): string { return value.replace(/[^a-z0-9._-]/giu, "-").toLowerCase().replace(/^-+|-+$/gu, "").slice(0, 80) || "artifact"; } @@ -679,18 +791,18 @@ function requireCiScriptPath(value: unknown): string { return scriptPath; } -async function dispatchSsh(command: string, waitMs: number, remoteTimeoutMs: number, pollCompletion = true): Promise { +async function dispatchSsh(command: string, waitMs: number, remoteTimeoutMs: number, pollCompletion = true, target = ciTarget(null)): Promise { const dispatchResponse = coreInternalFetch("/api/dispatch", { method: "POST", body: { - providerId: d601ProviderId, + providerId: target.providerId, command: "host.ssh", payload: { source: "ci-cli", mode: "exec", command, timeoutMs: remoteTimeoutMs, - cwd: "/home/ubuntu", + cwd: target.hostCwd, }, }, }); @@ -753,48 +865,48 @@ async function dispatchSsh(command: string, waitMs: number, remoteTimeoutMs: num }; } -async function runRemoteKubectl(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000): Promise { - const result = await runRemoteKubectlRaw(script, waitMs, remoteTimeoutMs); +async function runRemoteKubectl(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000, target = ciTarget(null)): Promise { + const result = await runRemoteKubectlRaw(script, waitMs, remoteTimeoutMs, target); if (!result.ok) { - throw new Error(`D601 kubectl command failed: ${result.stderr || result.stdout || JSON.stringify(result.raw)}`); + throw new Error(`${target.providerId} kubectl command failed: ${result.stderr || result.stdout || JSON.stringify(result.raw)}`); } return result; } -async function runRemoteKubectlRaw(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000): Promise { +async function runRemoteKubectlRaw(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000, target = ciTarget(null)): Promise { const command = [ "set -euo pipefail", - ...d601K3sGuardShellLines(d601Kubeconfig, { passOutput: "stderr" }), + ...ciTargetGuardShellLines(target, { passOutput: "stderr" }), script, ].join("\n"); - return dispatchSsh(command, waitMs, remoteTimeoutMs); + return dispatchSsh(command, waitMs, remoteTimeoutMs, true, target); } -async function uploadRemoteBase64(path: string, encoded: string): Promise { +async function uploadRemoteBase64(path: string, encoded: string, target = ciTarget(null)): Promise { const init = await dispatchSsh([ "set -euo pipefail", `target=${shellQuote(path)}`, "rm -f \"$target\"", ": > \"$target\"", "chmod 600 \"$target\"", - ].join("\n"), 20_000, 10_000); + ].join("\n"), 20_000, 10_000, true, target); if (!init.ok) return init; for (const chunk of chunks(encoded, 950)) { const append = await dispatchSsh([ "set -euo pipefail", `target=${shellQuote(path)}`, `printf %s ${shellQuote(chunk)} >> "$target"`, - ].join("\n"), 20_000, 10_000); + ].join("\n"), 20_000, 10_000, true, target); if (!append.ok) return append; } return dispatchSsh([ "set -euo pipefail", `target=${shellQuote(path)}`, "wc -c \"$target\"", - ].join("\n"), 20_000, 10_000); + ].join("\n"), 20_000, 10_000, true, target); } -async function runRemoteBackground(label: string, script: string, timeoutMs: number): Promise { +async function runRemoteBackground(label: string, script: string, timeoutMs: number, target = ciTarget(null)): Promise { const token = randomUUID().replace(/-/gu, "").slice(0, 12); const safeLabel = label.replace(/[^a-z0-9-]/giu, "-").toLowerCase().slice(0, 48); const base = `/tmp/unidesk-ci-${safeLabel}-${token}`; @@ -802,7 +914,7 @@ async function runRemoteBackground(label: string, script: string, timeoutMs: num const logPath = `${base}.log`; const donePath = `${base}.done`; const encoded = Buffer.from(script, "utf8").toString("base64"); - const upload = await uploadRemoteBase64(`${scriptPath}.b64`, encoded); + const upload = await uploadRemoteBase64(`${scriptPath}.b64`, encoded, target); if (!upload.ok) return upload; const start = await dispatchSsh([ "set -euo pipefail", @@ -815,7 +927,7 @@ async function runRemoteBackground(label: string, script: string, timeoutMs: num "chmod 700 \"$script_path\"", "nohup bash -lc \"bash '$script_path' >'$log_path' 2>&1; code=\\$?; printf '%s\\n' \\\"\\$code\\\" >'$done_path'\" >/tmp/unidesk-ci-nohup.log 2>&1 &", "printf 'remote_job_pid=%s\\nlog=%s\\ndone=%s\\n' \"$!\" \"$log_path\" \"$done_path\"", - ].join("\n"), 20_000, 10_000); + ].join("\n"), 20_000, 10_000, true, target); if (!start.ok) return start; const deadline = Date.now() + timeoutMs; @@ -833,7 +945,7 @@ async function runRemoteBackground(label: string, script: string, timeoutMs: num " printf 'REMOTE_RUNNING\\n'", "fi", "tail -n 160 \"$log_path\" 2>/dev/null || true", - ].join("\n"), 75_000, 12_000); + ].join("\n"), 75_000, 12_000, true, target); if (!latest.ok) { if (latest.status === "timeout" || latest.stderr.includes("did not finish within")) { continue; @@ -860,32 +972,32 @@ async function runRemoteBackground(label: string, script: string, timeoutMs: num }; } -async function remoteApplyManifest(path: string): Promise { +async function remoteApplyManifest(path: string, target = ciTarget(null)): Promise { const absolute = rootPath(path); if (!existsSync(absolute)) throw new Error(`manifest not found: ${path}`); const encoded = Buffer.from(readFileSync(absolute, "utf8"), "utf8").toString("base64"); const token = randomUUID().replace(/-/gu, "").slice(0, 12); const b64Path = `/tmp/unidesk-ci-apply-${token}.b64`; - const upload = await uploadRemoteBase64(b64Path, encoded); + const upload = await uploadRemoteBase64(b64Path, encoded, target); if (!upload.ok) throw new Error(`failed to upload manifest ${path}: ${upload.stderr || upload.stdout}`); const script = [ "set -euo pipefail", - ...d601K3sGuardShellLines(d601Kubeconfig), + ...ciTargetGuardShellLines(target), "tmp=$(mktemp /tmp/unidesk-ci-apply.XXXXXX.yaml)", `b64_path=${shellQuote(b64Path)}`, "trap 'rm -f \"$tmp\" \"$b64_path\"' EXIT", "base64 -d \"$b64_path\" > \"$tmp\"", "kubectl apply -f \"$tmp\"", ].join("\n"); - const result = await runRemoteBackground(`apply-${path.split("/").pop() ?? "manifest"}`, script, 180_000); + const result = await runRemoteBackground(`apply-${path.split("/").pop() ?? "manifest"}`, script, 180_000, target); if (!result.ok) throw new Error(`kubectl apply failed for ${path}: ${result.stderr || result.stdout}`); } -async function prewarmCiRuntimeImages(): Promise { +async function prewarmCiRuntimeImages(target = ciTarget(null)): Promise { const images = ciRuntimeImages.map(shellQuote).join(" "); const script = [ "set -euo pipefail", - ...d601K3sGuardShellLines(d601Kubeconfig), + ...ciTargetGuardShellLines(target), "export DOCKER_CONFIG=/tmp/unidesk-ci-docker-config", "mkdir -p \"$DOCKER_CONFIG\"", "printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"", @@ -900,7 +1012,13 @@ async function prewarmCiRuntimeImages(): Promise { "done", "pause_entrypoint=$(docker image inspect rancher/mirrored-pause:3.6 --format '{{json .Config.Entrypoint}}' 2>/dev/null || true)", "if ! printf '%s' \"$pause_entrypoint\" | grep -q '\"/pause\"'; then echo native_k3s_pause_image_invalid_entrypoint=$pause_entrypoint >&2; exit 1; fi", - "containerd_images=$(/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls 2>/tmp/unidesk-ci-containerd-images.err || true)", + "root_exec() {", + " if [ \"$(id -u)\" = \"0\" ]; then \"$@\"; return $?; fi", + " if command -v sudo >/dev/null 2>&1; then sudo \"$@\"; return $?; fi", + " if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- \"$@\"; return $?; fi", + " \"$@\"", + "}", + "containerd_images=$(root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls 2>/tmp/unidesk-ci-containerd-images.err || true)", "containerd_ready=1", "for image in \"${images[@]}\"; do", " case \"$image\" in", @@ -919,17 +1037,17 @@ async function prewarmCiRuntimeImages(): Promise { "fi", "rm -f /tmp/unidesk-ci-runtime-images.tar", "docker save \"${images[@]}\" -o /tmp/unidesk-ci-runtime-images.tar", - "/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import --digests --all-platforms /tmp/unidesk-ci-runtime-images.tar >/tmp/unidesk-ci-runtime-images-import.log", - "/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/rancher/mirrored-pause:3.6' >/dev/null", - "/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/oven/bun:1-debian' >/dev/null", - "/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/alpine/git:2.45.2' >/dev/null", - `/mnt/c/Windows/System32/wsl.exe -u root -- ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F ${shellQuote(`docker.io/library/${ciCodeQueueImage}`)} >/dev/null`, + "root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import --digests --all-platforms /tmp/unidesk-ci-runtime-images.tar >/tmp/unidesk-ci-runtime-images-import.log", + "root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/rancher/mirrored-pause:3.6' >/dev/null", + "root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/oven/bun:1-debian' >/dev/null", + "root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F 'docker.io/alpine/git:2.45.2' >/dev/null", + `root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F ${shellQuote(`docker.io/library/${target.codeQueueImage}`)} >/dev/null`, ].join("\n"); - const result = await runRemoteBackground("prewarm-runtime-images", script, 900_000); + const result = await runRemoteBackground("prewarm-runtime-images", script, 900_000, target); if (!result.ok) throw new Error(`CI runtime image prewarm failed: ${result.stderr || result.stdout}`); } -async function status(): Promise> { +async function status(target = ciTarget(null)): Promise> { const summary = await runRemoteKubectl([ "set -euo pipefail", "printf 'tekton_pipelines='", @@ -939,10 +1057,10 @@ async function status(): Promise> { "printf '\\nunidesk_ci='", "kubectl get pipeline,task,pipelinerun,eventlistener,svc -n unidesk-ci -o name 2>/dev/null | tr '\\n' ' ' || true", "printf '\\n'", - ].join("\n")); + ].join("\n"), 60_000, 45_000, target); return { ok: true, - providerId: d601ProviderId, + providerId: target.providerId, orchestrator: "native-k3s", tekton: { pipelineVersion: tektonPipelineVersion, @@ -952,14 +1070,14 @@ async function status(): Promise> { }; } -async function install(): Promise> { - if (!existsSync(rootPath("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"))) { +async function install(target = ciTarget(null)): Promise> { + if (!existsSync(rootPath(target.pipelineManifest))) { throw new Error("CI manifests are missing"); } - await prewarmCiRuntimeImages(); + await prewarmCiRuntimeImages(target); const installTektonScript = [ "set -euo pipefail", - ...d601K3sGuardShellLines(d601Kubeconfig), + ...ciTargetGuardShellLines(target), `kubectl apply -f ${shellQuote(tektonPipelineReleaseUrl)}`, "kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s", `kubectl apply -f ${shellQuote(tektonTriggersReleaseUrl)}`, @@ -967,12 +1085,12 @@ async function install(): Promise> { "kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s", "kubectl wait --for=condition=Available deployment --all -n tekton-pipelines-resolvers --timeout=900s", ].join("\n"); - const installTekton = await runRemoteBackground("install-tekton", installTektonScript, 1_200_000); + const installTekton = await runRemoteBackground("install-tekton", installTektonScript, 1_200_000, target); if (!installTekton.ok) throw new Error(`Tekton install failed: ${installTekton.stderr || installTekton.stdout}`); - await remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/tekton-install.yaml"); - await remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"); - await remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.triggers.yaml"); - return status(); + await remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/tekton-install.yaml", target); + await remoteApplyManifest(target.pipelineManifest, target); + await remoteApplyManifest("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.triggers.yaml", target); + return status(target); } function pipelineRunManifest(options: CiOptions): string { @@ -1314,11 +1432,11 @@ async function prepareClaudeqqArtifactSource(config: UniDeskConfig, options: CiP }; } -async function remoteCreatePipelineRun(manifest: string): Promise { +async function remoteCreatePipelineRun(manifest: string, target = ciTarget(null)): Promise { const encoded = Buffer.from(manifest, "utf8").toString("base64"); const token = randomUUID().replace(/-/gu, "").slice(0, 12); const b64Path = `/tmp/unidesk-ci-pipelinerun-${token}.b64`; - const upload = await uploadRemoteBase64(b64Path, encoded); + const upload = await uploadRemoteBase64(b64Path, encoded, target); if (!upload.ok) throw new Error(`failed to upload PipelineRun manifest: ${upload.stderr || upload.stdout}`); const result = await runRemoteKubectl([ "tmp=$(mktemp /tmp/unidesk-ci-run.XXXXXX.yaml)", @@ -1326,15 +1444,15 @@ async function remoteCreatePipelineRun(manifest: string): Promise { "trap 'rm -f \"$tmp\" \"$b64_path\"' EXIT", "base64 -d \"$b64_path\" > \"$tmp\"", "kubectl create -f \"$tmp\" -o jsonpath='{.metadata.name}'", - ].join("\n"), 60_000, 45_000); + ].join("\n"), 60_000, 45_000, target); return result.stdout.trim(); } -async function waitForPipelineRun(name: string, waitMs: number): Promise { +async function waitForPipelineRun(name: string, waitMs: number, target = ciTarget(null)): Promise { if (waitMs <= 0) return null; const command = [ "set -euo pipefail", - ...d601K3sGuardShellLines(d601Kubeconfig), + ...ciTargetGuardShellLines(target), `printf 'waiting_pipelinerun=%s\\n' ${shellQuote(name)}`, `deadline=$((SECONDS + ${Math.ceil(waitMs / 1000)}))`, "while [ \"$SECONDS\" -lt \"$deadline\" ]; do", @@ -1357,15 +1475,15 @@ async function waitForPipelineRun(name: string, waitMs: number): Promise { +async function readPipelineRunCondition(name: string, target = ciTarget(null)): Promise { const result = await runRemoteKubectlRaw([ "set -euo pipefail", `condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`, "printf '%s\\n' \"$condition\"", - ].join("\n"), 30_000, 15_000); + ].join("\n"), 30_000, 15_000, target); const [status = "", reason = "", ...messageParts] = result.stdout.trim().split("\t"); const message = messageParts.join("\t"); return { @@ -1973,23 +2091,24 @@ async function readArtifactSummaryFromPipelineRun(name: string, context: Artifac return completeArtifactSummaryFromRegistry(parseArtifactSummaryFromOutput(await readPipelineRunLogText(name), context), context); } -async function readPipelineRunLogText(name: string): Promise { +async function readPipelineRunLogText(name: string, target = ciTarget(null)): Promise { const result = await runRemoteKubectlRaw([ "set -euo pipefail", `kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o wide`, `kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o wide`, `for pod in $(kubectl get pods -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o name); do echo "===== $pod"; kubectl logs -n unidesk-ci "$pod" --all-containers=true --tail=240 || true; done`, - ].join("\n"), 60_000, 45_000); + ].join("\n"), 60_000, 45_000, target); return `${result.stdout}\n${result.stderr}`.trim(); } async function run(options: CiOptions): Promise> { - const name = await remoteCreatePipelineRun(pipelineRunManifest(options)); - const wait = await waitForPipelineRun(name, options.waitMs); - const condition = wait === null ? null : await readPipelineRunCondition(name); + const name = await remoteCreatePipelineRun(pipelineRunManifest(options), options.target); + const wait = await waitForPipelineRun(name, options.waitMs, options.target); + const condition = wait === null ? null : await readPipelineRunCondition(name, options.target); const waitSucceeded = pipelineRunWaitSucceeded(wait, condition); return { ok: waitSucceeded, + providerId: options.target.providerId, pipelineRun: name, namespace: "unidesk-ci", repoUrl: options.repoUrl, @@ -2004,8 +2123,8 @@ async function run(options: CiOptions): Promise> { }, condition, next: [ - `bun scripts/cli.ts ci logs ${name}`, - "bun scripts/cli.ts ci status", + `bun scripts/cli.ts ci logs ${name} --provider-id ${options.target.providerId}`, + `bun scripts/cli.ts ci status --provider-id ${options.target.providerId}`, ], }; } @@ -2582,13 +2701,14 @@ async function runDevE2E(options: CiDevE2EOptions): Promise> { +async function logs(name: string, target = ciTarget(null)): Promise> { if (name.length === 0) throw new Error("ci logs requires run id or PipelineRun name"); if (/^[a-z0-9]([-a-z0-9]{0,46}[a-z0-9])?$/u.test(name)) { const result = await dispatchSsh([ "set -euo pipefail", `run_id=${shellQuote(name)}`, - "result_dir=\"/home/ubuntu/.unidesk/runs/$run_id\"", + `result_root=${shellQuote(`${target.homeDir}/.unidesk/runs`)}`, + "result_dir=\"$result_root/$run_id\"", "printf 'result_dir=%s\\n' \"$result_dir\"", "found=0", "if [ -f \"$result_dir/result.json\" ]; then found=1; echo '===== result.json'; cat \"$result_dir/result.json\"; fi", @@ -2596,7 +2716,7 @@ async function logs(name: string): Promise> { "if [ -f \"$result_dir/runner.log\" ]; then found=1; echo '===== runner.log'; tail -n 240 \"$result_dir/runner.log\"; fi", "if [ -f \"$result_dir/pods.log\" ]; then found=1; echo '===== pods.log'; tail -n 240 \"$result_dir/pods.log\"; fi", "if [ \"$found\" = \"0\" ]; then echo \"no_run_files=$result_dir\" >&2; exit 42; fi", - ].join("\n"), 60_000, 45_000); + ].join("\n"), 60_000, 45_000, true, target); if (result.ok || (result.exitCode !== 42 && !result.stderr.includes("no_run_files="))) { return { ok: result.ok, @@ -2612,9 +2732,10 @@ async function logs(name: string): Promise> { `kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o wide`, `kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o wide`, `for pod in $(kubectl get pods -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o name); do echo "===== $pod"; kubectl logs -n unidesk-ci "$pod" --all-containers=true --tail=160 || true; done`, - ].join("\n"), 60_000, 45_000); + ].join("\n"), 60_000, 45_000, target); return { ok: true, + providerId: target.providerId, pipelineRun: name, output: result.stdout, stderr: result.stderr, @@ -2649,10 +2770,12 @@ export function ciHelp(): Record { const catalog = loadCiCatalog(); return { command: "ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs", - description: "Manage the D601 k3s Tekton CI gate. CI may publish commit-pinned image artifacts, but it intentionally does not deploy CD.", + description: "Manage native k3s Tekton CI on D601 or G14. CI may publish commit-pinned image artifacts, but it intentionally does not deploy CD.", examples: [ "bun scripts/cli.ts ci install", + "bun scripts/cli.ts ci install --provider-id G14", "bun scripts/cli.ts ci run --revision ", + "bun scripts/cli.ts ci run --provider-id G14 --revision ", "bun scripts/cli.ts ci publish-backend-core --commit ", "bun scripts/cli.ts ci publish-user-service --service baidu-netdisk --commit ", "bun scripts/cli.ts ci publish-user-service --service mdtodo --commit ", @@ -2661,7 +2784,7 @@ export function ciHelp(): Record { "bun scripts/cli.ts ci publish-user-service --service decision-center --commit ", "bun scripts/cli.ts ci publish-user-service --service frontend --commit ", "bun scripts/cli.ts ci run-dev-e2e --wait-ms 600000", - "bun scripts/cli.ts ci logs ", + "bun scripts/cli.ts ci logs [--provider-id G14]", ], tekton: { pipelineVersion: tektonPipelineVersion, @@ -2671,6 +2794,14 @@ export function ciHelp(): Record { triggers: tektonTriggersReleaseUrl, interceptors: tektonTriggersInterceptorsUrl, }, + targets: { + default: "D601", + g14: { + providerId: "G14", + pipelineManifest: g14CiPipelineManifest, + nodeSelector: "unidesk.ai/node-id=G14", + }, + }, }, backendCoreArtifact: { producer: "D601 CI", @@ -2716,13 +2847,14 @@ function requireRunId(value: string): string { export async function runCiCommand(config: UniDeskConfig, args: string[]): Promise> { const [action = "status", nameArg] = args; if (isHelpArg(action) || args.slice(1).some(isHelpArg)) return ciHelp(); - if (action === "install") return install(); - if (action === "status") return status(); + if (action === "install") return install(ciTarget(providerIdOption(args))); + if (action === "status") return status(ciTarget(providerIdOption(args))); if (action === "run") { + const target = ciTarget(providerIdOption(args)); const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk"; const revision = requireRevision(stringOption(args, "--revision") ?? stringOption(args, "--commit")); const waitMs = numberOption(args, "--wait-ms", 0); - return run({ repoUrl, revision, waitMs }); + return run({ repoUrl, revision, waitMs, target }); } if (action === "publish-backend-core") { if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) { @@ -2799,11 +2931,11 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi waitMs, }); } - if (action === "logs") return logs(nameArg ?? ""); + if (action === "logs") return logs(nameArg ?? "", ciTarget(providerIdOption(args))); throw new Error("ci command must be one of: install, status, run, publish-backend-core, publish-user-service, run-dev-e2e, logs"); } -export function startCiInstallJob(): Record { - const job = startJob("ci_install", ["bun", "scripts/cli.ts", "ci", "install"], "Install/refresh Tekton CI on D601 k3s"); +export function startCiInstallJob(providerId = d601ProviderId): Record { + const job = startJob("ci_install", ["bun", "scripts/cli.ts", "ci", "install", "--provider-id", providerId], `Install/refresh Tekton CI on ${providerId} native k3s`); return { ok: true, job }; } diff --git a/scripts/src/deploy-ssh-identity.ts b/scripts/src/deploy-ssh-identity.ts index c989f1aa..8c878a4b 100644 --- a/scripts/src/deploy-ssh-identity.ts +++ b/scripts/src/deploy-ssh-identity.ts @@ -23,6 +23,10 @@ const defaultPrivateKeyPath = "/root/.ssh/id_ed25519"; const defaultKnownHostsPath = "/root/.ssh/known_hosts"; const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789"; +function providerHomeDir(providerId: string): string { + return providerId === "G14" ? "/root" : "/home/ubuntu"; +} + function pgLiteral(value: string): string { return `'${value.replace(/'/gu, "''")}'`; } @@ -202,7 +206,8 @@ import subprocess import sys data = json.load(sys.stdin) -ssh_dir = pathlib.Path("/home/ubuntu/.ssh") +home_dir = pathlib.Path(str(data.get("homeDir") or os.environ.get("HOME") or "/home/ubuntu")) +ssh_dir = home_dir / ".ssh" private_key = str(data.get("privateKey") or "") public_key = str(data.get("publicKey") or "").strip() known_hosts = str(data.get("knownHosts") or "") @@ -321,6 +326,7 @@ function distributeGithubIdentity(providerId: string, identity: GithubSshIdentit privateKey: identity.privateKey, publicKey: identity.publicKey, knownHosts: identity.knownHosts, + homeDir: providerHomeDir(providerId), }); const remotePython = remoteInstallPythonSource(); const proxyPython = gitSshHttpConnectProxySource(); diff --git a/scripts/src/docker.ts b/scripts/src/docker.ts index 5cf5bdfb..b6ce21ba 100644 --- a/scripts/src/docker.ts +++ b/scripts/src/docker.ts @@ -50,7 +50,7 @@ export function unsupportedRebuildService(value: string | undefined): Record service.id === "code-queue"); + const codeQueueProviderId = codeQueueService?.providerId || "D601"; + const codeQueueRemoteWorkdir = codeQueueService?.development.worktreePath || "/home/ubuntu"; + const codeQueueExecutionProviderIds = codeQueueService?.deployment.expectedNodeIds?.join(",") || codeQueueProviderId; + const codeQueueEnvValue = (key: string, defaultValue: string): string => { + const value = process.env[key]; + return value === undefined || value.length === 0 ? defaultValue : value; + }; const lines = { UNIDESK_PUBLIC_HOST: config.network.publicHost, UNIDESK_CORE_PORT: String(config.network.core.port), @@ -233,16 +241,16 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): UNIDESK_CODE_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_MODEL") || runtimeSecret("MINIMAX_MODEL") || "MiniMax-M2.7", UNIDESK_CODE_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_BASE") || runtimeSecret("MINIMAX_API_BASE") || "https://api.minimaxi.com/v1", UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecretWithDefault("UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS", "90000", "60000"), - UNIDESK_CODE_QUEUE_REMOTE_WORKDIR: runtimeSecret("UNIDESK_CODE_QUEUE_REMOTE_WORKDIR") || "/home/ubuntu", - UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID") || "D601", + UNIDESK_CODE_QUEUE_REMOTE_WORKDIR: codeQueueEnvValue("UNIDESK_CODE_QUEUE_REMOTE_WORKDIR", codeQueueRemoteWorkdir), + UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID: codeQueueEnvValue("UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID", codeQueueProviderId), UNIDESK_CODE_QUEUE_TRACE_DATABASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_URL") || `postgres://${config.database.user}:${config.database.password}@database:${config.network.database.containerPort}/${config.database.name}`, UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX") || "2", UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX") || "1", - UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS: runtimeSecret("UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS") || "D601", - UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID") || "D601", + UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS: codeQueueEnvValue("UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS", codeQueueExecutionProviderIds), + UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: codeQueueEnvValue("UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID", codeQueueService?.development.providerId || codeQueueProviderId), UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE"), - UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR") || "/home/ubuntu", + UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR: codeQueueEnvValue("UNIDESK_CODE_QUEUE_DEV_CONTAINER_WORKDIR", codeQueueRemoteWorkdir), UNIDESK_OA_EVENT_FLOW_BASE_URL: runtimeSecret("UNIDESK_OA_EVENT_FLOW_BASE_URL") || "http://oa-event-flow:4255", UNIDESK_OA_EVENT_FLOW_PORT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_PORT") || "4255", UNIDESK_OA_EVENT_FLOW_BIND_HOST: runtimeSecretWithDefault("UNIDESK_OA_EVENT_FLOW_BIND_HOST", restrictedHostBind, "127.0.0.1"), diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 87f51918..2407e893 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -77,7 +77,7 @@ export function rootHelp(): unknown { { command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." }, { command: "debug task ", description: "Read a dispatched task record from internal core for CLI debugging." }, { command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." }, - { command: "ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs", description: "Manage D601 k3s Tekton CI; artifact publish commands build commit-pinned images in CI without deploying CD." }, + { command: "ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs", description: "Manage D601/G14 native k3s Tekton CI; artifact publish commands build commit-pinned images in CI without deploying CD." }, { command: "e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]", description: "Run selected public/internal/Playwright E2E checks; use --only for focused iteration and rerun without filters for final regression." }, { command: "bun scripts/playwright-cli.ts screenshot|open|eval ...", description: "Repo-owned Playwright wrapper for commander browser checks; headless by default, supports storage-state --session, and returns JSON guidance for unsupported interactive daemon commands." }, ], diff --git a/src/components/backend-core/src/config.rs b/src/components/backend-core/src/config.rs index 7ee0818a..3d516278 100644 --- a/src/components/backend-core/src/config.rs +++ b/src/components/backend-core/src/config.rs @@ -63,8 +63,9 @@ fn read_microservices_env() -> anyhow::Result> { } if service.deployment.mode != "unidesk-direct" && service.deployment.mode != "k3sctl-managed" + && service.deployment.mode != "internal-sidecar" { - bail!("MICROSERVICES_JSON[{index}].deployment.mode must be unidesk-direct or k3sctl-managed"); + bail!("MICROSERVICES_JSON[{index}].deployment.mode must be unidesk-direct, k3sctl-managed, or internal-sidecar"); } service.backend.allowed_methods = service .backend diff --git a/src/components/backend-core/src/microservice_proxy.rs b/src/components/backend-core/src/microservice_proxy.rs index 418cef5b..a168ab6f 100644 --- a/src/components/backend-core/src/microservice_proxy.rs +++ b/src/components/backend-core/src/microservice_proxy.rs @@ -175,7 +175,15 @@ fn json_i64(value: &Value, path: &[&str]) -> Option { current.as_i64() } -fn code_queue_route_postgres_check(scheduler_body: &Value) -> Value { +fn code_queue_infra_service_names(service: &MicroserviceConfig) -> (String, String) { + let provider_slug = service.provider_id.to_ascii_lowercase(); + ( + format!("{provider_slug}-provider-egress-proxy"), + format!("{provider_slug}-tcp-egress-gateway"), + ) +} + +fn code_queue_route_postgres_check(scheduler_body: &Value, tcp_egress_service: &str) -> Value { let storage = scheduler_body .get("queue") .and_then(|queue| queue.get("storage")) @@ -187,7 +195,7 @@ fn code_queue_route_postgres_check(scheduler_body: &Value) -> Value { let last_error = storage.get("lastError").cloned().unwrap_or(Value::Null); json!({ "ok": postgres_ready && last_error.is_null(), - "route": "d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432", + "route": format!("{tcp_egress_service}.unidesk.svc.cluster.local:15432"), "postgresReady": postgres_ready, "lastError": last_error, "source": "scheduler.health.queue.storage" @@ -1746,17 +1754,18 @@ async fn code_queue_health_response( && scheduler_body.get("ok").and_then(Value::as_bool) != Some(false); let scheduler_diagnostics = code_queue_adapter_diagnostics(state, service, "code-queue-scheduler").await; + let (provider_egress_service, tcp_egress_service) = code_queue_infra_service_names(service); let provider_egress_diagnostics = - code_queue_adapter_diagnostics(state, service, "d601-provider-egress-proxy").await; + code_queue_adapter_diagnostics(state, service, &provider_egress_service).await; let tcp_egress_diagnostics = - code_queue_adapter_diagnostics(state, service, "d601-tcp-egress-gateway").await; + code_queue_adapter_diagnostics(state, service, &tcp_egress_service).await; let scheduler_dependency = code_queue_dependency_check("code-queue-scheduler", &scheduler_diagnostics); let provider_egress = - code_queue_dependency_check("d601-provider-egress-proxy", &provider_egress_diagnostics); + code_queue_dependency_check(&provider_egress_service, &provider_egress_diagnostics); let tcp_egress = - code_queue_dependency_check("d601-tcp-egress-gateway", &tcp_egress_diagnostics); - let postgres_route = code_queue_route_postgres_check(&scheduler_body); + code_queue_dependency_check(&tcp_egress_service, &tcp_egress_diagnostics); + let postgres_route = code_queue_route_postgres_check(&scheduler_body, &tcp_egress_service); let stale_reconcile = code_queue_reconcile_check(&scheduler_body); let dependencies_ok = scheduler_dependency .get("ok") @@ -1791,15 +1800,15 @@ async fn code_queue_health_response( "scheduler": { "ok": scheduler_healthy, "status": scheduler_status, "body": scheduler_body, "timedOut": false, "requiredFor": ["running task steer", "running task interrupt", "scheduler claim/runner execution", "dev-container control"] }, "checks": { "scheduler": scheduler_dependency, - "d601ProviderEgressProxy": provider_egress, - "d601TcpEgressGateway": tcp_egress, + "providerEgressProxy": provider_egress, + "tcpEgressGateway": tcp_egress, "postgresRoute": postgres_route, "staleActiveReconcile": stale_reconcile }, "diagnostics": { "scheduler": scheduler_diagnostics, - "d601ProviderEgressProxy": provider_egress_diagnostics, - "d601TcpEgressGateway": tcp_egress_diagnostics + "providerEgressProxy": provider_egress_diagnostics, + "tcpEgressGateway": tcp_egress_diagnostics }, }); if head_only { @@ -1981,17 +1990,18 @@ async fn k3sctl_managed_diagnostics_response( .unwrap_or(Value::Null); let scheduler_diagnostics = code_queue_adapter_diagnostics(state, service, "code-queue-scheduler").await; + let (provider_egress_service, tcp_egress_service) = code_queue_infra_service_names(service); let provider_egress_diagnostics = - code_queue_adapter_diagnostics(state, service, "d601-provider-egress-proxy").await; + code_queue_adapter_diagnostics(state, service, &provider_egress_service).await; let tcp_egress_diagnostics = - code_queue_adapter_diagnostics(state, service, "d601-tcp-egress-gateway").await; + code_queue_adapter_diagnostics(state, service, &tcp_egress_service).await; let scheduler_dependency = code_queue_dependency_check("code-queue-scheduler", &scheduler_diagnostics); let provider_egress = - code_queue_dependency_check("d601-provider-egress-proxy", &provider_egress_diagnostics); + code_queue_dependency_check(&provider_egress_service, &provider_egress_diagnostics); let tcp_egress = - code_queue_dependency_check("d601-tcp-egress-gateway", &tcp_egress_diagnostics); - let postgres_route = code_queue_route_postgres_check(&scheduler_body); + code_queue_dependency_check(&tcp_egress_service, &tcp_egress_diagnostics); + let postgres_route = code_queue_route_postgres_check(&scheduler_body, &tcp_egress_service); let stale_reconcile = code_queue_reconcile_check(&scheduler_body); code_queue_ok = scheduler_dependency .get("ok") @@ -2015,20 +2025,20 @@ async fn k3sctl_managed_diagnostics_response( .unwrap_or(false); code_queue_checks = json!({ "scheduler": scheduler_dependency, - "d601ProviderEgressProxy": provider_egress, - "d601TcpEgressGateway": tcp_egress, + "providerEgressProxy": provider_egress, + "tcpEgressGateway": tcp_egress, "postgresRoute": postgres_route, "staleActiveReconcile": stale_reconcile }); code_queue_dependencies = json!({ - "postgresRoute": "d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432", - "requiredDeployments": ["d601-tcp-egress-gateway", "d601-provider-egress-proxy", "code-queue"], - "requiredEndpoints": ["d601-tcp-egress-gateway", "d601-provider-egress-proxy", "code-queue-scheduler"] + "postgresRoute": format!("{tcp_egress_service}.unidesk.svc.cluster.local:15432"), + "requiredDeployments": [tcp_egress_service, provider_egress_service, "code-queue".to_string()], + "requiredEndpoints": [tcp_egress_service, provider_egress_service, "code-queue-scheduler".to_string()] }); code_queue_adapter = json!({ "scheduler": scheduler_diagnostics, - "d601ProviderEgressProxy": provider_egress_diagnostics, - "d601TcpEgressGateway": tcp_egress_diagnostics + "providerEgressProxy": provider_egress_diagnostics, + "tcpEgressGateway": tcp_egress_diagnostics }); } let ok = (200..300).contains(&status2) diff --git a/src/components/microservices/k3sctl-adapter/docker-compose.g14.yml b/src/components/microservices/k3sctl-adapter/docker-compose.g14.yml new file mode 100644 index 00000000..1acebccf --- /dev/null +++ b/src/components/microservices/k3sctl-adapter/docker-compose.g14.yml @@ -0,0 +1,65 @@ +services: + k3sctl-adapter-g14: + image: unidesk-k3sctl-adapter:g14 + build: + context: ../../../.. + dockerfile: src/components/microservices/k3sctl-adapter/Dockerfile + args: + K3SCTL_ADAPTER_BASE_IMAGE: ${K3SCTL_ADAPTER_BASE_IMAGE:-oven/bun:1-debian} + container_name: k3sctl-adapter-g14 + restart: unless-stopped + env_file: + - path: ${K3SCTL_ADAPTER_ENV_FILE:-../../../../.state/k3sctl-adapter-g14.env} + required: false + ports: + - "127.0.0.1:${K3SCTL_ADAPTER_HOST_PORT:-4266}:4266" + environment: + HOST: "0.0.0.0" + PORT: "4266" + LOG_FILE: "/var/log/unidesk/k3sctl-adapter-g14.jsonl" + K3SCTL_CLUSTER_ID: "${K3SCTL_CLUSTER_ID:-unidesk-k3s-g14}" + K3SCTL_NODE_ID: "${K3SCTL_NODE_ID:-G14}" + K3SCTL_KUBECTL_ENABLED: "${K3SCTL_KUBECTL_ENABLED:-false}" + K3SCTL_KUBE_API_PROXY_ENABLED: "${K3SCTL_KUBE_API_PROXY_ENABLED:-true}" + K3SCTL_KUBECONFIG_PATH: "/var/lib/unidesk/k3s/kubeconfig" + K3SCTL_KUBE_API_CONNECT_HOST: "${K3SCTL_KUBE_API_CONNECT_HOST:-host.docker.internal}" + K3SCTL_KUBE_API_SSH_TUNNEL_ENABLED: "${K3SCTL_KUBE_API_SSH_TUNNEL_ENABLED:-false}" + K3SCTL_KUBE_API_SSH_HOST: "${K3SCTL_KUBE_API_SSH_HOST:-host.docker.internal}" + K3SCTL_KUBE_API_SSH_USER: "${K3SCTL_KUBE_API_SSH_USER:-root}" + K3SCTL_KUBE_API_SSH_KEY: "${K3SCTL_KUBE_API_SSH_KEY:-/run/host-ssh/id_ed25519}" + K3SCTL_KUBE_API_LOCAL_HOST: "${K3SCTL_KUBE_API_LOCAL_HOST:-127.0.0.1}" + K3SCTL_KUBE_API_LOCAL_PORT: "${K3SCTL_KUBE_API_LOCAL_PORT:-6443}" + K3SCTL_KUBE_API_REMOTE_HOST: "${K3SCTL_KUBE_API_REMOTE_HOST:-127.0.0.1}" + K3SCTL_KUBE_API_REMOTE_PORT: "${K3SCTL_KUBE_API_REMOTE_PORT:-6443}" + K3SCTL_NATIVE_SERVICE_PROXY_ENABLED: "${K3SCTL_NATIVE_SERVICE_PROXY_ENABLED:-true}" + K3SCTL_NATIVE_SERVICE_SSH_TUNNEL_ENABLED: "${K3SCTL_NATIVE_SERVICE_SSH_TUNNEL_ENABLED:-true}" + K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS: "${K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS:-1200}" + K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS: "${K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS:-10000}" + K3SCTL_NATIVE_SERVICE_RESOLUTION_TTL_MS: "${K3SCTL_NATIVE_SERVICE_RESOLUTION_TTL_MS:-30000}" + K3SCTL_NATIVE_SERVICE_TUNNEL_CONNECT_TIMEOUT_MS: "${K3SCTL_NATIVE_SERVICE_TUNNEL_CONNECT_TIMEOUT_MS:-3000}" + K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE:-}" + K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE_READ: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE_READ:-}" + K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE_WRITE: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE_WRITE:-}" + K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE_SCHEDULER: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE_SCHEDULER:-}" + K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.g14.k3s.json}" + K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}" + volumes: + - ${K3SCTL_ADAPTER_LOG_DIR:-../../../../.state/k3sctl-adapter-g14/logs}:/var/log/unidesk + - ${K3SCTL_KUBECONFIG_HOST_PATH:-/etc/rancher/k3s/k3s.yaml}:/var/lib/unidesk/k3s/kubeconfig:ro + - ${K3SCTL_HOST_SSH_KEY_DIR:-/root/.unidesk/host-ssh}:/run/host-ssh:ro + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - default + - provider-gateway + healthcheck: + test: ["CMD-SHELL", "curl -fsS --max-time 2 http://127.0.0.1:4266/health >/dev/null"] + interval: 5s + timeout: 3s + retries: 20 + +networks: + provider-gateway: + external: true + name: ${K3SCTL_PROVIDER_GATEWAY_NETWORK:-unidesk-g14_default} diff --git a/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml b/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml new file mode 100644 index 00000000..610fc207 --- /dev/null +++ b/src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml @@ -0,0 +1,1863 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: unidesk-ci + labels: + app.kubernetes.io/part-of: unidesk + unidesk.ai/purpose: ci +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: unidesk-ci-runner + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: unidesk-ci-runner + namespace: unidesk-ci +rules: + - apiGroups: [""] + resources: ["pods", "pods/log", "services"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] + - apiGroups: ["tekton.dev"] + resources: ["pipelineruns", "taskruns"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] + - apiGroups: ["triggers.tekton.dev"] + resources: ["eventlisteners", "triggers", "triggerbindings", "triggertemplates", "interceptors"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: unidesk-ci-runner + namespace: unidesk-ci +subjects: + - kind: ServiceAccount + name: unidesk-ci-runner + namespace: unidesk-ci +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: unidesk-ci-runner +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: unidesk-ci-trigger-reader + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +rules: + - apiGroups: ["triggers.tekton.dev"] + resources: ["clusterinterceptors", "clustertriggerbindings"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: unidesk-ci-trigger-reader + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +subjects: + - kind: ServiceAccount + name: unidesk-ci-runner + namespace: unidesk-ci +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: unidesk-ci-trigger-reader +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: unidesk-ci-dev-e2e-manager + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] + - apiGroups: [""] + resources: ["configmaps", "secrets", "services", "services/proxy", "pods", "pods/log"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: unidesk-ci-dev-e2e-manager + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +subjects: + - kind: ServiceAccount + name: unidesk-ci-runner + namespace: unidesk-ci +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: unidesk-ci-dev-e2e-manager +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: unidesk-ci-cross-namespace + namespace: unidesk +rules: + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: unidesk-ci-cross-namespace + namespace: unidesk +subjects: + - kind: ServiceAccount + name: unidesk-ci-runner + namespace: unidesk-ci +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: unidesk-ci-cross-namespace +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: unidesk-ci-cache + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: unidesk-ci-budgets + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +data: + firstPaintMs: "10000" + traceSummaryMs: "10000" + traceStepsMs: "20000" + traceStepDetailMs: "20000" + overviewP95Ms: "20000" +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: unidesk-repo-check + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: repo-check +spec: + params: + - name: repo-url + type: string + - name: revision + type: string + - name: image + type: string + default: unidesk-code-queue:dev + workspaces: + - name: source + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + steps: + - name: clone + image: alpine/git:2.45.2 + env: + - name: HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: HTTPS_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: ALL_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,ci-git-mirror,ci-git-mirror.unidesk-ci,ci-git-mirror.unidesk-ci.svc,ci-git-mirror.unidesk-ci.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local" + - name: http_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: https_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: all_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: no_proxy + value: "localhost,127.0.0.1,::1,ci-git-mirror,ci-git-mirror.unidesk-ci,ci-git-mirror.unidesk-ci.svc,ci-git-mirror.unidesk-ci.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local" + script: | + #!/bin/sh + set -eu + rm -rf "$(workspaces.source.path)/repo" + git clone --filter=blob:none "$(params.repo-url)" "$(workspaces.source.path)/repo" + cd "$(workspaces.source.path)/repo" + git fetch --depth=1 origin "$(params.revision)" + git checkout --detach FETCH_HEAD + git rev-parse HEAD | tee "$(workspaces.source.path)/commit.txt" + - name: install-and-check + image: "$(params.image)" + imagePullPolicy: Never + env: + - name: DOCKER_HOST + value: unix:///var/run/docker.sock + - name: BUN_INSTALL_CACHE_DIR + value: "$(workspaces.source.path)/cache/bun" + - name: HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: HTTPS_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: ALL_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local" + - name: http_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: https_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: all_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: no_proxy + value: "localhost,127.0.0.1,::1,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + script: | + #!/usr/bin/env bash + set -euo pipefail + cd "$(workspaces.source.path)/repo" + command -v bun + command -v git + command -v docker + docker compose version >/dev/null + bun install --frozen-lockfile + (cd src && bun install --frozen-lockfile) + UNIDESK_NATIVE_K3S_RUST_CHECK=1 bun scripts/cli.ts check --full --rust + cargo build --manifest-path src/components/microservices/code-queue-mgr/Cargo.toml --release + CQ_MGR_ID="$(printf '%s' "$(params.revision)" | tr -cd 'A-Za-z0-9_.-' | cut -c1-48)" + CQ_MGR_DB="$(workspaces.source.path)/code-queue-mgr-workdirs-postgres" + rm -rf "$CQ_MGR_DB" + mkdir -p "$CQ_MGR_DB" + docker rm -f "code-queue-mgr-workdirs-$CQ_MGR_ID" >/dev/null 2>&1 || true + docker run -d --name "code-queue-mgr-workdirs-$CQ_MGR_ID" \ + -p 127.0.0.1::5432 \ + -e POSTGRES_USER=unidesk_ci \ + -e POSTGRES_PASSWORD=unidesk_ci_password \ + -e POSTGRES_DB=unidesk_ci \ + -v "$CQ_MGR_DB:/var/lib/postgresql/data" \ + postgres:16-alpine >/tmp/code-queue-mgr-workdirs-postgres.cid + CQ_MGR_DB_PORT="$(docker inspect "code-queue-mgr-workdirs-$CQ_MGR_ID" --format '{{(index (index .NetworkSettings.Ports "5432/tcp") 0).HostPort}}')" + cleanup() { + docker rm -f "code-queue-mgr-workdirs-$CQ_MGR_ID" >/dev/null 2>&1 || true + } + trap cleanup EXIT + for _ in $(seq 1 60); do + if docker exec "code-queue-mgr-workdirs-$CQ_MGR_ID" pg_isready -U unidesk_ci -d unidesk_ci >/dev/null 2>&1; then + break + fi + sleep 1 + done + CQ_MGR_PORT="$(python3 - <<'PY' + import socket + s = socket.socket() + s.bind(("127.0.0.1", 0)) + print(s.getsockname()[1]) + s.close() + PY + )" + DATABASE_URL="postgres://unidesk_ci:unidesk_ci_password@127.0.0.1:$CQ_MGR_DB_PORT/unidesk_ci" \ + HOST=127.0.0.1 \ + PORT="$CQ_MGR_PORT" \ + CODE_QUEUE_MAIN_PROVIDER_ID=G14 \ + CODE_QUEUE_WORKDIR=/workspace \ + CODE_QUEUE_REMOTE_WORKDIR=/root/unidesk \ + src/components/microservices/code-queue-mgr/target/release/code-queue-mgr >"$(workspaces.source.path)/code-queue-mgr-workdirs.log" 2>&1 & + cq_mgr_pid="$!" + trap 'kill "$cq_mgr_pid" >/dev/null 2>&1 || true; cleanup' EXIT + for _ in $(seq 1 60); do + if curl -fsS "http://127.0.0.1:$CQ_MGR_PORT/health" >/dev/null 2>&1; then + break + fi + sleep 1 + done + workdirs_json="$(curl -fsS "http://127.0.0.1:$CQ_MGR_PORT/api/workdirs")" + printf '%s\n' "$workdirs_json" + WORKDIRS_JSON="$workdirs_json" bun -e 'const body=JSON.parse(process.env.WORKDIRS_JSON||"{}"); if (body.ok !== true || !Array.isArray(body.workdirs) || !body.workdirs.some((item)=>item && item.path === "/workspace" && item.providerId === "G14" && item.executionMode === "default")) { console.error(JSON.stringify(body)); process.exit(1); }' +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: unidesk-backend-core-artifact-publish + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: backend-core-artifact +spec: + params: + - name: repo-url + type: string + - name: revision + type: string + - name: dockerfile + type: string + - name: image-repository + type: string + - name: app-image + type: string + default: unidesk-code-queue:dev + - name: registry + type: string + default: 127.0.0.1:5000 + - name: source-host-path + type: string + results: + - name: backend_core_artifact_service_id + description: CI artifact summary service id. + - name: backend_core_artifact_source_commit + description: Full Git source commit used to build the artifact. + - name: backend_core_artifact_source_repo + description: Source repository URL used to build the artifact. + - name: backend_core_artifact_dockerfile + description: Repo-relative Dockerfile path used to build the artifact. + - name: backend_core_artifact_registry + description: G14 loopback registry host. + - name: backend_core_artifact_repository + description: Registry repository without tag or digest. + - name: backend_core_artifact_image + description: Commit-tagged image reference. + - name: backend_core_artifact_tag + description: Commit-pinned image tag. + - name: backend_core_artifact_digest + description: Registry manifest digest. + - name: backend_core_artifact_digest_ref + description: Immutable image digest reference. + workspaces: + - name: source + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + - name: prepared-source + hostPath: + path: /root/.unidesk/ci/backend-core-artifacts + type: Directory + steps: + - name: prepare-source + image: alpine/git:2.45.2 + volumeMounts: + - name: prepared-source + mountPath: /prepared-source + readOnly: true + script: | + #!/bin/sh + set -eu + dockerfile="$(params.dockerfile)" + case "$(params.revision)" in + [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;; + *) echo "backend_core_artifact_revision_must_be_full_sha=$(params.revision)" >&2; exit 2 ;; + esac + case "$dockerfile" in + /*|*..*|""|*latest*) echo "backend_core_artifact_dockerfile_invalid=$dockerfile" >&2; exit 2 ;; + esac + mkdir -p "$(workspaces.source.path)/backend-core-artifact-repo" + find "$(workspaces.source.path)/backend-core-artifact-repo" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + case "$(params.source-host-path)" in + /root/.unidesk/ci/backend-core-artifacts/*) ;; + *) echo "backend_core_artifact_source_host_path_invalid=$(params.source-host-path)" >&2; exit 2 ;; + esac + source_name="$(basename "$(params.source-host-path)")" + source_dir="/prepared-source/$source_name" + test -f "$source_dir/.unidesk-source-commit" + prepared_commit="$(cat "$source_dir/.unidesk-source-commit")" + test "$prepared_commit" = "$(params.revision)" + cp -a "$source_dir/." "$(workspaces.source.path)/backend-core-artifact-repo/" + cd "$(workspaces.source.path)/backend-core-artifact-repo" + test -f "$dockerfile" + test -d src/components/backend-core/src + printf '%s\n' "$prepared_commit" | tee "$(workspaces.source.path)/backend-core-artifact-commit.txt" + printf '%s\n' "$dockerfile" | tee "$(workspaces.source.path)/backend-core-artifact-dockerfile.txt" + - name: build-and-push + image: "$(params.app-image)" + imagePullPolicy: Never + workingDir: "$(workspaces.source.path)/backend-core-artifact-repo" + env: + - name: DOCKER_HOST + value: unix:///var/run/docker.sock + - name: HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: HTTPS_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: ALL_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local" + - name: http_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: https_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: all_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: no_proxy + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + script: | + #!/usr/bin/env bash + set -euo pipefail + commit="$(cat "$(workspaces.source.path)/backend-core-artifact-commit.txt")" + dockerfile="$(cat "$(workspaces.source.path)/backend-core-artifact-dockerfile.txt")" + registry="$(params.registry)" + image_repository="$(params.image-repository)" + test "$registry" = "127.0.0.1:5000" || { echo "backend_core_artifact_registry_must_be_g14_loopback=$registry" >&2; exit 2; } + case "$image_repository" in + ""|/*|*..*|*:*|*@*|*latest*|*[!a-z0-9._/-]*) echo "backend_core_artifact_image_repository_invalid=$image_repository" >&2; exit 2 ;; + esac + local_image="$image_repository:$commit" + registry_image="$registry/$image_repository:$commit" + repository="$registry/$image_repository" + command -v docker + docker version >/dev/null + docker run --rm --network host rancher/mirrored-library-busybox:1.36.1 wget -q -O- "http://$registry/v2/" >/dev/null + DOCKER_BUILDKIT=0 docker build \ + --network host \ + --build-arg HTTP_PROXY=http://127.0.0.1:18789 \ + --build-arg HTTPS_PROXY=http://127.0.0.1:18789 \ + --build-arg ALL_PROXY=http://127.0.0.1:18789 \ + --build-arg NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal \ + --label "unidesk.ai/service-id=backend-core" \ + --label "unidesk.ai/source-repo=$(params.repo-url)" \ + --label "unidesk.ai/source-commit=$commit" \ + --label "unidesk.ai/dockerfile=$dockerfile" \ + -t "$local_image" \ + -t "$registry_image" \ + -f "$dockerfile" \ + . + actual_commit="$(docker image inspect "$registry_image" --format '{{ index .Config.Labels "unidesk.ai/source-commit" }}')" + test "$actual_commit" = "$commit" + docker push "$registry_image" + docker pull "$registry_image" >/dev/null + repo_digests="$(docker image inspect "$registry_image" --format '{{json .RepoDigests}}')" + digest="$(docker image inspect "$registry_image" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -F@ -v repo="$repository" '$1 == repo { print $2; exit }')" + test -n "$digest" + digest_ref="$repository@$digest" + printf '%s' "backend-core" > "$(results.backend_core_artifact_service_id.path)" + printf '%s' "$commit" > "$(results.backend_core_artifact_source_commit.path)" + printf '%s' "$(params.repo-url)" > "$(results.backend_core_artifact_source_repo.path)" + printf '%s' "$dockerfile" > "$(results.backend_core_artifact_dockerfile.path)" + printf '%s' "$registry" > "$(results.backend_core_artifact_registry.path)" + printf '%s' "$repository" > "$(results.backend_core_artifact_repository.path)" + printf '%s' "$registry_image" > "$(results.backend_core_artifact_image.path)" + printf '%s' "$commit" > "$(results.backend_core_artifact_tag.path)" + printf '%s' "$digest" > "$(results.backend_core_artifact_digest.path)" + printf '%s' "$digest_ref" > "$(results.backend_core_artifact_digest_ref.path)" + printf 'backend_core_artifact_service_id=backend-core\nbackend_core_artifact_image=%s\nbackend_core_artifact_repository=%s\nbackend_core_artifact_tag=%s\nbackend_core_artifact_digest=%s\nbackend_core_artifact_digest_ref=%s\nbackend_core_artifact_source_commit=%s\nbackend_core_artifact_source_repo=%s\nbackend_core_artifact_dockerfile=%s\nbackend_core_artifact_registry=%s\nbackend_core_artifact_repo_digests=%s\n' "$registry_image" "$repository" "$commit" "$digest" "$digest_ref" "$commit" "$(params.repo-url)" "$dockerfile" "$registry" "$repo_digests" +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: unidesk-backend-core-artifact-publish + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: backend-core-artifact + app.kubernetes.io/part-of: unidesk +spec: + params: + - name: repo-url + type: string + default: https://github.com/pikasTech/unidesk + - name: revision + type: string + - name: dockerfile + type: string + default: src/components/backend-core/Dockerfile + - name: image-repository + type: string + default: unidesk/backend-core + - name: app-image + type: string + default: unidesk-code-queue:dev + - name: registry + type: string + default: 127.0.0.1:5000 + - name: source-host-path + type: string + workspaces: + - name: shared-workspace + tasks: + - name: publish-backend-core-artifact + taskRef: + name: unidesk-backend-core-artifact-publish + params: + - name: repo-url + value: "$(params.repo-url)" + - name: revision + value: "$(params.revision)" + - name: dockerfile + value: "$(params.dockerfile)" + - name: image-repository + value: "$(params.image-repository)" + - name: app-image + value: "$(params.app-image)" + - name: registry + value: "$(params.registry)" + - name: source-host-path + value: "$(params.source-host-path)" + workspaces: + - name: source + workspace: shared-workspace +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: unidesk-user-service-artifact-publish + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: user-service-artifact +spec: + params: + - name: repo-url + type: string + - name: revision + type: string + - name: service-id + type: string + - name: dockerfile + type: string + - name: image-repository + type: string + - name: app-image + type: string + default: unidesk-code-queue:dev + - name: registry + type: string + default: 127.0.0.1:5000 + - name: source-host-path + type: string + results: + - name: user_service_artifact_service_id + description: CI artifact summary service id. + - name: user_service_artifact_source_commit + description: Full Git source commit used to build the artifact. + - name: user_service_artifact_source_repo + description: Source repository URL used to build the artifact. + - name: user_service_artifact_dockerfile + description: Repo-relative Dockerfile path used to build the artifact. + - name: user_service_artifact_registry + description: G14 loopback registry host. + - name: user_service_artifact_repository + description: Registry repository without tag or digest. + - name: user_service_artifact_image + description: Commit-tagged image reference. + - name: user_service_artifact_tag + description: Commit-pinned image tag. + - name: user_service_artifact_digest + description: Registry manifest digest. + - name: user_service_artifact_digest_ref + description: Immutable image digest reference. + workspaces: + - name: source + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + - name: prepared-source + hostPath: + path: /root/.unidesk/ci/user-service-artifacts + type: Directory + steps: + - name: prepare-source + image: alpine/git:2.45.2 + volumeMounts: + - name: prepared-source + mountPath: /prepared-source + readOnly: true + script: | + #!/bin/sh + set -eu + service_id="$(params.service-id)" + dockerfile="$(params.dockerfile)" + source_host_path="$(params.source-host-path)" + case "$(params.revision)" in + [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;; + *) echo "user_service_artifact_revision_must_be_full_sha=$(params.revision)" >&2; exit 2 ;; + esac + printf '%s\n' "$service_id" | grep -Eq '^[a-z0-9]([-a-z0-9]{0,62}[a-z0-9])?$' || { echo "user_service_artifact_service_id_invalid=$service_id" >&2; exit 2; } + case "$dockerfile" in + /*|*..*|""|*latest*) echo "user_service_artifact_dockerfile_invalid=$dockerfile" >&2; exit 2 ;; + esac + expected_prefix="/root/.unidesk/ci/user-service-artifacts/$service_id/" + case "$source_host_path" in + "$expected_prefix"*) ;; + *) echo "user_service_artifact_source_host_path_invalid=$source_host_path" >&2; exit 2 ;; + esac + mkdir -p "$(workspaces.source.path)/user-service-artifact-repo" + find "$(workspaces.source.path)/user-service-artifact-repo" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + source_name="$(basename "$source_host_path")" + source_dir="/prepared-source/$service_id/$source_name" + test -f "$source_dir/.unidesk-source-commit" + test -f "$source_dir/.unidesk-service-id" + test -f "$source_dir/.unidesk-dockerfile" + prepared_commit="$(cat "$source_dir/.unidesk-source-commit")" + prepared_service_id="$(cat "$source_dir/.unidesk-service-id")" + prepared_dockerfile="$(cat "$source_dir/.unidesk-dockerfile")" + test "$prepared_commit" = "$(params.revision)" + test "$prepared_service_id" = "$service_id" + test "$prepared_dockerfile" = "$dockerfile" + cp -a "$source_dir/." "$(workspaces.source.path)/user-service-artifact-repo/" + cd "$(workspaces.source.path)/user-service-artifact-repo" + test -f "$dockerfile" + if [ "$service_id" = "claudeqq" ]; then + test -f "$(dirname "$dockerfile")/scripts/src/server_ts/package.json" + test -d "$(dirname "$dockerfile")/scripts/src/server_ts/src" + elif [ "$service_id" = "frontend" ]; then + test -d src/components/frontend/src + else + test -d "$(dirname "$dockerfile")/src" + fi + printf '%s\n' "$prepared_commit" | tee "$(workspaces.source.path)/user-service-artifact-commit.txt" + printf '%s\n' "$service_id" | tee "$(workspaces.source.path)/user-service-artifact-service-id.txt" + printf '%s\n' "$dockerfile" | tee "$(workspaces.source.path)/user-service-artifact-dockerfile.txt" + - name: build-and-push + image: "$(params.app-image)" + imagePullPolicy: Never + workingDir: "$(workspaces.source.path)/user-service-artifact-repo" + env: + - name: DOCKER_HOST + value: unix:///var/run/docker.sock + - name: HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: HTTPS_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: ALL_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local" + - name: http_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: https_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: all_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: no_proxy + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + script: | + #!/usr/bin/env bash + set -euo pipefail + commit="$(cat "$(workspaces.source.path)/user-service-artifact-commit.txt")" + service_id="$(cat "$(workspaces.source.path)/user-service-artifact-service-id.txt")" + dockerfile="$(cat "$(workspaces.source.path)/user-service-artifact-dockerfile.txt")" + registry="$(params.registry)" + image_repository="$(params.image-repository)" + test "$registry" = "127.0.0.1:5000" || { echo "user_service_artifact_registry_must_be_g14_loopback=$registry" >&2; exit 2; } + case "$image_repository" in + ""|/*|*..*|*:*|*@*|*latest*|*[!a-z0-9._/-]*) echo "user_service_artifact_image_repository_invalid=$image_repository" >&2; exit 2 ;; + esac + case "$commit" in + [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;; + *) echo "user_service_artifact_commit_invalid=$commit" >&2; exit 2 ;; + esac + local_image="$image_repository:$commit" + repository="$registry/$image_repository" + registry_image="$repository:$commit" + command -v docker + docker version >/dev/null + docker run --rm --network host rancher/mirrored-library-busybox:1.36.1 wget -q -O- "http://$registry/v2/" >/dev/null + build_context=. + if [ "$service_id" = "claudeqq" ]; then + build_context="$(dirname "$dockerfile")" + fi + base_args=() + if [ "$service_id" = "code-queue" ]; then + if docker image inspect unidesk-code-queue:g14 >/dev/null 2>&1; then + base_args=(--build-arg CODE_QUEUE_BASE_IMAGE=unidesk-code-queue:g14) + echo "user_service_artifact_base_image=unidesk-code-queue:g14" + else + echo "user_service_artifact_base_image_missing=unidesk-code-queue:g14" >&2 + echo "user_service_artifact_slow_build_blocked=true" >&2 + exit 2 + fi + else + echo "user_service_artifact_base_image=default" + fi + DOCKER_BUILDKIT=0 docker build \ + --network host \ + "${base_args[@]}" \ + --build-arg HTTP_PROXY=http://127.0.0.1:18789 \ + --build-arg HTTPS_PROXY=http://127.0.0.1:18789 \ + --build-arg ALL_PROXY=http://127.0.0.1:18789 \ + --build-arg NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal \ + --label "unidesk.ai/service-id=$service_id" \ + --label "unidesk.ai/source-repo=$(params.repo-url)" \ + --label "unidesk.ai/source-commit=$commit" \ + --label "unidesk.ai/dockerfile=$dockerfile" \ + -t "$local_image" \ + -t "$registry_image" \ + -f "$dockerfile" \ + "$build_context" + actual_service="$(docker image inspect "$registry_image" --format '{{ index .Config.Labels "unidesk.ai/service-id" }}')" + actual_commit="$(docker image inspect "$registry_image" --format '{{ index .Config.Labels "unidesk.ai/source-commit" }}')" + test "$actual_service" = "$service_id" + test "$actual_commit" = "$commit" + docker push "$registry_image" + docker pull "$registry_image" >/dev/null + repo_digests="$(docker image inspect "$registry_image" --format '{{json .RepoDigests}}')" + digest="$(docker image inspect "$registry_image" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -F@ -v repo="$repository" '$1 == repo { print $2; exit }')" + test -n "$digest" + digest_ref="$repository@$digest" + printf '%s' "$service_id" > "$(results.user_service_artifact_service_id.path)" + printf '%s' "$commit" > "$(results.user_service_artifact_source_commit.path)" + printf '%s' "$(params.repo-url)" > "$(results.user_service_artifact_source_repo.path)" + printf '%s' "$dockerfile" > "$(results.user_service_artifact_dockerfile.path)" + printf '%s' "$registry" > "$(results.user_service_artifact_registry.path)" + printf '%s' "$repository" > "$(results.user_service_artifact_repository.path)" + printf '%s' "$registry_image" > "$(results.user_service_artifact_image.path)" + printf '%s' "$commit" > "$(results.user_service_artifact_tag.path)" + printf '%s' "$digest" > "$(results.user_service_artifact_digest.path)" + printf '%s' "$digest_ref" > "$(results.user_service_artifact_digest_ref.path)" + printf 'user_service_artifact_service_id=%s\nuser_service_artifact_image=%s\nuser_service_artifact_repository=%s\nuser_service_artifact_tag=%s\nuser_service_artifact_digest=%s\nuser_service_artifact_digest_ref=%s\nuser_service_artifact_source_commit=%s\nuser_service_artifact_source_repo=%s\nuser_service_artifact_dockerfile=%s\nuser_service_artifact_registry=%s\nuser_service_artifact_repo_digests=%s\n' "$service_id" "$registry_image" "$repository" "$commit" "$digest" "$digest_ref" "$commit" "$(params.repo-url)" "$dockerfile" "$registry" "$repo_digests" +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: unidesk-user-service-artifact-publish + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: user-service-artifact + app.kubernetes.io/part-of: unidesk +spec: + params: + - name: repo-url + type: string + default: https://github.com/pikasTech/unidesk + - name: revision + type: string + - name: service-id + type: string + - name: dockerfile + type: string + - name: image-repository + type: string + - name: app-image + type: string + default: unidesk-code-queue:dev + - name: registry + type: string + default: 127.0.0.1:5000 + - name: source-host-path + type: string + workspaces: + - name: shared-workspace + tasks: + - name: publish-user-service-artifact + taskRef: + name: unidesk-user-service-artifact-publish + params: + - name: repo-url + value: "$(params.repo-url)" + - name: revision + value: "$(params.revision)" + - name: service-id + value: "$(params.service-id)" + - name: dockerfile + value: "$(params.dockerfile)" + - name: image-repository + value: "$(params.image-repository)" + - name: app-image + value: "$(params.app-image)" + - name: registry + value: "$(params.registry)" + - name: source-host-path + value: "$(params.source-host-path)" + workspaces: + - name: source + workspace: shared-workspace +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: unidesk-code-queue-read-perf + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: code-queue-performance +spec: + params: + - name: revision + type: string + - name: app-image + type: string + default: oven/bun:1-debian + workspaces: + - name: source + steps: + - name: start-read-service + image: "$(params.app-image)" + imagePullPolicy: Never + env: + - name: HTTP_PROXY + value: "" + - name: HTTPS_PROXY + value: "" + - name: ALL_PROXY + value: "" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local" + - name: http_proxy + value: "" + - name: https_proxy + value: "" + - name: all_proxy + value: "" + - name: no_proxy + value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local" + script: | + #!/bin/bash + set -euo pipefail + kube_api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}" + kube_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" + kube_ca="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + kube_namespace="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + kube() { + local method="$1" + shift + curl -fsS --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X "$method" "$@" + } + delete_if_exists() { + local path="$1" + local code + code="$(curl -sS -o /tmp/unidesk-ci-delete-response -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X DELETE "$kube_api/$path")" + if [ "$code" = "200" ] || [ "$code" = "202" ] || [ "$code" = "404" ]; then + return 0 + fi + cat /tmp/unidesk-ci-delete-response >&2 + return 1 + } + wait_deleted() { + local path="$1" + local deadline=$((SECONDS + 120)) + local code + while [ "$SECONDS" -lt "$deadline" ]; do + code="$(curl -sS -o /tmp/unidesk-ci-get-response -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" "$kube_api/$path")" + if [ "$code" = "404" ]; then + return 0 + fi + if [ "$code" != "200" ]; then + cat /tmp/unidesk-ci-get-response >&2 + return 1 + fi + sleep 2 + done + echo "timeout waiting for $path deletion" >&2 + return 1 + } + delete_if_exists "apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read" + delete_if_exists "api/v1/namespaces/$kube_namespace/services/code-queue-ci-read" + wait_deleted "apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read" + cat >/tmp/code-queue-ci-read-deployment.yaml </dev/null + kube PATCH \ + -H "Content-Type: application/apply-patch+yaml" \ + --data-binary @/tmp/code-queue-ci-read-01 \ + "$kube_api/api/v1/namespaces/$kube_namespace/services/code-queue-ci-read?fieldManager=unidesk-ci&force=true" >/dev/null + deadline=$((SECONDS + 420)) + while [ "$SECONDS" -lt "$deadline" ]; do + status="$(kube GET "$kube_api/apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read")" + replicas="$(printf '%s' "$status" | jq -r '.spec.replicas // 1')" + available="$(printf '%s' "$status" | jq -r '.status.availableReplicas // 0')" + updated="$(printf '%s' "$status" | jq -r '.status.updatedReplicas // 0')" + observed="$(printf '%s' "$status" | jq -r '.status.observedGeneration // 0')" + generation="$(printf '%s' "$status" | jq -r '.metadata.generation // 0')" + if [ "$available" -ge "$replicas" ] && [ "$updated" -ge "$replicas" ] && [ "$observed" -ge "$generation" ]; then + echo "code_queue_ci_read_rollout=available replicas=$available generation=$generation" + exit 0 + fi + sleep 2 + done + echo "code_queue_ci_read_rollout=timeout" >&2 + kube GET "$kube_api/apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read" >&2 + exit 1 + - name: measure + image: "$(params.app-image)" + imagePullPolicy: Never + workingDir: "$(workspaces.source.path)/repo" + env: + - name: CI_CODE_QUEUE_URL + value: "http://code-queue-ci-read.unidesk-ci.svc.cluster.local:4222" + - name: HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: HTTPS_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: ALL_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local" + - name: http_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: https_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: all_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: no_proxy + value: "localhost,127.0.0.1,::1,code-queue-ci-read,code-queue-ci-read.unidesk-ci,code-queue-ci-read.unidesk-ci.svc,code-queue-ci-read.unidesk-ci.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local" + - name: FIRST_PAINT_BUDGET_MS + valueFrom: + configMapKeyRef: + name: unidesk-ci-budgets + key: firstPaintMs + - name: TRACE_SUMMARY_BUDGET_MS + valueFrom: + configMapKeyRef: + name: unidesk-ci-budgets + key: traceSummaryMs + - name: TRACE_STEPS_BUDGET_MS + valueFrom: + configMapKeyRef: + name: unidesk-ci-budgets + key: traceStepsMs + - name: TRACE_STEP_DETAIL_BUDGET_MS + valueFrom: + configMapKeyRef: + name: unidesk-ci-budgets + key: traceStepDetailMs + - name: OVERVIEW_P95_BUDGET_MS + valueFrom: + configMapKeyRef: + name: unidesk-ci-budgets + key: overviewP95Ms + script: | + #!/usr/bin/env bash + set -euo pipefail + bun scripts/ci-code-queue-read-perf.ts + - name: cleanup + image: "$(params.app-image)" + imagePullPolicy: Never + env: + - name: HTTP_PROXY + value: "" + - name: HTTPS_PROXY + value: "" + - name: ALL_PROXY + value: "" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local" + - name: http_proxy + value: "" + - name: https_proxy + value: "" + - name: all_proxy + value: "" + - name: no_proxy + value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local" + script: | + #!/bin/bash + set -euo pipefail + kube_api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}" + kube_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" + kube_ca="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + kube_namespace="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + delete_resource() { + local path="$1" + local code + code="$(curl -sS -o /tmp/unidesk-ci-delete-response -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X DELETE "$kube_api/$path")" + if [ "$code" = "200" ] || [ "$code" = "202" ] || [ "$code" = "404" ]; then + return 0 + fi + cat /tmp/unidesk-ci-delete-response >&2 + return 1 + } + delete_resource "apis/apps/v1/namespaces/$kube_namespace/deployments/code-queue-ci-read" + delete_resource "api/v1/namespaces/$kube_namespace/services/code-queue-ci-read" +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: unidesk-dev-namespace-e2e + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: dev-namespace-e2e +spec: + params: + - name: repo-url + type: string + default: https://github.com/pikasTech/unidesk + - name: desired-ref + type: string + default: master + - name: deploy-commit + type: string + - name: environment + type: string + default: dev + - name: run-id + type: string + - name: keep-namespace + type: string + default: "false" + - name: app-image + type: string + default: unidesk-code-queue:dev + - name: backend-core-commit + type: string + default: unknown + - name: frontend-commit + type: string + default: unknown + - name: code-queue-commit + type: string + default: unknown + - name: deploy-json-b64 + type: string + default: "" + workspaces: + - name: source + steps: + - name: namespace-smoke-e2e + image: "$(params.app-image)" + imagePullPolicy: Never + env: + - name: HTTP_PROXY + value: "" + - name: HTTPS_PROXY + value: "" + - name: ALL_PROXY + value: "" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local" + - name: http_proxy + value: "" + - name: https_proxy + value: "" + - name: all_proxy + value: "" + - name: no_proxy + value: "localhost,127.0.0.1,::1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster.local" + script: | + #!/usr/bin/env bash + set -euo pipefail + ns="unidesk-ci-e2e-$(params.run-id)" + keep="$(params.keep-namespace)" + run_dir="$(workspaces.source.path)/dev-e2e-$(params.run-id)" + mkdir -p "$run_dir" + result_json="$run_dir/dev-e2e-result.json" + deploy_json_b64="$(params.deploy-json-b64)" + backend_commit="$(params.backend-core-commit)" + frontend_commit="$(params.frontend-commit)" + code_queue_commit="$(params.code-queue-commit)" + app_image="$(params.app-image)" + database_url="postgres://unidesk_ci:unidesk_ci_password@postgres-dev.$ns.svc.cluster.local:5432/unidesk_ci" + kube_api="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}" + kube_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" + kube_ca="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + kube() { + local method="$1" + shift + curl -fsS --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X "$method" "$@" + } + delete_path() { + local path="$1" + local code + code="$(curl -sS -o /tmp/unidesk-dev-e2e-delete-response -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X DELETE "$kube_api/$path")" + if [ "$code" = "200" ] || [ "$code" = "202" ] || [ "$code" = "404" ]; then + return 0 + fi + cat /tmp/unidesk-dev-e2e-delete-response >&2 + return 1 + } + proxy_get() { + local service_name="$1" + local path="$2" + local output_path="${3:-}" + local tries="${4:-1}" + local response_path="/tmp/unidesk-dev-e2e-proxy-${service_name}-$(printf '%s' "$path" | tr -c 'A-Za-z0-9' '-').json" + local code="000" + local attempt=1 + while [ "$attempt" -le "$tries" ]; do + code="$(curl -sS -o "$response_path" -w "%{http_code}" --cacert "$kube_ca" -H "Authorization: Bearer $kube_token" \ + "$kube_api/api/v1/namespaces/$ns/services/${service_name}:4222/proxy$path" || true)" + if [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; then + if [ -n "$output_path" ]; then + cp "$response_path" "$output_path" + else + cat "$response_path" + fi + return 0 + fi + echo "dev_e2e_proxy_get_retry service=$service_name path=$path status=$code attempt=$attempt/$tries" >&2 + cat "$response_path" >&2 || true + if [ "$attempt" -lt "$tries" ]; then sleep 2; fi + attempt=$((attempt + 1)) + done + if [ -n "$output_path" ]; then cp "$response_path" "$output_path" 2>/dev/null || true; fi + return 22 + } + proxy_json() { + local method="$1" + local service_name="$2" + local path="$3" + local body="${4:-}" + local output_path="${5:-}" + local tries="${6:-1}" + local response_path="/tmp/unidesk-dev-e2e-proxy-${service_name}-${method}-$(printf '%s' "$path" | tr -c 'A-Za-z0-9' '-').json" + local code="000" + local attempt=1 + local curl_args=(--cacert "$kube_ca" -H "Authorization: Bearer $kube_token" -X "$method") + if [ -n "$body" ]; then + curl_args+=(-H "Content-Type: application/json" --data "$body") + fi + while [ "$attempt" -le "$tries" ]; do + code="$(curl -sS -o "$response_path" -w "%{http_code}" "${curl_args[@]}" \ + "$kube_api/api/v1/namespaces/$ns/services/${service_name}:4222/proxy$path" || true)" + if [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; then + if [ -n "$output_path" ]; then + cp "$response_path" "$output_path" + else + cat "$response_path" + fi + return 0 + fi + echo "dev_e2e_proxy_json_retry method=$method service=$service_name path=$path status=$code attempt=$attempt/$tries" >&2 + cat "$response_path" >&2 || true + if [ "$attempt" -lt "$tries" ]; then sleep 2; fi + attempt=$((attempt + 1)) + done + if [ -n "$output_path" ]; then cp "$response_path" "$output_path" 2>/dev/null || true; fi + return 22 + } + wait_deployment_available() { + local deployment="$1" + local timeout_seconds="$2" + local deadline=$((SECONDS + timeout_seconds)) + while [ "$SECONDS" -lt "$deadline" ]; do + status="$(kube GET "$kube_api/apis/apps/v1/namespaces/$ns/deployments/$deployment")" + replicas="$(printf '%s' "$status" | jq -r '.spec.replicas // 1')" + available="$(printf '%s' "$status" | jq -r '.status.availableReplicas // 0')" + updated="$(printf '%s' "$status" | jq -r '.status.updatedReplicas // 0')" + observed="$(printf '%s' "$status" | jq -r '.status.observedGeneration // 0')" + generation="$(printf '%s' "$status" | jq -r '.metadata.generation // 0')" + if [ "$available" -ge "$replicas" ] && [ "$updated" -ge "$replicas" ] && [ "$observed" -ge "$generation" ]; then + echo "dev_e2e_rollout=available deployment=$deployment namespace=$ns replicas=$available generation=$generation" + return 0 + fi + sleep 2 + done + echo "dev_e2e_rollout=timeout deployment=$deployment namespace=$ns" >&2 + kube GET "$kube_api/apis/apps/v1/namespaces/$ns/deployments/$deployment" >&2 + return 1 + } + apply_code_queue_role() { + local role="$1" + local readiness_path="$2" + local scheduler_enabled="$3" + local name="code-queue-${role}-dev" + cat >/tmp/dev-e2e-code-queue-$role.yaml </dev/null + kube PATCH -H "Content-Type: application/apply-patch+yaml" --data-binary @/tmp/dev-e2e-code-queue-$role-01 "$kube_api/api/v1/namespaces/$ns/services/$name?fieldManager=unidesk-ci&force=true" >/dev/null + } + cleanup() { + if [ "$keep" = "true" ]; then + echo "dev_e2e_namespace_retained=$ns" + return 0 + fi + delete_path "api/v1/namespaces/$ns" || true + echo "dev_e2e_namespace_deleted=$ns" + } + trap cleanup EXIT + + delete_path "api/v1/namespaces/$ns" + cat >/tmp/dev-e2e-namespace.yaml </dev/null + + cat >/tmp/dev-e2e-configmap.yaml </dev/null + + database_url_b64="$(printf '%s' "$database_url" | base64 -w0)" + cat >/tmp/dev-e2e-secret.yaml </dev/null + + cat >/tmp/dev-e2e-postgres.yaml </dev/null + kube PATCH \ + -H "Content-Type: application/apply-patch+yaml" \ + --data-binary @/tmp/dev-e2e-postgres-01 \ + "$kube_api/api/v1/namespaces/$ns/services/postgres-dev?fieldManager=unidesk-ci&force=true" >/dev/null + wait_deployment_available postgres-dev 180 + + apply_code_queue_role scheduler /health true + apply_code_queue_role read /live false + apply_code_queue_role write /health false + wait_deployment_available code-queue-scheduler-dev 420 + wait_deployment_available code-queue-read-dev 420 + wait_deployment_available code-queue-write-dev 420 + + proxy_get code-queue-write-dev /health "$run_dir/code-queue-write-health.json" 30 + proxy_get code-queue-scheduler-dev /health "$run_dir/code-queue-scheduler-health.json" 30 + proxy_get code-queue-read-dev /live "$run_dir/code-queue-read-live.json" 30 + proxy_get code-queue-read-dev /api/workdirs "$run_dir/code-queue-read-workdirs.json" 30 + proxy_json POST code-queue-write-dev /api/workdirs '{"providerId":"G14-dev","executionMode":"default","path":"ci-workdirs-smoke"}' "$run_dir/code-queue-workdir-created.json" 30 + proxy_get code-queue-write-dev '/api/workdirs?providerId=G14-dev&executionMode=default' "$run_dir/code-queue-write-workdirs.json" 30 + proxy_json DELETE code-queue-write-dev /api/workdirs/G14-dev/default/ci-workdirs-smoke "" "$run_dir/code-queue-workdir-deleted.json" 30 + + bun - "$ns" "$(params.deploy-commit)" "$backend_commit" "$frontend_commit" "$code_queue_commit" "$app_image" "$result_json" "$run_dir" <<'BUN' + const [ns, deployCommit, backendCommit, frontendCommit, codeQueueCommit, appImage, resultPath, runDir] = process.argv.slice(2); + const runBase = new URL(`file://${runDir.replace(/\/+$/u, "")}/`); + async function readJson(name) { + return await Bun.file(new URL(name, runBase)).json(); + } + const health = await readJson("code-queue-write-health.json"); + const scheduler = await readJson("code-queue-scheduler-health.json"); + const readLive = await readJson("code-queue-read-live.json"); + const initialWorkdirs = await readJson("code-queue-read-workdirs.json"); + const created = await readJson("code-queue-workdir-created.json"); + const listed = await readJson("code-queue-write-workdirs.json"); + const deleted = await readJson("code-queue-workdir-deleted.json"); + const checks = [ + health?.ok === true && health?.role === "write" && health?.deploy?.commit === codeQueueCommit, + scheduler?.ok === true && scheduler?.role === "scheduler" && scheduler?.schedulerEnabled === true, + readLive?.ok === true && readLive?.role === "read", + initialWorkdirs?.ok === true && Array.isArray(initialWorkdirs?.workdirs), + created?.ok === true && created?.workdir?.providerId === "G14-dev" && created?.workdir?.path === "/workspace-dev/ci-workdirs-smoke", + Array.isArray(listed?.workdirs) && listed.workdirs.some((item) => item?.path === "/workspace-dev/ci-workdirs-smoke"), + deleted?.ok === true, + ]; + const result = { + ok: checks.every(Boolean), + namespace: ns, + deployCommit, + backendCoreCommit: backendCommit, + frontendCommit, + codeQueueCommit, + codeQueueImage: appImage, + accessPath: "kubernetes-api-service-proxy", + services: ["code-queue-scheduler-dev", "code-queue-read-dev", "code-queue-write-dev"], + health, + scheduler, + readLive, + initialWorkdirs, + created, + listed, + deleted, + }; + await Bun.write(resultPath, `${JSON.stringify(result, null, 2)}\n`); + console.log(JSON.stringify(result)); + if (!result.ok) { + process.exit(1); + } + BUN +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: unidesk-dev-namespace-e2e + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/component: dev-namespace-e2e + app.kubernetes.io/part-of: unidesk +spec: + params: + - name: repo-url + type: string + default: https://github.com/pikasTech/unidesk + - name: desired-ref + type: string + default: master + - name: deploy-commit + type: string + - name: environment + type: string + default: dev + - name: run-id + type: string + - name: keep-namespace + type: string + default: "false" + - name: app-image + type: string + default: unidesk-code-queue:dev + - name: backend-core-commit + type: string + default: unknown + - name: frontend-commit + type: string + default: unknown + - name: code-queue-commit + type: string + default: unknown + - name: deploy-json-b64 + type: string + default: "" + workspaces: + - name: shared-workspace + tasks: + - name: dev-namespace-e2e + taskRef: + name: unidesk-dev-namespace-e2e + params: + - name: repo-url + value: "$(params.repo-url)" + - name: desired-ref + value: "$(params.desired-ref)" + - name: deploy-commit + value: "$(params.deploy-commit)" + - name: environment + value: "$(params.environment)" + - name: run-id + value: "$(params.run-id)" + - name: keep-namespace + value: "$(params.keep-namespace)" + - name: app-image + value: "$(params.app-image)" + - name: backend-core-commit + value: "$(params.backend-core-commit)" + - name: frontend-commit + value: "$(params.frontend-commit)" + - name: code-queue-commit + value: "$(params.code-queue-commit)" + - name: deploy-json-b64 + value: "$(params.deploy-json-b64)" + workspaces: + - name: source + workspace: shared-workspace +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: unidesk-ci + namespace: unidesk-ci + labels: + app.kubernetes.io/name: unidesk-ci + app.kubernetes.io/part-of: unidesk +spec: + params: + - name: repo-url + type: string + default: https://github.com/pikasTech/unidesk + - name: revision + type: string + - name: check-image + type: string + default: unidesk-code-queue:dev + - name: code-queue-image + type: string + default: unidesk-code-queue:dev + workspaces: + - name: shared-workspace + tasks: + - name: repo-check + taskRef: + name: unidesk-repo-check + params: + - name: repo-url + value: "$(params.repo-url)" + - name: revision + value: "$(params.revision)" + - name: image + value: "$(params.check-image)" + workspaces: + - name: source + workspace: shared-workspace + - name: code-queue-read-perf + runAfter: + - repo-check + taskRef: + name: unidesk-code-queue-read-perf + params: + - name: revision + value: "$(params.revision)" + - name: app-image + value: "$(params.code-queue-image)" + workspaces: + - name: source + workspace: shared-workspace diff --git a/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k3s.json b/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k3s.json new file mode 100644 index 00000000..35987943 --- /dev/null +++ b/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k3s.json @@ -0,0 +1,228 @@ +[ + { + "apiVersion": "unidesk.ai/k3s/v1", + "kind": "ManagedKubernetesService", + "metadata": { + "name": "code-queue", + "namespace": "unidesk" + }, + "spec": { + "adapterServiceId": "k3sctl-adapter-g14", + "controlPlane": { + "type": "kubernetes", + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" + }, + "route": { + "kind": "kubernetes-service", + "serviceName": "code-queue-scheduler", + "servicePort": 4222, + "deploymentName": "code-queue" + }, + "activeInstanceId": "G14", + "singleWriter": true, + "expectedNodeIds": [ + "G14" + ], + "instances": [ + { + "id": "G14", + "nodeId": "G14", + "role": "primary", + "baseUrl": "kubernetes://unidesk/services/code-queue-scheduler:4222", + "healthPath": "/health", + "healthMode": "service-proxy" + } + ], + "requireAllInstancesHealthy": false + } + }, + { + "apiVersion": "unidesk.ai/k3s/v1", + "kind": "ManagedKubernetesService", + "metadata": { + "name": "code-queue-read", + "namespace": "unidesk" + }, + "spec": { + "adapterServiceId": "k3sctl-adapter-g14", + "controlPlane": { + "type": "kubernetes", + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" + }, + "route": { + "kind": "kubernetes-service", + "serviceName": "code-queue-read", + "servicePort": 4222 + }, + "activeInstanceId": "G14-read", + "singleWriter": false, + "expectedNodeIds": [ + "G14" + ], + "instances": [ + { + "id": "G14-read", + "nodeId": "G14", + "role": "standby", + "baseUrl": "kubernetes://unidesk/services/code-queue-read:4222", + "healthPath": "/live", + "healthMode": "service-proxy" + } + ], + "requireAllInstancesHealthy": false + } + }, + { + "apiVersion": "unidesk.ai/k3s/v1", + "kind": "ManagedKubernetesService", + "metadata": { + "name": "code-queue-write", + "namespace": "unidesk" + }, + "spec": { + "adapterServiceId": "k3sctl-adapter-g14", + "controlPlane": { + "type": "kubernetes", + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" + }, + "route": { + "kind": "kubernetes-service", + "serviceName": "code-queue-write", + "servicePort": 4222 + }, + "activeInstanceId": "G14-write", + "singleWriter": true, + "expectedNodeIds": [ + "G14" + ], + "instances": [ + { + "id": "G14-write", + "nodeId": "G14", + "role": "primary", + "baseUrl": "kubernetes://unidesk/services/code-queue-write:4222", + "healthPath": "/health", + "healthMode": "service-proxy" + } + ], + "requireAllInstancesHealthy": false + } + }, + { + "apiVersion": "unidesk.ai/k3s/v1", + "kind": "ManagedKubernetesService", + "metadata": { + "name": "code-queue-scheduler", + "namespace": "unidesk" + }, + "spec": { + "adapterServiceId": "k3sctl-adapter-g14", + "controlPlane": { + "type": "kubernetes", + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" + }, + "route": { + "kind": "kubernetes-service", + "serviceName": "code-queue-scheduler", + "servicePort": 4222, + "deploymentName": "code-queue" + }, + "activeInstanceId": "G14-scheduler", + "singleWriter": true, + "expectedNodeIds": [ + "G14" + ], + "instances": [ + { + "id": "G14-scheduler", + "nodeId": "G14", + "role": "primary", + "baseUrl": "kubernetes://unidesk/services/code-queue-scheduler:4222", + "healthPath": "/health", + "healthMode": "service-proxy" + } + ], + "requireAllInstancesHealthy": false + } + }, + { + "apiVersion": "unidesk.ai/k3s/v1", + "kind": "ManagedKubernetesService", + "metadata": { + "name": "g14-provider-egress-proxy", + "namespace": "unidesk" + }, + "spec": { + "adapterServiceId": "k3sctl-adapter-g14", + "controlPlane": { + "type": "kubernetes", + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" + }, + "route": { + "kind": "kubernetes-service", + "serviceName": "g14-provider-egress-proxy", + "servicePort": 18789, + "deploymentName": "g14-provider-egress-proxy" + }, + "activeInstanceId": "G14-provider-egress", + "singleWriter": false, + "expectedNodeIds": [ + "G14" + ], + "instances": [ + { + "id": "G14-provider-egress", + "nodeId": "G14", + "role": "primary", + "baseUrl": "kubernetes://unidesk/services/g14-provider-egress-proxy:18789", + "healthPath": "/__unidesk/egress-proxy/health", + "healthMode": "pod-ready" + } + ], + "requireAllInstancesHealthy": false + } + }, + { + "apiVersion": "unidesk.ai/k3s/v1", + "kind": "ManagedKubernetesService", + "metadata": { + "name": "g14-tcp-egress-gateway", + "namespace": "unidesk" + }, + "spec": { + "adapterServiceId": "k3sctl-adapter-g14", + "controlPlane": { + "type": "kubernetes", + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" + }, + "route": { + "kind": "kubernetes-service", + "serviceName": "g14-tcp-egress-gateway", + "servicePort": 18080, + "deploymentName": "g14-tcp-egress-gateway" + }, + "activeInstanceId": "G14-tcp-egress", + "singleWriter": false, + "expectedNodeIds": [ + "G14" + ], + "instances": [ + { + "id": "G14-tcp-egress", + "nodeId": "G14", + "role": "primary", + "baseUrl": "kubernetes://unidesk/services/g14-tcp-egress-gateway:18080", + "healthPath": "/health", + "healthMode": "service-proxy" + } + ], + "requireAllInstancesHealthy": false + } + } +] diff --git a/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml new file mode 100644 index 00000000..406cfaec --- /dev/null +++ b/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml @@ -0,0 +1,1231 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: unidesk + labels: + app.kubernetes.io/part-of: unidesk + unidesk.ai/k3s-cluster: unidesk-k3s +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: code-queue-read + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: read + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed + unidesk.ai/instance-id: G14-read +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: read + unidesk.ai/instance-id: G14-read + template: + metadata: + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: read + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed + unidesk.ai/instance-id: G14-read + unidesk.ai/node-id: G14 + spec: + nodeSelector: + unidesk.ai/node-id: G14 + terminationGracePeriodSeconds: 30 + containers: + - name: code-queue + image: unidesk-code-queue:g14 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 4222 + envFrom: + - secretRef: + name: code-queue-env + optional: true + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4222" + - name: DATABASE_URL + value: "postgres://unidesk:unidesk_dev_password@g14-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk" + - name: CODE_QUEUE_INSTANCE_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: CODE_QUEUE_SERVICE_ROLE + value: "read" + - name: CODE_QUEUE_SCHEDULER_ENABLED + value: "false" + - name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED + value: "false" + - name: CODE_QUEUE_DATA_DIR + value: "/var/lib/unidesk/code-queue" + - name: CODE_QUEUE_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_CODEX_HOME + value: "/var/lib/unidesk/code-queue/codex-home" + - name: CODE_QUEUE_OPENCODE_XDG_DIR + value: "/var/lib/unidesk/code-queue/opencode-xdg" + - name: CODE_QUEUE_SOURCE_CODEX_CONFIG + value: "/root/.codex/config.toml" + - name: UNIDESK_SKILLS_PATH + value: "/root/.agents/skills" + - name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH + value: "/root/.agents/skills" + - name: CODE_QUEUE_DEFAULT_MODEL + value: "gpt-5.5" + - name: CODE_QUEUE_MODELS + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" + - name: CODE_QUEUE_MODEL_REASONING_EFFORTS + value: "gpt-5.5=xhigh" + - name: CODE_QUEUE_SANDBOX + value: "danger-full-access" + - name: CODE_QUEUE_APPROVAL_POLICY + value: "never" + - name: CODE_QUEUE_MAX_ACTIVE_QUEUES + value: "0" + - name: CODE_QUEUE_DATABASE_POOL_MAX + value: "2" + - name: NODE_OPTIONS + value: "--max-old-space-size=512" + - name: GIT_CONFIG_COUNT + value: "1" + - name: GIT_CONFIG_KEY_0 + value: "safe.directory" + - name: GIT_CONFIG_VALUE_0 + value: "*" + - name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS + value: "10" + - name: CODE_QUEUE_IN_MEMORY_EVENT_RECORDS + value: "10" + - name: CODE_QUEUE_MAIN_PROVIDER_ID + value: "G14" + - name: CODE_QUEUE_REMOTE_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_EXECUTION_PROVIDER_IDS + value: "G14" + - name: CODE_QUEUE_DEV_CONTAINER_MASTER_HOST + value: "74.48.78.17" + - name: CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID + value: "G14" + - name: CODE_QUEUE_DEV_CONTAINER_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_EGRESS_PROXY_ENABLED + value: "false" + - name: CODE_QUEUE_EGRESS_PROXY_URL + value: "" + - name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database" + - name: HTTP_PROXY + value: "" + - name: HTTPS_PROXY + value: "" + - name: ALL_PROXY + value: "" + - name: http_proxy + value: "" + - name: https_proxy + value: "" + - name: all_proxy + value: "" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database" + - name: no_proxy + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database" + - name: OA_EVENT_FLOW_BASE_URL + value: "http://g14-tcp-egress-gateway.unidesk.svc.cluster.local:4255" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED + value: "false" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL + value: "" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE + value: "private" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID + value: "645275593" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS + value: "12000" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS + value: "15000" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS + value: "3" + - name: LOG_FILE + value: "/var/log/unidesk/code-queue-read.jsonl" + - name: UNIDESK_LOG_RETENTION_BYTES + value: "1GiB" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + - name: workspace + mountPath: /workspace + - name: workspace + mountPath: /home/ubuntu + - name: repo + mountPath: /root/unidesk + - name: repo + mountPath: /app + - name: codex-config + mountPath: /root/.codex/config.toml + readOnly: true + - name: codex-auth + mountPath: /root/.codex/auth.json + readOnly: true + - name: skills-dir + mountPath: /root/.agents/skills + readOnly: true + - name: ssh-dir + mountPath: /root/.ssh + readOnly: true + - name: logs + mountPath: /var/log/unidesk + - name: state + mountPath: /var/lib/unidesk/code-queue + readinessProbe: + httpGet: + path: /live + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 20 + livenessProbe: + httpGet: + path: /live + port: http + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + startupProbe: + httpGet: + path: /live + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + - name: workspace + hostPath: + path: /root/unidesk + type: Directory + - name: repo + hostPath: + path: /root/unidesk + type: Directory + - name: codex-config + hostPath: + path: /root/.codex/config.toml + type: File + - name: codex-auth + hostPath: + path: /root/.codex/auth.json + type: File + - name: skills-dir + hostPath: + path: /root/.agents/skills + type: Directory + - name: ssh-dir + hostPath: + path: /root/.ssh + type: Directory + - name: logs + hostPath: + path: /root/unidesk/.state/code-queue/logs + type: DirectoryOrCreate + - name: state + hostPath: + path: /root/unidesk/.state/code-queue + type: DirectoryOrCreate +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: code-queue-write + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: write + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed + unidesk.ai/instance-id: G14-write +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: write + unidesk.ai/instance-id: G14-write + template: + metadata: + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: write + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed + unidesk.ai/instance-id: G14-write + unidesk.ai/node-id: G14 + spec: + nodeSelector: + unidesk.ai/node-id: G14 + terminationGracePeriodSeconds: 30 + containers: + - name: code-queue + image: unidesk-code-queue:g14 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 4222 + envFrom: + - secretRef: + name: code-queue-env + optional: true + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4222" + - name: DATABASE_URL + value: "postgres://unidesk:unidesk_dev_password@g14-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk" + - name: CODE_QUEUE_INSTANCE_ID + value: "G14-write" + - name: CODE_QUEUE_SERVICE_ROLE + value: "write" + - name: CODE_QUEUE_SCHEDULER_ENABLED + value: "false" + - name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED + value: "false" + - name: CODE_QUEUE_DATA_DIR + value: "/var/lib/unidesk/code-queue" + - name: CODE_QUEUE_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_CODEX_HOME + value: "/var/lib/unidesk/code-queue/codex-home" + - name: CODE_QUEUE_OPENCODE_XDG_DIR + value: "/var/lib/unidesk/code-queue/opencode-xdg" + - name: CODE_QUEUE_SOURCE_CODEX_CONFIG + value: "/root/.codex/config.toml" + - name: UNIDESK_SKILLS_PATH + value: "/root/.agents/skills" + - name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH + value: "/root/.agents/skills" + - name: CODE_QUEUE_DEFAULT_MODEL + value: "gpt-5.5" + - name: CODE_QUEUE_MODELS + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" + - name: CODE_QUEUE_MODEL_REASONING_EFFORTS + value: "gpt-5.5=xhigh" + - name: CODE_QUEUE_SANDBOX + value: "danger-full-access" + - name: CODE_QUEUE_APPROVAL_POLICY + value: "never" + - name: CODE_QUEUE_MAX_ACTIVE_QUEUES + value: "0" + - name: CODE_QUEUE_DATABASE_POOL_MAX + value: "2" + - name: NODE_OPTIONS + value: "--max-old-space-size=768" + - name: GIT_CONFIG_COUNT + value: "1" + - name: GIT_CONFIG_KEY_0 + value: "safe.directory" + - name: GIT_CONFIG_VALUE_0 + value: "*" + - name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS + value: "10" + - name: CODE_QUEUE_IN_MEMORY_EVENT_RECORDS + value: "10" + - name: CODE_QUEUE_MAIN_PROVIDER_ID + value: "G14" + - name: CODE_QUEUE_REMOTE_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_EXECUTION_PROVIDER_IDS + value: "G14" + - name: CODE_QUEUE_DEV_CONTAINER_MASTER_HOST + value: "74.48.78.17" + - name: CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID + value: "G14" + - name: CODE_QUEUE_DEV_CONTAINER_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_EGRESS_PROXY_ENABLED + value: "false" + - name: CODE_QUEUE_EGRESS_PROXY_URL + value: "" + - name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database" + - name: HTTP_PROXY + value: "" + - name: HTTPS_PROXY + value: "" + - name: ALL_PROXY + value: "" + - name: http_proxy + value: "" + - name: https_proxy + value: "" + - name: all_proxy + value: "" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database" + - name: no_proxy + value: "localhost,127.0.0.1,::1,host.docker.internal,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database" + - name: OA_EVENT_FLOW_BASE_URL + value: "http://g14-tcp-egress-gateway.unidesk.svc.cluster.local:4255" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED + value: "false" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL + value: "" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE + value: "private" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID + value: "645275593" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS + value: "12000" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS + value: "15000" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS + value: "3" + - name: LOG_FILE + value: "/var/log/unidesk/code-queue-write.jsonl" + - name: UNIDESK_LOG_RETENTION_BYTES + value: "1GiB" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + - name: workspace + mountPath: /workspace + - name: workspace + mountPath: /home/ubuntu + - name: repo + mountPath: /root/unidesk + - name: repo + mountPath: /app + - name: codex-config + mountPath: /root/.codex/config.toml + readOnly: true + - name: codex-auth + mountPath: /root/.codex/auth.json + readOnly: true + - name: skills-dir + mountPath: /root/.agents/skills + readOnly: true + - name: ssh-dir + mountPath: /root/.ssh + readOnly: true + - name: logs + mountPath: /var/log/unidesk + - name: state + mountPath: /var/lib/unidesk/code-queue + readinessProbe: + httpGet: + path: /live + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 20 + livenessProbe: + httpGet: + path: /live + port: http + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + startupProbe: + httpGet: + path: /live + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + - name: workspace + hostPath: + path: /root/unidesk + type: Directory + - name: repo + hostPath: + path: /root/unidesk + type: Directory + - name: codex-config + hostPath: + path: /root/.codex/config.toml + type: File + - name: codex-auth + hostPath: + path: /root/.codex/auth.json + type: File + - name: skills-dir + hostPath: + path: /root/.agents/skills + type: Directory + - name: ssh-dir + hostPath: + path: /root/.ssh + type: Directory + - name: logs + hostPath: + path: /root/unidesk/.state/code-queue/logs + type: DirectoryOrCreate + - name: state + hostPath: + path: /root/unidesk/.state/code-queue + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: code-queue-scheduler + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: scheduler + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: code-queue + unidesk.ai/instance-id: G14 + ports: + - name: http + port: 4222 + targetPort: http +--- +apiVersion: v1 +kind: Service +metadata: + name: code-queue-read + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: read + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: read + unidesk.ai/instance-id: G14-read + ports: + - name: http + port: 4222 + targetPort: http +--- +apiVersion: v1 +kind: Service +metadata: + name: code-queue-write + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: write + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: code-queue + app.kubernetes.io/component: write + unidesk.ai/instance-id: G14-write + ports: + - name: http + port: 4222 + targetPort: http +--- +apiVersion: v1 +kind: Service +metadata: + name: g14-provider-egress-proxy + namespace: unidesk + labels: + app.kubernetes.io/name: provider-egress-proxy + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: provider-egress-proxy + unidesk.ai/provider-id: G14 + ports: + - name: http + port: 18789 + targetPort: http + protocol: TCP +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: g14-provider-egress-proxy + namespace: unidesk + labels: + app.kubernetes.io/name: provider-egress-proxy + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 +data: + provider-egress-proxy.js: | + const net = require("node:net"); + + const listenPort = Number(process.env.PROVIDER_EGRESS_LISTEN_PORT || 18790); + const upstreamHost = process.env.PROVIDER_EGRESS_UPSTREAM_HOST || "127.0.0.1"; + const upstreamPort = Number(process.env.PROVIDER_EGRESS_UPSTREAM_PORT || 18789); + + function closeBoth(left, right) { + left.destroy(); + right.destroy(); + } + + net.createServer((client) => { + const upstream = net.connect({ host: upstreamHost, port: upstreamPort }); + client.pipe(upstream); + upstream.pipe(client); + client.on("error", () => closeBoth(client, upstream)); + upstream.on("error", () => closeBoth(client, upstream)); + }).listen(listenPort, "0.0.0.0", () => { + console.log(JSON.stringify({ + ts: new Date().toISOString(), + service: "provider-egress-proxy", + level: "info", + message: "listening", + data: { listenPort, upstream: `${upstreamHost}:${upstreamPort}` }, + })); + }); +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: g14-provider-egress-proxy + namespace: unidesk + labels: + app.kubernetes.io/name: provider-egress-proxy + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: provider-egress-proxy + unidesk.ai/provider-id: G14 + template: + metadata: + labels: + app.kubernetes.io/name: provider-egress-proxy + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 + unidesk.ai/node-id: G14 + spec: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: + unidesk.ai/node-id: G14 + containers: + - name: provider-egress-proxy + image: unidesk-code-queue:g14 + imagePullPolicy: IfNotPresent + command: + - bun + - /etc/unidesk-provider-egress/provider-egress-proxy.js + ports: + - name: http + containerPort: 18790 + env: + - name: PROVIDER_EGRESS_LISTEN_PORT + value: "18790" + - name: PROVIDER_EGRESS_UPSTREAM_HOST + value: "127.0.0.1" + - name: PROVIDER_EGRESS_UPSTREAM_PORT + value: "18789" + volumeMounts: + - name: script + mountPath: /etc/unidesk-provider-egress + readOnly: true + readinessProbe: + tcpSocket: + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + livenessProbe: + tcpSocket: + port: http + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + volumes: + - name: script + configMap: + name: g14-provider-egress-proxy +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: g14-tcp-egress-gateway + namespace: unidesk + labels: + app.kubernetes.io/name: tcp-egress-gateway + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 +data: + tcp-egress-gateway.js: | + const net = require("node:net"); + + const proxyUrl = new URL(process.env.TCP_EGRESS_HTTP_PROXY || "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789"); + const proxyHost = proxyUrl.hostname; + const proxyPort = Number(proxyUrl.port || 80); + const healthPort = Number(process.env.TCP_EGRESS_HEALTH_PORT || 18080); + const rawRoutes = process.env.TCP_EGRESS_ROUTES || ""; + const startedAt = new Date().toISOString(); + + function positiveInt(value, fallback) { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; + } + + const connectAttempts = positiveInt(process.env.TCP_EGRESS_CONNECT_ATTEMPTS, 8); + const connectRetryBackoffMs = positiveInt(process.env.TCP_EGRESS_CONNECT_RETRY_BACKOFF_MS, 250); + const maxEarlyClientBytes = positiveInt(process.env.TCP_EGRESS_MAX_EARLY_CLIENT_BYTES, 1024 * 1024); + + function parseRoute(text) { + const parts = text.trim().split("="); + if (parts.length !== 3) throw new Error(`invalid route: ${text}`); + const [name, listenPortText, target] = parts; + const [targetHost, targetPortText] = target.split(":"); + const listenPort = Number(listenPortText); + const targetPort = Number(targetPortText); + if (!name || !targetHost || !Number.isInteger(listenPort) || !Number.isInteger(targetPort)) throw new Error(`invalid route: ${text}`); + return { name, listenPort, targetHost, targetPort }; + } + + const routes = rawRoutes.split(",").map((item) => item.trim()).filter(Boolean).map(parseRoute); + if (routes.length === 0) throw new Error("TCP_EGRESS_ROUTES must define at least one route"); + + function findHeaderEnd(buffer) { + for (let index = 0; index <= buffer.length - 4; index += 1) { + if (buffer[index] === 13 && buffer[index + 1] === 10 && buffer[index + 2] === 13 && buffer[index + 3] === 10) return index + 4; + } + return -1; + } + + function log(level, message, data = {}) { + console.log(JSON.stringify({ ts: new Date().toISOString(), service: "tcp-egress-gateway", level, message, data })); + } + + function pipeViaHttpConnect(client, route) { + const earlyClientChunks = []; + let earlyClientBytes = 0; + let proxy = null; + let tunnelReady = false; + let closed = false; + let attempt = 0; + let retryTimer = null; + + const cleanupProxy = () => { + if (proxy === null) return; + proxy.removeAllListeners(); + proxy.destroy(); + proxy = null; + }; + + const closeBoth = () => { + if (closed) return; + closed = true; + if (retryTimer !== null) clearTimeout(retryTimer); + client.destroy(); + cleanupProxy(); + }; + + const retryOrClose = (reason, data = {}) => { + cleanupProxy(); + if (!closed && attempt < connectAttempts) { + const delayMs = Math.min(connectRetryBackoffMs * attempt, 3000); + log("warn", "connect_retry", { route: route.name, attempt, attempts: connectAttempts, delayMs, reason, ...data }); + retryTimer = setTimeout(() => { + retryTimer = null; + startProxy(); + }, delayMs); + retryTimer.unref?.(); + return; + } + log("warn", "connect_rejected", { route: route.name, attempt, attempts: connectAttempts, reason, ...data }); + closeBoth(); + }; + + client.on("data", (chunk) => { + if (tunnelReady && proxy !== null) { + proxy.write(chunk); + return; + } + earlyClientBytes += chunk.length; + if (earlyClientBytes > maxEarlyClientBytes) { + log("warn", "early_client_buffer_limit_exceeded", { route: route.name, bytes: earlyClientBytes, limit: maxEarlyClientBytes }); + closeBoth(); + return; + } + earlyClientChunks.push(chunk); + }); + client.on("error", closeBoth); + client.on("close", closeBoth); + + const startProxy = () => { + if (closed) return; + attempt += 1; + tunnelReady = false; + const currentProxy = net.connect({ host: proxyHost, port: proxyPort }); + const proxyHeaderChunks = []; + proxy = currentProxy; + + currentProxy.on("connect", () => { + currentProxy.write(`CONNECT ${route.targetHost}:${route.targetPort} HTTP/1.1\r\nHost: ${route.targetHost}:${route.targetPort}\r\n\r\n`); + }); + currentProxy.on("data", (chunk) => { + if (currentProxy !== proxy) return; + if (tunnelReady) { + client.write(chunk); + return; + } + proxyHeaderChunks.push(chunk); + const headerBuffer = Buffer.concat(proxyHeaderChunks); + const headerEnd = findHeaderEnd(headerBuffer); + if (headerEnd < 0) return; + const header = headerBuffer.subarray(0, headerEnd).toString("latin1"); + if (!/^HTTP\/1\.[01] 200\b/u.test(header)) { + const body = headerBuffer.subarray(headerEnd).toString("utf8").slice(0, 300); + retryOrClose("non_200", { header: header.slice(0, 200), body: body.length > 0 ? body : null }); + return; + } + tunnelReady = true; + const remaining = headerBuffer.subarray(headerEnd); + if (remaining.length > 0) client.write(remaining); + for (const early of earlyClientChunks) currentProxy.write(early); + earlyClientChunks.length = 0; + earlyClientBytes = 0; + }); + currentProxy.on("error", (error) => { + if (currentProxy !== proxy) return; + retryOrClose("proxy_error", { error: String(error?.message || error) }); + }); + currentProxy.on("close", () => { + if (currentProxy !== proxy || closed) return; + if (!tunnelReady) { + retryOrClose("proxy_closed_before_tunnel"); + return; + } + closeBoth(); + }); + }; + + startProxy(); + } + + for (const route of routes) { + net.createServer((client) => pipeViaHttpConnect(client, route)).listen(route.listenPort, "0.0.0.0", () => { + log("info", "route_listening", { route: route.name, listenPort: route.listenPort, target: `${route.targetHost}:${route.targetPort}`, proxy: `${proxyHost}:${proxyPort}` }); + }); + } + + Bun.serve({ + hostname: "0.0.0.0", + port: healthPort, + fetch() { + return Response.json({ ok: true, service: "tcp-egress-gateway", startedAt, proxy: `${proxyHost}:${proxyPort}`, routes, connectAttempts, connectRetryBackoffMs }); + }, + }); +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: g14-tcp-egress-gateway + namespace: unidesk + labels: + app.kubernetes.io/name: tcp-egress-gateway + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: tcp-egress-gateway + unidesk.ai/provider-id: G14 + template: + metadata: + labels: + app.kubernetes.io/name: tcp-egress-gateway + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 + unidesk.ai/node-id: G14 + spec: + nodeSelector: + unidesk.ai/node-id: G14 + containers: + - name: tcp-egress-gateway + image: unidesk-code-queue:g14 + imagePullPolicy: IfNotPresent + command: + - bun + - /etc/unidesk-tcp-egress/tcp-egress-gateway.js + ports: + - name: pg + containerPort: 15432 + - name: oa + containerPort: 4255 + - name: health + containerPort: 18080 + env: + - name: TCP_EGRESS_HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: TCP_EGRESS_ROUTES + value: "postgres=15432=74.48.78.17:15432,oa-event-flow=4255=74.48.78.17:4255" + - name: TCP_EGRESS_HEALTH_PORT + value: "18080" + - name: TCP_EGRESS_CONNECT_ATTEMPTS + value: "8" + - name: TCP_EGRESS_CONNECT_RETRY_BACKOFF_MS + value: "250" + volumeMounts: + - name: script + mountPath: /etc/unidesk-tcp-egress + readOnly: true + readinessProbe: + httpGet: + path: /health + port: health + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + livenessProbe: + httpGet: + path: /health + port: health + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + volumes: + - name: script + configMap: + name: g14-tcp-egress-gateway +--- +apiVersion: v1 +kind: Service +metadata: + name: g14-tcp-egress-gateway + namespace: unidesk + labels: + app.kubernetes.io/name: tcp-egress-gateway + app.kubernetes.io/part-of: unidesk + unidesk.ai/provider-id: G14 +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: tcp-egress-gateway + unidesk.ai/provider-id: G14 + ports: + - name: pg + port: 15432 + targetPort: pg + - name: oa + port: 4255 + targetPort: oa + - name: health + port: 18080 + targetPort: health +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: code-queue + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed + unidesk.ai/instance-id: G14 +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app.kubernetes.io/name: code-queue + unidesk.ai/instance-id: G14 + template: + metadata: + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed + unidesk.ai/instance-id: G14 + unidesk.ai/node-id: G14 + spec: + nodeSelector: + unidesk.ai/node-id: G14 + terminationGracePeriodSeconds: 30 + containers: + - name: code-queue + image: unidesk-code-queue:g14 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 4222 + envFrom: + - secretRef: + name: code-queue-env + optional: true + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "4222" + - name: DATABASE_URL + value: "postgres://unidesk:unidesk_dev_password@g14-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk" + - name: CODE_QUEUE_INSTANCE_ID + value: "G14" + - name: CODE_QUEUE_SERVICE_ROLE + value: "scheduler" + - name: CODE_QUEUE_SCHEDULER_ENABLED + value: "true" + - name: CODE_QUEUE_SCHEDULER_POLL_INTERVAL_MS + value: "2000" + - name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED + value: "false" + - name: CODE_QUEUE_DATA_DIR + value: "/var/lib/unidesk/code-queue" + - name: CODE_QUEUE_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_CODEX_HOME + value: "/var/lib/unidesk/code-queue/codex-home" + - name: CODE_QUEUE_OPENCODE_XDG_DIR + value: "/var/lib/unidesk/code-queue/opencode-xdg" + - name: CODE_QUEUE_SOURCE_CODEX_CONFIG + value: "/root/.codex/config.toml" + - name: UNIDESK_SKILLS_PATH + value: "/root/.agents/skills" + - name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH + value: "/root/.agents/skills" + - name: CODE_QUEUE_DEFAULT_MODEL + value: "gpt-5.5" + - name: CODE_QUEUE_MODELS + value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,deepseek-chat,minimax-m2.7" + - name: CODE_QUEUE_MODEL_REASONING_EFFORTS + value: "gpt-5.5=xhigh" + - name: CODE_QUEUE_SANDBOX + value: "danger-full-access" + - name: CODE_QUEUE_APPROVAL_POLICY + value: "never" + - name: CODE_QUEUE_MAX_ACTIVE_QUEUES + value: "0" + - name: CODE_QUEUE_DATABASE_POOL_MAX + value: "2" + - name: NODE_OPTIONS + value: "--max-old-space-size=1024" + - name: GIT_CONFIG_COUNT + value: "1" + - name: GIT_CONFIG_KEY_0 + value: "safe.directory" + - name: GIT_CONFIG_VALUE_0 + value: "*" + - name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS + value: "10" + - name: CODE_QUEUE_IN_MEMORY_EVENT_RECORDS + value: "10" + - name: CODE_QUEUE_MAIN_PROVIDER_ID + value: "G14" + - name: CODE_QUEUE_REMOTE_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_EXECUTION_PROVIDER_IDS + value: "G14" + - name: CODE_QUEUE_DEV_CONTAINER_MASTER_HOST + value: "74.48.78.17" + - name: CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID + value: "G14" + - name: CODE_QUEUE_DEV_CONTAINER_WORKDIR + value: "/root/unidesk" + - name: CODE_QUEUE_EGRESS_PROXY_ENABLED + value: "true" + - name: CODE_QUEUE_EGRESS_PROXY_URL + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-G14,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com,api.minimaxi.com,.minimaxi.com" + - name: HTTP_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: HTTPS_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: ALL_PROXY + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: http_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: https_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: all_proxy + value: "http://g14-provider-egress-proxy.unidesk.svc.cluster.local:18789" + - name: NO_PROXY + value: "localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-G14,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com,api.minimaxi.com,.minimaxi.com" + - name: no_proxy + value: "localhost,127.0.0.1,::1,host.docker.internal,claudeqq,claudeqq.unidesk,claudeqq.unidesk.svc,claudeqq.unidesk.svc.cluster.local,g14-provider-egress-proxy,g14-provider-egress-proxy.unidesk,g14-provider-egress-proxy.unidesk.svc,g14-provider-egress-proxy.unidesk.svc.cluster.local,g14-tcp-egress-gateway,g14-tcp-egress-gateway.unidesk,g14-tcp-egress-gateway.unidesk.svc,g14-tcp-egress-gateway.unidesk.svc.cluster.local,172.25.0.3,unidesk-provider-gateway-G14,backend-core,oa-event-flow,database,hyueapi.com,.hyueapi.com,api.minimaxi.com,.minimaxi.com" + - name: OA_EVENT_FLOW_BASE_URL + value: "http://g14-tcp-egress-gateway.unidesk.svc.cluster.local:4255" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED + value: "true" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL + value: "http://claudeqq.unidesk.svc.cluster.local:3290" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE + value: "private" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID + value: "645275593" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS + value: "12000" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS + value: "15000" + - name: CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS + value: "3" + - name: LOG_FILE + value: "/var/log/unidesk/code-queue.jsonl" + - name: UNIDESK_LOG_RETENTION_BYTES + value: "1GiB" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + - name: workspace + mountPath: /workspace + - name: workspace + mountPath: /home/ubuntu + - name: repo + mountPath: /root/unidesk + - name: repo + mountPath: /app + - name: codex-config + mountPath: /root/.codex/config.toml + readOnly: true + - name: codex-auth + mountPath: /root/.codex/auth.json + readOnly: true + - name: skills-dir + mountPath: /root/.agents/skills + readOnly: true + - name: ssh-dir + mountPath: /root/.ssh + readOnly: true + - name: logs + mountPath: /var/log/unidesk + - name: state + mountPath: /var/lib/unidesk/code-queue + readinessProbe: + httpGet: + path: /health + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 20 + livenessProbe: + httpGet: + path: /live + port: http + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + startupProbe: + httpGet: + path: /live + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 60 + resources: + requests: + cpu: 250m + memory: 15Gi + limits: + memory: 15Gi + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + - name: workspace + hostPath: + path: /root/unidesk + type: Directory + - name: repo + hostPath: + path: /root/unidesk + type: Directory + - name: codex-config + hostPath: + path: /root/.codex/config.toml + type: File + - name: codex-auth + hostPath: + path: /root/.codex/auth.json + type: File + - name: skills-dir + hostPath: + path: /root/.agents/skills + type: Directory + - name: ssh-dir + hostPath: + path: /root/.ssh + type: Directory + - name: logs + hostPath: + path: /root/unidesk/.state/code-queue/logs + type: DirectoryOrCreate + - name: state + hostPath: + path: /root/unidesk/.state/code-queue + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: code-queue + namespace: unidesk + labels: + app.kubernetes.io/name: code-queue + app.kubernetes.io/part-of: unidesk + unidesk.ai/deployment-mode: k3sctl-managed +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: code-queue + unidesk.ai/instance-id: G14 + ports: + - name: http + port: 4222 + targetPort: http