diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..02e22d2 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,82 @@ +use crate::commands::{DEFAULT, TIDPLOY_DEFAULT}; +use crate::secret_store::{get_password, set_password}; + +use crate::state::State; + +use keyring::Error as KeyringError; + +use rpassword::prompt_password; + +use std::io::Error as IOError; + +use thiserror::Error as ThisError; + +#[derive(ThisError, Debug)] +#[error("{msg} {source}")] +pub(crate) struct AuthError { + pub(crate) msg: String, + pub(crate) source: AuthErrorKind, +} + +#[derive(ThisError, Debug)] +pub(crate) enum AuthErrorKind { + #[error("Failed to get password from prompt! {0}")] + Prompt(#[from] IOError), + #[error("No password saved.")] + NoPassword, + #[error("Internal keyring failure. {0}")] + Keyring(#[from] KeyringError), +} + +pub(crate) fn auth_command(state: &State, key: String) -> Result<(), AuthError> { + let password = prompt_password("Enter password:\n").map_err(|e| AuthError { + msg: "Failed to create password prompt!".to_owned(), + source: e.into(), + })?; + let path_str = state.deploy_path.as_str().replace('/', "\\\\"); + let store_key: String = format!( + "{}:{}/{}/{}", + key, state.repo.name, path_str, state.commit_sha + ); + println!("{}", key); + set_password(&password, &store_key).map_err(|e| { + let msg = format!( + "Could not set password in auth command with store_key {}!", + store_key + ); + AuthError { + msg, + source: e.into(), + } + })?; + Ok(println!("Set password with store_key {}!", &store_key)) +} + +pub(crate) fn auth_get_password(state: &State, key: &str) -> Result { + let path_str = state.deploy_path.as_str().replace('/', "\\\\"); + let store_key: String = format!( + "{}:{}/{}/{}", + key, state.repo.name, path_str, state.commit_sha + ); + if let Some(password) = get_password(&store_key)? { + return Ok(password); + } + let store_key_default_commit = format!( + "{}:{}/{}/{}", + key, state.repo.name, path_str, TIDPLOY_DEFAULT + ); + if let Some(password) = get_password(&store_key_default_commit)? { + return Ok(password); + } + + let store_key_default_commit_deploy = + format!("{}:{}/{}/{}", key, state.repo.name, "", TIDPLOY_DEFAULT); + if let Some(password) = get_password(&store_key_default_commit_deploy)? { + return Ok(password); + } + let store_key_default = format!("{}:{}/{}/{}", key, DEFAULT, "", TIDPLOY_DEFAULT); + match get_password(&store_key_default)? { + Some(password) => Ok(password), + None => Err(AuthErrorKind::NoPassword), + } +} diff --git a/src/commands.rs b/src/commands.rs index b7e7ca1..423915f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,31 +1,12 @@ +use crate::auth::{auth_command, AuthError}; +use crate::errors::ProcessError; +use crate::process::run_entrypoint; -use crate::config::{load_dploy_config, DployConfig, traverse_configs, ConfigError}; -use crate::errors::{GitError, ProcessError, RelPathError}; -use crate::filesystem::{FileError, get_current_dir}; -use crate::git::{git_root_origin_url, relative_to_git_root}; -use crate::secret_store::{get_password, set_password}; -use crate::secrets::SecretOutput; -use crate::state::{StateContext, LoadError}; -use clap::{Parser, Subcommand, ValueEnum}; -use keyring::Error as KeyringError; -use rpassword::prompt_password; -use spinoff::{spinners, Spinner}; -use std::env::VarError; -use std::ffi::OsString; -use std::fs::{self}; -use std::path::PathBuf; -use std::process::Output; -use std::{ - collections::HashMap, - env, - io::BufRead, - io::BufReader, - io::Error as IOError, - path::Path, - process::{Command as Cmd, Stdio}, -}; +use crate::state::{create_state_pre, create_state_run, CliEnvState, LoadError, StateContext}; +use clap::{Parser, Subcommand}; + +// use std::time::Instant; use thiserror::Error as ThisError; -use relative_path::RelativePathBuf; pub(crate) const DEFAULT_INFER: &str = "default_infer"; pub(crate) const TIDPLOY_DEFAULT: &str = "_tidploy_default"; @@ -37,31 +18,29 @@ struct Cli { #[command(subcommand)] command: Commands, - #[arg(long, value_enum)] + #[arg(long, value_enum, global = true)] context: Option, - #[arg(long)] - no_network: Option, + #[arg(long, global = true)] + network: Option, - #[arg(short, long)] + /// Set the repository URL, defaults to 'default_infer', in which case it is inferred from the current repository. Set to 'default' to not set it. + /// Falls back to environment variable using TIDPLOY_REPO and then to config with key 'repo_url' + /// For infering, it looks at the URL set to the 'origin' remote + #[arg(short, long, global = true)] repo: Option, - #[arg(short, long)] + #[arg(short, long, global = true)] tag: Option, - #[arg(short, long)] + #[arg(short, long, global = true)] deploy_pth: Option, - - } #[derive(Subcommand, Debug)] enum Commands { /// Save authentication details for specific stage until reboot - Auth { - key: String - }, - + Auth { key: String }, // /// Download tag or version with specific env, run automatically if using deploy // Download, @@ -73,16 +52,14 @@ enum Commands { // #[arg(short)] // variables: Vec // }, + /// Run an entrypoint using the password set for a specific repo and stage 'deploy', can be used after download + Run { + #[arg(short = 'x', long = "exe", default_value = "_tidploy_default")] + executable: String, - - // /// Run an entrypoint using the password set for a specific repo and stage 'deploy', can be used after download - // Run { - // #[arg(long = "exe", default_value = "_tidploy_default")] - // executable: String, - - // #[arg(short)] - // variables: Vec - // }, + #[arg(short, num_args = 2)] + variables: Vec, + }, } #[derive(ThisError, Debug)] @@ -92,19 +69,49 @@ pub struct Error(#[from] ErrorRepr); #[derive(ThisError, Debug)] enum ErrorRepr { #[error("Load error failure! {0}")] - Load(#[from] LoadError) + Load(#[from] LoadError), + #[error("Auth failure! {0}")] + Auth(#[from] AuthError), + #[error("Error unning executable! {0}")] + Exe(#[from] ProcessError), } - - - - pub(crate) fn run_cli() -> Result<(), Error> { + //let now = Instant::now(); + let args = Cli::parse(); - let state = create_state(args.context, args.use_network, args.repo, args.tag, args.deploy_pth); + //println!("{:?}", args); + + let cli_state = CliEnvState { + context: args.context, + network: args.network, + repo_url: args.repo, + deploy_path: args.deploy_pth, + tag: args.tag, + }; match args.command { - Commands::Auth { key } => Ok(), + Commands::Auth { key } => { + let state = create_state_pre(cli_state).map_err(ErrorRepr::Load)?; + //println!("{:?}", state); + //println!("time {}", now.elapsed().as_secs_f64()); + + Ok(auth_command(&state, key).map_err(ErrorRepr::Auth)?) + } + Commands::Run { + executable, + variables, + } => { + let state = create_state_run(cli_state, Some(executable), variables, false) + .map_err(ErrorRepr::Load)?; + //println!("{:?}", state); + //println!("time {}", now.elapsed().as_secs_f64()); + + run_entrypoint(state.current_dir, &state.exe_name, state.envs) + .map_err(ErrorRepr::Exe)?; + + Ok(()) + } } -} \ No newline at end of file +} diff --git a/src/config.rs b/src/config.rs index db77054..17ac01e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,19 @@ use relative_path::RelativePathBuf; use serde::Deserialize; -use std::{fs, path::{Path, PathBuf}, io::Error as IOError, ops::ControlFlow}; +use std::{ + collections::HashMap, + fs, + io::Error as IOError, + ops::ControlFlow, + path::{Path, PathBuf}, +}; use thiserror::Error as ThisError; #[derive(ThisError, Debug)] #[error("{msg} {source}")] pub(crate) struct ConfigError { pub(crate) msg: String, - pub(crate) source: ConfigErrorKind + pub(crate) source: ConfigErrorKind, } #[derive(Debug, ThisError)] @@ -17,9 +23,7 @@ pub(crate) enum ConfigErrorKind { #[error("Failed to parse config TOML! {0}")] TOMLDecode(#[from] toml::de::Error), #[error("Failed to parse config JSON! {0}")] - JSONDecode(#[from] serde_json::Error), - #[error("env_var must be set if using run or running dployer!")] - NoEnvVar, + JSONDecode(#[from] serde_json::Error) } #[derive(Deserialize)] @@ -29,13 +33,13 @@ pub(crate) struct DployConfig { pub(crate) deploy_path: Option, pub(crate) tag: Option, pub(crate) vars: Option>, - pub(crate) exe_name: Option + pub(crate) exe_name: Option, } #[derive(Deserialize, Clone)] pub(crate) struct ConfigVar { - key: String, - env_name: String + pub(crate) key: String, + pub(crate) env_name: String, } // impl DployConfig { @@ -62,13 +66,26 @@ pub(crate) struct ConfigVar { // } // } -pub(crate) fn load_dploy_config>(file_path_dir: P) -> Result { +pub(crate) fn load_dploy_config>( + file_path_dir: P, +) -> Result { let dir_path = file_path_dir.as_ref(); let toml_path = dir_path.join("tidploy.toml"); let json_path = dir_path.join("tidploy.json"); let choose_json = json_path.exists(); let file_path = if choose_json { json_path } else { toml_path }; + if !file_path.exists() { + return Ok(DployConfig { + network: None, + repo_url: None, + deploy_path: None, + tag: None, + exe_name: None, + vars: None, + }); + } + let config_str = fs::read_to_string(file_path)?; let dploy_config: DployConfig = if choose_json { @@ -82,9 +99,41 @@ pub(crate) fn load_dploy_config>(file_path_dir: P) -> Result(original: Option, replacing: Option) -> Option { if replacing.is_some() { - return replacing.clone() + return replacing; + } + original +} + +pub(crate) fn merge_vars( + root_vars: Option>, + overwrite_vars: Option>, +) -> Option> { + if let Some(root_vars) = root_vars { + if let Some(overwrite_vars) = overwrite_vars { + let mut vars_map: HashMap = root_vars + .iter() + .map(|v| (v.key.clone(), v.env_name.clone())) + .collect(); + + for cfg_var in overwrite_vars { + vars_map.insert(cfg_var.key, cfg_var.env_name); + } + + Some( + vars_map + .into_iter() + .map(|(k, v)| ConfigVar { + env_name: v, + key: k, + }) + .collect(), + ) + } else { + Some(root_vars) + } + } else { + overwrite_vars.clone() } - original.clone() } fn overwrite_config(root_config: DployConfig, overwrite_config: DployConfig) -> DployConfig { @@ -93,24 +142,33 @@ fn overwrite_config(root_config: DployConfig, overwrite_config: DployConfig) -> repo_url: overwrite_option(root_config.repo_url, overwrite_config.repo_url), deploy_path: overwrite_option(root_config.deploy_path, overwrite_config.deploy_path), tag: overwrite_option(root_config.tag, overwrite_config.tag), - vars: overwrite_option(root_config.vars, overwrite_config.vars), - exe_name: overwrite_option(root_config.exe_name, overwrite_config.exe_name) + vars: merge_vars(root_config.vars, overwrite_config.vars), + exe_name: overwrite_option(root_config.exe_name, overwrite_config.exe_name), } } -pub(crate) fn traverse_configs(start_path: PathBuf, final_path: RelativePathBuf) -> Result { - let root_config = load_dploy_config(start_path).map_err(|source| { - let msg = format!("Failed to load root config at path {:?} while traversing configs.", start_path); +pub(crate) fn traverse_configs( + start_path: PathBuf, + final_path: RelativePathBuf, +) -> Result { + let root_config = load_dploy_config(&start_path).map_err(|source| { + let msg = format!( + "Failed to load root config at path {:?} while traversing configs.", + start_path + ); ConfigError { msg, source } })?; - let paths: Vec = final_path.components().scan(RelativePathBuf::new(), |state, component| { - state.join(component.as_str()); - Some(state.to_path(&start_path)) - }).collect(); + let paths: Vec = final_path + .components() + .scan(RelativePathBuf::new(), |state, component| { + state.join(component.as_str()); + Some(state.to_path(&start_path)) + }) + .collect(); let combined_config = paths.iter().try_fold(root_config, |state, path| { - let inner_config = load_dploy_config(start_path); + let inner_config = load_dploy_config(&start_path); match inner_config { Ok(config) => ControlFlow::Continue(overwrite_config(state, config)), @@ -120,9 +178,9 @@ pub(crate) fn traverse_configs(start_path: PathBuf, final_path: RelativePathBuf) } } }); - + match combined_config { ControlFlow::Break(e) => Err(e), - ControlFlow::Continue(config) => Ok(config) + ControlFlow::Continue(config) => Ok(config), } -} \ No newline at end of file +} diff --git a/src/errors.rs b/src/errors.rs index 5b1965a..9b63eca 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,21 +1,21 @@ +use relative_path::FromPathError; use std::io::Error as StdIOError; use std::process::ExitStatus; use std::string::FromUtf8Error; -use relative_path::FromPathError; use thiserror::Error as ThisError; #[derive(ThisError, Debug)] #[error("{msg} {source}")] struct IOError { msg: String, - source: StdIOError + source: StdIOError, } #[derive(ThisError, Debug)] #[error("{msg} {source}")] pub(crate) struct ProcessError { - msg: String, - source: ProcessErrorKind + pub(crate) msg: String, + pub(crate) source: ProcessErrorKind, } #[derive(Debug, ThisError)] @@ -30,51 +30,56 @@ pub(crate) enum ProcessErrorKind { Failed(std::process::ExitStatus), } -#[derive(ThisError, Debug)] -#[error("{msg} {source}")] -pub(crate) struct GitError { - pub(crate) msg: &'static str, - pub(crate) source: GitErrorKind +#[derive(Debug, ThisError)] +pub(crate) enum GitError { + #[error("External Git process failed: {0}")] + Process(#[from] ProcessError), } impl GitError { - pub(crate) fn from_io(e: StdIOError, msg: &'static str) -> GitError { - return GitError { msg, source: ProcessErrorKind::IO(e).into()} + pub(crate) fn from_io(e: StdIOError, msg: String) -> GitError { + ProcessError { + msg, + source: ProcessErrorKind::IO(e), + } + .into() } - pub(crate) fn from_f(f: ExitStatus, msg: &'static str) -> GitError { - return GitError { msg, source: ProcessErrorKind::Failed(f).into()} + pub(crate) fn from_f(f: ExitStatus, msg: String) -> GitError { + ProcessError { + msg, + source: ProcessErrorKind::Failed(f), + } + .into() } - pub(crate) fn from_dec(e: FromUtf8Error, msg: &'static str) -> GitError { - return GitError { msg, source: ProcessErrorKind::Decode(e).into()} + pub(crate) fn from_dec(e: FromUtf8Error, msg: String) -> GitError { + ProcessError { + msg, + source: ProcessErrorKind::Decode(e), + } + .into() } -} - - -#[derive(Debug, ThisError)] -pub(crate) enum GitErrorKind { - #[error("Failure for external Git process! {0}")] - Process(#[from] ProcessErrorKind), - #[error("Failure decoding Git output! {0}")] - Decode(#[from] FromUtf8Error), } #[derive(ThisError, Debug)] #[error("{msg} {source}")] pub(crate) struct RelPathError { pub(crate) msg: String, - pub(crate) source: RelPathErrorKind + pub(crate) source: RelPathErrorKind, } impl RelPathError { pub(crate) fn from_knd(e: impl Into, msg: String) -> RelPathError { - return RelPathError { msg, source: e.into() } + RelPathError { + msg, + source: e.into(), + } } -} +} #[derive(Debug, ThisError)] -enum RelPathErrorKind { +pub(crate) enum RelPathErrorKind { #[error(transparent)] - FromPath(#[from] FromPathError) -} \ No newline at end of file + FromPath(#[from] FromPathError), +} diff --git a/src/filesystem.rs b/src/filesystem.rs index af6b41e..6856fa0 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,19 +1,19 @@ +use std::{env, io::Error as StdIOError, path::PathBuf}; use thiserror::Error as ThisError; -use std::{io::Error as StdIOError, path::PathBuf, env}; #[derive(ThisError, Debug)] #[error("{msg} {source}")] pub(crate) struct FileError { pub(crate) msg: String, - pub(crate) source: FileErrorKind + pub(crate) source: FileErrorKind, } #[derive(Debug, ThisError)] pub(crate) enum FileErrorKind { #[error("IO error reading current dir! {0}")] - NoCurrentDir(#[from] StdIOError) + NoCurrentDir(#[from] StdIOError), } pub(crate) fn get_current_dir() -> Result { env::current_dir().map_err(FileErrorKind::NoCurrentDir) -} \ No newline at end of file +} diff --git a/src/git.rs b/src/git.rs index 5444dc3..d8bce4f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,23 +1,11 @@ +use crate::errors::GitError; +use crate::process::process_out; -use crate::config::{load_dploy_config, ConfigError}; -use crate::errors::{GitError, GitErrorKind, ProcessErrorKind}; -use crate::secret_store::{get_password, set_password}; -use crate::secrets::SecretOutput; +use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64USNP; use base64::Engine; -use clap::{Parser, Subcommand, ValueEnum}; -use keyring::Error as KeyringError; -use rpassword::prompt_password; -use spinoff::{spinners, Spinner}; -use std::ffi::OsString; -use std::fs::{self}; -use std::process::Output; -use std::string::FromUtf8Error; -use std::{ - io::Error as IOError, - process::{Command as Cmd, Stdio}, -}; + +use std::process::Command as Cmd; use thiserror::Error as ThisError; -use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64USNP; pub(crate) fn git_root_origin_url() -> Result { let git_origin_output = Cmd::new("git") @@ -25,59 +13,86 @@ pub(crate) fn git_root_origin_url() -> Result { .arg("--get") .arg("remote.origin.url") .output() - .map_err(|e| GitError::from_io(e, "IO failure for git config get remote.origin.url!"))?; + .map_err(|e| { + GitError::from_io( + e, + "IO failure for git config get remote.origin.url!".to_owned(), + ) + })?; if !git_origin_output.status.success() { - return Err(GitError::from_f(git_origin_output.status, "Git get remote origin failed!")) + return Err(GitError::from_f( + git_origin_output.status, + "Git get remote origin failed!".to_owned(), + )); } - Ok(String::from_utf8(git_origin_output.stdout).map_err(|e| GitError::from_dec(e, "Failed to decode Git origin output!"))? + Ok(String::from_utf8(git_origin_output.stdout) + .map_err(|e| GitError::from_dec(e, "Failed to decode Git origin output!".to_owned()))? .trim_end() .to_owned()) } pub(crate) fn relative_to_git_root() -> Result { - let git_root_relative_output = Cmd::new("git") .arg("rev-parse") .arg("--show-prefix") .output() - .map_err(|e| GitError::from_io(e, "IO failure for get relative to git root!"))?; + .map_err(|e| GitError::from_io(e, "IO failure for get relative to git root!".to_owned()))?; if !git_root_relative_output.status.success() { - return Err(GitError::from_f(git_root_relative_output.status, "Git get relative to root failed!")) + return Err(GitError::from_f( + git_root_relative_output.status, + "Git get relative to root failed!".to_owned(), + )); } - Ok(String::from_utf8(git_root_relative_output.stdout).map_err(|e| GitError::from_dec(e, "Failed to decode Git relative to root path!"))? + Ok(String::from_utf8(git_root_relative_output.stdout) + .map_err(|e| { + GitError::from_dec(e, "Failed to decode Git relative to root path!".to_owned()) + })? .trim_end() .to_owned()) } #[derive(Debug, ThisError)] pub(crate) enum RepoParseError { - #[error("Repo URL {0} doesn't end with /.git and cannot be parsed!")] + #[error("Repo URL '{0}' doesn't end with /.git and cannot be parsed!")] InvalidURL(String), } +#[derive(Debug)] pub(crate) struct Repo { pub(crate) name: String, pub(crate) encoded_url: String, - pub(crate) url: String + pub(crate) url: String, } pub(crate) fn parse_repo_url(url: String) -> Result { - let mut split_parts: Vec<&str> = url.split('/').collect(); + let split_parts: Vec<&str> = url.split('/').collect(); + + if split_parts.len() <= 1 { + return Err(RepoParseError::InvalidURL(url)); + } let last_part = *split_parts .last() .ok_or(RepoParseError::InvalidURL(url.clone()))?; - let first_parts = split_parts.get(0..split_parts.len()-1).map(|a| a.to_vec().join("/")); + + let first_parts = split_parts + .get(0..split_parts.len() - 1) + .map(|a| a.to_vec().join("/")); + let encoded_url = if let Some(pre_part) = first_parts { B64USNP.encode(pre_part) } else { - return Err(RepoParseError::InvalidURL(url)) + return Err(RepoParseError::InvalidURL(url)); }; let split_parts_dot: Vec<&str> = last_part.split('.').collect(); + if split_parts_dot.len() <= 1 { + return Err(RepoParseError::InvalidURL(url)); + } + let name = (*split_parts_dot .first() .ok_or(RepoParseError::InvalidURL(url.clone()))?) @@ -86,6 +101,43 @@ pub(crate) fn parse_repo_url(url: String) -> Result { Ok(Repo { name, encoded_url, - url + url, }) -} \ No newline at end of file +} + +pub(crate) fn rev_parse_tag(tag: &str, use_origin: bool) -> Result { + let _prefixed_tag = if use_origin { + if tag.starts_with("origin/") { + tag.to_owned() // If it already contains origin/ we will just leave it as is + } else { + format!("origin/{}", tag) + } + } else { + tag.to_owned() + }; + + let parsed_tag_output = Cmd::new("git") + .arg("rev-parse") + .arg(tag) + .output() + .map_err(|e| GitError::from_io(e, "IO failure for parsing Git tag!".to_owned()))?; + + if !parsed_tag_output.status.success() { + let err_out = process_out( + parsed_tag_output.stderr, + "Git parse tag failed! Could not decode output!".to_owned(), + )?; + let _msg = format!("Git parse tag failed! err: {}", err_out); + return Err(GitError::from_f( + parsed_tag_output.status, + "Git parse tag failed!".to_owned(), + )); + } + + Ok(String::from_utf8(parsed_tag_output.stdout) + .map_err(|e| { + GitError::from_dec(e, "Failed to decode Git relative to root path!".to_owned()) + })? + .trim_end() + .to_owned()) +} diff --git a/src/main.rs b/src/main.rs index d622549..d7c2ef6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,15 @@ -mod cli; +mod auth; +mod commands; mod config; mod errors; -mod secret_store; -mod secrets; -mod commands; -mod git; mod filesystem; +mod git; +mod process; +mod secret_store; mod state; fn main() { - let program = cli::run_cli(); + let program = commands::run_cli(); if let Err(program_err) = program { eprintln!("Error: {}", program_err); std::process::exit(1) diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..d8fbeee --- /dev/null +++ b/src/process.rs @@ -0,0 +1,66 @@ +use std::{ + collections::HashMap, + io::{BufRead, BufReader}, + path::Path, + process::{Command as Cmd, Stdio}, +}; + +use crate::errors::{ProcessError, ProcessErrorKind}; + +pub(crate) fn process_out(bytes: Vec, err_msg: String) -> Result { + Ok(String::from_utf8(bytes) + .map_err(|e| ProcessError { + msg: err_msg, + source: ProcessErrorKind::Decode(e), + })? + .trim_end() + .to_owned()) +} + +fn err_ctx>(e: impl Into, p: P) -> ProcessError { + let msg = format!("IO error running entrypoint at path: {:?}", p.as_ref()); + ProcessError { + msg, + source: e.into(), + } +} + +pub(crate) fn run_entrypoint>( + entrypoint_dir: P, + entrypoint: &str, + envs: HashMap, +) -> Result<(), ProcessError> { + println!("Running {}!", &entrypoint); + let program_path = entrypoint_dir.as_ref().join(entrypoint); + let mut entrypoint_output = Cmd::new(&program_path) + .current_dir(&entrypoint_dir) + .envs(&envs) + .stdout(Stdio::piped()) + .spawn() + .map_err(|e| err_ctx(e, &program_path))?; + + let entrypoint_stdout = entrypoint_output + .stdout + .take() + .ok_or_else(|| err_ctx(ProcessErrorKind::NoOutput, &program_path))?; + + let reader = BufReader::new(entrypoint_stdout); + + reader + .lines() + .map_while(Result::ok) + .for_each(|line| println!("{}", line)); + + let output_stderr = entrypoint_output + .wait_with_output() + .map_err(|e| err_ctx(e, &program_path))? + .stderr; + if !output_stderr.is_empty() { + println!( + "Entrypoint {:?} failed with error: {}", + program_path.as_path(), + process_out(output_stderr, "Failed to decode entrypoint".to_owned())? + ) + } + Ok(()) +} diff --git a/src/secret_store.rs b/src/secret_store.rs index f185174..32dba6a 100644 --- a/src/secret_store.rs +++ b/src/secret_store.rs @@ -1,8 +1,7 @@ -use keyring::{Entry, Error::NoEntry, Result as KeyringResult}; +use keyring::{Entry, Error::NoEntry, Result}; -pub(crate) fn get_password(name: &str, stage: &str) -> KeyringResult> { - let user = format!("{}_{}", name, stage); - let entry = Entry::new("ti_dploy", &user)?; +pub(crate) fn get_password(key: &str) -> Result> { + let entry = Entry::new("ti_dploy", key)?; match entry.get_password() { Ok(pw) => Ok(Some(pw)), Err(NoEntry) => Ok(None), @@ -10,9 +9,8 @@ pub(crate) fn get_password(name: &str, stage: &str) -> KeyringResult KeyringResult<()> { - let user = format!("{}_{}", name, stage); - let entry = Entry::new("ti_dploy", &user)?; +pub(crate) fn set_password(password: &str, key: &str) -> Result<()> { + let entry = Entry::new("ti_dploy", key)?; entry.set_password(password)?; Ok(()) } diff --git a/src/secrets.rs b/src/secrets.rs deleted file mode 100644 index 48de031..0000000 --- a/src/secrets.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -pub(crate) struct SecretOutput { - pub(crate) key: String, - pub(crate) value: String, -} diff --git a/src/state.rs b/src/state.rs index 6e3dcb3..488c245 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,65 +1,57 @@ +use crate::auth::{auth_get_password, AuthError}; +use crate::commands::{DEFAULT, DEFAULT_INFER, TIDPLOY_DEFAULT}; +use crate::config::{ + load_dploy_config, merge_vars, traverse_configs, ConfigError, ConfigVar, DployConfig, +}; +use crate::errors::{GitError, RelPathError}; +use crate::filesystem::{get_current_dir, FileError}; +use crate::git::{ + git_root_origin_url, parse_repo_url, relative_to_git_root, rev_parse_tag, Repo, RepoParseError, +}; + +use clap::ValueEnum; + +use relative_path::RelativePathBuf; -use crate::commands::{DEFAULT, DEFAULT_INFER}; -use crate::config::{load_dploy_config, DployConfig, traverse_configs, ConfigError}; -use crate::errors::{GitError, ProcessError, RelPathError}; -use crate::filesystem::{FileError, get_current_dir}; -use crate::git::{git_root_origin_url, relative_to_git_root, RepoParseError, Repo, parse_repo_url}; -use crate::secret_store::{get_password, set_password}; -use crate::secrets::SecretOutput; -use clap::{Parser, Subcommand, ValueEnum}; -use keyring::Error as KeyringError; -use rpassword::prompt_password; -use spinoff::{spinners, Spinner}; use std::env::VarError; -use std::ffi::OsString; -use std::fs::{self}; + use std::path::PathBuf; -use std::process::Output; -use std::{ - collections::HashMap, - env, - io::BufRead, - io::BufReader, - io::Error as IOError, - path::Path, - process::{Command as Cmd, Stdio}, -}; +use std::{collections::HashMap, env}; use thiserror::Error as ThisError; -use relative_path::RelativePathBuf; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] pub(crate) enum StateContext { None, - Git + Git, } impl StateContext { - fn as_str(&self) -> &'static str { - match self { - StateContext::None => "none", - StateContext::Git => "git" - } - } + // fn as_str(&self) -> &'static str { + // match self { + // StateContext::None => "none", + // StateContext::Git => "git", + // } + // } fn from_str(s: &str) -> Option { match s { "none" => Some(StateContext::None), "git" => Some(StateContext::Git), - _ => None + _ => None, } } } - - -struct State { - network: bool, - context: StateContext, - repo: Repo, - deploy_dir: RelativePathBuf, - commit_sha: String, - envs: HashMap, - exe_name: String +#[derive(Debug)] +pub(crate) struct State { + pub(crate) network: bool, + pub(crate) context: StateContext, + pub(crate) repo: Repo, + pub(crate) deploy_path: RelativePathBuf, + pub(crate) commit_sha: String, + pub(crate) envs: HashMap, + pub(crate) exe_name: String, + pub(crate) current_dir: PathBuf, } #[derive(Debug, ThisError)] @@ -69,37 +61,56 @@ pub(crate) enum LoadError { #[error("Failure creating relative path during load! {0}")] RelPath(#[from] RelPathError), #[error("Failure to read env variable {var} as unicode during load!")] - VarNotUnicode { - var: String - }, + VarNotUnicode { var: String }, #[error("{msg}")] - BadValue { - msg: String, - }, + BadValue { msg: String }, #[error("Failure with file during load! {0}")] File(#[from] FileError), #[error("Failure loading config during load! {0}")] Config(#[from] ConfigError), #[error("Failure parsing Git url during load! {0}")] - RepoParse(#[from] RepoParseError) + RepoParse(#[from] RepoParseError), + #[error("Failure getting value of env! {0}")] + Auth(#[from] AuthError), } +struct CliEnvRunState { + envs: Vec, + exe_name: Option, +} - -struct CliEnvState { - context: Option, - no_network: Option, - repo_url: Option, - deploy_path: Option, - tag: Option, +pub(crate) struct CliEnvState { + pub(crate) context: Option, + pub(crate) network: Option, + pub(crate) repo_url: Option, + pub(crate) deploy_path: Option, + pub(crate) tag: Option, } +fn load_state_run_vars() -> CliEnvRunState { + let mut envs_vec = Vec::new(); + + let mut exe_name = None; + + for (k, v) in env::vars() { + if k == "TIDPLOY_EXE" { + exe_name = Some(v) + } else if k.starts_with("TIDPLOY_VAR_") { + let env_name = k.strip_prefix("TIDPLOY_VAR_").unwrap().to_owned(); + envs_vec.push(ConfigVar { env_name, key: v }) + } + } + CliEnvRunState { + envs: envs_vec, + exe_name, + } +} fn load_state_vars() -> CliEnvState { let mut env_state = CliEnvState { context: None, - no_network: None, + network: None, repo_url: None, deploy_path: None, tag: None, @@ -108,97 +119,228 @@ fn load_state_vars() -> CliEnvState { for (k, v) in env::vars() { match k.as_str() { "TIDPLOY_REPO" => env_state.repo_url = Some(v), - "TIDPLOY_NETWORK" => env_state.no_network = Some(v == "0"), + "TIDPLOY_NETWORK" => env_state.network = Some(v != "0" && v.to_lowercase() != "false"), "TIDPLOY_TAG" => env_state.tag = Some(v), - "TIDPLOY_PTH" => env_state.deploy_path = Some(v) + "TIDPLOY_PTH" => env_state.deploy_path = Some(v), + _ => {} } } env_state } -fn merge_options(original: Option, preferred: Option, most_preferred: Option) -> Option { +fn merge_options( + original: Option, + preferred: Option, + most_preferred: Option, +) -> Option { if most_preferred.is_some() { - return most_preferred.clone() + return most_preferred; } if preferred.is_some() { - return preferred.clone() + return preferred; } - original.clone() + original } -fn merge_state(config: DployConfig, envs: CliEnvState, cli: CliEnvState) -> CliEnvState { +fn merge_state(config: &DployConfig, envs: CliEnvState, cli: CliEnvState) -> CliEnvState { CliEnvState { // Already set context: None, - no_network: merge_options(config.network.map(|b| !b), envs.no_network, cli.no_network), - repo_url: merge_options(config.repo_url, envs.repo_url, cli.repo_url), - deploy_path: merge_options(config.deploy_path, envs.deploy_path, cli.deploy_path), - tag: merge_options(config.tag, envs.tag, cli.tag), + network: merge_options(config.network, envs.network, cli.network), + repo_url: merge_options(config.repo_url.clone(), envs.repo_url, cli.repo_url), + deploy_path: merge_options( + config.deploy_path.clone(), + envs.deploy_path, + cli.deploy_path, + ), + tag: merge_options(config.tag.clone(), envs.tag, cli.tag), + } +} + +fn merge_run_state( + config: &DployConfig, + envs: CliEnvRunState, + cli: CliEnvRunState, +) -> CliEnvRunState { + let envs_overwrite_config = merge_vars(config.vars.clone(), Some(envs.envs)); + let cli_overwrite_envs = merge_vars(envs_overwrite_config, Some(cli.envs)).unwrap(); + + CliEnvRunState { + exe_name: merge_options(config.exe_name.clone(), envs.exe_name, cli.exe_name), + envs: cli_overwrite_envs, + } +} + +fn set_state( + state: &mut State, + merged_state: CliEnvState, + merged_run_state: Option, + load_tag: bool, +) -> Result<(), LoadError> { + let repo_url = match state.context { + StateContext::None => match merged_state.repo_url { + Some(value) if value == DEFAULT_INFER => git_root_origin_url()?, // Only infer if explicitly set to infer + Some(value) => value, + None => DEFAULT.to_owned(), // Unset here defaults to just leaving it as 'default' + }, + StateContext::Git => match merged_state.repo_url { + Some(value) if value == DEFAULT_INFER => git_root_origin_url()?, + Some(value) => value, + None => git_root_origin_url()?, + }, + }; + + match repo_url.as_str() { + DEFAULT => { /* Keep as default */ } + _other => state.repo = parse_repo_url(repo_url)?, + } + + if let Some(value) = merged_state.network { + state.network = value + }; + + let tag = match merged_state.tag { + Some(value) => value, + None => TIDPLOY_DEFAULT.to_owned(), + }; + + if let Some(value) = merged_state.deploy_path { + state.deploy_path = RelativePathBuf::from_path(&value).map_err(|e| { + let msg = format!("Failed to get relative path for deploy path: {}!", value); + RelPathError::from_knd(e, msg) + })? + }; + + // TODO maybe infer the tag from the current folder or checked out tag + + // We only want to load the tag when we've actually downloaded the target repository + if load_tag && tag != TIDPLOY_DEFAULT { + state.commit_sha = rev_parse_tag(&tag, state.network)?; + } else { + state.commit_sha = tag; } + + if let Some(merged_run_state) = merged_run_state { + for e in merged_run_state.envs { + let pass = auth_get_password(state, &e.key).map_err(|source| { + let msg = format!("Failed to get password with key {} from passwords while loading envs into state!", e.key); + AuthError { msg, source } + })?; + + state.envs.insert(e.env_name, pass); + } + + if let Some(exe_name) = merged_run_state.exe_name { + state.exe_name = exe_name + } + + if state.exe_name == TIDPLOY_DEFAULT { + state.exe_name = "entrypoint.sh".to_owned(); + } + } + + Ok(()) +} + +pub(crate) fn create_state_pre(cli_state: CliEnvState) -> Result { + create_state(cli_state, None, false) } +fn parse_cli_envs(envs: Vec) -> Vec { + envs.chunks_exact(2) + .map(|c| ConfigVar { + key: c.get(0).unwrap().to_owned(), + env_name: c.get(1).unwrap().to_owned(), + }) + .collect() +} +pub(crate) fn create_state_run( + cli_state: CliEnvState, + exe_name: Option, + envs: Vec, + load_tag: bool, +) -> Result { + let cli_run_state = CliEnvRunState { + exe_name, + envs: parse_cli_envs(envs), + }; + create_state(cli_state, Some(cli_run_state), load_tag) +} -fn create_state(cli_state: CliEnvState) -> Result { +fn create_state( + cli_state: CliEnvState, + cli_run_state: Option, + load_tag: bool, +) -> Result { let mut state = State { network: true, context: StateContext::Git, repo: Repo { name: DEFAULT.to_owned(), url: "".to_owned(), - encoded_url: "".to_owned() + encoded_url: "".to_owned(), }, - deploy_dir: RelativePathBuf::new(), - commit_sha: DEFAULT.to_owned(), + deploy_path: RelativePathBuf::new(), + commit_sha: TIDPLOY_DEFAULT.to_owned(), envs: HashMap::::new(), - exe_name: DEFAULT.to_owned() + exe_name: TIDPLOY_DEFAULT.to_owned(), + current_dir: PathBuf::new(), }; let env_state = load_state_vars(); + let env_run_state = if cli_run_state.is_some() { + Some(load_state_run_vars()) + } else { + None + }; state.context = match cli_state.context { None => match env::var("TIDPLOY_CONTEXT") { - Ok(val) => StateContext::from_str(&val).ok_or(LoadError::BadValue { msg: "Environment value TIDPLOY_CONTEXT is not one of \"none\" or \"git\"!".to_owned() })?, - Err(VarError::NotUnicode(_)) => return Err(LoadError::VarNotUnicode { var: "TIDPLOY_CONTEXT".to_owned() }), - _ => StateContext::Git + Ok(val) => StateContext::from_str(&val).ok_or(LoadError::BadValue { + msg: "Environment value TIDPLOY_CONTEXT is not one of \"none\" or \"git\"!" + .to_owned(), + })?, + Err(VarError::NotUnicode(_)) => { + return Err(LoadError::VarNotUnicode { + var: "TIDPLOY_CONTEXT".to_owned(), + }) + } + _ => StateContext::Git, }, - Some(cli_context) => StateContext::from_str(&cli_context).ok_or(LoadError::BadValue { msg: "Argument for context is not one of \"none\" or \"git\"!".to_owned() })?, + Some(cli_context) => cli_context, }; //let state_env_vars = load_state_vars()?; - let current_dir = get_current_dir().map_err(|source| FileError { source, msg: "Failed to get current dir to use for loading configs!".to_owned() })?; - match state.context { + state.current_dir = get_current_dir().map_err(|source| FileError { + source, + msg: "Failed to get current dir to use for loading configs!".to_owned(), + })?; + let dploy_config = match state.context { StateContext::Git => { let git_root_relative = relative_to_git_root()?; - let git_root_relative = RelativePathBuf::from_path(&git_root_relative).unwrap(); - let dploy_config = traverse_configs(current_dir, git_root_relative)?; - - let merged_state = merge_state(dploy_config, env_state, cli_state); - - let repo_url = match merged_state.repo_url { - Some(value) if value == DEFAULT_INFER => git_root_origin_url()?, - Some(value) => value, - None => git_root_origin_url()? - }; - - match repo_url.as_str() { - DEFAULT => { /* Keep as default */ }, - other => state.repo = parse_repo_url(repo_url)? - } - - - - - Ok() - }, + let git_root_relative = RelativePathBuf::from_path(git_root_relative).unwrap(); + traverse_configs(state.current_dir.clone(), git_root_relative)? + } StateContext::None => { - let dploy_config = load_dploy_config(current_dir) - .map_err(|source| ConfigError { source, msg: "Failed to load config of current dir when loading with context none!".to_owned()})?; + load_dploy_config(state.current_dir.clone()).map_err(|source| ConfigError { + source, + msg: "Failed to load config of current dir when loading with context none!" + .to_owned(), + })? + } + }; - let merged_state = merge_state(dploy_config, env_state, cli_state); + let merged_state = merge_state(&dploy_config, env_state, cli_state); - Ok() - } + if let Some(cli_run_state) = cli_run_state { + let merged_run_state = + merge_run_state(&dploy_config, cli_run_state, env_run_state.unwrap()); + set_state(&mut state, merged_state, Some(merged_run_state), load_tag)?; + } else { + set_state(&mut state, merged_state, None, load_tag)?; } -} \ No newline at end of file + + Ok(state) +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..2319af3 --- /dev/null +++ b/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo $ABC \ No newline at end of file