Migrate Code Queue infra target to G14

This commit is contained in:
Codex
2026-05-24 14:06:58 +00:00
parent 0f22b92436
commit 7cc60e9785
12 changed files with 3713 additions and 115 deletions
+57 -8
View File
@@ -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 直管 PostgreSQLD601 k3s Code Queue 负责 scheduler/runner、active run steer/interrupt 和执行态写回。",
"providerId": "G14",
"description": "Code Queue 的用户服务 ID 保持稳定;队列管理、提交、历史和轻量 Trace 读取默认由主 server code-queue-mgr 直管 PostgreSQLG14 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"
}
},
{
+11 -6
View File
@@ -115,11 +115,11 @@ export function checkHelp(): Record<string, unknown> {
{ 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 };
}
+201 -69
View File
@@ -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:-<unset>}\" >&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<DispatchResult> {
async function dispatchSsh(command: string, waitMs: number, remoteTimeoutMs: number, pollCompletion = true, target = ciTarget(null)): Promise<DispatchResult> {
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<DispatchResult> {
const result = await runRemoteKubectlRaw(script, waitMs, remoteTimeoutMs);
async function runRemoteKubectl(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000, target = ciTarget(null)): Promise<DispatchResult> {
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<DispatchResult> {
async function runRemoteKubectlRaw(script: string, waitMs = 60_000, remoteTimeoutMs = 45_000, target = ciTarget(null)): Promise<DispatchResult> {
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<DispatchResult> {
async function uploadRemoteBase64(path: string, encoded: string, target = ciTarget(null)): Promise<DispatchResult> {
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<DispatchResult> {
async function runRemoteBackground(label: string, script: string, timeoutMs: number, target = ciTarget(null)): Promise<DispatchResult> {
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<void> {
async function remoteApplyManifest(path: string, target = ciTarget(null)): Promise<void> {
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<void> {
async function prewarmCiRuntimeImages(target = ciTarget(null)): Promise<void> {
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<void> {
"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<void> {
"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<Record<string, unknown>> {
async function status(target = ciTarget(null)): Promise<Record<string, unknown>> {
const summary = await runRemoteKubectl([
"set -euo pipefail",
"printf 'tekton_pipelines='",
@@ -939,10 +1057,10 @@ async function status(): Promise<Record<string, unknown>> {
"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<Record<string, unknown>> {
};
}
async function install(): Promise<Record<string, unknown>> {
if (!existsSync(rootPath("src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"))) {
async function install(target = ciTarget(null)): Promise<Record<string, unknown>> {
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<Record<string, unknown>> {
"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<string> {
async function remoteCreatePipelineRun(manifest: string, target = ciTarget(null)): Promise<string> {
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<string> {
"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<DispatchResult | null> {
async function waitForPipelineRun(name: string, waitMs: number, target = ciTarget(null)): Promise<DispatchResult | null> {
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<Dispatc
`kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`,
"exit 124",
].join("\n");
return dispatchSsh(command, waitMs + 30_000, waitMs + 20_000);
return dispatchSsh(command, waitMs + 30_000, waitMs + 20_000, true, target);
}
async function readPipelineRunCondition(name: string): Promise<PipelineRunCondition> {
async function readPipelineRunCondition(name: string, target = ciTarget(null)): Promise<PipelineRunCondition> {
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<string> {
async function readPipelineRunLogText(name: string, target = ciTarget(null)): Promise<string> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
},
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<Record<string, unkno
};
}
async function logs(name: string): Promise<Record<string, unknown>> {
async function logs(name: string, target = ciTarget(null)): Promise<Record<string, unknown>> {
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<Record<string, unknown>> {
"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<Record<string, unknown>> {
`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<string, unknown> {
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 <commit>",
"bun scripts/cli.ts ci run --provider-id G14 --revision <commit>",
"bun scripts/cli.ts ci publish-backend-core --commit <full-sha>",
"bun scripts/cli.ts ci publish-user-service --service baidu-netdisk --commit <full-sha>",
"bun scripts/cli.ts ci publish-user-service --service mdtodo --commit <full-sha>",
@@ -2661,7 +2784,7 @@ export function ciHelp(): Record<string, unknown> {
"bun scripts/cli.ts ci publish-user-service --service decision-center --commit <full-sha>",
"bun scripts/cli.ts ci publish-user-service --service frontend --commit <full-sha>",
"bun scripts/cli.ts ci run-dev-e2e --wait-ms 600000",
"bun scripts/cli.ts ci logs <runId>",
"bun scripts/cli.ts ci logs <runId> [--provider-id G14]",
],
tekton: {
pipelineVersion: tektonPipelineVersion,
@@ -2671,6 +2794,14 @@ export function ciHelp(): Record<string, unknown> {
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<Record<string, unknown>> {
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<string, unknown> {
const job = startJob("ci_install", ["bun", "scripts/cli.ts", "ci", "install"], "Install/refresh Tekton CI on D601 k3s");
export function startCiInstallJob(providerId = d601ProviderId): Record<string, unknown> {
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 };
}
+7 -1
View File
@@ -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();
+14 -6
View File
@@ -50,7 +50,7 @@ export function unsupportedRebuildService(value: string | undefined): Record<str
},
"code-queue": {
classification: "standard-artifact",
reason: "code-queue execution plane is D601 k3s-managed and only supports the dev artifact consumer in this phase",
reason: "code-queue execution plane is native-k3s-managed and only supports the dev artifact consumer in this phase",
replacement: "deploy apply --env dev --service code-queue or artifact-registry deploy-service --env dev --service code-queue",
deleteAllowedLater: false,
},
@@ -138,6 +138,14 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
const labels = JSON.stringify(config.providerGateway.labels);
const microservices = JSON.stringify(config.microservices);
const restrictedHostBind = config.network.restrictedHostAccess?.bindHost || "127.0.0.1";
const codeQueueService = config.microservices.find((service) => 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"),
+1 -1
View File
@@ -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 <taskId|latest>", 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." },
],
+2 -1
View File
@@ -63,8 +63,9 @@ fn read_microservices_env() -> anyhow::Result<Vec<MicroserviceConfig>> {
}
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
@@ -175,7 +175,15 @@ fn json_i64(value: &Value, path: &[&str]) -> Option<i64> {
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)
@@ -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}
File diff suppressed because it is too large Load Diff
@@ -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
}
}
]
File diff suppressed because it is too large Load Diff