mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-22 06:19:47 +08:00
feat(gateway): add elevated gateway info (#2202)
* feat(cli)!: fold gateway metadata into list BREAKING CHANGE: openshell gateway info no longer shows local gateway registration metadata. Use openshell gateway list or openshell gateway list -o json for local registration details. Signed-off-by: Evan Lezar <elezar@nvidia.com> * feat(gateway): add elevated gateway info Signed-off-by: Evan Lezar <elezar@nvidia.com> --------- Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -240,7 +240,7 @@ const HELP_TEMPLATE: &str = "\
|
||||
provider: Manage provider configuration
|
||||
|
||||
\x1b[1mGATEWAY COMMANDS\x1b[0m
|
||||
gateway: Manage gateway registrations
|
||||
gateway: Manage gateways
|
||||
status: Show gateway status and information
|
||||
inference: Manage inference configuration
|
||||
doctor: Diagnose gateway issues
|
||||
@@ -524,7 +524,7 @@ enum Commands {
|
||||
// ===================================================================
|
||||
// GATEWAY COMMANDS
|
||||
// ===================================================================
|
||||
/// Manage gateway registrations.
|
||||
/// Manage gateways.
|
||||
#[command(alias = "gw", after_help = GATEWAY_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)]
|
||||
Gateway {
|
||||
#[command(subcommand)]
|
||||
@@ -1105,18 +1105,19 @@ enum GatewayCommands {
|
||||
name: Option<String>,
|
||||
},
|
||||
|
||||
/// Show gateway registration details.
|
||||
/// Show elevated live gateway runtime information.
|
||||
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
|
||||
Info {
|
||||
/// Gateway name (defaults to active gateway).
|
||||
#[arg(long, env = "OPENSHELL_GATEWAY", add = ArgValueCompleter::new(completers::complete_gateway_names))]
|
||||
name: Option<String>,
|
||||
/// Output format.
|
||||
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
|
||||
output: OutputFormat,
|
||||
},
|
||||
|
||||
/// List registered gateways.
|
||||
///
|
||||
/// Prints a table of all registered gateways with their endpoint, type,
|
||||
/// and authentication mode. The active gateway is marked with `*`.
|
||||
/// Prints all registered gateways with their endpoint, type,
|
||||
/// authentication mode, source, and remote registration details.
|
||||
/// The active gateway is marked with `*`.
|
||||
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
|
||||
List {
|
||||
/// Output format.
|
||||
@@ -2056,11 +2057,14 @@ async fn main() -> Result<()> {
|
||||
GatewayCommands::Select { name } => {
|
||||
run::gateway_select(name.as_deref(), &cli.gateway)?;
|
||||
}
|
||||
GatewayCommands::Info { name } => {
|
||||
let name = name
|
||||
.or_else(|| resolve_gateway_name(&cli.gateway))
|
||||
.unwrap_or_else(|| "openshell".to_string());
|
||||
run::gateway_admin_info(&name)?;
|
||||
GatewayCommands::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()?;
|
||||
}
|
||||
}
|
||||
GatewayCommands::List { output } => {
|
||||
run::gateway_list(&cli.gateway, output.as_str())?;
|
||||
@@ -3445,7 +3449,7 @@ mod tests {
|
||||
for (raw_args, index) in [
|
||||
(vec!["openshell", "--gateway", "a"], 2),
|
||||
(vec!["openshell", "gateway", "select", "a"], 3),
|
||||
(vec!["openshell", "gateway", "info", "--name", "a"], 4),
|
||||
(vec!["openshell", "gateway", "remove", "a"], 3),
|
||||
] {
|
||||
let mut cmd = Cli::command();
|
||||
let args: Vec<OsString> = raw_args.iter().copied().map(Into::into).collect();
|
||||
@@ -3473,6 +3477,26 @@ mod tests {
|
||||
assert!(matches!(cli.command, Some(Commands::Status)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_info_accepts_output_json() {
|
||||
let cli = Cli::try_parse_from(["openshell", "gateway", "info", "-o", "json"])
|
||||
.expect("gateway info -o json should parse");
|
||||
|
||||
assert!(matches!(
|
||||
cli.command,
|
||||
Some(Commands::Gateway {
|
||||
command: Some(GatewayCommands::Info {
|
||||
output: OutputFormat::Json
|
||||
})
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_info_is_not_a_command() {
|
||||
assert!(Cli::try_parse_from(["openshell", "info"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_aliases_still_parse() {
|
||||
let cli = Cli::try_parse_from(["openshell", "lg", "sandbox-1"])
|
||||
|
||||
+320
-65
@@ -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};
|
||||
@@ -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<()> {
|
||||
@@ -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
|
||||
@@ -1356,31 +1502,73 @@ pub fn gateway_list(gateway_flag: &Option<String>, output: &str) -> Result<()> {
|
||||
.max()
|
||||
.unwrap_or(6)
|
||||
.max(6);
|
||||
let auth_width = gateways
|
||||
.iter()
|
||||
.map(|g| gateway_auth_label(&g.metadata).len())
|
||||
.max()
|
||||
.unwrap_or(4)
|
||||
.max(4);
|
||||
let remote_labels: Vec<Option<String>> = gateways
|
||||
.iter()
|
||||
.map(|g| gateway_remote_label(&g.metadata))
|
||||
.collect();
|
||||
let show_remote = remote_labels.iter().any(Option::is_some);
|
||||
let remote_width = remote_labels
|
||||
.iter()
|
||||
.filter_map(|label| label.as_ref().map(String::len))
|
||||
.max()
|
||||
.unwrap_or(6)
|
||||
.max(6);
|
||||
|
||||
// Print header
|
||||
println!(
|
||||
" {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {}",
|
||||
"NAME".bold(),
|
||||
"ENDPOINT".bold(),
|
||||
"TYPE".bold(),
|
||||
"SOURCE".bold(),
|
||||
"AUTH".bold(),
|
||||
);
|
||||
if show_remote {
|
||||
println!(
|
||||
" {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {:<auth_width$} {:<remote_width$}",
|
||||
"NAME".bold(),
|
||||
"ENDPOINT".bold(),
|
||||
"TYPE".bold(),
|
||||
"SOURCE".bold(),
|
||||
"AUTH".bold(),
|
||||
"REMOTE".bold(),
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {}",
|
||||
"NAME".bold(),
|
||||
"ENDPOINT".bold(),
|
||||
"TYPE".bold(),
|
||||
"SOURCE".bold(),
|
||||
"AUTH".bold(),
|
||||
);
|
||||
}
|
||||
|
||||
// Print rows
|
||||
for gateway in gateways {
|
||||
for (gateway, remote_label) in gateways.iter().zip(remote_labels.iter()) {
|
||||
let metadata = &gateway.metadata;
|
||||
let is_active = active.as_deref() == Some(&metadata.name);
|
||||
let marker = if is_active { "*" } else { " " };
|
||||
let gw_type = gateway_type_label(metadata);
|
||||
let gw_auth = gateway_auth_label(metadata);
|
||||
let line = format!(
|
||||
"{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {gw_auth}",
|
||||
metadata.name,
|
||||
metadata.gateway_endpoint,
|
||||
gw_type,
|
||||
gateway.source.label(),
|
||||
);
|
||||
let line = if show_remote {
|
||||
let remote_label = remote_label.as_deref().unwrap_or("-");
|
||||
format!(
|
||||
"{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {:<auth_width$} {:<remote_width$}",
|
||||
metadata.name,
|
||||
metadata.gateway_endpoint,
|
||||
gw_type,
|
||||
gateway.source.label(),
|
||||
gw_auth,
|
||||
remote_label,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {gw_auth}",
|
||||
metadata.name,
|
||||
metadata.gateway_endpoint,
|
||||
gw_type,
|
||||
gateway.source.label(),
|
||||
)
|
||||
};
|
||||
if is_active {
|
||||
println!("{}", line.green());
|
||||
} else {
|
||||
@@ -1400,9 +1588,23 @@ fn gateway_to_json(gateway: &ListedGateway, active: &Option<String>) -> serde_js
|
||||
"source": gateway.source.label(),
|
||||
"auth": gateway_auth_label(metadata),
|
||||
"active": active.as_deref() == Some(&metadata.name),
|
||||
"is_remote": metadata.is_remote,
|
||||
"remote_host": &metadata.remote_host,
|
||||
"resolved_host": &metadata.resolved_host,
|
||||
})
|
||||
}
|
||||
|
||||
fn gateway_remote_label(gateway: &GatewayMetadata) -> Option<String> {
|
||||
match (&gateway.remote_host, &gateway.resolved_host) {
|
||||
(Some(remote), Some(resolved)) if remote != resolved => {
|
||||
Some(format!("{remote} -> {resolved}"))
|
||||
}
|
||||
(Some(remote), _) => Some(remote.clone()),
|
||||
(None, Some(resolved)) => Some(resolved.clone()),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn http_health_check(server: &str, tls: &TlsOptions) -> Result<Option<StatusCode>> {
|
||||
let base = server.trim_end_matches('/');
|
||||
let uri: hyper::Uri = format!("{base}/healthz").parse().into_diagnostic()?;
|
||||
@@ -1522,38 +1724,6 @@ pub fn gateway_remove(name: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show gateway registration details.
|
||||
pub fn gateway_admin_info(name: &str) -> Result<()> {
|
||||
let metadata = get_gateway_metadata(name).ok_or_else(|| {
|
||||
miette::miette!(
|
||||
"No gateway metadata found for '{name}'.\n\
|
||||
Register it first: openshell gateway add <endpoint> --name {name}"
|
||||
)
|
||||
})?;
|
||||
|
||||
println!("{}", "Gateway Info".cyan().bold());
|
||||
println!();
|
||||
println!(" {} {}", "Gateway:".dimmed(), metadata.name);
|
||||
println!(
|
||||
" {} {}",
|
||||
"Gateway endpoint:".dimmed(),
|
||||
metadata.gateway_endpoint
|
||||
);
|
||||
|
||||
if metadata.is_remote {
|
||||
if let Some(ref host) = metadata.remote_host {
|
||||
println!(" {} {host}", "Remote host:".dimmed());
|
||||
} else {
|
||||
println!(" {} External registration", "Type:".dimmed());
|
||||
}
|
||||
if let Some(ref resolved) = metadata.resolved_host {
|
||||
println!(" {} {resolved}", "Resolved host:".dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate system prerequisites for running a gateway.
|
||||
///
|
||||
/// Checks Docker connectivity and reports the result. Returns exit code 0
|
||||
@@ -7887,11 +8057,13 @@ fn format_timestamp_ms(ms: i64) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
PolicyGetView, ProvisioningStep, TlsOptions, build_sandbox_resource_limits,
|
||||
ComputeDriverCapabilitiesView, ComputeDriverInfoView, GatewayInfoView, PolicyGetView,
|
||||
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,
|
||||
gateway_type_label, git_sync_files, http_health_check, import_local_package_mtls_bundle,
|
||||
gateway_auth_label, gateway_env_override_warning, gateway_info_to_json,
|
||||
gateway_remote_label, 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,
|
||||
parse_credential_pairs, parse_driver_config_json, parse_secret_material_env_pairs,
|
||||
@@ -9035,6 +9207,89 @@ mod tests {
|
||||
assert_eq!(json["active"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_to_json_includes_remote_registration_details() {
|
||||
let gateway = ListedGateway {
|
||||
metadata: GatewayMetadata {
|
||||
name: "remote-vm".to_string(),
|
||||
gateway_endpoint: "https://127.0.0.1:17670".to_string(),
|
||||
is_remote: true,
|
||||
remote_host: Some("user@gateway-alias".to_string()),
|
||||
resolved_host: Some("10.0.0.5".to_string()),
|
||||
auth_mode: Some("mtls".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
source: GatewayMetadataSource::User,
|
||||
};
|
||||
|
||||
let json = gateway_to_json(&gateway, &Some("local-vm".to_string()));
|
||||
|
||||
assert_eq!(json["source"], "user");
|
||||
assert_eq!(json["type"], "remote");
|
||||
assert_eq!(json["auth"], "mtls");
|
||||
assert_eq!(json["active"], false);
|
||||
assert_eq!(json["is_remote"], true);
|
||||
assert_eq!(json["remote_host"], "user@gateway-alias");
|
||||
assert_eq!(json["resolved_host"], "10.0.0.5");
|
||||
assert_eq!(
|
||||
gateway_remote_label(&gateway.metadata).as_deref(),
|
||||
Some("user@gateway-alias -> 10.0.0.5")
|
||||
);
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
||||
@@ -86,6 +86,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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -108,6 +108,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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -58,6 +58,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>,
|
||||
|
||||
@@ -87,6 +87,17 @@ impl OpenShell for TestOpenShell {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_gateway_info(
|
||||
&self,
|
||||
_: tonic::Request<proto::GetGatewayInfoRequest>,
|
||||
) -> Result<Response<proto::GetGatewayInfoResponse>, Status> {
|
||||
Ok(Response::new(proto::GetGatewayInfoResponse {
|
||||
status: proto::ServiceStatus::Healthy.into(),
|
||||
gateway_version: "test-1.2.3".to_string(),
|
||||
compute_drivers: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn update_provider_profiles(
|
||||
&self,
|
||||
_: tonic::Request<proto::UpdateProviderProfilesRequest>,
|
||||
|
||||
@@ -60,6 +60,16 @@ type SharedComputeDriver =
|
||||
|
||||
const DELETE_PHASE_CAS_RETRY_LIMIT: usize = 3;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ComputeDriverInfoSnapshot {
|
||||
/// Gateway-selected driver name used for routing and `driver_config` keys.
|
||||
pub name: String,
|
||||
/// Driver-reported human-readable name from the startup capability snapshot.
|
||||
pub driver_name: String,
|
||||
/// Driver-reported implementation version from the startup capability snapshot.
|
||||
pub driver_version: String,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
trait ShutdownCleanup: Send + Sync {
|
||||
async fn cleanup_on_shutdown(&self) -> Result<(), String>;
|
||||
@@ -259,7 +269,7 @@ impl ComputeDriver for RemoteComputeDriver {
|
||||
#[derive(Clone)]
|
||||
pub struct ComputeRuntime {
|
||||
driver: SharedComputeDriver,
|
||||
driver_name: String,
|
||||
driver_info: ComputeDriverInfoSnapshot,
|
||||
shutdown_cleanup: Option<Arc<dyn ShutdownCleanup>>,
|
||||
startup_resume: Option<Arc<dyn StartupResume>>,
|
||||
_driver_process: Option<Arc<ManagedDriverProcess>>,
|
||||
@@ -307,10 +317,15 @@ impl ComputeRuntime {
|
||||
in_tree = driver_kind.is_some(),
|
||||
"Compute driver connected"
|
||||
);
|
||||
let driver_info = ComputeDriverInfoSnapshot {
|
||||
name: driver_name,
|
||||
driver_name: capabilities.driver_name,
|
||||
driver_version: capabilities.driver_version,
|
||||
};
|
||||
let default_image = capabilities.default_image;
|
||||
Ok(Self {
|
||||
driver,
|
||||
driver_name,
|
||||
driver_info,
|
||||
shutdown_cleanup,
|
||||
startup_resume,
|
||||
_driver_process: driver_process,
|
||||
@@ -456,9 +471,14 @@ impl ComputeRuntime {
|
||||
&self.default_image
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn driver_info_snapshots(&self) -> &[ComputeDriverInfoSnapshot] {
|
||||
std::slice::from_ref(&self.driver_info)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn driver_kind(&self) -> Option<ComputeDriverKind> {
|
||||
self.driver_name.parse().ok()
|
||||
self.driver_info.name.parse().ok()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -467,8 +487,8 @@ impl ComputeRuntime {
|
||||
}
|
||||
|
||||
pub async fn validate_sandbox_create(&self, sandbox: &Sandbox) -> Result<(), Status> {
|
||||
let driver_sandbox =
|
||||
driver_sandbox_from_public(sandbox, &self.driver_name).map_err(|status| *status)?;
|
||||
let driver_sandbox = driver_sandbox_from_public(sandbox, &self.driver_info.name)
|
||||
.map_err(|status| *status)?;
|
||||
self.driver
|
||||
.validate_sandbox_create(Request::new(ValidateSandboxCreateRequest {
|
||||
sandbox: Some(driver_sandbox),
|
||||
@@ -483,8 +503,8 @@ impl ComputeRuntime {
|
||||
sandbox_token: Option<String>,
|
||||
) -> Result<Sandbox, Status> {
|
||||
let sandbox_id = sandbox.object_id().to_string();
|
||||
let mut driver_sandbox =
|
||||
driver_sandbox_from_public(&sandbox, &self.driver_name).map_err(|status| *status)?;
|
||||
let mut driver_sandbox = driver_sandbox_from_public(&sandbox, &self.driver_info.name)
|
||||
.map_err(|status| *status)?;
|
||||
|
||||
// Create with MustCreate condition to prevent duplicate creation race
|
||||
self.sandbox_index.update_from_sandbox(&sandbox);
|
||||
@@ -2097,7 +2117,11 @@ impl ComputeDriver for NoopTestDriver {
|
||||
pub async fn new_test_runtime(store: Arc<Store>) -> ComputeRuntime {
|
||||
ComputeRuntime {
|
||||
driver: Arc::new(NoopTestDriver),
|
||||
driver_name: "test".to_string(),
|
||||
driver_info: ComputeDriverInfoSnapshot {
|
||||
name: "test".to_string(),
|
||||
driver_name: "test".to_string(),
|
||||
driver_version: "test".to_string(),
|
||||
},
|
||||
shutdown_cleanup: None,
|
||||
startup_resume: None,
|
||||
_driver_process: None,
|
||||
@@ -2352,7 +2376,11 @@ mod tests {
|
||||
let store = Arc::new(Store::connect("sqlite::memory:").await.unwrap());
|
||||
ComputeRuntime {
|
||||
driver,
|
||||
driver_name: "test-driver".to_string(),
|
||||
driver_info: ComputeDriverInfoSnapshot {
|
||||
name: "test-driver".to_string(),
|
||||
driver_name: "test-driver".to_string(),
|
||||
driver_version: "test".to_string(),
|
||||
},
|
||||
shutdown_cleanup: None,
|
||||
startup_resume,
|
||||
_driver_process: None,
|
||||
|
||||
@@ -13,18 +13,19 @@ mod validation;
|
||||
use openshell_core::proto::{
|
||||
ApproveAllDraftChunksRequest, ApproveAllDraftChunksResponse, ApproveDraftChunkRequest,
|
||||
ApproveDraftChunkResponse, AttachSandboxProviderRequest, AttachSandboxProviderResponse,
|
||||
ClearDraftChunksRequest, ClearDraftChunksResponse, ConfigureProviderRefreshRequest,
|
||||
ConfigureProviderRefreshResponse, CreateProviderRequest, CreateSandboxRequest,
|
||||
CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderProfileRequest,
|
||||
DeleteProviderProfileResponse, DeleteProviderRefreshRequest, DeleteProviderRefreshResponse,
|
||||
DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse,
|
||||
DeleteServiceRequest, DeleteServiceResponse, DetachSandboxProviderRequest,
|
||||
DetachSandboxProviderResponse, EditDraftChunkRequest, EditDraftChunkResponse, ExecSandboxEvent,
|
||||
ExecSandboxInput, ExecSandboxRequest, ExposeServiceRequest, GatewayMessage,
|
||||
GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse,
|
||||
GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderProfileRequest,
|
||||
GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest,
|
||||
GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest,
|
||||
ClearDraftChunksRequest, ClearDraftChunksResponse, ComputeDriverCapabilities,
|
||||
ComputeDriverInfo, ConfigureProviderRefreshRequest, ConfigureProviderRefreshResponse,
|
||||
CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse,
|
||||
DeleteProviderProfileRequest, DeleteProviderProfileResponse, DeleteProviderRefreshRequest,
|
||||
DeleteProviderRefreshResponse, DeleteProviderRequest, DeleteProviderResponse,
|
||||
DeleteSandboxRequest, DeleteSandboxResponse, DeleteServiceRequest, DeleteServiceResponse,
|
||||
DetachSandboxProviderRequest, DetachSandboxProviderResponse, EditDraftChunkRequest,
|
||||
EditDraftChunkResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest,
|
||||
ExposeServiceRequest, GatewayMessage, GetDraftHistoryRequest, GetDraftHistoryResponse,
|
||||
GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest,
|
||||
GetGatewayConfigResponse, GetGatewayInfoRequest, GetGatewayInfoResponse,
|
||||
GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse,
|
||||
GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest,
|
||||
GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse,
|
||||
GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest,
|
||||
GetServiceRequest, HealthRequest, HealthResponse, ImportProviderProfilesRequest,
|
||||
@@ -206,6 +207,32 @@ impl OpenShell for OpenShellService {
|
||||
}))
|
||||
}
|
||||
|
||||
#[rpc_auth(auth = "bearer", scope = "config:read", role = "admin")]
|
||||
async fn get_gateway_info(
|
||||
&self,
|
||||
_request: Request<GetGatewayInfoRequest>,
|
||||
) -> Result<Response<GetGatewayInfoResponse>, Status> {
|
||||
let compute_drivers = self
|
||||
.state
|
||||
.compute
|
||||
.driver_info_snapshots()
|
||||
.iter()
|
||||
.map(|driver| ComputeDriverInfo {
|
||||
name: driver.name.clone(),
|
||||
capabilities: Some(ComputeDriverCapabilities {
|
||||
driver_name: driver.driver_name.clone(),
|
||||
driver_version: driver.driver_version.clone(),
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Response::new(GetGatewayInfoResponse {
|
||||
status: ServiceStatus::Healthy.into(),
|
||||
gateway_version: openshell_core::VERSION.to_string(),
|
||||
compute_drivers,
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Sandbox lifecycle ---
|
||||
|
||||
#[rpc_auth(auth = "bearer", scope = "sandbox:write", role = "user")]
|
||||
|
||||
@@ -62,6 +62,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>,
|
||||
|
||||
@@ -111,6 +111,12 @@ impl OpenShell for RelayGateway {
|
||||
) -> Result<Response<openshell_core::proto::HealthResponse>, Status> {
|
||||
Err(Status::unimplemented("unused"))
|
||||
}
|
||||
async fn get_gateway_info(
|
||||
&self,
|
||||
_: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
|
||||
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
|
||||
Err(Status::unimplemented("unused"))
|
||||
}
|
||||
async fn create_sandbox(
|
||||
&self,
|
||||
_: tonic::Request<openshell_core::proto::CreateSandboxRequest>,
|
||||
|
||||
@@ -84,12 +84,21 @@ The active gateway is the persisted default. The `-g` flag and the `OPENSHELL_GA
|
||||
|
||||
Installers can seed read-only gateway entries for package-managed local services. By default the CLI reads these from `/etc/openshell`, using the same `active_gateway` plus `gateways/<name>/metadata.json` layout as per-user config. Packages can override that system config root with a non-empty absolute `OPENSHELL_SYSTEM_GATEWAY_DIR` when needed; empty or relative values fall back to `/etc/openshell` and log a warning. These entries appear in `openshell gateway list` and can be selected like user registrations. `openshell gateway list` and `openshell term` label each gateway as `user` or `system` so you can see which config layer owns it. `openshell gateway remove` removes only per-user registrations. Register a per-user gateway with the same name when you need to shadow an installer-provided default.
|
||||
|
||||
List all registered gateways:
|
||||
List all registered gateways. The table shows each gateway's endpoint, type,
|
||||
config source, authentication mode, and remote target when a remote registration
|
||||
has one:
|
||||
|
||||
```shell
|
||||
openshell gateway list
|
||||
```
|
||||
|
||||
Use structured output when you need the complete local registration metadata,
|
||||
including `remote_host` and `resolved_host` for remote registrations:
|
||||
|
||||
```shell
|
||||
openshell gateway list -o json
|
||||
```
|
||||
|
||||
Switch the active gateway:
|
||||
|
||||
```shell
|
||||
@@ -104,17 +113,24 @@ openshell status -g staging
|
||||
|
||||
## Inspect Gateway Status
|
||||
|
||||
Use `openshell status` for a quick health check:
|
||||
Use `openshell status` for a live gateway check. It verifies that the CLI can
|
||||
reach the running gateway, then reports health and the gateway version:
|
||||
|
||||
```shell
|
||||
openshell status
|
||||
```
|
||||
|
||||
Use `openshell gateway info` when you need the registered endpoint, gateway metadata, or compute driver details:
|
||||
Use `openshell gateway info` when you need elevated runtime details such as
|
||||
initialized compute drivers and driver-reported capability versions:
|
||||
|
||||
```shell
|
||||
openshell gateway info
|
||||
openshell gateway info --name production
|
||||
```
|
||||
|
||||
Use structured output when scripting against live runtime info:
|
||||
|
||||
```shell
|
||||
openshell gateway info -o json
|
||||
```
|
||||
|
||||
Remove a local CLI registration without stopping the gateway service:
|
||||
@@ -136,7 +152,7 @@ For Docker-backed local gateways, inspect Docker and the gateway process or cont
|
||||
|
||||
```shell
|
||||
openshell doctor check
|
||||
openshell gateway info
|
||||
openshell gateway list
|
||||
```
|
||||
|
||||
For Kubernetes gateways, inspect the gateway workload and cluster events:
|
||||
|
||||
@@ -22,6 +22,9 @@ service OpenShell {
|
||||
// Check the health of the service.
|
||||
rpc Health(HealthRequest) returns (HealthResponse);
|
||||
|
||||
// Fetch elevated live gateway runtime metadata.
|
||||
rpc GetGatewayInfo(GetGatewayInfoRequest) returns (GetGatewayInfoResponse);
|
||||
|
||||
// Create a new sandbox.
|
||||
rpc CreateSandbox(CreateSandboxRequest) returns (SandboxResponse);
|
||||
|
||||
@@ -290,6 +293,40 @@ message HealthResponse {
|
||||
string version = 2;
|
||||
}
|
||||
|
||||
// Gateway info request.
|
||||
message GetGatewayInfoRequest {}
|
||||
|
||||
// Gateway info response.
|
||||
message GetGatewayInfoResponse {
|
||||
// Service status.
|
||||
ServiceStatus status = 1;
|
||||
|
||||
// OpenShell gateway binary version.
|
||||
string gateway_version = 2;
|
||||
|
||||
// Compute driver runtimes initialized by this gateway. Current gateways
|
||||
// return exactly one entry.
|
||||
repeated ComputeDriverInfo compute_drivers = 3;
|
||||
}
|
||||
|
||||
// Info for one initialized compute driver runtime.
|
||||
message ComputeDriverInfo {
|
||||
// Gateway-selected driver name used for routing and driver_config keys.
|
||||
string name = 1;
|
||||
|
||||
// Capabilities reported by the driver during gateway runtime initialization.
|
||||
ComputeDriverCapabilities capabilities = 2;
|
||||
}
|
||||
|
||||
// Public compute driver capability snapshot.
|
||||
message ComputeDriverCapabilities {
|
||||
// Driver-reported human-readable name from the startup capability snapshot.
|
||||
string driver_name = 1;
|
||||
|
||||
// Driver-reported implementation version from the startup capability snapshot.
|
||||
string driver_version = 2;
|
||||
}
|
||||
|
||||
// Public sandbox resource exposed by the OpenShell API.
|
||||
//
|
||||
// This is the canonical gateway-owned view of a sandbox. It merges user intent
|
||||
|
||||
Reference in New Issue
Block a user