Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.

Commit 0cd7182

Browse files
committed
feat(orion): fan-out image publish and public presign endpoint
Allow build-custom-image to upload/register across envs, avoid Start Runner hangs behind the update lock, and sign Orion image URLs with an optional public RustFS host so out-of-cluster schedulers can download.
1 parent 09a093e commit 0cd7182

21 files changed

Lines changed: 1050 additions & 203 deletions

File tree

‎clients/orion-scheduler-client/src/http_client.rs‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,24 @@ impl OrionSchedulerHttpClient {
5959
"Starting runner via scheduler: server_ws={}",
6060
payload.server_ws
6161
);
62+
// Conflict checks must not block behind a multi-minute image download;
63+
// scheduler returns 503 quickly when the update lock is busy. Keep a
64+
// modest client budget for network + signing + lock try.
6265
let req = self
6366
.client
6467
.post(&url)
65-
.timeout(Duration::from_secs(10))
68+
.timeout(Duration::from_secs(30))
6669
.json(&payload);
6770
let res = self.auth_headers(req).send().await?;
6871
let status = res.status();
6972
let body: StartRunnerSchedulerResponse = res.json().await?;
70-
// 200 OK (idempotent), 202 Accepted (provisioning), 409 Conflict
71-
if status.is_success() || status.as_u16() == 202 || status.as_u16() == 409 {
73+
// 200 OK (idempotent), 202 Accepted (provisioning), 409 Conflict,
74+
// 503 Busy (another provision holds the update lock).
75+
if status.is_success()
76+
|| status.as_u16() == 202
77+
|| status.as_u16() == 409
78+
|| status.as_u16() == 503
79+
{
7280
Ok(body)
7381
} else {
7482
Err(anyhow::anyhow!(

‎clients/orion-scheduler-client/src/lib.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ pub struct StartRunnerPayload {
2828
pub image_cpus: Option<u32>,
2929
#[serde(skip_serializing_if = "Option::is_none")]
3030
pub image_memory_mb: Option<u32>,
31+
/// Catalog metadata (from mono when starting via `image_id`).
32+
#[serde(skip_serializing_if = "Option::is_none")]
33+
pub image_name: Option<String>,
34+
#[serde(skip_serializing_if = "Option::is_none")]
35+
pub image_built_at: Option<String>,
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
pub toolchain_rust: Option<String>,
38+
#[serde(skip_serializing_if = "Option::is_none")]
39+
pub toolchain_buck2: Option<String>,
40+
#[serde(skip_serializing_if = "Option::is_none")]
41+
pub toolchain_python: Option<String>,
42+
#[serde(skip_serializing_if = "Option::is_none")]
43+
pub kernel: Option<String>,
3144
/// When set, write `ORION_RETAIN_ANTARES_MOUNTS` into the guest `.env`.
3245
#[serde(skip_serializing_if = "Option::is_none")]
3346
pub retain_antares_mounts: Option<bool>,

‎common/src/config/mod.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,13 @@ pub struct S3Config {
614614
pub access_key_id: String,
615615
pub secret_access_key: String,
616616
pub endpoint_url: String,
617+
/// Optional endpoint used only when generating presigned URLs.
618+
///
619+
/// Keep `endpoint_url` as the in-cluster address for mono PUT/GET, and set
620+
/// this to a host that out-of-cluster clients (e.g. orion-scheduler) can
621+
/// reach. Empty = sign with `endpoint_url` (unchanged behavior).
622+
#[serde(default)]
623+
pub presign_endpoint_url: String,
617624
}
618625

619626
#[derive(Debug, Serialize, Deserialize, Default, Clone)]

‎config/config.toml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,15 @@ secret_access_key = ""
152152
# set this to the service endpoint.
153153
endpoint_url = "http://localhost:9000"
154154

155+
# Optional: endpoint used only for presigned GET/PUT URLs.
156+
# Use when mono talks to RustFS over an in-cluster URL but external clients
157+
# (orion-scheduler on the host) need a public hostname. Signature includes
158+
# Host, so this must match the URL clients will open. Empty = use endpoint_url.
159+
# Example:
160+
# endpoint_url = "http://rustfs.mega-dev.svc.cluster.local:9000"
161+
# presign_endpoint_url = "https://rustfs.xuanwu.openatom.cn"
162+
presign_endpoint_url = ""
163+
155164

156165
[object_storage.gcs]
157166
# Name of the GCS bucket

‎io-orbit/src/adapter.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ pub enum UploadStrategy {
4141
pub struct ObjectStoreAdapter {
4242
/// The concrete backend store used for all object operations.
4343
pub store: BackendStore,
44+
/// Optional S3 client used only for `signed_url` (public/presign endpoint).
45+
/// When `None`, signing uses [`Self::store`].
46+
pub presign_store: Option<Arc<AmazonS3>>,
4447
/// The upload strategy used when writing new objects.
4548
pub upload_strategy: UploadStrategy,
4649
}
@@ -165,6 +168,16 @@ impl MegaObjectStorage for ObjectStoreAdapter {
165168
) -> Result<Option<String>, MegaError> {
166169
let path = key.to_object_store_path();
167170

171+
if let Some(presign) = &self.presign_store {
172+
return Ok(Some(
173+
presign
174+
.signed_url(method, &path, expires_in)
175+
.await
176+
.map_err(IoOrbitError::from)?
177+
.to_string(),
178+
));
179+
}
180+
168181
let url = match &self.store {
169182
BackendStore::S3(s3) => Some(
170183
s3.signed_url(method, &path, expires_in)
@@ -1171,6 +1184,7 @@ mod tests {
11711184
let local = Arc::new(LocalFileSystem::new_with_prefix(dir.path()).unwrap());
11721185
let adapter = ObjectStoreAdapter {
11731186
store: BackendStore::Local(Arc::clone(&local)),
1187+
presign_store: None,
11741188
upload_strategy: UploadStrategy::SinglePut,
11751189
};
11761190
let key = ObjectKey {

‎io-orbit/src/factory.rs‎

Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ use std::{
44
};
55

66
use common::{
7-
config::{ObjectStorageBackend, ObjectStorageConfig},
7+
config::{ObjectStorageBackend, ObjectStorageConfig, S3Config},
88
errors::MegaError,
99
};
10-
use object_store::{aws::AmazonS3Builder, gcp::GoogleCloudStorageBuilder, local::LocalFileSystem};
10+
use object_store::{
11+
aws::{AmazonS3, AmazonS3Builder},
12+
gcp::GoogleCloudStorageBuilder,
13+
local::LocalFileSystem,
14+
};
1115

1216
use crate::{
1317
adapter::{BackendStore, ObjectStoreAdapter, UploadStrategy},
@@ -42,6 +46,7 @@ impl MegaObjectStorageWrapper {
4246
let store = BackendStore::Local(Arc::new(fs));
4347
let adapter = Arc::new(ObjectStoreAdapter {
4448
store,
49+
presign_store: None,
4550
upload_strategy: UploadStrategy::SinglePut,
4651
});
4752
MegaObjectStorageWrapper::new(adapter)
@@ -62,26 +67,58 @@ impl ObjectStorageFactory {
6267
}
6368
}
6469

65-
/// Shared S3 / S3-compatible construction (differs only by endpoint and upload strategy).
66-
async fn build_s3_like(
67-
cfg: &ObjectStorageConfig,
70+
fn build_amazon_s3(
71+
s3_cfg: &S3Config,
72+
endpoint: &str,
6873
compatible: bool,
69-
) -> Result<MegaObjectStorageWrapper, MegaError> {
70-
let s3_cfg = cfg.s3.clone();
74+
) -> Result<AmazonS3, MegaError> {
7175
let mut builder = AmazonS3Builder::new()
7276
.with_region(&s3_cfg.region)
7377
.with_bucket_name(&s3_cfg.bucket)
7478
.with_access_key_id(&s3_cfg.access_key_id)
7579
.with_secret_access_key(&s3_cfg.secret_access_key);
76-
if compatible {
77-
builder = builder
78-
.with_endpoint(&s3_cfg.endpoint_url)
79-
.with_allow_http(true)
80-
.with_virtual_hosted_style_request(false);
80+
81+
if compatible || !endpoint.is_empty() {
82+
if !endpoint.is_empty() {
83+
builder = builder.with_endpoint(endpoint);
84+
}
85+
if compatible {
86+
// S3-compatible (RustFS/MinIO): path-style; allow plain HTTP for
87+
// in-cluster endpoints. HTTPS public endpoints still work with
88+
// allow_http(true).
89+
builder = builder
90+
.with_allow_http(true)
91+
.with_virtual_hosted_style_request(false);
92+
} else if endpoint.starts_with("http://") {
93+
builder = builder.with_allow_http(true);
94+
}
8195
}
82-
let s3 = builder
83-
.build()
84-
.map_err(|e| MegaError::Other(e.to_string()))?;
96+
97+
builder.build().map_err(|e| MegaError::Other(e.to_string()))
98+
}
99+
100+
/// Shared S3 / S3-compatible construction (differs only by endpoint and upload strategy).
101+
async fn build_s3_like(
102+
cfg: &ObjectStorageConfig,
103+
compatible: bool,
104+
) -> Result<MegaObjectStorageWrapper, MegaError> {
105+
let s3_cfg = &cfg.s3;
106+
let endpoint = if compatible {
107+
s3_cfg.endpoint_url.as_str()
108+
} else {
109+
// Real AWS: empty endpoint uses the default regional endpoint.
110+
""
111+
};
112+
let s3 = build_amazon_s3(s3_cfg, endpoint, compatible)?;
113+
114+
let presign_store = {
115+
let presign_ep = s3_cfg.presign_endpoint_url.trim();
116+
if presign_ep.is_empty() {
117+
None
118+
} else {
119+
Some(Arc::new(build_amazon_s3(s3_cfg, presign_ep, compatible)?))
120+
}
121+
};
85122

86123
let store = BackendStore::S3(Arc::new(s3));
87124
let upload_strategy = if compatible {
@@ -91,6 +128,7 @@ async fn build_s3_like(
91128
};
92129
let adapter = Arc::new(ObjectStoreAdapter {
93130
store,
131+
presign_store,
94132
upload_strategy,
95133
});
96134

@@ -106,6 +144,7 @@ async fn build_gcs(cfg: &ObjectStorageConfig) -> Result<MegaObjectStorageWrapper
106144
let store = BackendStore::Gcs(Arc::new(gcs));
107145
let adapter = Arc::new(ObjectStoreAdapter {
108146
store,
147+
presign_store: None,
109148
upload_strategy: UploadStrategy::SinglePut,
110149
});
111150

@@ -122,8 +161,103 @@ async fn build_local(cfg: &ObjectStorageConfig) -> Result<MegaObjectStorageWrapp
122161
let store = BackendStore::Local(Arc::new(fs));
123162
let adapter = Arc::new(ObjectStoreAdapter {
124163
store,
164+
presign_store: None,
125165
upload_strategy: UploadStrategy::SinglePut,
126166
});
127167

128168
Ok(MegaObjectStorageWrapper::new(adapter))
129169
}
170+
171+
#[cfg(test)]
172+
mod tests {
173+
use std::time::Duration;
174+
175+
use common::config::{
176+
GcsConfig, LocalConfig, ObjectStorageBackend, ObjectStorageConfig, S3Config,
177+
};
178+
use object_store::{path::Path, signer::Signer};
179+
use reqwest::Method;
180+
181+
use super::build_amazon_s3;
182+
use crate::object_storage::{ObjectKey, ObjectNamespace};
183+
184+
fn sample_s3_config(endpoint: &str, presign: &str) -> S3Config {
185+
S3Config {
186+
region: "us-east-1".into(),
187+
bucket: "buck2hub-assets".into(),
188+
access_key_id: "testkey".into(),
189+
secret_access_key: "testsecret".into(),
190+
endpoint_url: endpoint.into(),
191+
presign_endpoint_url: presign.into(),
192+
}
193+
}
194+
195+
#[tokio::test]
196+
async fn signed_url_uses_endpoint_url_when_presign_unset() {
197+
let s3_cfg = sample_s3_config("http://rustfs.internal:9000", "");
198+
let s3 = build_amazon_s3(&s3_cfg, &s3_cfg.endpoint_url, true).unwrap();
199+
let path = Path::from("orion-images/abc/debian-13-buck2.qcow2");
200+
let url = s3
201+
.signed_url(Method::GET, &path, Duration::from_secs(60))
202+
.await
203+
.unwrap()
204+
.to_string();
205+
assert!(
206+
url.starts_with("http://rustfs.internal:9000/"),
207+
"expected internal host, got {url}"
208+
);
209+
}
210+
211+
#[tokio::test]
212+
async fn signed_url_uses_presign_endpoint_when_set() {
213+
let s3_cfg = sample_s3_config(
214+
"http://rustfs.mega-dev.svc.cluster.local:9000",
215+
"https://rustfs.xuanwu.openatom.cn",
216+
);
217+
let presign = build_amazon_s3(&s3_cfg, &s3_cfg.presign_endpoint_url, true).unwrap();
218+
let path = Path::from("orion-images/abc/debian-13-buck2.qcow2");
219+
let url = presign
220+
.signed_url(Method::GET, &path, Duration::from_secs(60))
221+
.await
222+
.unwrap()
223+
.to_string();
224+
assert!(
225+
url.starts_with("https://rustfs.xuanwu.openatom.cn/"),
226+
"expected public host, got {url}"
227+
);
228+
assert!(
229+
!url.contains("svc.cluster.local"),
230+
"presign URL must not use in-cluster host: {url}"
231+
);
232+
}
233+
234+
#[tokio::test]
235+
async fn factory_wires_presign_store_for_compatible() {
236+
use crate::factory::ObjectStorageFactory;
237+
238+
let cfg = ObjectStorageConfig {
239+
storage_type: ObjectStorageBackend::S3Compatible,
240+
s3: sample_s3_config(
241+
"http://rustfs.internal:9000",
242+
"https://rustfs.example.public",
243+
),
244+
gcs: GcsConfig::default(),
245+
local: LocalConfig::default(),
246+
};
247+
let wrapper = ObjectStorageFactory::build(&cfg).await.unwrap();
248+
let key = ObjectKey {
249+
namespace: ObjectNamespace::OrionImage,
250+
key: "deadbeef/debian-13-buck2.qcow2".into(),
251+
};
252+
let url = wrapper
253+
.inner
254+
.signed_url(&key, Method::GET, Duration::from_secs(120))
255+
.await
256+
.unwrap()
257+
.expect("s3compatible must support presign");
258+
assert!(
259+
url.starts_with("https://rustfs.example.public/"),
260+
"factory should sign with presign_endpoint_url, got {url}"
261+
);
262+
}
263+
}

‎jupiter/src/service/orion_vm_image_service.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ impl OrionVmImageService {
4242
self.st.list_all().await
4343
}
4444

45+
/// Newest catalog row by `created_at` (same order as [`Self::list`]).
46+
pub async fn latest(&self) -> Result<Option<orion_vm_image::Model>, MegaError> {
47+
Ok(self.list().await?.into_iter().next())
48+
}
49+
4550
pub async fn get(&self, id: &str) -> Result<Option<orion_vm_image::Model>, MegaError> {
4651
self.st.find_by_id(id).await
4752
}

0 commit comments

Comments
 (0)