Skip to content

Commit 09ebe77

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

5 files changed

Lines changed: 162 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: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,10 @@ enum PolicyCommands {
14911491
#[arg(long)]
14921492
full: bool,
14931493

1494+
/// Output as JSON.
1495+
#[arg(long)]
1496+
json: bool,
1497+
14941498
/// Show the global policy revision.
14951499
#[arg(long)]
14961500
global: bool,
@@ -2167,13 +2171,16 @@ async fn main() -> Result<()> {
21672171
name,
21682172
rev,
21692173
full,
2174+
json,
21702175
global,
21712176
} => {
21722177
if global {
2173-
run::sandbox_policy_get_global(&ctx.endpoint, rev, full, &tls).await?;
2178+
run::sandbox_policy_get_global(&ctx.endpoint, rev, full, json, &tls)
2179+
.await?;
21742180
} else {
21752181
let name = resolve_sandbox_name(name, &ctx.name)?;
2176-
run::sandbox_policy_get(&ctx.endpoint, &name, rev, full, &tls).await?;
2182+
run::sandbox_policy_get(&ctx.endpoint, &name, rev, full, json, &tls)
2183+
.await?;
21772184
}
21782185
}
21792186
PolicyCommands::List {
@@ -3557,6 +3564,33 @@ mod tests {
35573564
}
35583565
}
35593566

3567+
#[test]
3568+
fn policy_get_json_parses() {
3569+
let cli = Cli::try_parse_from([
3570+
"openshell",
3571+
"policy",
3572+
"get",
3573+
"my-sandbox",
3574+
"--full",
3575+
"--json",
3576+
])
3577+
.expect("policy get --json should parse");
3578+
3579+
match cli.command {
3580+
Some(Commands::Policy {
3581+
command:
3582+
Some(PolicyCommands::Get {
3583+
name, full, json, ..
3584+
}),
3585+
}) => {
3586+
assert_eq!(name.as_deref(), Some("my-sandbox"));
3587+
assert!(full);
3588+
assert!(json);
3589+
}
3590+
other => panic!("expected policy get command, got: {other:?}"),
3591+
}
3592+
}
3593+
35603594
#[test]
35613595
fn policy_delete_global_parses() {
35623596
let cli = Cli::try_parse_from(["openshell", "policy", "delete", "--global", "--yes"])

crates/openshell-cli/src/run.rs

Lines changed: 81 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+
json: bool,
56565657
tls: &TlsOptions,
56575658
) -> Result<()> {
56585659
let mut client = grpc_client(server, tls).await?;
@@ -5669,6 +5670,19 @@ 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+
if json {
5674+
let obj = policy_revision_to_json(
5675+
"sandbox",
5676+
Some(name),
5677+
Some(inner.active_version),
5678+
&rev,
5679+
status,
5680+
full,
5681+
)?;
5682+
println!("{}", serde_json::to_string_pretty(&obj).into_diagnostic()?);
5683+
return Ok(());
5684+
}
5685+
56725686
println!("Version: {}", rev.version);
56735687
println!("Hash: {}", rev.policy_hash);
56745688
println!("Status: {status:?}");
@@ -5704,6 +5718,7 @@ pub async fn sandbox_policy_get_global(
57045718
server: &str,
57055719
version: u32,
57065720
full: bool,
5721+
json: bool,
57075722
tls: &TlsOptions,
57085723
) -> Result<()> {
57095724
let mut client = grpc_client(server, tls).await?;
@@ -5720,6 +5735,12 @@ pub async fn sandbox_policy_get_global(
57205735
let inner = status_resp.into_inner();
57215736
if let Some(rev) = inner.revision {
57225737
let status = PolicyStatus::try_from(rev.status).unwrap_or(PolicyStatus::Unspecified);
5738+
if json {
5739+
let obj = policy_revision_to_json("global", None, None, &rev, status, full)?;
5740+
println!("{}", serde_json::to_string_pretty(&obj).into_diagnostic()?);
5741+
return Ok(());
5742+
}
5743+
57235744
println!("Scope: global");
57245745
println!("Version: {}", rev.version);
57255746
println!("Hash: {}", rev.policy_hash);
@@ -5748,6 +5769,66 @@ pub async fn sandbox_policy_get_global(
57485769
Ok(())
57495770
}
57505771

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