Skip to content

Commit 728a41b

Browse files
authored
Merge branch '3.x' into feature/per-host-temp-dir
2 parents c81980d + 93542da commit 728a41b

14 files changed

Lines changed: 326 additions & 0 deletions

src/pyinfra/operations/docker.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,138 @@ def image(image: str, present: bool = True, force: bool = False):
284284
host.noop("There is no {0} image!".format(image))
285285

286286

287+
@operation()
288+
def build(
289+
path: str,
290+
tags: str | list[str] | None = None,
291+
dockerfile: str | None = None,
292+
build_args: dict[str, str] | None = None,
293+
labels: dict[str, str] | None = None,
294+
target: str | None = None,
295+
platform: str | None = None,
296+
network: str | None = None,
297+
cache_from: str | list[str] | None = None,
298+
cache_to: str | list[str] | None = None,
299+
secrets: list[str] | None = None,
300+
pull: bool = False,
301+
no_cache: bool = False,
302+
force: bool = False,
303+
builder: str | None = None,
304+
build_cmd: str = "docker build",
305+
):
306+
"""
307+
Build a Docker image from a context directory or URL.
308+
309+
+ path: build context (local directory or git/http URL)
310+
+ tags: tag or list of tags to apply to the built image (maps to ``-t``)
311+
+ dockerfile: path to the Dockerfile (maps to ``-f``; relative to the context)
312+
+ build_args: ``--build-arg`` values as a mapping
313+
+ labels: ``--label`` values as a mapping
314+
+ target: ``--target`` stage for multi-stage Dockerfiles
315+
+ platform: ``--platform`` (e.g. ``linux/amd64``)
316+
+ network: ``--network`` (e.g. ``host``)
317+
+ cache_from: ``--cache-from`` value or list of values; accepts image
318+
references and full backend specs (e.g. ``type=registry,ref=...``,
319+
``type=gha``, ``type=local,src=/tmp/.buildx-cache``)
320+
+ cache_to: ``--cache-to`` value or list of values; same backend syntax as
321+
``cache_from`` (BuildKit / ``docker buildx build`` only)
322+
+ secrets: list of raw ``--secret`` specs (e.g. ``id=mysecret,src=/run/secret``)
323+
+ pull: pass ``--pull`` to always fetch newer base images
324+
+ no_cache: pass ``--no-cache``
325+
+ force: rebuild even if all provided tags already exist locally
326+
+ builder: optional name passed via ``--builder`` to select a buildx
327+
builder instance (BuildKit only)
328+
+ build_cmd: command used to invoke the build, defaults to ``docker build``;
329+
set to ``"docker buildx build"`` to force BuildKit
330+
331+
Idempotency is tag-based: when ``tags`` is provided and ``force`` is false, the
332+
operation no-ops if every tag already exists on the target. Without ``tags``
333+
the build always runs (Docker's own layer cache still applies).
334+
335+
**Examples:**
336+
337+
.. code:: python
338+
339+
from pyinfra.operations import docker
340+
341+
# Build and tag a local context
342+
docker.build(
343+
name="Build app image",
344+
path="/srv/app",
345+
tags=["app:1.0", "app:latest"],
346+
build_args={"VERSION": "1.0"},
347+
pull=True,
348+
)
349+
350+
# BuildKit cross-platform build from a custom Dockerfile,
351+
# using a named buildx builder and a registry cache backend
352+
docker.build(
353+
name="Build multi-arch app",
354+
path="/srv/app",
355+
tags="registry.io/app:arm64",
356+
dockerfile="docker/Dockerfile.prod",
357+
platform="linux/arm64",
358+
build_cmd="docker buildx build",
359+
builder="multiarch",
360+
cache_from="type=registry,ref=registry.io/app:cache",
361+
cache_to="type=registry,ref=registry.io/app:cache,mode=max",
362+
)
363+
"""
364+
if not path:
365+
raise OperationError("docker.build requires a context path")
366+
367+
tag_list: list[str] = (
368+
[tags] if isinstance(tags, str) else list(tags) if tags is not None else []
369+
)
370+
cache_from_list: list[str] = (
371+
[cache_from]
372+
if isinstance(cache_from, str)
373+
else list(cache_from)
374+
if cache_from is not None
375+
else []
376+
)
377+
cache_to_list: list[str] = (
378+
[cache_to] if isinstance(cache_to, str) else list(cache_to) if cache_to is not None else []
379+
)
380+
381+
if tag_list and not force:
382+
missing = [tag for tag in tag_list if not host.get_fact(DockerImage, object_id=tag)]
383+
if not missing:
384+
host.noop("Image(s) {0} already exist!".format(", ".join(tag_list)))
385+
return
386+
387+
command_bits: list[str | QuoteString] = [build_cmd]
388+
if builder:
389+
command_bits.extend(["--builder", QuoteString(builder)])
390+
for tag in tag_list:
391+
command_bits.extend(["-t", QuoteString(tag)])
392+
if dockerfile:
393+
command_bits.extend(["-f", QuoteString(dockerfile)])
394+
for key, value in (build_args or {}).items():
395+
command_bits.extend(["--build-arg", QuoteString("{0}={1}".format(key, value))])
396+
for key, value in (labels or {}).items():
397+
command_bits.extend(["--label", QuoteString("{0}={1}".format(key, value))])
398+
if target:
399+
command_bits.extend(["--target", QuoteString(target)])
400+
if platform:
401+
command_bits.extend(["--platform", QuoteString(platform)])
402+
if network:
403+
command_bits.extend(["--network", QuoteString(network)])
404+
for cache in cache_from_list:
405+
command_bits.extend(["--cache-from", QuoteString(cache)])
406+
for cache in cache_to_list:
407+
command_bits.extend(["--cache-to", QuoteString(cache)])
408+
for secret in secrets or []:
409+
command_bits.extend(["--secret", QuoteString(secret)])
410+
if pull:
411+
command_bits.append("--pull")
412+
if no_cache:
413+
command_bits.append("--no-cache")
414+
command_bits.append(QuoteString(path))
415+
416+
yield StringCommand(*command_bits)
417+
418+
287419
@operation()
288420
def volume(volume: str, driver: str = "", labels: list[str] | None = None, present: bool = True):
289421
"""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app"
4+
},
5+
"commands": [
6+
"docker build /srv/app"
7+
]
8+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": "app:1.0",
5+
"network": "host",
6+
"cache_from": ["app:builder", "app:latest"],
7+
"secrets": ["id=npmrc,src=/root/.npmrc"]
8+
},
9+
"facts": {
10+
"docker.DockerImage": {
11+
"object_id=app:1.0": []
12+
}
13+
},
14+
"commands": [
15+
"docker build -t app:1.0 --network host --cache-from app:builder --cache-from app:latest --secret id=npmrc,src=/root/.npmrc /srv/app"
16+
]
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": "registry.io/app:cache",
5+
"build_cmd": "docker buildx build",
6+
"cache_from": "type=registry,ref=registry.io/app:cache",
7+
"cache_to": ["type=registry,ref=registry.io/app:cache,mode=max", "type=inline"]
8+
},
9+
"facts": {
10+
"docker.DockerImage": {
11+
"object_id=registry.io/app:cache": []
12+
}
13+
},
14+
"commands": [
15+
"docker buildx build -t registry.io/app:cache --cache-from type=registry,ref=registry.io/app:cache --cache-to type=registry,ref=registry.io/app:cache,mode=max --cache-to type=inline /srv/app"
16+
]
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": "app:prod",
5+
"dockerfile": "docker/Dockerfile.prod"
6+
},
7+
"facts": {
8+
"docker.DockerImage": {
9+
"object_id=app:prod": []
10+
}
11+
},
12+
"commands": [
13+
"docker build -t app:prod -f docker/Dockerfile.prod /srv/app"
14+
]
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": "app:1.0",
5+
"force": true
6+
},
7+
"commands": [
8+
"docker build -t app:1.0 /srv/app"
9+
]
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"kwargs": {
3+
"path": ""
4+
},
5+
"exception": {
6+
"name": "OperationError",
7+
"message": "docker.build requires a context path"
8+
}
9+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": ["app:1.0", "app:latest"]
5+
},
6+
"facts": {
7+
"docker.DockerImage": {
8+
"object_id=app:1.0": [
9+
{"Id": "sha256:abcd1234", "RepoTags": ["app:1.0"]}
10+
],
11+
"object_id=app:latest": [
12+
{"Id": "sha256:abcd1234", "RepoTags": ["app:latest"]}
13+
]
14+
}
15+
},
16+
"commands": [],
17+
"noop_description": "Image(s) app:1.0, app:latest already exist!"
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": ["app:1.0", "app:latest"]
5+
},
6+
"facts": {
7+
"docker.DockerImage": {
8+
"object_id=app:1.0": [
9+
{"Id": "sha256:abcd1234", "RepoTags": ["app:1.0"]}
10+
],
11+
"object_id=app:latest": []
12+
}
13+
},
14+
"commands": [
15+
"docker build -t app:1.0 -t app:latest /srv/app"
16+
]
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"kwargs": {
3+
"path": "/srv/app",
4+
"tags": "app:1.0"
5+
},
6+
"facts": {
7+
"docker.DockerImage": {
8+
"object_id=app:1.0": [
9+
{
10+
"Id": "sha256:abcd1234",
11+
"RepoTags": ["app:1.0"]
12+
}
13+
]
14+
}
15+
},
16+
"commands": [],
17+
"noop_description": "Image(s) app:1.0 already exist!"
18+
}

0 commit comments

Comments
 (0)