Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] First pass at parameter history, part 2 #283

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -86,6 +87,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, ParameterRuleDetail};
pub use parameters::{ParameterDetailMap, Parameters};
pub use project_details::ProjectDetails;
Expand Down
80 changes: 80 additions & 0 deletions src/database/parameter_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::database::HistoryAction;
use cloudtruth_restapi::models::{
ParameterTimelineEntry, ParameterTimelineEntryEnvironment, ParameterTimelineEntryParameter,
};
use once_cell::sync::OnceCell;

static DEFAULT_ENV_HISTORY: OnceCell<ParameterTimelineEntryEnvironment> = OnceCell::new();
static DEFAULT_PARAM_HISTORY: OnceCell<ParameterTimelineEntryParameter> = 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::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 {
fn from(api: &ParameterTimelineEntry) -> Self {
let first = api.history_environments.first();
let env_hist: &ParameterTimelineEntryEnvironment = match first {
Some(v) => v,
_ => default_environment_history(),
};
let param_hist = match &api.history_parameter {
Some(p) => &*p,
_ => default_parameter_history(),
};

Self {
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.clone().unwrap_or_default()),
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()
}
}
53 changes: 52 additions & 1 deletion src/database/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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;
Expand Down Expand Up @@ -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<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())),
}
}
}
141 changes: 131 additions & 10 deletions src/parameters.rs
Original file line number Diff line number Diff line change
@@ -1,15 +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,
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, OpenApiConfig, ParamExportFormat, ParamExportOptions,
ParamRuleType, ParameterDetails, ParameterError, Parameters, Projects, ResolvedDetails,
TaskStepDetails,
EnvironmentDetails, Environments, HistoryAction, OpenApiConfig, ParamExportFormat,
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,
Expand All @@ -28,6 +28,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 @@ -1157,13 +1162,13 @@ fn proc_param_drift(
parameters: &Parameters,
resolved: &ResolvedDetails,
) -> Result<()> {
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 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 param_map =
parameters.get_parameter_detail_map(rest_cfg, proj_id, env_id, false, as_of, tag)?;
let excludes = vec![
Expand Down Expand Up @@ -1253,6 +1258,120 @@ fn proc_param_drift(
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 Down Expand Up @@ -1280,6 +1399,8 @@ pub fn process_parameters_command(
proc_param_push(subcmd_args, rest_cfg, &parameters, resolved)?;
} else if let Some(subcmd_args) = subcmd_args.subcommand_matches("drift") {
proc_param_drift(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 @@ -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
Expand Down Expand Up @@ -1098,6 +1099,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