Skip to content

Commit 8311ed4

Browse files
committedJun 4, 2024
Add tests for container reaping behavior
1 parent 4f0d717 commit 8311ed4

File tree

5 files changed

+410
-6
lines changed

5 files changed

+410
-6
lines changed
 

‎Cargo.lock

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ thiserror = "1.0.61"
2727
tokio = { version = "1.38.0", features = ["full"] }
2828
tracing = "0.1.40"
2929
tracing-subscriber = "0.3.18"
30+
31+
[dev-dependencies]
32+
serial_test = { version = "3.1.1" }

‎src/lib.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub struct ReapVolumesConfig<'a> {
5151
}
5252

5353
#[derive(Debug)]
54-
enum RemovalStatus {
54+
pub enum RemovalStatus {
5555
/// Used in dry-run mode to indicate that a resource is eligible for removal.
5656
Eligible,
5757
/// Resource was successfully removed.
@@ -108,7 +108,7 @@ impl Filter {
108108
}
109109

110110
#[derive(Debug, PartialEq, Eq)]
111-
enum ResourceType {
111+
pub enum ResourceType {
112112
Container,
113113
Network,
114114
#[allow(dead_code)]
@@ -135,12 +135,18 @@ impl fmt::Display for ResourceType {
135135
#[tabled(rename_all = "PascalCase")]
136136
pub struct Resource {
137137
#[tabled(rename = "Resource Type")]
138-
resource_type: ResourceType,
138+
pub resource_type: ResourceType,
139139
#[tabled(skip)]
140140
#[allow(dead_code)]
141-
id: String,
142-
name: String,
143-
status: RemovalStatus,
141+
pub id: String,
142+
pub name: String,
143+
pub status: RemovalStatus,
144+
}
145+
146+
impl PartialEq for Resource {
147+
fn eq(&self, other: &Self) -> bool {
148+
self.resource_type == other.resource_type && self.id == other.id
149+
}
144150
}
145151

146152
impl Resource {

‎tests/common.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//! Common utility functions for integration tests.
2+
#![allow(dead_code)]
3+
4+
use bollard::container::{Config, NetworkingConfig};
5+
use bollard::network::CreateNetworkOptions;
6+
use bollard::secret::{ContainerCreateResponse, EndpointSettings, NetworkCreateResponse};
7+
use bollard::Docker;
8+
use chrono::Utc;
9+
use docker_reaper::{
10+
reap_containers, reap_networks, reap_volumes, Filter, ReapContainersConfig, ReapNetworksConfig,
11+
ReapVolumesConfig,
12+
};
13+
use std::collections::HashMap;
14+
use std::sync::OnceLock;
15+
16+
/// A label set on all test-created Docker resources.
17+
pub(crate) const TEST_LABEL: &str = "docker-reaper-test";
18+
19+
/// Obtain a client for the local Docker daemon.
20+
pub(crate) fn docker_client() -> &'static Docker {
21+
static CLIENT: OnceLock<Docker> = OnceLock::new();
22+
CLIENT.get_or_init(|| {
23+
Docker::connect_with_local_defaults().expect("failed to connect to Docker daemon")
24+
})
25+
}
26+
27+
/// Return type for [run_container] calls.
28+
/// A network will not be created unless the `with_network` argument was `true`.
29+
pub(crate) struct RunContainerResult {
30+
pub(crate) container_id: String,
31+
pub(crate) network_id: Option<String>,
32+
}
33+
34+
/// Run a container on the local Docker daemon.
35+
/// The label [TEST_LABEL] will always be set. Additional labels may also be specified.
36+
pub(crate) async fn run_container(
37+
with_network: bool,
38+
extra_labels: Option<HashMap<String, String>>,
39+
) -> RunContainerResult {
40+
let client = docker_client();
41+
let mut labels = HashMap::from([(TEST_LABEL.to_string(), "true".to_string())]);
42+
if let Some(extra_labels) = extra_labels {
43+
labels.extend(extra_labels.into_iter())
44+
}
45+
let mut network_id = None;
46+
if with_network {
47+
let name = Utc::now().timestamp_millis().to_string(); // network names must be unique
48+
client
49+
.create_network(CreateNetworkOptions {
50+
name: name.clone(),
51+
labels: labels.clone(),
52+
..Default::default()
53+
})
54+
.await
55+
.expect("failed to create network");
56+
// We use names rather than actual IDs to uniquely identify networks in docker-reaper
57+
// because they are more meaningful in the user-facing output. Docker's handling of network
58+
// names vs. IDs is strange - they can effectively be used interchangably.
59+
network_id = Some(name);
60+
}
61+
62+
let ContainerCreateResponse {
63+
id: container_id, ..
64+
} = client
65+
.create_container::<String, String>(
66+
None,
67+
Config {
68+
tty: Some(true),
69+
cmd: Some(vec![String::from("bash")]),
70+
image: Some("ubuntu".to_string()),
71+
labels: Some(labels),
72+
networking_config: {
73+
if with_network {
74+
Some(NetworkingConfig {
75+
endpoints_config: HashMap::from([(
76+
"docker-reaper-test-network".to_string(),
77+
EndpointSettings {
78+
network_id: network_id.clone(),
79+
..Default::default()
80+
},
81+
)]),
82+
})
83+
} else {
84+
None
85+
}
86+
},
87+
..Default::default()
88+
},
89+
)
90+
.await
91+
.expect("failed to create container");
92+
client
93+
.start_container::<&str>(&container_id, None)
94+
.await
95+
.expect(&format!("failed to start container {container_id}"));
96+
RunContainerResult {
97+
container_id,
98+
network_id,
99+
}
100+
}
101+
102+
/// Check whether a container with the given ID exists.
103+
pub(crate) async fn container_exists(id: &str) -> bool {
104+
let client = docker_client();
105+
match client.inspect_container(id, None).await {
106+
Ok(_) => return true,
107+
Err(err) => match err {
108+
bollard::errors::Error::DockerResponseServerError {
109+
status_code: 404, ..
110+
} => return false,
111+
_ => panic!("unexpected error: {err}"),
112+
},
113+
}
114+
}
115+
116+
/// Check whether a network with the given ID exists.
117+
pub(crate) async fn network_exists(id: &str) -> bool {
118+
let client = docker_client();
119+
match client.inspect_network::<&str>(id, None).await {
120+
Ok(_) => return true,
121+
Err(err) => match err {
122+
bollard::errors::Error::DockerResponseServerError {
123+
status_code: 404, ..
124+
} => return false,
125+
_ => panic!("unexpected error: {err}"),
126+
},
127+
}
128+
}
129+
130+
/// Clean up all remaining test resources.
131+
pub(crate) async fn cleanup() {
132+
let client = docker_client();
133+
reap_containers(
134+
client,
135+
&ReapContainersConfig {
136+
dry_run: false,
137+
min_age: None,
138+
max_age: None,
139+
filters: &vec![Filter::new("label", TEST_LABEL)],
140+
reap_networks: true,
141+
},
142+
)
143+
.await
144+
.expect("failed to clean up containers");
145+
146+
reap_networks(
147+
client,
148+
&ReapNetworksConfig {
149+
dry_run: false,
150+
min_age: None,
151+
max_age: None,
152+
filters: &vec![Filter::new("label", TEST_LABEL)],
153+
},
154+
)
155+
.await
156+
.expect("failed to clean up networks");
157+
158+
reap_volumes(
159+
client,
160+
&ReapVolumesConfig {
161+
dry_run: false,
162+
min_age: None,
163+
max_age: None,
164+
filters: &vec![Filter::new("label", TEST_LABEL)],
165+
},
166+
)
167+
.await
168+
.expect("failed to clean up volumes");
169+
}

0 commit comments

Comments
 (0)