Skip to content
Open
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
29 changes: 29 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,22 @@ enum SandboxCommands {
#[arg(long)]
gpu: bool,

/// Minimum CPU cores requested (Kubernetes quantity, e.g. "500m", "2").
#[arg(long, value_name = "QUANTITY", help_heading = "RESOURCE FLAGS")]
cpu_request: Option<String>,

/// Maximum CPU cores allowed (Kubernetes quantity, e.g. "500m", "4").
#[arg(long, value_name = "QUANTITY", help_heading = "RESOURCE FLAGS")]
cpu_limit: Option<String>,

/// Minimum memory requested (Kubernetes quantity, e.g. "256Mi", "4Gi").
#[arg(long, value_name = "QUANTITY", help_heading = "RESOURCE FLAGS")]
memory_request: Option<String>,

/// Maximum memory allowed (Kubernetes quantity, e.g. "512Mi", "8Gi").
#[arg(long, value_name = "QUANTITY", help_heading = "RESOURCE FLAGS")]
memory_limit: Option<String>,

/// SSH destination for remote bootstrap (e.g., user@hostname).
/// Only used when no cluster exists yet; ignored if a cluster is
/// already active.
Expand Down Expand Up @@ -2307,6 +2323,10 @@ async fn main() -> Result<()> {
no_keep,
editor,
gpu,
cpu_request,
cpu_limit,
memory_request,
memory_limit,
remote,
ssh_key,
providers,
Expand Down Expand Up @@ -2368,6 +2388,13 @@ async fn main() -> Result<()> {
(local, remote, !no_git_ignore)
});

let resource_args = run::ResourceArgs {
cpu_request,
cpu_limit,
memory_request,
memory_limit,
};

let editor = editor.map(Into::into);
let forward = forward
.map(|s| openshell_core::forward::ForwardSpec::parse(&s))
Expand Down Expand Up @@ -2402,6 +2429,7 @@ async fn main() -> Result<()> {
upload_spec.as_ref(),
keep,
gpu,
&resource_args,
editor,
remote.as_deref(),
ssh_key.as_deref(),
Expand All @@ -2425,6 +2453,7 @@ async fn main() -> Result<()> {
upload_spec.as_ref(),
keep,
gpu,
&resource_args,
editor,
remote.as_deref(),
ssh_key.as_deref(),
Expand Down
143 changes: 139 additions & 4 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,72 @@ pub use openshell_core::forward::{
find_forward_by_port, list_forwards, stop_forward, stop_forwards_for_sandbox,
};

/// Compute resource arguments for sandbox creation.
#[derive(Debug, Default)]
pub struct ResourceArgs {
pub cpu_request: Option<String>,
pub cpu_limit: Option<String>,
pub memory_request: Option<String>,
pub memory_limit: Option<String>,
}

impl ResourceArgs {
/// Build a `prost_types::Struct` matching the Kubernetes resources shape
/// expected by `extract_typed_resources` in the gateway server.
///
/// Returns `None` when no CPU or memory fields are set (GPU is handled
/// separately via `SandboxSpec.gpu`).
pub fn to_resources_struct(&self) -> Option<prost_types::Struct> {
use prost_types::{Struct, Value, value::Kind};

let mut requests = std::collections::BTreeMap::new();
let mut limits = std::collections::BTreeMap::new();

if let Some(ref v) = self.cpu_request {
requests.insert(
"cpu".to_string(),
Value { kind: Some(Kind::StringValue(v.clone())) },
);
}
if let Some(ref v) = self.memory_request {
requests.insert(
"memory".to_string(),
Value { kind: Some(Kind::StringValue(v.clone())) },
);
}
if let Some(ref v) = self.cpu_limit {
limits.insert(
"cpu".to_string(),
Value { kind: Some(Kind::StringValue(v.clone())) },
);
}
if let Some(ref v) = self.memory_limit {
limits.insert(
"memory".to_string(),
Value { kind: Some(Kind::StringValue(v.clone())) },
);
}
if requests.is_empty() && limits.is_empty() {
return None;
}

let mut fields = std::collections::BTreeMap::new();
if !requests.is_empty() {
fields.insert(
"requests".to_string(),
Value { kind: Some(Kind::StructValue(Struct { fields: requests })) },
);
}
if !limits.is_empty() {
fields.insert(
"limits".to_string(),
Value { kind: Some(Kind::StructValue(Struct { fields: limits })) },
);
}
Some(Struct { fields })
}
}

/// Convert a sandbox phase integer to a human-readable string.
fn phase_name(phase: i32) -> &'static str {
match SandboxPhase::try_from(phase) {
Expand Down Expand Up @@ -1923,6 +1989,7 @@ pub async fn sandbox_create_with_bootstrap(
upload: Option<&(String, Option<String>, bool)>,
keep: bool,
gpu: bool,
resource_args: &ResourceArgs,
editor: Option<Editor>,
remote: Option<&str>,
ssh_key: Option<&str>,
Expand Down Expand Up @@ -1954,6 +2021,7 @@ pub async fn sandbox_create_with_bootstrap(
upload,
keep,
gpu,
resource_args,
editor,
remote,
ssh_key,
Expand Down Expand Up @@ -2010,6 +2078,7 @@ pub async fn sandbox_create(
upload: Option<&(String, Option<String>, bool)>,
keep: bool,
gpu: bool,
resource_args: &ResourceArgs,
editor: Option<Editor>,
remote: Option<&str>,
ssh_key: Option<&str>,
Expand Down Expand Up @@ -2109,10 +2178,19 @@ pub async fn sandbox_create(

let policy = load_sandbox_policy(policy)?;

let template = image.map(|img| SandboxTemplate {
image: img,
..SandboxTemplate::default()
});
let resources_struct = resource_args.to_resources_struct();
let template = match (image, &resources_struct) {
(Some(img), _) => Some(SandboxTemplate {
image: img,
resources: resources_struct,
..SandboxTemplate::default()
}),
(None, Some(_)) => Some(SandboxTemplate {
resources: resources_struct,
..SandboxTemplate::default()
}),
(None, None) => None,
};

let request = CreateSandboxRequest {
spec: Some(SandboxSpec {
Expand Down Expand Up @@ -6184,4 +6262,61 @@ mod tests {
"should end with single-quote: {ssh_escaped}"
);
}

// ------------------------------------------------------------------
// ResourceArgs tests
// ------------------------------------------------------------------

use super::ResourceArgs;
use prost_types::value::Kind;

#[test]
fn resource_args_empty_returns_none() {
let args = ResourceArgs::default();
assert!(args.to_resources_struct().is_none());
}

#[test]
fn resource_args_cpu_only_request() {
let args = ResourceArgs {
cpu_request: Some("500m".to_string()),
..ResourceArgs::default()
};
let s = args.to_resources_struct().expect("should return Some");
let requests = &s.fields["requests"];
let Kind::StructValue(inner) = requests.kind.as_ref().unwrap() else {
panic!("expected StructValue");
};
let Kind::StringValue(cpu) = inner.fields["cpu"].kind.as_ref().unwrap() else {
panic!("expected StringValue");
};
assert_eq!(cpu, "500m");
assert!(!s.fields.contains_key("limits"));
}

#[test]
fn resource_args_all_four_fields() {
let args = ResourceArgs {
cpu_request: Some("500m".to_string()),
cpu_limit: Some("4".to_string()),
memory_request: Some("256Mi".to_string()),
memory_limit: Some("8Gi".to_string()),
};
let s = args.to_resources_struct().expect("should return Some");

let get = |section: &str, key: &str| -> String {
let Kind::StructValue(sec) = s.fields[section].kind.as_ref().unwrap() else {
panic!("expected StructValue for {section}");
};
let Kind::StringValue(val) = sec.fields[key].kind.as_ref().unwrap() else {
panic!("expected StringValue for {section}.{key}");
};
val.clone()
};

assert_eq!(get("requests", "cpu"), "500m");
assert_eq!(get("requests", "memory"), "256Mi");
assert_eq!(get("limits", "cpu"), "4");
assert_eq!(get("limits", "memory"), "8Gi");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ async fn sandbox_create_keeps_command_sessions_by_default() {
None,
true,
false,
&run::ResourceArgs::default(),
None,
None,
None,
Expand Down Expand Up @@ -612,6 +613,7 @@ async fn sandbox_create_deletes_command_sessions_with_no_keep() {
None,
false,
false,
&run::ResourceArgs::default(),
None,
None,
None,
Expand Down Expand Up @@ -656,6 +658,7 @@ async fn sandbox_create_deletes_shell_sessions_with_no_keep() {
None,
false,
false,
&run::ResourceArgs::default(),
None,
None,
None,
Expand Down Expand Up @@ -700,6 +703,7 @@ async fn sandbox_create_keeps_sandbox_with_hidden_keep_flag() {
None,
true,
false,
&run::ResourceArgs::default(),
None,
None,
None,
Expand Down Expand Up @@ -741,6 +745,7 @@ async fn sandbox_create_keeps_sandbox_with_forwarding() {
None,
false,
false,
&run::ResourceArgs::default(),
None,
None,
None,
Expand Down
Loading