Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 175 additions & 90 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ orion = { path = "orion" }
orion-client = { path = "clients/orion-client" }
orion-scheduler-client = { path = "clients/orion-scheduler-client" }

git-internal = "0.8.7"
git-internal = "0.9.0"
libvault = "0.3.0"

#====
Expand All @@ -63,7 +63,7 @@ futures = "0.3.34"
futures-util = "0.3.34"
axum = { version = "0.8.9", features = ["macros", "json"] }
axum-extra = "0.12.6"
russh = "0.63.2"
russh = "0.63.3"
tower-http = "0.7.1"
tower = "0.5.3"
tower-sessions = { version = "0.15", features = ["memory-store"] }
Expand Down Expand Up @@ -93,7 +93,7 @@ hmac = "0.13"

idgenerator = "2.0.0"
config = "0.15.25"
reqwest = "0.13.4"
reqwest = "0.13.5"
uuid = "1.24.1"
regex = "1.13.1"
ctrlc = "3.5.2"
Expand Down
18 changes: 18 additions & 0 deletions ceres/src/application/api_service/mono/admin/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ impl AdminApplicationService {
})
}

/// Ensure orion-image-publisher bot + fresh catalog-register token
/// (for secret-gated bootstrap-orion-image).
pub async fn ensure_orion_image_publisher_bot_token(
&self,
) -> Result<crate::model::bots::BootstrapInitBotResponse, MegaError> {
let (bot, token_plain) = self
.ctx
.storage()
.bots_storage()
.ensure_orion_image_publisher_bot_token()
.await?;
Ok(crate::model::bots::BootstrapInitBotResponse {
bot_id: bot.id,
bot_name: bot.name,
token: token_plain,
})
}

pub async fn list_bot_tokens(&self, bot_id: i64) -> Result<Vec<ListBotTokenItem>, MegaError> {
Ok(self
.ctx
Expand Down
65 changes: 60 additions & 5 deletions ceres/src/application/api_service/mono/admin/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ const ADMIN_CACHE_KEY_SUFFIX: &str = "admin:list";

impl AdminApplicationService {
/// Check if a user is an admin (config `monorepo.admin` or Cedar).
///
/// Config admins are resolved without loading Cedar, so they remain valid
/// when `.mega_cedar.json` is missing or its blob cannot be fetched.
pub async fn check_is_admin(&self, username: &str) -> Result<bool, MegaError> {
let username = username.trim();
if username.is_empty() {
return Ok(false);
}
if self.config_admins().iter().any(|a| a == username) {
return Ok(true);
}
let admins = self.get_effective_admins().await?;
Ok(admins.iter().any(|a| a == username))
}
Expand Down Expand Up @@ -69,7 +75,7 @@ impl AdminApplicationService {
///
/// Config admins are always merged after cache/file load so a stale Redis
/// Cedar list cannot drop configured admins. If `.mega_cedar.json` is
/// missing, config admins alone still apply.
/// missing or cannot be fetched/parsed, config admins alone still apply.
async fn get_effective_admins(&self) -> Result<Vec<String>, MegaError> {
let cedar_admins = self.get_cedar_admins().await?;
Ok(self.merge_with_config_admins(cedar_admins))
Expand Down Expand Up @@ -199,8 +205,57 @@ impl AdminApplicationService {
}

fn is_admin_config_unavailable(err: &MegaError) -> bool {
let msg = err.to_string();
msg.contains(".mega_cedar.json not found")
|| msg.contains("Root ref not found")
|| msg.contains("Root tree not found")
match err {
MegaError::ObjStorageNotFound(_)
| MegaError::ObjStorageInconsistent(_)
| MegaError::ObjStorage(_)
| MegaError::SerdeJson(_) => true,
MegaError::Other(msg) => {
msg.contains(".mega_cedar.json not found")
|| msg.contains("Root ref not found")
|| msg.contains("Root tree not found")
|| msg.contains("UTF-8 decode failed")
|| msg.contains("JSON parse failed")
}
_ => false,
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn obj_storage_inconsistent_is_unavailable() {
let err = MegaError::ObjStorageInconsistent(
"[obj_missing_in_s3_but_has_meta] Object missing".into(),
);
assert!(is_admin_config_unavailable(&err));
}

#[test]
fn obj_storage_not_found_is_unavailable() {
let err = MegaError::ObjStorageNotFound("blob missing".into());
assert!(is_admin_config_unavailable(&err));
}

#[test]
fn missing_cedar_file_is_unavailable() {
let err = MegaError::Other(".mega_cedar.json not found in root directory".into());
assert!(is_admin_config_unavailable(&err));
}

#[test]
fn utf8_and_json_parse_failures_are_unavailable() {
let utf8 = MegaError::Other("UTF-8 decode failed: invalid utf-8".into());
let json = MegaError::Other("JSON parse failed: expected value".into());
assert!(is_admin_config_unavailable(&utf8));
assert!(is_admin_config_unavailable(&json));
}

#[test]
fn unrelated_bad_request_is_not_unavailable() {
let err = MegaError::BadRequest("admins must not be empty".into());
assert!(!is_admin_config_unavailable(&err));
}
}
7 changes: 5 additions & 2 deletions ceres/src/model/bots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,15 @@ impl From<InstallationTargetType> for InstallationTargetTypeEnum {
#[derive(Debug, Clone)]
pub struct BotIdentity {
pub bot_id: i64,
pub bot_name: String,
pub token_id: i64,
}

impl BotIdentity {
pub fn from_models(bot: callisto::bots::Model, token: callisto::bot_tokens::Model) -> Self {
Self {
bot_id: bot.id,
bot_name: bot.name,
token_id: token.id,
}
}
Expand Down Expand Up @@ -160,10 +162,11 @@ pub struct CreateBotTokenResponse {
pub token_plain: String,
}

/// Response for mega-init bot bootstrap (`POST /bots/bootstrap-init`).
/// Response for secret-gated bot bootstrap (`POST /bots/bootstrap-init` or
/// `POST /bots/bootstrap-orion-image`).
///
/// Requires header `X-Mega-Init-Secret` matching `MEGA_INIT_BOOTSTRAP_SECRET`.
/// `token` is a `bot_` push token returned once; use as Bearer (or Basic password).
/// `token` is a `bot_` token returned once; use as Bearer (or Basic password).
#[derive(Serialize, ToSchema)]
pub struct BootstrapInitBotResponse {
#[serde(serialize_with = "serialize_i64_as_string")]
Expand Down
1 change: 1 addition & 0 deletions ceres/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod label;
pub mod merge_queue;
pub mod note;
pub mod notification;
pub mod orion_image;
pub mod orion_runner;
pub mod serde_snowflake;
pub mod tag;
Expand Down
83 changes: 83 additions & 0 deletions ceres/src/model/orion_image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct OrionVmImageResponse {
pub id: String,
pub digest: String,
pub object_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info_object_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub built_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rust: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub buck2: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub python: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub created_at: String,
}

#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct OrionVmImageListResponse {
pub count: usize,
pub images: Vec<OrionVmImageResponse>,
}

#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct RegisterOrionVmImageRequest {
/// Content digest, e.g. `sha256:<hex>`.
pub digest: String,
/// Key under the `orion-images/` namespace, e.g. `{hex}/debian-13-buck2.qcow2`.
pub object_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info_object_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub built_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rust: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub buck2: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub python: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}

#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct PresignOrionVmImageRequest {
/// Content digest, e.g. `sha256:<hex>`.
pub digest: String,
/// Base name used in the object key (default `debian-13-buck2`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_name: Option<String>,
/// When true, also return a PUT URL for `{hex}/image-info.json`.
#[serde(default)]
pub with_info: bool,
}

#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct PresignOrionVmImageResponse {
pub object_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info_object_key: Option<String>,
pub qcow2_put_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info_put_url: Option<String>,
pub expires_in_secs: u64,
}
4 changes: 4 additions & 0 deletions ceres/src/model/orion_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub struct StartRunnerRequest {
pub image_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_digest: Option<String>,
/// Catalog image id from `GET /api/v1/orion/images`. Mutually exclusive with
/// `image_path` / `image_url`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_disk_gb: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand Down
14 changes: 11 additions & 3 deletions clients/orion-scheduler-client/src/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,24 @@ impl OrionSchedulerHttpClient {
"Starting runner via scheduler: server_ws={}",
payload.server_ws
);
// Conflict checks must not block behind a multi-minute image download;
// scheduler returns 503 quickly when the update lock is busy. Keep a
// modest client budget for network + signing + lock try.
let req = self
.client
.post(&url)
.timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.json(&payload);
let res = self.auth_headers(req).send().await?;
let status = res.status();
let body: StartRunnerSchedulerResponse = res.json().await?;
// 200 OK (idempotent), 202 Accepted (provisioning), 409 Conflict
if status.is_success() || status.as_u16() == 202 || status.as_u16() == 409 {
// 200 OK (idempotent), 202 Accepted (provisioning), 409 Conflict,
// 503 Busy (another provision holds the update lock).
if status.is_success()
|| status.as_u16() == 202
|| status.as_u16() == 409
|| status.as_u16() == 503
{
Ok(body)
} else {
Err(anyhow::anyhow!(
Expand Down
13 changes: 13 additions & 0 deletions clients/orion-scheduler-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ pub struct StartRunnerPayload {
pub image_cpus: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_memory_mb: Option<u32>,
/// Catalog metadata (from mono when starting via `image_id`).
#[serde(skip_serializing_if = "Option::is_none")]
pub image_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_built_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolchain_rust: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolchain_buck2: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolchain_python: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kernel: Option<String>,
/// When set, write `ORION_RETAIN_ANTARES_MOUNTS` into the guest `.env`.
#[serde(skip_serializing_if = "Option::is_none")]
pub retain_antares_mounts: Option<bool>,
Expand Down
7 changes: 7 additions & 0 deletions common/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,13 @@ pub struct S3Config {
pub access_key_id: String,
pub secret_access_key: String,
pub endpoint_url: String,
/// Optional endpoint used only when generating presigned URLs.
///
/// Keep `endpoint_url` as the in-cluster address for mono PUT/GET, and set
/// this to a host that out-of-cluster clients (e.g. orion-scheduler) can
/// reach. Empty = sign with `endpoint_url` (unchanged behavior).
#[serde(default)]
pub presign_endpoint_url: String,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
Expand Down
9 changes: 9 additions & 0 deletions config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ secret_access_key = ""
# set this to the service endpoint.
endpoint_url = "http://localhost:9000"

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


[object_storage.gcs]
# Name of the GCS bucket
Expand Down
Loading
Loading