cinderella

[unmaintained] simple CI engine
Log | Files | Refs | README | LICENSE

commit 672809cf8eb1b7255d741b0bf654135cccfcd14d
parent c22f43603dee3f2b1f25a7621dd56381cee30ab3
Author: Stefan Koch <programming@stefan-koch.name>
Date:   Sun,  8 Dec 2019 12:02:03 +0100

Merge branch 'master' into development

Diffstat:
MCargo.lock | 39+++++++++++++++++++++++++++++++++++++++
MCargo.toml | 1+
MREADME.md | 7+++++++
Msrc/config.rs | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/execution.rs | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/lib.rs | 77+++++++++++++++++++++++++++++------------------------------------------------
Msrc/parser.rs | 15+++++++++++++++
Msrc/variables.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
8 files changed, 373 insertions(+), 108 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -81,6 +81,7 @@ dependencies = [ name = "cinderella" version = "0.1.0" dependencies = [ + "duct 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)", "evalexpr 5.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -126,6 +127,17 @@ dependencies = [ ] [[package]] +name = "duct" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "os_pipe 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "shared_child 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "email" version = "0.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -467,6 +479,11 @@ dependencies = [ ] [[package]] +name = "once_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "openssl" version = "0.10.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -506,6 +523,15 @@ dependencies = [ ] [[package]] +name = "os_pipe" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -802,6 +828,15 @@ dependencies = [ ] [[package]] +name = "shared_child" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "smallvec" version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -985,6 +1020,7 @@ dependencies = [ "checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" "checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" "checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +"checksum duct 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1607fa68d55be208e83bcfbcfffbc1ec65c9fbcf9eb1a5d548dc3ac0100743b0" "checksum email 0.0.20 (registry+https://github.com/rust-lang/crates.io-index)" = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4" "checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" "checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" @@ -1023,10 +1059,12 @@ dependencies = [ "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +"checksum once_cell 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "891f486f630e5c5a4916c7e16c4b24a53e78c860b646e9f8e005e4f16847bfed" "checksum openssl 0.10.24 (registry+https://github.com/rust-lang/crates.io-index)" = "8152bb5a9b5b721538462336e3bef9a539f892715e5037fda0f984577311af15" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-src 111.6.0+1.1.1d (registry+https://github.com/rust-lang/crates.io-index)" = "b9c2da1de8a7a3f860919c01540b03a6db16de042405a8a07a5e9d0b4b825d9c" "checksum openssl-sys 0.9.49 (registry+https://github.com/rust-lang/crates.io-index)" = "f4fad9e54bd23bd4cbbe48fdc08a1b8091707ac869ef8508edea2fec77dcc884" +"checksum os_pipe 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "db4d06355a7090ce852965b2d08e11426c315438462638c6d721448d0b47aa22" "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum pkg-config 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c1d2cfa5a714db3b5f24f0915e74fcdf91d09d496ba61329705dda7774d2af" "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" @@ -1062,6 +1100,7 @@ dependencies = [ "checksum serde 1.0.100 (registry+https://github.com/rust-lang/crates.io-index)" = "f4473e8506b213730ff2061073b48fa51dcc66349219e2e7c5608f0296a1d95a" "checksum serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "4b133a43a1ecd55d4086bd5b4dc6c1751c68b1bfbeba7a5040442022c7e7c02e" "checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704" +"checksum shared_child 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8cebcf3a403e4deafaf34dc882c4a1b6a648b43e5670aa2e4bb985914eaeb2d2" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" "checksum sodiumoxide 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585232e78a4fc18133eef9946d3080befdf68b906c51b621531c37e91787fa2b" "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" diff --git a/Cargo.toml b/Cargo.toml @@ -16,6 +16,7 @@ lettre = "0.9" lettre_email = "0.9" sodiumoxide = "0.2.5" rpassword = "4.0" +duct = "0.13" [dev-dependencies] tempfile = "3" diff --git a/README.md b/README.md @@ -115,6 +115,13 @@ the whole Cinderella configuration file. Encrypted Variables ------------------- +**Warning:** I am not security expert and there has been no analysis regarding +the security of my implementation. I am using `sodiumoxide` internally, but +still I could do something wrong. If you want to use this feature on a public +repository, please review my implementation. I personally only use it for +internal repositories at the moment. If you find any vulnerabilities in my +implementation please tell me. + Sometimes a script needs to use credentials that you do not want to store in a version control system in plaintext. For this use case, Cinderella supports the storage of variables in an encrypted file. This file has to be diff --git a/src/config.rs b/src/config.rs @@ -4,9 +4,15 @@ use std::path::PathBuf; use serde::Deserialize; use toml; +pub struct Configs<'a> { + pub cinderella_config: &'a CinderellaConfig, + pub execution_config: &'a ExecutionConfig, +} + #[derive(Deserialize, Debug)] -pub struct Config { +pub struct CinderellaConfig { pub email: Option<Email>, + pub secrets: Option<Secrets>, } #[derive(Deserialize, Debug)] @@ -18,17 +24,58 @@ pub struct Email { pub to: String, } -pub fn read_config(path: PathBuf) -> Config { - match fs::read_to_string(path) { - Ok(contents) => { - toml::from_str(&contents).expect("Configuration invalid") - }, - _ => Config { - email: None +#[derive(Deserialize, Debug)] +pub struct Secrets { + pub password: String, +} + +impl CinderellaConfig { + pub fn from_file(path: PathBuf) -> CinderellaConfig { + match fs::read_to_string(path) { + Ok(contents) => { + toml::from_str(&contents).expect("Configuration invalid") + }, + _ => CinderellaConfig { + email: None, + secrets: None, + } } } } +pub struct ExecutionConfig { + pub repo_url: String, + pub branch: Option<String>, + pub cinderella_filepath: Option<String>, +} + +impl ExecutionConfig { + // TODO: This approach only works for URLs, not for local paths. + pub fn name(&self) -> String { + self.repo_url.split('/').last().unwrap().to_string() + } + + pub fn cinderella_file(&self, folder: &PathBuf) -> PathBuf { + let filepath = match &self.cinderella_filepath { + Some(filepath) => PathBuf::from(filepath), + None => { + let mut cinderella_file = folder.clone(); + cinderella_file.push(".cinderella.toml"); + cinderella_file + }, + }; + + filepath + } + + pub fn secrets_file(&self, folder: &PathBuf) -> PathBuf { + let mut secrets_file = folder.clone(); + secrets_file.push(".cinderella"); + secrets_file.push("secrets"); + secrets_file + } +} + #[cfg(test)] mod tests { use super::*; @@ -49,7 +96,7 @@ mod tests { let f = tmpfile.as_file_mut(); f.write_all(config.as_bytes()).expect("Unable to write to file"); - let config = read_config(tmpfile.path().to_path_buf()); + let config = CinderellaConfig::from_file(tmpfile.path().to_path_buf()); let email = config.email.unwrap(); assert_eq!(email.server, "localhost"); @@ -64,8 +111,31 @@ mod tests { let mut path = PathBuf::new(); path.push("/tmp/some/invalid/path/config.toml"); - let config = read_config(path); + let config = CinderellaConfig::from_file(path); assert!(config.email.is_none()); } + + #[test] + fn test_secrets_file_path() { + // this test exists to ensure that we recognize when the expected + // path to the secrets file changes, so that we can mention this in + // the change notes + + let exec_config = ExecutionConfig { + repo_url: String::from("https://example.com/my-repo.git"), + branch: Some(String::from("master")), + cinderella_filepath: None, + }; + + let base_path = PathBuf::from("/tmp/work-dir"); + let secrets_file = exec_config.secrets_file(&base_path); + + assert_eq!( + secrets_file, + PathBuf::from("/tmp/work-dir/.cinderella/secrets") + ); + + + } } diff --git a/src/execution.rs b/src/execution.rs @@ -1,27 +1,95 @@ use std::collections::HashMap; -use std::process::Command; -use std::io::Write; +use std::io::{BufRead, BufReader}; use evalexpr; +use duct::cmd; use crate::parser; use crate::pipeline; pub enum ExecutionResult { NoExecution, - Success, - // failed command and its output - BuildError(String, String, Option<i32>), - ExecutionError(String, String), + Success(Vec<StepResult>), + Error(Vec<StepResult>), } -pub fn execute<W: Write>( +pub enum StepResult { + Success(String, String), + Error(String, String, Option<i32>), +} + +struct Command { + command: String, + args: Vec<String>, +} + +impl Command { + fn command_string(&self) -> String { + let mut command = String::from(&self.command); + + for arg in &self.args { + command.push_str(" "); + + if arg.contains(" ") { + command.push_str("\""); + command.push_str(&arg); + command.push_str("\""); + } else { + command.push_str(&arg); + } + } + + command + } + + fn execute(&self) -> StepResult { + let reader = cmd(&self.command, &self.args).stderr_to_stdout() + .reader().unwrap(); + let f = BufReader::new(&reader); + + let mut outtext = String::new(); + + for line in f.lines() { + match line { + Ok(line) => { + println!("{}", line); + + // TODO: Newline style should be system dependent + outtext.push_str(&line); + outtext.push_str("\n"); + }, + _ => { + reader.kill().expect("Could not kill reader"); + return StepResult::Error( + self.command_string(), + outtext, + // TODO: How can we get the correct code here? + None + ); + }, + } + } + + // guaranteed to be Ok(Some(_)) after EOF + let output = reader.try_wait().unwrap().unwrap(); + match output.status.success() { + true => StepResult::Success(self.command_string(), outtext), + false => { + StepResult::Error( + self.command_string(), + outtext, + output.status.code() + ) + }, + } + } +} + +pub fn execute( pipelines: &Vec<pipeline::Pipeline>, - variables: &HashMap<String, String>, - stdout: &mut W) -> ExecutionResult + variables: &HashMap<String, String>) -> ExecutionResult { - // TODO: Refactor this whole function to get a cleaner design - let mut executed_at_least_one = false; + let mut done_steps = Vec::new(); for pipeline in pipelines { let execute = match &pipeline.when { @@ -32,60 +100,52 @@ pub fn execute<W: Write>( }; if execute { - executed_at_least_one = true; - let res = execute_pipeline(pipeline, &variables, stdout); + let res = execute_pipeline(pipeline, &variables); match res { - ExecutionResult::BuildError(_, _, _) | ExecutionResult::ExecutionError(_, _) => { - return res; - }, - _ => (), + ExecutionResult::Success(steps) => done_steps.extend(steps), + ExecutionResult::Error(_) => return res, + ExecutionResult::NoExecution => (), } } } - if executed_at_least_one { - ExecutionResult::Success + if done_steps.len() > 0 { + ExecutionResult::Success(done_steps) } else { ExecutionResult::NoExecution } } -fn execute_pipeline<W: Write>( +fn execute_pipeline( pipeline: &pipeline::Pipeline, - variables: &HashMap<String, String>, - stdout: &mut W) -> ExecutionResult + variables: &HashMap<String, String>) -> ExecutionResult { - writeln!(stdout, "Executing pipeline \"{}\"\n", pipeline.name).unwrap(); + let mut step_results = Vec::new(); for cmd in &pipeline.commands { - writeln!(stdout, "Step: {}", cmd).unwrap(); - let cmd = replace_variables(&cmd, &variables); // TODO: Raise error if some variables remain unsubstituted? - let parts = parser::parse_command(&cmd); - let output = Command::new(&parts[0]) - .args(&parts[1..]) - .output(); - let output = match output { - Ok(output) => output, - Err(e) => return ExecutionResult::ExecutionError(cmd, e.to_string()), - }; - stdout.write_all(&output.stdout).unwrap(); + let cmd = Command { + command: String::from(&parts[0]), + args: parts[1..].to_vec(), + }; - let outtext = String::from_utf8(output.stdout.iter().map(|&c| c as u8).collect()).unwrap(); - if !output.status.success() { - return ExecutionResult::BuildError( - String::from(format!("Pipeline failed in step: {}", cmd)), - outtext, - output.status.code() - ); + let result = cmd.execute(); + match result { + StepResult::Success(_, _) => { + step_results.push(result); + }, + StepResult::Error(_, _, _) => { + step_results.push(result); + return ExecutionResult::Error(step_results); + }, } } - ExecutionResult::Success + ExecutionResult::Success(step_results) } fn execute_test(test: &str, variables: &HashMap<String, String>) -> bool { @@ -122,9 +182,25 @@ mod tests { fn execute_stringout(pipeline: Pipeline, variables: HashMap<String, String>) -> String { - let mut stdout = Vec::new(); - execute(&vec![pipeline], &variables, &mut stdout); - String::from_utf8(stdout.iter().map(|&c| c as u8).collect()).unwrap() + let res = execute(&vec![pipeline], &variables); + + let mut out = String::new(); + match res { + ExecutionResult::Success(steps) + | ExecutionResult::Error(steps) => + { + for step in steps { + let text = match step { + StepResult::Success(_command, out) => out, + StepResult::Error(_command, out, _code) => out, + }; + out.push_str(&text); + } + }, + _ => (), + } + + out } #[test] @@ -138,11 +214,35 @@ mod tests { let result = execute_stringout(pipeline, variables); - assert!(result.contains("Executing pipeline \"my-test\"")); assert!(result.contains("this is my test")); } #[test] + fn test_execute_error_statement() { + let pipeline = Pipeline { + name: String::from("error-test"), + commands: vec!["bash -c \"exit 1\"".to_string()], + when: None, + }; + let variables = HashMap::new(); + + let result = execute(&vec![pipeline], &variables); + + match result { + ExecutionResult::Error(steps) => { + if let StepResult::Error(cmd, _out, _code) = &steps[0] { + assert_eq!(cmd, "bash -c \"exit 1\""); + } else { + assert!(false); + } + }, + // fail if something different from error is returned + _ => assert!(false), + } + } + + + #[test] fn test_pipeline_with_variables() { let pipeline = Pipeline { name: String::from("my-test"), @@ -169,7 +269,6 @@ mod tests { let result = execute_stringout(pipeline, variables); - println!("{}", result); assert!(!result.contains("non-master")); } @@ -185,7 +284,6 @@ mod tests { let result = execute_stringout(pipeline, variables); - println!("{}", result); assert!(result.contains("Building master")); } } diff --git a/src/lib.rs b/src/lib.rs @@ -1,6 +1,5 @@ use std::env; use std::fs; -use std::io; use std::path::{Path, PathBuf}; use rand::Rng; @@ -15,36 +14,13 @@ mod mail; mod crypto; mod variables; -use crate::execution::ExecutionResult; +pub use crate::config::ExecutionConfig; + +use crate::config::{CinderellaConfig, Configs}; +use crate::execution::{ExecutionResult, StepResult}; use crate::vcs::CodeSource; use crate::vcs::WorkingCopy; -pub struct ExecutionConfig { - pub repo_url: String, - pub branch: Option<String>, - pub cinderella_filepath: Option<String>, -} - -impl ExecutionConfig { - // TODO: This approach only works for URLs, not for local paths. - fn name(&self) -> String { - self.repo_url.split('/').last().unwrap().to_string() - } - - fn cinderella_file(&self, folder: &PathBuf) -> PathBuf { - let filepath = match &self.cinderella_filepath { - Some(filepath) => PathBuf::from(filepath), - None => { - let mut cinderella_file = folder.clone(); - cinderella_file.push(".cinderella.toml"); - cinderella_file - }, - }; - - filepath - } -} - fn random_dir(base_path: &str) -> PathBuf { let mut tempdir = PathBuf::from(base_path); @@ -66,7 +42,11 @@ fn appconfig_file() -> PathBuf { } pub fn run(exec_config: &ExecutionConfig) { - let config = config::read_config(appconfig_file()); + let cinderella_config = CinderellaConfig::from_file(appconfig_file()); + let configs = Configs { + cinderella_config: &cinderella_config, + execution_config: exec_config, + }; let repo = vcs::GitSource { src: exec_config.repo_url.clone(), @@ -92,29 +72,30 @@ pub fn run(exec_config: &ExecutionConfig) { if let Some(pipelines) = pipeline::load_pipeline(&cinderella_file) { // TODO: Check if execution was successful. If not and if email is // configured, send a mail - let variables = variables::load(&exec_config.branch); - let res = execution::execute(&pipelines, &variables, &mut io::stdout()); + let variables = variables::load(&workdir.path, &configs); + let res = execution::execute(&pipelines, &variables); match res { - ExecutionResult::BuildError(msg, output, code) => { - eprintln!("Build failed: {}\n\n{}", msg, output); - - let code_msg = match code { - Some(code) => format!("Exited with status code: {}", code), - None => format!("Process terminated by signal") - }; - let mailer = mail::build_mailer(&config.email); - mailer.send_mail( - &exec_config.name(), - &format!("Build failed: {}\n{}\n\n{}", msg, code_msg, output)); - }, - ExecutionResult::ExecutionError(msg, output) => { - eprintln!("Build failed: {}\n\n{}", msg, output); - - let mailer = mail::build_mailer(&config.email); + ExecutionResult::Error(steps) => { + let mut output = String::new(); + + for step in steps { + match step { + StepResult::Success(command, out) + | StepResult::Error(command, out, _) => + { + output.push_str(&command); + // TODO: newline should be system-dependent + output.push_str("\n"); + output.push_str(&out); + }, + } + } + + let mailer = mail::build_mailer(&cinderella_config.email); mailer.send_mail( &exec_config.name(), - &format!("Build failed: {}\n\n{}", msg, output)); + &format!("Build failed:\n\n{}", output)); }, _ => (), } diff --git a/src/parser.rs b/src/parser.rs @@ -12,6 +12,7 @@ pub fn parse_command(command: &str) -> Vec<String> { if c == '"' { parts.push(String::from(&command[start_idx..i])); in_quotes = false; + start_idx = i + 1; } } else { if c == '\\' && !next_char_escaped { @@ -52,6 +53,7 @@ mod tests { fn test_parse_simple_command() { let result = parse_command("program execute something"); + assert_eq!(result.len(), 3); assert_eq!(result[0], "program"); assert_eq!(result[1], "execute"); assert_eq!(result[2], "something"); @@ -61,6 +63,7 @@ mod tests { fn test_parse_command_with_quoted_args() { let result = parse_command("program \"execute something\" and \"something else\""); + assert_eq!(result.len(), 4); assert_eq!(result[0], "program"); assert_eq!(result[1], "execute something"); assert_eq!(result[2], "and"); @@ -71,14 +74,26 @@ mod tests { fn test_parse_command_with_spaced_arg() { let result = parse_command("program execute\\ something"); + assert_eq!(result.len(), 2); assert_eq!(result[0], "program"); assert_eq!(result[1], "execute something"); } #[test] + fn test_bash_command() { + let result = parse_command("bash -c \"exit 1\""); + + assert_eq!(result.len(), 3); + assert_eq!(result[0], "bash"); + assert_eq!(result[1], "-c"); + assert_eq!(result[2], "exit 1"); + } + + #[test] fn test_parse_virtualenv_tox_command() { let result = parse_command("bash -c \"virtualenv env && source env/bin/activate && tox\""); + assert_eq!(result.len(), 3); assert_eq!(result[0], "bash"); assert_eq!(result[1], "-c"); assert_eq!(result[2], "virtualenv env && source env/bin/activate && tox"); diff --git a/src/variables.rs b/src/variables.rs @@ -1,11 +1,65 @@ use std::collections::HashMap; +use std::path::PathBuf; -pub fn load(branch: &Option<String>) -> HashMap<String, String> { +use toml; + +use crate::config::Configs; +use crate::crypto; + +pub fn load(workdir: &PathBuf, configs: &Configs) -> HashMap<String, String> { + let mut variables = HashMap::new(); + + variables.extend(load_internal(configs)); + variables.extend(load_secrets_from_file(workdir, configs)); + + variables +} + +fn load_internal(configs: &Configs) -> HashMap<String, String> { let mut variables = HashMap::new(); - if let Some(branch) = &branch { + if let Some(branch) = &configs.execution_config.branch { variables.insert("branch".to_string(), branch.to_string()); } variables } + +fn load_secrets_from_file(workdir: &PathBuf, configs: &Configs) + -> HashMap<String, String> +{ + let mut variables = HashMap::new(); + + let secrets_file = configs.execution_config.secrets_file(workdir); + + // TODO: If secrets not defined output that not defined? + if let Some(secrets) = &configs.cinderella_config.secrets { + let password = &secrets.password; + let decrypted = crypto::decrypt_file(&secrets_file, &password); + + if let Ok(secrets_content) = decrypted { + variables.extend(load_secrets(&secrets_content)); + }; + } + + variables +} + +fn load_secrets(toml_definition: &str) -> HashMap<String, String> +{ + toml::from_str(toml_definition).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_secrets() { + let config = "USERNAME = \"my-user\"\nPASSWORD = \"my-pass\""; + let variables = load_secrets(config); + + assert_eq!(variables["USERNAME"], "my-user"); + assert_eq!(variables["PASSWORD"], "my-pass"); + } +}