diff --git a/README.md b/README.md index d5f4e86..87a6d70 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,12 @@ pippo -c -p -e log save --service -p -e log tail --service --log ``` +### dry-run mode + +You can pass the flag `--dry-run` on the command line to preview the changes for +* environment variables +* pipeline variables + ### CI mode Since updating running pipelines or environments that are currently updating is not possible pippo will normally wait until it is possible. diff --git a/src/clap_app.rs b/src/clap_app.rs index fb60fce..edda328 100644 --- a/src/clap_app.rs +++ b/src/clap_app.rs @@ -12,7 +12,9 @@ use crate::client::CloudManagerClient; use crate::config::CloudManagerConfig; use crate::encryption::{decrypt, encrypt}; use crate::logs::{download_log, tail_log}; -use crate::models::{Domain, LogType, ServiceType}; +use crate::models::{ + Domain, EnvironmentVariableServiceType, LogType, PipelineVariableServiceType, ServiceType, +}; use crate::variables::{ get_env_vars, get_pipeline_vars, set_env_vars_from_file, set_pipeline_vars_from_file, }; @@ -71,7 +73,8 @@ pub async fn init_cli() { "🚀 Patching environment variables from input file {}\n", input ); - set_env_vars_from_file(input, &mut cm_client, cli.ci_mode).await; + set_env_vars_from_file(input, &mut cm_client, cli.ci_mode, cli.dry_run_mode) + .await; process::exit(0); } } @@ -94,6 +97,17 @@ pub async fn init_cli() { .await .unwrap(); println!("{}", serde_json::to_string_pretty(&env_vars).unwrap()); + if let Some(vf) = env_vars.variables.iter().find(|vf| { + vf.service == EnvironmentVariableServiceType::Invalid + }) { + eprintln!( + "{:>8} {} '{}: {}'", + "⚠".yellow(), + "WARN, invalid service type detected for variable".yellow(), + vf.name, + vf.service + ); + } } } else { eprintln!("❌ You have to provide a valid Cloud Manager environment ID to run this command!"); @@ -206,7 +220,13 @@ pub async fn init_cli() { { if let PipelineVarsCommands::Set { input } = &pipeline_vars_command { println!("🚀 Patching pipeline variables from input file {}\n", input); - set_pipeline_vars_from_file(input, &mut cm_client, cli.ci_mode).await; + set_pipeline_vars_from_file( + input, + &mut cm_client, + cli.ci_mode, + cli.dry_run_mode, + ) + .await; process::exit(0); } } @@ -276,10 +296,24 @@ pub async fn init_cli() { get_pipeline_vars(&mut cm_client, program_id, &pipeline_id) .await .unwrap(); + println!( "{}", serde_json::to_string_pretty(&pipeline_vars).unwrap() ); + if let Some(vf) = pipeline_vars + .variables + .iter() + .find(|vf| vf.service == PipelineVariableServiceType::Invalid) + { + eprintln!( + "{:>8} {} '{}: {}'", + "⚠".yellow(), + "WARN, invalid service type detected for variable".yellow(), + vf.name, + vf.service + ); + } } } else { eprintln!("❌ You have to provide a valid Cloud Manager pipeline ID to run this command!"); diff --git a/src/clap_models.rs b/src/clap_models.rs index 46e7c89..c608f9b 100644 --- a/src/clap_models.rs +++ b/src/clap_models.rs @@ -35,6 +35,10 @@ pub struct Cli { #[clap(long = "ci", global = true, action = ArgAction::SetTrue )] pub ci_mode: bool, + /// Only log but to not apply any changes + #[clap(long = "dry-run", global = true, action = ArgAction::SetTrue )] + pub dry_run_mode: bool, + #[clap(subcommand)] pub command: Option, } diff --git a/src/models.rs b/src/models.rs index e73e8b7..26b406a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,6 @@ use chrono::NaiveDate; use serde::{Deserialize, Serialize}; +use std::fmt; use strum_macros::{EnumString, IntoStaticStr}; // Common models used across multiple modules @@ -30,7 +31,7 @@ pub struct DomainConfig { #[derive(Debug, Deserialize, Serialize)] pub struct EnvironmentsConfig { pub id: u32, - pub variables: Vec, + pub variables: Vec, pub domains: Option>, } @@ -38,29 +39,105 @@ pub struct EnvironmentsConfig { #[derive(Debug, Deserialize, Serialize)] pub struct PipelinesConfig { pub id: u32, - pub variables: Vec, + pub variables: Vec, +} + +/// Model for common cloud manager variables + +/// Possible types that a variable can have +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum VariableType { + String, + SecretString, } /// Model for all information about a Cloud Manager environment variable #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Variable { +pub struct EnvironmentVariable { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub value: Option, #[serde(rename(deserialize = "type", serialize = "type"))] pub variable_type: VariableType, - #[serde(skip_serializing_if = "Option::is_none")] - pub service: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, + #[serde( + default = "EnvironmentVariableServiceType::default", + skip_serializing_if = "environment_variable_skip_serializing" + )] + pub service: EnvironmentVariableServiceType, } -/// Possible types that a variable can have +/// Possible service types that an environment variable can have +#[derive(Clone, Debug, Deserialize, Serialize, IntoStaticStr, EnumString, PartialEq, Eq)] +#[strum(serialize_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum EnvironmentVariableServiceType { + All, + Author, + Publish, + Preview, + #[serde(other)] + Invalid, +} + +impl fmt::Display for EnvironmentVariableServiceType { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "{}", + format!("{}", serde_json::to_string(self).unwrap().to_string()) + ) + } +} +fn environment_variable_skip_serializing(t: &EnvironmentVariableServiceType) -> bool { + *t == EnvironmentVariableServiceType::All +} + +impl EnvironmentVariableServiceType { + fn default() -> Self { + EnvironmentVariableServiceType::All + } +} + +/// Model for all information about a Cloud Manager pipeline variable +/// Model for all information about a Cloud Manager environment variable #[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PipelineVariable { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + #[serde(rename(deserialize = "type", serialize = "type"))] + pub variable_type: VariableType, + #[serde(default = "PipelineVariableServiceType::default")] + pub service: PipelineVariableServiceType, +} + +/// Possible service types that an pipeline variable can have +#[derive(Clone, Debug, Deserialize, Serialize, IntoStaticStr, EnumString, PartialEq, Eq)] +#[strum(serialize_all = "camelCase")] #[serde(rename_all = "camelCase")] -pub enum VariableType { - String, - SecretString, +pub enum PipelineVariableServiceType { + Build, + UiTest, + FunctionalTest, + #[serde(other)] + Invalid, +} + +impl fmt::Display for PipelineVariableServiceType { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "{}", + format!("{}", serde_json::to_string(self).unwrap().to_string()) + ) + } +} + +impl PipelineVariableServiceType { + fn default() -> Self { + PipelineVariableServiceType::Build + } } /// Model for the necessary JWT claims to retrieve an Adobe access token @@ -139,15 +216,28 @@ pub struct Environment { /// Struct to serialize the response of requesting /api/program/{id}/environment/{id}/variables #[derive(Debug, Deserialize, Serialize)] -pub struct VariablesResponse { +pub struct EnvironmentVariablesResponse { + #[serde(rename(deserialize = "_embedded", serialize = "_embedded"))] + pub variables_list: EnvironmentVariablesList, +} + +/// Struct to serialize the response of requesting /api/program/{id}/environment/{id}/variables +#[derive(Debug, Deserialize, Serialize)] +pub struct PipelineVariablesResponse { #[serde(rename(deserialize = "_embedded", serialize = "_embedded"))] - pub variables_list: VariablesList, + pub variables_list: PipelineVariablesList, +} + +/// Struct that holds a list of variables +#[derive(Debug, Deserialize, Serialize)] +pub struct EnvironmentVariablesList { + pub variables: Vec, } /// Struct that holds a list of variables #[derive(Debug, Deserialize, Serialize)] -pub struct VariablesList { - pub variables: Vec, +pub struct PipelineVariablesList { + pub variables: Vec, } // Models for representing Cloud Manager pipelines and descendant objects diff --git a/src/variables.rs b/src/variables.rs index 1e6e876..13df0d5 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -2,7 +2,11 @@ use crate::client::{AdobeConnector, CloudManagerClient}; use crate::encryption::decrypt; use crate::environments::get_environment; use crate::errors::throw_adobe_api_error; -use crate::models::{Variable, VariableType, VariablesList, VariablesResponse, YamlConfig}; +use crate::models::{ + EnvironmentVariable, EnvironmentVariableServiceType, EnvironmentVariablesList, + EnvironmentVariablesResponse, PipelineVariable, PipelineVariableServiceType, + PipelineVariablesList, PipelineVariablesResponse, VariableType, YamlConfig, +}; use crate::pipelines::get_pipeline; use crate::HOST_NAME; use colored::*; @@ -11,10 +15,17 @@ use std::process; use std::thread::sleep; use std::time::Duration; -// Make variables comparable - if they have the same name, they are the same. -impl PartialEq for Variable { +// Make environment variables comparable - if they have the same name and same service they are the same. +impl PartialEq for EnvironmentVariable { fn eq(&self, other: &Self) -> bool { - self.name == other.name + self.name == other.name && self.service == other.service + } +} + +// Make pipeline variables comparable - if they have the same name and same service they are the same. +impl PartialEq for PipelineVariable { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.service == other.service } } @@ -35,7 +46,7 @@ pub async fn get_env_vars( client: &mut CloudManagerClient, program_id: u32, env_id: u32, -) -> Result { +) -> Result { let request_path = format!( "{}/api/program/{}/environment/{}/variables", HOST_NAME, program_id, env_id @@ -45,8 +56,8 @@ pub async fn get_env_vars( .await? .text() .await?; - let variables: VariablesResponse = - serde_json::from_str(response.as_str()).unwrap_or_else(|_| { + let variables: EnvironmentVariablesResponse = serde_json::from_str(response.as_str()) + .unwrap_or_else(|_| { throw_adobe_api_error(response); process::exit(1); }); @@ -71,7 +82,7 @@ pub async fn set_env_vars( client: &mut CloudManagerClient, program_id: u32, env_id: u32, - variables: &[Variable], + variables: &[EnvironmentVariable], ) -> Result { let request_path = format!( "{}/api/program/{}/environment/{}/variables", @@ -103,6 +114,7 @@ pub async fn set_env_vars_from_file( file_path: &str, client: &mut CloudManagerClient, ci_mode: bool, + dry_run: bool, ) { let input = std::fs::read_to_string(file_path).expect("Unable to read file"); let input: YamlConfig = serde_yaml::from_str(input.as_str()).unwrap_or_else(|err| { @@ -124,7 +136,7 @@ pub async fn set_env_vars_from_file( // The vector that holds the final variables that will be set or deleted. Will be constructed // by comparing the variables that are currently set in Cloud Manager and those in the local // YAML config file. - let mut vars_final: Vec = vec![]; + let mut vars_final: Vec = vec![]; // Check if the targeted environment is ready '_retry: loop { @@ -176,45 +188,71 @@ pub async fn set_env_vars_from_file( let vars_cloud = get_env_vars(client, p.id, e.id).await.unwrap().variables; for vc in vars_cloud { if !vars_yaml.clone().contains(&vc) { - let variable_to_be_deleted = Variable { + let variable_to_be_deleted = EnvironmentVariable { name: vc.name, value: None, variable_type: vc.variable_type, service: vc.service, - status: None, }; vars_final.push(variable_to_be_deleted); } } + if let Some(vf) = vars_final + .iter() + .find(|vf| vf.service == EnvironmentVariableServiceType::Invalid) + { + eprintln!( + "{:>8} {} '{}: {}'", + "❌".red(), + "ERROR, invalid service type detected for variable".red(), + vf.name, + vf.service + ); + process::exit(3); + } + for vf in &vars_final { match vf.value { None => { - println!("{:>8} DELETING '{}'", "✍", vf.name); + println!( + "{:>8} DELETING '{}', service: {}", + "✍", vf.name, vf.service + ); } Some(_) => { - println!("{:>8} UPDATING '{}'", "✍", vf.name) + println!( + "{:>8} UPDATING '{}', service: {}", + "✍", vf.name, vf.service + ) } } } - match set_env_vars(client, p.id, e.id, &vars_final).await { - Ok(status) => match status { - StatusCode::NO_CONTENT => { - println!("{:>8} Success", "✔"); - } - _ => { - eprintln!( - "{:>8} {}", - "Error, check output above".red(), - "❌".red() - ); - process::exit(2); + if dry_run { + println!( + "{:>8} --dry-run detected. Not performing any actions.", + "⚠️", + ); + } else { + match set_env_vars(client, p.id, e.id, &vars_final).await { + Ok(status) => match status { + StatusCode::NO_CONTENT => { + println!("{:>8} Success", "✔"); + } + _ => { + eprintln!( + "{:>8} {}", + "Error, check output above".red(), + "❌".red() + ); + process::exit(2); + } + }, + Err(error) => { + eprintln!("{} {}", "❌ API error: ".red().bold(), error); + process::exit(1); } - }, - Err(error) => { - eprintln!("{} {}", "❌ API error: ".red().bold(), error); - process::exit(1); } } break '_retry; @@ -249,7 +287,7 @@ pub async fn get_pipeline_vars( client: &mut CloudManagerClient, program_id: u32, pipeline_id: &u32, -) -> Result { +) -> Result { let request_path = format!( "{}/api/program/{}/pipeline/{}/variables", HOST_NAME, program_id, pipeline_id @@ -259,8 +297,8 @@ pub async fn get_pipeline_vars( .await? .text() .await?; - let variables: VariablesResponse = - serde_json::from_str(response.as_str()).unwrap_or_else(|_| { + let variables: PipelineVariablesResponse = serde_json::from_str(response.as_str()) + .unwrap_or_else(|_| { throw_adobe_api_error(response); process::exit(1); }); @@ -285,7 +323,7 @@ pub async fn set_pipeline_vars( client: &mut CloudManagerClient, program_id: u32, pipeline_id: u32, - variables: &[Variable], + variables: &[PipelineVariable], ) -> Result { let request_path = format!( "{}/api/program/{}/pipeline/{}/variables", @@ -317,6 +355,7 @@ pub async fn set_pipeline_vars_from_file( file_path: &str, client: &mut CloudManagerClient, ci_mode: bool, + dry_run: bool, ) { let input = std::fs::read_to_string(file_path).expect("Unable to read file"); let input: YamlConfig = serde_yaml::from_str(input.as_str()).unwrap_or_else(|err| { @@ -338,7 +377,7 @@ pub async fn set_pipeline_vars_from_file( // The vector that holds the final variables that will be set or deleted. Will be constructed // by comparing the variables that are currently set in Cloud Manager and those in the local // YAML config file. - let mut vars_final: Vec = vec![]; + let mut vars_final: Vec = vec![]; // Check if the targeted environment is ready '_retry: loop { @@ -393,47 +432,74 @@ pub async fn set_pipeline_vars_from_file( .variables; for vc in vars_cloud { if !vars_yaml.clone().contains(&vc) { - let variable_to_be_deleted = Variable { + let variable_to_be_deleted = PipelineVariable { name: vc.name, value: None, variable_type: vc.variable_type, service: vc.service, - status: None, }; vars_final.push(variable_to_be_deleted); } } + if let Some(vf) = vars_final + .iter() + .find(|vf| vf.service == PipelineVariableServiceType::Invalid) + { + eprintln!( + "{:>8} {} '{}: {}'", + "❌".red(), + "ERROR, invalid service type detected for variable".red(), + vf.name, + vf.service + ); + process::exit(3); + } + for vf in &vars_final { match vf.value { None => { - println!("{:>8} DELETING '{}'", "✍", vf.name); + println!( + "{:>8} DELETING '{}', service: {}", + "✍", vf.name, vf.service + ); } Some(_) => { - println!("{:>8} UPDATING '{}'", "✍", vf.name) + println!( + "{:>8} UPDATING '{}', service: {}", + "✍", vf.name, vf.service + ) } } } - match set_pipeline_vars(client, p.id, l.id, &vars_final).await { - Ok(status) => match status { - StatusCode::NO_CONTENT => { - println!("{:>8} Success", "✔"); - } - _ => { - eprintln!( - "{:>8} {}", - "Error, check output above".red(), - "❌".red() - ); - process::exit(2); + if dry_run { + println!( + "{:>8} --dry-run detected. Not performing any actions.", + "⚠️", + ); + } else { + match set_pipeline_vars(client, p.id, l.id, &vars_final).await { + Ok(status) => match status { + StatusCode::NO_CONTENT => { + println!("{:>8} Success", "✔"); + } + _ => { + eprintln!( + "{:>8} {}", + "Error, check output above".red(), + "❌".red() + ); + process::exit(2); + } + }, + Err(error) => { + eprintln!("{} {}", "❌ API error: ".red().bold(), error); + process::exit(1); } - }, - Err(error) => { - eprintln!("{} {}", "❌ API error: ".red().bold(), error); - process::exit(1); } } + break '_retry; } }