From eb9a025e2fe8aa765fcae9761b233104d903829c Mon Sep 17 00:00:00 2001 From: Lucas Kent Date: Wed, 12 Apr 2023 15:41:34 +1000 Subject: [PATCH] Remove shotover specific logic from DockerCompose --- custom-transforms-example/tests/test.rs | 4 +- shotover-proxy/benches/benches/cassandra.rs | 7 +- shotover-proxy/benches/benches/redis.rs | 5 +- shotover-proxy/examples/cassandra_bench.rs | 4 +- .../examples/cassandra_cluster_bench.rs | 4 +- .../examples/cassandra_cluster_flamegraph.rs | 4 +- .../examples/cassandra_flamegraph.rs | 4 +- shotover-proxy/examples/kafka_bench.rs | 6 +- shotover-proxy/examples/kafka_flamegraph.rs | 4 +- .../cluster/single_rack_v4.rs | 3 +- .../tests/cassandra_int_tests/mod.rs | 45 +- shotover-proxy/tests/examples/mod.rs | 4 +- shotover-proxy/tests/kafka_int_tests/mod.rs | 7 +- shotover-proxy/tests/redis_int_tests/mod.rs | 22 +- shotover-proxy/tests/transforms/tee.rs | 6 +- test-helpers/src/cert.rs | 2 +- test-helpers/src/docker_compose.rs | 463 ++++-------------- test-helpers/src/docker_compose_runner.rs | 281 +++++++++++ test-helpers/src/flamegraph.rs | 2 +- test-helpers/src/latte.rs | 8 +- test-helpers/src/lib.rs | 1 + 21 files changed, 447 insertions(+), 439 deletions(-) create mode 100644 test-helpers/src/docker_compose_runner.rs diff --git a/custom-transforms-example/tests/test.rs b/custom-transforms-example/tests/test.rs index b5b67b9ee..8f756e7e9 100644 --- a/custom-transforms-example/tests/test.rs +++ b/custom-transforms-example/tests/test.rs @@ -2,13 +2,13 @@ use redis::aio::Connection; use redis::Cmd; use std::time::Duration; use test_helpers::connection::redis_connection; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::shotover_process::{BinProcess, EventMatcher, Level}; #[tokio::test(flavor = "multi_thread")] async fn test_custom_transform() { // Setup shotover and the redis server it connects to - let _compose = DockerCompose::new("config/docker-compose.yaml"); + let _compose = docker_compose("config/docker-compose.yaml"); let shotover = shotover_proxy("config/topology.yaml").await; let mut connection = redis_connection::new_async(6379).await; diff --git a/shotover-proxy/benches/benches/cassandra.rs b/shotover-proxy/benches/benches/cassandra.rs index 02083bb1e..7eb8ea112 100644 --- a/shotover-proxy/benches/benches/cassandra.rs +++ b/shotover-proxy/benches/benches/cassandra.rs @@ -3,7 +3,8 @@ use criterion::{criterion_group, Criterion}; use test_helpers::connection::cassandra::{ CassandraConnection, CassandraConnectionBuilder, CassandraDriver, }; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; +use test_helpers::docker_compose_runner::DockerCompose; use test_helpers::lazy::new_lazy_shared; use test_helpers::shotover_process::{BinProcess, ShotoverProcessBuilder}; use tokio::runtime::Runtime; @@ -231,7 +232,7 @@ impl BenchResources { .enable_all() .build() .unwrap(); - let compose = DockerCompose::new(compose_file); + let compose = docker_compose(compose_file); let shotover = Some( tokio.block_on(ShotoverProcessBuilder::new_with_topology(shotover_topology).start()), ); @@ -260,7 +261,7 @@ impl BenchResources { .enable_all() .build() .unwrap(); - let compose = DockerCompose::new(compose_file); + let compose = docker_compose(compose_file); let shotover = Some( tokio.block_on(ShotoverProcessBuilder::new_with_topology(shotover_topology).start()), ); diff --git a/shotover-proxy/benches/benches/redis.rs b/shotover-proxy/benches/benches/redis.rs index 2869bbd3c..2d32454fc 100644 --- a/shotover-proxy/benches/benches/redis.rs +++ b/shotover-proxy/benches/benches/redis.rs @@ -2,7 +2,8 @@ use criterion::{criterion_group, Criterion}; use redis::Cmd; use std::path::Path; use test_helpers::connection::redis_connection; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; +use test_helpers::docker_compose_runner::DockerCompose; use test_helpers::lazy::new_lazy_shared; use test_helpers::shotover_process::{BinProcess, ShotoverProcessBuilder}; use tokio::runtime::Runtime; @@ -168,7 +169,7 @@ impl BenchResources { .enable_all() .build() .unwrap(); - let compose = DockerCompose::new(compose_file); + let compose = docker_compose(compose_file); let shotover = Some( tokio.block_on(ShotoverProcessBuilder::new_with_topology(shotover_topology).start()), ); diff --git a/shotover-proxy/examples/cassandra_bench.rs b/shotover-proxy/examples/cassandra_bench.rs index 34784e33f..f20930eb6 100644 --- a/shotover-proxy/examples/cassandra_bench.rs +++ b/shotover-proxy/examples/cassandra_bench.rs @@ -1,5 +1,5 @@ use clap::Parser; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::latte::Latte; use test_helpers::shotover_process::ShotoverProcessBuilder; @@ -25,7 +25,7 @@ async fn main() { let latte = Latte::new(args.rate, 1); let bench = "read"; { - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", args.config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", args.config_dir)); let shotover = ShotoverProcessBuilder::new_with_topology(&format!( "{}/topology.yaml", diff --git a/shotover-proxy/examples/cassandra_cluster_bench.rs b/shotover-proxy/examples/cassandra_cluster_bench.rs index dbdd8cdbc..8041bd6ea 100644 --- a/shotover-proxy/examples/cassandra_cluster_bench.rs +++ b/shotover-proxy/examples/cassandra_cluster_bench.rs @@ -1,4 +1,4 @@ -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::latte::Latte; use test_helpers::shotover_process::ShotoverProcessBuilder; @@ -10,7 +10,7 @@ async fn main() { let config_dir = "example-configs/cassandra-cluster-v4"; let bench = "read"; { - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", config_dir)); let shotover = ShotoverProcessBuilder::new_with_topology(&format!("{}/topology.yaml", config_dir)) .start() diff --git a/shotover-proxy/examples/cassandra_cluster_flamegraph.rs b/shotover-proxy/examples/cassandra_cluster_flamegraph.rs index d7cead24b..e7d0a6f69 100644 --- a/shotover-proxy/examples/cassandra_cluster_flamegraph.rs +++ b/shotover-proxy/examples/cassandra_cluster_flamegraph.rs @@ -1,4 +1,4 @@ -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::flamegraph::Perf; use test_helpers::latte::Latte; use test_helpers::shotover_process::ShotoverProcessBuilder; @@ -17,7 +17,7 @@ async fn main() { let config_dir = "example-configs/cassandra-cluster-v4"; let bench = "read"; { - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", config_dir)); latte.init(bench, "172.16.1.2:9044"); let shotover = diff --git a/shotover-proxy/examples/cassandra_flamegraph.rs b/shotover-proxy/examples/cassandra_flamegraph.rs index 2c42b5320..ebef13a30 100644 --- a/shotover-proxy/examples/cassandra_flamegraph.rs +++ b/shotover-proxy/examples/cassandra_flamegraph.rs @@ -1,4 +1,4 @@ -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::flamegraph::Perf; use test_helpers::latte::Latte; use test_helpers::shotover_process::ShotoverProcessBuilder; @@ -17,7 +17,7 @@ async fn main() { let config_dir = "example-configs/cassandra-passthrough"; let bench = "read"; { - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", config_dir)); latte.init(bench, "localhost:9043"); let shotover = diff --git a/shotover-proxy/examples/kafka_bench.rs b/shotover-proxy/examples/kafka_bench.rs index 99e2f56d1..33396740f 100644 --- a/shotover-proxy/examples/kafka_bench.rs +++ b/shotover-proxy/examples/kafka_bench.rs @@ -1,4 +1,4 @@ -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::kafka_producer_perf_test::run_producer_bench; use test_helpers::shotover_process::ShotoverProcessBuilder; @@ -8,7 +8,7 @@ async fn main() { let config_dir = "tests/test-configs/kafka/passthrough"; { - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", config_dir)); let shotover = ShotoverProcessBuilder::new_with_topology(&format!("{}/topology.yaml", config_dir)) .start() @@ -21,7 +21,7 @@ async fn main() { } // restart the docker container to avoid running out of disk space - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", config_dir)); println!("\nBenching Direct Kafka ..."); run_producer_bench("[localhost]:9092"); } diff --git a/shotover-proxy/examples/kafka_flamegraph.rs b/shotover-proxy/examples/kafka_flamegraph.rs index 76c2b6a1b..5c41f0696 100644 --- a/shotover-proxy/examples/kafka_flamegraph.rs +++ b/shotover-proxy/examples/kafka_flamegraph.rs @@ -1,4 +1,4 @@ -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::flamegraph::Perf; use test_helpers::kafka_producer_perf_test::run_producer_bench; use test_helpers::shotover_process::ShotoverProcessBuilder; @@ -14,7 +14,7 @@ async fn main() { test_helpers::bench::init(); let config_dir = "tests/test-configs/kafka/passthrough"; { - let _compose = DockerCompose::new(&format!("{}/docker-compose.yaml", config_dir)); + let _compose = docker_compose(&format!("{}/docker-compose.yaml", config_dir)); let shotover = ShotoverProcessBuilder::new_with_topology(&format!("{}/topology.yaml", config_dir)) diff --git a/shotover-proxy/tests/cassandra_int_tests/cluster/single_rack_v4.rs b/shotover-proxy/tests/cassandra_int_tests/cluster/single_rack_v4.rs index 5e074ad66..61702fb71 100644 --- a/shotover-proxy/tests/cassandra_int_tests/cluster/single_rack_v4.rs +++ b/shotover-proxy/tests/cassandra_int_tests/cluster/single_rack_v4.rs @@ -7,7 +7,8 @@ use test_helpers::connection::cassandra::{ assert_query_result, run_query, CassandraConnection, CassandraConnectionBuilder, CassandraDriver, ResultValue, }; -use test_helpers::docker_compose::DockerCompose; + +use test_helpers::docker_compose_runner::DockerCompose; use tokio::sync::broadcast; use tokio::time::timeout; diff --git a/shotover-proxy/tests/cassandra_int_tests/mod.rs b/shotover-proxy/tests/cassandra_int_tests/mod.rs index b0ebccb62..085612c6c 100644 --- a/shotover-proxy/tests/cassandra_int_tests/mod.rs +++ b/shotover-proxy/tests/cassandra_int_tests/mod.rs @@ -14,7 +14,7 @@ use test_helpers::connection::cassandra::{ }; use test_helpers::connection::cassandra::{Compression, ProtocolVersion}; use test_helpers::connection::redis_connection; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::{docker_compose, new_moto}; use test_helpers::shotover_process::{Count, EventMatcher, Level, ShotoverProcessBuilder}; use tokio::time::{timeout, Duration}; @@ -59,7 +59,7 @@ where #[tokio::test(flavor = "multi_thread")] #[serial] async fn passthrough_standard(#[case] driver: CassandraDriver) { - let _compose = DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-passthrough/topology.yaml", @@ -81,7 +81,7 @@ async fn passthrough_standard(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn passthrough_encode(#[case] driver: CassandraDriver) { - let _compose = DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-passthrough/topology-encode.yaml", @@ -104,7 +104,7 @@ async fn passthrough_encode(#[case] driver: CassandraDriver) { #[serial] async fn source_tls_and_single_tls(#[case] driver: CassandraDriver) { test_helpers::cert::generate_cassandra_test_certs(); - let _compose = DockerCompose::new("example-configs/cassandra-tls/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-tls/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology("example-configs/cassandra-tls/topology.yaml") @@ -145,7 +145,7 @@ async fn source_tls_and_single_tls(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn cluster_single_rack_v3(#[case] driver: CassandraDriver) { - let _compose = DockerCompose::new("example-configs/cassandra-cluster-v3/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-cluster-v3/docker-compose.yaml"); { let shotover = ShotoverProcessBuilder::new_with_topology( @@ -181,7 +181,7 @@ async fn cluster_single_rack_v3(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn cluster_single_rack_v4(#[case] driver: CassandraDriver) { - let compose = DockerCompose::new("example-configs/cassandra-cluster-v4/docker-compose.yaml"); + let compose = docker_compose("example-configs/cassandra-cluster-v4/docker-compose.yaml"); let connection = || async { let mut connection = CassandraConnectionBuilder::new("127.0.0.1", 9042, driver) @@ -245,7 +245,7 @@ async fn cluster_single_rack_v4(#[case] driver: CassandraDriver) { #[serial] async fn cluster_multi_rack(#[case] driver: CassandraDriver) { let _compose = - DockerCompose::new("example-configs/cassandra-cluster-multi-rack/docker-compose.yaml"); + docker_compose("example-configs/cassandra-cluster-multi-rack/docker-compose.yaml"); { let shotover_rack1 = ShotoverProcessBuilder::new_with_topology( @@ -300,7 +300,7 @@ async fn source_tls_and_cluster_tls(#[case] driver: CassandraDriver) { test_helpers::cert::generate_cassandra_test_certs(); let ca_cert = "example-configs/docker-images/cassandra-tls-4.0.6/certs/localhost_CA.crt"; - let _compose = DockerCompose::new("example-configs/cassandra-cluster-tls/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-cluster-tls/docker-compose.yaml"); { let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-cluster-tls/topology.yaml", @@ -349,7 +349,7 @@ async fn source_tls_and_cluster_tls(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn cassandra_redis_cache(#[case] driver: CassandraDriver) { - let _compose = DockerCompose::new("example-configs/cassandra-redis-cache/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-redis-cache/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-redis-cache/topology.yaml", @@ -380,8 +380,7 @@ async fn cassandra_redis_cache(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn protect_transform_local(#[case] driver: CassandraDriver) { - let _compose = - DockerCompose::new("example-configs/cassandra-protect-local/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-protect-local/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-protect-local/topology.yaml", @@ -407,8 +406,8 @@ async fn protect_transform_local(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn protect_transform_aws(#[case] driver: CassandraDriver) { - let _compose = DockerCompose::new("example-configs/cassandra-protect-aws/docker-compose.yaml"); - let _compose_aws = DockerCompose::new_moto(); + let _compose = docker_compose("example-configs/cassandra-protect-aws/docker-compose.yaml"); + let _compose_aws = new_moto(); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-protect-aws/topology.yaml", @@ -434,7 +433,7 @@ async fn protect_transform_aws(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn peers_rewrite_v4(#[case] driver: CassandraDriver) { - let _docker_compose = DockerCompose::new( + let _docker_compose = docker_compose( "tests/test-configs/cassandra-peers-rewrite/docker-compose-4.0-cassandra.yaml", ); @@ -536,7 +535,7 @@ async fn peers_rewrite_v4(#[case] driver: CassandraDriver) { #[tokio::test(flavor = "multi_thread")] #[serial] async fn peers_rewrite_v3(#[case] driver: CassandraDriver) { - let _docker_compose = DockerCompose::new( + let _docker_compose = docker_compose( "tests/test-configs/cassandra-peers-rewrite/docker-compose-3.11-cassandra.yaml", ); @@ -575,7 +574,7 @@ async fn peers_rewrite_v3(#[case] driver: CassandraDriver) { #[serial] async fn request_throttling(#[case] driver: CassandraDriver) { let _docker_compose = - DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "tests/test-configs/cassandra-request-throttling.yaml", @@ -682,8 +681,7 @@ async fn request_throttling(#[case] driver: CassandraDriver) { #[serial] async fn compression_single(#[case] driver: CassandraDriver) { async fn test(driver: CassandraDriver, topology_path: &str, compression: Compression) { - let _compose = - DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology(topology_path) .start() .await; @@ -715,8 +713,7 @@ async fn compression_single(#[case] driver: CassandraDriver) { #[serial] async fn compression_cluster(#[case] driver: CassandraDriver) { async fn test(driver: CassandraDriver, topology_path: &str, compression: Compression) { - let _compose = - DockerCompose::new("example-configs/cassandra-cluster-v4/docker-compose.yaml"); + let _compose = docker_compose("example-configs/cassandra-cluster-v4/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology(topology_path) .start() .await; @@ -750,7 +747,7 @@ async fn compression_cluster(#[case] driver: CassandraDriver) { #[serial] async fn events_keyspace(#[case] driver: CassandraDriver) { let _docker_compose = - DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-passthrough/topology.yaml", @@ -791,7 +788,7 @@ async fn events_keyspace(#[case] driver: CassandraDriver) { #[serial] async fn test_protocol_v3(#[case] driver: CassandraDriver) { let _docker_compose = - DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-passthrough/topology-encode.yaml", @@ -816,7 +813,7 @@ async fn test_protocol_v3(#[case] driver: CassandraDriver) { #[serial] async fn test_protocol_v4(#[case] driver: CassandraDriver) { let _docker_compose = - DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-passthrough/topology-encode.yaml", @@ -841,7 +838,7 @@ async fn test_protocol_v4(#[case] driver: CassandraDriver) { #[serial] async fn test_protocol_v5_single(#[case] driver: CassandraDriver) { let _docker_compose = - DockerCompose::new("example-configs/cassandra-passthrough/docker-compose.yaml"); + docker_compose("example-configs/cassandra-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/cassandra-passthrough/topology-encode.yaml", diff --git a/shotover-proxy/tests/examples/mod.rs b/shotover-proxy/tests/examples/mod.rs index adcf96175..4586e6ebb 100644 --- a/shotover-proxy/tests/examples/mod.rs +++ b/shotover-proxy/tests/examples/mod.rs @@ -3,13 +3,13 @@ use serial_test::serial; use test_helpers::connection::cassandra::{ assert_query_result, CassandraConnectionBuilder, CassandraDriver, ResultValue, }; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; #[tokio::test(flavor = "multi_thread")] #[serial] async fn test_cassandra_rewrite_peers_example() { let _docker_compose = - DockerCompose::new("example-configs-docker/cassandra-peers-rewrite/docker-compose.yaml"); + docker_compose("example-configs-docker/cassandra-peers-rewrite/docker-compose.yaml"); let connection = CassandraConnectionBuilder::new("172.16.1.2", 9043, CassandraDriver::Datastax) .build() diff --git a/shotover-proxy/tests/kafka_int_tests/mod.rs b/shotover-proxy/tests/kafka_int_tests/mod.rs index cb6eefadb..9ababef8d 100644 --- a/shotover-proxy/tests/kafka_int_tests/mod.rs +++ b/shotover-proxy/tests/kafka_int_tests/mod.rs @@ -1,5 +1,6 @@ use serial_test::serial; -use test_helpers::{docker_compose::DockerCompose, shotover_process::ShotoverProcessBuilder}; +use test_helpers::docker_compose::docker_compose; +use test_helpers::shotover_process::ShotoverProcessBuilder; mod test_cases; @@ -7,7 +8,7 @@ mod test_cases; #[serial] async fn passthrough_standard() { let _docker_compose = - DockerCompose::new("tests/test-configs/kafka/passthrough/docker-compose.yaml"); + docker_compose("tests/test-configs/kafka/passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "tests/test-configs/kafka/passthrough/topology.yaml", ) @@ -23,7 +24,7 @@ async fn passthrough_standard() { #[serial] async fn passthrough_encode() { let _docker_compose = - DockerCompose::new("tests/test-configs/kafka/passthrough/docker-compose.yaml"); + docker_compose("tests/test-configs/kafka/passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "tests/test-configs/kafka/passthrough/topology-encode.yaml", ) diff --git a/shotover-proxy/tests/redis_int_tests/mod.rs b/shotover-proxy/tests/redis_int_tests/mod.rs index d03c6ebe5..830b12660 100644 --- a/shotover-proxy/tests/redis_int_tests/mod.rs +++ b/shotover-proxy/tests/redis_int_tests/mod.rs @@ -7,7 +7,7 @@ use std::path::Path; use std::thread::sleep; use std::time::Duration; use test_helpers::connection::redis_connection; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::shotover_process::{self, Count, EventMatcher, Level}; pub mod assert; @@ -28,7 +28,7 @@ Caused by: #[tokio::test(flavor = "multi_thread")] #[serial] async fn passthrough() { - let _compose = DockerCompose::new("example-configs/redis-passthrough/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/redis-passthrough/topology.yaml", ) @@ -85,7 +85,7 @@ async fn cluster_tls() { test_helpers::cert::generate_redis_test_certs(Path::new("example-configs/redis-tls/certs")); { - let _compose = DockerCompose::new("example-configs/redis-cluster-tls/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-cluster-tls/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/redis-cluster-tls/topology.yaml", ) @@ -104,7 +104,7 @@ async fn cluster_tls() { // Quick test to verify it works with private key { let _compose = - DockerCompose::new("example-configs/redis-cluster-tls/docker-compose-with-key.yaml"); + docker_compose("example-configs/redis-cluster-tls/docker-compose-with-key.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/redis-cluster-tls/topology-with-key.yaml", ) @@ -122,7 +122,7 @@ async fn cluster_tls() { async fn source_tls_and_single_tls() { test_helpers::cert::generate_redis_test_certs(Path::new("example-configs/redis-tls/certs")); - let _compose = DockerCompose::new("example-configs/redis-tls/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-tls/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology("example-configs/redis-tls/topology.yaml") .start() @@ -141,7 +141,7 @@ async fn source_tls_and_single_tls() { #[serial] async fn cluster_ports_rewrite() { let _compose = - DockerCompose::new("tests/test-configs/redis-cluster-ports-rewrite/docker-compose.yaml"); + docker_compose("tests/test-configs/redis-cluster-ports-rewrite/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "tests/test-configs/redis-cluster-ports-rewrite/topology.yaml", ) @@ -162,7 +162,7 @@ async fn cluster_ports_rewrite() { #[tokio::test(flavor = "multi_thread")] #[serial] async fn multi() { - let _compose = DockerCompose::new("example-configs/redis-multi/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-multi/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology("example-configs/redis-multi/topology.yaml") .start() @@ -190,7 +190,7 @@ Caused by: #[tokio::test(flavor = "multi_thread")] #[serial] async fn cluster_auth() { - let _compose = DockerCompose::new("tests/test-configs/redis-cluster-auth/docker-compose.yaml"); + let _compose = docker_compose("tests/test-configs/redis-cluster-auth/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "tests/test-configs/redis-cluster-auth/topology.yaml", ) @@ -207,7 +207,7 @@ async fn cluster_auth() { #[tokio::test(flavor = "multi_thread")] #[serial] async fn cluster_hiding() { - let _compose = DockerCompose::new("example-configs/redis-cluster-hiding/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-cluster-hiding/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/redis-cluster-hiding/topology.yaml", ) @@ -228,7 +228,7 @@ async fn cluster_hiding() { #[tokio::test(flavor = "multi_thread")] #[serial] async fn cluster_handling() { - let _compose = DockerCompose::new("example-configs/redis-cluster-handling/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-cluster-handling/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "example-configs/redis-cluster-handling/topology.yaml", ) @@ -250,7 +250,7 @@ async fn cluster_handling() { #[tokio::test(flavor = "multi_thread")] #[serial] async fn cluster_dr() { - let _compose = DockerCompose::new("example-configs/redis-cluster-dr/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-cluster-dr/docker-compose.yaml"); let nodes = vec![ "redis://127.0.0.1:2120/", diff --git a/shotover-proxy/tests/transforms/tee.rs b/shotover-proxy/tests/transforms/tee.rs index 4e3449374..4fccb15c5 100644 --- a/shotover-proxy/tests/transforms/tee.rs +++ b/shotover-proxy/tests/transforms/tee.rs @@ -1,6 +1,6 @@ use serial_test::serial; use test_helpers::connection::redis_connection; -use test_helpers::docker_compose::DockerCompose; +use test_helpers::docker_compose::docker_compose; use test_helpers::shotover_process::ShotoverProcessBuilder; #[tokio::test(flavor = "multi_thread")] @@ -91,7 +91,7 @@ async fn test_fail_with_mismatch() { #[tokio::test(flavor = "multi_thread")] #[serial] async fn test_subchain_matches() { - let _compose = DockerCompose::new("example-configs/redis-passthrough/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology("tests/test-configs/tee/subchain.yaml") .start() @@ -128,7 +128,7 @@ async fn test_subchain_matches() { #[tokio::test(flavor = "multi_thread")] #[serial] async fn test_subchain_with_mismatch() { - let _compose = DockerCompose::new("example-configs/redis-passthrough/docker-compose.yaml"); + let _compose = docker_compose("example-configs/redis-passthrough/docker-compose.yaml"); let shotover = ShotoverProcessBuilder::new_with_topology( "tests/test-configs/tee/subchain_with_mismatch.yaml", ) diff --git a/test-helpers/src/cert.rs b/test-helpers/src/cert.rs index 364be3b73..9372a1888 100644 --- a/test-helpers/src/cert.rs +++ b/test-helpers/src/cert.rs @@ -1,4 +1,4 @@ -use crate::docker_compose::run_command; +use crate::docker_compose_runner::run_command; use rcgen::{BasicConstraints, Certificate, CertificateParams, DnType, IsCa, SanType}; use std::path::Path; diff --git a/test-helpers/src/docker_compose.rs b/test-helpers/src/docker_compose.rs index 6707949f5..dca8d03c4 100644 --- a/test-helpers/src/docker_compose.rs +++ b/test-helpers/src/docker_compose.rs @@ -1,14 +1,5 @@ -use anyhow::{anyhow, Result}; -use regex::Regex; -use serde_yaml::Value; -use std::collections::HashMap; -use std::fmt::Write; -use std::io::ErrorKind; -use std::process::Command; -use std::time::{self, Duration}; +use crate::docker_compose_runner::*; use std::{env, path::Path}; -use subprocess::{Exec, Redirection}; -use tracing::trace; use tracing_subscriber::fmt::TestWriter; fn setup_tracing_subscriber_for_test_logic() { @@ -19,380 +10,114 @@ fn setup_tracing_subscriber_for_test_logic() { .ok(); } -/// Runs a command and returns the output as a string. -/// -/// Both stderr and stdout are returned in the result. -/// -/// # Arguments -/// * `command` - The system command to run -/// * `args` - An array of command line arguments for the command -pub fn run_command(command: &str, args: &[&str]) -> Result { - trace!("executing {}", command); - let data = Exec::cmd(command) - .args(args) - .stdout(Redirection::Pipe) - .stderr(Redirection::Merge) - .capture()?; - - if data.exit_status.success() { - Ok(data.stdout_str()) - } else { - Err(anyhow!( - "command {} {:?} exited with {:?} and output:\n{}", - command, - args, - data.exit_status, - data.stdout_str() - )) - } -} - -#[must_use] -pub struct DockerCompose { - file_path: String, +pub fn docker_compose(file_path: &str) -> DockerCompose { + setup_tracing_subscriber_for_test_logic(); + DockerCompose::new(get_image_waiters(), build_images, file_path) } -impl DockerCompose { - /// Creates a new DockerCompose object by submitting a file to the underlying docker-compose - /// system. Executes `docker-compose -f [file_path] up -d` - /// - /// Will spin until it detects all the containers have started up by inspecting the logs for a magic string. - /// This logic is implemented internally per docker image name. - /// If a service uses an image that hasnt had this logic implemented for it yet - /// a panic will occur instructing the developer to implement this logic. - /// - /// # Arguments - /// * `file_path` - The path to the docker-compose yaml file. - /// - /// # Panics - /// * Will panic if docker-compose is not installed - pub fn new(file_path: &str) -> Self { - setup_tracing_subscriber_for_test_logic(); - - if let Err(ErrorKind::NotFound) = Command::new("docker-compose") - .output() - .map_err(|e| e.kind()) - { - panic!("Could not find docker-compose. Have you installed it?"); - } - - // It is critical that clean_up is run before everything else as the internal `docker-compose` commands act as validation - // for the docker-compose.yaml file that we later manually parse with poor error handling - DockerCompose::clean_up(file_path).unwrap(); - - let service_to_image = DockerCompose::get_service_to_image(file_path); +/// Creates a new DockerCompose running an instance of moto the AWS mocking server +pub fn new_moto() -> DockerCompose { + // Overwrite any existing AWS credential env vars belonging to the user with dummy values to be sure that + // we wont hit their real AWS account in the case of a bug in shotover or the test + env::set_var("AWS_ACCESS_KEY_ID", "dummy-access-key"); + env::set_var("AWS_SECRET_ACCESS_KEY", "dummy-access-key-secret"); - DockerCompose::build_images(&service_to_image); - - run_command("docker-compose", &["-f", file_path, "up", "-d"]).unwrap(); - - DockerCompose::wait_for_containers_to_startup(service_to_image, file_path); - - DockerCompose { - file_path: file_path.to_string(), - } - } - - /// Creates a new DockerCompose running an instance of moto the AWS mocking server - pub fn new_moto() -> Self { - // Overwrite any existing AWS credential env vars belonging to the user with dummy values to be sure that - // we wont hit their real AWS account in the case of a bug in shotover or the test - env::set_var("AWS_ACCESS_KEY_ID", "dummy-access-key"); - env::set_var("AWS_SECRET_ACCESS_KEY", "dummy-access-key-secret"); + docker_compose("tests/transforms/docker-compose-moto.yaml") +} - DockerCompose::new("tests/transforms/docker-compose-moto.yaml") - } +fn get_image_waiters() -> &'static [Image] { + &[ + Image { + name: "shotover/shotover-proxy", + log_regex_to_wait_for: r"accepting inbound connections", + }, + Image { + name: "motoserver/moto", + log_regex_to_wait_for: r"Press CTRL\+C to quit", + }, + Image { + name: "library/redis:5.0.9", + log_regex_to_wait_for: r"Ready to accept connections", + }, + Image { + name: "library/redis:6.2.5", + log_regex_to_wait_for: r"Ready to accept connections", + }, + Image { + name: "bitnami/redis-cluster:6.0-debian-10", + //`Cluster state changed` is created by the node services + //`Cluster correctly created` is created by the init service + log_regex_to_wait_for: r"Cluster state changed|Cluster correctly created", + }, + Image { + name: "bitnami/cassandra:4.0.6", + log_regex_to_wait_for: r"Startup complete", + }, + Image { + name: "shotover-int-tests/cassandra:4.0.6", + log_regex_to_wait_for: r"Startup complete", + }, + Image { + name: "shotover-int-tests/cassandra-tls:4.0.6", + log_regex_to_wait_for: r"Startup complete", + }, + Image { + name: "shotover-int-tests/cassandra:3.11.13", + log_regex_to_wait_for: r"Startup complete", + }, + Image { + name: "bitnami/kafka:3.3.2", + log_regex_to_wait_for: r"Kafka Server started", + }, + ] +} - /// Stops the container with the provided service name - pub fn stop_service(&self, service_name: &str) { +fn build_images(service_to_image: &[&str]) { + if service_to_image + .iter() + .any(|x| *x == "shotover-int-tests/cassandra:4.0.6") + { run_command( - "docker-compose", - &["-f", &self.file_path, "stop", service_name], + "docker", + &[ + "build", + "example-configs/docker-images/cassandra-4.0.6", + "--tag", + "shotover-int-tests/cassandra:4.0.6", + ], ) .unwrap(); } - - /// Kills the container with the provided service name - pub fn kill_service(&self, service_name: &str) { + if service_to_image + .iter() + .any(|x| *x == "shotover-int-tests/cassandra:3.11.13") + { run_command( - "docker-compose", - &["-f", &self.file_path, "kill", service_name], + "docker", + &[ + "build", + "example-configs/docker-images/cassandra-3.11.13", + "--tag", + "shotover-int-tests/cassandra:3.11.13", + ], ) .unwrap(); } - - /// Restarts the container with the provided service name - pub fn start_service(&self, service_name: &str) { + if service_to_image + .iter() + .any(|x| *x == "shotover-int-tests/cassandra-tls:4.0.6") + && Path::new("example-configs/docker-images/cassandra-tls-4.0.6/certs/keystore.p12") + .exists() + { run_command( - "docker-compose", - &["-f", &self.file_path, "start", service_name], - ) - .unwrap(); - - // TODO: call wait_for_containers_to_startup - } - - fn wait_for_containers_to_startup(service_to_image: HashMap, file_path: &str) { - let images = [ - Image { - name: "shotover/shotover-proxy", - log_regex_to_wait_for: r"accepting inbound connections", - }, - Image { - name: "motoserver/moto", - log_regex_to_wait_for: r"Press CTRL\+C to quit", - }, - Image { - name: "library/redis:5.0.9", - log_regex_to_wait_for: r"Ready to accept connections", - }, - Image { - name: "library/redis:6.2.5", - log_regex_to_wait_for: r"Ready to accept connections", - }, - Image { - name: "docker.io/bitnami/redis-cluster:6.0-debian-10", - log_regex_to_wait_for: r"Cluster state changed|Cluster correctly created", - }, - Image { - name: "bitnami/redis-cluster:6.0-debian-10", - //`Cluster state changed` is created by the node services - //`Cluster correctly created` is created by the init service - log_regex_to_wait_for: r"Cluster state changed|Cluster correctly created", - }, - Image { - name: "bitnami/cassandra:4.0.6", - log_regex_to_wait_for: r"Startup complete", - }, - Image { - name: "shotover-int-tests/cassandra:4.0.6", - log_regex_to_wait_for: r"Startup complete", - }, - Image { - name: "shotover-int-tests/cassandra-tls:4.0.6", - log_regex_to_wait_for: r"Startup complete", - }, - Image { - name: "shotover-int-tests/cassandra:3.11.13", - log_regex_to_wait_for: r"Startup complete", - }, - Image { - name: "bitnami/kafka:3.3.2", - log_regex_to_wait_for: r"Kafka Server started", - }, - ]; - - let services: Vec = - service_to_image - .into_iter() - .map( - |(service_name, image_name)| match images.iter().find(|image| image.name == image_name) { - Some(image) => Service { - name: service_name, - log_to_wait_for: Regex::new(image.log_regex_to_wait_for).unwrap(), - }, - None => panic!("DockerCompose does not yet know about the image {image_name}, please add it to the list above."), - }, - ) - .collect(); - - DockerCompose::wait_for_logs(file_path, &services); - } - - fn get_service_to_image(file_path: &str) -> HashMap { - let compose_yaml: Value = - serde_yaml::from_str(&std::fs::read_to_string(file_path).unwrap()).unwrap(); - let mut result = HashMap::new(); - match compose_yaml { - Value::Mapping(root) => match root.get("services").unwrap() { - Value::Mapping(services) => { - for (service_name, service) in services { - let service_name = match service_name { - Value::String(service_name) => service_name, - service_name => panic!("Unexpected service_name {service_name:?}"), - }; - match service { - Value::Mapping(service) => { - let image = match service.get("image").unwrap() { - Value::String(image) => image, - image => panic!("Unexpected image {image:?}"), - }; - result.insert(service_name.clone(), image.clone()); - } - service => panic!("Unexpected service {service:?}"), - } - } - } - services => panic!("Unexpected services {services:?}"), - }, - root => panic!("Unexpected root {root:?}"), - } - result - } - - /// Wait until the requirements in every Service is met. - /// Will panic if a timeout occurs. - fn wait_for_logs(file_path: &str, services: &[Service]) { - let timeout = Duration::from_secs(120); - - // TODO: remove this check once CI docker-compose is updated (probably ubuntu 22.04) - let can_use_status_flag = run_command("docker-compose", &["-f", file_path, "ps", "--help"]) - .unwrap() - .contains("--status"); - - let instant = time::Instant::now(); - loop { - // check if every service is completely ready - if services.iter().all(|service| { - let log = run_command("docker-compose", &["-f", file_path, "logs", &service.name]) - .unwrap(); - service.log_to_wait_for.is_match(&log) - }) { - return; - } - - let all_logs = run_command("docker-compose", &["-f", file_path, "logs"]).unwrap(); - - // check if the service has failed in some way - // this allows us to report the failure to the developer a lot sooner than just relying on the timeout - if can_use_status_flag { - DockerCompose::assert_no_containers_in_service_with_status( - file_path, "exited", &all_logs, - ); - DockerCompose::assert_no_containers_in_service_with_status( - file_path, "dead", &all_logs, - ); - DockerCompose::assert_no_containers_in_service_with_status( - file_path, "removing", &all_logs, - ); - } - - // if all else fails timeout the wait - if instant.elapsed() > timeout { - let mut results = "".to_owned(); - for service in services { - let log = - run_command("docker-compose", &["-f", file_path, "logs", &service.name]) - .unwrap(); - let found = if service.log_to_wait_for.is_match(&log) { - "Found" - } else { - "Missing" - }; - - writeln!( - results, - "* Service {}, searched for '{}', was {}", - service.name, service.log_to_wait_for, found - ) - .unwrap(); - } - - panic!("wait_for_log {timeout:?} timer expired. Results:\n{results}\nLogs:\n{all_logs}"); - } - } - } - - fn assert_no_containers_in_service_with_status(file_path: &str, status: &str, full_log: &str) { - let containers = run_command( - "docker-compose", - &["-f", file_path, "ps", "--status", status], + "docker", + &[ + "build", + "example-configs/docker-images/cassandra-tls-4.0.6", + "--tag", + "shotover-int-tests/cassandra-tls:4.0.6", + ], ) .unwrap(); - // One line for the table heading. If there are more lines then there is some data indicating that containers exist with this status - if containers.matches('\n').count() > 1 { - panic!( - "At least one container failed to initialize\n{containers}\nFull log\n{full_log}" - ); - } - } - - fn build_images(service_to_image: &HashMap) { - if service_to_image - .values() - .any(|x| x == "shotover-int-tests/cassandra:4.0.6") - { - run_command( - "docker", - &[ - "build", - "example-configs/docker-images/cassandra-4.0.6", - "--tag", - "shotover-int-tests/cassandra:4.0.6", - ], - ) - .unwrap(); - } - if service_to_image - .values() - .any(|x| x == "shotover-int-tests/cassandra:3.11.13") - { - run_command( - "docker", - &[ - "build", - "example-configs/docker-images/cassandra-3.11.13", - "--tag", - "shotover-int-tests/cassandra:3.11.13", - ], - ) - .unwrap(); - } - if service_to_image - .values() - .any(|x| x == "shotover-int-tests/cassandra-tls:4.0.6") - && Path::new("example-configs/docker-images/cassandra-tls-4.0.6/certs/keystore.p12") - .exists() - { - run_command( - "docker", - &[ - "build", - "example-configs/docker-images/cassandra-tls-4.0.6", - "--tag", - "shotover-int-tests/cassandra-tls:4.0.6", - ], - ) - .unwrap(); - } - } - - /// Cleans up the docker-compose by shutting down the running system and removing the images. - /// - /// # Arguments - /// * `file_path` - The path to the docker-compose yaml file that was used to start docker. - fn clean_up(file_path: &str) -> Result<()> { - trace!("bringing down docker compose {}", file_path); - - run_command("docker-compose", &["-f", file_path, "kill"])?; - run_command("docker-compose", &["-f", file_path, "down", "-v"])?; - - Ok(()) - } -} - -struct Image<'a> { - name: &'a str, - log_regex_to_wait_for: &'a str, -} - -struct Service { - name: String, - log_to_wait_for: Regex, -} - -impl Drop for DockerCompose { - fn drop(&mut self) { - if std::thread::panicking() { - if let Err(err) = DockerCompose::clean_up(&self.file_path) { - // We need to use println! here instead of error! because error! does not - // get output when panicking - println!( - "ERROR: docker compose failed to bring down while already panicking: {err:?}", - ); - } - } else { - DockerCompose::clean_up(&self.file_path).unwrap(); - } } } diff --git a/test-helpers/src/docker_compose_runner.rs b/test-helpers/src/docker_compose_runner.rs new file mode 100644 index 000000000..0b998abe6 --- /dev/null +++ b/test-helpers/src/docker_compose_runner.rs @@ -0,0 +1,281 @@ +use anyhow::{anyhow, Result}; +use regex::Regex; +use serde_yaml::Value; +use std::collections::HashMap; +use std::fmt::Write; +use std::io::ErrorKind; +use std::process::Command; +use std::time::{self, Duration}; +use subprocess::{Exec, Redirection}; +use tracing::trace; + +/// Runs a command and returns the output as a string. +/// +/// Both stderr and stdout are returned in the result. +/// +/// # Arguments +/// * `command` - The system command to run +/// * `args` - An array of command line arguments for the command +pub(crate) fn run_command(command: &str, args: &[&str]) -> Result { + trace!("executing {}", command); + let data = Exec::cmd(command) + .args(args) + .stdout(Redirection::Pipe) + .stderr(Redirection::Merge) + .capture()?; + + if data.exit_status.success() { + Ok(data.stdout_str()) + } else { + Err(anyhow!( + "command {} {:?} exited with {:?} and output:\n{}", + command, + args, + data.exit_status, + data.stdout_str() + )) + } +} + +#[must_use] +pub struct DockerCompose { + file_path: String, +} + +impl DockerCompose { + /// Runs docker-compose on the provided docker-compose.yaml file. + /// Dropping the returned object will stop and destroy the launched docker-compose services. + /// + /// image_waiters gives DockerCompose a way to know when a container has finished starting up. + /// Each entry defines an image name and a regex such that if the regex matches on a log line output by a container running that image the container is considered started up. + /// + /// image_builder is a callback allowing the user to build a docker image if the docker-compose.yaml depends on it. + /// The argument is an iterator over all the image names docker-compose is going to use. + pub fn new( + image_waiters: &'static [Image], + image_builder: impl FnOnce(&[&str]), + yaml_path: &str, + ) -> Self { + if let Err(ErrorKind::NotFound) = Command::new("docker-compose") + .output() + .map_err(|e| e.kind()) + { + panic!("Could not find docker-compose. Have you installed it?"); + } + + // It is critical that clean_up is run before everything else as the internal `docker-compose` commands act as validation + // for the docker-compose.yaml file that we later manually parse with poor error handling + DockerCompose::clean_up(yaml_path).unwrap(); + + let service_to_image = DockerCompose::get_service_to_image(yaml_path); + + let images: Vec<&str> = service_to_image.values().map(|x| x.as_ref()).collect(); + image_builder(&images); + + run_command("docker-compose", &["-f", yaml_path, "up", "-d"]).unwrap(); + + DockerCompose::wait_for_containers_to_startup(image_waiters, service_to_image, yaml_path); + + DockerCompose { + file_path: yaml_path.to_string(), + } + } + + /// Stops the container with the provided service name + pub fn stop_service(&self, service_name: &str) { + run_command( + "docker-compose", + &["-f", &self.file_path, "stop", service_name], + ) + .unwrap(); + } + + /// Kills the container with the provided service name + pub fn kill_service(&self, service_name: &str) { + run_command( + "docker-compose", + &["-f", &self.file_path, "kill", service_name], + ) + .unwrap(); + } + + /// Restarts the container with the provided service name + pub fn start_service(&self, service_name: &str) { + run_command( + "docker-compose", + &["-f", &self.file_path, "start", service_name], + ) + .unwrap(); + + // TODO: call wait_for_containers_to_startup + } + + fn wait_for_containers_to_startup( + image_waiters: &[Image], + service_to_image: HashMap, + file_path: &str, + ) { + let services: Vec = + service_to_image + .into_iter() + .map( + |(service_name, image_name)| match image_waiters.iter().find(|image| image.name == image_name) { + Some(image) => Service { + name: service_name, + log_to_wait_for: Regex::new(image.log_regex_to_wait_for).unwrap(), + }, + None => panic!("The image_waiters list given to DockerCompose::new does not include the image {image_name}, please add it to the list."), + }, + ) + .collect(); + + DockerCompose::wait_for_logs(file_path, &services); + } + + fn get_service_to_image(file_path: &str) -> HashMap { + let compose_yaml: Value = + serde_yaml::from_str(&std::fs::read_to_string(file_path).unwrap()).unwrap(); + let mut result = HashMap::new(); + match compose_yaml { + Value::Mapping(root) => match root.get("services").unwrap() { + Value::Mapping(services) => { + for (service_name, service) in services { + let service_name = match service_name { + Value::String(service_name) => service_name, + service_name => panic!("Unexpected service_name {service_name:?}"), + }; + match service { + Value::Mapping(service) => { + let image = match service.get("image").unwrap() { + Value::String(image) => image, + image => panic!("Unexpected image {image:?}"), + }; + result.insert(service_name.clone(), image.clone()); + } + service => panic!("Unexpected service {service:?}"), + } + } + } + services => panic!("Unexpected services {services:?}"), + }, + root => panic!("Unexpected root {root:?}"), + } + result + } + + /// Wait until the requirements in every Service is met. + /// Will panic if a timeout occurs. + fn wait_for_logs(file_path: &str, services: &[Service]) { + let timeout = Duration::from_secs(120); + + // TODO: remove this check once CI docker-compose is updated (probably ubuntu 22.04) + let can_use_status_flag = run_command("docker-compose", &["-f", file_path, "ps", "--help"]) + .unwrap() + .contains("--status"); + + let instant = time::Instant::now(); + loop { + // check if every service is completely ready + if services.iter().all(|service| { + let log = run_command("docker-compose", &["-f", file_path, "logs", &service.name]) + .unwrap(); + service.log_to_wait_for.is_match(&log) + }) { + return; + } + + let all_logs = run_command("docker-compose", &["-f", file_path, "logs"]).unwrap(); + + // check if the service has failed in some way + // this allows us to report the failure to the developer a lot sooner than just relying on the timeout + if can_use_status_flag { + DockerCompose::assert_no_containers_in_service_with_status( + file_path, "exited", &all_logs, + ); + DockerCompose::assert_no_containers_in_service_with_status( + file_path, "dead", &all_logs, + ); + DockerCompose::assert_no_containers_in_service_with_status( + file_path, "removing", &all_logs, + ); + } + + // if all else fails timeout the wait + if instant.elapsed() > timeout { + let mut results = "".to_owned(); + for service in services { + let log = + run_command("docker-compose", &["-f", file_path, "logs", &service.name]) + .unwrap(); + let found = if service.log_to_wait_for.is_match(&log) { + "Found" + } else { + "Missing" + }; + + writeln!( + results, + "* Service {}, searched for '{}', was {}", + service.name, service.log_to_wait_for, found + ) + .unwrap(); + } + + panic!("wait_for_log {timeout:?} timer expired. Results:\n{results}\nLogs:\n{all_logs}"); + } + } + } + + fn assert_no_containers_in_service_with_status(file_path: &str, status: &str, full_log: &str) { + let containers = run_command( + "docker-compose", + &["-f", file_path, "ps", "--status", status], + ) + .unwrap(); + // One line for the table heading. If there are more lines then there is some data indicating that containers exist with this status + if containers.matches('\n').count() > 1 { + panic!( + "At least one container failed to initialize\n{containers}\nFull log\n{full_log}" + ); + } + } + + /// Cleans up the docker-compose by shutting down the running system and removing the images. + /// + /// # Arguments + /// * `file_path` - The path to the docker-compose yaml file that was used to start docker. + fn clean_up(file_path: &str) -> Result<()> { + trace!("bringing down docker compose {}", file_path); + + run_command("docker-compose", &["-f", file_path, "kill"])?; + run_command("docker-compose", &["-f", file_path, "down", "-v"])?; + + Ok(()) + } +} + +pub struct Image { + pub name: &'static str, + pub log_regex_to_wait_for: &'static str, +} + +struct Service { + name: String, + log_to_wait_for: Regex, +} + +impl Drop for DockerCompose { + fn drop(&mut self) { + if std::thread::panicking() { + if let Err(err) = DockerCompose::clean_up(&self.file_path) { + // We need to use println! here instead of error! because error! does not + // get output when panicking + println!( + "ERROR: docker compose failed to bring down while already panicking: {err:?}", + ); + } + } else { + DockerCompose::clean_up(&self.file_path).unwrap(); + } + } +} diff --git a/test-helpers/src/flamegraph.rs b/test-helpers/src/flamegraph.rs index 6c48ce8db..9c4d1ff8d 100644 --- a/test-helpers/src/flamegraph.rs +++ b/test-helpers/src/flamegraph.rs @@ -5,7 +5,7 @@ use std::fs::File; use std::io::{BufReader, BufWriter}; use std::process::{Child, Command}; -use crate::docker_compose::run_command; +use crate::docker_compose_runner::run_command; pub struct Perf(Child); diff --git a/test-helpers/src/latte.rs b/test-helpers/src/latte.rs index 1f8a1226e..b2d97de7f 100644 --- a/test-helpers/src/latte.rs +++ b/test-helpers/src/latte.rs @@ -11,7 +11,7 @@ pub struct Latte { impl Latte { pub fn new(rate: u64, threads: u64) -> Latte { - crate::docker_compose::run_command( + crate::docker_compose_runner::run_command( "cargo", &[ "install", @@ -26,7 +26,7 @@ impl Latte { } pub fn init(&self, name: &str, address_load: &str) { - crate::docker_compose::run_command( + crate::docker_compose_runner::run_command( "latte", &[ "schema", @@ -40,7 +40,7 @@ impl Latte { ], ) .unwrap(); - crate::docker_compose::run_command( + crate::docker_compose_runner::run_command( "latte", &[ "load", @@ -57,7 +57,7 @@ impl Latte { } pub fn bench(&self, name: &str, address_bench: &str) { - crate::docker_compose::run_command( + crate::docker_compose_runner::run_command( "latte", &[ "run", diff --git a/test-helpers/src/lib.rs b/test-helpers/src/lib.rs index 5d7180b9e..8dd4fedd8 100644 --- a/test-helpers/src/lib.rs +++ b/test-helpers/src/lib.rs @@ -2,6 +2,7 @@ pub mod bench; pub mod cert; pub mod connection; pub mod docker_compose; +pub mod docker_compose_runner; pub mod flamegraph; pub mod kafka_producer_perf_test; pub mod latte;