Skip to content

Commit 0cbd2d6

Browse files
authored
feat(cli): add -o json/yaml output format to sandbox list (#1422)
Support machine-readable output for `openshell sandbox list` via `-o json` and `-o yaml` flags, matching the pattern established for provider profile commands. Rename ProviderProfileOutput to shared OutputFormat enum. Signed-off-by: Florent Benoit <fbenoit@redhat.com>
1 parent 09bd8a9 commit 0cbd2d6

5 files changed

Lines changed: 120 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/openshell-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ openshell-prover = { path = "../openshell-prover" }
2323
openshell-tui = { path = "../openshell-tui" }
2424
serde = { workspace = true }
2525
serde_json = { workspace = true }
26+
serde_yml = { workspace = true }
2627
prost-types = { workspace = true }
2728

2829
# Async runtime

crates/openshell-cli/src/main.rs

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -647,13 +647,13 @@ fn normalize_completion_script(output: Vec<u8>, executable: &std::path::Path) ->
647647
}
648648

649649
#[derive(Clone, Debug, ValueEnum)]
650-
enum ProviderProfileOutput {
650+
enum OutputFormat {
651651
Table,
652652
Yaml,
653653
Json,
654654
}
655655

656-
impl ProviderProfileOutput {
656+
impl OutputFormat {
657657
fn as_str(&self) -> &'static str {
658658
match self {
659659
Self::Table => "table",
@@ -736,8 +736,8 @@ enum ProviderCommands {
736736
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
737737
ListProfiles {
738738
/// Output format.
739-
#[arg(short = 'o', long = "output", value_enum, default_value_t = ProviderProfileOutput::Table)]
740-
output: ProviderProfileOutput,
739+
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
740+
output: OutputFormat,
741741
},
742742

743743
/// Manage provider profiles.
@@ -786,8 +786,8 @@ enum ProviderProfileCommands {
786786
id: String,
787787

788788
/// Output format.
789-
#[arg(short = 'o', long = "output", value_enum, default_value_t = ProviderProfileOutput::Yaml)]
790-
output: ProviderProfileOutput,
789+
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Yaml)]
790+
output: OutputFormat,
791791
},
792792

793793
/// Import provider profiles from a file or directory.
@@ -1167,16 +1167,20 @@ enum SandboxCommands {
11671167
offset: u32,
11681168

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

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

11771177
/// Filter sandboxes by label selector (key1=value1,key2=value2).
11781178
#[arg(long)]
11791179
selector: Option<String>,
1180+
1181+
/// Output format.
1182+
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with_all = ["ids", "names"])]
1183+
output: OutputFormat,
11801184
},
11811185

11821186
/// Delete a sandbox by name.
@@ -2527,6 +2531,7 @@ async fn main() -> Result<()> {
25272531
ids,
25282532
names,
25292533
selector,
2534+
output,
25302535
} => {
25312536
run::sandbox_list(
25322537
endpoint,
@@ -2535,6 +2540,7 @@ async fn main() -> Result<()> {
25352540
ids,
25362541
names,
25372542
selector.as_deref(),
2543+
output.as_str(),
25382544
&tls,
25392545
)
25402546
.await?;
@@ -3398,7 +3404,7 @@ mod tests {
33983404
cli.command,
33993405
Some(Commands::Provider {
34003406
command: Some(ProviderCommands::ListProfiles {
3401-
output: ProviderProfileOutput::Table
3407+
output: OutputFormat::Table
34023408
})
34033409
})
34043410
));
@@ -3413,7 +3419,7 @@ mod tests {
34133419
cli.command,
34143420
Some(Commands::Provider {
34153421
command: Some(ProviderCommands::ListProfiles {
3416-
output: ProviderProfileOutput::Json
3422+
output: OutputFormat::Json
34173423
})
34183424
})
34193425
));
@@ -3436,7 +3442,7 @@ mod tests {
34363442
Some(Commands::Provider {
34373443
command: Some(ProviderCommands::Profile(ProviderProfileCommands::Export {
34383444
id,
3439-
output: ProviderProfileOutput::Yaml
3445+
output: OutputFormat::Yaml
34403446
}))
34413447
}) if id == "custom-api"
34423448
));
@@ -3473,6 +3479,66 @@ mod tests {
34733479
));
34743480
}
34753481

3482+
#[test]
3483+
fn sandbox_list_default_output_is_table() {
3484+
let cli = Cli::try_parse_from(["openshell", "sandbox", "list"])
3485+
.expect("sandbox list should parse");
3486+
3487+
assert!(matches!(
3488+
cli.command,
3489+
Some(Commands::Sandbox {
3490+
command: Some(SandboxCommands::List {
3491+
output: OutputFormat::Table,
3492+
..
3493+
})
3494+
})
3495+
));
3496+
}
3497+
3498+
#[test]
3499+
fn sandbox_list_accepts_output_json() {
3500+
let cli = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "json"])
3501+
.expect("sandbox list -o json should parse");
3502+
3503+
assert!(matches!(
3504+
cli.command,
3505+
Some(Commands::Sandbox {
3506+
command: Some(SandboxCommands::List {
3507+
output: OutputFormat::Json,
3508+
..
3509+
})
3510+
})
3511+
));
3512+
}
3513+
3514+
#[test]
3515+
fn sandbox_list_accepts_output_yaml() {
3516+
let cli = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "yaml"])
3517+
.expect("sandbox list -o yaml should parse");
3518+
3519+
assert!(matches!(
3520+
cli.command,
3521+
Some(Commands::Sandbox {
3522+
command: Some(SandboxCommands::List {
3523+
output: OutputFormat::Yaml,
3524+
..
3525+
})
3526+
})
3527+
));
3528+
}
3529+
3530+
#[test]
3531+
fn sandbox_list_json_conflicts_with_ids() {
3532+
let result = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "json", "--ids"]);
3533+
assert!(result.is_err(), "--ids and -o json should conflict");
3534+
}
3535+
3536+
#[test]
3537+
fn sandbox_list_json_conflicts_with_names() {
3538+
let result = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "json", "--names"]);
3539+
assert!(result.is_err(), "--names and -o json should conflict");
3540+
}
3541+
34763542
#[test]
34773543
fn provider_create_accepts_custom_profile_type_ids() {
34783544
let cli = Cli::try_parse_from([

crates/openshell-cli/src/run.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3045,13 +3045,15 @@ fn print_sandbox_policy(policy: &SandboxPolicy) {
30453045
}
30463046

30473047
/// List sandboxes.
3048+
#[allow(clippy::too_many_arguments)]
30483049
pub async fn sandbox_list(
30493050
server: &str,
30503051
limit: u32,
30513052
offset: u32,
30523053
ids_only: bool,
30533054
names_only: bool,
30543055
label_selector: Option<&str>,
3056+
output: &str,
30553057
tls: &TlsOptions,
30563058
) -> Result<()> {
30573059
let mut client = grpc_client(server, tls).await?;
@@ -3066,6 +3068,25 @@ pub async fn sandbox_list(
30663068
.into_diagnostic()?;
30673069

30683070
let sandboxes = response.into_inner().sandboxes;
3071+
3072+
match output {
3073+
"json" => {
3074+
let items: Vec<serde_json::Value> = sandboxes.iter().map(sandbox_to_json).collect();
3075+
println!(
3076+
"{}",
3077+
serde_json::to_string_pretty(&items).into_diagnostic()?
3078+
);
3079+
return Ok(());
3080+
}
3081+
"yaml" => {
3082+
let items: Vec<serde_json::Value> = sandboxes.iter().map(sandbox_to_json).collect();
3083+
print!("{}", serde_yml::to_string(&items).into_diagnostic()?);
3084+
return Ok(());
3085+
}
3086+
"table" => {}
3087+
_ => return Err(miette!("unsupported output format: {output}")),
3088+
}
3089+
30693090
if sandboxes.is_empty() {
30703091
if !ids_only && !names_only {
30713092
println!("No sandboxes found.");
@@ -3126,6 +3147,19 @@ pub async fn sandbox_list(
31263147
Ok(())
31273148
}
31283149

3150+
fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
3151+
let meta = sandbox.metadata.as_ref();
3152+
let labels = meta.map_or_else(|| serde_json::json!({}), |m| serde_json::json!(m.labels));
3153+
serde_json::json!({
3154+
"id": sandbox.object_id(),
3155+
"name": sandbox.object_name(),
3156+
"labels": labels,
3157+
"created_at": format_epoch_ms(meta.map_or(0, |m| m.created_at_ms)),
3158+
"phase": phase_name(sandbox.phase),
3159+
"current_policy_version": sandbox.current_policy_version,
3160+
})
3161+
}
3162+
31293163
pub async fn sandbox_provider_list(server: &str, name: &str, tls: &TlsOptions) -> Result<()> {
31303164
let mut client = grpc_client(server, tls).await?;
31313165
let response = client

docs/sandboxes/manage-sandboxes.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@ Filter the list by labels when you want a narrower view:
211211
openshell sandbox list --selector team=platform
212212
```
213213
214+
Use `-o json` or `-o yaml` for machine-readable output:
215+
216+
```shell
217+
openshell sandbox list -o json
218+
openshell sandbox list -o yaml
219+
```
220+
214221
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:
215222
216223
```shell

0 commit comments

Comments
 (0)