Skip to content

Commit

Permalink
First pass at parameter history, part 2
Browse files Browse the repository at this point in the history
  • Loading branch information
rickporter-tuono committed Dec 7, 2021
1 parent 8367f24 commit e7b4891
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 7 deletions.
6 changes: 6 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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};
Expand Down
73 changes: 73 additions & 0 deletions src/database/parameter_history.rs
Original file line number Diff line number Diff line change
@@ -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<ParameterTimelineEntryEnvironment> = 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()
}
}
54 changes: 52 additions & 2 deletions src/database/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -766,4 +766,54 @@ impl Parameters {
}
Ok(total)
}

pub fn get_histories(
&self,
rest_cfg: &OpenApiConfig,
proj_id: &str,
as_of: Option<String>,
tag: Option<String>,
) -> Result<Vec<ParameterHistory>, 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<String>,
tag: Option<String>,
) -> Result<Vec<ParameterHistory>, 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())),
}
}
}
131 changes: 126 additions & 5 deletions src/parameters.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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,
Expand Down Expand Up @@ -1067,6 +1072,120 @@ fn proc_param_push(
Ok(())
}

pub fn get_changes(
current: &ParameterHistory,
previous: Option<ParameterHistory>,
properties: &[&str],
) -> Vec<String> {
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<ParameterHistory> {
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<ParameterHistory>;

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, &param_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,
Expand All @@ -1092,6 +1211,8 @@ pub fn process_parameters_command(
proc_param_env(subcmd_args, rest_cfg, &parameters, resolved)?;
} else if let Some(subcmd_args) = subcmd_args.subcommand_matches(PUSH_SUBCMD) {
proc_param_push(subcmd_args, rest_cfg, &parameters, resolved)?;
} else if let Some(subcmd_args) = subcmd_args.subcommand_matches(HISTORY_SUBCMD) {
proc_param_history(subcmd_args, rest_cfg, &parameters, resolved)?;
} else {
warn_missing_subcommand("parameters");
}
Expand Down
19 changes: 19 additions & 0 deletions tests/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -874,6 +875,24 @@ OPTIONS:
ARGS:
<KEY> 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 <datetime|tag> Date/time (or tag) for parameter history
-f, --format <format> Format for parameter history output [default: table] [possible values: table, csv,
json, yaml]

ARGS:
<KEY> Parameter name (optional)
========================================
cloudtruth-parameters-list
List CloudTruth parameters

Expand Down

0 comments on commit e7b4891

Please sign in to comment.