From e7b489103707b96cfd5fb34602a042d1008640f4 Mon Sep 17 00:00:00 2001 From: Rick Porter Date: Mon, 22 Nov 2021 09:19:22 -0500 Subject: [PATCH] First pass at parameter history, part 2 --- src/cli.rs | 6 ++ src/database/mod.rs | 2 + src/database/parameter_history.rs | 73 +++++++++++++++++ src/database/parameters.rs | 54 +++++++++++- src/parameters.rs | 131 ++++++++++++++++++++++++++++-- tests/help.txt | 19 +++++ 6 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 src/database/parameter_history.rs diff --git a/src/cli.rs b/src/cli.rs index bf5b3d1c6..5b0287d89 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -583,6 +583,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 8d09f2507..0a68d6de1 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -24,6 +24,7 @@ mod openapi; mod parameter_details; mod parameter_error; mod parameter_export; +mod parameter_history; mod parameter_rules; mod parameter_types; mod parameters; @@ -74,6 +75,7 @@ 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, ParameterDetailRule}; pub use parameter_types::ParamType; pub use parameters::{ParameterDetailMap, Parameters}; diff --git a/src/database/parameter_history.rs b/src/database/parameter_history.rs new file mode 100644 index 000000000..19afa7a9f --- /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 0fc1b68e4..11efa5e4a 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, TaskStep, NO_PAGE_COUNT, NO_PAGE_SIZE, - WRAP_SECRETS, + ParamType, ParameterDetails, ParameterError, ParameterHistory, TaskStep, NO_PAGE_COUNT, + NO_PAGE_SIZE, WRAP_SECRETS, }; use cloudtruth_restapi::apis::projects_api::*; use cloudtruth_restapi::apis::Error::ResponseError; @@ -766,4 +766,54 @@ impl Parameters { } Ok(total) } + + 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 662b352e1..ce3e151dd 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -1,13 +1,13 @@ 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, KEY_ARG, LIST_SUBCMD, PUSH_SUBCMD, - RENAME_OPT, SECRETS_FLAG, SET_SUBCMD, SHOW_TIMES_FLAG, + DESCRIPTION_OPT, DIFF_SUBCMD, FORMAT_OPT, GET_SUBCMD, HISTORY_SUBCMD, KEY_ARG, LIST_SUBCMD, + PUSH_SUBCMD, RENAME_OPT, SECRETS_FLAG, SET_SUBCMD, SHOW_TIMES_FLAG, }; use crate::config::DEFAULT_ENV_NAME; use crate::database::{ - EnvironmentDetails, Environments, OpenApiConfig, ParamExportFormat, ParamExportOptions, - ParamRuleType, ParamType, ParameterDetails, ParameterError, Parameters, Projects, - ResolvedDetails, TaskStep, + EnvironmentDetails, Environments, HistoryAction, OpenApiConfig, ParamExportFormat, + ParamExportOptions, ParamRuleType, ParamType, ParameterDetails, ParameterError, + ParameterHistory, Parameters, Projects, ResolvedDetails, TaskStep, }; use crate::table::Table; use crate::{ @@ -23,6 +23,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, @@ -1067,6 +1072,120 @@ fn proc_param_push( 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, @@ -1092,6 +1211,8 @@ pub fn process_parameters_command( proc_param_env(subcmd_args, rest_cfg, ¶meters, resolved)?; } else if let Some(subcmd_args) = subcmd_args.subcommand_matches(PUSH_SUBCMD) { proc_param_push(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 6fd9a2da4..dbf489ed3 100644 --- a/tests/help.txt +++ b/tests/help.txt @@ -774,6 +774,7 @@ SUBCOMMANDS: alphanumeric and underscore in key names. Formats available are: dotenv, docker, and shell. 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 @@ -874,6 +875,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