Skip to content

Commit

Permalink
Enhance 'param diff' command to enable time-based differences
Browse files Browse the repository at this point in the history
  • Loading branch information
rickporter-tuono authored Aug 30, 2021
1 parent 35a3c07 commit 6c65140
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 78 deletions.
19 changes: 10 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,13 @@ pub fn build_cli() -> App<'static, 'static> {
SubCommand::with_name("differences")
.visible_aliases(&["difference", "differ", "diff"])
.about("Show differences between properties from two environments")
.arg(Arg::with_name("ENV1")
.required(true)
.index(1)
.help("Name of first environment for comparison."))
.arg(Arg::with_name("ENV2")
.required(true)
.index(2)
.help("Name of second environment for comparison."))
.arg(Arg::with_name("ENV")
.short("e")
.long("env")
.takes_value(true)
.multiple(true)
.help("Up to two environment(s) to be compared.")
)
.arg(Arg::with_name("properties")
.short("p")
.long("property")
Expand All @@ -346,7 +345,9 @@ pub fn build_cli() -> App<'static, 'static> {
.multiple(true)
.default_value("value")
.help("List of the properties to compare."))
.arg(param_as_of_arg())
.arg(param_as_of_arg()
.multiple(true)
.help("Up to two times to be compared"))
.arg(table_format_options().help("Display difference format"))
.arg(secrets_display_flag().help("Show secret values")),
]),
Expand Down
184 changes: 126 additions & 58 deletions src/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,73 +91,141 @@ fn proc_param_diff(
resolved: &ResolvedIds,
) -> Result<()> {
let show_secrets = subcmd_args.is_present(SECRETS_FLAG);
let as_of = parse_datetime(subcmd_args.value_of(AS_OF_ARG));
let fmt = subcmd_args.value_of(FORMAT_OPT).unwrap();
let properties: Vec<&str> = subcmd_args.values_of("properties").unwrap().collect();
let env1_name = subcmd_args.value_of("ENV1").unwrap();
let env2_name = subcmd_args.value_of("ENV2").unwrap();
let as_list: Vec<&str> = subcmd_args
.values_of(AS_OF_ARG)
.unwrap_or_default()
.collect();
let env_list: Vec<&str> = subcmd_args.values_of("ENV").unwrap_or_default().collect();
let max_len: usize = 2;

if env_list.len() > max_len {
warning_message(format!(
"Can specify a maximum of {} environment values.",
max_len
))?;
return Ok(());
}
if as_list.len() > max_len {
warning_message(format!(
"Can specify a maximum of {} as-of values.",
max_len
))?;
return Ok(());
}

if env1_name == env2_name {
let env1_name: String;
let env2_name: String;
if env_list.len() == 2 {
env1_name = env_list[0].to_string();
env2_name = env_list[1].to_string();
} else if env_list.len() == 1 {
env1_name = resolved.environment_display_name();
env2_name = env_list[0].to_string();
} else {
env1_name = resolved.environment_display_name();
env2_name = resolved.environment_display_name();
}

let as_of1: Option<String>;
let as_of2: Option<String>;
if as_list.len() == 2 {
as_of1 = parse_datetime(Some(as_list[0]));
as_of2 = parse_datetime(Some(as_list[1]));
} else if as_list.len() == 1 {
// puts the specified time in other column
as_of1 = None;
as_of2 = parse_datetime(Some(as_list[0]));
} else {
as_of1 = None;
as_of2 = None;
}

if env1_name == env2_name && as_of1 == as_of2 {
warning_message("Invalid comparing an environment to itself".to_string())?;
return Ok(());
}

let header1: String;
let header2: String;
if env1_name == env2_name {
header1 = as_of1.clone().unwrap_or_else(|| "Current".to_string());
header2 = as_of2.clone().unwrap_or_else(|| "Unspecified".to_string());
} else if as_of1 == as_of2 {
header1 = env1_name.to_string();
header2 = env2_name.to_string();
} else {
let proj_id = resolved.project_id();

// fetch all environments once, and then determine id's from the same map that is
// used to resolve the environment names.
let environments = Environments::new();
let env_url_map = environments.get_url_name_map(rest_cfg);
let env1_id = environments.id_from_map(env1_name, &env_url_map)?;
let env2_id = environments.id_from_map(env2_name, &env_url_map)?;

let env1_values = parameters.get_parameter_detail_map(
rest_cfg,
&env_url_map,
proj_id,
&env1_id,
!show_secrets,
as_of.clone(),
)?;
let env2_values = parameters.get_parameter_detail_map(
rest_cfg,
&env_url_map,
proj_id,
&env2_id,
!show_secrets,
as_of,
)?;
let mut param_list: Vec<String> = env1_values.iter().map(|(k, _)| k.clone()).collect();
param_list.sort_by_key(|l| l.to_lowercase());
header1 = match as_of1 {
Some(ref a) => format!("{} ({})", env1_name, a),
_ => env1_name.to_string(),
};
header2 = match as_of2 {
Some(ref a) => format!("{} ({})", env2_name, a),
_ => env2_name.to_string(),
};
}

let default_param = ParameterDetails::default();
let mut added = false;
let mut table = Table::new("parameter");
let mut errors: Vec<String> = vec![];
table.set_header(&["Parameter", env1_name, env2_name]);
for param_name in param_list {
let details1 = env1_values.get(&param_name).unwrap_or(&default_param);
let details2 = env2_values.get(&param_name).unwrap_or(&default_param);
let env1 = details1.get_properties(&properties).join(",\n");
let env2 = details2.get_properties(&properties).join(",\n");
if !details1.error.is_empty() {
errors.push(format_param_error(&param_name, &details1.error))
}
// NOTE: do not put redundant errors on the list, but the errors could be due to
// different FQNs
if !details2.error.is_empty() && details1.error != details2.error {
errors.push(format_param_error(&param_name, &details2.error))
}
if env1 != env2 {
table.add_row(vec![param_name, env1, env2]);
added = true;
}
// fetch all environments once, and then determine id's from the same map that is
// used to resolve the environment names.
let environments = Environments::new();
let env_url_map = environments.get_url_name_map(rest_cfg);
let env1_id = environments.id_from_map(&env1_name, &env_url_map)?;
let env2_id = environments.id_from_map(&env2_name, &env_url_map)?;

let proj_id = resolved.project_id();
let env1_values = parameters.get_parameter_detail_map(
rest_cfg,
&env_url_map,
proj_id,
&env1_id,
!show_secrets,
as_of1,
)?;
let env2_values = parameters.get_parameter_detail_map(
rest_cfg,
&env_url_map,
proj_id,
&env2_id,
!show_secrets,
as_of2,
)?;

// get the names from both lists to make sure we get the added/deleted parameters, too
let mut param_list: Vec<String> = env1_values.iter().map(|(k, _)| k.clone()).collect();
param_list.append(&mut env2_values.iter().map(|(k, _)| k.clone()).collect());
param_list.sort_by_key(|l| l.to_lowercase());
param_list.dedup();

let default_param = ParameterDetails::default();
let mut added = false;
let mut table = Table::new("parameter");
let mut errors: Vec<String> = vec![];
table.set_header(&["Parameter", &header1, &header2]);
for param_name in param_list {
let details1 = env1_values.get(&param_name).unwrap_or(&default_param);
let details2 = env2_values.get(&param_name).unwrap_or(&default_param);
let env1 = details1.get_properties(&properties).join(",\n");
let env2 = details2.get_properties(&properties).join(",\n");
if !details1.error.is_empty() {
errors.push(format_param_error(&param_name, &details1.error))
}
if added {
table.render(fmt)?;
} else {
println!("No parameters or differences in compared properties found.");
// NOTE: do not put redundant errors on the list, but the errors could be due to
// different FQNs
if !details2.error.is_empty() && details1.error != details2.error {
errors.push(format_param_error(&param_name, &details2.error))
}
if env1 != env2 {
table.add_row(vec![param_name, env1, env2]);
added = true;
}
warn_unresolved_params(&errors)?;
}
if added {
table.render(fmt)?;
} else {
println!("No parameters or differences in compared properties found.");
}
warn_unresolved_params(&errors)?;
Ok(())
}

Expand Down
9 changes: 3 additions & 6 deletions tests/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -284,23 +284,20 @@ cloudtruth-parameters-differences
Show differences between properties from two environments

USAGE:
cloudtruth parameters differences [FLAGS] [OPTIONS] <ENV1> <ENV2>
cloudtruth parameters differences [FLAGS] [OPTIONS]

FLAGS:
-h, --help Prints help information
-s, --secrets Show secret values
-V, --version Prints version information

OPTIONS:
--as-of <datetime> Date/time of parameter value(s)
-e, --env <ENV>... Up to two environment(s) to be compared.
--as-of <datetime>... Up to two times to be compared
-f, --format <format> Display difference format [default: table] [possible values: table, csv, json,
yaml]
-p, --property <properties>... List of the properties to compare. [default: value] [possible values: value,
environment, fqn, jmes-path, secret, created-at, modified-at]

ARGS:
<ENV1> Name of first environment for comparison.
<ENV2> Name of second environment for comparison.
========================================
cloudtruth-parameters-environment
Shows values across environments
Expand Down
1 change: 1 addition & 0 deletions tests/pytest/live_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def live_test(*args):
env[CT_TEST_LOG_COMMANDS] = str(int(args.log_commands))
env[CT_TEST_LOG_OUTPUT] = str(int(args.log_output))
if args.job_id:
print(f"JOB_ID: {args.job_id}")
env[CT_TEST_JOB_ID] = args.job_id

cli = get_cli_base_cmd()
Expand Down
81 changes: 76 additions & 5 deletions tests/pytest/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,7 +1141,7 @@ def test_parameter_diff(self):
self.set_param(cmd_env, proj_name, param2, value2a, env=env_a, secret=True)

# first set of comparisons
diff_cmd = sub_cmd + f"diff '{env_a}' '{env_b}' -f csv "
diff_cmd = sub_cmd + f"diff -e '{env_a}' --env '{env_b}' -f csv "
result = self.run_cli(cmd_env, diff_cmd)
self.assertEqual(result.return_value, 0)
self.assertEqual(result.out(), f"""\
Expand Down Expand Up @@ -1200,7 +1200,7 @@ def split_time_strings(value: str) -> Tuple:
return value.replace("\n", "").replace("\'", "").replace("Z", "").split(",")

# check that we get back timestamp properties
diff_json_cmd = sub_cmd + f"diff '{env_a}' '{env_b}' -f json "
diff_json_cmd = sub_cmd + f"diff -e '{env_a}' -e '{env_b}' -f json "
result = self.run_cli(cmd_env, diff_json_cmd + "-p created-at --property modified-at")
self.assertEqual(result.return_value, 0)
output = eval(result.out())
Expand All @@ -1227,24 +1227,95 @@ def split_time_strings(value: str) -> Tuple:
result = self.run_cli(cmd_env, diff_cmd + "-s --property fqn")
self.assertIn("No parameters or differences in compared properties found", result.out())

#####################
# Time diff
diff_csv = sub_cmd + "diff -f csv "

# test single time
result = self.run_cli(cmd_env, diff_csv + f"--as-of '{modified_a}'")
self.assertEqual(result.return_value, 0)
self.assertEqual(result.out(), f"""\
Parameter,Current,{modified_a}
{param1},{value1d},-
{param2},{REDACTED},-
""")

# compare 2 points in time (with secrets)
result = self.run_cli(cmd_env, diff_csv + f"-s --as-of '{modified_a}' --as-of '{modified_b}'")
self.assertEqual(result.return_value, 0)
self.assertEqual(result.out(), f"""\
Parameter,{modified_a},{modified_b}
{param1},-,{value1d}
{param2},-,{value2d}
""")

# compare 2 points in time where there are no differences
result = self.run_cli(cmd_env, diff_csv + f"--as-of '{created_a}' --as-of '{modified_a}'")
self.assertEqual(result.return_value, 0)
self.assertIn("No parameters or differences in compared properties found", result.out())

#####################
# Combination environment/time diff

# if just one env/time, it applies to the right hand side
result = self.run_cli(cmd_env, diff_csv + f"-e '{env_a}' --as-of '{modified_a}'")
self.assertEqual(result.return_value, 0)
self.assertEqual(result.out(), f"""\
Parameter,default,{env_a} ({modified_a})
{param1},{value1d},{value1a}
""")

# the full set of environments/times (with secrets)
cmd = diff_csv + f"-e '{env_a}' --as-of '{modified_a}' -e '{env_b}' --as-of '{modified_b}' -s"
result = self.run_cli(cmd_env, cmd)
self.assertEqual(result.return_value, 0)
self.assertEqual(result.out(), f"""\
Parameter,{env_a} ({modified_a}),{env_b} ({modified_b})
{param1},{value1a},{same}
{param2},{value2a},{value2b}
""")

#####################
# Error cases

# no comparing to yourself
result = self.run_cli(cmd_env, sub_cmd + f"difference '{env_a}' '{env_a}'")
result = self.run_cli(cmd_env, sub_cmd + "difference")
self.assertEqual(result.return_value, 0)
self.assertIn("Invalid comparing an environment to itself", result.err())

matched_envs = f"-e '{env_a}' " * 2
result = self.run_cli(cmd_env, sub_cmd + f"difference {matched_envs}")
self.assertEqual(result.return_value, 0)
self.assertIn("Invalid comparing an environment to itself", result.err())

matched_times = "--as-of 2021-08-27 " * 2
result = self.run_cli(cmd_env, sub_cmd + f"difference {matched_times}")
self.assertEqual(result.return_value, 0)
self.assertIn("Invalid comparing an environment to itself", result.err())

result = self.run_cli(cmd_env, sub_cmd + f"difference {matched_times} {matched_envs}")
self.assertEqual(result.return_value, 0)
self.assertIn("Invalid comparing an environment to itself", result.err())

# first environment DNE
result = self.run_cli(cmd_env, sub_cmd + "differ 'charlie-foxtrot' '{env_b}'")
result = self.run_cli(cmd_env, sub_cmd + "differ -e 'charlie-foxtrot' -e '{env_b}'")
self.assertNotEqual(result.return_value, 0)
self.assertIn("Did not find environment 'charlie-foxtrot'", result.err())

# second environment DNE
result = self.run_cli(cmd_env, sub_cmd + f"differences '{env_a}' 'missing'")
result = self.run_cli(cmd_env, sub_cmd + f"differences -e '{env_a}' -e 'missing'")
self.assertNotEqual(result.return_value, 0)
self.assertIn("Did not find environment 'missing'", result.err())

# too many specified
result = self.run_cli(cmd_env, sub_cmd + "diff -e env1 --env env2 -e env3")
self.assertEqual(result.return_value, 0)
self.assertIn("Can specify a maximum of 2 environment values", result.err())

result = self.run_cli(cmd_env, sub_cmd + "diff --as-of 2021-08-01 --as-of 2021-08-02 --as-of 2021-08-03")
self.assertEqual(result.return_value, 0)
self.assertIn("Can specify a maximum of 2 as-of values", result.err())

# cleanup
self.delete_environment(cmd_env, env_a)
self.delete_environment(cmd_env, env_b)
Expand Down

0 comments on commit 6c65140

Please sign in to comment.