From 4f1c9a7e49b53d5fac0f15ea2c503b6f4019419f Mon Sep 17 00:00:00 2001 From: Timon Borter Date: Tue, 9 Jul 2024 22:07:41 +0200 Subject: [PATCH] ci: remove github services for devcontainers --- .github/workflows/build.yml | 16 --- Cargo.toml | 6 +- DEVELOPMENT.md | 10 +- dev/podman.sh | 0 src/config.rs | 1 + src/vault.rs | 1 + src/workflow.rs | 5 +- tests/common/mod.rs | 44 +++++++ tests/init_vault.rs | 29 ++++- tests/resources/init_vault/new_path.yml | 7 -- .../rotate/invalid_initialized_secret.yml | 7 -- .../resources/rotate/non_existing_secret.yml | 7 -- tests/resources/rotate/secrets.yml | 7 -- tests/rotate.rs | 118 ++++++++++++++++-- 14 files changed, 191 insertions(+), 67 deletions(-) mode change 100755 => 100644 dev/podman.sh create mode 100644 tests/common/mod.rs delete mode 100644 tests/resources/init_vault/new_path.yml delete mode 100644 tests/resources/rotate/invalid_initialized_secret.yml delete mode 100644 tests/resources/rotate/non_existing_secret.yml delete mode 100644 tests/resources/rotate/secrets.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3246bbf..529fc3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,22 +18,6 @@ jobs: build: name: 'Rust Build' runs-on: ubuntu-latest - services: - postgres: - image: postgres:12.19-alpine3.20 - ports: - - 5432:5432 - env: - POSTGRES_DB: demo - POSTGRES_USER: demo - POSTGRES_PASSWORD: demo_password - vault: - image: hashicorp/vault:1.17.1 - ports: - - 8200:8200 - options: --cap-add=IPC_LOCK - env: - VAULT_DEV_ROOT_TOKEN_ID: 'root-token' steps: - name: Check out code uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 869f4f8..459bc0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,17 @@ clap = { version = "4.5.8", features = ["derive"] } env_logger = "0.11.3" lazy_static = "1.5.0" log = "0.4.22" +postgres = "0.19.7" +rand = "0.9.0-alpha.1" serde = { version = "1.0.203", features = ["derive"] } serde_yaml = "0.9.34+deprecated" tokio = { version = "1.38.0", features = ["macros", "rt"] } vaultrs = "0.7.2" -rand = "0.9.0-alpha.1" -postgres = "0.19.7" [dev-dependencies] assert_cmd = "2.0.14" predicates = "3.1.0" reqwest = { version = "0.12.5", features = ["json"] } serde_json = "1.0.120" +testcontainers = { version = "0.20.0", features = ["blocking"] } +testcontainers-modules = { version = "0.8.0", features = ["blocking", "postgres"] } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 46091bf..9a92eec 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -111,9 +111,15 @@ Cargo makes it easy to run the project's unit and integration tests: cargo tests ``` -**Note that the integration tests need active Vault and PostgreSQL connections, as described [here](#environment-setup).** +**Note that the integration tests make use of [testcontainers](https://testcontainers.com) in order to spin up ArgoCD, Vault and PostgreSQL.** -Cargo will automatically discover and execute the tests defined within the project. +#### A Note for Windows Users + +If testcontainers fail to connect to your Docker socket on Windows, add the below environment variable to the test command: + +```shell +DOCKER_HOST=tcp://localhost:2375 cargo test +``` ### Running the CLI diff --git a/dev/podman.sh b/dev/podman.sh old mode 100755 new mode 100644 diff --git a/src/config.rs b/src/config.rs index 89b4040..fed9f85 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,6 +35,7 @@ pub(crate) fn read_config(config_path: PathBuf) -> Config { serde_yaml::from_str(&config_data).expect("Failed to parse configuration") } +#[cfg(test)] mod tests { use super::*; diff --git a/src/vault.rs b/src/vault.rs index 0c36aea..7926224 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -99,6 +99,7 @@ fn get_vault_client(config: &Config) -> VaultClient { vault_client } +#[cfg(test)] mod tests { use super::*; use crate::config::PostgresConfig; diff --git a/src/workflow.rs b/src/workflow.rs index 12505cc..7a6e431 100644 --- a/src/workflow.rs +++ b/src/workflow.rs @@ -1,7 +1,4 @@ -use std::fmt::format; - use log::{debug, trace}; -use vaultrs::auth::userpass::user::update_password; use crate::cli::RotateArgs; use crate::config::Config; @@ -90,9 +87,9 @@ fn update_passive_user_postgres_password( debug!("Successfully rotated PostgreSQL password of passive user"); } +#[cfg(test)] mod tests { use super::*; - use postgres::Client; #[test] fn switch_active_user_user1_active() { diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..0de07ae --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,44 @@ +use std::env::temp_dir; +use std::fs::File; +use std::io::Write; + +use testcontainers::{ + core::{IntoContainerPort, WaitFor}, + Container, GenericImage, ImageExt, +}; +use testcontainers_modules::postgres::Postgres; +use testcontainers_modules::testcontainers::runners::SyncRunner; + +pub(crate) fn postgres_container() -> Container { + Postgres::default() + .with_env_var("POSTGRES_DB", "demo") + .with_env_var("POSTGRES_USER", "demo") + .with_env_var("POSTGRES_PASSWORD", "demo_password") + .start() + .expect("PostgreSQL database started") +} + +pub(crate) fn vault_container() -> Container { + GenericImage::new("hashicorp/vault", "1.17.1") + .with_exposed_port(8200.tcp()) + .with_wait_for(WaitFor::message_on_stdout( + "==> Vault server started! Log data will stream in below", + )) + .with_env_var("VAULT_DEV_ROOT_TOKEN_ID", "root-token") + .start() + .expect("Vault started") +} + +pub(crate) fn write_string_to_tempfile(content: &str) -> String { + let mut dir = temp_dir(); + let filename = format!("temp_file_{}", rand::random::()); + + dir.push(filename); + + let mut file = File::create(dir.clone()).expect("Failed to create tmp file"); + + file.write_all(content.as_bytes()) + .expect("Failed to write into tmp file"); + + dir.to_string_lossy().to_string() +} diff --git a/tests/init_vault.rs b/tests/init_vault.rs index aa73b1e..cf5e17d 100644 --- a/tests/init_vault.rs +++ b/tests/init_vault.rs @@ -9,16 +9,37 @@ use reqwest::{Client, Response}; use serde_json::Value; use tokio::runtime::{Builder, Runtime}; +mod common; + lazy_static! { static ref BIN_PATH: PathBuf = cargo_bin(env!("CARGO_PKG_NAME")); } #[test] fn init_vault_new_path() { + let vault_container = common::vault_container(); + + let vault_host = vault_container.get_host().unwrap(); + let vault_port = vault_container.get_host_port_ipv4(8200).unwrap(); + Command::new(&*BIN_PATH) .arg("init-vault") .arg("-c") - .arg("tests/resources/init_vault/new_path.yml") + .arg(common::write_string_to_tempfile( + format!( + // language=yaml + " +postgres: + host: 'localhost' + port: 5432 + database: 'demo' +vault: + address: 'http://{vault_host}:{vault_port}' + path: 'init/vault/new/path' +" + ) + .as_str(), + )) .env("VAULT_TOKEN", "root-token") .assert() .success() @@ -27,10 +48,10 @@ fn init_vault_new_path() { )); let client = Client::new(); - let url = "http://localhost:8200/v1/secret/data/init/vault/new/path"; + let url = format!("http://{vault_host}:{vault_port}/v1/secret/data/init/vault/new/path"); let rt: Runtime = create_tokio_runtime(); - let json = read_secret_as_json(client, url, rt); + let json = read_secret_as_json(client, url.as_str(), rt); assert_json_value_equals(&json, "postgresql_active_user", "TBD"); assert_json_value_equals(&json, "postgresql_active_user_password", "TBD"); @@ -42,6 +63,8 @@ fn init_vault_new_path() { #[test] fn init_vault_invalid_url() { + common::vault_container(); + Command::new(&*BIN_PATH) .arg("init-vault") .arg("-c") diff --git a/tests/resources/init_vault/new_path.yml b/tests/resources/init_vault/new_path.yml deleted file mode 100644 index 5fbf668..0000000 --- a/tests/resources/init_vault/new_path.yml +++ /dev/null @@ -1,7 +0,0 @@ -postgres: - host: 'localhost' - port: 5432 - database: 'demo' -vault: - address: 'http://localhost:8200' - path: 'init/vault/new/path' diff --git a/tests/resources/rotate/invalid_initialized_secret.yml b/tests/resources/rotate/invalid_initialized_secret.yml deleted file mode 100644 index 465954f..0000000 --- a/tests/resources/rotate/invalid_initialized_secret.yml +++ /dev/null @@ -1,7 +0,0 @@ -postgres: - host: 'localhost' - port: 5432 - database: 'demo' -vault: - address: 'http://localhost:8200' - path: 'rotate/invalid/initialized/secret' diff --git a/tests/resources/rotate/non_existing_secret.yml b/tests/resources/rotate/non_existing_secret.yml deleted file mode 100644 index 6519b39..0000000 --- a/tests/resources/rotate/non_existing_secret.yml +++ /dev/null @@ -1,7 +0,0 @@ -postgres: - host: 'localhost' - port: 5432 - database: 'demo' -vault: - address: 'http://localhost:8200' - path: 'rotate/non/existing/path' diff --git a/tests/resources/rotate/secrets.yml b/tests/resources/rotate/secrets.yml deleted file mode 100644 index 668b189..0000000 --- a/tests/resources/rotate/secrets.yml +++ /dev/null @@ -1,7 +0,0 @@ -postgres: - host: 'localhost' - port: 5432 - database: 'demo' -vault: - address: 'http://localhost:8200' - path: 'rotate/secrets' diff --git a/tests/rotate.rs b/tests/rotate.rs index cac04eb..fbe681e 100644 --- a/tests/rotate.rs +++ b/tests/rotate.rs @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::runtime::{Builder, Runtime}; +mod common; + lazy_static! { static ref BIN_PATH: PathBuf = cargo_bin(env!("CARGO_PKG_NAME")); } @@ -32,27 +34,59 @@ struct VaultSecretDTO { #[test] fn rotate_secrets() { + let vault_container = common::vault_container(); + + let vault_host = vault_container.get_host().unwrap(); + let vault_port = vault_container.get_host_port_ipv4(8200).unwrap(); + + let postgres_container = common::postgres_container(); + + let postgres_host = postgres_container.get_host().unwrap().to_string(); + let postgres_port = postgres_container + .get_host_port_ipv4(5432) + .unwrap() + .to_string(); + let http_client = Client::new(); - let url = "http://localhost:8200/v1/secret/data/rotate/secrets"; + let url = format!("http://{vault_host}:{vault_port}/v1/secret/data/rotate/secrets"); let rt: Runtime = create_tokio_runtime(); - reset_vault_secret_path(&http_client, url, &rt); + reset_vault_secret_path(&http_client, url.as_str(), &rt); - let mut postgres_client = connect_postgres_client("demo", "demo_password"); + let mut postgres_client = connect_postgres_client( + postgres_host.as_str(), + postgres_port.as_str(), + "demo", + "demo_password", + ); reset_role_initial_password(&mut postgres_client, "user1"); reset_role_initial_password(&mut postgres_client, "user2"); Command::new(&*BIN_PATH) .arg("rotate") .arg("-c") - .arg("tests/resources/rotate/secrets.yml") + .arg(common::write_string_to_tempfile( + format!( + // language=yaml + " +postgres: + host: '{postgres_host}' + port: {postgres_port} + database: 'demo' +vault: + address: 'http://{vault_host}:{vault_port}' + path: 'rotate/secrets' + " + ) + .as_str(), + )) .env("VAULT_TOKEN", "root-token") .assert() .success() .stdout(contains("Successfully rotated all secrets")); - let json = read_secret_as_json(http_client, url, rt); + let json = read_secret_as_json(http_client, url.as_str(), rt); assert_eq!( json["data"]["data"]["postgresql_active_user"] @@ -88,12 +122,16 @@ fn rotate_secrets() { ); connect_postgres_client( + postgres_host.as_str(), + postgres_port.as_str(), "user1", json["data"]["data"]["postgresql_user_1_password"] .as_str() .unwrap(), ); connect_postgres_client( + postgres_host.as_str(), + postgres_port.as_str(), "user2", json["data"]["data"]["postgresql_user_2_password"] .as_str() @@ -103,16 +141,45 @@ fn rotate_secrets() { #[test] fn rotate_invalid_initialized_secret() { + let vault_container = common::vault_container(); + + let vault_host = vault_container.get_host().unwrap(); + let vault_port = vault_container.get_host_port_ipv4(8200).unwrap(); + + let postgres_container = common::postgres_container(); + + let postgres_host = postgres_container.get_host().unwrap().to_string(); + let postgres_port = postgres_container + .get_host_port_ipv4(5432) + .unwrap() + .to_string(); + let http_client = Client::new(); - let url = "http://localhost:8200/v1/secret/data/rotate/invalid/initialized/secret"; + let url = format!( + "http://{vault_host}:{vault_port}/v1/secret/data/rotate/invalid/initialized/secret" + ); let rt: Runtime = create_tokio_runtime(); - create_invalid_vault_secret_path(&http_client, url, &rt); + create_invalid_vault_secret_path(&http_client, url.as_str(), &rt); Command::new(&*BIN_PATH) .arg("rotate") .arg("-c") - .arg("tests/resources/rotate/invalid_initialized_secret.yml") + .arg(common::write_string_to_tempfile( + format!( + // language=yaml + " +postgres: + host: '{postgres_host}' + port: {postgres_port} + database: 'demo' +vault: + address: 'http://{vault_host}:{vault_port}' + path: 'rotate/invalid/initialized/secret' + " + ) + .as_str(), + )) .env("VAULT_TOKEN", "root-token") .assert() .failure() @@ -123,10 +190,37 @@ fn rotate_invalid_initialized_secret() { #[test] fn rotate_non_existing_secret() { + let vault_container = common::vault_container(); + + let vault_host = vault_container.get_host().unwrap(); + let vault_port = vault_container.get_host_port_ipv4(8200).unwrap(); + + let postgres_container = common::postgres_container(); + + let postgres_host = postgres_container.get_host().unwrap().to_string(); + let postgres_port = postgres_container + .get_host_port_ipv4(5432) + .unwrap() + .to_string(); + Command::new(&*BIN_PATH) .arg("rotate") .arg("-c") - .arg("tests/resources/rotate/non_existing_secret.yml") + .arg(common::write_string_to_tempfile( + format!( + // language=yaml + " +postgres: + host: '{postgres_host}' + port: {postgres_port} + database: 'demo' +vault: + address: 'http://{vault_host}:{vault_port}' + path: 'rotate/non/existing/path' + " + ) + .as_str(), + )) .env("VAULT_TOKEN", "root-token") .assert() .failure() @@ -183,9 +277,9 @@ fn write_vault_secret(client: &Client, url: &str, rt: &Runtime, initial_secret: .expect("Error initializing Vault for 'rotate_secrets'"); } -fn connect_postgres_client(user: &str, password: &str) -> postgres::Client { - let mut postgres_client = postgres::Client::connect( - format!("host=localhost port=5432 dbname=demo user={user} password={password}").as_str(), +fn connect_postgres_client(host: &str, port: &str, user: &str, password: &str) -> postgres::Client { + let postgres_client = postgres::Client::connect( + format!("host={host} port={port} dbname=demo user={user} password={password}").as_str(), NoTls, ) .expect("Failed to build PostgreSQL connection");