Skip to content

Commit 141d190

Browse files
committed
fix(cli): add json output for policy get
1 parent 910d3f0 commit 141d190

5 files changed

Lines changed: 199 additions & 2 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/src/main.rs‎

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,21 @@ impl ProviderProfileOutput {
663663
}
664664
}
665665

666+
#[derive(Clone, Debug, ValueEnum)]
667+
enum PolicyGetOutput {
668+
Table,
669+
Json,
670+
}
671+
672+
impl PolicyGetOutput {
673+
fn as_str(&self) -> &'static str {
674+
match self {
675+
Self::Table => "table",
676+
Self::Json => "json",
677+
}
678+
}
679+
}
680+
666681
#[derive(Clone, Debug, ValueEnum)]
667682
enum CliEditor {
668683
Vscode,
@@ -1491,6 +1506,10 @@ enum PolicyCommands {
14911506
#[arg(long)]
14921507
full: bool,
14931508

1509+
/// Output format.
1510+
#[arg(short = 'o', long = "output", value_enum, default_value_t = PolicyGetOutput::Table)]
1511+
output: PolicyGetOutput,
1512+
14941513
/// Show the global policy revision.
14951514
#[arg(long)]
14961515
global: bool,
@@ -2167,13 +2186,29 @@ async fn main() -> Result<()> {
21672186
name,
21682187
rev,
21692188
full,
2189+
output,
21702190
global,
21712191
} => {
21722192
if global {
2173-
run::sandbox_policy_get_global(&ctx.endpoint, rev, full, &tls).await?;
2193+
run::sandbox_policy_get_global(
2194+
&ctx.endpoint,
2195+
rev,
2196+
full,
2197+
output.as_str(),
2198+
&tls,
2199+
)
2200+
.await?;
21742201
} else {
21752202
let name = resolve_sandbox_name(name, &ctx.name)?;
2176-
run::sandbox_policy_get(&ctx.endpoint, &name, rev, full, &tls).await?;
2203+
run::sandbox_policy_get(
2204+
&ctx.endpoint,
2205+
&name,
2206+
rev,
2207+
full,
2208+
output.as_str(),
2209+
&tls,
2210+
)
2211+
.await?;
21772212
}
21782213
}
21792214
PolicyCommands::List {
@@ -3557,6 +3592,34 @@ mod tests {
35573592
}
35583593
}
35593594

3595+
#[test]
3596+
fn policy_get_json_output_parses() {
3597+
let cli = Cli::try_parse_from([
3598+
"openshell",
3599+
"policy",
3600+
"get",
3601+
"my-sandbox",
3602+
"--full",
3603+
"-o",
3604+
"json",
3605+
])
3606+
.expect("policy get -o json should parse");
3607+
3608+
match cli.command {
3609+
Some(Commands::Policy {
3610+
command:
3611+
Some(PolicyCommands::Get {
3612+
name, full, output, ..
3613+
}),
3614+
}) => {
3615+
assert_eq!(name.as_deref(), Some("my-sandbox"));
3616+
assert!(full);
3617+
assert!(matches!(output, PolicyGetOutput::Json));
3618+
}
3619+
other => panic!("expected policy get command, got: {other:?}"),
3620+
}
3621+
}
3622+
35603623
#[test]
35613624
fn policy_delete_global_parses() {
35623625
let cli = Cli::try_parse_from(["openshell", "policy", "delete", "--global", "--yes"])

‎crates/openshell-cli/src/run.rs‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5653,6 +5653,7 @@ pub async fn sandbox_policy_get(
56535653
name: &str,
56545654
version: u32,
56555655
full: bool,
5656+
output: &str,
56565657
tls: &TlsOptions,
56575658
) -> Result<()> {
56585659
let mut client = grpc_client(server, tls).await?;
@@ -5669,6 +5670,23 @@ pub async fn sandbox_policy_get(
56695670
let inner = status_resp.into_inner();
56705671
if let Some(rev) = inner.revision {
56715672
let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified);
5673+
match output {
5674+
"json" => {
5675+
let obj = policy_revision_to_json(
5676+
"sandbox",
5677+
Some(name),
5678+
Some(inner.active_version),
5679+
&rev,
5680+
status,
5681+
full,
5682+
)?;
5683+
println!("{}", serde_json::to_string_pretty(&obj).into_diagnostic()?);
5684+
return Ok(());
5685+
}
5686+
"table" => {}
5687+
_ => return Err(miette!("unsupported output format: {output}")),
5688+
}
5689+
56725690
println!("Version: {}", rev.version);
56735691
println!("Hash: {}", rev.policy_hash);
56745692
println!("Status: {status:?}");
@@ -5704,6 +5722,7 @@ pub async fn sandbox_policy_get_global(
57045722
server: &str,
57055723
version: u32,
57065724
full: bool,
5725+
output: &str,
57075726
tls: &TlsOptions,
57085727
) -> Result<()> {
57095728
let mut client = grpc_client(server, tls).await?;
@@ -5720,6 +5739,16 @@ pub async fn sandbox_policy_get_global(
57205739
let inner = status_resp.into_inner();
57215740
if let Some(rev) = inner.revision {
57225741
let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified);
5742+
match output {
5743+
"json" => {
5744+
let obj = policy_revision_to_json("global", None, None, &rev, status, full)?;
5745+
println!("{}", serde_json::to_string_pretty(&obj).into_diagnostic()?);
5746+
return Ok(());
5747+
}
5748+
"table" => {}
5749+
_ => return Err(miette!("unsupported output format: {output}")),
5750+
}
5751+
57235752
println!("Scope: global");
57245753
println!("Version: {}", rev.version);
57255754
println!("Hash: {}", rev.policy_hash);
@@ -5748,6 +5777,66 @@ pub async fn sandbox_policy_get_global(
57485777
Ok(())
57495778
}
57505779

5780+
fn policy_status_json_name(status: PolicyStatus) -> &'static str {
5781+
match status {
5782+
PolicyStatus::Unspecified => "unspecified",
5783+
PolicyStatus::Pending => "pending",
5784+
PolicyStatus::Loaded => "loaded",
5785+
PolicyStatus::Failed => "failed",
5786+
PolicyStatus::Superseded => "superseded",
5787+
}
5788+
}
5789+
5790+
fn policy_revision_to_json(
5791+
scope: &str,
5792+
sandbox: Option<&str>,
5793+
active_version: Option<u32>,
5794+
rev: &openshell_core::proto::SandboxPolicyRevision,
5795+
status: PolicyStatus,
5796+
full: bool,
5797+
) -> Result<serde_json::Value> {
5798+
let mut obj = serde_json::Map::new();
5799+
obj.insert("scope".to_string(), serde_json::json!(scope));
5800+
if let Some(sandbox) = sandbox {
5801+
obj.insert("sandbox".to_string(), serde_json::json!(sandbox));
5802+
}
5803+
obj.insert("version".to_string(), serde_json::json!(rev.version));
5804+
obj.insert("hash".to_string(), serde_json::json!(rev.policy_hash));
5805+
obj.insert(
5806+
"status".to_string(),
5807+
serde_json::json!(policy_status_json_name(status)),
5808+
);
5809+
if let Some(active_version) = active_version {
5810+
obj.insert(
5811+
"active_version".to_string(),
5812+
serde_json::json!(active_version),
5813+
);
5814+
}
5815+
if rev.created_at_ms > 0 {
5816+
obj.insert(
5817+
"created_at_ms".to_string(),
5818+
serde_json::json!(rev.created_at_ms),
5819+
);
5820+
}
5821+
if rev.loaded_at_ms > 0 {
5822+
obj.insert(
5823+
"loaded_at_ms".to_string(),
5824+
serde_json::json!(rev.loaded_at_ms),
5825+
);
5826+
}
5827+
if !rev.load_error.is_empty() {
5828+
obj.insert("load_error".to_string(), serde_json::json!(rev.load_error));
5829+
}
5830+
if full {
5831+
let policy = match rev.policy.as_ref() {
5832+
Some(policy) => openshell_policy::sandbox_policy_to_json_value(policy)?,
5833+
None => serde_json::Value::Null,
5834+
};
5835+
obj.insert("policy".to_string(), policy);
5836+
}
5837+
Ok(serde_json::Value::Object(obj))
5838+
}
5839+
57515840
pub async fn sandbox_policy_list(
57525841
server: &str,
57535842
name: &str,

‎crates/openshell-policy/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ repository.workspace = true
1313
[dependencies]
1414
openshell-core = { path = "../openshell-core" }
1515
serde = { workspace = true }
16+
serde_json = { workspace = true }
1617
serde_yml = { workspace = true }
1718
miette = { workspace = true }
1819

‎crates/openshell-policy/src/lib.rs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,25 @@ pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result<String> {
558558
.wrap_err("failed to serialize policy to YAML")
559559
}
560560

561+
/// Convert a proto sandbox policy into the canonical policy JSON representation.
562+
///
563+
/// The shape mirrors the YAML schema used by [`serialize_sandbox_policy`], so
564+
/// automation can use the same documented field names in either format.
565+
pub fn sandbox_policy_to_json_value(policy: &SandboxPolicy) -> Result<serde_json::Value> {
566+
let json_repr = from_proto(policy);
567+
serde_json::to_value(&json_repr)
568+
.into_diagnostic()
569+
.wrap_err("failed to serialize policy to JSON")
570+
}
571+
572+
/// Serialize a proto sandbox policy to a pretty-printed JSON string.
573+
pub fn serialize_sandbox_policy_json(policy: &SandboxPolicy) -> Result<String> {
574+
let json_repr = sandbox_policy_to_json_value(policy)?;
575+
serde_json::to_string_pretty(&json_repr)
576+
.into_diagnostic()
577+
.wrap_err("failed to serialize policy to JSON")
578+
}
579+
561580
/// Load a sandbox policy from an explicit source.
562581
///
563582
/// Resolution order:
@@ -881,6 +900,30 @@ mod tests {
881900
);
882901
}
883902

903+
/// Verify that JSON serialization uses the same canonical schema keys as YAML.
904+
#[test]
905+
fn serialized_json_uses_policy_schema_keys() {
906+
let proto = parse_sandbox_policy(
907+
r"
908+
version: 1
909+
network_policies:
910+
github:
911+
endpoints:
912+
- host: api.github.com
913+
port: 443
914+
protocol: https
915+
binaries:
916+
- path: /usr/bin/curl
917+
",
918+
)
919+
.expect("parse failed");
920+
let json = sandbox_policy_to_json_value(&proto).expect("serialize failed");
921+
922+
assert_eq!(json["version"], serde_json::json!(1));
923+
assert!(json.get("filesystem").is_none());
924+
assert!(json.get("network_policies").is_some());
925+
}
926+
884927
/// Verify that `allowed_ips` survives the round-trip.
885928
#[test]
886929
fn round_trip_preserves_allowed_ips() {

0 commit comments

Comments
 (0)