use reqwest::{Certificate, StatusCode}; use serde::Deserialize; use std::collections::HashMap; use std::ffi::OsStr; use std::io::Read; use std::sync::atomic::{AtomicUsize, Ordering}; use std::{ fs::File, path::{Path, PathBuf}, process::{Command, Stdio}, time::Duration, }; use xand_utils::{ procutils::{ChildKillMode, KillChildOnDrop}, timeout, }; static NEXT_AVAILABLE_EXTERNAL_PORT: AtomicUsize = AtomicUsize::new(8202); // The vault version must be kept in sync with that used by the ci job in .gitlab-ci.yaml const VAULT_DOCKER_IMAGE: &str = "vault:1.5.0"; const CLIENT_TIMEOUT: Duration = Duration::from_secs(30); const INIT_CHECK_INTERVAL: Duration = Duration::from_millis(200); const INTERNAL_VAULT_PORT: usize = 8202; const PRIVATE_ADMIN_VAULT_ADDRESS: &str = "http://127.0.0.1:8200"; const VAULT_PEM_CERT_PATH: &str = "tests/certs/valid_test_cert_chain/rootCA.pem"; pub struct TestVault { _vault_handle: KillChildOnDrop, container_name: String, pub vault_address: String, pub vault_token: String, } pub enum TlsConfiguration { Enabled, Disabled, } impl TestVault { pub async fn start(tls_configuration: TlsConfiguration) -> Self { let external_vault_port = Self::get_next_external_port(); let address_scheme = match tls_configuration { TlsConfiguration::Enabled => "https", TlsConfiguration::Disabled => "http", }; let config_file_name = match tls_configuration { TlsConfiguration::Enabled => "vault_tls_enabled.json", TlsConfiguration::Disabled => "vault_tls_disabled.json", }; let vault_address = format!("{}://localhost:{}", address_scheme, external_vault_port); let container_name = format!("integ-test-vault-{}", external_vault_port); let vault_token = String::from("fake-vault-token"); let cmd = { let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let tests_root = crate_root.join("tests"); let mut cmd = Command::new("docker"); cmd.arg("run") // Docker options .arg("--rm") .arg("--name").arg(&container_name) .arg("-v").arg(format!("{}/vault_configs:/config:ro", tests_root.to_str().unwrap())) .arg("-v").arg(format!("{}/certs/valid_test_cert_chain:/certs:ro", tests_root.to_str().unwrap())) .arg("-p").arg(format!("{}:{}", external_vault_port, INTERNAL_VAULT_PORT)) // Image to run .arg(VAULT_DOCKER_IMAGE) // Vault options .arg("server") .arg("-config").arg(format!("/config/{}", config_file_name)) .arg("-dev") .arg("-dev-root-token-id").arg(&vault_token); let log_dir = crate_root.join("test_logs"); Self::configure_container_logging( &mut cmd, &log_dir, &format!("vault-{}", external_vault_port), ); cmd }; let mut vault_handle = KillChildOnDrop::with_kill_mode(cmd, ChildKillMode::SIGTERM); vault_handle.spawn().unwrap(); Self::block_until_vault_healthy(&vault_address, tls_configuration).await; let test_vault = TestVault { _vault_handle: vault_handle, container_name, vault_address, vault_token, }; test_vault .run_command_in_vault_docker_container(&["vault", "secrets", "disable", "secret"]); test_vault.run_command_in_vault_docker_container(&[ "vault", "secrets", "enable", "-version=1", "-path=secret/", "kv", ]); test_vault } fn configure_container_logging(cmd: &mut Command, log_dir: &Path, log_file_basename: &str) { Self::crate_dir_if_nonexistent(log_dir).unwrap(); let stdout_log_file_path = log_dir.join(format!("{}.stdout.txt", log_file_basename)); let stderr_log_file_path = log_dir.join(format!("{}.stderr.txt", log_file_basename)); let stdout_file = File::create(&stdout_log_file_path).unwrap(); let stderr_file = File::create(&stderr_log_file_path).unwrap(); println!("Vault logs:"); println!("\tstdout: {}", stdout_log_file_path.to_string_lossy()); println!("\tstderr: {}", stderr_log_file_path.to_string_lossy()); cmd.stderr(stderr_file).stdout(stdout_file); } fn crate_dir_if_nonexistent>(path: P) -> Result<(), std::io::Error> { std::fs::create_dir(path).or_else(|e| { if e.kind() == std::io::ErrorKind::AlreadyExists { Ok(()) } else { Err(e) } }) } fn get_next_external_port() -> usize { NEXT_AVAILABLE_EXTERNAL_PORT.fetch_add(1, Ordering::SeqCst) } fn load_vault_cert() -> Certificate { let mut buf = Vec::new(); File::open(VAULT_PEM_CERT_PATH) .unwrap() .read_to_end(&mut buf) .unwrap(); Certificate::from_pem(&buf).unwrap() } async fn block_until_vault_healthy(vault_address: &str, tls_configuration: TlsConfiguration) { let mut builder = reqwest::Client::builder().timeout(CLIENT_TIMEOUT); if let TlsConfiguration::Enabled = tls_configuration { builder = builder.add_root_certificate(Self::load_vault_cert()) } let client = builder.build().unwrap(); let health_url = format!("{}/v1/sys/health", vault_address); timeout!(CLIENT_TIMEOUT, INIT_CHECK_INTERVAL, { let res = client.get(&health_url).send().await; match res { Ok(response) => { response.status() == StatusCode::OK && response.json::().await.unwrap().initialized } Err(e) => { println!("Waiting for Vault initialization. Returned error: {:?}", e); false } } }); } fn run_command_in_vault_docker_container(&self, args: I) where I: IntoIterator + Clone, S: AsRef, { let command_debug_repr: Vec = args .clone() .into_iter() .map(|s| s.as_ref().to_string_lossy().to_string()) .collect(); println!( "Executing command {:?} in container {}...", command_debug_repr, &self.container_name ); let child = Command::new("docker") .arg("exec") // Environment vars pointing to running Vault .arg("--env").arg(format!("VAULT_ADDR={}", PRIVATE_ADMIN_VAULT_ADDRESS)) .arg("--env").arg(format!("VAULT_TOKEN={}", self.vault_token)) // Current Vault container name .arg(&self.container_name) // Command to run .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let output = child.wait_with_output().unwrap(); println!( "\tstdout: {:?}", String::from_utf8_lossy(output.stdout.as_slice()) ); println!( "\tstderr: {:?}", String::from_utf8_lossy(output.stderr.as_slice()) ); assert_eq!(0, output.status.code().unwrap()); } pub fn set_secrets(&self, path: &str, secrets: HashMap) { let mut args = vec![ String::from("vault"), String::from("kv"), String::from("put"), String::from(path), ]; for (key, value) in secrets { args.push(format!("{}={}", key, value)); } self.run_command_in_vault_docker_container(args); } pub fn seal_vault(&self) { let args = vec![ String::from("vault"), String::from("operator"), String::from("seal"), ]; self.run_command_in_vault_docker_container(args); } } #[derive(Debug, Deserialize)] struct HealthResponse { initialized: bool, }