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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/openshell-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ openshell-prover = { path = "../openshell-prover" }
openshell-tui = { path = "../openshell-tui" }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yml = { workspace = true }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid YAML in new projects in general, but also specifically in Rust since serde-yaml has gone through some maintenance issues yaml/yaml-serde#2

Especially for this use case, there's no reason not to just use JSON which is about machine readability.

prost-types = { workspace = true }

# Async runtime
Expand Down
88 changes: 77 additions & 11 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,13 +647,13 @@ fn normalize_completion_script(output: Vec<u8>, executable: &std::path::Path) ->
}

#[derive(Clone, Debug, ValueEnum)]
enum ProviderProfileOutput {
enum OutputFormat {
Table,
Yaml,
Json,
}

impl ProviderProfileOutput {
impl OutputFormat {
fn as_str(&self) -> &'static str {
match self {
Self::Table => "table",
Expand Down Expand Up @@ -736,8 +736,8 @@ enum ProviderCommands {
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
ListProfiles {
/// Output format.
#[arg(short = 'o', long = "output", value_enum, default_value_t = ProviderProfileOutput::Table)]
output: ProviderProfileOutput,
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
output: OutputFormat,
},

/// Manage provider profiles.
Expand Down Expand Up @@ -786,8 +786,8 @@ enum ProviderProfileCommands {
id: String,

/// Output format.
#[arg(short = 'o', long = "output", value_enum, default_value_t = ProviderProfileOutput::Yaml)]
output: ProviderProfileOutput,
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Yaml)]
output: OutputFormat,
},

/// Import provider profiles from a file or directory.
Expand Down Expand Up @@ -1167,16 +1167,20 @@ enum SandboxCommands {
offset: u32,

/// Print only sandbox ids (one per line).
#[arg(long, conflicts_with = "names")]
#[arg(long, conflicts_with_all = ["names", "output"])]
ids: bool,

/// Print only sandbox names (one per line).
#[arg(long, conflicts_with = "ids")]
#[arg(long, conflicts_with_all = ["ids", "output"])]
names: bool,

/// Filter sandboxes by label selector (key1=value1,key2=value2).
#[arg(long)]
selector: Option<String>,

/// Output format.
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with_all = ["ids", "names"])]
output: OutputFormat,
},

/// Delete a sandbox by name.
Expand Down Expand Up @@ -2527,6 +2531,7 @@ async fn main() -> Result<()> {
ids,
names,
selector,
output,
} => {
run::sandbox_list(
endpoint,
Expand All @@ -2535,6 +2540,7 @@ async fn main() -> Result<()> {
ids,
names,
selector.as_deref(),
output.as_str(),
&tls,
)
.await?;
Expand Down Expand Up @@ -3398,7 +3404,7 @@ mod tests {
cli.command,
Some(Commands::Provider {
command: Some(ProviderCommands::ListProfiles {
output: ProviderProfileOutput::Table
output: OutputFormat::Table
})
})
));
Expand All @@ -3413,7 +3419,7 @@ mod tests {
cli.command,
Some(Commands::Provider {
command: Some(ProviderCommands::ListProfiles {
output: ProviderProfileOutput::Json
output: OutputFormat::Json
})
})
));
Expand All @@ -3436,7 +3442,7 @@ mod tests {
Some(Commands::Provider {
command: Some(ProviderCommands::Profile(ProviderProfileCommands::Export {
id,
output: ProviderProfileOutput::Yaml
output: OutputFormat::Yaml
}))
}) if id == "custom-api"
));
Expand Down Expand Up @@ -3473,6 +3479,66 @@ mod tests {
));
}

#[test]
fn sandbox_list_default_output_is_table() {
let cli = Cli::try_parse_from(["openshell", "sandbox", "list"])
.expect("sandbox list should parse");

assert!(matches!(
cli.command,
Some(Commands::Sandbox {
command: Some(SandboxCommands::List {
output: OutputFormat::Table,
..
})
})
));
}

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

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

#[test]
fn sandbox_list_accepts_output_yaml() {
let cli = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "yaml"])
.expect("sandbox list -o yaml should parse");

assert!(matches!(
cli.command,
Some(Commands::Sandbox {
command: Some(SandboxCommands::List {
output: OutputFormat::Yaml,
..
})
})
));
}

#[test]
fn sandbox_list_json_conflicts_with_ids() {
let result = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "json", "--ids"]);
assert!(result.is_err(), "--ids and -o json should conflict");
}

#[test]
fn sandbox_list_json_conflicts_with_names() {
let result = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "json", "--names"]);
assert!(result.is_err(), "--names and -o json should conflict");
}

#[test]
fn provider_create_accepts_custom_profile_type_ids() {
let cli = Cli::try_parse_from([
Expand Down
34 changes: 34 additions & 0 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3045,13 +3045,15 @@ fn print_sandbox_policy(policy: &SandboxPolicy) {
}

/// List sandboxes.
#[allow(clippy::too_many_arguments)]
pub async fn sandbox_list(
server: &str,
limit: u32,
offset: u32,
ids_only: bool,
names_only: bool,
label_selector: Option<&str>,
output: &str,
tls: &TlsOptions,
) -> Result<()> {
let mut client = grpc_client(server, tls).await?;
Expand All @@ -3066,6 +3068,25 @@ pub async fn sandbox_list(
.into_diagnostic()?;

let sandboxes = response.into_inner().sandboxes;

match output {
"json" => {
let items: Vec<serde_json::Value> = sandboxes.iter().map(sandbox_to_json).collect();
println!(
"{}",
serde_json::to_string_pretty(&items).into_diagnostic()?
);
return Ok(());
}
"yaml" => {
let items: Vec<serde_json::Value> = sandboxes.iter().map(sandbox_to_json).collect();
print!("{}", serde_yml::to_string(&items).into_diagnostic()?);
return Ok(());
}
"table" => {}
_ => return Err(miette!("unsupported output format: {output}")),
}

if sandboxes.is_empty() {
if !ids_only && !names_only {
println!("No sandboxes found.");
Expand Down Expand Up @@ -3126,6 +3147,19 @@ pub async fn sandbox_list(
Ok(())
}

fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
let meta = sandbox.metadata.as_ref();
let labels = meta.map_or_else(|| serde_json::json!({}), |m| serde_json::json!(m.labels));
serde_json::json!({
Comment on lines +3150 to +3153
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have checked to see if this can be done via #[derive(Serialize) on sandbox, or failing that a direct impl Serialize

"id": sandbox.object_id(),
"name": sandbox.object_name(),
"labels": labels,
"created_at": format_epoch_ms(meta.map_or(0, |m| m.created_at_ms)),
"phase": phase_name(sandbox.phase),
"current_policy_version": sandbox.current_policy_version,
})
}

pub async fn sandbox_provider_list(server: &str, name: &str, tls: &TlsOptions) -> Result<()> {
let mut client = grpc_client(server, tls).await?;
let response = client
Expand Down
7 changes: 7 additions & 0 deletions docs/sandboxes/manage-sandboxes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ Filter the list by labels when you want a narrower view:
openshell sandbox list --selector team=platform
```

Use `-o json` or `-o yaml` for machine-readable output:

```shell
openshell sandbox list -o json
openshell sandbox list -o yaml
```

Get detailed information about a specific sandbox. The output lists **Policy source** (`sandbox` or `global`), **Revision** (the active policy’s row version for that source), and the formatted active policy YAML:

```shell
Expand Down
Loading