Skip to content

Commit 72b35be

Browse files
DDtKeymbodmer
andauthored
feat: support build options - no_cache, skip_if_exists and buildargs (#856)
Signed-off-by: mbodmer <[email protected]> Co-authored-by: mbodmer <[email protected]>
1 parent f76070e commit 72b35be

File tree

16 files changed

+740
-55
lines changed

16 files changed

+740
-55
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ license = "MIT OR Apache-2.0"
1515
readme = "README.md"
1616
repository = "https://github.com/testcontainers/testcontainers-rs"
1717
rust-version = "1.88"
18+
19+
[patch.crates-io]
20+
bollard = { git = "https://github.com/DDtKey/bollard.git", branch = "fix/providerless-session" }

docs/features/building_images.md

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
# Building Docker Images
2+
3+
Testcontainers for Rust supports building Docker images directly within your tests. This is useful when you need to test against custom-built images with specific configurations, or when your test requires a dynamically generated Dockerfile.
4+
5+
## Building a Simple Image
6+
7+
Use [`GenericBuildableImage`](https://docs.rs/testcontainers/latest/testcontainers/struct.GenericBuildableImage.html) to define an image that will be built from a Dockerfile:
8+
9+
```rust
10+
use testcontainers::{
11+
core::WaitFor,
12+
runners::AsyncRunner,
13+
GenericBuildableImage,
14+
};
15+
16+
#[tokio::test]
17+
async fn test_custom_image() -> Result<(), Box<dyn std::error::Error>> {
18+
let image = GenericBuildableImage::new("my-test-app", "latest")
19+
.with_dockerfile_string(r#"
20+
FROM alpine:latest
21+
COPY --chmod=0755 app.sh /usr/local/bin/app
22+
ENTRYPOINT ["/usr/local/bin/app"]
23+
"#)
24+
.with_data(
25+
r#"#!/bin/sh
26+
echo "Hello from custom image!"
27+
"#,
28+
"./app.sh",
29+
)
30+
.build_image()
31+
.await?;
32+
33+
let container = image
34+
.with_wait_for(WaitFor::message_on_stdout("Hello from custom image!"))
35+
.start()
36+
.await?;
37+
38+
Ok(())
39+
}
40+
```
41+
42+
## Adding Files to the Build Context
43+
44+
You can add files from your filesystem or provide inline data:
45+
46+
### From Filesystem
47+
48+
```rust
49+
let image = GenericBuildableImage::new("my-app", "latest")
50+
.with_dockerfile("./path/to/Dockerfile")
51+
.with_file("./target/release/myapp", "./myapp")
52+
.build_image()
53+
.await?;
54+
```
55+
56+
### Inline Data
57+
58+
```rust
59+
let image = GenericBuildableImage::new("my-app", "latest")
60+
.with_dockerfile_string("FROM alpine:latest\nCOPY config.json /config.json")
61+
.with_data(r#"{"port": 8080}"#, "./config.json")
62+
.build_image()
63+
.await?;
64+
```
65+
66+
## Build Options
67+
68+
The [`BuildImageOptions`](https://docs.rs/testcontainers/latest/testcontainers/core/struct.BuildImageOptions.html) struct provides fine-grained control over the build process.
69+
70+
### Skip Building if Image Exists
71+
72+
When running tests repeatedly, you can skip rebuilding if the image already exists:
73+
74+
```rust
75+
use testcontainers::core::BuildImageOptions;
76+
77+
let image = GenericBuildableImage::new("my-app", "v1.0")
78+
.with_dockerfile_string("FROM alpine:latest")
79+
.build_image_with(
80+
BuildImageOptions::new()
81+
.with_skip_if_exists(true)
82+
)
83+
.await?;
84+
```
85+
86+
This option:
87+
- Checks if an image with the same descriptor (name:tag) already exists
88+
- Skips the build if found, using the existing image
89+
- Is thread-safe - parallel tests building the same image will be serialized
90+
91+
### Disable Build Cache
92+
93+
Force a fresh build without using Docker's layer cache:
94+
95+
```rust
96+
let image = GenericBuildableImage::new("my-app", "latest")
97+
.with_dockerfile_string("FROM alpine:latest\nRUN apk update")
98+
.build_image_with(
99+
BuildImageOptions::new()
100+
.with_no_cache(true)
101+
)
102+
.await?;
103+
```
104+
105+
### Build Arguments
106+
107+
Pass build-time variables to your Dockerfile using `ARG` instructions:
108+
109+
```rust
110+
let image = GenericBuildableImage::new("my-app", "latest")
111+
.with_dockerfile_string(r#"
112+
FROM alpine:latest
113+
ARG VERSION
114+
ARG BUILD_DATE
115+
RUN echo "Building version ${VERSION} on ${BUILD_DATE}"
116+
"#)
117+
.build_image_with(
118+
BuildImageOptions::new()
119+
.with_build_arg("VERSION", "1.0.0")
120+
.with_build_arg("BUILD_DATE", "2024-10-25")
121+
)
122+
.await?;
123+
```
124+
125+
You can also provide build arguments as a HashMap:
126+
127+
```rust
128+
use std::collections::HashMap;
129+
130+
let mut args = HashMap::new();
131+
args.insert("VERSION".to_string(), "1.0.0".to_string());
132+
args.insert("ENVIRONMENT".to_string(), "test".to_string());
133+
134+
let image = GenericBuildableImage::new("my-app", "latest")
135+
.with_dockerfile_string("FROM alpine:latest\nARG VERSION\nARG ENVIRONMENT")
136+
.build_image_with(
137+
BuildImageOptions::new()
138+
.with_build_args(args)
139+
)
140+
.await?;
141+
```
142+
143+
### Combining Options
144+
145+
All build options can be chained together:
146+
147+
```rust
148+
let image = GenericBuildableImage::new("my-app", "latest")
149+
.with_dockerfile_string("FROM alpine:latest\nARG VERSION")
150+
.build_image_with(
151+
BuildImageOptions::new()
152+
.with_skip_if_exists(true)
153+
.with_no_cache(false)
154+
.with_build_arg("VERSION", "1.0.0")
155+
)
156+
.await?;
157+
```
158+
159+
## Synchronous API
160+
161+
For non-async tests, use the [`SyncBuilder`](https://docs.rs/testcontainers/latest/testcontainers/runners/trait.SyncBuilder.html) trait to build images and [`SyncRunner`](https://docs.rs/testcontainers/latest/testcontainers/runners/trait.SyncRunner.html) to run containers (requires the `blocking` feature):
162+
163+
```rust
164+
use testcontainers::{
165+
core::BuildImageOptions,
166+
runners::{SyncBuilder, SyncRunner},
167+
GenericBuildableImage,
168+
};
169+
170+
#[test]
171+
fn test_sync_build() -> Result<(), Box<dyn std::error::Error>> {
172+
let image = GenericBuildableImage::new("my-app", "latest")
173+
.with_dockerfile_string("FROM alpine:latest")
174+
.build_image()?;
175+
176+
let container = image.start()?;
177+
Ok(())
178+
}
179+
180+
#[test]
181+
fn test_sync_build_with_options() -> Result<(), Box<dyn std::error::Error>> {
182+
let image = GenericBuildableImage::new("my-app", "latest")
183+
.with_dockerfile_string("FROM alpine:latest\nARG VERSION")
184+
.build_image_with(
185+
BuildImageOptions::new()
186+
.with_skip_if_exists(true)
187+
.with_build_arg("VERSION", "1.0.0")
188+
)?;
189+
190+
let container = image.start()?;
191+
Ok(())
192+
}
193+
```
194+
195+
## Best Practices
196+
197+
### Use Descriptive Tags
198+
199+
Use meaningful image names and tags to avoid conflicts:
200+
201+
```rust
202+
// Good: specific and descriptive
203+
GenericBuildableImage::new("test-user-service", "integration-v1")
204+
205+
// Avoid: generic names that might conflict
206+
GenericBuildableImage::new("test", "latest")
207+
```
208+
209+
### Leverage skip_if_exists for Faster Tests
210+
211+
When your image doesn't change between test runs:
212+
213+
```rust
214+
let image = GenericBuildableImage::new("my-stable-app", "v1.0")
215+
.with_dockerfile_string("FROM alpine:latest\nRUN apk add curl")
216+
.build_image_with(
217+
BuildImageOptions::new()
218+
.with_skip_if_exists(true)
219+
)
220+
.await?;
221+
```
222+
223+
### Use Build Arguments for Flexibility
224+
225+
Make your test images configurable:
226+
227+
```rust
228+
fn build_test_image(version: &str) -> GenericBuildableImage {
229+
GenericBuildableImage::new("my-app", version)
230+
.with_dockerfile_string("FROM alpine:latest\nARG APP_VERSION\nENV VERSION=$APP_VERSION")
231+
.build_image_with(
232+
BuildImageOptions::new()
233+
.with_build_arg("APP_VERSION", version)
234+
)
235+
}
236+
```
237+
238+
## Common Patterns
239+
240+
### Building from a Complex Dockerfile
241+
242+
```rust
243+
let dockerfile = r#"
244+
FROM rust:1.75 as builder
245+
WORKDIR /app
246+
COPY Cargo.toml Cargo.lock ./
247+
COPY src ./src
248+
RUN cargo build --release
249+
250+
FROM debian:bookworm-slim
251+
COPY --from=builder /app/target/release/myapp /usr/local/bin/
252+
CMD ["/usr/local/bin/myapp"]
253+
"#;
254+
255+
let image = GenericBuildableImage::new("my-rust-app", "latest")
256+
.with_dockerfile_string(dockerfile)
257+
.with_file("./Cargo.toml", "./Cargo.toml")
258+
.with_file("./Cargo.lock", "./Cargo.lock")
259+
.with_file("./src", "./src")
260+
.build_image()
261+
.await?;
262+
```
263+
264+
### Building Multiple Variants
265+
266+
```rust
267+
async fn build_variant(variant: &str, port: u16) -> Result<GenericImage, Box<dyn std::error::Error>> {
268+
GenericBuildableImage::new("my-app", variant)
269+
.with_dockerfile_string(format!(r#"
270+
FROM alpine:latest
271+
ARG PORT
272+
ENV APP_PORT=$PORT
273+
CMD ["sh", "-c", "echo 'Running on port $APP_PORT' && sleep infinity"]
274+
"#))
275+
.build_image_with(
276+
BuildImageOptions::new()
277+
.with_build_arg("PORT", port.to_string())
278+
.with_skip_if_exists(true)
279+
)
280+
.await
281+
}
282+
283+
#[tokio::test]
284+
async fn test_multiple_variants() -> Result<(), Box<dyn std::error::Error>> {
285+
let image1 = build_variant("dev", 8080).await?;
286+
let image2 = build_variant("staging", 8081).await?;
287+
288+
// Use images in tests...
289+
Ok(())
290+
}
291+
```
292+
293+
## Troubleshooting
294+
295+
### Build Fails with "no active session" Error
296+
297+
This typically occurs when BuildKit encounters issues. Potential solutions:
298+
299+
1. Remove the build cache and retry:
300+
```rust
301+
BuildImageOptions::new().with_no_cache(true)
302+
```
303+
304+
2. Ensure Docker daemon is running properly:
305+
```bash
306+
docker info
307+
```
308+
309+
3. In CI environments, ensure BuildKit is properly initialized
310+
311+
### Image Not Found After Build
312+
313+
Verify the descriptor matches exactly:
314+
315+
```rust
316+
let image = GenericBuildableImage::new("my-app", "v1.0") // Name and tag must match
317+
.with_dockerfile_string("FROM alpine:latest")
318+
.build_image()
319+
.await?;
320+
321+
// The image is now available as "my-app:v1.0"
322+
```
323+
324+
### Build Arguments Not Working
325+
326+
Ensure ARG instructions are in the Dockerfile before they're used:
327+
328+
```dockerfile
329+
# Correct
330+
ARG VERSION
331+
ENV APP_VERSION=$VERSION
332+
333+
# Incorrect - ARG comes after usage
334+
ENV APP_VERSION=$VERSION
335+
ARG VERSION
336+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ nav:
4545
- features/wait_strategies.md
4646
- features/exec_commands.md
4747
- features/networking.md
48+
- features/building_images.md
4849
- System Requirements:
4950
- system_requirements/docker.md
5051
- Contributing:

testcontainers/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"]
1717

1818
[dependencies]
1919
async-trait = { version = "0.1" }
20-
bollard = { version = "0.19.1", features = ["buildkit"] }
20+
bollard = { version = "0.19.3", features = ["buildkit_providerless"] }
2121
bytes = "1.6.0"
2222
conquer-once = { version = "0.4", optional = true }
2323
docker_credential = "1.3.1"

testcontainers/src/core.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#[cfg(feature = "reusable-containers")]
22
pub use self::image::ReuseDirective;
33
pub use self::{
4-
build_context::BuildContextBuilder,
5-
buildable::BuildableImage,
4+
build::{
5+
build_context::BuildContextBuilder, build_options::BuildImageOptions,
6+
buildable::BuildableImage,
7+
},
68
containers::*,
79
copy::{CopyDataSource, CopyToContainer, CopyToContainerCollection, CopyToContainerError},
810
healthcheck::Healthcheck,
@@ -12,11 +14,10 @@ pub use self::{
1214
wait::{cmd_wait::CmdWaitFor, WaitFor},
1315
};
1416

15-
mod buildable;
1617
mod image;
1718

1819
pub(crate) mod async_drop;
19-
pub(crate) mod build_context;
20+
pub mod build;
2021
pub mod client;
2122
pub(crate) mod containers;
2223
pub(crate) mod copy;
File renamed without changes.

0 commit comments

Comments
 (0)