diff --git a/README.md b/README.md index be42c66..69d83c4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ FLAGS: OPTIONS: -b, --base64-parameter Same as --parameter, but for values already encoded in Base64 + -f, --param-file ... + Supplies a Yaml file defining any named parameters -p, --parameter Supplies a value for the named parameter -s, --secret @@ -33,13 +35,23 @@ ARGS: Run `ktmpl TEMPLATE` where TEMPLATE is a path to a Kubernetes manifest template in YAML format. The included [example.yml](example.yml) is a working example template. -To provide values for template parameters, use the `--parameter` option to supply key-value pairs. +To provide values for template parameters, use either the `--parameter` or the `--parameter-file` option to supply key-value pairs. +Using the `--parameter` option: + Using the provided example.yml, this would mean: ``` bash ktmpl example.yml --parameter MONGODB_PASSWORD secret ``` +Using the `--parameter-file` option: + +``` +ktmpl example.yml --parameter-file params.yaml +``` + +If the same parameter is defined more than once, the last defined value will be used. Passing parameters on the command line `--parameter` will override any that are defined via the `--parameter-file` construct + Template parameters that have default values can be overridden with the same mechanism: ``` bash diff --git a/params.yaml b/params.yaml new file mode 100644 index 0000000..46aeab0 --- /dev/null +++ b/params.yaml @@ -0,0 +1,5 @@ +--- +MONGODB_PASSWORD: secret +MONGODB_USER: bar +REPLICA_COUNT: 2 + diff --git a/src/lib.rs b/src/lib.rs index 535e130..f67007b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,16 +80,18 @@ extern crate yaml_rust as yaml; pub use template::Template; pub use parameter::{ParameterValue, ParameterValues}; +pub use parameterfile::{ParameterFile}; pub use secret::{Secret, Secrets}; mod parameter; +mod parameterfile; mod processor; mod secret; mod template; #[cfg(test)] mod tests { - use super::{ParameterValue, ParameterValues, Secret, Secrets, Template}; + use super::{ParameterValue, ParameterValues, ParameterFile, Secret, Secrets, Template}; #[test] fn encode_secrets() { @@ -193,4 +195,83 @@ parameters: assert!(template.process().is_err()); } + + #[test] + fn parse_parameter_file() { + let parameter_file_contents = r#" +--- +db_user: bob +db_password: changeme +db_port: 1234 +"#; + let template_contents = r#" +--- +kind: Template +apiVersion: v1 +metadata: + name: param-file-example +objects: + - kind: Pod + apiVersion: v1 + metadata: + name: db_app + spec: + containers: + - name: mydb + image: mydb + env: + - name: USERNAME + value: $(db_user) + - name: PASSWORD + value: $(db_password) + - name: DB_PORT + value: $(db_port) +parameters: + - name: db_user + description: Database username + required: true + parameterType: string + - name: db_password + description: Database user password + required: true + parameterType: base64 + - name: db_port + description: Database port + required: true + parameterType: int +"#; + + let parameter_file = ParameterFile::from_str(parameter_file_contents.to_string()).unwrap(); + let template = Template::new( + template_contents.to_string(), + parameter_file.parameters, + Some(Secrets::new()), + ).unwrap(); + + let processed_template = template.process().unwrap(); + + assert_eq!( + processed_template.lines().map(|l| l.trim_right()).collect::>().join("\n"), + r#"--- +apiVersion: v1 +kind: Pod +metadata: + name: db_app +spec: + containers: + - + env: + - + name: USERNAME + value: bob + - + name: PASSWORD + value: "Y2hhbmdlbWU=" + - + name: DB_PORT + value: "1234" + image: mydb + name: mydb"# + ); + } } diff --git a/src/main.rs b/src/main.rs index 245f11b..11c0ce8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::process::exit; use clap::{App, AppSettings, Arg, Values}; -use ktmpl::{Template, ParameterValue, ParameterValues, Secret, Secrets}; +use ktmpl::{Template, ParameterValue, ParameterValues, ParameterFile, Secret, Secrets}; fn main() { if let Err(error) = real_main() { @@ -64,12 +64,31 @@ fn real_main() -> Result<(), String> { .number_of_values(2) .value_names(&["NAME", "NAMESPACE"]) ) + .arg( + Arg::with_name("parameter-file") + .help("Supplies a Yaml file defining any named parameters") + .next_line_help(true) + .long("parameter-file") + .short("f") + .multiple(true) + .takes_value(true) + .number_of_values(1) + .value_names(&["FILENAME"]) + ) .get_matches(); - let mut values = match matches.values_of("parameter") { - Some(parameters) => parameter_values(parameters, false), - None => HashMap::new(), - }; + let mut values = HashMap::new(); + + // Parse Parameter files first, passing command line parameters + // should override any values supplied via a file + if let Some(files) = matches.values_of("parameter-file") { + let params_from_file = parameter_files(files); + values.extend(params_from_file); + } + + if let Some(parameters) = matches.values_of("parameter") { + values.extend(parameter_values(parameters, false)); + } if let Some(parameters) = matches.values_of("base64-parameter") { let encoded_values = parameter_values(parameters, true); @@ -104,6 +123,20 @@ fn real_main() -> Result<(), String> { } } +fn parameter_files(mut param_files: Values) -> ParameterValues { + let mut parameter_values = ParameterValues::new(); + + loop { + if let Some(f) = param_files.next() { + let param_file = ParameterFile::from_file(&f).unwrap(); + parameter_values.extend(param_file.parameters); + } else { + break; + } + } + parameter_values +} + fn parameter_values(mut parameters: Values, base64_encoded: bool) -> ParameterValues { let mut parameter_values = ParameterValues::new(); diff --git a/src/parameterfile.rs b/src/parameterfile.rs new file mode 100644 index 0000000..44c7ab1 --- /dev/null +++ b/src/parameterfile.rs @@ -0,0 +1,93 @@ +use std::error::Error; +use std::fs::File; +use std::io::Read; + +use yaml::{Yaml, YamlLoader}; +use parameter::{ParameterValue, ParameterValues}; + +#[derive(Debug)] +/// ParameterFile struct +pub struct ParameterFile { + /// filename + pub filename: String, + /// document string + pub doc_str: String, + /// parameters allocated from this file + pub parameters: ParameterValues, +} + +fn parse_document(doc_str: &String, parameter_values: &mut ParameterValues) { + let docs = YamlLoader::load_from_str(doc_str).unwrap(); + for doc in &docs { + let primary_key = ""; + let param_values = parse_yaml(doc, primary_key); + parameter_values.extend(param_values); + } +} + +fn parse_yaml(doc: &Yaml, primary_key: &str) -> ParameterValues { + let mut param_values = ParameterValues::new(); + match doc { + &Yaml::Hash(ref h) => { + for (key,value) in h { + let combined_key = primary_key.to_string() + key.as_str().unwrap(); + match value { + &Yaml::String(ref s) => { + let pv = ParameterValue::Plain(s.to_string()); + param_values.insert(combined_key,pv); + }, + &Yaml::Integer(ref i) => { + let pv = ParameterValue::Plain(i.to_string()); + param_values.insert(combined_key,pv); + }, + &Yaml::Real(ref r) => { + let pv = ParameterValue::Plain(r.to_string()); + param_values.insert(combined_key,pv); + }, + _ => { + // Value type not supported + // Array, Alias and None + } + } + } + }, + &Yaml::String(ref s) => { + let pv = ParameterValue::Plain(s.to_string()); + param_values.insert(primary_key.to_string(),pv); + }, + _ => { + // Key type not supported + } + } + param_values +} + +impl ParameterFile { + /// Create a new parameterfile object, composed of a filename + /// and the parsed parameters + pub fn from_file(filename: &str) -> Result { + let mut parameter_values = ParameterValues::new(); + let mut fh = File::open(filename).map_err(|err| err.description().to_owned()).unwrap(); + let mut contents = String::new(); + fh.read_to_string(&mut contents).map_err(|err| err.description().to_owned())?; + parse_document(&contents, &mut parameter_values); + + Ok(ParameterFile { + filename: String::from(filename), + doc_str: String::from(contents), + parameters: parameter_values, + }) + } + + /// Create a new parameterfile object from a String representing a yaml document + pub fn from_str(doc_str: String) -> Result { + let mut parameter_values = ParameterValues::new(); + parse_document(&doc_str, &mut parameter_values); + + Ok(ParameterFile { + filename: String::from(""), + doc_str: String::from(doc_str), + parameters: parameter_values, + }) + } +}