Skip to content
Open
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
98 changes: 97 additions & 1 deletion src/cmds/cloud/aws_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ pub fn run(subcommand: &str, args: &[String], verbose: u8) -> Result<i32> {
format!("{} {}", subcommand, args.join(" "))
};

// When the user explicitly requests a structured, parseable output format
// (--output json/yaml), honor it losslessly. rtk's summarizers and generic
// schema compression would otherwise drop field values and emit non-parseable
// output, breaking the --output contract (issue #2139).
if explicit_lossless_format(args) {
return run_passthrough(subcommand, args, verbose, &full_sub);
}

// Route to specialized handlers
match subcommand {
"sts" if !args.is_empty() && args[0] == "get-caller-identity" => run_aws_filtered(
Expand Down Expand Up @@ -215,6 +223,66 @@ fn is_structured_operation(args: &[String]) -> bool {
|| op == "receive-message"
}

/// Returns true when the user explicitly requested a structured, parseable
/// output format (`--output json` / `--output yaml`). In that case rtk must not
/// summarize or schema-compress — the output has to stay byte-faithful so that
/// downstream `jq` / `json.load` parsing keeps working.
fn explicit_lossless_format(args: &[String]) -> bool {
// Scan all args (last --output wins, matching aws CLI semantics for repeats).
let mut last: Option<bool> = None;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--output" {
// A dangling --output (no value) is malformed and aws will reject it;
// don't silently apply lossy compression to it.
last = Some(match iter.next() {
Some(fmt) => matches!(fmt.as_str(), "json" | "yaml" | "yaml-stream"),
None => true,
});
} else if let Some(fmt) = arg.strip_prefix("--output=") {
last = Some(matches!(fmt, "json" | "yaml" | "yaml-stream"));
}
}
last.unwrap_or(false)
}

/// Execute an aws command without filtering, preserving output byte-for-byte.
/// Used when the user explicitly requested a lossless output format.
fn run_passthrough(subcommand: &str, args: &[String], verbose: u8, full_sub: &str) -> Result<i32> {
let timer = tracking::TimedExecution::start();

let mut cmd = resolved_command("aws");
cmd.arg(subcommand);
for arg in args {
cmd.arg(arg);
}

if verbose > 0 {
eprintln!("Running: aws {}", full_sub);
}

let output = cmd.output().context("Failed to run aws CLI")?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let cmd_label = format!("aws {}", full_sub);
let rtk_label = format!("rtk aws {}", full_sub);

if !output.status.success() {
eprintln!("{}", stderr.trim());
timer.track(&cmd_label, &rtk_label, &stderr, &stderr);
return Ok(exit_code_from_output(&output, "aws"));
}

// No tee hint: output is already byte-faithful, so there is no truncated
// data to recover — teeing would just duplicate stdout.
print!("{}", stdout);
if !stderr.is_empty() {
eprint!("{}", stderr);
}
timer.track(&cmd_label, &rtk_label, &stdout, &stdout);
Ok(0)
}

/// Generic strategy: force --output json for structured ops, compress via json_cmd schema
fn run_generic(subcommand: &str, args: &[String], verbose: u8, full_sub: &str) -> Result<i32> {
let timer = tracking::TimedExecution::start();
Expand All @@ -224,7 +292,7 @@ fn run_generic(subcommand: &str, args: &[String], verbose: u8, full_sub: &str) -

let mut has_output_flag = false;
for arg in args {
if arg == "--output" {
if arg == "--output" || arg.starts_with("--output=") {
has_output_flag = true;
}
cmd.arg(arg);
Expand Down Expand Up @@ -1546,6 +1614,34 @@ mod tests {
use super::*;
use crate::core::utils::count_tokens;

#[test]
fn test_explicit_lossless_format() {
let s = |a: &[&str]| a.iter().map(|x| x.to_string()).collect::<Vec<_>>();
// Explicit json/yaml → lossless passthrough
assert!(explicit_lossless_format(&s(&[
"get-function",
"--function-name",
"fn",
"--output",
"json"
])));
assert!(explicit_lossless_format(&s(&["get-function", "--output=json"])));
assert!(explicit_lossless_format(&s(&["describe-stacks", "--output", "yaml"])));
// Lossy/human formats and absent flag → keep normal filtering
assert!(!explicit_lossless_format(&s(&["get-function", "--output", "table"])));
assert!(!explicit_lossless_format(&s(&["get-function", "--output", "text"])));
assert!(!explicit_lossless_format(&s(&["list-functions"])));
// Repeated --output: last wins (matches aws CLI)
assert!(explicit_lossless_format(&s(&[
"--output", "table", "--output", "json"
])));
assert!(!explicit_lossless_format(&s(&[
"--output", "json", "--output", "table"
])));
// Dangling --output (malformed) → don't apply lossy compression
assert!(explicit_lossless_format(&s(&["get-function", "--output"])));
}

#[test]
fn test_snapshot_sts_identity() {
let json = r#"{
Expand Down