dockertest/composition.rs
1//! Represent a concrete instance of an Image, before it is ran as a Container.
2
3use crate::image::Image;
4use crate::waitfor::{NoWait, WaitFor};
5
6use std::collections::HashMap;
7use tracing::{event, Level};
8
9/// Specifies the starting policy of a container specification.
10///
11/// - [StartPolicy::Strict] policy will enforce that the container is started in the order
12/// it was added to [DockerTest].
13/// - [StartPolicy::Relaxed] policy will not enforce any ordering,
14/// all container specifications with a relaxed policy will be started concurrently.
15/// These are all started asynchrously started before the strict policy containers
16/// are started sequentially.
17///
18/// [DockerTest]: crate::DockerTest
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum StartPolicy {
21 /// Concurrently start the Container with other Relaxed instances.
22 Relaxed,
23 /// Start Containers' sequentially in the order added to DockerTest.
24 Strict,
25}
26
27/// Specifies who is responsible for managing a static container.
28///
29/// - [StaticManagementPolicy::External] indicates that the user is responsible for managing the
30/// container, DockerTest will never start or remove/stop the container. The container will
31/// be available through its handle in [DockerOperations]. If no external network is
32/// supplied, the test-scoped network will be added to the external network, and subsequently
33/// removed once the test terminates.
34/// The externally managed container is assumed to be in a running state when the test starts.
35/// If DockerTest cannot locate the the container, the test will fail.
36/// - [StaticManagementPolicy::Internal] indicates that DockerTest will handle the lifecycle of
37/// the container between all DockerTest instances within the test binary.
38/// - [StaticManagementPolicy::Dynamic] indicates that DockerTest will start the
39/// container if it does not already exists and will not clean it up. This way the same
40/// container can be re-used across multiple `cargo test` invocations.
41/// If the `DOCKERTEST_DYNAMIC` environment variable is set to `INTERNAL` or `EXTERNAL`, the management policy
42/// will instead be set accordingly (either [StaticManagementPolicy::Internal] or [StaticManagementPolicy::External].
43/// The purpose of this is to facilitate running tests locally and in CI/CD pipelines without having to alter management policies.
44/// If a container already exists in a non-running state with the same name as a container with this policy, the startup
45/// procedure will fail.
46///
47/// [DockerOperations]: crate::DockerOperations
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum StaticManagementPolicy {
50 /// The lifecycle of the container is managed by the user.
51 External,
52 /// DockerTest handles the lifecycle of the container.
53 Internal,
54 /// DockerTest starts the container if it does not exist and does not remove it, and will
55 /// re-use the container across `cargo test` invocations.
56 Dynamic,
57}
58
59/// Specifies how should dockertest should handle log output from this container.
60#[derive(Clone, Debug)]
61pub enum LogAction {
62 /// Forward all outputs to their respective output sources of the dockertest process.
63 Forward,
64 /// Forward [LogSource] outputs to a specified file.
65 ForwardToFile {
66 /// The filepath to output to.
67 path: String,
68 },
69 /// Forward [LogSource] outputs to stdout of the dockertest process.
70 ForwardToStdOut,
71 /// Forward [LogSource] outputs to stderr of the dockertest process.
72 ForwardToStdErr,
73}
74
75/// Specifies which log sources we want to read from containers.
76#[derive(Clone, Debug)]
77pub enum LogSource {
78 /// Read stderr only.
79 StdErr,
80 /// Read stdout only.
81 StdOut,
82 /// Read stdout and stderr.
83 Both,
84}
85
86/// Specifies when [LogAction] is applicable.
87#[derive(Clone, Debug)]
88pub enum LogPolicy {
89 /// [LogAction] is always applicable.
90 Always,
91 /// [LogAction] is applicable only if an error occures.
92 OnError,
93 /// [LogAction] is applicable only if a startup error occures.
94 OnStartupError,
95}
96
97/// Specifies how dockertest should handle logging output from this specific container.
98#[derive(Clone, Debug)]
99pub struct LogOptions {
100 /// The logging actions to be performed.
101 pub action: LogAction,
102 /// Under which conditions should we perform the log actions?
103 pub policy: LogPolicy,
104 /// Specifies log sources we want to read from container.
105 pub source: LogSource,
106}
107
108impl Default for LogOptions {
109 fn default() -> LogOptions {
110 LogOptions {
111 action: LogAction::Forward,
112 policy: LogPolicy::OnError,
113 source: LogSource::StdErr,
114 }
115 }
116}
117
118/// Represents an instance of an [Image].
119///
120/// The Composition is used to specialize an image whose name, version, tag and source is known,
121/// but before one can create a [crate::container:: OperationalContainer] from an image,
122/// it must be augmented with information about how to start it, how to ensure it has been
123/// started, environment variables and runtime commands.
124/// Thus, this structure represents the concrete instance of an [Image] that will be started
125/// and become a [crate::container::OperationalContainer].
126///
127/// NOTE: This is an internal implementation detail. This used to be a public interface.
128#[derive(Clone, Debug)]
129pub struct Composition {
130 /// User provided name of the container.
131 ///
132 /// This will dictate the final container_name and the container_handle_key of the container
133 /// that will be created from this Composition.
134 user_provided_container_name: Option<String>,
135
136 /// Network aliases for the container.
137 pub(crate) network_aliases: Option<Vec<String>>,
138
139 /// The name of the container to be created by this Composition.
140 ///
141 /// When the composition is created, this field defaults to the repository name of the
142 /// associated image. If the user provides an alternative container name, this will be stored
143 /// in its own dedicated field.
144 ///
145 /// The final format of the container name we will create will be on the following format:
146 /// `{namespace}-{name}-{suffix}` where
147 /// - `{namespace}` is the configured namespace with [crate::DockerTest].
148 /// - `{name}` is either the user provided container name, or this default value.
149 /// - `{suffix}` randomly generated pattern.
150 pub(crate) container_name: String,
151
152 /// A trait object holding the implementation that indicate container readiness.
153 pub(crate) wait: Box<dyn WaitFor>,
154
155 /// The environmentable variables that will be passed to the container.
156 pub(crate) env: HashMap<String, String>,
157
158 /// The command to pass to the container.
159 pub(crate) cmd: Vec<String>,
160
161 /// The start policy of this container, codifing the inter-depdencies between containers.
162 pub(crate) start_policy: StartPolicy,
163
164 /// The base image that will be the container we will be starting.
165 image: Image,
166
167 /// Named volumes associated with this composition, are in the form of:
168 /// - "(VOLUME_NAME,CONTAINER_PATH)"
169 pub(crate) named_volumes: Vec<(String, String)>,
170
171 /// Final form of named volume names.
172 ///
173 /// DockerTest is responsible for constructing the final names and adding them to this vector.
174 /// The final name will be on the form `VOLUME_NAME-RANDOM_SUFFIX/CONTAINER_PATH`.
175 pub(crate) final_named_volume_names: Vec<String>,
176
177 /// Bind mounts associated with this composition, are in the form of:
178 /// - `HOST_PATH:CONTAINER_PATH`
179 ///
180 /// NOTE: As bind mounts do not outlive the container they are mounted in they do not need to
181 /// be cleaned up.
182 pub(crate) bind_mounts: Vec<String>,
183
184 /// All user specified container name injections as environment variables.
185 /// Tuple contains (handle, env).
186 pub(crate) inject_container_name_env: Vec<(String, String)>,
187
188 /// Port mapping (used for Windows-compatibility)
189 pub(crate) port: Vec<(String, String)>,
190
191 /// Allocates an ephemeral host port for all of a container’s exposed ports.
192 ///
193 /// Port forwarding is useful on operating systems where there is no network connectivity
194 /// between system and the Docker Desktop VM.
195 pub(crate) publish_all_ports: bool,
196
197 /// Who is responsible for managing the lifecycle of the container.
198 ///
199 /// A composition can be marked as static, where the lifecycle of the container outlives
200 /// the individual test.
201 management: Option<StaticManagementPolicy>,
202
203 /// Logging options for this specific container.
204 pub(crate) log_options: Option<LogOptions>,
205
206 /// Tmpfs mount paths to create.
207 ///
208 /// These are destination paths within the container to create tmpfs filesystems for,
209 /// they require no source paths.
210 ///
211 /// tmpfs details: <https://docs.docker.com/engine/storage/tmpfs/>
212 pub(crate) tmpfs: Vec<String>,
213
214 /// Whether this composition should be started in privileged mode.
215 /// Privileged mode is required for some images, such as the `docker:dind` image.
216 /// See https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
217 /// for more information.
218 /// Defaults to false.
219 /// NOTE: This is only supported on Linux.
220 /// NOTE: This is only supported on Docker 1.13 and above.
221 /// NOTE: This is only supported on Docker API 1.25 and above.
222 /// NOTE: This is only supported on Docker Engine 1.13 and above.
223 pub(crate) privileged: bool,
224}
225
226impl Composition {
227 /// Creates a [Composition] based on the [Image] repository name provided.
228 ///
229 /// This will internally create the [Image] based on the provided repository name,
230 /// and default the tag to `latest`.
231 ///
232 /// This is the shortcut method of constructing a [Composition].
233 /// See [with_image](Composition::with_image) to create one with a provided [Image].
234 pub fn with_repository<T: ToString>(repository: T) -> Composition {
235 let copy = repository.to_string();
236 Composition {
237 user_provided_container_name: None,
238 network_aliases: None,
239 image: Image::with_repository(©),
240 container_name: copy.replace('/', "-"),
241 wait: Box::new(NoWait {}),
242 env: HashMap::new(),
243 cmd: Vec::new(),
244 start_policy: StartPolicy::Relaxed,
245 bind_mounts: Vec::new(),
246 named_volumes: Vec::new(),
247 inject_container_name_env: Vec::new(),
248 final_named_volume_names: Vec::new(),
249 port: Vec::new(),
250 publish_all_ports: false,
251 management: None,
252 log_options: Some(LogOptions::default()),
253 privileged: false,
254 tmpfs: Vec::new(),
255 }
256 }
257
258 /// Creates a [Composition] with the provided [Image].
259 ///
260 /// This is the long-winded way of defining a [Composition].
261 /// See [with_repository](Composition::with_repository) for the shortcut method.
262 pub fn with_image(image: Image) -> Composition {
263 Composition {
264 user_provided_container_name: None,
265 network_aliases: None,
266 container_name: image.repository().to_string().replace('/', "-"),
267 image,
268 wait: Box::new(NoWait {}),
269 env: HashMap::new(),
270 cmd: Vec::new(),
271 start_policy: StartPolicy::Relaxed,
272 bind_mounts: Vec::new(),
273 named_volumes: Vec::new(),
274 inject_container_name_env: Vec::new(),
275 final_named_volume_names: Vec::new(),
276 port: Vec::new(),
277 publish_all_ports: false,
278 management: None,
279 log_options: Some(LogOptions::default()),
280 privileged: false,
281 tmpfs: Vec::new(),
282 }
283 }
284
285 /// Adds the given tmpfs mount paths to this [Composition]
286 ///
287 /// See [tmpfs] for details.
288 ///
289 /// [tmpfs]: Composition::tmpfs
290 #[cfg(target_os = "linux")]
291 pub fn with_tmpfs(self, paths: Vec<String>) -> Composition {
292 Composition {
293 tmpfs: paths,
294 ..self
295 }
296 }
297
298 /// Sets the [StartPolicy] for this [Composition].
299 ///
300 /// Defaults to a [relaxed](StartPolicy::Relaxed) policy.
301 pub fn with_start_policy(self, start_policy: StartPolicy) -> Composition {
302 Composition {
303 start_policy,
304 ..self
305 }
306 }
307
308 /// Assigns the full set of environmental variables available for the [OperationalContainer].
309 ///
310 /// Each key in the map should be the environmental variable name
311 /// and its corresponding value will be set as its value.
312 ///
313 /// This method replaces the entire existing env map provided.
314 ///
315 /// [OperationalContainer]: crate::container::OperationalContainer
316 pub fn with_env(self, env: HashMap<String, String>) -> Composition {
317 Composition { env, ..self }
318 }
319
320 /// Sets the command of the container.
321 ///
322 /// If no entries in the command vector is provided to the [Composition],
323 /// the command within the [Image] will be used, if any.
324 pub fn with_cmd(self, cmd: Vec<String>) -> Composition {
325 Composition { cmd, ..self }
326 }
327
328 /// Add a host port mapping to the container.
329 ///
330 /// This is useful when the host environment running docker cannot support IP routing
331 /// within the docker network, such that test containers cannot communicate between themselves.
332 /// This escape hatch allows the host to be involved to route traffic.
333 /// This mechanism is not recommended, as concurrent tests utilizing the same host port
334 /// will fail since the port is already in use.
335 /// It is recommended to use [Composition::publish_all_ports].
336 ///
337 /// If an port mapping on the exported port has already been issued on the [Composition],
338 /// it will be overidden.
339 pub fn port_map(&mut self, exported: u32, host: u32) -> &mut Composition {
340 self.port
341 .push((format!("{}/tcp", exported), format!("{}", host)));
342 self
343 }
344
345 /// Allocates an ephemeral host port for all of the container's exposed ports.
346 ///
347 /// Mapped host ports can be found via [crate::container::OperationalContainer::host_port] method.
348 pub fn publish_all_ports(&mut self, publish: bool) -> &mut Composition {
349 self.publish_all_ports = publish;
350 self
351 }
352
353 /// Sets the name of the container that will eventually be started.
354 ///
355 /// This is merely part of the final container name, and the full container name issued
356 /// to docker will be generated.
357 /// The container name assigned here is also used to resolve the `handle` concept used
358 /// by dockertest.
359 ///
360 /// The container name defaults to the repository name.
361 ///
362 /// NOTE: If the `Composition` is a static container with an
363 /// `External` management policy the container name *MUST* match the container_name of
364 /// the external container and is required to be set.
365 pub fn with_container_name<T: ToString>(self, container_name: T) -> Composition {
366 Composition {
367 user_provided_container_name: Some(container_name.to_string()),
368 ..self
369 }
370 }
371
372 /// Sets network aliases for this `Composition`.
373 pub fn with_alias(self, aliases: Vec<String>) -> Composition {
374 Composition {
375 network_aliases: Some(aliases),
376 ..self
377 }
378 }
379
380 /// Adds network alias to this `Composition`
381 pub fn alias(&mut self, alias: String) -> &mut Composition {
382 match self.network_aliases {
383 Some(ref mut network_aliases) => network_aliases.push(alias),
384 None => self.network_aliases = Some(vec![alias]),
385 };
386 self
387 }
388
389 /// Sets the `WaitFor` trait object for this `Composition`.
390 ///
391 /// The default `WaitFor` implementation used is [RunningWait].
392 ///
393 /// [RunningWait]: crate::waitfor::RunningWait
394 pub fn with_wait_for(self, wait: Box<dyn WaitFor>) -> Composition {
395 Composition { wait, ..self }
396 }
397
398 /// Sets log options for this `Composition`.
399 /// By default `LogAction::Forward`, `LogPolicy::OnError`, and `LogSource::StdErr` is enabled.
400 /// To clear default log option pass `None` or specify your own log options.
401 pub fn with_log_options(self, log_options: Option<LogOptions>) -> Composition {
402 Composition {
403 log_options,
404 ..self
405 }
406 }
407
408 /// Sets the environment variable to the given value.
409 ///
410 /// NOTE: if [with_env] is called after a call to [env], all values added by [env] will be overwritten.
411 ///
412 /// [env]: Composition::env
413 /// [with_env]: Composition::with_env
414 pub fn env<T: ToString, S: ToString>(&mut self, name: T, value: S) -> &mut Composition {
415 self.env.insert(name.to_string(), value.to_string());
416 self
417 }
418
419 /// Appends the command string to the current command vector.
420 ///
421 /// If no entries in the command vector is provided to the [Composition],
422 /// the command within the [Image] will be used, if any.
423 ///
424 /// NOTE: if [with_cmd] is called after a call to [cmd], all entries to the command vector
425 /// added with [cmd] will be overwritten.
426 ///
427 /// [cmd]: Composition::cmd
428 /// [with_cmd]: Composition::with_cmd
429 pub fn cmd<T: ToString>(&mut self, cmd: T) -> &mut Composition {
430 self.cmd.push(cmd.to_string());
431 self
432 }
433
434 /// Appends the tmpfs mount path to the current set of tmpfs mount paths.
435 ///
436 /// NOTE: if [with_tmpfs] is called after a call to [tmpfs], all entries to the tmpfs vector
437 /// added with [with_tmpfs] will be overwritten.
438 ///
439 /// Details:
440 /// - Only available on linux.
441 /// - Size of the tmpfs mount defaults to 50% of the hosts total RAM.
442 /// - Defaults to file mode '1777' (world-writable).
443 ///
444 /// [tmpfs]: Composition::tmpfs
445 /// [with_tmpfs]: Composition::with_tmpfs
446 #[cfg(target_os = "linux")]
447 pub fn tmpfs<T: ToString>(&mut self, path: T) -> &mut Composition {
448 self.tmpfs.push(path.to_string());
449 self
450 }
451
452 /// Adds the given named volume to the Composition.
453 /// Named volumes can be shared between containers, specifying the same named volume for
454 /// another Composition will give both access to the volume.
455 /// `path_in_container` has to be an absolute path.
456 pub fn named_volume<T: ToString, S: ToString>(
457 &mut self,
458 volume_name: T,
459 path_in_container: S,
460 ) -> &mut Composition {
461 self.named_volumes
462 .push((volume_name.to_string(), path_in_container.to_string()));
463 self
464 }
465 /// Adds the given bind mount to the Composition.
466 /// A bind mount only exists for a single container and maps a given file or directory from the
467 /// host to the container.
468 /// Use named volumes if you want to share data between containers.
469 /// The `host_path` can either point to a directory or a file that MUST exist on the local host.
470 /// `path_in_container` has to be an absolute path.
471 pub fn bind_mount<T: ToString, S: ToString>(
472 &mut self,
473 host_path: T,
474 path_in_container: S,
475 ) -> &mut Composition {
476 // The ':Z' is needed due to permission issues, see
477 // https://stackoverflow.com/questions/24288616/permission-denied-on-accessing-host-directory-in-docker
478 // for more details
479 self.bind_mounts.push(format!(
480 "{}:{}:Z",
481 host_path.to_string(),
482 path_in_container.to_string()
483 ));
484 self
485 }
486
487 /// Inject the generated container name identified by `handle` into
488 /// this Composition environment variable `env`.
489 ///
490 /// This is used to establish inter communication between running containers
491 /// controlled by dockertest. This is traditionally established through environment variables
492 /// for connection details, and thus the DNS resolving capabilities within docker will
493 /// map the container name into the correct IP address.
494 ///
495 /// To correctly use this feature, the `StartPolicy` between the dependant containers
496 /// must be configured such that these connections can successfully be established.
497 /// Dockertest will not make any attempt to verify the integrity of these dependencies.
498 pub fn inject_container_name<T: ToString, E: ToString>(
499 &mut self,
500 handle: T,
501 env: E,
502 ) -> &mut Composition {
503 self.inject_container_name_env
504 .push((handle.to_string(), env.to_string()));
505 self
506 }
507
508 /// Defines this as a static container which will will only be cleaned up after the full test
509 /// binary has executed.
510 /// If the static container is used across multiple tests in the same test binary, Dockertest can only guarantee that
511 /// the container will be started in its designated start order or earlier as other tests might
512 /// have already started it.
513 /// The container_name *MUST* be set to a unique value when using static containers.
514 /// To refer to the same container across `Dockertest` instances set the same container name for the
515 /// compostions.
516 ///
517 /// NOTE: When the `External` management policy is used, the container_name must be set to the
518 /// name of the external container.
519 pub fn static_container(&mut self, management: StaticManagementPolicy) -> &mut Composition {
520 let management = match management {
521 StaticManagementPolicy::External | StaticManagementPolicy::Internal => management,
522 StaticManagementPolicy::Dynamic => match std::env::var("DOCKERTEST_DYNAMIC") {
523 Ok(val) => match val.as_str() {
524 "EXTERNAL" => StaticManagementPolicy::External,
525 "INTERNAL" => StaticManagementPolicy::Internal,
526 "DYNAMIC" => StaticManagementPolicy::Dynamic,
527 _ => {
528 event!(Level::WARN, "DOCKERTEST_DYNAMIC environment variable set to unknown value, defaulting to Dynamic policy");
529 StaticManagementPolicy::Dynamic
530 }
531 },
532 Err(_) => management,
533 },
534 };
535 self.management = Some(management);
536 self
537 }
538
539 /// Should this container be started with priviledged mode enabled?
540 /// This is required for some containers to run correctly.
541 /// See https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
542 /// for more details.
543 pub fn privileged(&mut self) -> &mut Composition {
544 self.privileged = true;
545 self
546 }
547
548 /// Fetch the assigned [StaticManagementPolicy], if any.
549 pub(crate) fn static_management_policy(&self) -> &Option<StaticManagementPolicy> {
550 &self.management
551 }
552
553 /// Query whether this Composition should be handled through static container checks.
554 pub(crate) fn is_static(&self) -> bool {
555 self.management.is_some()
556 }
557
558 // Configure the container's name with the given namespace as prefix
559 // and suffix.
560 // We do this to ensure that we do not have overlapping container names
561 // and make it clear which containers are run by DockerTest.
562 pub(crate) fn configure_container_name(&mut self, namespace: &str, suffix: &str) {
563 let name = match &self.user_provided_container_name {
564 None => self.image.repository(),
565 Some(n) => n,
566 };
567
568 if !self.is_static() {
569 // The docker daemon does not like '/' or '\' in container names
570 let stripped_name = name.replace('/', "_");
571
572 self.container_name = format!("{}-{}-{}", namespace, stripped_name, suffix);
573 } else {
574 self.container_name = name.to_string();
575 }
576 }
577
578 // Returns the Image associated with this Composition.
579 pub(crate) fn image(&self) -> &Image {
580 &self.image
581 }
582
583 /// Retrieve a copy of the applicable handle name for this composition.
584 ///
585 /// NOTE: this value will be outdated if [Composition::with_container_name] is invoked
586 /// with a different name.
587 pub fn handle(&self) -> String {
588 match &self.user_provided_container_name {
589 None => self.image.repository().to_string(),
590 Some(n) => n.clone(),
591 }
592 }
593}