From 2a60991435e3b5526da442109e77a8747e516ec9 Mon Sep 17 00:00:00 2001 From: Rick Porter Date: Mon, 22 Nov 2021 09:19:22 -0500 Subject: [PATCH 1/2] First pass at parameter history, part 2 --- src/cli.rs | 6 ++ src/database/mod.rs | 3 + src/database/parameter_history.rs | 73 ++++++++++++++++ src/database/parameters.rs | 53 +++++++++++- src/parameters.rs | 139 +++++++++++++++++++++++++++--- tests/help.txt | 19 ++++ 6 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 src/database/parameter_history.rs diff --git a/src/cli.rs b/src/cli.rs index 4b913685..facf10fd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -707,6 +707,12 @@ pub fn build_cli() -> App<'static, 'static> { .long("details") .help("Show all parameter details")) .arg(key_arg().help("Name of parameter to get")), + SubCommand::with_name(HISTORY_SUBCMD) + .visible_aliases(HISTORY_ALIASES) + .arg(key_arg().help("Parameter name (optional)").required(false)) + .arg(as_of_arg().help("Date/time (or tag) for parameter history")) + .arg(table_format_options().help("Format for parameter history output")) + .about("View parameter history"), SubCommand::with_name(LIST_SUBCMD) .visible_aliases(LIST_ALIASES) .about("List CloudTruth parameters") diff --git a/src/database/mod.rs b/src/database/mod.rs index 62ae8fef..1f537b12 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -29,6 +29,7 @@ mod openapi; mod parameter_details; mod parameter_error; mod parameter_export; +mod parameter_history; mod parameter_rules; mod parameters; mod project_details; @@ -86,7 +87,9 @@ pub use openapi::{ pub use parameter_details::ParameterDetails; pub use parameter_error::ParameterError; pub use parameter_export::{ParamExportFormat, ParamExportOptions}; +pub use parameter_history::ParameterHistory; pub use parameter_rules::{ParamRuleType, ParameterRuleDetail}; +pub use parameter_types::ParamType; pub use parameters::{ParameterDetailMap, Parameters}; pub use project_details::ProjectDetails; pub use project_error::ProjectError; diff --git a/src/database/parameter_history.rs b/src/database/parameter_history.rs new file mode 100644 index 00000000..19afa7a9 --- /dev/null +++ b/src/database/parameter_history.rs @@ -0,0 +1,73 @@ +use crate::database::HistoryAction; +use cloudtruth_restapi::models::{ParameterTimelineEntry, ParameterTimelineEntryEnvironment}; +use once_cell::sync::OnceCell; +use std::ops::Deref; + +static DEFAULT_ENV_HISTORY: OnceCell = OnceCell::new(); + +#[derive(Clone, Debug)] +pub struct ParameterHistory { + pub id: String, + pub name: String, + + // TODO: can we get description, value, rules, FQN, jmes_path?? + pub env_name: String, + + // these are from the timeline + pub date: String, + pub change_type: HistoryAction, + pub user: String, +} + +/// Gets the singleton default History +fn default_environment_history() -> &'static ParameterTimelineEntryEnvironment { + DEFAULT_ENV_HISTORY.get_or_init(|| ParameterTimelineEntryEnvironment { + id: "".to_string(), + name: "".to_string(), + _override: false, + }) +} + +impl From<&ParameterTimelineEntry> for ParameterHistory { + fn from(api: &ParameterTimelineEntry) -> Self { + let first = api.history_environments.first(); + let env_hist: &ParameterTimelineEntryEnvironment = match first { + Some(v) => v, + _ => default_environment_history(), + }; + + Self { + id: api.history_parameter.id.clone(), + name: api.history_parameter.name.clone(), + + env_name: env_hist.name.clone(), + + date: api.history_date.clone(), + change_type: HistoryAction::from(*api.history_type.deref()), + user: api.history_user.clone().unwrap_or_default(), + } + } +} + +impl ParameterHistory { + pub fn get_property(&self, name: &str) -> String { + match name { + "name" => self.name.clone(), + "environment" => self.env_name.clone(), + // TODO: add more here once available + x => format!("Unhandled property: {}", x), + } + } + + pub fn get_id(&self) -> String { + self.id.clone() + } + + pub fn get_date(&self) -> String { + self.date.clone() + } + + pub fn get_action(&self) -> HistoryAction { + self.change_type.clone() + } +} diff --git a/src/database/parameters.rs b/src/database/parameters.rs index 84789336..c758be78 100644 --- a/src/database/parameters.rs +++ b/src/database/parameters.rs @@ -2,7 +2,8 @@ use crate::database::openapi::key_from_config; use crate::database::{ extract_details, extract_from_json, page_size, response_message, secret_encode_wrap, secret_unwrap_decode, CryptoAlgorithm, OpenApiConfig, ParamExportOptions, ParamRuleType, - ParameterDetails, ParameterError, TaskStepDetails, NO_PAGE_COUNT, NO_PAGE_SIZE, WRAP_SECRETS, + ParamType, ParameterDetails, ParameterError, ParameterHistory, TaskStep, TaskStepDetails, + NO_PAGE_COUNT, NO_PAGE_SIZE, WRAP_SECRETS, }; use cloudtruth_restapi::apis::projects_api::*; use cloudtruth_restapi::apis::utils_api::utils_generate_password_create; @@ -826,4 +827,54 @@ impl Parameters { Err(e) => Err(ParameterError::UnhandledError(e.to_string())), } } + + pub fn get_histories( + &self, + rest_cfg: &OpenApiConfig, + proj_id: &str, + as_of: Option, + tag: Option, + ) -> Result, ParameterError> { + let response = + projects_parameters_timelines_retrieve(rest_cfg, proj_id, as_of, tag.as_deref()); + match response { + Ok(timeline) => Ok(timeline + .results + .iter() + .map(ParameterHistory::from) + .collect()), + Err(ResponseError(ref content)) => { + Err(response_error(&content.status, &content.content)) + } + Err(e) => Err(ParameterError::UnhandledError(e.to_string())), + } + } + + pub fn get_history_for( + &self, + rest_cfg: &OpenApiConfig, + proj_id: &str, + param_id: &str, + as_of: Option, + tag: Option, + ) -> Result, ParameterError> { + let response = projects_parameters_timeline_retrieve( + rest_cfg, + param_id, + proj_id, + as_of, + tag.as_deref(), + ); + match response { + Ok(timeline) => Ok(timeline + .results + .iter() + .map(ParameterHistory::from) + .collect()), + Err(ResponseError(ref content)) => { + Err(response_error(&content.status, &content.content)) + } + Err(e) => Err(ParameterError::UnhandledError(e.to_string())), + } + } } diff --git a/src/parameters.rs b/src/parameters.rs index 16e3b405..9e46556b 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -1,15 +1,9 @@ -use crate::cli::{ - binary_name, show_values, true_false_option, AS_OF_ARG, CONFIRM_FLAG, DELETE_SUBCMD, - DESCRIPTION_OPT, DIFF_SUBCMD, FORMAT_OPT, GET_SUBCMD, JMES_PATH_ARG, KEY_ARG, LIST_SUBCMD, - PUSH_SUBCMD, RENAME_OPT, RULE_MAX_ARG, RULE_MAX_LEN_ARG, RULE_MIN_ARG, RULE_MIN_LEN_ARG, - RULE_NO_MAX_ARG, RULE_NO_MAX_LEN_ARG, RULE_NO_MIN_ARG, RULE_NO_MIN_LEN_ARG, RULE_NO_REGEX_ARG, - RULE_REGEX_ARG, SECRETS_FLAG, SET_SUBCMD, SHOW_TIMES_FLAG, -}; +use crate::cli::{binary_name, show_values, true_false_option, AS_OF_ARG, CONFIRM_FLAG, DELETE_SUBCMD, DESCRIPTION_OPT, DIFF_SUBCMD, FORMAT_OPT, GET_SUBCMD, JMES_PATH_ARG, KEY_ARG, LIST_SUBCMD, PUSH_SUBCMD, RENAME_OPT, RULE_MAX_ARG, RULE_MAX_LEN_ARG, RULE_MIN_ARG, RULE_MIN_LEN_ARG, RULE_NO_MAX_ARG, RULE_NO_MAX_LEN_ARG, RULE_NO_MIN_ARG, RULE_NO_MIN_LEN_ARG, RULE_NO_REGEX_ARG, RULE_REGEX_ARG, SECRETS_FLAG, SET_SUBCMD, SHOW_TIMES_FLAG, HISTORY_SUBCMD}; use crate::config::DEFAULT_ENV_NAME; use crate::database::{ - EnvironmentDetails, Environments, OpenApiConfig, ParamExportFormat, ParamExportOptions, - ParamRuleType, ParameterDetails, ParameterError, Parameters, Projects, ResolvedDetails, - TaskStepDetails, + EnvironmentDetails, Environments, HistoryAction, OpenApiConfig, ParamExportFormat, + ParamExportOptions, ParamRuleType, ParamType, ParameterDetails, ParameterError, + ParameterHistory, Parameters, Projects, ResolvedDetails, TaskStepDetails, }; use crate::lib::{ error_message, format_param_error, help_message, parse_datetime, parse_tag, user_confirm, @@ -28,6 +22,11 @@ use std::fs; use std::process; use std::str::FromStr; +const PARAMETER_HISTORY_PROPERTIES: &[&str] = &[ + "name", + "environment", // "value", "description", "fqn", "jmes-path" +]; + fn proc_param_delete( subcmd_args: &ArgMatches, rest_cfg: &OpenApiConfig, @@ -1157,10 +1156,6 @@ fn proc_param_drift( parameters: &Parameters, resolved: &ResolvedDetails, ) -> Result<()> { - let proj_id = resolved.project_id(); - let env_id = resolved.environment_id(); - let as_of = parse_datetime(subcmd_args.value_of(AS_OF_ARG)); - let tag = parse_tag(subcmd_args.value_of(AS_OF_ARG)); let show_secrets = subcmd_args.is_present(SECRETS_FLAG); let show_values = show_values(subcmd_args); let fmt = subcmd_args.value_of(FORMAT_OPT).unwrap(); @@ -1253,6 +1248,120 @@ fn proc_param_drift( Ok(()) } +pub fn get_changes( + current: &ParameterHistory, + previous: Option, + properties: &[&str], +) -> Vec { + let mut changes = vec![]; + if let Some(prev) = previous { + if current.get_action() != HistoryAction::Delete { + for prop in properties { + let curr_value = current.get_property(prop); + if prev.get_property(prop) != curr_value { + changes.push(format!("{}: {}", prop, curr_value)) + } + } + } + } else { + // NOTE: print this info even on a delete, if there's nothing earlier + for prop in properties { + let curr_value = current.get_property(prop); + if !curr_value.is_empty() { + changes.push(format!("{}: {}", prop, curr_value)) + } + } + } + changes +} + +pub fn find_previous( + history: &[ParameterHistory], + current: &ParameterHistory, +) -> Option { + let mut found = None; + let curr_id = current.get_id(); + let curr_date = current.get_date(); + for entry in history { + if entry.get_id() == curr_id && entry.get_date() < curr_date { + found = Some(entry.clone()) + } + } + found +} + +fn proc_param_history( + subcmd_args: &ArgMatches, + rest_cfg: &OpenApiConfig, + parameters: &Parameters, + resolved: &ResolvedDetails, +) -> Result<()> { + let proj_name = resolved.project_display_name(); + let proj_id = resolved.project_id(); + let env_id = resolved.environment_id(); + let as_of = parse_datetime(subcmd_args.value_of(AS_OF_ARG)); + let tag = parse_tag(subcmd_args.value_of(AS_OF_ARG)); + let key_name = subcmd_args.value_of(KEY_ARG); + let fmt = subcmd_args.value_of(FORMAT_OPT).unwrap(); + let modifier; + let add_name; + let history: Vec; + + if let Some(param_name) = key_name { + let param_id; + modifier = format!("for '{}' ", param_name); + add_name = false; + if let Some(details) = parameters.get_details_by_name( + rest_cfg, proj_id, env_id, param_name, false, true, None, None, + )? { + param_id = details.id; + } else { + error_message(format!( + "Did not find parameter '{}' in project '{}'", + param_name, proj_name + )); + process::exit(13); + } + history = parameters.get_history_for(rest_cfg, proj_id, ¶m_id, as_of, tag)?; + } else { + modifier = "".to_string(); + add_name = true; + history = parameters.get_histories(rest_cfg, proj_id, as_of, tag)?; + }; + + if history.is_empty() { + println!( + "No parameter history {}in project '{}'.", + modifier, proj_name + ); + } else { + let name_index = 2; + let mut table = Table::new("parameter-history"); + let mut hdr: Vec<&str> = vec!["Date", "Action", "Changes"]; + if add_name { + hdr.insert(name_index, "Name"); + } + table.set_header(&hdr); + + let orig_list = history.clone(); + for ref entry in history { + let prev = find_previous(&orig_list, entry); + let changes = get_changes(entry, prev, PARAMETER_HISTORY_PROPERTIES); + let mut row = vec![ + entry.date.clone(), + entry.change_type.to_string(), + changes.join("\n"), + ]; + if add_name { + row.insert(name_index, entry.name.clone()) + } + table.add_row(row); + } + table.render(fmt)?; + } + Ok(()) +} + /// Process the 'parameters' sub-command pub fn process_parameters_command( subcmd_args: &ArgMatches, @@ -1280,6 +1389,8 @@ pub fn process_parameters_command( proc_param_push(subcmd_args, rest_cfg, ¶meters, resolved)?; } else if let Some(subcmd_args) = subcmd_args.subcommand_matches("drift") { proc_param_drift(subcmd_args, rest_cfg, ¶meters, resolved)?; + } else if let Some(subcmd_args) = subcmd_args.subcommand_matches(HISTORY_SUBCMD) { + proc_param_history(subcmd_args, rest_cfg, ¶meters, resolved)?; } else { warn_missing_subcommand("parameters"); } diff --git a/tests/help.txt b/tests/help.txt index 8cadc808..105df461 100644 --- a/tests/help.txt +++ b/tests/help.txt @@ -982,6 +982,7 @@ SUBCOMMANDS: [aliases: expo, exp, ex] get Gets value for parameter in the selected environment help Prints this message or the help of the given subcommand(s) + history View parameter history [aliases: hist, h] list List CloudTruth parameters [aliases: ls, l] pushes Show push task steps for parameters [aliases: push, pu, p] set Set a value in the selected project/environment for an existing parameter or creates a new one if @@ -1098,6 +1099,24 @@ OPTIONS: ARGS: Name of parameter to get ======================================== +cloudtruth-parameters-history +View parameter history + +USAGE: + cloudtruth parameters history [OPTIONS] [KEY] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + --as-of Date/time (or tag) for parameter history + -f, --format Format for parameter history output [default: table] [possible values: table, csv, + json, yaml] + +ARGS: + Parameter name (optional) +======================================== cloudtruth-parameters-list List CloudTruth parameters From c5bcafd7fa4594eeb96993c05367f24e495cc9f8 Mon Sep 17 00:00:00 2001 From: Rick Porter Date: Thu, 27 Jan 2022 09:32:40 -0500 Subject: [PATCH 2/2] Fix latest merge --- src/database/mod.rs | 1 - src/database/parameter_history.rs | 27 +++++++++++++++++---------- src/database/parameters.rs | 4 ++-- src/parameters.rs | 16 +++++++++++++--- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 1f537b12..b5031e9b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -89,7 +89,6 @@ pub use parameter_error::ParameterError; pub use parameter_export::{ParamExportFormat, ParamExportOptions}; pub use parameter_history::ParameterHistory; pub use parameter_rules::{ParamRuleType, ParameterRuleDetail}; -pub use parameter_types::ParamType; pub use parameters::{ParameterDetailMap, Parameters}; pub use project_details::ProjectDetails; pub use project_error::ProjectError; diff --git a/src/database/parameter_history.rs b/src/database/parameter_history.rs index 19afa7a9..3914b636 100644 --- a/src/database/parameter_history.rs +++ b/src/database/parameter_history.rs @@ -1,9 +1,11 @@ use crate::database::HistoryAction; -use cloudtruth_restapi::models::{ParameterTimelineEntry, ParameterTimelineEntryEnvironment}; +use cloudtruth_restapi::models::{ + ParameterTimelineEntry, ParameterTimelineEntryEnvironment, ParameterTimelineEntryParameter, +}; use once_cell::sync::OnceCell; -use std::ops::Deref; static DEFAULT_ENV_HISTORY: OnceCell = OnceCell::new(); +static DEFAULT_PARAM_HISTORY: OnceCell = OnceCell::new(); #[derive(Clone, Debug)] pub struct ParameterHistory { @@ -21,11 +23,12 @@ pub struct ParameterHistory { /// Gets the singleton default History fn default_environment_history() -> &'static ParameterTimelineEntryEnvironment { - DEFAULT_ENV_HISTORY.get_or_init(|| ParameterTimelineEntryEnvironment { - id: "".to_string(), - name: "".to_string(), - _override: false, - }) + DEFAULT_ENV_HISTORY.get_or_init(ParameterTimelineEntryEnvironment::default) +} + +/// Gets the singleton default History +fn default_parameter_history() -> &'static ParameterTimelineEntryParameter { + DEFAULT_PARAM_HISTORY.get_or_init(ParameterTimelineEntryParameter::default) } impl From<&ParameterTimelineEntry> for ParameterHistory { @@ -35,15 +38,19 @@ impl From<&ParameterTimelineEntry> for ParameterHistory { Some(v) => v, _ => default_environment_history(), }; + let param_hist = match &api.history_parameter { + Some(p) => &*p, + _ => default_parameter_history(), + }; Self { - id: api.history_parameter.id.clone(), - name: api.history_parameter.name.clone(), + id: param_hist.id.clone(), + name: param_hist.name.clone(), env_name: env_hist.name.clone(), date: api.history_date.clone(), - change_type: HistoryAction::from(*api.history_type.deref()), + change_type: HistoryAction::from(*api.history_type.clone().unwrap_or_default()), user: api.history_user.clone().unwrap_or_default(), } } diff --git a/src/database/parameters.rs b/src/database/parameters.rs index c758be78..cfcfe6d2 100644 --- a/src/database/parameters.rs +++ b/src/database/parameters.rs @@ -2,8 +2,8 @@ use crate::database::openapi::key_from_config; use crate::database::{ extract_details, extract_from_json, page_size, response_message, secret_encode_wrap, secret_unwrap_decode, CryptoAlgorithm, OpenApiConfig, ParamExportOptions, ParamRuleType, - ParamType, ParameterDetails, ParameterError, ParameterHistory, TaskStep, TaskStepDetails, - NO_PAGE_COUNT, NO_PAGE_SIZE, WRAP_SECRETS, + ParameterDetails, ParameterError, ParameterHistory, TaskStepDetails, NO_PAGE_COUNT, + NO_PAGE_SIZE, WRAP_SECRETS, }; use cloudtruth_restapi::apis::projects_api::*; use cloudtruth_restapi::apis::utils_api::utils_generate_password_create; diff --git a/src/parameters.rs b/src/parameters.rs index 9e46556b..2a9168b2 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -1,9 +1,15 @@ -use crate::cli::{binary_name, show_values, true_false_option, AS_OF_ARG, CONFIRM_FLAG, DELETE_SUBCMD, DESCRIPTION_OPT, DIFF_SUBCMD, FORMAT_OPT, GET_SUBCMD, JMES_PATH_ARG, KEY_ARG, LIST_SUBCMD, PUSH_SUBCMD, RENAME_OPT, RULE_MAX_ARG, RULE_MAX_LEN_ARG, RULE_MIN_ARG, RULE_MIN_LEN_ARG, RULE_NO_MAX_ARG, RULE_NO_MAX_LEN_ARG, RULE_NO_MIN_ARG, RULE_NO_MIN_LEN_ARG, RULE_NO_REGEX_ARG, RULE_REGEX_ARG, SECRETS_FLAG, SET_SUBCMD, SHOW_TIMES_FLAG, HISTORY_SUBCMD}; +use crate::cli::{ + binary_name, show_values, true_false_option, AS_OF_ARG, CONFIRM_FLAG, DELETE_SUBCMD, + DESCRIPTION_OPT, DIFF_SUBCMD, FORMAT_OPT, GET_SUBCMD, HISTORY_SUBCMD, JMES_PATH_ARG, KEY_ARG, + LIST_SUBCMD, PUSH_SUBCMD, RENAME_OPT, RULE_MAX_ARG, RULE_MAX_LEN_ARG, RULE_MIN_ARG, + RULE_MIN_LEN_ARG, RULE_NO_MAX_ARG, RULE_NO_MAX_LEN_ARG, RULE_NO_MIN_ARG, RULE_NO_MIN_LEN_ARG, + RULE_NO_REGEX_ARG, RULE_REGEX_ARG, SECRETS_FLAG, SET_SUBCMD, SHOW_TIMES_FLAG, +}; use crate::config::DEFAULT_ENV_NAME; use crate::database::{ EnvironmentDetails, Environments, HistoryAction, OpenApiConfig, ParamExportFormat, - ParamExportOptions, ParamRuleType, ParamType, ParameterDetails, ParameterError, - ParameterHistory, Parameters, Projects, ResolvedDetails, TaskStepDetails, + ParamExportOptions, ParamRuleType, ParameterDetails, ParameterError, ParameterHistory, + Parameters, Projects, ResolvedDetails, TaskStepDetails, }; use crate::lib::{ error_message, format_param_error, help_message, parse_datetime, parse_tag, user_confirm, @@ -1159,6 +1165,10 @@ fn proc_param_drift( let show_secrets = subcmd_args.is_present(SECRETS_FLAG); let show_values = show_values(subcmd_args); let fmt = subcmd_args.value_of(FORMAT_OPT).unwrap(); + let proj_id = resolved.project_id(); + let env_id = resolved.environment_id(); + let as_of = parse_datetime(subcmd_args.value_of(AS_OF_ARG)); + let tag = parse_tag(subcmd_args.value_of(AS_OF_ARG)); let param_map = parameters.get_parameter_detail_map(rest_cfg, proj_id, env_id, false, as_of, tag)?; let excludes = vec![