Skip to content

Commit cd3f905

Browse files
committed
feat(api)!: model CPU and memory as typed compute resource requirements
Add typed cpu/memory fields to ResourceRequirements in both the public and compute-driver protos, replacing the Struct-based SandboxTemplate.resources passthrough for portable sizing. Update CLI --cpu/--memory to populate the typed fields, gateway validation to reject the legacy template.resources.limits/requests.cpu|memory path, and the Kubernetes, Docker, and Podman drivers to consume the typed requirements. VM and MXC drivers explicitly reject typed CPU/memory until sizing support lands, rather than silently ignoring it. Docs and SDK surface updates are still outstanding. BREAKING CHANGE: SandboxTemplate.resources.limits.cpu, limits.memory, requests.cpu, and requests.memory are no longer interpreted as portable sandbox sizing and are now rejected by the gateway. Callers must migrate to spec.resource_requirements.cpu.limit / memory.limit. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent d1155aa commit cd3f905

16 files changed

Lines changed: 679 additions & 387 deletions

File tree

crates/openshell-cli/src/main.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use openshell_bootstrap::{
1919
use openshell_cli::completers;
2020
use openshell_cli::run;
2121
use openshell_cli::tls::TlsOptions;
22-
use openshell_core::proto::GpuResourceRequirements;
22+
use openshell_core::proto::{GpuResourceRequirements, ResourceRequirements};
2323

2424
/// Resolved gateway context: name + gateway endpoint.
2525
struct GatewayContext {
@@ -3066,6 +3066,21 @@ async fn run_async() -> Result<()> {
30663066
.transpose()?;
30673067
let keep = keep || !no_keep || editor.is_some() || forward.is_some();
30683068
let gpu_requirements: Option<GpuResourceRequirements> = gpu.map(Into::into);
3069+
let cpu_requirements = run::build_cpu_resource_requirements(cpu.as_deref())?;
3070+
let memory_requirements =
3071+
run::build_memory_resource_requirements(memory.as_deref())?;
3072+
let resource_requirements = if gpu_requirements.is_some()
3073+
|| cpu_requirements.is_some()
3074+
|| memory_requirements.is_some()
3075+
{
3076+
Some(ResourceRequirements {
3077+
gpu: gpu_requirements,
3078+
cpu: cpu_requirements,
3079+
memory: memory_requirements,
3080+
})
3081+
} else {
3082+
None
3083+
};
30693084

30703085
let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?;
30713086
let endpoint = &ctx.endpoint;
@@ -3079,9 +3094,7 @@ async fn run_async() -> Result<()> {
30793094
from: from.as_deref(),
30803095
uploads: &upload_specs,
30813096
keep,
3082-
gpu_requirements,
3083-
cpu: cpu.as_deref(),
3084-
memory: memory.as_deref(),
3097+
resource_requirements,
30853098
driver_config_json: driver_config_json.as_deref(),
30863099
editor,
30873100
providers: &providers,

crates/openshell-cli/src/run.rs

Lines changed: 71 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,27 @@ use openshell_core::net::set_tcp_nodelay_best_effort;
3535
use openshell_core::proto::ProviderProfileCategory;
3636
use openshell_core::proto::{
3737
ApproveAllDraftChunksRequest, ApproveDraftChunkRequest, AttachSandboxProviderRequest,
38-
ClearDraftChunksRequest, ConfigureProviderRefreshRequest, CreateProviderRequest,
39-
CreateSandboxRequest, CreateSshSessionRequest, DeleteInferenceRouteRequest,
40-
DeleteProviderProfileRequest, DeleteProviderRefreshRequest, DeleteProviderRequest,
41-
DeleteSandboxRequest, DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest,
42-
ExposeServiceRequest, GetCurrentUserRequest, GetDraftHistoryRequest, GetDraftPolicyRequest,
43-
GetGatewayConfigRequest, GetInferenceRouteRequest, GetProviderProfileRequest,
44-
GetProviderRefreshStatusRequest, GetProviderRequest, GetSandboxConfigRequest,
45-
GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxPolicyStatusRequest,
46-
GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, ImportProviderProfilesRequest,
47-
LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest,
48-
ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest,
49-
ListServicesRequest, PolicySource, PolicyStatus, Provider,
50-
ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStatus,
51-
ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfile,
52-
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
53-
ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox,
54-
SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate, ServiceEndpointResponse,
55-
SetInferenceRouteRequest, SettingScope, StartSandboxRequest, StopSandboxRequest,
56-
TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest,
57-
UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event,
58-
setting_value, tcp_forward_init,
38+
ClearDraftChunksRequest, ConfigureProviderRefreshRequest, CpuResourceRequirements,
39+
CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest,
40+
DeleteInferenceRouteRequest, DeleteProviderProfileRequest, DeleteProviderRefreshRequest,
41+
DeleteProviderRequest, DeleteSandboxRequest, DeleteServiceRequest,
42+
DetachSandboxProviderRequest, ExecSandboxRequest, ExposeServiceRequest, GetCurrentUserRequest,
43+
GetDraftHistoryRequest, GetDraftPolicyRequest, GetGatewayConfigRequest,
44+
GetInferenceRouteRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest,
45+
GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest,
46+
GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest,
47+
ImportProviderProfilesRequest, LintProviderProfilesRequest, ListProviderProfilesRequest,
48+
ListProvidersRequest, ListSandboxPoliciesRequest, ListSandboxProvidersRequest,
49+
ListSandboxesRequest, ListServicesRequest, MemoryResourceRequirements, PolicySource,
50+
PolicyStatus, Provider, ProviderCredentialRefreshRecoveryAction,
51+
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy,
52+
ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileDiagnostic,
53+
ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements,
54+
RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy,
55+
SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetInferenceRouteRequest, SettingScope,
56+
StartSandboxRequest, StopSandboxRequest, TcpForwardFrame, TcpForwardInit, TcpRelayTarget,
57+
UpdateConfigRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest,
58+
exec_sandbox_event, setting_value, tcp_forward_init,
5959
};
6060
use openshell_core::settings;
6161
use openshell_core::{ObjectId, ObjectName, ObjectWorkspace};
@@ -224,41 +224,28 @@ fn sandbox_should_persist(keep: bool, forward: Option<&ForwardSpec>) -> bool {
224224
keep || forward.is_some()
225225
}
226226

227-
fn build_sandbox_resource_limits(
227+
pub fn build_cpu_resource_requirements(
228228
cpu: Option<&str>,
229-
memory: Option<&str>,
230-
) -> Result<Option<prost_types::Struct>> {
231-
use prost_types::{Struct, Value, value::Kind};
232-
233-
fn string_value(value: String) -> Value {
234-
Value {
235-
kind: Some(Kind::StringValue(value)),
236-
}
237-
}
229+
) -> Result<Option<CpuResourceRequirements>> {
230+
let Some(cpu) = cpu else {
231+
return Ok(None);
232+
};
238233

239-
let mut limits = std::collections::BTreeMap::new();
240-
if let Some(cpu) = cpu {
241-
limits.insert("cpu".to_string(), string_value(validate_cpu_quantity(cpu)?));
242-
}
243-
if let Some(memory) = memory {
244-
limits.insert(
245-
"memory".to_string(),
246-
string_value(validate_memory_quantity(memory)?),
247-
);
248-
}
234+
Ok(Some(CpuResourceRequirements {
235+
limit: validate_cpu_quantity(cpu)?,
236+
}))
237+
}
249238

250-
if limits.is_empty() {
239+
pub fn build_memory_resource_requirements(
240+
memory: Option<&str>,
241+
) -> Result<Option<MemoryResourceRequirements>> {
242+
let Some(memory) = memory else {
251243
return Ok(None);
252-
}
244+
};
253245

254-
let mut fields = std::collections::BTreeMap::new();
255-
fields.insert(
256-
"limits".to_string(),
257-
Value {
258-
kind: Some(Kind::StructValue(Struct { fields: limits })),
259-
},
260-
);
261-
Ok(Some(Struct { fields }))
246+
Ok(Some(MemoryResourceRequirements {
247+
limit: validate_memory_quantity(memory)?,
248+
}))
262249
}
263250

264251
fn parse_driver_config_json(value: &str) -> Result<prost_types::Struct> {
@@ -370,9 +357,7 @@ pub struct SandboxCreateConfig<'a> {
370357
pub from: Option<&'a str>,
371358
pub uploads: &'a [(String, Option<String>, bool)],
372359
pub keep: bool,
373-
pub gpu_requirements: Option<GpuResourceRequirements>,
374-
pub cpu: Option<&'a str>,
375-
pub memory: Option<&'a str>,
360+
pub resource_requirements: Option<ResourceRequirements>,
376361
pub driver_config_json: Option<&'a str>,
377362
pub editor: Option<Editor>,
378363
pub providers: &'a [String],
@@ -395,9 +380,7 @@ impl Default for SandboxCreateConfig<'_> {
395380
from: None,
396381
uploads: &[],
397382
keep: false,
398-
gpu_requirements: None,
399-
cpu: None,
400-
memory: None,
383+
resource_requirements: None,
401384
driver_config_json: None,
402385
editor: None,
403386
providers: &[],
@@ -428,9 +411,7 @@ pub async fn sandbox_create(
428411
from,
429412
uploads,
430413
keep,
431-
gpu_requirements,
432-
cpu,
433-
memory,
414+
resource_requirements,
434415
driver_config_json,
435416
editor,
436417
providers,
@@ -513,24 +494,20 @@ pub async fn sandbox_create(
513494
.await?;
514495

515496
let policy = load_sandbox_policy(policy)?;
516-
let resource_limits = build_sandbox_resource_limits(cpu, memory)?;
517497
let driver_config = driver_config_json
518498
.map(parse_driver_config_json)
519499
.transpose()?;
520500

521-
let template = if image.is_some() || resource_limits.is_some() || driver_config.is_some() {
501+
let template = if image.is_some() || driver_config.is_some() {
522502
Some(SandboxTemplate {
523503
image: image.unwrap_or_default(),
524-
resources: resource_limits,
525504
driver_config,
526505
..SandboxTemplate::default()
527506
})
528507
} else {
529508
None
530509
};
531510

532-
let resource_requirements = gpu_requirements.map(|gpu| ResourceRequirements { gpu: Some(gpu) });
533-
534511
let main_terminal = tty_override
535512
.unwrap_or_else(|| std::io::stdin().is_terminal() && std::io::stdout().is_terminal());
536513
let main_command = if command.is_empty() {
@@ -540,7 +517,7 @@ pub async fn sandbox_create(
540517
};
541518
let request = CreateSandboxRequest {
542519
spec: Some(SandboxSpec {
543-
resource_requirements,
520+
resource_requirements: resource_requirements.clone(),
544521
environment,
545522
policy,
546523
providers: configured_providers,
@@ -7404,9 +7381,9 @@ fn format_endpoint(endpoint: &openshell_core::proto::NetworkEndpoint) -> String
74047381
#[cfg(test)]
74057382
mod tests {
74067383
use super::{
7407-
PolicyGetView, ProvisioningStep, build_sandbox_resource_limits,
7408-
dockerfile_sources_supported_for_gateway, format_endpoint,
7409-
format_provider_attachment_table, git_sync_files, inferred_provider_type,
7384+
PolicyGetView, ProvisioningStep, build_cpu_resource_requirements,
7385+
build_memory_resource_requirements, dockerfile_sources_supported_for_gateway,
7386+
format_endpoint, format_provider_attachment_table, git_sync_files, inferred_provider_type,
74107387
parse_cli_setting_value, parse_credential_expiry_cli_value, parse_credential_expiry_pairs,
74117388
parse_credential_pairs, parse_driver_config_json, parse_secret_material_env_pairs,
74127389
policy_revision_to_json, provider_profile_allows_empty_credentials,
@@ -7812,52 +7789,29 @@ mod tests {
78127789
}
78137790

78147791
#[test]
7815-
fn build_sandbox_resource_limits_sets_limits_only() {
7816-
let resources = build_sandbox_resource_limits(Some("500m"), Some("2Gi"))
7817-
.expect("resource limits should parse")
7818-
.expect("resource limits should be present");
7792+
fn build_cpu_resource_requirements_sets_typed_limit() {
7793+
let cpu = build_cpu_resource_requirements(Some("500m"))
7794+
.expect("CPU limit should parse")
7795+
.expect("CPU requirements should be present");
78197796

7820-
let limits = resources
7821-
.fields
7822-
.get("limits")
7823-
.and_then(|value| value.kind.as_ref())
7824-
.and_then(|kind| match kind {
7825-
prost_types::value::Kind::StructValue(inner) => Some(inner),
7826-
_ => None,
7827-
})
7828-
.expect("limits should be a struct");
7797+
assert_eq!(cpu.limit, "500m");
7798+
}
78297799

7830-
assert_eq!(
7831-
limits
7832-
.fields
7833-
.get("cpu")
7834-
.and_then(|value| value.kind.as_ref())
7835-
.and_then(|kind| match kind {
7836-
prost_types::value::Kind::StringValue(value) => Some(value.as_str()),
7837-
_ => None,
7838-
}),
7839-
Some("500m")
7840-
);
7841-
assert_eq!(
7842-
limits
7843-
.fields
7844-
.get("memory")
7845-
.and_then(|value| value.kind.as_ref())
7846-
.and_then(|kind| match kind {
7847-
prost_types::value::Kind::StringValue(value) => Some(value.as_str()),
7848-
_ => None,
7849-
}),
7850-
Some("2Gi")
7851-
);
7852-
assert!(!resources.fields.contains_key("requests"));
7800+
#[test]
7801+
fn build_memory_resource_requirements_sets_typed_limit() {
7802+
let memory = build_memory_resource_requirements(Some("2Gi"))
7803+
.expect("memory limit should parse")
7804+
.expect("memory requirements should be present");
7805+
7806+
assert_eq!(memory.limit, "2Gi");
78537807
}
78547808

78557809
#[test]
7856-
fn build_sandbox_resource_limits_rejects_invalid_quantities() {
7857-
assert!(build_sandbox_resource_limits(Some("0"), None).is_err());
7858-
assert!(build_sandbox_resource_limits(Some("half"), None).is_err());
7859-
assert!(build_sandbox_resource_limits(None, Some("0Gi")).is_err());
7860-
assert!(build_sandbox_resource_limits(None, Some("1.5Gi")).is_err());
7810+
fn build_cpu_and_memory_resource_requirements_reject_invalid_quantities() {
7811+
assert!(build_cpu_resource_requirements(Some("0")).is_err());
7812+
assert!(build_cpu_resource_requirements(Some("half")).is_err());
7813+
assert!(build_memory_resource_requirements(Some("0Gi")).is_err());
7814+
assert!(build_memory_resource_requirements(Some("1.5Gi")).is_err());
78617815
}
78627816

78637817
#[test]
@@ -8157,6 +8111,8 @@ mod tests {
81578111
fn provisioning_timeout_message_includes_condition_and_gpu_hint() {
81588112
let resource_requirements = ResourceRequirements {
81598113
gpu: Some(GpuResourceRequirements { count: None }),
8114+
cpu: None,
8115+
memory: None,
81608116
};
81618117
let message = provisioning_timeout_message(
81628118
120,
@@ -8178,7 +8134,11 @@ mod tests {
81788134

81798135
#[test]
81808136
fn provisioning_timeout_message_omits_gpu_hint_without_gpu_requirements() {
8181-
let resource_requirements = ResourceRequirements { gpu: None };
8137+
let resource_requirements = ResourceRequirements {
8138+
gpu: None,
8139+
cpu: None,
8140+
memory: None,
8141+
};
81828142
let message = provisioning_timeout_message(120, Some(&resource_requirements), None);
81838143

81848144
assert_eq!(message, "sandbox provisioning timed out after 120s");

0 commit comments

Comments
 (0)