commit b12fa5d0d51562d6700290d4ca7214d347806f3e
parent 68b85da189c1fa158329deb89b1b9110667f98b5
Author: Stefan Koch <programming@stefan-koch.name>
Date: Fri, 1 May 2020 10:13:34 +0200
Merge branch 'master' of https://github.com/aufziehvogel/cinderella
Diffstat:
8 files changed, 156 insertions(+), 55 deletions(-)
diff --git a/.cinderella.toml b/.cinderella.toml
@@ -16,6 +16,9 @@ when = "\"%BRANCH\" == \"master\""
[build-release]
commands = [
+ # set the tag number as tool version
+ "sed -i -e 's/^version = .*/version = \"%TAG\"/' Cargo.toml",
+
"cargo build --release",
"cp target/release/cinderella \"/opt/cinderella/cinderella-%TAG\"",
]
diff --git a/Cargo.lock b/Cargo.lock
@@ -90,7 +90,7 @@ dependencies = [
[[package]]
name = "cinderella"
-version = "0.1.0"
+version = "0.0.1-dev"
dependencies = [
"assert_cmd 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
"duct 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,6 +1,7 @@
[package]
name = "cinderella"
-version = "0.1.0"
+# version is automatically set in the build pipeline
+version = "0.0.1-dev"
authors = ["Stefan Koch <programming@stefan-koch.name>"]
edition = "2018"
diff --git a/README.md b/README.md
@@ -83,6 +83,15 @@ Currently supported variables are:
- `%BRANCH`: The name of the branch that is built, if it is a branch
- `%TAG`: The name of the tag that is built, if it is a tag
+### Environment Variables
+
+It is possible to use environment variables in your commands, e.g.
+`commands = ["echo $HOME"]`. Cinderella will substitute them by their
+values before the command gets sent to the operating system.
+
+This is also true if you use `bash` or other shells in your commands list.
+This means that in such cases the plaintext value of the environment
+variable will be visible in your shell history.
### Conditions
@@ -192,19 +201,20 @@ Open Points
This is a list of open points that are subject to implementation:
+- allow cinderella to `run` with a non-git folder: Logic should be so that
+ user passes a URL or a path to `run` (same as now) and Cinderella checks
+ if this is a URL, a local git repository or a local folder without git and
+ then runs the correct `Source` and `WorkingCopy`
- introduce command shortcuts for commands that are often used but annoying
to write in their full form
- `"[bash] foo && bar"` for `"bash -c \"foo && bar\""`
- `"[python-venv env] pip install -r requirements.txt && ./foo"` for
`"bash -c \"source env/bin/activate && pip install -r requirements.txt && ./foo\""`
-- substitute important environment variables like `$HOME` so that we do not
- have to run a bash in order to use them (or all environment variables?)
-- provide a way to set version numbers from a tag with Cinderella easily; i.e.
- when a tag 1.2 is created a Cinderella command in the pipeline file should
- set the version number at the right places (maybe this is already easily
- possible, maybe some adjustments to Cinderella are needed)
- keep a status of the last result per repository (to send *OK again* mails)
- send a more detailed error message on the build error reason:
- return code of the failed command
- full log of the executed commands (in a prompt style, command followed by
output)
+- send a mail with all compiler warnings? (or optionally to be
+ enabled/disabled in .cinderella.toml?); otherwise developers never see the
+ warnings
diff --git a/src/config.rs b/src/config.rs
@@ -59,6 +59,7 @@ pub struct ExecutionConfig {
impl ExecutionConfig {
// TODO: This approach only works for URLs, not for local paths.
+ // TODO: Move the name() function to the CodeSource
pub fn name(&self) -> String {
self.repo_url.split('/').last().unwrap().to_string()
}
@@ -150,7 +151,5 @@ mod tests {
secrets_file,
PathBuf::from("/tmp/work-dir/.cinderella/secrets")
);
-
-
}
}
diff --git a/src/execution.rs b/src/execution.rs
@@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::io::{BufRead, BufReader};
+use std::env;
use evalexpr;
use duct::cmd;
@@ -25,19 +26,16 @@ struct Command {
impl Command {
fn command_string(&self) -> String {
- let mut command = String::from(&self.command);
-
- for arg in &self.args {
- command.push_str(" ");
+ let mut parts = self.args.clone();
+ parts.insert(0, String::from(&self.command));
+ let command = parts.iter().map(|arg| {
if arg.contains(" ") {
- command.push_str("\"");
- command.push_str(&arg);
- command.push_str("\"");
+ format!("\"{}\"", arg)
} else {
- command.push_str(&arg);
+ String::from(arg)
}
- }
+ }).collect::<Vec<String>>().join(" ");
command
}
@@ -121,33 +119,39 @@ fn execute_pipeline(
pipeline: &pipeline::Pipeline,
variables: &HashMap<String, String>) -> ExecutionResult
{
- let mut step_results = Vec::new();
+ let res = pipeline.commands.iter()
+ .try_fold(Vec::<StepResult>::new(), |mut step_results, cmd| {
+ let result = execute_step(&cmd, variables);
+
+ match result {
+ StepResult::Success(_, _) => {
+ step_results.push(result);
+ Ok(step_results)
+ },
+ StepResult::Error(_, _, _) => {
+ step_results.push(result);
+ Err(step_results)
+ }
+ }
+ });
- for cmd in &pipeline.commands {
- let cmd = replace_variables(&cmd, &variables);
- // TODO: Raise error if some variables remain unsubstituted?
- let parts = parser::parse_command(&cmd);
+ match res {
+ Ok(step_results) => ExecutionResult::Success(step_results),
+ Err(step_results) => ExecutionResult::Error(step_results),
+ }
+}
- let cmd = Command {
- command: String::from(&parts[0]),
- args: parts[1..].to_vec(),
- };
+fn execute_step(cmd: &str, variables: &HashMap<String, String>) -> StepResult {
+ let cmd = replace_variables(&cmd, &variables);
+ let parts = parser::parse_command(&cmd);
- let result = cmd.execute();
- match result {
- StepResult::Success(_, _) => {
- step_results.push(result);
- },
- StepResult::Error(_, _, _) => {
- step_results.push(result);
- return ExecutionResult::Error(step_results);
- },
- }
- }
+ let cmd = Command {
+ command: String::from(&parts[0]),
+ args: parts[1..].to_vec(),
+ };
- ExecutionResult::Success(step_results)
+ cmd.execute()
}
-
fn execute_test(test: &str, variables: &HashMap<String, String>) -> bool {
// not possible to use evalexpr Context, because evalexpr only handles
// standard variable names without special characters (percentage
@@ -171,9 +175,24 @@ fn replace_variables(command: &str, variables: &HashMap<String, String>)
res = res.replace(&varname, replacement);
}
+ let res = replace_envvars(&res);
+
+ res
+}
+
+fn replace_envvars(command: &str) -> String
+{
+ let mut res = String::from(command);
+
+ for (key, value) in env::vars() {
+ let varname = format!("${}", key.to_uppercase());
+ res = res.replace(&varname, &value);
+ }
+
res
}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/main.rs b/src/main.rs
@@ -1,17 +1,27 @@
use std::env;
use std::path::Path;
+use std::process;
use rpassword;
use getopts::Options;
use cinderella::ExecutionConfig;
-fn print_usage(program: &str, opts: Options) {
- let brief = format!("Usage: {} REPO_URL [options]", program);
+fn print_usage(program: &str) {
+ println!("Usage: {} (run | encrypt | decrypt)", program);
+}
+
+fn print_usage_command(program: &str, argline: &str, opts: Options) {
+ let brief = format!("Usage: {} {}", program, argline);
print!("{}", opts.usage(&brief));
}
fn main() {
+ const NAME: &'static str = env!("CARGO_PKG_NAME");
+ const VERSION: &'static str = env!("CARGO_PKG_VERSION");
+ println!("{} v{}", NAME, VERSION);
+
let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
match args.get(1) {
Some(command) => {
@@ -19,25 +29,41 @@ fn main() {
"run" => run(args),
"encrypt" => encrypt(args),
"decrypt" => decrypt(args),
- _ => println!("Unknown command!"),
+ "--help" | "-h" => print_usage(&program),
+ _ => {
+ println!("Unknown command!");
+ print_usage(&program);
+ },
}
},
- None => println!("Please provide a command"),
+ None => {
+ println!("Please provide a command");
+ print_usage(&program);
+ },
}
}
-fn parse_password_arg(args: Vec<String>) -> Option<String> {
- let mut opts = Options::new();
- opts.optopt("p", "password", "set the password for encryption/decryption", "PASSWORD");
-
+fn parse_password_arg(opts: &Options, args: Vec<String>)
+ -> Result<Option<String>, String>
+{
match opts.parse(&args[2..]) {
- Ok(m) => m.opt_str("p"),
- Err(f) => panic!(f.to_string()),
+ Ok(m) => Ok(m.opt_str("p")),
+ Err(f) => Err(f.to_string()),
}
}
fn encrypt(args: Vec<String>) {
- let pass = match parse_password_arg(args) {
+ let mut opts = Options::new();
+ opts.optopt("p", "password", "set the password for encryption/decryption", "PASSWORD");
+
+ let pass_or_none = parse_password_arg(&opts, args)
+ .unwrap_or_else(|msg| {
+ println!("{}", msg);
+ print_usage_command("cinderella", "encrypt [options]", opts);
+ process::exit(1);
+ });
+
+ let pass = match pass_or_none {
Some(pass) => pass,
None => rpassword::read_password_from_tty(Some("Password: ")).unwrap(),
};
@@ -47,7 +73,17 @@ fn encrypt(args: Vec<String>) {
}
fn decrypt(args: Vec<String>) {
- let pass = match parse_password_arg(args) {
+ let mut opts = Options::new();
+ opts.optopt("p", "password", "set the password for encryption/decryption", "PASSWORD");
+
+ let pass_or_none = parse_password_arg(&opts, args)
+ .unwrap_or_else(|msg| {
+ println!("{}", msg);
+ print_usage_command("cinderella", "decrypt [options]", opts);
+ process::exit(1);
+ });
+
+ let pass = match pass_or_none {
Some(pass) => pass,
None => rpassword::read_password_from_tty(Some("Password: ")).unwrap(),
};
@@ -66,13 +102,17 @@ fn run(args: Vec<String>) {
let matches = match opts.parse(&args[2..]) {
Ok(m) => { m },
- Err(f) => { panic!(f.to_string()) },
+ Err(f) => {
+ println!("{}", f.to_string());
+ print_usage_command(&program, "run [options] REPO", opts);
+ process::exit(1);
+ },
};
let repository_url = if !matches.free.is_empty() {
matches.free[0].clone()
} else {
- print_usage(&program, opts);
+ print_usage(&program);
return;
};
@@ -83,5 +123,6 @@ fn run(args: Vec<String>) {
cinderella_filepath: matches.opt_str("f"),
};
+ // TODO: Handle error from cinderella:run and display error message + usage
cinderella::run(&repo)
}
diff --git a/tests/cli.rs b/tests/cli.rs
@@ -38,3 +38,31 @@ fn test_encrypt_decrypt() {
assert_eq!("MY_SECRET = \"secret\"", res.trim());
}
+
+#[test]
+fn test_environment_variables_get_replaced() {
+ let dir = tempfile::tempdir().unwrap();
+ let cinderella_file = dir.path().join(".cinderella.toml");
+
+ let mut file = File::create(&cinderella_file).unwrap();
+ writeln!(file, "[test]\ncommands = [\"echo $MY_ENV_VAR\"]").unwrap();
+
+ // Create a git repo, TODO: Would be nicer if we could run cinderella
+ // also on folders without having a git repo
+ Command::new("git")
+ .arg("init")
+ .current_dir(&dir)
+ .output()
+ .expect("Creation of git repo failed");
+
+ let output = Command::cargo_bin("cinderella").unwrap()
+ .args(vec!["run", "-f", &cinderella_file.to_string_lossy(), "."])
+ .current_dir(&dir)
+ .env("MY_ENV_VAR", "test-env-var")
+ .output()
+ .expect("Execution failed");
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+
+ assert!(stdout.contains("test-env-var"));
+}