From b33c405996cbfcfd8f925debfa1f5999180369cf Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Wed, 29 Jul 2026 02:41:44 +0000 Subject: [PATCH 1/3] feat(orion): surface VM image, resources, and toolchain in Client UI Record image-info at build time and return it from scheduler status so the Orion Client runner panel can show image digest/build time, CPU/mem/disk, and rust/buck2/python versions without probing the guest. --- Cargo.lock | 73 ++-- Cargo.toml | 2 +- ceres/src/model/orion_runner.rs | 22 + .../orion-scheduler-client/src/http_client.rs | 22 + clients/orion-scheduler-client/src/lib.rs | 22 + mono/src/api/router/orion_runner_router.rs | 11 + .../hooks/OrionClient/useGetRunnerStatus.ts | 5 +- moon/apps/web/pages/[org]/oc/index.tsx | 62 +++ moon/packages/types/generated.ts | 46 ++ orion-scheduler/scripts/build-custom-image.sh | 399 +++++++++++++++--- orion-scheduler/src/handlers.rs | 13 +- orion-scheduler/src/orion_deployer.rs | 135 +++++- orion-scheduler/src/state.rs | 25 ++ 13 files changed, 719 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54144f9ee..fda1c2b2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1365,14 +1365,15 @@ dependencies = [ [[package]] name = "cedar-policy" -version = "4.11.2" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdd98501120bc31fc09f92d3f58c489f4ee4b846904109ac63dd8256de7b84d" +checksum = "f73547a0114dff845fcb8d4877548a665fb8ff949a30cb0963444d6c30ffd962" dependencies = [ "cedar-policy-core", "cedar-policy-formatter", - "itertools 0.14.0", + "itertools 0.15.0", "linked-hash-map", + "linked_hash_set", "miette", "ref-cast", "semver", @@ -1385,14 +1386,14 @@ dependencies = [ [[package]] name = "cedar-policy-core" -version = "4.11.2" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d41088b497e39b6789e87e05ad6549b7f64a663a4d6ecd73a748dc3a9a98a9" +checksum = "c9d9ec16707c7b60aa73b922dc255ec0b97b6892e8e58e45188b15d874ca3590" dependencies = [ "chrono", "educe", "either", - "itertools 0.14.0", + "itertools 0.15.0", "lalrpop", "lalrpop-util", "linked-hash-map", @@ -1413,12 +1414,12 @@ dependencies = [ [[package]] name = "cedar-policy-formatter" -version = "4.11.2" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6c28be47d77dcbc04c2a80181a2c0db9725ed60d24832c6401396cc2ae114b" +checksum = "2483b2fa74f74b1b3945bec59feb7a3fc0ea56b6fb35564ef300884dd8415d4b" dependencies = [ "cedar-policy-core", - "itertools 0.14.0", + "itertools 0.15.0", "logos", "miette", "pretty", @@ -1643,7 +1644,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1680,7 +1681,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -1718,7 +1719,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "winnow 1.0.4", "yaml-rust2", ] @@ -2506,7 +2507,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2523,13 +2524,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -2670,9 +2671,9 @@ dependencies = [ [[package]] name = "educe" -version = "0.6.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +checksum = "92c3e1715a2bf74bc8f68cd7bae12ff144f02669c602106ad1fa16f2ba62e646" dependencies = [ "enum-ordinalize", "proc-macro2", @@ -2896,7 +2897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3856,7 +3857,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.5", "system-configuration", "tokio", "tower-service", @@ -4664,7 +4665,7 @@ dependencies = [ "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.9.0", + "redox_syscall 0.9.1", ] [[package]] @@ -5279,7 +5280,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6700,7 +6701,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.19", "tokio", "tracing", @@ -6739,9 +6740,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6975,9 +6976,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e" dependencies = [ "bitflags 2.13.1", ] @@ -7503,9 +7504,9 @@ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc-literal-escaper" -version = "0.0.7" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be87abb9e40db7466e0681dc8ecd9dcfd40360cb10b4c8fe24a7c4c3669b198" +checksum = "bfe6f213fb658c8fb95baabd5420393438cf5a98d707f5dd701d9197c705f71e" [[package]] name = "rustc_version" @@ -7548,7 +7549,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7616,7 +7617,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9426,7 +9427,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9435,7 +9436,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9703,9 +9704,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -10741,7 +10742,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 15707214a..9369be665 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ reqwest = "0.13.4" uuid = "1.24.0" regex = "1.13.1" ctrlc = "3.5.2" -cedar-policy = "4.11.2" +cedar-policy = "4.12.0" secp256k1 = "0.31.1" pgp = "0.20.0" base64 = "0.23.0" diff --git a/ceres/src/model/orion_runner.rs b/ceres/src/model/orion_runner.rs index d2b2068df..514ec6d19 100644 --- a/ceres/src/model/orion_runner.rs +++ b/ceres/src/model/orion_runner.rs @@ -48,4 +48,26 @@ pub struct RunnerStatusResponse { pub error: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub uptime_secs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_digest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_cpus: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_memory_mb: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_disk_gb: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub image_built_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub toolchain_rust: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub toolchain_buck2: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub toolchain_python: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub kernel: Option, } diff --git a/clients/orion-scheduler-client/src/http_client.rs b/clients/orion-scheduler-client/src/http_client.rs index fe205bfdf..86f800384 100644 --- a/clients/orion-scheduler-client/src/http_client.rs +++ b/clients/orion-scheduler-client/src/http_client.rs @@ -91,6 +91,17 @@ impl OrionSchedulerHttpClient { uptime_secs: None, log_file: None, error: Some("VM not found".to_string()), + image_path: None, + image_digest: None, + image_cpus: None, + image_memory_mb: None, + image_disk_gb: None, + image_name: None, + image_built_at: None, + toolchain_rust: None, + toolchain_buck2: None, + toolchain_python: None, + kernel: None, }); } if status.is_success() { @@ -123,6 +134,17 @@ impl OrionSchedulerHttpClient { uptime_secs: None, log_file: None, error: None, + image_path: None, + image_digest: None, + image_cpus: None, + image_memory_mb: None, + image_disk_gb: None, + image_name: None, + image_built_at: None, + toolchain_rust: None, + toolchain_buck2: None, + toolchain_python: None, + kernel: None, }); } Ok(serde_json::from_value(v)?) diff --git a/clients/orion-scheduler-client/src/lib.rs b/clients/orion-scheduler-client/src/lib.rs index 7dcd8c8af..0375268e3 100644 --- a/clients/orion-scheduler-client/src/lib.rs +++ b/clients/orion-scheduler-client/src/lib.rs @@ -64,6 +64,28 @@ pub struct SchedulerStatusResponse { pub log_file: Option, #[serde(default)] pub error: Option, + #[serde(default)] + pub image_path: Option, + #[serde(default)] + pub image_digest: Option, + #[serde(default)] + pub image_cpus: Option, + #[serde(default)] + pub image_memory_mb: Option, + #[serde(default)] + pub image_disk_gb: Option, + #[serde(default)] + pub image_name: Option, + #[serde(default)] + pub image_built_at: Option, + #[serde(default)] + pub toolchain_rust: Option, + #[serde(default)] + pub toolchain_buck2: Option, + #[serde(default)] + pub toolchain_python: Option, + #[serde(default)] + pub kernel: Option, } #[derive(Clone)] diff --git a/mono/src/api/router/orion_runner_router.rs b/mono/src/api/router/orion_runner_router.rs index 7f8779d4c..a47deffdf 100644 --- a/mono/src/api/router/orion_runner_router.rs +++ b/mono/src/api/router/orion_runner_router.rs @@ -257,6 +257,17 @@ async fn get_runner_status( log_file: sched.log_file, error: sched.error, uptime_secs: sched.uptime_secs, + image_path: sched.image_path, + image_digest: sched.image_digest, + image_cpus: sched.image_cpus, + image_memory_mb: sched.image_memory_mb, + image_disk_gb: sched.image_disk_gb, + image_name: sched.image_name, + image_built_at: sched.image_built_at, + toolchain_rust: sched.toolchain_rust, + toolchain_buck2: sched.toolchain_buck2, + toolchain_python: sched.toolchain_python, + kernel: sched.kernel, })))) } diff --git a/moon/apps/web/hooks/OrionClient/useGetRunnerStatus.ts b/moon/apps/web/hooks/OrionClient/useGetRunnerStatus.ts index 76b2ff1b3..84edb7101 100644 --- a/moon/apps/web/hooks/OrionClient/useGetRunnerStatus.ts +++ b/moon/apps/web/hooks/OrionClient/useGetRunnerStatus.ts @@ -5,7 +5,7 @@ import type { RunnerStatusResponse } from '@gitmono/types/generated' import { legacyApiClient } from '@/utils/queryClient' export function useGetRunnerStatus(vmId: string | null, phase: string | null) { - const shouldPoll = !!vmId && phase === 'provisioning' + const shouldPoll = !!vmId && (phase === 'provisioning' || phase === 'running') const query = legacyApiClient.v1.getApiOrionRunnersById() return useQuery({ @@ -19,7 +19,8 @@ export function useGetRunnerStatus(vmId: string | null, phase: string | null) { return result.data }, enabled: !!vmId, - refetchInterval: shouldPoll ? 5000 : false, + // Provisioning needs frequent polls; running only for uptime refresh. + refetchInterval: shouldPoll ? (phase === 'running' ? 15000 : 5000) : false, staleTime: 0 }) } diff --git a/moon/apps/web/pages/[org]/oc/index.tsx b/moon/apps/web/pages/[org]/oc/index.tsx index fafb60057..ff2133767 100644 --- a/moon/apps/web/pages/[org]/oc/index.tsx +++ b/moon/apps/web/pages/[org]/oc/index.tsx @@ -41,6 +41,17 @@ function domainFromClientHostname(hostname: string): string | null { } } +function formatUptime(totalSecs: number): string { + const secs = Math.max(0, Math.floor(totalSecs)) + const h = Math.floor(secs / 3600) + const m = Math.floor((secs % 3600) / 60) + const s = secs % 60 + + if (h > 0) return `${h}h ${m}m ${s}s` + if (m > 0) return `${m}m ${s}s` + return `${s}s` +} + /** True when the log panel is only showing a VM wait placeholder (no real log lines yet). */ function isVmWaitingLog(logs: string): boolean { const trimmed = logs.trim() @@ -493,6 +504,57 @@ const OrionClientPage: PageWithLayout = () => { VM IP: {runnerStatus.vm_ip} ) : null} + {runnerStatus?.image_name || runnerStatus?.image_digest ? ( + + Image: {runnerStatus.image_name ?? 'unknown'} + {runnerStatus.image_digest + ? ` (${runnerStatus.image_digest.replace(/^sha256:/, '').slice(0, 12)})` + : ''} + + ) : null} + {runnerStatus?.image_built_at ? ( + + Built: {runnerStatus.image_built_at} + + ) : null} + {runnerStatus?.image_cpus != null || + runnerStatus?.image_memory_mb != null || + runnerStatus?.image_disk_gb != null ? ( + + Resources:{' '} + {[ + runnerStatus.image_cpus != null ? `${runnerStatus.image_cpus} vCPU` : null, + runnerStatus.image_memory_mb != null + ? `${Math.round(runnerStatus.image_memory_mb / 1024)} GiB RAM` + : null, + runnerStatus.image_disk_gb != null ? `${runnerStatus.image_disk_gb} GiB disk` : null + ] + .filter(Boolean) + .join(' / ')} + + ) : null} + {runnerStatus?.toolchain_rust || runnerStatus?.toolchain_buck2 || runnerStatus?.toolchain_python ? ( + + Toolchains:{' '} + {[ + runnerStatus.toolchain_rust ? `rust ${runnerStatus.toolchain_rust}` : null, + runnerStatus.toolchain_buck2 ? `buck2 ${runnerStatus.toolchain_buck2}` : null, + runnerStatus.toolchain_python ? `python ${runnerStatus.toolchain_python}` : null + ] + .filter(Boolean) + .join(' · ')} + + ) : null} + {runnerStatus?.kernel ? ( + + Kernel: {runnerStatus.kernel} + + ) : null} + {runnerStatus?.uptime_secs != null ? ( + + Uptime: {formatUptime(runnerStatus.uptime_secs)} + + ) : null} {runnerStatus?.log_file ? ( Log file: {runnerStatus.log_file} diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index cce50a8dc..b88f9b0a3 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -4391,8 +4391,31 @@ export type CommonResultRunnerStatusResponse = { data?: { domain?: string | null error?: string | null + /** + * @format int32 + * @min 0 + */ + image_cpus?: number | null + image_digest?: string | null + /** + * @format int32 + * @min 0 + */ + image_disk_gb?: number | null + /** + * @format int32 + * @min 0 + */ + image_memory_mb?: number | null + image_built_at?: string | null + image_name?: string | null + image_path?: string | null + kernel?: string | null log_file?: string | null phase: string + toolchain_buck2?: string | null + toolchain_python?: string | null + toolchain_rust?: string | null /** * @format int64 * @min 0 @@ -5763,8 +5786,31 @@ export type ReviewersResponse = { export type RunnerStatusResponse = { domain?: string | null error?: string | null + /** + * @format int32 + * @min 0 + */ + image_cpus?: number | null + image_digest?: string | null + /** + * @format int32 + * @min 0 + */ + image_disk_gb?: number | null + /** + * @format int32 + * @min 0 + */ + image_memory_mb?: number | null + image_built_at?: string | null + image_name?: string | null + image_path?: string | null + kernel?: string | null log_file?: string | null phase: string + toolchain_buck2?: string | null + toolchain_python?: string | null + toolchain_rust?: string | null /** * @format int64 * @min 0 diff --git a/orion-scheduler/scripts/build-custom-image.sh b/orion-scheduler/scripts/build-custom-image.sh index d39a57ddd..b2e40c3eb 100755 --- a/orion-scheduler/scripts/build-custom-image.sh +++ b/orion-scheduler/scripts/build-custom-image.sh @@ -56,27 +56,52 @@ BASE_CHECKSUM_URL="${BASE_MIRROR_URL%/}/SHA512SUMS" IMAGE_SIZE="15G" -RUST_VERSION="1.96.0" +RUST_VERSION="1.97.1" RUST_ARCH="x86_64-unknown-linux-gnu" +# CN rustup dist mirror (RUSTUP_DIST_SERVER). Xuanwu mirrors .tar.xz reliably; +# undated /dist/rust-*.tar.* and .tar.gz often 404 — we use the dated xz path +# from channel-rust-${RUST_VERSION}.toml. Override for the official CDN: +# RUSTUP_DIST_SERVER=https://static.rust-lang.org sudo -E bash scripts/build-custom-image.sh +# Docs: https://xuanwu.openatom.cn/guide/quick-start/install.html +RUSTUP_DIST_SERVER="${RUSTUP_DIST_SERVER:-https://mirror.xuanwu.openatom.cn}" +# Optional offline overrides. If either is unset, both date + xz_hash are +# resolved from ${RUSTUP_DIST_SERVER}/dist/channel-rust-${RUST_VERSION}.toml +# so bumping RUST_VERSION alone is enough (avoids 404 from a stale date dir). +RUST_DIST_DATE="${RUST_DIST_DATE:-}" +RUST_SHA256="${RUST_SHA256:-}" # Host-side cache for the Rust toolchain tarball. Pre-place the file here to # skip the download (useful when the network is flaky/blocked): -# curl -fL -o /tmp/rust-1.96.0-x86_64-unknown-linux-gnu.tar.gz "$RUST_TARBALL_URL" -RUST_TARBALL="/tmp/rust-${RUST_VERSION}-${RUST_ARCH}.tar.gz" -RUST_TARBALL_URL="https://static.rust-lang.org/dist/rust-${RUST_VERSION}-${RUST_ARCH}.tar.gz" +# curl -fL -o /tmp/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz "$RUST_TARBALL_URL" +# Truncated/corrupt caches are detected via SHA256 and re-downloaded. +RUST_TARBALL="/tmp/rust-${RUST_VERSION}-${RUST_ARCH}.tar.xz" +RUST_TARBALL_URL="" # set by resolve_rust_channel BUCK2_VERSION="2026-04-15" -BUCK2_ARCH="x86_64-unknown-linux-musl" -BUCK2_URL="https://github.com/facebook/buck2/releases/download/${BUCK2_VERSION}/buck2-${BUCK2_ARCH}.zst" -# Host-side cache for the buck2 archive. Pre-place the file here to skip the -# in-chroot GitHub download entirely (useful when GitHub is flaky/blocked): -# curl -fL -o /tmp/buck2-x86_64-unknown-linux-musl.zst "$BUCK2_URL" -BUCK2_TARBALL="/tmp/buck2-${BUCK2_ARCH}.zst" +BUCK2_ARCH="x86_64-unknown-linux-gnu" +# Label recorded in orion-image-info.json (release tag, not just the date). +BUCK2_LABEL="scorpiofs-execvp-2026-04-15" +# BUCK2_URL="https://github.com/facebook/buck2/releases/download/${BUCK2_VERSION}/buck2-${BUCK2_ARCH}.zst" +BUCK2_URL="https://github.com/Ivanbeethoven/buck2/releases/download/${BUCK2_LABEL}/buck2-${BUCK2_ARCH}.zst" + +# Persistent host-side cache (NOT /tmp — systemd-tmpfiles / reboots clear that). +# Lives next to apt caches under /var/cache/orion-image and is never deleted by +# this script after a successful download. Override with BUCK2_CACHE_DIR=... +BUCK2_CACHE_DIR="${BUCK2_CACHE_DIR:-/var/cache/orion-image/buck2}" +BUCK2_TARBALL="${BUCK2_CACHE_DIR}/buck2-${BUCK2_ARCH}.zst" # Host-side apt caches, bind-mounted into the chroot so repeated image builds # reuse already-downloaded indexes and .deb packages. They live on the host and # are unmounted before the image is sealed, so cached data is NOT baked into the # final image. Override with APT_LISTS_DIR / APT_CACHE_DIR. APT_LISTS_DIR="${APT_LISTS_DIR:-/var/cache/orion-image/apt-lists}" APT_CACHE_DIR="${APT_CACHE_DIR:-/var/cache/orion-image/apt-archives}" +# CN apt mirrors written into the image before chroot apt-get. Official +# deb.debian.org is often too slow from CN build hosts (apt appears "stuck"). +# Override, e.g.: +# APT_MIRROR=https://mirrors.aliyun.com/debian \ +# APT_SECURITY_MIRROR=https://mirrors.aliyun.com/debian-security \ +# sudo -E bash scripts/build-custom-image.sh +APT_MIRROR="${APT_MIRROR:-https://mirrors.ustc.edu.cn/debian}" +APT_SECURITY_MIRROR="${APT_SECURITY_MIRROR:-https://mirrors.ustc.edu.cn/debian-security}" # CPython tarball for prelude remote_python_toolchain / toolchains//:cpython_archive. # Baked into a local GitHub HTTPS mirror (see chroot) so buck2 http_archive keeps @@ -113,6 +138,7 @@ fix_qlean_ownership() { # ============================================================================ MOUNT_DIR="" BUILD_STAGE="init" +CLEANUP_RUNNING=0 log_stage() { BUILD_STAGE="$1" @@ -123,21 +149,95 @@ log_cmd() { echo "[build-custom-image] \$ $*" } +# Unmount image tree safely. Never `rm -rf` the mount dir while /proc|/sys|/dev +# are still bind-mounted — that prints endless "Operation not permitted" under +# proc and can wedge the host. Prefer lazy umount if busy after Ctrl-C. +unmount_image_tree() { + local root="$1" + local sub tries + + [ -n "$root" ] && [ -d "$root" ] || return 0 + + # Drop processes still in the chroot (apt-get, etc.) so umount can succeed. + if command -v fuser >/dev/null 2>&1; then + sudo fuser -km "$root" >/dev/null 2>&1 || true + sleep 0.5 + fi + + for sub in var/lib/apt/lists var/cache/apt/archives proc sys dev; do + if mountpoint -q "$root/$sub" 2>/dev/null; then + sudo umount "$root/$sub" 2>/dev/null || sudo umount -l "$root/$sub" 2>/dev/null || true + fi + done + + # Any nested mounts left under $root (e.g. unexpected bind)? + if command -v findmnt >/dev/null 2>&1; then + findmnt -Rnn -o TARGET --target "$root" 2>/dev/null \ + | awk -v r="$root" '$0 != r { print }' \ + | sort -r \ + | while IFS= read -r tgt; do + sudo umount "$tgt" 2>/dev/null || sudo umount -l "$tgt" 2>/dev/null || true + done + fi + + if mountpoint -q "$root" 2>/dev/null; then + for tries in 1 2 3 4 5; do + sudo umount "$root" 2>/dev/null && break + sudo umount -l "$root" 2>/dev/null && break + sleep 1 + done + fi + + if mountpoint -q "$root" 2>/dev/null; then + echo "[build-custom-image] WARNING: $root still mounted; NOT removing dir (avoid rm -rf on /proc)" >&2 + findmnt -R "$root" 2>/dev/null || true + return 1 + fi + + rmdir "$root" 2>/dev/null || true + return 0 +} + +# Disconnect NBD and drop any leftover qemu-nbd holding this image's write lock. +release_nbd() { + local img="${1:-}" + if [ -n "$img" ]; then + local pids + pids=$(ps -eo pid=,args= | awk -v img="$img" ' + $0 ~ /qemu-nbd/ && index($0, img) { print $1 } + ') + if [ -n "$pids" ]; then + echo "[build-custom-image] Killing stale qemu-nbd for $img: $pids" + # shellcheck disable=SC2086 + sudo kill $pids 2>/dev/null || true + sleep 1 + # shellcheck disable=SC2086 + sudo kill -9 $pids 2>/dev/null || true + fi + fi + sudo qemu-nbd --disconnect "$NBD_DEV" 2>/dev/null || true +} + cleanup() { local rc=$? + # Re-entrant: a second Ctrl-C while umount is in progress must not start + # another cleanup (or worse, an rm) on a half-unmounted tree. + if [ "$CLEANUP_RUNNING" -ne 0 ]; then + return + fi + CLEANUP_RUNNING=1 + # Ignore further INT/TERM during cleanup so we finish umounts. + trap '' INT TERM HUP set +e if [ $rc -ne 0 ]; then echo "[build-custom-image] FAILED with exit code $rc (stage: ${BUILD_STAGE:-unknown})" >&2 fi if [ -n "$MOUNT_DIR" ]; then echo "[build-custom-image] cleanup: unmounting $MOUNT_DIR (stage was: ${BUILD_STAGE:-unknown})" - for sub in var/lib/apt/lists var/cache/apt/archives proc sys dev; do - mountpoint -q "$MOUNT_DIR/$sub" && sudo umount "$MOUNT_DIR/$sub" - done - mountpoint -q "$MOUNT_DIR" && sudo umount "$MOUNT_DIR" - [ -d "$MOUNT_DIR" ] && rmdir "$MOUNT_DIR" 2>/dev/null + unmount_image_tree "$MOUNT_DIR" || true + MOUNT_DIR="" fi - sudo qemu-nbd --disconnect "$NBD_DEV" 2>/dev/null + release_nbd "${CUSTOM_IMAGE:-}" exit $rc } trap cleanup EXIT INT TERM HUP @@ -199,6 +299,17 @@ INITRD=$(ls "$BASE_DIR"/initrd.img-* 2>/dev/null | head -n1 || true) mkdir -p "$IMAGE_DIR" +# Recover from a previous Ctrl-C that left bind mounts behind. Never rm -rf +# those dirs while /proc is still mounted. +if compgen -G '/tmp/custom-image-mnt.*' >/dev/null 2>&1; then + echo "[build-custom-image] Cleaning stale mount dirs under /tmp/custom-image-mnt.*" + for stale in /tmp/custom-image-mnt.*; do + [ -d "$stale" ] || continue + unmount_image_tree "$stale" || true + done +fi +release_nbd "$CUSTOM_IMAGE" + # ============================================================================ # Stage 1: Copy + resize qcow2 # ============================================================================ @@ -212,31 +323,167 @@ qemu-img resize "$CUSTOM_IMAGE" "$IMAGE_SIZE" # ============================================================================ # Stage 2: Download Rust on host (avoids needing DNS inside chroot) # ============================================================================ -if [ ! -f "$RUST_TARBALL" ]; then - echo "[build-custom-image] Downloading Rust ${RUST_VERSION}..." - curl -fsSL -o "$RUST_TARBALL" "$RUST_TARBALL_URL" +# Xuanwu (and most rustup mirrors) require /dist/YYYY-MM-DD/rust-VER-ARCH.tar.xz. +# Resolve that date + xz_hash from the channel manifest so RUST_VERSION bumps +# don't leave a stale RUST_DIST_DATE and 404. +resolve_rust_channel() { + local channel_url="${RUSTUP_DIST_SERVER%/}/dist/channel-rust-${RUST_VERSION}.toml" + local channel_file tmp_date tmp_hash + + if [ -n "$RUST_DIST_DATE" ] && [ -n "$RUST_SHA256" ]; then + RUST_TARBALL_URL="${RUSTUP_DIST_SERVER%/}/dist/${RUST_DIST_DATE}/rust-${RUST_VERSION}-${RUST_ARCH}.tar.xz" + echo "[build-custom-image] Using overridden Rust dist: ${RUST_TARBALL_URL}" + return 0 + fi + + echo "[build-custom-image] Resolving Rust ${RUST_VERSION} channel: ${channel_url}" + channel_file=$(mktemp) + if ! curl -fsSL --connect-timeout 20 --max-time 60 --retry 3 \ + -o "$channel_file" "$channel_url"; then + rm -f "$channel_file" + echo "[build-custom-image] ERROR: failed to fetch Rust channel from $channel_url" >&2 + exit 56 + fi + + # Read from a file (not a pipe): under `set -o pipefail`, feeding the + # whole channel through a pipe and exiting early triggers SIGPIPE 141. + tmp_date=$(awk ' + /^date = "/ { + sub(/^date = "/, "") + sub(/"$/, "") + print + exit + } + ' "$channel_file") + tmp_hash=$(awk -v arch="$RUST_ARCH" ' + $0 == "[pkg.rust.target." arch "]" { p = 1; next } + /^\[/ { p = 0 } + p && /^xz_hash = "/ { + sub(/^xz_hash = "/, "") + sub(/"$/, "") + print + exit + } + ' "$channel_file") + rm -f "$channel_file" + + if [ -z "$tmp_date" ] || [ -z "$tmp_hash" ]; then + echo "[build-custom-image] ERROR: could not parse date/xz_hash for ${RUST_ARCH} from $channel_url" >&2 + exit 1 + fi + + RUST_DIST_DATE="${RUST_DIST_DATE:-$tmp_date}" + RUST_SHA256="${RUST_SHA256:-$tmp_hash}" + RUST_TARBALL_URL="${RUSTUP_DIST_SERVER%/}/dist/${RUST_DIST_DATE}/rust-${RUST_VERSION}-${RUST_ARCH}.tar.xz" + echo "[build-custom-image] Rust dist date=${RUST_DIST_DATE} sha256=${RUST_SHA256}" + echo "[build-custom-image] Rust URL: ${RUST_TARBALL_URL}" +} + +# Reuse a cached archive only if SHA256 matches. A flaky download can leave a +# truncated archive that still passes `[ -f ]` and then fails later in chroot. +verify_rust_tarball() { + local actual + actual=$(sha256sum "$RUST_TARBALL" | awk '{ print $1 }') + if [ "$RUST_SHA256" = "$actual" ]; then + return 0 + fi + echo "[build-custom-image] Rust tarball checksum mismatch (will re-download)" >&2 + echo "[build-custom-image] expected: $RUST_SHA256" >&2 + echo "[build-custom-image] actual: $actual" >&2 + echo "[build-custom-image] size: $(du -sh "$RUST_TARBALL" | cut -f1) ($(stat -c%s "$RUST_TARBALL") bytes)" >&2 + return 1 +} + +download_rust_tarball() { + echo "[build-custom-image] Downloading Rust ${RUST_VERSION} from ${RUSTUP_DIST_SERVER}..." + # Resume + retries: toolchain mirrors can still truncate mid-transfer on + # flaky links (same class of failure as GitHub TLS resets for buck2). + if ! curl -fL --connect-timeout 30 --retry 5 --retry-delay 3 --retry-all-errors \ + -C - -o "$RUST_TARBALL" "$RUST_TARBALL_URL"; then + rm -f "$RUST_TARBALL" + echo "[build-custom-image] ERROR: failed to download Rust from $RUST_TARBALL_URL" >&2 + echo "[build-custom-image] Pre-download it manually then re-run, e.g.:" >&2 + echo "[build-custom-image] curl -fL -o $RUST_TARBALL \"$RUST_TARBALL_URL\"" >&2 + exit 56 + fi echo "[build-custom-image] Rust tarball downloaded: $(du -sh "$RUST_TARBALL" | cut -f1)" -else +} + +resolve_rust_channel + +if [ -f "$RUST_TARBALL" ] && verify_rust_tarball; then echo "[build-custom-image] Using cached Rust tarball: $(du -sh "$RUST_TARBALL" | cut -f1)" +else + rm -f "$RUST_TARBALL" + download_rust_tarball + if ! verify_rust_tarball; then + rm -f "$RUST_TARBALL" + echo "[build-custom-image] ERROR: Rust tarball checksum mismatch after download" >&2 + exit 1 + fi fi # ============================================================================ # Stage 2b: Download buck2 on host (avoids needing reliable network in chroot) # ============================================================================ -# Reuse a manually pre-downloaded archive if present at $BUCK2_TARBALL. -if [ ! -f "$BUCK2_TARBALL" ]; then - echo "[build-custom-image] Downloading buck2 ${BUCK2_VERSION}..." +# Cache under $BUCK2_CACHE_DIR (durable). On success the file is kept forever by +# this script — only replace it when missing/corrupt. Partial downloads go to +# a sibling .partial file so a failed curl never clobbers a good cache. +mkdir -p "$BUCK2_CACHE_DIR" +# One-time migrate from the old /tmp location if present. +for _old_buck2 in \ + "/tmp/buck2-${BUCK2_ARCH}.zst" \ + "/tmp/buck2-x86_64-unknown-linux-musl.zst" \ + "/tmp/buck2-x86_64-unknown-linux-gnu.zst"; do + if [ ! -f "$BUCK2_TARBALL" ] && [ -s "$_old_buck2" ]; then + echo "[build-custom-image] Migrating buck2 cache $_old_buck2 -> $BUCK2_TARBALL" + cp -a "$_old_buck2" "$BUCK2_TARBALL" + break + fi +done +unset _old_buck2 + +verify_buck2_tarball() { + [ -s "$BUCK2_TARBALL" ] || return 1 + if command -v zstd >/dev/null 2>&1; then + zstd -t "$BUCK2_TARBALL" >/dev/null 2>&1 || return 1 + fi + return 0 +} + +download_buck2_tarball() { + local part="${BUCK2_TARBALL}.partial" + echo "[build-custom-image] Downloading buck2 ${BUCK2_VERSION} -> $BUCK2_TARBALL" + rm -f "$part" if ! curl -fL --connect-timeout 30 --retry 5 --retry-delay 3 --retry-all-errors \ - -C - -o "$BUCK2_TARBALL" "$BUCK2_URL"; then - rm -f "$BUCK2_TARBALL" + -C - -o "$part" "$BUCK2_URL"; then + rm -f "$part" echo "[build-custom-image] ERROR: failed to download buck2 from $BUCK2_URL" >&2 echo "[build-custom-image] Pre-download it manually then re-run, e.g.:" >&2 - echo "[build-custom-image] curl -fL -o $BUCK2_TARBALL \"$BUCK2_URL\"" >&2 + echo "[build-custom-image] sudo mkdir -p $BUCK2_CACHE_DIR" >&2 + echo "[build-custom-image] sudo curl -fL -o $BUCK2_TARBALL \"$BUCK2_URL\"" >&2 exit 56 fi - echo "[build-custom-image] buck2 downloaded: $(du -sh "$BUCK2_TARBALL" | cut -f1)" + if ! [ -s "$part" ]; then + rm -f "$part" + echo "[build-custom-image] ERROR: buck2 download empty: $part" >&2 + exit 1 + fi + if command -v zstd >/dev/null 2>&1 && ! zstd -t "$part" >/dev/null 2>&1; then + rm -f "$part" + echo "[build-custom-image] ERROR: buck2 archive failed zstd integrity check" >&2 + exit 1 + fi + mv -f "$part" "$BUCK2_TARBALL" + chmod 644 "$BUCK2_TARBALL" 2>/dev/null || true + echo "[build-custom-image] buck2 cached: $(du -sh "$BUCK2_TARBALL" | cut -f1) -> $BUCK2_TARBALL" +} + +if verify_buck2_tarball; then + echo "[build-custom-image] Using cached buck2 archive: $(du -sh "$BUCK2_TARBALL" | cut -f1) ($BUCK2_TARBALL)" else - echo "[build-custom-image] Using cached buck2 archive: $(du -sh "$BUCK2_TARBALL" | cut -f1)" + rm -f "$BUCK2_TARBALL" + download_buck2_tarball fi # ============================================================================ @@ -284,18 +531,18 @@ if [ ! -e "$NBD_DEV" ]; then echo "[build-custom-image] Loading NBD kernel module..." sudo modprobe nbd max_part=8 if ! wait_for "$NBD_DEV" 20; then - echo "[build-custom-image] ERROR: $NBD_DEV not found after loading module" + echo "[build-custom-image] ERROR: $NBD_DEV not found after loading module" >&2 exit 1 fi fi echo "[build-custom-image] Connecting NBD device..." -sudo qemu-nbd --disconnect "$NBD_DEV" 2>/dev/null || true +release_nbd "$CUSTOM_IMAGE" sudo qemu-nbd -c "$NBD_DEV" "$CUSTOM_IMAGE" sudo udevadm settle if ! wait_for "$NBD_PART" 40; then - echo "[build-custom-image] ERROR: $NBD_PART did not appear" + echo "[build-custom-image] ERROR: $NBD_PART did not appear" >&2 exit 1 fi @@ -347,17 +594,33 @@ sudo mkdir -p "$MOUNT_DIR/var/lib/apt/lists" "$MOUNT_DIR/var/cache/apt/archives/ sudo mount --bind "$APT_LISTS_DIR" "$MOUNT_DIR/var/lib/apt/lists" sudo mount --bind "$APT_CACHE_DIR" "$MOUNT_DIR/var/cache/apt/archives" +# Point the image at a CN Debian mirror before chroot apt-get. Debian 13 cloud +# images use mirror+file:///etc/apt/mirrors/*.list (not classic sources.list). +if [ -d "$MOUNT_DIR/etc/apt/mirrors" ]; then + echo "[build-custom-image] Configuring apt mirrors:" + echo "[build-custom-image] debian: $APT_MIRROR" + echo "[build-custom-image] debian-security: $APT_SECURITY_MIRROR" + printf '%s\n' "$APT_MIRROR" | sudo tee "$MOUNT_DIR/etc/apt/mirrors/debian.list" >/dev/null + printf '%s\n' "$APT_SECURITY_MIRROR" | sudo tee "$MOUNT_DIR/etc/apt/mirrors/debian-security.list" >/dev/null +fi +# Avoid hanging forever on flaky links during apt-get update/install. +sudo tee "$MOUNT_DIR/etc/apt/apt.conf.d/80orion-retries" >/dev/null <<'APT_CONF' +Acquire::Retries "5"; +Acquire::http::Timeout "30"; +Acquire::https::Timeout "30"; +APT_CONF + # Copy Rust tarball into the image's /tmp before entering chroot -sudo cp "$RUST_TARBALL" "$MOUNT_DIR/tmp/rust.tar.gz" +sudo cp "$RUST_TARBALL" "$MOUNT_DIR/tmp/rust.tar.xz" # Copy buck2 archive in too so the chroot reuses it instead of hitting GitHub. sudo cp "$BUCK2_TARBALL" "$MOUNT_DIR/tmp/buck2.zst" # Copy cpython_archive tarball for the in-image GitHub mirror (toolchains unchanged). sudo cp "$CPYTHON_TARBALL" "$MOUNT_DIR/tmp/cpython.tar.gz" # Resolve github.com while the host still has normal DNS (chroot will point it at 127.0.0.1). -GITHUB_UPSTREAM_IP=$(getent ahostsv4 github.com 2>/dev/null | awk '{print $1; exit}') -if [ -z "$GITHUB_UPSTREAM_IP" ]; then - GITHUB_UPSTREAM_IP="140.82.121.3" +GITHUB_UPSTREAM_IP=$(getent ahostsv4 github.com 2>/dev/null | awk '{ print $1; exit }') +if [ -z "$GITHUB_UPSTREAM_IP" ] || [ "$GITHUB_UPSTREAM_IP" = "127.0.0.1" ]; then + GITHUB_UPSTREAM_IP="20.205.243.166" echo "[build-custom-image] WARNING: could not resolve github.com; using fallback ${GITHUB_UPSTREAM_IP}" else echo "[build-custom-image] github.com upstream IP for mirror proxy: ${GITHUB_UPSTREAM_IP}" @@ -396,15 +659,18 @@ echo "=== [chroot] Disk space before install ===" df -h / echo "=== [chroot] Extracting Rust toolchain ===" -tar -xzf /tmp/rust.tar.gz -C /tmp/ +tar -xJf /tmp/rust.tar.xz -C /tmp/ bash /tmp/rust-*/install.sh --destdir="" --prefix=/root/.cargo --without=rust-docs -rm -rf /tmp/rust.tar.gz /tmp/rust-* +rm -rf /tmp/rust.tar.xz /tmp/rust-* ln -sf /root/.cargo/bin/rustc /usr/local/bin/rustc ln -sf /root/.cargo/bin/cargo /usr/local/bin/cargo echo "=== [chroot] Rust version: $(/root/.cargo/bin/rustc --version) ===" echo "=== [chroot] Installing apt packages ===" +echo "=== [chroot] apt mirrors: ===" +cat /etc/apt/mirrors/debian.list 2>/dev/null || true +cat /etc/apt/mirrors/debian-security.list 2>/dev/null || true apt-get update apt-get install -y \ clang lld pkg-config protobuf-compiler zstd fuse curl git nginx \ @@ -571,7 +837,9 @@ echo "=== [chroot] Cleaning temp files (host apt caches kept) ===" # unmounted (not copied) before the image is sealed, so it doesn't end up in # the final image anyway. rm -f /var/cache/apt/*.bin -rm -rf /tmp/* /var/tmp/* +# Only clear known scratch files — never `rm -rf /tmp/*` from the host against +# a still-mounted image tree (bind-mounted /proc cannot be deleted that way). +rm -rf /tmp/rust-* /tmp/*.tar.* /tmp/*.zst /tmp/*.gz /var/tmp/* 2>/dev/null || true rm -f /usr/sbin/policy-rc.d echo "=== [chroot] Clearing cloud-init state ===" @@ -614,35 +882,46 @@ if [ "$TEE_RC" -ne 0 ]; then fi echo "[build-custom-image] chroot install OK" +# Write image metadata into the still-mounted guest (and later onto the host +# beside the published qcow2). Scheduler reads the host sidecar at VM create. +write_orion_image_info() { + local dest="$1" + local built_at kernel_ver + built_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + kernel_ver="" + if [ -n "${KERNEL:-}" ]; then + kernel_ver=$(basename "$KERNEL" | sed 's/^vmlinuz-//') + fi + cat > "$dest" <&2 - mountpoint "$MOUNT_DIR/$_sub" 2>/dev/null || true - exit 1 - fi -done - -echo "[build-custom-image] Unmounting image..." -log_cmd umount "$MOUNT_DIR" -if ! sudo umount "$MOUNT_DIR"; then - echo "[build-custom-image] ERROR: umount $MOUNT_DIR failed (exit $?)" >&2 - mountpoint "$MOUNT_DIR" 2>/dev/null || true +echo "[build-custom-image] Unmounting image tree..." +if ! unmount_image_tree "$MOUNT_DIR"; then + echo "[build-custom-image] ERROR: failed to unmount $MOUNT_DIR" >&2 exit 1 fi -rmdir "$MOUNT_DIR" MOUNT_DIR="" log_cmd qemu-nbd --disconnect "$NBD_DEV" -if ! sudo qemu-nbd --disconnect "$NBD_DEV"; then - echo "[build-custom-image] ERROR: qemu-nbd --disconnect $NBD_DEV failed (exit $?)" >&2 - exit 1 -fi +release_nbd "$CUSTOM_IMAGE" echo "[build-custom-image] NBD disconnected" UNCOMPACT_SIZE=$(du -sh "$CUSTOM_IMAGE" | cut -f1) @@ -744,6 +1023,10 @@ JSON_EOF echo "[build-custom-image] JSON updated: $(cat "$PUBLISHED_JSON")" fi +# Host sidecar next to the subdir build and the flat published qcow2. +write_orion_image_info "$IMAGE_DIR/image-info.json" +write_orion_image_info "$OUTPUT_DIR/${IMAGE_NAME}.image-info.json" + if [ -z "${NEW_DIGEST:-}" ]; then NEW_DIGEST=$(sha256sum "$CUSTOM_IMAGE" | awk '{print $1}') fi diff --git a/orion-scheduler/src/handlers.rs b/orion-scheduler/src/handlers.rs index c970757e5..aeeb04de6 100644 --- a/orion-scheduler/src/handlers.rs +++ b/orion-scheduler/src/handlers.rs @@ -207,7 +207,18 @@ fn vm_json(vm: &crate::state::VmInfo) -> serde_json::Value { "vm_ip": vm.ip, "uptime_secs": uptime_secs, "log_file": vm.log_file, - "error": vm.error + "error": vm.error, + "image_path": vm.image_path, + "image_digest": vm.image_digest, + "image_cpus": vm.image_cpus, + "image_memory_mb": vm.image_memory_mb, + "image_disk_gb": vm.image_disk_gb, + "image_name": vm.image_name, + "image_built_at": vm.image_built_at, + "toolchain_rust": vm.toolchain_rust, + "toolchain_buck2": vm.toolchain_buck2, + "toolchain_python": vm.toolchain_python, + "kernel": vm.kernel, }) } diff --git a/orion-scheduler/src/orion_deployer.rs b/orion-scheduler/src/orion_deployer.rs index b2f6895aa..cad3ecee0 100644 --- a/orion-scheduler/src/orion_deployer.rs +++ b/orion-scheduler/src/orion_deployer.rs @@ -14,6 +14,101 @@ use crate::{ type ImageSpecBuildResult = (Option, Option, Option, Option); +/// Sidecar written by `build-custom-image.sh` next to the qcow2. +#[derive(Debug, Clone, Default, serde::Deserialize)] +struct OrionImageInfoFile { + #[serde(default)] + image_name: Option, + #[serde(default)] + built_at: Option, + #[serde(default)] + rust: Option, + #[serde(default)] + buck2: Option, + #[serde(default)] + python: Option, + #[serde(default)] + kernel: Option, +} + +/// Load `*.image-info.json` / `image-info.json` beside a local qcow2 path. +fn load_image_info_sidecar(image_path: &str) -> OrionImageInfoFile { + let expanded = expand_tilde(image_path); + let path = PathBuf::from(&expanded); + let mut candidates: Vec = Vec::new(); + if let Some(parent) = path.parent() { + if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { + candidates.push(parent.join(format!("{stem}.image-info.json"))); + } + candidates.push(parent.join("image-info.json")); + } + for cand in candidates { + match std::fs::read_to_string(&cand) { + Ok(raw) => match serde_json::from_str::(&raw) { + Ok(info) => { + info!( + "[orion-deploy] Loaded image info sidecar: {}", + cand.display() + ); + return info; + } + Err(e) => { + tracing::warn!( + "[orion-deploy] Failed to parse image info {}: {}", + cand.display(), + e + ); + } + }, + Err(_) => continue, + } + } + OrionImageInfoFile::default() +} + +#[derive(Default)] +struct VmRuntimeFields { + ip: Option, + log_file: Option, + error: Option, +} + +fn vm_info_from_params( + id: &str, + domain: &str, + label: &str, + phase: VmPhase, + image_params: &ImageParams, + runtime: VmRuntimeFields, +) -> VmInfo { + let sidecar = image_params + .path + .as_deref() + .map(load_image_info_sidecar) + .unwrap_or_default(); + VmInfo { + id: id.to_string(), + domain: domain.to_string(), + target: label.to_string(), + phase, + ip: runtime.ip, + created_at: std::time::Instant::now(), + log_file: runtime.log_file, + error: runtime.error, + image_path: image_params.path.clone(), + image_digest: image_params.digest.clone(), + image_cpus: image_params.cpus, + image_memory_mb: image_params.memory_mb, + image_disk_gb: image_params.disk_gb, + image_name: sidecar.image_name, + image_built_at: sidecar.built_at, + toolchain_rust: sidecar.rust, + toolchain_buck2: sidecar.buck2, + toolchain_python: sidecar.python, + kernel: sidecar.kernel, + } +} + /// Validate inline runner environment URLs from the webhook payload. pub fn validate_runner_env( server_ws: &str, @@ -114,16 +209,14 @@ async fn handle_update_inner( shutdown_domain(state, domain).await?; } - let provisioning_info = VmInfo { - id: vm_name.to_string(), - domain: domain.to_string(), - target: label.to_string(), - phase: VmPhase::Provisioning, - ip: None, - created_at: std::time::Instant::now(), - log_file: None, - error: None, - }; + let provisioning_info = vm_info_from_params( + vm_name, + domain, + label, + VmPhase::Provisioning, + &image_params, + VmRuntimeFields::default(), + ); state.set_vm_provisioning(provisioning_info).await; info!("Creating new VM in keep_alive mode: {}", vm_name); @@ -146,16 +239,18 @@ async fn handle_update_inner( let vm_ip = machine.get_ip().await.ok().flatten(); info!("[orion-deploy] VM IP: {:?}", vm_ip); - let vm_info = VmInfo { - id: vm_name.to_string(), - domain: domain.to_string(), - target: label.to_string(), - phase: VmPhase::Running, - ip: vm_ip, - created_at: std::time::Instant::now(), - log_file: Some(log_file.clone()), - error: None, - }; + let vm_info = vm_info_from_params( + vm_name, + domain, + label, + VmPhase::Running, + &image_params, + VmRuntimeFields { + ip: vm_ip, + log_file: Some(log_file.clone()), + ..Default::default() + }, + ); state.set_vm(vm_info, machine).await; info!( diff --git a/orion-scheduler/src/state.rs b/orion-scheduler/src/state.rs index ec33741d8..47f86ec2a 100644 --- a/orion-scheduler/src/state.rs +++ b/orion-scheduler/src/state.rs @@ -37,6 +37,20 @@ pub struct VmInfo { pub log_file: Option, /// Error message when phase is Failed pub error: Option, + /// Local qcow2 path used to create this VM (if any). + pub image_path: Option, + /// Image digest used at create (`sha256:...`). + pub image_digest: Option, + pub image_cpus: Option, + pub image_memory_mb: Option, + pub image_disk_gb: Option, + /// From host `*.image-info.json` beside the qcow2 (optional). + pub image_name: Option, + pub image_built_at: Option, + pub toolchain_rust: Option, + pub toolchain_buck2: Option, + pub toolchain_python: Option, + pub kernel: Option, } pub struct VmEntry { @@ -230,6 +244,17 @@ mod tests { created_at: std::time::Instant::now(), log_file: None, error: None, + image_path: None, + image_digest: None, + image_cpus: None, + image_memory_mb: None, + image_disk_gb: None, + image_name: None, + image_built_at: None, + toolchain_rust: None, + toolchain_buck2: None, + toolchain_python: None, + kernel: None, } } From 2b2e960decedde1f0784b60a716ed97165b0d48e Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Wed, 29 Jul 2026 11:34:10 +0800 Subject: [PATCH 2/3] chore(ci): temporarily disable AWS ECR image pushes Keep Harbor-only deploys while AWS pushes are paused; flip PUSH_AWS_ECR back to true to restore ECR Public tags and manifests. --- .github/workflows/mono-engine-deploy.yml | 36 +++++++++++++++----- .github/workflows/orion-server-deploy.yml | 34 +++++++++++++----- .github/workflows/web-deploy.yml | 32 ++++++++++++----- .github/workflows/web-sync-server-deploy.yml | 31 ++++++++++------- 4 files changed, 96 insertions(+), 37 deletions(-) diff --git a/.github/workflows/mono-engine-deploy.yml b/.github/workflows/mono-engine-deploy.yml index 57654df0a..2b5f51b1b 100644 --- a/.github/workflows/mono-engine-deploy.yml +++ b/.github/workflows/mono-engine-deploy.yml @@ -21,6 +21,8 @@ env: REPOSITORY: mega/mono-engine IMAGE_TAG_BASE: latest HARBOR_REGISTRY: registry.xuanwu.openatom.cn + # TEMP: skip AWS ECR Public push; Harbor only. Re-enable ECR steps below to restore. + PUSH_AWS_ECR: "false" # Using AWS access key for authentication. # actions: write is required for docker buildx --cache-{from,to} type=gha. @@ -50,9 +52,10 @@ jobs: uses: crazy-max/ghaction-github-runtime@v3 # ----------------------------- - # AWS / ECR Public + # AWS / ECR Public (TEMP disabled) # ----------------------------- - name: Configure AWS Credentials + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -60,6 +63,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -73,23 +77,31 @@ jobs: password: ${{ secrets.HARBOR_PASSWORD }} # ----------------------------- - # Build & push to ECR + Harbor + # Build & push (Harbor; ECR when PUSH_AWS_ECR=true) # ----------------------------- - - name: Build & push image to ECR and Harbor (amd64) + - name: Build & push image (amd64) env: ECR_REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} IMAGE_TAG_BASE: ${{ env.IMAGE_TAG_BASE }} + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail ARCH_SUFFIX=amd64 IMAGE_TAG="${IMAGE_TAG_BASE}-${ARCH_SUFFIX}" - ECR_IMAGE="$ECR_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" HARBOR_IMAGE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}:$IMAGE_TAG" CACHE_SCOPE="mono-engine-${ARCH_SUFFIX}" - echo "ECR_IMAGE=$ECR_IMAGE" + TAGS=(-t "$HARBOR_IMAGE") + if [ "$PUSH_AWS_ECR" = "true" ]; then + ECR_IMAGE="$ECR_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" + echo "ECR_IMAGE=$ECR_IMAGE" + TAGS+=(-t "$ECR_IMAGE") + else + echo "TEMP: skipping AWS ECR push (Harbor only)" + fi + echo "HARBOR_IMAGE=$HARBOR_IMAGE" echo "CACHE_SCOPE=$CACHE_SCOPE" @@ -99,8 +111,7 @@ jobs: --provenance=false \ --sbom=false \ -f ./mono/Dockerfile \ - -t "$ECR_IMAGE" \ - -t "$HARBOR_IMAGE" \ + "${TAGS[@]}" \ --push . manifest: @@ -110,6 +121,7 @@ jobs: contents: read steps: - name: Configure AWS Credentials + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -117,6 +129,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -132,9 +145,9 @@ jobs: - name: Create & push manifest env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail - IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" SHORT_SHA="${GITHUB_SHA:0:7}" TAG_BASE="${{ env.IMAGE_TAG_BASE }}" @@ -155,7 +168,12 @@ jobs: docker manifest push "$base:$SHORT_SHA" } - push_manifests "$IMAGE_BASE" + if [ "$PUSH_AWS_ECR" = "true" ]; then + IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + push_manifests "$IMAGE_BASE" + else + echo "TEMP: skipping AWS ECR manifests (Harbor only)" + fi push_manifests "$HARBOR_BASE" deploy-aws: diff --git a/.github/workflows/orion-server-deploy.yml b/.github/workflows/orion-server-deploy.yml index 9aa4003e7..87b39c2b6 100644 --- a/.github/workflows/orion-server-deploy.yml +++ b/.github/workflows/orion-server-deploy.yml @@ -12,6 +12,8 @@ env: REPOSITORY: mega/orion-server IMAGE_TAG_BASE: latest HARBOR_REGISTRY: registry.xuanwu.openatom.cn + # TEMP: skip AWS ECR Public push; Harbor only. Re-enable ECR steps below to restore. + PUSH_AWS_ECR: "false" # actions: write is required for docker buildx --cache-{from,to} type=gha. permissions: @@ -44,9 +46,10 @@ jobs: uses: crazy-max/ghaction-github-runtime@v3 # ----------------------------- - # AWS auth + # AWS auth (TEMP disabled) # ----------------------------- - name: Configure AWS Credentials + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -54,6 +57,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -67,23 +71,31 @@ jobs: password: ${{ secrets.HARBOR_PASSWORD }} # ----------------------------- - # Build & push to AWS + Harbor + # Build & push (Harbor; ECR when PUSH_AWS_ECR=true) # ----------------------------- - - name: Build & push amd64 image to AWS and Harbor + - name: Build & push amd64 image env: AWS_REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail PLATFORM="linux/amd64" ARCH_SUFFIX="amd64" - AWS_IMAGE_BASE="$AWS_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_IMAGE_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" TAG="${{ env.IMAGE_TAG_BASE }}-$ARCH_SUFFIX" CACHE_SCOPE="orion-server-$ARCH_SUFFIX" + TAGS=(-t "$HARBOR_IMAGE_BASE:$TAG") + if [ "$PUSH_AWS_ECR" = "true" ]; then + AWS_IMAGE_BASE="$AWS_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + TAGS+=(-t "$AWS_IMAGE_BASE:$TAG") + else + echo "TEMP: skipping AWS ECR push (Harbor only)" + fi + docker buildx build \ --platform "$PLATFORM" \ --cache-from type=gha,scope=$CACHE_SCOPE \ @@ -91,8 +103,7 @@ jobs: --provenance=false \ --sbom=false \ -f orion-server/Dockerfile \ - -t "$AWS_IMAGE_BASE:$TAG" \ - -t "$HARBOR_IMAGE_BASE:$TAG" \ + "${TAGS[@]}" \ --push . manifest: @@ -102,6 +113,7 @@ jobs: contents: read steps: - name: Configure AWS Credentials + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -109,6 +121,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -124,9 +137,9 @@ jobs: - name: Create & push manifest env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail - IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" SHORT_SHA="${GITHUB_SHA:0:7}" TAG_BASE="${{ env.IMAGE_TAG_BASE }}" @@ -147,7 +160,12 @@ jobs: docker manifest push "$base:$SHORT_SHA" } - push_manifests "$IMAGE_BASE" + if [ "$PUSH_AWS_ECR" = "true" ]; then + IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + push_manifests "$IMAGE_BASE" + else + echo "TEMP: skipping AWS ECR manifests (Harbor only)" + fi push_manifests "$HARBOR_BASE" deploy-aws: diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 62f18a63c..2eaaa32e7 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -14,6 +14,8 @@ env: REPOSITORY: mega/mega-ui IMAGE_VERSION: latest HARBOR_REGISTRY: registry.xuanwu.openatom.cn + # TEMP: skip AWS ECR Public push; Harbor only. Re-enable ECR steps below to restore. + PUSH_AWS_ECR: "false" # actions: write is required for docker buildx --cache-{from,to} type=gha. permissions: @@ -54,6 +56,7 @@ jobs: uses: crazy-max/ghaction-github-runtime@v3 - name: Configure AWS Credentials (ECR Public, us-east-1) + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -61,6 +64,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -78,6 +82,7 @@ jobs: env: ECR_PUBLIC_REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} PLATFORM: linux/amd64 + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail ARCH_SUFFIX="amd64" @@ -85,11 +90,18 @@ jobs: SHORT_SHA="${GITHUB_SHA:0:7}" SHA_TAG="${SHORT_SHA}-${ARCH_SUFFIX}" - PUBLIC_IMAGE="$ECR_PUBLIC_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" - PUBLIC_IMAGE_SHA="$ECR_PUBLIC_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$SHA_TAG" HARBOR_IMAGE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}:$IMAGE_TAG" HARBOR_IMAGE_SHA="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}:$SHA_TAG" + TAGS=(-t "$HARBOR_IMAGE" -t "$HARBOR_IMAGE_SHA") + if [ "$PUSH_AWS_ECR" = "true" ]; then + PUBLIC_IMAGE="$ECR_PUBLIC_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" + PUBLIC_IMAGE_SHA="$ECR_PUBLIC_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$SHA_TAG" + TAGS+=(-t "$PUBLIC_IMAGE" -t "$PUBLIC_IMAGE_SHA") + else + echo "TEMP: skipping AWS ECR push (Harbor only)" + fi + docker buildx build \ --platform "$PLATFORM" \ --build-arg TIPTAP_PRIVATE_REGISTRY_KEY="$TIPTAP_PRIVATE_REGISTRY_KEY" \ @@ -98,10 +110,7 @@ jobs: --provenance=false \ --sbom=false \ -f apps/web/Dockerfile \ - -t "$PUBLIC_IMAGE" \ - -t "$PUBLIC_IMAGE_SHA" \ - -t "$HARBOR_IMAGE" \ - -t "$HARBOR_IMAGE_SHA" . \ + "${TAGS[@]}" . \ --push manifest: @@ -111,6 +120,7 @@ jobs: contents: read steps: - name: Configure AWS Credentials + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -118,6 +128,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -133,10 +144,10 @@ jobs: - name: Create & push unified manifest env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail - IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" TAG="${IMAGE_VERSION}" SHORT_SHA="${GITHUB_SHA:0:7}" @@ -152,7 +163,12 @@ jobs: docker manifest push "$base:$SHORT_SHA" } - push_manifests "$IMAGE_BASE" + if [ "$PUSH_AWS_ECR" = "true" ]; then + IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + push_manifests "$IMAGE_BASE" + else + echo "TEMP: skipping AWS ECR manifests (Harbor only)" + fi push_manifests "$HARBOR_BASE" deploy-aws: diff --git a/.github/workflows/web-sync-server-deploy.yml b/.github/workflows/web-sync-server-deploy.yml index 8e4245653..13f9d33f1 100644 --- a/.github/workflows/web-sync-server-deploy.yml +++ b/.github/workflows/web-sync-server-deploy.yml @@ -23,6 +23,8 @@ env: REPOSITORY: mega/web-sync-server IMAGE_TAG: latest HARBOR_REGISTRY: registry.xuanwu.openatom.cn + # TEMP: skip AWS ECR Public push; Harbor only. Re-enable ECR steps below to restore. + PUSH_AWS_ECR: "false" jobs: build-and-push: @@ -37,6 +39,7 @@ jobs: node-version: "26" - name: Configure AWS Credentials + if: env.PUSH_AWS_ECR == 'true' uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -44,6 +47,7 @@ jobs: aws-region: us-east-1 - name: Login to Amazon ECR Public + if: env.PUSH_AWS_ECR == 'true' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v2 with: @@ -56,31 +60,34 @@ jobs: username: ${{ secrets.HARBOR_USERNAME }} password: ${{ secrets.HARBOR_PASSWORD }} - - name: Build, tag, and push docker image to ECR Public and Harbor + - name: Build, tag, and push docker image working-directory: moon env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} + PUSH_AWS_ECR: ${{ env.PUSH_AWS_ECR }} run: | set -euo pipefail - AWS_IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_IMAGE_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" IMAGE_TAG="${{ env.IMAGE_TAG }}" SHORT_SHA="${GITHUB_SHA:0:7}" docker build \ -f apps/sync-server/Dockerfile \ - -t "$AWS_IMAGE_BASE:$IMAGE_TAG" . + -t "$HARBOR_IMAGE_BASE:$IMAGE_TAG" \ + -t "$HARBOR_IMAGE_BASE:$SHORT_SHA" \ + . + + if [ "$PUSH_AWS_ECR" = "true" ]; then + AWS_IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + docker tag "$HARBOR_IMAGE_BASE:$IMAGE_TAG" "$AWS_IMAGE_BASE:$IMAGE_TAG" + docker tag "$HARBOR_IMAGE_BASE:$IMAGE_TAG" "$AWS_IMAGE_BASE:$SHORT_SHA" + docker push "$AWS_IMAGE_BASE:$IMAGE_TAG" + docker push "$AWS_IMAGE_BASE:$SHORT_SHA" + else + echo "TEMP: skipping AWS ECR push (Harbor only)" + fi - docker tag "$AWS_IMAGE_BASE:$IMAGE_TAG" "$AWS_IMAGE_BASE:$SHORT_SHA" - docker tag "$AWS_IMAGE_BASE:$IMAGE_TAG" "$HARBOR_IMAGE_BASE:$IMAGE_TAG" - docker tag "$AWS_IMAGE_BASE:$IMAGE_TAG" "$HARBOR_IMAGE_BASE:$SHORT_SHA" - - # Push to AWS ECR Public (latest + commit hash) - docker push "$AWS_IMAGE_BASE:$IMAGE_TAG" - docker push "$AWS_IMAGE_BASE:$SHORT_SHA" - - # Push to Harbor (latest + commit hash) docker push "$HARBOR_IMAGE_BASE:$IMAGE_TAG" docker push "$HARBOR_IMAGE_BASE:$SHORT_SHA" From e50b07b4c42ff7ce30adf45d6163cf41c1949df4 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Wed, 29 Jul 2026 11:34:30 +0800 Subject: [PATCH 3/3] feat(orion): keep runner VMs listed on /oc Expose GET /api/v1/orion/runners from scheduler /status and show a persistent Runner VMs table so image/phase/IP details stay visible after startup logs close. --- ceres/src/model/orion_runner.rs | 9 + .../orion-scheduler-client/src/http_client.rs | 70 +++--- clients/orion-scheduler-client/src/lib.rs | 47 +++- mono/src/api/router/orion_runner_router.rs | 134 +++++++++--- .../components/OrionClient/RunnersTable.tsx | 200 ++++++++++++++++++ .../apps/web/components/OrionClient/index.tsx | 1 + .../web/hooks/OrionClient/useGetRunnerList.ts | 24 +++ .../hooks/OrionClient/usePostStartRunner.ts | 5 +- moon/apps/web/pages/[org]/oc/index.tsx | 43 +++- moon/packages/types/generated.ts | 60 +++++- 10 files changed, 522 insertions(+), 71 deletions(-) create mode 100644 moon/apps/web/components/OrionClient/RunnersTable.tsx create mode 100644 moon/apps/web/hooks/OrionClient/useGetRunnerList.ts diff --git a/ceres/src/model/orion_runner.rs b/ceres/src/model/orion_runner.rs index 514ec6d19..140fbeb90 100644 --- a/ceres/src/model/orion_runner.rs +++ b/ceres/src/model/orion_runner.rs @@ -41,6 +41,8 @@ pub struct RunnerStatusResponse { #[serde(default, skip_serializing_if = "Option::is_none")] pub domain: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub target: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub vm_ip: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub log_file: Option, @@ -71,3 +73,10 @@ pub struct RunnerStatusResponse { #[serde(default, skip_serializing_if = "Option::is_none")] pub kernel: Option, } + +/// List of runner VMs currently tracked by orion-scheduler. +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct RunnerListResponse { + pub count: usize, + pub runners: Vec, +} diff --git a/clients/orion-scheduler-client/src/http_client.rs b/clients/orion-scheduler-client/src/http_client.rs index 86f800384..8ee043d36 100644 --- a/clients/orion-scheduler-client/src/http_client.rs +++ b/clients/orion-scheduler-client/src/http_client.rs @@ -3,7 +3,10 @@ use std::time::Duration; use http::header::AUTHORIZATION; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::client::IntoClientRequest}; -use crate::{SchedulerStatusResponse, StartRunnerPayload, StartRunnerSchedulerResponse}; +use crate::{ + SchedulerStatusResponse, SchedulerVmListResponse, StartRunnerPayload, + StartRunnerSchedulerResponse, +}; pub type TerminalWebSocket = WebSocketStream>; @@ -87,6 +90,7 @@ impl OrionSchedulerHttpClient { phase: Some("no_vm".to_string()), vm_id: Some(vm_id.to_string()), domain: None, + target: None, vm_ip: None, uptime_secs: None, log_file: None, @@ -114,48 +118,50 @@ impl OrionSchedulerHttpClient { } } - pub async fn get_status(&self) -> anyhow::Result { + pub async fn list_vms(&self) -> anyhow::Result { let url = format!("{}/status", self.base_url); let req = self.client.get(&url).timeout(Duration::from_secs(30)); let res = self.auth_headers(req).send().await?; if res.status().is_success() { - // List form — not used by mono GET by id path anymore. - let v: serde_json::Value = res.json().await?; - if let Some(vms) = v.get("vms").and_then(|x| x.as_array()) { - if let Some(first) = vms.first() { - return Ok(serde_json::from_value(first.clone())?); - } - return Ok(SchedulerStatusResponse { - status: "no_vm".to_string(), - phase: Some("no_vm".to_string()), - vm_id: None, - domain: None, - vm_ip: None, - uptime_secs: None, - log_file: None, - error: None, - image_path: None, - image_digest: None, - image_cpus: None, - image_memory_mb: None, - image_disk_gb: None, - image_name: None, - image_built_at: None, - toolchain_rust: None, - toolchain_buck2: None, - toolchain_python: None, - kernel: None, - }); - } - Ok(serde_json::from_value(v)?) + Ok(res.json().await?) } else { Err(anyhow::anyhow!( - "Scheduler get_status failed: {}", + "Scheduler list_vms failed: {}", res.status() )) } } + pub async fn get_status(&self) -> anyhow::Result { + let list = self.list_vms().await?; + Ok(list + .vms + .into_iter() + .next() + .unwrap_or(SchedulerStatusResponse { + status: "no_vm".to_string(), + phase: Some("no_vm".to_string()), + vm_id: None, + domain: None, + target: None, + vm_ip: None, + uptime_secs: None, + log_file: None, + error: None, + image_path: None, + image_digest: None, + image_cpus: None, + image_memory_mb: None, + image_disk_gb: None, + image_name: None, + image_built_at: None, + toolchain_rust: None, + toolchain_buck2: None, + toolchain_python: None, + kernel: None, + })) + } + /// Open an SSE stream of live Orion runner logs (`GET /logs/orion/stream`). /// /// Prefer `vm_id`; when absent, `domain` is used. At least one must be set. diff --git a/clients/orion-scheduler-client/src/lib.rs b/clients/orion-scheduler-client/src/lib.rs index 0375268e3..b3d6458c5 100644 --- a/clients/orion-scheduler-client/src/lib.rs +++ b/clients/orion-scheduler-client/src/lib.rs @@ -57,6 +57,8 @@ pub struct SchedulerStatusResponse { #[serde(default)] pub domain: Option, #[serde(default)] + pub target: Option, + #[serde(default)] pub vm_ip: Option, #[serde(default)] pub uptime_secs: Option, @@ -88,6 +90,16 @@ pub struct SchedulerStatusResponse { pub kernel: Option, } +/// Response from scheduler `GET /status` (all VMs). +#[derive(Debug, Clone, Deserialize)] +pub struct SchedulerVmListResponse { + pub status: String, + #[serde(default)] + pub count: usize, + #[serde(default)] + pub vms: Vec, +} + #[derive(Clone)] pub struct OrionSchedulerClient { http: OrionSchedulerHttpClient, @@ -116,9 +128,40 @@ impl OrionSchedulerClient { self.http.get_vm_status(vm_id).await } - /// Backward-compatible list/status endpoint. + /// Backward-compatible list/status endpoint (returns first VM if any). pub async fn get_status(&self) -> anyhow::Result { - self.http.get_status().await + let list = self.list_vms().await?; + Ok(list + .vms + .into_iter() + .next() + .unwrap_or(SchedulerStatusResponse { + status: "no_vm".to_string(), + phase: Some("no_vm".to_string()), + vm_id: None, + domain: None, + target: None, + vm_ip: None, + uptime_secs: None, + log_file: None, + error: None, + image_path: None, + image_digest: None, + image_cpus: None, + image_memory_mb: None, + image_disk_gb: None, + image_name: None, + image_built_at: None, + toolchain_rust: None, + toolchain_buck2: None, + toolchain_python: None, + kernel: None, + })) + } + + /// List all VMs tracked by the scheduler (`GET /status`). + pub async fn list_vms(&self) -> anyhow::Result { + self.http.list_vms().await } /// Proxy-friendly SSE stream of runner / orion-client startup logs. diff --git a/mono/src/api/router/orion_runner_router.rs b/mono/src/api/router/orion_runner_router.rs index a47deffdf..40fa7a9b6 100644 --- a/mono/src/api/router/orion_runner_router.rs +++ b/mono/src/api/router/orion_runner_router.rs @@ -8,10 +8,12 @@ use axum::{ response::{IntoResponse, Response}, routing::get, }; -use ceres::model::orion_runner::{RunnerStatusResponse, StartRunnerRequest, StartRunnerResponse}; +use ceres::model::orion_runner::{ + RunnerListResponse, RunnerStatusResponse, StartRunnerRequest, StartRunnerResponse, +}; use common::config::BuildConfig; use futures::{SinkExt, StreamExt, TryStreamExt}; -use orion_scheduler_client::{OrionSchedulerClient, StartRunnerPayload}; +use orion_scheduler_client::{OrionSchedulerClient, SchedulerStatusResponse, StartRunnerPayload}; use tokio_tungstenite::tungstenite::Message as TungsteniteMessage; use utoipa_axum::{router::OpenApiRouter, routes}; @@ -24,6 +26,7 @@ pub fn routers() -> OpenApiRouter { OpenApiRouter::new().nest( "/orion/runners", OpenApiRouter::new() + .routes(routes!(list_runners)) .routes(routes!(start_runner)) .routes(routes!(stream_runner_logs)) .routes(routes!(get_runner_status)) @@ -121,6 +124,103 @@ fn is_local_runner_domain(host: &str) -> bool { || host.ends_with(".local") } +fn runner_status_from_sched( + sched: SchedulerStatusResponse, + fallback_id: Option<&str>, +) -> Option { + let phase = sched + .phase + .clone() + .or_else(|| Some(sched.status.clone())) + .unwrap_or_else(|| "unknown".to_string()); + if phase == "no_vm" { + return None; + } + let vm_id = sched.vm_id.or_else(|| fallback_id.map(str::to_string))?; + Some(RunnerStatusResponse { + vm_id, + phase, + domain: sched.domain, + target: sched.target.filter(|t| !t.is_empty()), + vm_ip: sched.vm_ip, + log_file: sched.log_file, + error: sched.error, + uptime_secs: sched.uptime_secs, + image_path: sched.image_path, + image_digest: sched.image_digest, + image_cpus: sched.image_cpus, + image_memory_mb: sched.image_memory_mb, + image_disk_gb: sched.image_disk_gb, + image_name: sched.image_name, + image_built_at: sched.image_built_at, + toolchain_rust: sched.toolchain_rust, + toolchain_buck2: sched.toolchain_buck2, + toolchain_python: sched.toolchain_python, + kernel: sched.kernel, + }) +} + +/// List Orion runner VMs currently tracked by orion-scheduler. +#[utoipa::path( + get, + path = "/", + responses( + (status = 200, body = CommonResult, content_type = "application/json"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden - admin only"), + (status = 503, description = "Scheduler not configured"), + (status = 502, description = "Scheduler unreachable"), + ), + tag = ORION_RUNNER_TAG +)] +async fn list_runners( + user: LoginUser, + State(state): State, +) -> Result>, ApiError> { + ensure_admin(&state, &user).await?; + let client = scheduler_client(&state)?; + + let list = client.list_vms().await.map_err(|e| { + ApiError::with_status( + StatusCode::BAD_GATEWAY, + anyhow!("Scheduler request failed: {}", e), + ) + })?; + + let mut runners: Vec = list + .vms + .into_iter() + .filter_map(|vm| runner_status_from_sched(vm, None)) + .collect(); + // Stable order: running first, then provisioning, then failed; domain asc. + runners.sort_by(|a, b| { + phase_rank(&a.phase) + .cmp(&phase_rank(&b.phase)) + .then_with(|| { + a.domain + .as_deref() + .unwrap_or("") + .cmp(b.domain.as_deref().unwrap_or("")) + }) + .then_with(|| a.vm_id.cmp(&b.vm_id)) + }); + + let count = runners.len(); + Ok(Json(CommonResult::success(Some(RunnerListResponse { + count, + runners, + })))) +} + +fn phase_rank(phase: &str) -> u8 { + match phase { + "running" => 0, + "provisioning" => 1, + "failed" => 2, + _ => 3, + } +} + /// Start a new Orion runner VM via orion-scheduler. #[utoipa::path( post, @@ -239,36 +339,14 @@ async fn get_runner_status( ) })?; - let phase = sched - .phase - .clone() - .or_else(|| Some(sched.status.clone())) - .unwrap_or_else(|| "unknown".to_string()); + let status = runner_status_from_sched(sched, Some(&id)) + .ok_or_else(|| ApiError::not_found(anyhow!("Runner VM '{}' not found", id)))?; - if phase == "no_vm" || sched.vm_id.as_deref() != Some(id.as_str()) { + if status.vm_id != id { return Err(ApiError::not_found(anyhow!("Runner VM '{}' not found", id))); } - Ok(Json(CommonResult::success(Some(RunnerStatusResponse { - vm_id: id, - phase, - domain: sched.domain, - vm_ip: sched.vm_ip, - log_file: sched.log_file, - error: sched.error, - uptime_secs: sched.uptime_secs, - image_path: sched.image_path, - image_digest: sched.image_digest, - image_cpus: sched.image_cpus, - image_memory_mb: sched.image_memory_mb, - image_disk_gb: sched.image_disk_gb, - image_name: sched.image_name, - image_built_at: sched.image_built_at, - toolchain_rust: sched.toolchain_rust, - toolchain_buck2: sched.toolchain_buck2, - toolchain_python: sched.toolchain_python, - kernel: sched.kernel, - })))) + Ok(Json(CommonResult::success(Some(status)))) } /// Proxy live Orion runner / client startup logs from orion-scheduler as SSE. diff --git a/moon/apps/web/components/OrionClient/RunnersTable.tsx b/moon/apps/web/components/OrionClient/RunnersTable.tsx new file mode 100644 index 000000000..a18629326 --- /dev/null +++ b/moon/apps/web/components/OrionClient/RunnersTable.tsx @@ -0,0 +1,200 @@ +'use client' + +import React from 'react' +import { ThemeProvider } from '@primer/react' +import { DataTable } from '@primer/react/experimental' +import { useTheme } from 'next-themes' + +import type { RunnerStatusResponse } from '@gitmono/types/generated' +import { Badge, Button, UIText } from '@gitmono/ui' + +function colWidth(parts: number) { + return `minmax(0, ${parts}fr)` +} + +function shortDigest(digest: string | null | undefined): string | null { + if (!digest) return null + return digest.replace(/^sha256:/, '').slice(0, 12) +} + +function formatUptime(totalSecs: number | null | undefined): string { + if (totalSecs == null) return '—' + const secs = Math.max(0, Math.floor(totalSecs)) + const h = Math.floor(secs / 3600) + const m = Math.floor((secs % 3600) / 60) + const s = secs % 60 + + if (h > 0) return `${h}h ${m}m` + if (m > 0) return `${m}m ${s}s` + return `${s}s` +} + +function phaseBadge(phase: string) { + const normalized = phase.toLowerCase() + const color = + normalized === 'running' + ? 'green' + : normalized === 'provisioning' + ? 'blue' + : normalized === 'failed' + ? 'brand' + : 'default' + + return ( + + {phase || 'unknown'} + + ) +} + +interface RunnersTableProps { + runners: RunnerStatusResponse[] + isLoading?: boolean + errorMessage?: string | null + onViewLogs?: (runner: RunnerStatusResponse) => void + onConnectTerminal?: (runner: RunnerStatusResponse) => void + canManage?: boolean +} + +type Row = RunnerStatusResponse & { id: string } + +export function RunnersTable({ + runners, + isLoading, + errorMessage, + onViewLogs, + onConnectTerminal, + canManage = false +}: RunnersTableProps) { + const { resolvedTheme } = useTheme() + const rows = React.useMemo(() => runners.map((r) => ({ ...r, id: r.vm_id })), [runners]) + const showActions = Boolean(canManage && (onViewLogs || onConnectTerminal)) + + const columns = React.useMemo( + () => [ + { + header: 'VM ID', + field: 'vm_id', + rowHeader: true, + width: colWidth(14), + renderCell: (row: Row) => ( +
+ + {row.vm_id} + +
+ ) + }, + { + header: 'Domain', + field: 'domain', + width: colWidth(16), + renderCell: (row: Row) =>
{row.domain || '—'}
+ }, + { + header: 'Phase', + field: 'phase', + width: colWidth(12), + renderCell: (row: Row) => phaseBadge(row.phase) + }, + { + header: 'VM IP', + field: 'vm_ip', + width: colWidth(12), + renderCell: (row: Row) =>
{row.vm_ip || '—'}
+ }, + { + header: 'Image', + field: 'image_name', + width: colWidth(18), + renderCell: (row: Row) => { + const digest = shortDigest(row.image_digest) + const label = row.image_name || digest || '—' + + return ( +
+ {label} + {row.image_name && digest ? ( + ({digest}) + ) : null} +
+ ) + } + }, + { + header: 'Resources', + field: 'image_cpus', + width: colWidth(14), + renderCell: (row: Row) => { + const parts = [ + row.image_cpus != null ? `${row.image_cpus} vCPU` : null, + row.image_memory_mb != null ? `${Math.round(row.image_memory_mb / 1024)} GiB` : null, + row.image_disk_gb != null ? `${row.image_disk_gb} GiB disk` : null + ].filter(Boolean) + + return
{parts.length ? parts.join(' / ') : '—'}
+ } + }, + { + header: 'Uptime', + field: 'uptime_secs', + width: colWidth(10), + renderCell: (row: Row) =>
{formatUptime(row.uptime_secs)}
+ }, + ...(showActions + ? [ + { + header: 'Actions', + field: 'id', + width: colWidth(14), + renderCell: (row: Row) => ( +
+ {onViewLogs ? ( + + ) : null} + {onConnectTerminal && row.phase === 'running' ? ( + + ) : null} +
+ ) + } + ] + : []) + ], + [onConnectTerminal, onViewLogs, showActions] + ) + + return ( +
+
+ + Runner VMs + + + {isLoading ? 'Loading…' : `${runners.length} tracked`} + +
+ {errorMessage ? ( + + {errorMessage} + + ) : null} + + {rows.length === 0 && !isLoading ? ( +
+ + No runner VMs yet. Use Start Runner to provision one — it will stay listed here while the scheduler tracks + it. + +
+ ) : ( + + )} +
+
+ ) +} diff --git a/moon/apps/web/components/OrionClient/index.tsx b/moon/apps/web/components/OrionClient/index.tsx index f9ca186ec..c68de8cfa 100644 --- a/moon/apps/web/components/OrionClient/index.tsx +++ b/moon/apps/web/components/OrionClient/index.tsx @@ -2,4 +2,5 @@ export * from './types' export * from './StatusBadge' export * from './CapabilitiesBadgeList' export * from './ClientsTable' +export * from './RunnersTable' export * from './VmTerminal' diff --git a/moon/apps/web/hooks/OrionClient/useGetRunnerList.ts b/moon/apps/web/hooks/OrionClient/useGetRunnerList.ts new file mode 100644 index 000000000..ab9d78d6c --- /dev/null +++ b/moon/apps/web/hooks/OrionClient/useGetRunnerList.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query' + +import type { RunnerListResponse } from '@gitmono/types/generated' + +import { legacyApiClient } from '@/utils/queryClient' + +export function useGetRunnerList(enabled = true) { + const query = legacyApiClient.v1.getApiOrionRunners() + + return useQuery({ + queryKey: query.requestKey(), + queryFn: async () => { + const result = await query.request() + + if (!result.req_result || !result.data) { + throw new Error(result.err_message || 'Failed to fetch runner VMs') + } + return result.data + }, + enabled, + refetchInterval: enabled ? 10_000 : false, + staleTime: 0 + }) +} diff --git a/moon/apps/web/hooks/OrionClient/usePostStartRunner.ts b/moon/apps/web/hooks/OrionClient/usePostStartRunner.ts index b6ef27d02..39567159f 100644 --- a/moon/apps/web/hooks/OrionClient/usePostStartRunner.ts +++ b/moon/apps/web/hooks/OrionClient/usePostStartRunner.ts @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'react-hot-toast' import type { StartRunnerRequest, StartRunnerResponse } from '@gitmono/types/generated' @@ -6,7 +6,9 @@ import type { StartRunnerRequest, StartRunnerResponse } from '@gitmono/types/gen import { legacyApiClient } from '@/utils/queryClient' export function usePostStartRunner() { + const queryClient = useQueryClient() const mutation = legacyApiClient.v1.postApiOrionRunners() + const listQuery = legacyApiClient.v1.getApiOrionRunners() return useMutation({ mutationFn: async (body) => { @@ -18,6 +20,7 @@ export function usePostStartRunner() { return result.data }, onSuccess: (data) => { + void queryClient.invalidateQueries({ queryKey: listQuery.baseKey }) if (data.phase === 'running') { toast.success(data.domain ? `Runner already running (${data.domain})` : 'Runner already running') } else { diff --git a/moon/apps/web/pages/[org]/oc/index.tsx b/moon/apps/web/pages/[org]/oc/index.tsx index ff2133767..f8f735d1b 100644 --- a/moon/apps/web/pages/[org]/oc/index.tsx +++ b/moon/apps/web/pages/[org]/oc/index.tsx @@ -15,10 +15,11 @@ import { Button, UIText } from '@gitmono/ui' import { RefreshIcon } from '@gitmono/ui/Icons' import { AppLayout } from '@/components/Layout/AppLayout' -import { ClientsTable, OrionClient, OrionClientStatus, VmTerminal } from '@/components/OrionClient' +import { ClientsTable, OrionClient, OrionClientStatus, RunnersTable, VmTerminal } from '@/components/OrionClient' import AuthAppProviders from '@/components/Providers/AuthAppProviders' import { useAdminCheck } from '@/hooks/admin/useAdminCheck' import { usePostOrionClientsInfo } from '@/hooks/OrionClient/OrionClientsInfo' +import { useGetRunnerList } from '@/hooks/OrionClient/useGetRunnerList' import { useGetRunnerStatus } from '@/hooks/OrionClient/useGetRunnerStatus' import { usePostStartRunner } from '@/hooks/OrionClient/usePostStartRunner' import { useRunnerLogsSSE } from '@/hooks/OrionClient/useRunnerLogsSSE' @@ -111,6 +112,12 @@ const OrionClientPage: PageWithLayout = () => { const isAdmin = adminCheck?.data?.is_admin || false const { mutate: startRunner, isPending: isStartingRunner } = usePostStartRunner() + const { + data: runnerList, + isLoading: isLoadingRunners, + error: runnerListError, + refetch: refetchRunners + } = useGetRunnerList(isAdmin) const runnerStatusVmId = logSource === 'runner' ? activeLogKey : null const { data: runnerStatus } = useGetRunnerStatus(runnerStatusVmId, activePhase) const { logs: runnerLogs, status: runnerLogsStatus, error: runnerLogsError } = useRunnerLogsSSE(activeLogKey) @@ -190,12 +197,16 @@ const OrionClientPage: PageWithLayout = () => { const handleRefresh = React.useCallback(() => { if (showingOverlay) return + if (isAdmin) { + void refetchRunners() + } + mutate(requestPayload, { onSuccess: (data) => { setClientsPage(data) } }) - }, [mutate, requestPayload, showingOverlay]) + }, [isAdmin, mutate, refetchRunners, requestPayload, showingOverlay]) React.useEffect(() => { if (!runnerStatus) return @@ -314,6 +325,23 @@ const OrionClientPage: PageWithLayout = () => { [openTerminalPanel] ) + const handleViewRunnerLogs = React.useCallback( + (runner: { vm_id: string; domain?: string | null; phase?: string | null }) => { + openLogPanel(runner.vm_id, 'runner', { + domain: runner.domain ?? null, + phase: runner.phase ?? null + }) + }, + [openLogPanel] + ) + + const handleConnectRunnerTerminal = React.useCallback( + (runner: { vm_id: string; domain?: string | null }) => { + openTerminalPanel(runner.vm_id, 'runner', { domain: runner.domain ?? null }) + }, + [openTerminalPanel] + ) + const handleLogsKeyDown = React.useCallback( async (e: React.KeyboardEvent) => { const meta = e.metaKey || e.ctrlKey @@ -672,6 +700,17 @@ const OrionClientPage: PageWithLayout = () => { {!showingOverlay ? ( <> + {isAdmin ? ( + + ) : null} +
extends HttpClient { @@ -21202,6 +21226,30 @@ export class Api extends HttpClient { + const base = 'GET:/api/v1/orion/runners' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (params: RequestParams = {}) => + this.request({ + path: `/api/v1/orion/runners`, + method: 'GET', + format: 'json', + ...params + }) + } + }, + /** * No description *