1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
//! The various mechanism available to specify a container to be part of the test.
use std::collections::HashMap;
use crate::{
composition::{Composition, StaticManagementPolicy},
waitfor::WaitFor,
Image, LogOptions, StartPolicy,
};
mod private {
pub use super::*;
pub trait Sealed {}
impl Sealed for TestBodySpecification {}
impl Sealed for TestSuiteSpecification {}
impl Sealed for DynamicSpecification {}
impl Sealed for ExternalSpecification {}
}
/// Implemented by types that can represent and instruct how dockertest should interact with
/// a specific docker container.
pub trait ContainerSpecification: private::Sealed {
/// Convert the specification of a container into a `Composition`.
///
/// The `Composition` type is an internal implementation detail.
fn into_composition(self) -> Composition;
}
macro_rules! impl_specify_container {
($s:ty) => {
impl $s {
/// Sets the [StartPolicy] when having to create and spin up this container.
///
/// The start policy is needed to determine the order of containers to start in order
/// to successfully start dependant containers.
///
/// If not specified, [StartPolicy::Relaxed] is the default policy.
pub fn set_start_policy(self, start_policy: StartPolicy) -> Self {
Self {
composition: self.composition.with_start_policy(start_policy),
}
}
/// Assign the full set of environment variables into the [OperationalContainer].
///
/// Each key in the map should be the environmental variable name
/// and its corresponding value will be set as its value.
///
/// This method replaces all existing environment variables previously provided.
///
/// [OperationalContainer]: crate::container::OperationalContainer
pub fn replace_env(self, env: HashMap<String, String>) -> Self {
Self {
composition: self.composition.with_env(env),
}
}
/// Modify a single environment variable available for the [OperationalContainer].
///
/// A [replace_env] call will undo what has been configured individually with this
/// method.
///
/// [OperationalContainer]: crate::container::OperationalContainer
/// [replace_env]: Self::replace_env
pub fn modify_env<T: ToString, S: ToString>(&mut self, name: T, value: S) -> &mut Self {
self.composition.env(name, value);
self
}
/// Assign the full set of command vector entries for the [OperationalContainer].
///
/// This method replaces all existing command vector entries previously provided.
///
/// [OperationalContainer]: crate::container::OperationalContainer
pub fn replace_cmd(self, cmd: Vec<String>) -> Self {
Self {
composition: self.composition.with_cmd(cmd),
}
}
/// Appends the tmpfs mount path to the current set of tmpfs mount paths.
///
/// Details:
/// - Only available on linux.
/// - Size of the tmpfs mount defaults to 50% of the hosts total RAM.
/// - Defaults to file mode '1777' (world-writable).
///
/// [tmpfs]: Self::tmpfs
/// [replace_tmpfs]: Self::replace_tmpfs
#[cfg(target_os = "linux")]
pub fn tmpfs<T: ToString>(&mut self, path: T) -> &mut Self {
self.composition.tmpfs(path.to_string());
self
}
/// Replaces all the tmpfs mount paths for the [OperationalContainer]
///
/// This method replaces all existing tmpfs mount paths previously provided.
///
/// See [tmpfs] for details.
///
/// [tmpfs]: Self::tmpfs
/// [OperationalContainer]: crate::container::OperationalContainer
#[cfg(target_os = "linux")]
pub fn replace_tmpfs(self, paths: Vec<String>) -> Self {
Self {
composition: self.composition.with_tmpfs(paths),
}
}
/// Append a command entry into the command vector.
///
/// A [replace_cmd] call will undo what has been configured individually with this
/// method.
///
/// [replace_cmd]: Self::replace_cmd
pub fn append_cmd<T: ToString>(&mut self, cmd: T) -> &mut Self {
self.composition.cmd(cmd);
self
}
/// Allocate an ephemeral host port for all exposed ports specified in the container.
///
/// Mapped host ports can be found via [OperationalContainer::host_port] method.
///
/// [OperationalContainer::host_port]: crate::container::OperationalContainer::host_port
pub fn set_publish_all_ports(mut self, publish: bool) -> Self {
self.composition.publish_all_ports(publish);
self
}
/// Add a host port mapping to the container.
///
/// This is useful when the host environment running docker cannot support IP routing
/// within the docker network, such that test containers cannot communicate between
/// themselves. This escape hatch allows the host to be involved to route traffic.
///
/// This mechanism is not recommended, as concurrent tests utilizing the same host port
/// will fail since the port is already in use. If utilizing the host is needed, it is
/// recommended to use [set_publish_all_ports].
///
/// This function can overwrite previously mapped ports, if invoked repeatedly.
///
/// [set_publish_all_ports]: Self::set_publish_all_ports
// TODO: Add a replace_port_map that takes (exported, host) tuples
// TODO: Guarantee that a modification of one already exported/host value is
// actually constructed and overriden correctly.
pub fn modify_port_map(&mut self, exported: u32, host: u32) -> &mut Self {
self.composition.port_map(exported, host);
self
}
/// Specify the privilege mode of the started container.
///
/// This may be required for some containers to run correctly.
/// See the corresponding [docker reference] on this topic.
///
/// [docker reference]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
pub fn privileged(&mut self, privileged: bool) -> &mut Self {
self.composition.privileged = privileged;
self
}
/// Specify the privilege mode of the started container.
///
/// This may be required for some containers to run correctly.
/// See the corresponding [docker reference] on this topic.
///
/// [docker reference]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
pub fn set_privileged(mut self, privileged: bool) -> Self {
self.composition.privileged = privileged;
self
}
/// Specify a string handle used to retrieve a reference to the [OperationalContainer]
/// within the test body.
///
/// This value defaults to the repository name of the image used when constructing
/// this container specification.
///
/// [OperationalContainer]: crate::container::OperationalContainer
pub fn set_handle<T: ToString>(self, handle: T) -> Self {
Self {
composition: self.composition.with_container_name(handle),
}
}
/// Assign the full set of container name aliases on the docker network.
pub fn replace_network_alias(self, aliases: Vec<String>) -> Self {
Self {
composition: self.composition.with_alias(aliases),
}
}
/// Add a single container name alias on the docker network.
pub fn append_network_alias(&mut self, alias: String) -> &mut Self {
self.composition.alias(alias);
self
}
/// Set the [WaitFor] trait object for this container specification.
///
/// If not specified, [RunningWait] will be the default value.
///
/// [WaitFor]: crate::waitfor::WaitFor
/// [RunningWait]: crate::waitfor::RunningWait
pub fn set_wait_for(self, wait: Box<dyn WaitFor>) -> Self {
Self {
composition: self.composition.with_wait_for(wait),
}
}
/// Specify how to handle logging from the container.
///
/// If not specified, [LogAction::Forward], [LogPolicy::OnError] and
/// [LogSource::StdErr] is enabled. To clear the default options, invoke this method
/// with a `None` value.
///
/// [LogSource::StdErr]: crate::composition::LogSource
/// [LogAction::Forward]: crate::composition::LogAction
/// [LogPolicy::OnError]: crate::composition::LogPolicy
pub fn set_log_options(self, log_options: Option<LogOptions>) -> Self {
Self {
composition: self.composition.with_log_options(log_options),
}
}
/// Add a named volume to this container.
///
/// Named volumes can be shared between multiple containers. By specifying the same
/// volume name across multiple container specifications, both container will be
/// given access to the same volume.
///
/// * `path_in_container` must be an absolute path.
// TODO: Add a set_ variant
pub fn modify_named_volume<T: ToString, S: ToString>(
&mut self,
volume_name: T,
path_in_container: S,
) -> &mut Self {
self.composition
.named_volume(volume_name, path_in_container);
self
}
/// Add a bind mount to this container.
///
/// A bind mount only exists for a single container, and maps a given file or directory
/// from the host into the container.
///
/// Use named volumes if you require shared data access between containers.
///
/// * `host_path` can either point to a file or directory that must exist on the host.
/// * `path_in_container` must be an absolute path.
pub fn modify_bind_mount<T: ToString, S: ToString>(
&mut self,
host_path: T,
path_in_container: S,
) -> &mut Self {
self.composition.bind_mount(host_path, path_in_container);
self
}
/// Inject the full, generated container name identified by `handle` into this
/// container specification environment.
///
/// This is used to establish inter communication between running containers
/// controlled by dockertest. This is traditionally established through environment
/// variables for connection details, and thus the DNS resolving capabilities within
/// docker will map the container name into the correct IP address.
///
/// To correctly use this feature, the `StartPolicy` between the dependant containers
/// must be configured such that these connections can successfully be established.
/// Dockertest will not make any attempt to verify the integrity of these dependencies.
// TODO: naming
// TODO: Refactor to use some reference mechanism
pub fn inject_container_name<T: ToString, E: ToString>(
&mut self,
handle: T,
env: E,
) -> &mut Self {
self.composition.inject_container_name(handle, env);
self
}
}
};
}
/// A specification of a container external to dockertest.
///
/// The management and lifecycle of this container is unknown and not touched by dockertest.
#[derive(Clone, Debug)]
pub struct ExternalSpecification {
name: String,
}
impl ExternalSpecification {
/// Create a new [ExternalSpecification] with the full container name of an existing container.
pub fn with_container_name<T: ToString>(name: T) -> Self {
Self {
name: name.to_string(),
}
}
}
impl ContainerSpecification for ExternalSpecification {
fn into_composition(self) -> Composition {
let mut c = Composition::with_repository("NOT REQUIRED").with_container_name(self.name);
c.static_container(StaticManagementPolicy::External);
c
}
}
/// A specification of a container that shall live across all tests within the testsuite,
/// and should be appropriately shut down once all tests in the suite have terminated.
///
/// NOTE: [TestSuiteSpecification] is an experimental management mode.
#[derive(Clone, Debug)]
pub struct TestSuiteSpecification {
composition: Composition,
}
impl ContainerSpecification for TestSuiteSpecification {
fn into_composition(mut self) -> Composition {
self.composition
.static_container(StaticManagementPolicy::Internal);
self.composition
}
}
impl TestSuiteSpecification {
/// Create a new [TestSuiteSpecification] based on the image pointed to by this repository.
///
/// This will internally create an [Image] based on the provided repository name,
/// and default the tag to `latest`.
///
/// NOTE: [TestSuiteSpecification] is an experimental management mode.
pub fn with_repository<T: ToString>(repository: T) -> Self {
Self {
composition: Composition::with_repository(repository),
}
}
/// Create a new [TestSuiteSpecification] based on provided [Image].
///
/// NOTE: [TestSuiteSpecification] is an experimental management mode.
pub fn with_image(image: Image) -> Self {
Self {
composition: Composition::with_image(image),
}
}
}
impl_specify_container!(TestSuiteSpecification);
/// The standard container specification.
///
/// This containers' lifecycle is managed entirely within a single dockertest test body run.
/// It is created, started, and ensured exited all within the scope of the test body.
pub struct TestBodySpecification {
composition: Composition,
}
impl ContainerSpecification for TestBodySpecification {
fn into_composition(self) -> Composition {
self.composition
}
}
impl TestBodySpecification {
/// Create a new [TestBodySpecification] based on the image pointed to by this repository.
///
/// This will internally create an [Image] based on the provided repository name,
/// and default the tag to `latest`.
pub fn with_repository<T: ToString>(repository: T) -> Self {
Self {
composition: Composition::with_repository(repository),
}
}
/// Create a new [TestBodySpecification] based on provided [Image].
pub fn with_image(image: Image) -> Self {
Self {
composition: Composition::with_image(image),
}
}
}
impl_specify_container!(TestBodySpecification);
/// A full specification of a container whose lifecycle is partially managed by dockertest.
///
/// A dynamic container specification has the ability to create and re-use an existing
/// container matching its properties, but will never attempt to terminate the container.
#[derive(Clone, Debug)]
pub struct DynamicSpecification {
composition: Composition,
}
impl ContainerSpecification for DynamicSpecification {
fn into_composition(mut self) -> Composition {
self.composition
.static_container(StaticManagementPolicy::Dynamic);
self.composition
}
}
impl DynamicSpecification {
/// Create a new [TestSuiteSpecification] based on the image pointed to by this repository.
///
/// The provided `container_name` will be used to locate an existing container, or create
/// the container if it does not exist.
pub fn with_repository<T: ToString, S: ToString>(repository: T, container_name: S) -> Self {
Self {
composition: Composition::with_repository(repository)
.with_container_name(container_name),
}
}
/// Create a new [TestBodySpecification] based on provided [Image].
///
/// The provided `container_name` will be used to locate an existing container, or create
/// the container if it does not exist.
pub fn with_image<S: ToString>(image: Image, container_name: S) -> Self {
Self {
composition: Composition::with_image(image).with_container_name(container_name),
}
}
}
impl_specify_container!(DynamicSpecification);