libcnb_test/
container_config.rs

1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4/// Config used when starting a container.
5///
6/// By default the container will run the CNB default process-type, however this can be
7/// overridden using [`ContainerConfig::entrypoint`] and [`ContainerConfig::command`].
8/// See: [CNB App Developer Guide: Run a multi-process app](https://buildpacks.io/docs/app-developer-guide/run-an-app/#run-a-multi-process-app)
9///
10/// # Example
11/// ```no_run
12/// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
13///
14/// TestRunner::default().build(
15///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
16///     |context| {
17///         // ...
18///         context.start_container(
19///             ContainerConfig::new()
20///                 .env("PORT", "12345")
21///                 .expose_port(12345),
22///             |container| {
23///                 // ...
24///             },
25///         );
26///     },
27/// );
28/// ```
29#[derive(Clone, Default)]
30pub struct ContainerConfig {
31    pub(crate) entrypoint: Option<String>,
32    pub(crate) command: Option<Vec<String>>,
33    pub(crate) env: HashMap<String, String>,
34    pub(crate) exposed_ports: HashSet<u16>,
35    pub(crate) bind_mounts: HashMap<PathBuf, PathBuf>,
36}
37
38impl ContainerConfig {
39    /// Creates an empty [`ContainerConfig`] instance.
40    ///
41    /// By default the container will run the CNB default process-type, however this can be
42    /// overridden using [`ContainerConfig::entrypoint`] and [`ContainerConfig::command`].
43    /// See: [CNB App Developer Guide: Run a multi-process app](https://buildpacks.io/docs/app-developer-guide/run-an-app/#run-a-multi-process-app)
44    ///
45    /// # Example
46    /// ```no_run
47    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
48    ///
49    /// TestRunner::default().build(
50    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
51    ///     |context| {
52    ///         // ...
53    ///         context.start_container(
54    ///             ContainerConfig::new()
55    ///                 .env("PORT", "12345")
56    ///                 .expose_port(12345),
57    ///             |container| {
58    ///                 // ...
59    ///             },
60    ///         );
61    ///     },
62    /// );
63    /// ```
64    #[must_use]
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Override the image's `entrypoint` (which is the CNB default process-type).
70    ///
71    /// See: [CNB App Developer Guide: Run a multi-process app](https://buildpacks.io/docs/app-developer-guide/run-an-app/#run-a-multi-process-app)
72    ///
73    /// # Example
74    /// ```no_run
75    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
76    ///
77    /// TestRunner::default().build(
78    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
79    ///     |context| {
80    ///         // ...
81    ///         context.start_container(ContainerConfig::new().entrypoint("worker"), |container| {
82    ///             // ...
83    ///         });
84    ///     },
85    /// );
86    /// ```
87    pub fn entrypoint(&mut self, entrypoint: impl Into<String>) -> &mut Self {
88        self.entrypoint = Some(entrypoint.into());
89        self
90    }
91
92    /// Set the container's `command` (CNB images have no default command).
93    ///
94    /// See: [CNB App Developer Guide: Run a multi-process app](https://buildpacks.io/docs/app-developer-guide/run-an-app/#run-a-multi-process-app)
95    ///
96    /// # Example
97    /// ```no_run
98    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
99    ///
100    /// TestRunner::default().build(
101    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
102    ///     |context| {
103    ///         // ...
104    ///         context.start_container(
105    ///             ContainerConfig::new().command(["--additional-arg1", "--additional-arg2"]),
106    ///             |container| {
107    ///                 // ...
108    ///             },
109    ///         );
110    ///     },
111    /// );
112    /// ```
113    pub fn command<I: IntoIterator<Item = S>, S: Into<String>>(&mut self, command: I) -> &mut Self {
114        self.command = Some(command.into_iter().map(S::into).collect());
115        self
116    }
117
118    /// Exposes a given port of the container to the host machine.
119    ///
120    /// The given port is mapped to a random port on the host machine. Use
121    /// [`crate::ContainerContext::address_for_port`] to obtain the local port for a mapped port.
122    ///
123    /// # Example
124    /// ```no_run
125    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
126    ///
127    /// TestRunner::default().build(
128    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
129    ///     |context| {
130    ///         // ...
131    ///         context.start_container(
132    ///             ContainerConfig::new()
133    ///                 .env("PORT", "12345")
134    ///                 .expose_port(12345),
135    ///             |container| {
136    ///                 let address_on_host = container.address_for_port(12345);
137    ///                 // ...
138    ///             },
139    ///         );
140    ///     },
141    /// );
142    /// ```
143    pub fn expose_port(&mut self, port: u16) -> &mut Self {
144        self.exposed_ports.insert(port);
145        self
146    }
147
148    /// Inserts or updates an environment variable mapping for the container.
149    ///
150    /// # Example
151    /// ```no_run
152    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
153    ///
154    /// TestRunner::default().build(
155    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
156    ///     |context| {
157    ///         // ...
158    ///         context.start_container(
159    ///             ContainerConfig::new()
160    ///                 .env("PORT", "5678")
161    ///                 .env("DEBUG", "true"),
162    ///             |container| {
163    ///                 // ...
164    ///             },
165    ///         );
166    ///     },
167    /// );
168    /// ```
169    pub fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
170        self.env.insert(key.into(), value.into());
171        self
172    }
173
174    /// Mount a host file or directory `source` into the container `target`. Useful for
175    /// integration tests that depend on persistent storage shared between container executions.
176    ///
177    /// See: [Docker Engine: Bind Mounts](https://docs.docker.com/engine/storage/bind-mounts/)
178    ///
179    /// # Example
180    /// ```no_run
181    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
182    ///
183    /// TestRunner::default().build(
184    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
185    ///     |context| {
186    ///         // ...
187    ///         context.start_container(
188    ///             ContainerConfig::new().bind_mount("/shared/cache", "/workspace/cache"),
189    ///             |container| {
190    ///                 // ...
191    ///             },
192    ///         );
193    ///     },
194    /// );
195    /// ```
196    pub fn bind_mount(
197        &mut self,
198        source: impl Into<PathBuf>,
199        target: impl Into<PathBuf>,
200    ) -> &mut Self {
201        self.bind_mounts.insert(source.into(), target.into());
202        self
203    }
204
205    /// Adds or updates multiple environment variable mappings for the container.
206    ///
207    /// # Example
208    /// ```no_run
209    /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner};
210    ///
211    /// TestRunner::default().build(
212    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
213    ///     |context| {
214    ///         // ...
215    ///         context.start_container(
216    ///             ContainerConfig::new().envs([("PORT", "5678"), ("DEBUG", "true")]),
217    ///             |container| {
218    ///                 // ...
219    ///             },
220    ///         );
221    ///     },
222    /// );
223    /// ```
224    pub fn envs<K: Into<String>, V: Into<String>, I: IntoIterator<Item = (K, V)>>(
225        &mut self,
226        envs: I,
227    ) -> &mut Self {
228        envs.into_iter().for_each(|(key, value)| {
229            self.env(key.into(), value.into());
230        });
231
232        self
233    }
234}