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
4 changes: 4 additions & 0 deletions architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ The capability RPC reports driver identity, version, and the default sandbox
image used by the gateway. GPU availability stays driver-local and is validated
when a sandbox create request asks for GPU resources.

The gateway records driver identity and version from the startup capability
response. Elevated gateway info reports that initialized driver snapshot instead
of re-querying drivers on each request.

## Runtime Summary

| Runtime | Best fit | Sandbox boundary | Notes |
Expand Down
34 changes: 34 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,14 @@ enum Commands {
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
Status,

/// Show elevated live gateway runtime information.
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
Info {
/// Output format.
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
output: OutputFormat,
},

/// Manage inference configuration.
#[command(after_help = INFERENCE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)]
Inference {
Expand Down Expand Up @@ -2100,6 +2108,19 @@ async fn main() -> Result<()> {
}
}

// -----------------------------------------------------------
// Top-level info
// -----------------------------------------------------------
Some(Commands::Info { output }) => {
if let Ok(ctx) = resolve_gateway(&cli.gateway, &cli.gateway_endpoint) {
let mut tls = tls.with_gateway_name(&ctx.name);
apply_auth(&mut tls, &ctx.name);
run::gateway_info(&ctx.name, &ctx.endpoint, &tls, output.as_str()).await?;
} else {
run::gateway_info_not_configured()?;
}
}

// -----------------------------------------------------------
// Top-level forward (was `sandbox forward`)
// -----------------------------------------------------------
Expand Down Expand Up @@ -3466,6 +3487,19 @@ mod tests {
assert!(matches!(cli.command, Some(Commands::Status)));
}

#[test]
fn info_accepts_output_json() {
let cli = Cli::try_parse_from(["openshell", "info", "-o", "json"])
.expect("info -o json should parse");

assert!(matches!(
cli.command,
Some(Commands::Info {
output: OutputFormat::Json
})
));
}

#[test]
fn hidden_aliases_still_parse() {
let cli = Cli::try_parse_from(["openshell", "lg", "sandbox-1"])
Expand Down
236 changes: 218 additions & 18 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,21 @@ use openshell_core::proto::{
DeleteProviderRefreshRequest, DeleteProviderRequest, DeleteSandboxRequest,
DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest, ExposeServiceRequest,
GetClusterInferenceRequest, GetDraftHistoryRequest, GetDraftPolicyRequest,
GetGatewayConfigRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest,
GetProviderRequest, GetSandboxConfigRequest, GetSandboxLogsRequest,
GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, GpuResourceRequirements,
HealthRequest, ImportProviderProfilesRequest, LintProviderProfilesRequest,
ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest,
ListSandboxProvidersRequest, ListSandboxesRequest, ListServicesRequest, PlatformEvent,
PolicySource, PolicyStatus, Provider, ProviderCredentialRefreshStatus,
ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileDiagnostic,
ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements,
RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy,
SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetClusterInferenceRequest,
SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit, TcpRelayTarget,
UpdateConfigRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest,
exec_sandbox_event, setting_value, tcp_forward_init,
GetGatewayConfigRequest, GetGatewayInfoRequest, GetProviderProfileRequest,
GetProviderRefreshStatusRequest, GetProviderRequest, GetSandboxConfigRequest,
GetSandboxLogsRequest, GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest,
GpuResourceRequirements, HealthRequest, ImportProviderProfilesRequest,
LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest,
ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest,
ListServicesRequest, PlatformEvent, PolicySource, PolicyStatus, Provider,
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderProfile,
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox,
SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate, ServiceEndpointResponse,
ServiceStatus, SetClusterInferenceRequest, SettingScope, SettingValue, TcpForwardFrame,
TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, UpdateProviderProfilesRequest,
UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event, setting_value,
tcp_forward_init,
};
use openshell_core::settings::{self, SettingValueKind};
use openshell_core::{ObjectId, ObjectName};
Expand Down Expand Up @@ -525,6 +526,28 @@ fn print_sandbox_header(sandbox: &Sandbox, display: Option<&ProvisioningDisplay>
}
}

#[derive(Debug, Clone)]
struct GatewayInfoView {
gateway: String,
server: String,
auth: Option<&'static str>,
status: String,
version: String,
compute_drivers: Vec<ComputeDriverInfoView>,
}

#[derive(Debug, Clone)]
struct ComputeDriverInfoView {
name: String,
capabilities: ComputeDriverCapabilitiesView,
}

#[derive(Debug, Clone)]
struct ComputeDriverCapabilitiesView {
driver_name: String,
driver_version: String,
}

/// Show gateway status.
#[allow(clippy::branches_sharing_code)]
pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) -> Result<()> {
Expand Down Expand Up @@ -582,6 +605,129 @@ pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions)
Ok(())
}

fn gateway_service_status_name(status: i32) -> &'static str {
match ServiceStatus::try_from(status) {
Ok(ServiceStatus::Healthy) => "healthy",
Ok(ServiceStatus::Degraded) => "degraded",
Ok(ServiceStatus::Unhealthy) => "unhealthy",
Ok(ServiceStatus::Unspecified) | Err(_) => "unknown",
}
}

/// Show elevated gateway runtime information.
pub async fn gateway_info(
gateway_name: &str,
server: &str,
tls: &TlsOptions,
output: &str,
) -> Result<()> {
let mut client = grpc_client(server, tls).await?;
let info = client
.get_gateway_info(GetGatewayInfoRequest {})
.await
.map_err(|err| match err.code() {
Code::Unimplemented => {
miette!("gateway info is not supported by this gateway version")
}
Code::PermissionDenied => miette!("gateway info requires admin privileges: {err}"),
_ => miette!("get_gateway_info failed: {err}"),
})?
.into_inner();

let view = GatewayInfoView {
gateway: gateway_name.to_string(),
server: server.to_string(),
auth: tls.is_bearer_auth().then_some("bearer"),
status: gateway_service_status_name(info.status).to_string(),
version: info.gateway_version,
compute_drivers: info
.compute_drivers
.into_iter()
.map(|driver| {
let capabilities = driver.capabilities.unwrap_or_default();
ComputeDriverInfoView {
name: driver.name,
capabilities: ComputeDriverCapabilitiesView {
driver_name: capabilities.driver_name,
driver_version: capabilities.driver_version,
},
}
})
.collect(),
};

print_gateway_info(&view, output)
}

pub fn gateway_info_not_configured() -> Result<()> {
Err(miette!(
"No gateway configured.\nRegister a gateway with: openshell gateway add <endpoint>"
))
}

fn print_gateway_info(view: &GatewayInfoView, output: &str) -> Result<()> {
if crate::output::print_output_single(output, view, gateway_info_to_json)? {
return Ok(());
}

println!("{}", "Gateway Info".cyan().bold());
println!();
println!(" {} {}", "Gateway:".dimmed(), view.gateway);
println!(" {} {}", "Server:".dimmed(), view.server);
if view.auth.is_some() {
println!(" {} Edge (bearer token)", "Auth:".dimmed());
}
println!(" {} {}", "Status:".dimmed(), view.status);
println!(" {} {}", "Version:".dimmed(), view.version);
print_compute_driver_info(&view.compute_drivers);

Ok(())
}

fn print_compute_driver_info(drivers: &[ComputeDriverInfoView]) {
if drivers.is_empty() {
return;
}

println!(" {}", "Compute drivers:".dimmed());
for driver in drivers {
println!(" {}", driver.name);
if driver.capabilities.driver_name != driver.name {
println!(
" {} {}",
"Driver name:".dimmed(),
driver.capabilities.driver_name
);
}
println!(
" {} {}",
"Driver version:".dimmed(),
driver.capabilities.driver_version
);
}
}

fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value {
serde_json::json!({
"gateway": &view.gateway,
"server": &view.server,
"auth": view.auth,
"status": &view.status,
"version": &view.version,
"compute_drivers": view
.compute_drivers
.iter()
.map(|driver| serde_json::json!({
"name": &driver.name,
"capabilities": {
"driver_name": &driver.capabilities.driver_name,
"driver_version": &driver.capabilities.driver_version,
},
}))
.collect::<Vec<_>>(),
})
}

/// Set the active gateway.
pub fn gateway_use(name: &str) -> Result<()> {
// Verify the gateway exists
Expand Down Expand Up @@ -7838,10 +7984,11 @@ fn format_timestamp_ms(ms: i64) -> String {
#[cfg(test)]
mod tests {
use super::{
ProvisioningStep, TlsOptions, build_sandbox_resource_limits,
dockerfile_sources_supported_for_gateway, format_endpoint, format_gateway_select_header,
format_gateway_select_items, format_provider_attachment_table, gateway_add,
gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_to_json,
ComputeDriverCapabilitiesView, ComputeDriverInfoView, GatewayInfoView, ProvisioningStep,
TlsOptions, build_sandbox_resource_limits, dockerfile_sources_supported_for_gateway,
format_endpoint, format_gateway_select_header, format_gateway_select_items,
format_provider_attachment_table, gateway_add, gateway_auth_label,
gateway_env_override_warning, gateway_info_to_json, gateway_select_with, gateway_to_json,
gateway_type_label, git_sync_files, http_health_check, import_local_package_mtls_bundle,
inferred_provider_type, mtls_certs_exist_for_gateway, package_managed_tls_dirs,
parse_cli_setting_value, parse_credential_expiry_cli_value, parse_credential_expiry_pairs,
Expand Down Expand Up @@ -8889,6 +9036,59 @@ mod tests {
assert_eq!(json["active"], true);
}

#[test]
fn gateway_info_json_includes_compute_drivers_when_available() {
let view = GatewayInfoView {
gateway: "openshell".to_string(),
server: "https://127.0.0.1:17670".to_string(),
auth: Some("bearer"),
status: "healthy".to_string(),
version: "0.0.75".to_string(),
compute_drivers: vec![ComputeDriverInfoView {
name: "podman".to_string(),
capabilities: ComputeDriverCapabilitiesView {
driver_name: "podman".to_string(),
driver_version: "0.0.75".to_string(),
},
}],
};

let json = gateway_info_to_json(&view);

assert_eq!(json["gateway"], "openshell");
assert_eq!(json["status"], "healthy");
assert_eq!(json["version"], "0.0.75");
assert_eq!(json["compute_drivers"][0]["name"], "podman");
assert_eq!(
json["compute_drivers"][0]["capabilities"]["driver_name"],
"podman"
);
assert_eq!(
json["compute_drivers"][0]["capabilities"]["driver_version"],
"0.0.75"
);
}

#[test]
fn gateway_info_json_includes_empty_compute_driver_list() {
let view = GatewayInfoView {
gateway: "openshell".to_string(),
server: "https://127.0.0.1:17670".to_string(),
auth: None,
status: "healthy".to_string(),
version: "0.0.74".to_string(),
compute_drivers: Vec::new(),
};

let json = gateway_info_to_json(&view);

assert!(
json["compute_drivers"]
.as_array()
.is_some_and(Vec::is_empty)
);
}

#[test]
fn gateway_auth_label_defaults_https_gateways_to_mtls() {
let gateway = GatewayMetadata {
Expand Down
7 changes: 7 additions & 0 deletions crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ impl OpenShell for TestOpenShell {
}))
}

async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}

async fn create_sandbox(
&self,
_request: tonic::Request<CreateSandboxRequest>,
Expand Down
7 changes: 7 additions & 0 deletions crates/openshell-cli/tests/mtls_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ impl OpenShell for TestOpenShell {
}))
}

async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}

async fn create_sandbox(
&self,
_request: tonic::Request<openshell_core::proto::CreateSandboxRequest>,
Expand Down
7 changes: 7 additions & 0 deletions crates/openshell-cli/tests/provider_commands_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ impl OpenShell for TestOpenShell {
}))
}

async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}

async fn create_sandbox(
&self,
_request: tonic::Request<CreateSandboxRequest>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ impl OpenShell for TestOpenShell {
}))
}

async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}

async fn create_sandbox(
&self,
request: tonic::Request<CreateSandboxRequest>,
Expand Down
Loading
Loading