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:
M | Cargo.lock | | | 39 | +++++++++++++++++++++++++++++++++++++++ |
M | Cargo.toml | | | 1 | + |
M | README.md | | | 7 | +++++++ |
M | src/config.rs | | | 90 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
M | src/execution.rs | | | 194 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- |
M | src/lib.rs | | | 77 | +++++++++++++++++++++++++++++------------------------------------------------ |
M | src/parser.rs | | | 15 | +++++++++++++++ |
M | src/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");
+ }
+}