• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 24791541718

22 Apr 2026 05:01PM UTC coverage: 92.911% (-0.02%) from 92.926%
24791541718

Pull #23133

github

web-flow
Merge d56db518e into 3d0987454
Pull Request #23133: Add buildctl engine

408 of 440 new or added lines in 13 files covered. (92.73%)

2 existing lines in 2 files now uncovered.

91882 of 98892 relevant lines covered (92.91%)

4.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.78
/src/python/pants/backend/docker/goals/package_image_test.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
1✔
5

6
import json
1✔
7
import logging
1✔
8
import os.path
1✔
9
from collections import namedtuple
1✔
10
from collections.abc import Callable
1✔
11
from textwrap import dedent
1✔
12
from typing import Any, ContextManager, cast
1✔
13

14
import pytest
1✔
15

16
from pants.backend.docker.engine_types import DockerBuildEngine, DockerEngines
1✔
17
from pants.backend.docker.goals.package_image import (
1✔
18
    DockerImageBuildProcess,
19
    DockerImageRefs,
20
    DockerImageTagValueError,
21
    DockerInfoV1,
22
    DockerPackageFieldSet,
23
    DockerRepositoryNameError,
24
    GetImageRefsRequest,
25
    ImageRefRegistry,
26
    ImageRefTag,
27
    build_docker_image,
28
    get_docker_image_build_process,
29
    get_image_refs,
30
    parse_image_id_from_buildkit_output,
31
    parse_image_id_from_podman_build_output,
32
    rules,
33
)
34
from pants.backend.docker.package_types import (
1✔
35
    DockerPushOnPackageBehavior,
36
    DockerPushOnPackageException,
37
)
38
from pants.backend.docker.registries import DockerRegistries, DockerRegistryOptions
1✔
39
from pants.backend.docker.subsystems.docker_options import DockerOptions
1✔
40
from pants.backend.docker.subsystems.dockerfile_parser import DockerfileInfo
1✔
41
from pants.backend.docker.target_types import (
1✔
42
    DockerImageTags,
43
    DockerImageTagsField,
44
    DockerImageTagsRequest,
45
    DockerImageTarget,
46
)
47
from pants.backend.docker.util_rules.binaries import BuildctlBinary, DockerBinary, PodmanBinary
1✔
48
from pants.backend.docker.util_rules.docker_build_args import (
1✔
49
    DockerBuildArgs,
50
    DockerBuildArgsRequest,
51
)
52
from pants.backend.docker.util_rules.docker_build_args import rules as build_args_rules
1✔
53
from pants.backend.docker.util_rules.docker_build_context import (
1✔
54
    DockerBuildContext,
55
    DockerBuildContextRequest,
56
)
57
from pants.backend.docker.util_rules.docker_build_env import (
1✔
58
    DockerBuildEnvironment,
59
    DockerBuildEnvironmentRequest,
60
)
61
from pants.backend.docker.util_rules.docker_build_env import rules as build_env_rules
1✔
62
from pants.engine.addresses import Address
1✔
63
from pants.engine.fs import (
1✔
64
    EMPTY_DIGEST,
65
    EMPTY_FILE_DIGEST,
66
    EMPTY_SNAPSHOT,
67
    CreateDigest,
68
    Digest,
69
    FileContent,
70
    Snapshot,
71
)
72
from pants.engine.platform import Platform
1✔
73
from pants.engine.process import (
1✔
74
    FallibleProcessResult,
75
    Process,
76
    ProcessExecutionEnvironment,
77
    ProcessExecutionFailure,
78
    ProcessResultMetadata,
79
)
80
from pants.engine.target import InvalidFieldException, WrappedTarget
1✔
81
from pants.engine.unions import UnionMembership, UnionRule
1✔
82
from pants.option.global_options import GlobalOptions, KeepSandboxes
1✔
83
from pants.testutil.option_util import create_subsystem
1✔
84
from pants.testutil.pytest_util import assert_logged, no_exception
1✔
85
from pants.testutil.rule_runner import QueryRule, RuleRunner, run_rule_with_mocks
1✔
86
from pants.util.frozendict import FrozenDict
1✔
87
from pants.util.value_interpolation import InterpolationContext, InterpolationError
1✔
88

89

90
@pytest.fixture
1✔
91
def rule_runner() -> RuleRunner:
1✔
92
    return RuleRunner(
1✔
93
        rules=[
94
            *rules(),
95
            *build_args_rules(),
96
            *build_env_rules(),
97
            QueryRule(GlobalOptions, []),
98
            QueryRule(DockerOptions, []),
99
            QueryRule(DockerBuildArgs, [DockerBuildArgsRequest]),
100
            QueryRule(DockerBuildEnvironment, [DockerBuildEnvironmentRequest]),
101
        ],
102
        target_types=[DockerImageTarget],
103
    )
104

105

106
class DockerImageTagsRequestPlugin(DockerImageTagsRequest):
1✔
107
    pass
1✔
108

109

110
def _create_build_context_mock(
1✔
111
    rule_runner: RuleRunner,
112
    address: Address,
113
    build_context_snapshot: Snapshot,
114
    copy_sources: tuple[str, ...],
115
    copy_build_args,
116
    version_tags: tuple[str, ...],
117
):
118
    """Create a mock function for create_docker_build_context."""
119
    tgt = rule_runner.get_target(address)
1✔
120

121
    def build_context_mock(request: DockerBuildContextRequest) -> DockerBuildContext:
1✔
122
        return DockerBuildContext.create(
1✔
123
            snapshot=build_context_snapshot,
124
            upstream_image_ids=[],
125
            dockerfile_info=DockerfileInfo(
126
                request.address,
127
                digest=EMPTY_DIGEST,
128
                source=os.path.join(address.spec_path, "Dockerfile"),
129
                copy_source_paths=copy_sources,
130
                copy_build_args=copy_build_args,
131
                version_tags=version_tags,
132
            ),
133
            build_args=rule_runner.request(DockerBuildArgs, [DockerBuildArgsRequest(tgt)]),
134
            build_env=rule_runner.request(
135
                DockerBuildEnvironment, [DockerBuildEnvironmentRequest(tgt)]
136
            ),
137
        )
138

139
    return build_context_mock
1✔
140

141

142
def _setup_docker_options(rule_runner: RuleRunner, options: dict | None) -> DockerOptions:
1✔
143
    """Setup DockerOptions with sensible defaults."""
144
    if options is not None:
1✔
145
        opts = options.copy()
1✔
146
        opts.setdefault("registries", {})
1✔
147
        opts.setdefault("default_repository", "{name}")
1✔
148
        opts.setdefault("default_context_root", "")
1✔
149
        opts.setdefault("build_args", [])
1✔
150
        opts.setdefault("build_target_stage", None)
1✔
151
        opts.setdefault("build_hosts", None)
1✔
152
        opts.setdefault("build_verbose", False)
1✔
153
        opts.setdefault("build_no_cache", False)
1✔
154
        opts.setdefault("engine", DockerEngines())
1✔
155
        opts.setdefault("env_vars", [])
1✔
156
        opts.setdefault("suggest_renames", True)
1✔
157
        opts.setdefault("push_on_package", DockerPushOnPackageBehavior.WARN)
1✔
158
        return create_subsystem(DockerOptions, **opts)
1✔
159
    else:
160
        return rule_runner.request(DockerOptions, [])
1✔
161

162

163
def _create_union_membership() -> UnionMembership:
1✔
164
    """Create union membership for Docker image tags plugin."""
165
    return UnionMembership.from_rules(
1✔
166
        [UnionRule(DockerImageTagsRequest, DockerImageTagsRequestPlugin)]
167
    )
168

169

170
def assert_build_process(
1✔
171
    rule_runner: RuleRunner,
172
    address: Address,
173
    *,
174
    options: dict | None = None,
175
    build_process_assertions: Callable[[DockerImageBuildProcess], None] | None = None,
176
    copy_sources: tuple[str, ...] = (),
177
    copy_build_args=(),
178
    build_context_snapshot: Snapshot = EMPTY_SNAPSHOT,
179
    version_tags: tuple[str, ...] = (),
180
    image_refs: DockerImageRefs | None = None,
181
    binary: DockerBinary | PodmanBinary | BuildctlBinary = DockerBinary("/dummy/docker"),
182
) -> DockerImageBuildProcess:
183
    """Test helper for get_docker_image_build_process rule.
184

185
    Tests Process construction without execution. Returns DockerImageBuildProcess for validation.
186
    Tests can access result.process for Process-specific assertions.
187
    """
188
    tgt = rule_runner.get_target(address)
1✔
189

190
    # Auto-generate image_refs if not provided (same logic as old assert_build)
191
    if image_refs is None:
1✔
192
        repository = address.target_name
1✔
193
        image_tags = tgt.get(DockerImageTagsField).value
1✔
194
        tags_to_use = ("latest",) if image_tags is None else image_tags
1✔
195
        image_refs = DockerImageRefs(
1✔
196
            [
197
                ImageRefRegistry(
198
                    registry=None,
199
                    repository=repository,
200
                    tags=tuple(
201
                        ImageRefTag(
202
                            template=tag,
203
                            formatted=tag,
204
                            full_name=f"{repository}:{tag}",
205
                            uses_local_alias=False,
206
                        )
207
                        for tag in tags_to_use
208
                    ),
209
                )
210
            ]
211
        )
212

213
    build_context_mock = _create_build_context_mock(
1✔
214
        rule_runner, address, build_context_snapshot, copy_sources, copy_build_args, version_tags
215
    )
216
    docker_options = _setup_docker_options(rule_runner, options)
1✔
217

218
    match binary:
1✔
219
        case BuildctlBinary():
1✔
220
            binary_rule_path = "pants.backend.docker.util_rules.binaries.get_buildctl"
1✔
221
        case PodmanBinary():
1✔
NEW
222
            binary_rule_path = "pants.backend.docker.util_rules.binaries.get_podman"
×
223
        case _:
1✔
224
            binary_rule_path = "pants.backend.docker.util_rules.binaries.get_docker"
1✔
225

226
    result = run_rule_with_mocks(
1✔
227
        get_docker_image_build_process,
228
        rule_args=[
229
            DockerPackageFieldSet.create(tgt),
230
            docker_options,
231
        ],
232
        mock_calls={
233
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
234
            "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt),
235
            "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs,
236
            binary_rule_path: lambda *args: binary,
237
        },
238
        union_membership=_create_union_membership(),
239
        show_warnings=False,
240
    )
241

242
    # Run optional assertions
243
    if build_process_assertions:
1✔
244
        build_process_assertions(result)
1✔
245

246
    return result
1✔
247

248

249
def assert_get_image_refs(
1✔
250
    rule_runner: RuleRunner,
251
    address: Address,
252
    *,
253
    options: dict | None = None,
254
    expected_refs: DockerImageRefs | None = None,
255
    version_tags: tuple[str, ...] = (),
256
    plugin_tags: tuple[str, ...] = (),
257
    copy_sources: tuple[str, ...] = (),
258
    copy_build_args=(),
259
    build_context_snapshot: Snapshot = EMPTY_SNAPSHOT,
260
    build_upstream_images: bool = True,
261
) -> DockerImageRefs:
262
    """Test helper for get_image_refs rule.
263

264
    Returns DockerImageRefs for validation. Optionally asserts against expected_refs.
265
    """
266
    tgt = rule_runner.get_target(address)
1✔
267

268
    build_context_mock = _create_build_context_mock(
1✔
269
        rule_runner, address, build_context_snapshot, copy_sources, copy_build_args, version_tags
270
    )
271
    docker_options = _setup_docker_options(rule_runner, options)
1✔
272
    union_membership = _create_union_membership()
1✔
273

274
    field_set = DockerPackageFieldSet.create(tgt)
1✔
275
    result = run_rule_with_mocks(
1✔
276
        get_image_refs,
277
        rule_args=[
278
            GetImageRefsRequest(
279
                field_set=field_set,
280
                build_upstream_images=build_upstream_images,
281
            ),
282
            docker_options,
283
            union_membership,
284
        ],
285
        mock_calls={
286
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
287
            "pants.engine.internals.graph.resolve_target": lambda *_, **__: WrappedTarget(tgt),
288
            "pants.backend.docker.target_types.get_docker_image_tags": lambda *_,
289
            **__: DockerImageTags(plugin_tags),
290
        },
291
        union_membership=union_membership,
292
    )
293

294
    if expected_refs is not None:
1✔
295
        assert result == expected_refs
1✔
296

297
    return result
1✔
298

299

300
def test_get_image_refs(rule_runner: RuleRunner) -> None:
1✔
301
    rule_runner.write_files(
1✔
302
        {
303
            "docker/test/BUILD": dedent(
304
                """\
305

306
                docker_image(
307
                  name="test1",
308
                  image_tags=["1.2.3"],
309
                  repository="{directory}/{name}",
310
                )
311
                docker_image(
312
                  name="test2",
313
                  image_tags=["1.2.3"],
314
                )
315
                docker_image(
316
                  name="test3",
317
                  image_tags=["1.2.3"],
318
                  repository="{parent_directory}/{directory}/{name}",
319
                )
320
                docker_image(
321
                  name="test4",
322
                  image_tags=["1.2.3"],
323
                  repository="{directory}/four/test-four",
324
                )
325
                docker_image(
326
                  name="test5",
327
                  image_tags=["latest", "alpha-1.0", "alpha-1"],
328
                )
329
                docker_image(
330
                  name="test6",
331
                  image_tags=["1.2.3"],
332
                  repository="xyz/{full_directory}/{name}",
333
                )
334
                docker_image(
335
                  name="err1",
336
                  repository="{bad_template}",
337
                )
338
                """
339
            ),
340
            "docker/test/Dockerfile": "FROM python:3.8",
341
        }
342
    )
343

344
    assert_get_image_refs(
1✔
345
        rule_runner,
346
        Address("docker/test", target_name="test1"),
347
        expected_refs=DockerImageRefs(
348
            [
349
                ImageRefRegistry(
350
                    registry=None,
351
                    repository="test/test1",
352
                    tags=(
353
                        ImageRefTag(
354
                            template="1.2.3",
355
                            formatted="1.2.3",
356
                            full_name="test/test1:1.2.3",
357
                            uses_local_alias=False,
358
                        ),
359
                    ),
360
                ),
361
            ]
362
        ),
363
    )
364
    assert_get_image_refs(
1✔
365
        rule_runner,
366
        Address("docker/test", target_name="test2"),
367
        expected_refs=DockerImageRefs(
368
            [
369
                ImageRefRegistry(
370
                    registry=None,
371
                    repository="test2",
372
                    tags=(
373
                        ImageRefTag(
374
                            template="1.2.3",
375
                            formatted="1.2.3",
376
                            full_name="test2:1.2.3",
377
                            uses_local_alias=False,
378
                        ),
379
                    ),
380
                ),
381
            ]
382
        ),
383
    )
384
    assert_get_image_refs(
1✔
385
        rule_runner,
386
        Address("docker/test", target_name="test3"),
387
        expected_refs=DockerImageRefs(
388
            [
389
                ImageRefRegistry(
390
                    registry=None,
391
                    repository="docker/test/test3",
392
                    tags=(
393
                        ImageRefTag(
394
                            template="1.2.3",
395
                            formatted="1.2.3",
396
                            full_name="docker/test/test3:1.2.3",
397
                            uses_local_alias=False,
398
                        ),
399
                    ),
400
                ),
401
            ]
402
        ),
403
    )
404

405
    assert_get_image_refs(
1✔
406
        rule_runner,
407
        Address("docker/test", target_name="test4"),
408
        expected_refs=DockerImageRefs(
409
            [
410
                ImageRefRegistry(
411
                    registry=None,
412
                    repository="test/four/test-four",
413
                    tags=(
414
                        ImageRefTag(
415
                            template="1.2.3",
416
                            formatted="1.2.3",
417
                            full_name="test/four/test-four:1.2.3",
418
                            uses_local_alias=False,
419
                        ),
420
                    ),
421
                ),
422
            ]
423
        ),
424
    )
425

426
    assert_get_image_refs(
1✔
427
        rule_runner,
428
        Address("docker/test", target_name="test5"),
429
        options=dict(default_repository="{directory}/{name}"),
430
        expected_refs=DockerImageRefs(
431
            [
432
                ImageRefRegistry(
433
                    registry=None,
434
                    repository="test/test5",
435
                    tags=(
436
                        ImageRefTag(
437
                            template="latest",
438
                            formatted="latest",
439
                            full_name="test/test5:latest",
440
                            uses_local_alias=False,
441
                        ),
442
                        ImageRefTag(
443
                            template="alpha-1.0",
444
                            formatted="alpha-1.0",
445
                            full_name="test/test5:alpha-1.0",
446
                            uses_local_alias=False,
447
                        ),
448
                        ImageRefTag(
449
                            template="alpha-1",
450
                            formatted="alpha-1",
451
                            full_name="test/test5:alpha-1",
452
                            uses_local_alias=False,
453
                        ),
454
                    ),
455
                ),
456
            ]
457
        ),
458
    )
459

460
    assert_get_image_refs(
1✔
461
        rule_runner,
462
        Address("docker/test", target_name="test6"),
463
        expected_refs=DockerImageRefs(
464
            [
465
                ImageRefRegistry(
466
                    registry=None,
467
                    repository="xyz/docker/test/test6",
468
                    tags=(
469
                        ImageRefTag(
470
                            template="1.2.3",
471
                            formatted="1.2.3",
472
                            full_name="xyz/docker/test/test6:1.2.3",
473
                            uses_local_alias=False,
474
                        ),
475
                    ),
476
                ),
477
            ]
478
        ),
479
    )
480

481
    err1 = (
1✔
482
        r"Invalid value for the `repository` field of the `docker_image` target at "
483
        r"docker/test:err1: '{bad_template}'\.\n\nThe placeholder 'bad_template' is unknown\. "
484
        r"Try with one of: build_args, default_repository, directory, full_directory, name, "
485
        r"pants, parent_directory, tags, target_repository\."
486
    )
487
    with pytest.raises(DockerRepositoryNameError, match=err1):
1✔
488
        assert_get_image_refs(
1✔
489
            rule_runner,
490
            Address("docker/test", target_name="err1"),
491
        )
492

493

494
def test_get_image_refs_with_registries(rule_runner: RuleRunner) -> None:
1✔
495
    rule_runner.write_files(
1✔
496
        {
497
            "docker/test/BUILD": dedent(
498
                """\
499
                docker_image(name="addr1", image_tags=["1.2.3"], registries=["myregistry1domain:port"])
500
                docker_image(name="addr2", image_tags=["1.2.3"], registries=["myregistry2domain:port"])
501
                docker_image(name="addr3", image_tags=["1.2.3"], registries=["myregistry3domain:port"])
502
                docker_image(name="alias1", image_tags=["1.2.3"], registries=["@reg1"])
503
                docker_image(name="alias2", image_tags=["1.2.3"], registries=["@reg2"])
504
                docker_image(name="alias3", image_tags=["1.2.3"], registries=["reg3"])
505
                docker_image(name="unreg", image_tags=["1.2.3"], registries=[])
506
                docker_image(name="def", image_tags=["1.2.3"])
507
                docker_image(name="multi", image_tags=["1.2.3"], registries=["@reg2", "@reg1"])
508
                docker_image(name="extra_tags", image_tags=["1.2.3"], registries=["@reg1", "@extra"])
509
                """
510
            ),
511
            "docker/test/Dockerfile": "FROM python:3.8",
512
        }
513
    )
514

515
    options = {
1✔
516
        "default_repository": "{name}",
517
        "registries": {
518
            "reg1": {"address": "myregistry1domain:port"},
519
            "reg2": {"address": "myregistry2domain:port", "default": True},
520
            "extra": {"address": "extra", "extra_image_tags": ["latest"]},
521
        },
522
    }
523

524
    assert_get_image_refs(
1✔
525
        rule_runner,
526
        Address("docker/test", target_name="addr1"),
527
        options=options,
528
        expected_refs=DockerImageRefs(
529
            [
530
                ImageRefRegistry(
531
                    registry=DockerRegistryOptions(address="myregistry1domain:port", alias="reg1"),
532
                    repository="addr1",
533
                    tags=(
534
                        ImageRefTag(
535
                            template="1.2.3",
536
                            formatted="1.2.3",
537
                            full_name="myregistry1domain:port/addr1:1.2.3",
538
                            uses_local_alias=False,
539
                        ),
540
                    ),
541
                ),
542
            ]
543
        ),
544
    )
545
    assert_get_image_refs(
1✔
546
        rule_runner,
547
        Address("docker/test", target_name="addr2"),
548
        options=options,
549
        expected_refs=DockerImageRefs(
550
            [
551
                ImageRefRegistry(
552
                    registry=DockerRegistryOptions(
553
                        address="myregistry2domain:port", alias="reg2", default=True
554
                    ),
555
                    repository="addr2",
556
                    tags=(
557
                        ImageRefTag(
558
                            template="1.2.3",
559
                            formatted="1.2.3",
560
                            full_name="myregistry2domain:port/addr2:1.2.3",
561
                            uses_local_alias=False,
562
                        ),
563
                    ),
564
                ),
565
            ]
566
        ),
567
    )
568

569
    assert_get_image_refs(
1✔
570
        rule_runner,
571
        Address("docker/test", target_name="addr3"),
572
        options=options,
573
        expected_refs=DockerImageRefs(
574
            [
575
                ImageRefRegistry(
576
                    registry=DockerRegistryOptions(address="myregistry3domain:port"),
577
                    repository="addr3",
578
                    tags=(
579
                        ImageRefTag(
580
                            template="1.2.3",
581
                            formatted="1.2.3",
582
                            full_name="myregistry3domain:port/addr3:1.2.3",
583
                            uses_local_alias=False,
584
                        ),
585
                    ),
586
                ),
587
            ]
588
        ),
589
    )
590

591
    assert_get_image_refs(
1✔
592
        rule_runner,
593
        Address("docker/test", target_name="alias1"),
594
        options=options,
595
        expected_refs=DockerImageRefs(
596
            [
597
                ImageRefRegistry(
598
                    registry=DockerRegistryOptions(alias="reg1", address="myregistry1domain:port"),
599
                    repository="alias1",
600
                    tags=(
601
                        ImageRefTag(
602
                            template="1.2.3",
603
                            formatted="1.2.3",
604
                            full_name="myregistry1domain:port/alias1:1.2.3",
605
                            uses_local_alias=False,
606
                        ),
607
                    ),
608
                ),
609
            ]
610
        ),
611
    )
612

613
    assert_get_image_refs(
1✔
614
        rule_runner,
615
        Address("docker/test", target_name="alias2"),
616
        options=options,
617
        expected_refs=DockerImageRefs(
618
            [
619
                ImageRefRegistry(
620
                    registry=DockerRegistryOptions(
621
                        address="myregistry2domain:port", alias="reg2", default=True
622
                    ),
623
                    repository="alias2",
624
                    tags=(
625
                        ImageRefTag(
626
                            template="1.2.3",
627
                            formatted="1.2.3",
628
                            full_name="myregistry2domain:port/alias2:1.2.3",
629
                            uses_local_alias=False,
630
                        ),
631
                    ),
632
                ),
633
            ]
634
        ),
635
    )
636

637
    assert_get_image_refs(
1✔
638
        rule_runner,
639
        Address("docker/test", target_name="alias3"),
640
        options=options,
641
        expected_refs=DockerImageRefs(
642
            [
643
                ImageRefRegistry(
644
                    registry=DockerRegistryOptions(address="reg3"),
645
                    repository="alias3",
646
                    tags=(
647
                        ImageRefTag(
648
                            template="1.2.3",
649
                            formatted="1.2.3",
650
                            full_name="reg3/alias3:1.2.3",
651
                            uses_local_alias=False,
652
                        ),
653
                    ),
654
                ),
655
            ]
656
        ),
657
    )
658

659
    assert_get_image_refs(
1✔
660
        rule_runner,
661
        Address("docker/test", target_name="unreg"),
662
        options=options,
663
        expected_refs=DockerImageRefs(
664
            [
665
                ImageRefRegistry(
666
                    registry=None,
667
                    repository="unreg",
668
                    tags=(
669
                        ImageRefTag(
670
                            template="1.2.3",
671
                            formatted="1.2.3",
672
                            full_name="unreg:1.2.3",
673
                            uses_local_alias=False,
674
                        ),
675
                    ),
676
                ),
677
            ]
678
        ),
679
    )
680

681
    assert_get_image_refs(
1✔
682
        rule_runner,
683
        Address("docker/test", target_name="def"),
684
        options=options,
685
        expected_refs=DockerImageRefs(
686
            [
687
                ImageRefRegistry(
688
                    registry=DockerRegistryOptions(
689
                        address="myregistry2domain:port", alias="reg2", default=True
690
                    ),
691
                    repository="def",
692
                    tags=(
693
                        ImageRefTag(
694
                            template="1.2.3",
695
                            formatted="1.2.3",
696
                            full_name="myregistry2domain:port/def:1.2.3",
697
                            uses_local_alias=False,
698
                        ),
699
                    ),
700
                ),
701
            ]
702
        ),
703
    )
704
    assert_get_image_refs(
1✔
705
        rule_runner,
706
        Address("docker/test", target_name="multi"),
707
        options=options,
708
        expected_refs=DockerImageRefs(
709
            [
710
                ImageRefRegistry(
711
                    registry=DockerRegistryOptions(
712
                        address="myregistry2domain:port", alias="reg2", default=True
713
                    ),
714
                    repository="multi",
715
                    tags=(
716
                        ImageRefTag(
717
                            template="1.2.3",
718
                            formatted="1.2.3",
719
                            full_name="myregistry2domain:port/multi:1.2.3",
720
                            uses_local_alias=False,
721
                        ),
722
                    ),
723
                ),
724
                ImageRefRegistry(
725
                    registry=DockerRegistryOptions(alias="reg1", address="myregistry1domain:port"),
726
                    repository="multi",
727
                    tags=(
728
                        ImageRefTag(
729
                            template="1.2.3",
730
                            formatted="1.2.3",
731
                            full_name="myregistry1domain:port/multi:1.2.3",
732
                            uses_local_alias=False,
733
                        ),
734
                    ),
735
                ),
736
            ]
737
        ),
738
    )
739

740
    assert_get_image_refs(
1✔
741
        rule_runner,
742
        Address("docker/test", target_name="extra_tags"),
743
        options=options,
744
        expected_refs=DockerImageRefs(
745
            [
746
                ImageRefRegistry(
747
                    registry=DockerRegistryOptions(address="myregistry1domain:port", alias="reg1"),
748
                    repository="extra_tags",
749
                    tags=(
750
                        ImageRefTag(
751
                            template="1.2.3",
752
                            formatted="1.2.3",
753
                            full_name="myregistry1domain:port/extra_tags:1.2.3",
754
                            uses_local_alias=False,
755
                        ),
756
                    ),
757
                ),
758
                ImageRefRegistry(
759
                    registry=DockerRegistryOptions(
760
                        alias="extra", address="extra", extra_image_tags=("latest",)
761
                    ),
762
                    repository="extra_tags",
763
                    tags=(
764
                        ImageRefTag(
765
                            template="1.2.3",
766
                            formatted="1.2.3",
767
                            full_name="extra/extra_tags:1.2.3",
768
                            uses_local_alias=False,
769
                        ),
770
                        ImageRefTag(
771
                            template="latest",
772
                            formatted="latest",
773
                            full_name="extra/extra_tags:latest",
774
                            uses_local_alias=False,
775
                        ),
776
                    ),
777
                ),
778
            ]
779
        ),
780
    )
781

782

783
def test_dynamic_image_version(rule_runner: RuleRunner) -> None:
1✔
784
    interpolation_context = InterpolationContext.from_dict(
1✔
785
        {
786
            "baseimage": {"tag": "3.8"},
787
            "stage0": {"tag": "3.8"},
788
            "interim": {"tag": "latest"},
789
            "stage2": {"tag": "latest"},
790
            "output": {"tag": "1-1"},
791
        }
792
    )
793

794
    def assert_tags(name: str, *expect_tags: str) -> None:
1✔
795
        tgt = rule_runner.get_target(Address("docker/test", target_name=name))
1✔
796
        fs = DockerPackageFieldSet.create(tgt)
1✔
797
        image_refs = fs.image_refs(
1✔
798
            "image",
799
            DockerRegistries.from_dict({}),
800
            interpolation_context,
801
        )
802
        tags = tuple(t.full_name for r in image_refs for t in r.tags)
1✔
803
        assert expect_tags == tags
1✔
804

805
    rule_runner.write_files(
1✔
806
        {
807
            "docker/test/BUILD": dedent(
808
                """\
809
                docker_image(name="ver_1")
810
                docker_image(
811
                  name="ver_2",
812
                  image_tags=["{baseimage.tag}-{stage2.tag}", "beta"]
813
                )
814
                docker_image(name="err_1", image_tags=["{unknown_stage}"])
815
                docker_image(name="err_2", image_tags=["{stage0.unknown_value}"])
816
                """
817
            ),
818
        }
819
    )
820

821
    assert_tags("ver_1", "image:latest")
1✔
822
    assert_tags("ver_2", "image:3.8-latest", "image:beta")
1✔
823

824
    err_1 = (
1✔
825
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
826
        r"docker/test:err_1: '{unknown_stage}'\.\n\n"
827
        r"The placeholder 'unknown_stage' is unknown\. Try with one of: baseimage, interim, "
828
        r"output, stage0, stage2\."
829
    )
830
    with pytest.raises(DockerImageTagValueError, match=err_1):
1✔
831
        assert_tags("err_1")
1✔
832

833
    err_2 = (
1✔
834
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
835
        r"docker/test:err_2: '{stage0.unknown_value}'\.\n\n"
836
        r"The placeholder 'unknown_value' is unknown\. Try with one of: tag\."
837
    )
838
    with pytest.raises(DockerImageTagValueError, match=err_2):
1✔
839
        assert_tags("err_2")
1✔
840

841

842
def test_docker_build_process_environment(rule_runner: RuleRunner) -> None:
1✔
843
    rule_runner.write_files(
1✔
844
        {"docker/test/BUILD": 'docker_image(name="env1", image_tags=["1.2.3"])'}
845
    )
846
    rule_runner.set_options(
1✔
847
        [],
848
        env={
849
            "INHERIT": "from Pants env",
850
            "PANTS_DOCKER_ENV_VARS": '["VAR=value", "INHERIT"]',
851
        },
852
    )
853

854
    def check_build_process(result: DockerImageBuildProcess):
1✔
855
        assert result.process.argv == (
1✔
856
            "/dummy/docker",
857
            "build",
858
            "--pull=False",
859
            "--tag",
860
            "env1:1.2.3",
861
            "--file",
862
            "docker/test/Dockerfile",
863
            ".",
864
        )
865
        assert result.process.env == FrozenDict(
1✔
866
            {
867
                "INHERIT": "from Pants env",
868
                "VAR": "value",
869
                "__UPSTREAM_IMAGE_IDS": "",
870
            }
871
        )
872

873
    assert_build_process(
1✔
874
        rule_runner,
875
        Address("docker/test", target_name="env1"),
876
        build_process_assertions=check_build_process,
877
    )
878

879

880
def test_build_docker_image(rule_runner: RuleRunner) -> None:
1✔
881
    """Test build_docker_image rule orchestration and metadata creation."""
882
    rule_runner.write_files(
1✔
883
        {"docker/test/BUILD": 'docker_image(name="img1", image_tags=["1.2.3"])'}
884
    )
885

886
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
1✔
887
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
888
    metadata_file_path: list[str] = []
1✔
889
    metadata_file_contents: list[bytes] = []
1✔
890

891
    # Create mock DockerImageBuildProcess
892
    image_refs = DockerImageRefs(
1✔
893
        [
894
            ImageRefRegistry(
895
                registry=None,
896
                repository="img1",
897
                tags=(
898
                    ImageRefTag(
899
                        template="1.2.3",
900
                        formatted="1.2.3",
901
                        full_name="img1:1.2.3",
902
                        uses_local_alias=False,
903
                    ),
904
                ),
905
            )
906
        ]
907
    )
908

909
    process = Process(
1✔
910
        argv=(
911
            "/dummy/docker",
912
            "build",
913
            "--tag",
914
            "img1:1.2.3",
915
            "--pull=False",
916
            "--file",
917
            "docker/test/Dockerfile",
918
            ".",
919
        ),
920
        description="docker build",
921
        input_digest=EMPTY_DIGEST,
922
    )
923

924
    build_context = DockerBuildContext.create(
1✔
925
        snapshot=EMPTY_SNAPSHOT,
926
        upstream_image_ids=[],
927
        dockerfile_info=DockerfileInfo(
928
            tgt.address,
929
            digest=EMPTY_DIGEST,
930
            source="docker/test/Dockerfile",
931
        ),
932
        build_args=DockerBuildArgs(()),
933
        build_env=DockerBuildEnvironment.create({}),
934
    )
935

936
    mock_build_process = DockerImageBuildProcess(
1✔
937
        process=process,
938
        context=build_context,
939
        context_root=".",
940
        image_refs=image_refs,
941
        tags=("img1:1.2.3",),
942
    )
943

944
    # Mock get_docker_image_build_process to return our mock
945
    def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
1✔
946
        assert field_set == under_test_fs
1✔
947
        return mock_build_process
1✔
948

949
    # Mock execute_process to return success with image ID
950
    def mock_execute_process(_process: Process) -> FallibleProcessResult:
1✔
951
        return FallibleProcessResult(
1✔
952
            exit_code=0,
953
            stdout=b"Successfully built abc123\n",
954
            stderr=b"",
955
            stdout_digest=EMPTY_FILE_DIGEST,
956
            stderr_digest=EMPTY_FILE_DIGEST,
957
            output_digest=EMPTY_DIGEST,
958
            metadata=ProcessResultMetadata(
959
                0,
960
                ProcessExecutionEnvironment(
961
                    environment_name=None,
962
                    platform=Platform.create_for_localhost().value,
963
                    docker_image=None,
964
                    remote_execution=False,
965
                    remote_execution_extra_platform_properties=[],
966
                    execute_in_workspace=False,
967
                    keep_sandboxes="never",
968
                ),
969
                "ran_locally",
970
                0,
971
            ),
972
        )
973

974
    # Mock create_digest to capture metadata
975
    def mock_create_digest(request: CreateDigest) -> Digest:
1✔
976
        assert len(request) == 1
1✔
977
        assert isinstance(request[0], FileContent)
1✔
978
        metadata_file_path.append(request[0].path)
1✔
979
        metadata_file_contents.append(request[0].content)
1✔
980
        return EMPTY_DIGEST
1✔
981

982
    docker_options = _setup_docker_options(rule_runner, None)
1✔
983
    global_options = rule_runner.request(GlobalOptions, [])
1✔
984

985
    # Execute the rule
986
    result = run_rule_with_mocks(
1✔
987
        build_docker_image,
988
        rule_args=[
989
            under_test_fs,
990
            docker_options,
991
            global_options,
992
            KeepSandboxes.never,
993
        ],
994
        mock_calls={
995
            "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
996
            "pants.engine.intrinsics.execute_process": mock_execute_process,
997
            "pants.engine.intrinsics.create_digest": mock_create_digest,
998
        },
999
        show_warnings=False,
1000
    )
1001

1002
    # Validate BuiltPackage result
1003
    assert result.digest == EMPTY_DIGEST
1✔
1004
    assert len(result.artifacts) == 1
1✔
1005
    assert len(metadata_file_path) == 1
1✔
1006
    assert result.artifacts[0].relpath == metadata_file_path[0]
1✔
1007

1008
    # Validate metadata file content
1009
    metadata = json.loads(metadata_file_contents[0])
1✔
1010
    assert metadata["version"] == 1
1✔
1011
    assert metadata["image_id"] == "abc123"
1✔
1012
    assert isinstance(metadata["registries"], list)
1✔
1013
    assert len(metadata["registries"]) == 1
1✔
1014
    assert metadata["registries"][0]["repository"] == "img1"
1✔
1015
    assert metadata["registries"][0]["tags"][0]["tag"] == "1.2.3"
1✔
1016

1017

1018
def test_docker_build_pull(rule_runner: RuleRunner) -> None:
1✔
1019
    rule_runner.write_files({"docker/test/BUILD": 'docker_image(name="args1", pull=True)'})
1✔
1020

1021
    def check_build_process(result: DockerImageBuildProcess):
1✔
1022
        assert result.process.argv == (
1✔
1023
            "/dummy/docker",
1024
            "build",
1025
            "--pull=True",
1026
            "--tag",
1027
            "args1:latest",
1028
            "--file",
1029
            "docker/test/Dockerfile",
1030
            ".",
1031
        )
1032

1033
    assert_build_process(
1✔
1034
        rule_runner,
1035
        Address("docker/test", target_name="args1"),
1036
        build_process_assertions=check_build_process,
1037
    )
1038

1039

1040
def test_docker_build_squash(rule_runner: RuleRunner) -> None:
1✔
1041
    rule_runner.write_files(
1✔
1042
        {
1043
            "docker/test/BUILD": dedent(
1044
                """\
1045
            docker_image(name="args1", squash=True)
1046
            docker_image(name="args2", squash=False)
1047
            """
1048
            )
1049
        }
1050
    )
1051

1052
    def check_build_process(result: DockerImageBuildProcess):
1✔
1053
        assert result.process.argv == (
1✔
1054
            "/dummy/docker",
1055
            "build",
1056
            "--pull=False",
1057
            "--squash",
1058
            "--tag",
1059
            "args1:latest",
1060
            "--file",
1061
            "docker/test/Dockerfile",
1062
            ".",
1063
        )
1064

1065
    def check_build_process_no_squash(result: DockerImageBuildProcess):
1✔
1066
        assert result.process.argv == (
1✔
1067
            "/dummy/docker",
1068
            "build",
1069
            "--pull=False",
1070
            "--tag",
1071
            "args2:latest",
1072
            "--file",
1073
            "docker/test/Dockerfile",
1074
            ".",
1075
        )
1076

1077
    assert_build_process(
1✔
1078
        rule_runner,
1079
        Address("docker/test", target_name="args1"),
1080
        build_process_assertions=check_build_process,
1081
    )
1082
    assert_build_process(
1✔
1083
        rule_runner,
1084
        Address("docker/test", target_name="args2"),
1085
        build_process_assertions=check_build_process_no_squash,
1086
    )
1087

1088

1089
def test_docker_build_args(rule_runner: RuleRunner) -> None:
1✔
1090
    rule_runner.write_files(
1✔
1091
        {"docker/test/BUILD": 'docker_image(name="args1", image_tags=["1.2.3"])'}
1092
    )
1093
    rule_runner.set_options(
1✔
1094
        [],
1095
        env={
1096
            "INHERIT": "from Pants env",
1097
            "PANTS_DOCKER_BUILD_ARGS": '["VAR=value", "INHERIT"]',
1098
        },
1099
    )
1100

1101
    def check_build_process(result: DockerImageBuildProcess):
1✔
1102
        assert result.process.argv == (
1✔
1103
            "/dummy/docker",
1104
            "build",
1105
            "--pull=False",
1106
            "--tag",
1107
            "args1:1.2.3",
1108
            "--build-arg",
1109
            "INHERIT",
1110
            "--build-arg",
1111
            "VAR=value",
1112
            "--file",
1113
            "docker/test/Dockerfile",
1114
            ".",
1115
        )
1116

1117
        # Check that we pull in name only args via env.
1118
        assert result.process.env == FrozenDict(
1✔
1119
            {
1120
                "INHERIT": "from Pants env",
1121
                "__UPSTREAM_IMAGE_IDS": "",
1122
            }
1123
        )
1124

1125
    assert_build_process(
1✔
1126
        rule_runner,
1127
        Address("docker/test", target_name="args1"),
1128
        build_process_assertions=check_build_process,
1129
    )
1130

1131

1132
def test_docker_image_version_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
1133
    rule_runner.write_files(
1✔
1134
        {"docker/test/BUILD": 'docker_image(name="ver1", image_tags=["{build_args.VERSION}"])'}
1135
    )
1136
    rule_runner.set_options(
1✔
1137
        [],
1138
        env={
1139
            "PANTS_DOCKER_BUILD_ARGS": '["VERSION=1.2.3"]',
1140
        },
1141
    )
1142

1143
    refs = assert_get_image_refs(
1✔
1144
        rule_runner,
1145
        Address("docker/test", target_name="ver1"),
1146
    )
1147
    assert len(refs) == 1
1✔
1148
    assert refs[0].registry is None
1✔
1149
    assert refs[0].repository == "ver1"
1✔
1150
    assert len(refs[0].tags) == 1
1✔
1151
    assert refs[0].tags[0].template == "{build_args.VERSION}"
1✔
1152
    assert refs[0].tags[0].formatted == "1.2.3"
1✔
1153
    assert refs[0].tags[0].full_name == "ver1:1.2.3"
1✔
1154

1155

1156
def test_docker_repository_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
1157
    rule_runner.write_files(
1✔
1158
        {"docker/test/BUILD": 'docker_image(name="image", repository="{build_args.REPO}")'}
1159
    )
1160
    rule_runner.set_options(
1✔
1161
        [],
1162
        env={
1163
            "PANTS_DOCKER_BUILD_ARGS": '["REPO=test/image"]',
1164
        },
1165
    )
1166

1167
    refs = assert_get_image_refs(
1✔
1168
        rule_runner,
1169
        Address("docker/test", target_name="image"),
1170
    )
1171
    assert refs[0].repository == "test/image"
1✔
1172
    assert refs[0].tags[0].formatted == "latest"
1✔
1173
    assert refs[0].tags[0].full_name == "test/image:latest"
1✔
1174

1175

1176
def test_docker_extra_build_args_field(rule_runner: RuleRunner) -> None:
1✔
1177
    rule_runner.write_files(
1✔
1178
        {
1179
            "docker/test/BUILD": dedent(
1180
                """\
1181
                docker_image(
1182
                  name="img1",
1183
                  extra_build_args=[
1184
                    "FROM_ENV",
1185
                    "SET=value",
1186
                    "DEFAULT2=overridden",
1187
                  ]
1188
                )
1189
                """
1190
            ),
1191
        }
1192
    )
1193
    rule_runner.set_options(
1✔
1194
        [
1195
            "--docker-build-args=DEFAULT1=global1",
1196
            "--docker-build-args=DEFAULT2=global2",
1197
        ],
1198
        env={
1199
            "FROM_ENV": "env value",
1200
            "SET": "no care",
1201
        },
1202
    )
1203

1204
    def check_build_process(result: DockerImageBuildProcess):
1✔
1205
        assert result.process.argv == (
1✔
1206
            "/dummy/docker",
1207
            "build",
1208
            "--pull=False",
1209
            "--tag",
1210
            "img1:latest",
1211
            "--build-arg",
1212
            "DEFAULT1=global1",
1213
            "--build-arg",
1214
            "DEFAULT2=overridden",
1215
            "--build-arg",
1216
            "FROM_ENV",
1217
            "--build-arg",
1218
            "SET=value",
1219
            "--file",
1220
            "docker/test/Dockerfile",
1221
            ".",
1222
        )
1223

1224
        assert result.process.env == FrozenDict(
1✔
1225
            {
1226
                "FROM_ENV": "env value",
1227
                "__UPSTREAM_IMAGE_IDS": "",
1228
            }
1229
        )
1230

1231
    assert_build_process(
1✔
1232
        rule_runner,
1233
        Address("docker/test", target_name="img1"),
1234
        build_process_assertions=check_build_process,
1235
    )
1236

1237

1238
def test_docker_build_secrets_option(rule_runner: RuleRunner) -> None:
1✔
1239
    rule_runner.write_files(
1✔
1240
        {
1241
            "docker/test/BUILD": dedent(
1242
                """\
1243
                docker_image(
1244
                  name="img1",
1245
                  secrets={
1246
                    "system-secret": "/var/run/secrets/mysecret",
1247
                    "project-secret": "secrets/mysecret",
1248
                    "target-secret": "./mysecret",
1249
                  }
1250
                )
1251
                """
1252
            ),
1253
        }
1254
    )
1255

1256
    def check_build_process(result: DockerImageBuildProcess):
1✔
1257
        assert result.process.argv == (
1✔
1258
            "/dummy/docker",
1259
            "build",
1260
            "--pull=False",
1261
            "--secret",
1262
            "id=system-secret,src=/var/run/secrets/mysecret",
1263
            "--secret",
1264
            f"id=project-secret,src={rule_runner.build_root}/secrets/mysecret",
1265
            "--secret",
1266
            f"id=target-secret,src={rule_runner.build_root}/docker/test/mysecret",
1267
            "--tag",
1268
            "img1:latest",
1269
            "--file",
1270
            "docker/test/Dockerfile",
1271
            ".",
1272
        )
1273

1274
    assert_build_process(
1✔
1275
        rule_runner,
1276
        Address("docker/test", target_name="img1"),
1277
        build_process_assertions=check_build_process,
1278
    )
1279

1280

1281
def test_docker_build_ssh_option(rule_runner: RuleRunner) -> None:
1✔
1282
    rule_runner.write_files(
1✔
1283
        {
1284
            "docker/test/BUILD": dedent(
1285
                """\
1286
                docker_image(
1287
                  name="img1",
1288
                  ssh=["default"],
1289
                )
1290
                """
1291
            ),
1292
        }
1293
    )
1294

1295
    def check_build_process(result: DockerImageBuildProcess):
1✔
1296
        assert result.process.argv == (
1✔
1297
            "/dummy/docker",
1298
            "build",
1299
            "--pull=False",
1300
            "--ssh",
1301
            "default",
1302
            "--tag",
1303
            "img1:latest",
1304
            "--file",
1305
            "docker/test/Dockerfile",
1306
            ".",
1307
        )
1308

1309
    assert_build_process(
1✔
1310
        rule_runner,
1311
        Address("docker/test", target_name="img1"),
1312
        build_process_assertions=check_build_process,
1313
    )
1314

1315

1316
def test_docker_build_no_cache_option(rule_runner: RuleRunner) -> None:
1✔
1317
    rule_runner.set_options(
1✔
1318
        [],
1319
        env={
1320
            "PANTS_DOCKER_BUILD_NO_CACHE": "true",
1321
        },
1322
    )
1323
    rule_runner.write_files(
1✔
1324
        {
1325
            "docker/test/BUILD": dedent(
1326
                """\
1327
                docker_image(
1328
                  name="img1",
1329
                )
1330
                """
1331
            ),
1332
        }
1333
    )
1334

1335
    def check_build_process(result: DockerImageBuildProcess):
1✔
1336
        assert result.process.argv == (
1✔
1337
            "/dummy/docker",
1338
            "build",
1339
            "--pull=False",
1340
            "--no-cache",
1341
            "--tag",
1342
            "img1:latest",
1343
            "--file",
1344
            "docker/test/Dockerfile",
1345
            ".",
1346
        )
1347

1348
    assert_build_process(
1✔
1349
        rule_runner,
1350
        Address("docker/test", target_name="img1"),
1351
        build_process_assertions=check_build_process,
1352
    )
1353

1354

1355
def test_docker_build_hosts_option(rule_runner: RuleRunner) -> None:
1✔
1356
    rule_runner.set_options(
1✔
1357
        [],
1358
        env={
1359
            "PANTS_DOCKER_BUILD_HOSTS": '{"global": "9.9.9.9"}',
1360
        },
1361
    )
1362
    rule_runner.write_files(
1✔
1363
        {
1364
            "docker/test/BUILD": dedent(
1365
                """\
1366
                docker_image(
1367
                  name="img1",
1368
                  extra_build_hosts={"docker": "10.180.0.1", "docker2": "10.180.0.2"},
1369
                )
1370
                """
1371
            ),
1372
        }
1373
    )
1374

1375
    def check_build_process(result: DockerImageBuildProcess):
1✔
1376
        assert result.process.argv == (
1✔
1377
            "/dummy/docker",
1378
            "build",
1379
            "--add-host",
1380
            "global:9.9.9.9",
1381
            "--add-host",
1382
            "docker:10.180.0.1",
1383
            "--add-host",
1384
            "docker2:10.180.0.2",
1385
            "--pull=False",
1386
            "--tag",
1387
            "img1:latest",
1388
            "--file",
1389
            "docker/test/Dockerfile",
1390
            ".",
1391
        )
1392

1393
    assert_build_process(
1✔
1394
        rule_runner,
1395
        Address("docker/test", target_name="img1"),
1396
        build_process_assertions=check_build_process,
1397
    )
1398

1399

1400
def test_docker_cache_to_option(rule_runner: RuleRunner) -> None:
1✔
1401
    rule_runner.write_files(
1✔
1402
        {
1403
            "docker/test/BUILD": dedent(
1404
                """\
1405
                docker_image(
1406
                  name="img1",
1407
                  cache_to={"type": "local", "dest": "/tmp/docker/pants-test-cache"},
1408
                )
1409
                """
1410
            ),
1411
        }
1412
    )
1413

1414
    def check_build_process(result: DockerImageBuildProcess):
1✔
1415
        assert result.process.argv == (
1✔
1416
            "/dummy/docker",
1417
            "build",
1418
            "--cache-to=type=local,dest=/tmp/docker/pants-test-cache",
1419
            "--pull=False",
1420
            "--tag",
1421
            "img1:latest",
1422
            "--file",
1423
            "docker/test/Dockerfile",
1424
            ".",
1425
        )
1426

1427
    assert_build_process(
1✔
1428
        rule_runner,
1429
        Address("docker/test", target_name="img1"),
1430
        build_process_assertions=check_build_process,
1431
        options=dict(use_buildx=True),
1432
    )
1433

1434

1435
def test_docker_cache_from_option(rule_runner: RuleRunner) -> None:
1✔
1436
    rule_runner.write_files(
1✔
1437
        {
1438
            "docker/test/BUILD": dedent(
1439
                """\
1440
                docker_image(
1441
                  name="img1",
1442
                  cache_from=[{"type": "local", "dest": "/tmp/docker/pants-test-cache1"}, {"type": "local", "dest": "/tmp/docker/pants-test-cache2"}],
1443
                )
1444
                """
1445
            ),
1446
        }
1447
    )
1448

1449
    def check_build_process(result: DockerImageBuildProcess):
1✔
1450
        assert result.process.argv == (
1✔
1451
            "/dummy/docker",
1452
            "build",
1453
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache1",
1454
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache2",
1455
            "--pull=False",
1456
            "--tag",
1457
            "img1:latest",
1458
            "--file",
1459
            "docker/test/Dockerfile",
1460
            ".",
1461
        )
1462

1463
    assert_build_process(
1✔
1464
        rule_runner,
1465
        Address("docker/test", target_name="img1"),
1466
        build_process_assertions=check_build_process,
1467
        options=dict(use_buildx=True),
1468
    )
1469

1470

1471
@pytest.mark.parametrize(
1✔
1472
    ["output", "expected_output_arg"],
1473
    [
1474
        (None, None),
1475
        ({"type": "registry"}, "type=registry"),
1476
        ({"type": "image", "push": "true"}, "type=image,push=true"),
1477
    ],
1478
)
1479
def test_docker_output_option(
1✔
1480
    rule_runner: RuleRunner, output: dict | None, expected_output_arg: str | None
1481
) -> None:
1482
    """Testing non-default output type 'image'.
1483

1484
    Default output type 'docker' tested implicitly in other scenarios
1485
    """
1486
    output_str = f"output={repr(output)}," if output else ""
1✔
1487
    rule_runner.write_files(
1✔
1488
        {
1489
            "docker/test/BUILD": dedent(
1490
                f"""\
1491
                docker_image(
1492
                  name="img1",
1493
                  {output_str}
1494
                )
1495
                """
1496
            ),
1497
        }
1498
    )
1499
    output_args = ("--output", expected_output_arg) if expected_output_arg else ()
1✔
1500
    expected_argv = (
1✔
1501
        "/dummy/docker",
1502
        "build",
1503
        "--pull=False",
1504
        *output_args,
1505
        "--tag",
1506
        "img1:latest",
1507
        "--file",
1508
        "docker/test/Dockerfile",
1509
        ".",
1510
    )
1511

1512
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
1513
        assert result.process.argv == expected_argv
1✔
1514

1515
    assert_build_process(
1✔
1516
        rule_runner,
1517
        Address("docker/test", target_name="img1"),
1518
        build_process_assertions=check_build_process,
1519
        options=dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.ALLOW),
1520
    )
1521

1522

1523
@pytest.mark.parametrize(
1✔
1524
    ["output", "expect_error", "expected_output_arg"],
1525
    [
1526
        ({"type": "docker"}, False, "type=docker"),
1527
        ({"type": "registry"}, True, None),
1528
        ({"type": "image", "push": "true"}, True, None),
1529
    ],
1530
)
1531
def test_docker_output_option_when_push_on_package_error(
1✔
1532
    rule_runner: RuleRunner,
1533
    output: dict | None,
1534
    expect_error: bool,
1535
    expected_output_arg: str | None,
1536
) -> None:
1537
    output_str = f"output={repr(output)}," if output else ""
1✔
1538
    rule_runner.write_files(
1✔
1539
        {
1540
            "docker/test/BUILD": dedent(
1541
                f"""\
1542
                docker_image(
1543
                  name="img1",
1544
                  {output_str}
1545
                )
1546
                """
1547
            ),
1548
        }
1549
    )
1550

1551
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
1✔
1552
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
1553

1554
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
1555
        assert result.process.argv == (
1✔
1556
            "/dummy/docker",
1557
            "build",
1558
            "--pull=False",
1559
            "--output",
1560
            expected_output_arg,
1561
            "--tag",
1562
            "img1:latest",
1563
            "--file",
1564
            "docker/test/Dockerfile",
1565
            ".",
1566
        )
1567

1568
    build_process = assert_build_process(
1✔
1569
        rule_runner,
1570
        Address("docker/test", target_name="img1"),
1571
        build_process_assertions=check_build_process if expected_output_arg else None,
1572
        options=dict(use_buildx=True),
1573
    )
1574

1575
    def mock_execute_process(process: Process) -> FallibleProcessResult:
1✔
1576
        assert process == build_process.process
1✔
1577
        return FallibleProcessResult(
1✔
1578
            exit_code=0,
1579
            stdout=b"Successfully built abc123",
1580
            stderr=b"",
1581
            stdout_digest=EMPTY_FILE_DIGEST,
1582
            stderr_digest=EMPTY_FILE_DIGEST,
1583
            output_digest=EMPTY_DIGEST,
1584
            metadata=ProcessResultMetadata(
1585
                0,
1586
                ProcessExecutionEnvironment(
1587
                    environment_name=None,
1588
                    platform=Platform.create_for_localhost().value,
1589
                    docker_image=None,
1590
                    remote_execution=False,
1591
                    remote_execution_extra_platform_properties=[],
1592
                    execute_in_workspace=False,
1593
                    keep_sandboxes="never",
1594
                ),
1595
                "ran_locally",
1596
                0,
1597
            ),
1598
        )
1599

1600
    def mock_create_digest(request: CreateDigest) -> Digest:
1✔
1601
        return EMPTY_DIGEST
1✔
1602

1603
    def mock_get_build_process_success(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
1✔
1604
        assert field_set == under_test_fs
1✔
1605
        return build_process
1✔
1606

1607
    docker_options = _setup_docker_options(
1✔
1608
        rule_runner, dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.ERROR)
1609
    )
1610
    global_options = rule_runner.request(GlobalOptions, [])
1✔
1611

1612
    try:
1✔
1613
        run_rule_with_mocks(
1✔
1614
            build_docker_image,
1615
            rule_args=[
1616
                under_test_fs,
1617
                docker_options,
1618
                global_options,
1619
                KeepSandboxes.never,
1620
            ],
1621
            mock_calls={
1622
                "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process_success,
1623
                "pants.engine.intrinsics.execute_process": mock_execute_process,
1624
                "pants.engine.intrinsics.create_digest": mock_create_digest,
1625
            },
1626
            show_warnings=False,
1627
        )
1628
    except DockerPushOnPackageException:
1✔
1629
        assert expect_error
1✔
1630
    else:
1631
        assert not expect_error
1✔
1632

1633

1634
@pytest.mark.parametrize(
1✔
1635
    ["output", "expected_output_arg", "expected_message"],
1636
    [
1637
        (None, None, None),
1638
        (
1639
            {"type": "registry"},
1640
            "type=registry",
1641
            "Docker image docker/test:img1 will push to a registry during packaging",
1642
        ),
1643
        (
1644
            {"type": "image", "push": "true"},
1645
            "type=image,push=true",
1646
            "Docker image docker/test:img1 will push to a registry during packaging",
1647
        ),
1648
    ],
1649
)
1650
def test_docker_output_option_when_push_on_package_warn(
1✔
1651
    rule_runner: RuleRunner,
1652
    caplog: pytest.LogCaptureFixture,
1653
    output: dict | None,
1654
    expected_output_arg: str,
1655
    expected_message: str | None,
1656
) -> None:
1657
    output_str = f"output={repr(output)}," if output else ""
1✔
1658
    rule_runner.write_files(
1✔
1659
        {
1660
            "docker/test/BUILD": dedent(
1661
                f"""\
1662
                docker_image(
1663
                  name="img1",
1664
                  {output_str}
1665
                )
1666
                """
1667
            ),
1668
        }
1669
    )
1670

1671
    # Step 1: Validate Process construction using assert_build_process
1672
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
1673
        output_args = ("--output", expected_output_arg) if expected_output_arg else ()
1✔
1674
        assert result.process.argv == (
1✔
1675
            "/dummy/docker",
1676
            "build",
1677
            "--pull=False",
1678
            *output_args,
1679
            "--tag",
1680
            "img1:latest",
1681
            "--file",
1682
            "docker/test/Dockerfile",
1683
            ".",
1684
        )
1685

1686
    build_process = assert_build_process(
1✔
1687
        rule_runner,
1688
        Address("docker/test", target_name="img1"),
1689
        build_process_assertions=check_build_process,
1690
        options=dict(use_buildx=True),
1691
    )
1692

1693
    # Step 2: Test build_docker_image with WARN behavior
1694
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
1✔
1695
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
1696

1697
    def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
1✔
1698
        assert field_set == under_test_fs
1✔
1699
        return build_process
1✔
1700

1701
    def mock_execute_process(_process: Process) -> FallibleProcessResult:
1✔
1702
        return FallibleProcessResult(
1✔
1703
            exit_code=0,
1704
            stdout=b"Successfully built abc123",
1705
            stderr=b"",
1706
            stdout_digest=EMPTY_FILE_DIGEST,
1707
            stderr_digest=EMPTY_FILE_DIGEST,
1708
            output_digest=EMPTY_DIGEST,
1709
            metadata=ProcessResultMetadata(
1710
                0,
1711
                ProcessExecutionEnvironment(
1712
                    environment_name=None,
1713
                    platform=Platform.create_for_localhost().value,
1714
                    docker_image=None,
1715
                    remote_execution=False,
1716
                    remote_execution_extra_platform_properties=[],
1717
                    execute_in_workspace=False,
1718
                    keep_sandboxes="never",
1719
                ),
1720
                "ran_locally",
1721
                0,
1722
            ),
1723
        )
1724

1725
    def mock_create_digest(_request: CreateDigest) -> Digest:
1✔
1726
        return EMPTY_DIGEST
1✔
1727

1728
    docker_options = _setup_docker_options(
1✔
1729
        rule_runner, dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.WARN)
1730
    )
1731
    global_options = rule_runner.request(GlobalOptions, [])
1✔
1732

1733
    caplog.set_level(logging.WARNING)
1✔
1734

1735
    run_rule_with_mocks(
1✔
1736
        build_docker_image,
1737
        rule_args=[
1738
            under_test_fs,
1739
            docker_options,
1740
            global_options,
1741
            KeepSandboxes.never,
1742
        ],
1743
        mock_calls={
1744
            "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
1745
            "pants.engine.intrinsics.execute_process": mock_execute_process,
1746
            "pants.engine.intrinsics.create_digest": mock_create_digest,
1747
        },
1748
        show_warnings=False,
1749
    )
1750

1751
    # Validate warning was logged
1752
    has_message = expected_message in [
1✔
1753
        record.message for record in caplog.records if record.levelno == logging.WARNING
1754
    ]
1755
    assert has_message is (expected_message is not None)
1✔
1756

1757

1758
@pytest.mark.parametrize(
1✔
1759
    ["output", "expected_output_args"],
1760
    [
1761
        (None, None),
1762
        ({"type": "docker"}, ["--output", "type=docker"]),
1763
        ({"type": "registry"}, None),
1764
        ({"type": "image", "push": "true"}, None),
1765
    ],
1766
)
1767
def test_docker_output_option_when_push_on_package_ignore(
1✔
1768
    rule_runner: RuleRunner, output: dict | None, expected_output_args: list[str] | None
1769
) -> None:
1770
    output_str = f"output={repr(output)}," if output else ""
1✔
1771
    rule_runner.write_files(
1✔
1772
        {
1773
            "docker/test/BUILD": dedent(
1774
                f"""\
1775
                docker_image(
1776
                  name="img1",
1777
                  {output_str}
1778
                )
1779
                """
1780
            ),
1781
        }
1782
    )
1783
    expected_output_args = expected_output_args or []
1✔
1784
    docker_options = _setup_docker_options(
1✔
1785
        rule_runner, dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.IGNORE)
1786
    )
1787
    global_options = rule_runner.request(GlobalOptions, [])
1✔
1788
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
1✔
1789
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
1790

1791
    if expected_output_args or output is None:
1✔
1792

1793
        def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
1794
            assert result.process.argv == (
1✔
1795
                "/dummy/docker",
1796
                "build",
1797
                "--pull=False",
1798
                *expected_output_args,
1799
                "--tag",
1800
                "img1:latest",
1801
                "--file",
1802
                "docker/test/Dockerfile",
1803
                ".",
1804
            )
1805

1806
        build_process = assert_build_process(
1✔
1807
            rule_runner,
1808
            Address("docker/test", target_name="img1"),
1809
            build_process_assertions=check_build_process,
1810
            options=dict(use_buildx=True),
1811
        )
1812

1813
        def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
1✔
1814
            assert field_set == under_test_fs
1✔
1815
            return build_process
1✔
1816

1817
        def mock_execute_process(process: Process) -> FallibleProcessResult:
1✔
1818
            assert process == build_process.process
1✔
1819
            return FallibleProcessResult(
1✔
1820
                exit_code=0,
1821
                stdout=b"Successfully built abc123",
1822
                stderr=b"",
1823
                stdout_digest=EMPTY_FILE_DIGEST,
1824
                stderr_digest=EMPTY_FILE_DIGEST,
1825
                output_digest=EMPTY_DIGEST,
1826
                metadata=ProcessResultMetadata(
1827
                    0,
1828
                    ProcessExecutionEnvironment(
1829
                        environment_name=None,
1830
                        platform=Platform.create_for_localhost().value,
1831
                        docker_image=None,
1832
                        remote_execution=False,
1833
                        remote_execution_extra_platform_properties=[],
1834
                        execute_in_workspace=False,
1835
                        keep_sandboxes="never",
1836
                    ),
1837
                    "ran_locally",
1838
                    0,
1839
                ),
1840
            )
1841

1842
        def mock_create_digest(_request: CreateDigest) -> Digest:
1✔
1843
            return EMPTY_DIGEST
1✔
1844

1845
        mock_calls: dict[str, Callable[..., Any]] | None = {
1✔
1846
            "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
1847
            "pants.engine.intrinsics.execute_process": mock_execute_process,
1848
            "pants.engine.intrinsics.create_digest": mock_create_digest,
1849
        }
1850
    else:
1851
        mock_calls = None
1✔
1852

1853
    result = run_rule_with_mocks(
1✔
1854
        build_docker_image,
1855
        rule_args=[
1856
            under_test_fs,
1857
            docker_options,
1858
            global_options,
1859
            KeepSandboxes.never,
1860
        ],
1861
        mock_calls=mock_calls,
1862
        show_warnings=False,
1863
    )
1864

1865
    assert result.digest == EMPTY_DIGEST
1✔
1866
    assert len(result.artifacts) == (1 if (output is None or expected_output_args) else 0)
1✔
1867

1868

1869
def test_docker_build_network_option(rule_runner: RuleRunner) -> None:
1✔
1870
    rule_runner.write_files(
1✔
1871
        {
1872
            "docker/test/BUILD": dedent(
1873
                """\
1874
                docker_image(
1875
                  name="img1",
1876
                  build_network="host",
1877
                )
1878
                """
1879
            ),
1880
        }
1881
    )
1882

1883
    def check_build_process(result: DockerImageBuildProcess):
1✔
1884
        assert result.process.argv == (
1✔
1885
            "/dummy/docker",
1886
            "build",
1887
            "--network=host",
1888
            "--pull=False",
1889
            "--tag",
1890
            "img1:latest",
1891
            "--file",
1892
            "docker/test/Dockerfile",
1893
            ".",
1894
        )
1895

1896
    assert_build_process(
1✔
1897
        rule_runner,
1898
        Address("docker/test", target_name="img1"),
1899
        build_process_assertions=check_build_process,
1900
    )
1901

1902

1903
def test_docker_build_platform_option(rule_runner: RuleRunner) -> None:
1✔
1904
    rule_runner.write_files(
1✔
1905
        {
1906
            "docker/test/BUILD": dedent(
1907
                """\
1908
                docker_image(
1909
                  name="img1",
1910
                  build_platform=["linux/amd64", "linux/arm64", "linux/arm/v7"],
1911
                )
1912
                """
1913
            ),
1914
        }
1915
    )
1916

1917
    def check_build_process(result: DockerImageBuildProcess):
1✔
1918
        assert result.process.argv == (
1✔
1919
            "/dummy/docker",
1920
            "build",
1921
            "--platform=linux/amd64,linux/arm64,linux/arm/v7",
1922
            "--pull=False",
1923
            "--tag",
1924
            "img1:latest",
1925
            "--file",
1926
            "docker/test/Dockerfile",
1927
            ".",
1928
        )
1929

1930
    assert_build_process(
1✔
1931
        rule_runner,
1932
        Address("docker/test", target_name="img1"),
1933
        build_process_assertions=check_build_process,
1934
    )
1935

1936

1937
def test_docker_build_labels_option(rule_runner: RuleRunner) -> None:
1✔
1938
    rule_runner.write_files(
1✔
1939
        {
1940
            "docker/test/BUILD": dedent(
1941
                """\
1942
                docker_image(
1943
                  name="img1",
1944
                  extra_build_args=[
1945
                    "BUILD_SLAVE=tbs06",
1946
                    "BUILD_NUMBER=13934",
1947
                  ],
1948
                  image_labels={
1949
                    "build.host": "{build_args.BUILD_SLAVE}",
1950
                    "build.job": "{build_args.BUILD_NUMBER}",
1951
                  }
1952
                )
1953
                """
1954
            ),
1955
        }
1956
    )
1957

1958
    def check_build_process(result: DockerImageBuildProcess):
1✔
1959
        assert result.process.argv == (
1✔
1960
            "/dummy/docker",
1961
            "build",
1962
            "--label",
1963
            "build.host=tbs06",
1964
            "--label",
1965
            "build.job=13934",
1966
            "--pull=False",
1967
            "--tag",
1968
            "img1:latest",
1969
            "--build-arg",
1970
            "BUILD_NUMBER=13934",
1971
            "--build-arg",
1972
            "BUILD_SLAVE=tbs06",
1973
            "--file",
1974
            "docker/test/Dockerfile",
1975
            ".",
1976
        )
1977

1978
    assert_build_process(
1✔
1979
        rule_runner,
1980
        Address("docker/test", target_name="img1"),
1981
        build_process_assertions=check_build_process,
1982
    )
1983

1984

1985
@pytest.mark.parametrize("suggest_renames", [True, False])
1✔
1986
@pytest.mark.parametrize(
1✔
1987
    "context_root, copy_sources, build_context_files, expect_logged, fail_log_contains",
1988
    [
1989
        (
1990
            None,
1991
            ("src/project/bin.pex",),
1992
            ("src.project/binary.pex", "src/project/app.py"),
1993
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1994
            [
1995
                "suggested renames:\n\n  * src/project/bin.pex => src.project/binary.pex\n\n",
1996
                "There are files in the Docker build context that were not referenced by ",
1997
                "  * src/project/app.py\n\n",
1998
            ],
1999
        ),
2000
        (
2001
            "./",
2002
            ("config.txt",),
2003
            ("docker/test/conf/config.txt",),
2004
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2005
            [
2006
                "suggested renames:\n\n  * config.txt => conf/config.txt\n\n",
2007
            ],
2008
        ),
2009
        (
2010
            "./",
2011
            ("conf/config.txt",),
2012
            (
2013
                "docker/test/conf/config.txt",
2014
                "src.project/binary.pex",
2015
            ),
2016
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2017
            [
2018
                "There are unreachable files in these directories, excluded from the build context "
2019
                "due to `context_root` being 'docker/test':\n\n"
2020
                "  * src.project\n\n"
2021
                "Suggested `context_root` setting is '' in order to include all files in the "
2022
                "build context, otherwise relocate the files to be part of the current "
2023
                "`context_root` 'docker/test'."
2024
            ],
2025
        ),
2026
        (
2027
            "./config",
2028
            (),
2029
            (
2030
                "docker/test/config/..unusal-name",
2031
                "docker/test/config/.rc",
2032
                "docker/test/config/.a",
2033
                "docker/test/config/.conf.d/b",
2034
            ),
2035
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2036
            [
2037
                "There are files in the Docker build context that were not referenced by "
2038
                "any `COPY` instruction (this is not an error):\n"
2039
                "\n"
2040
                "  * ..unusal-name\n"
2041
                "  * .a\n"
2042
                "  * .conf.d/b\n"
2043
                "  * .rc\n"
2044
            ],
2045
        ),
2046
    ],
2047
)
2048
def test_docker_build_fail_logs(
1✔
2049
    rule_runner: RuleRunner,
2050
    caplog,
2051
    context_root: str | None,
2052
    copy_sources: tuple[str, ...],
2053
    build_context_files: tuple[str, ...],
2054
    expect_logged: list[tuple[int, str]] | None,
2055
    fail_log_contains: list[str],
2056
    suggest_renames: bool,
2057
) -> None:
2058
    caplog.set_level(logging.INFO)
1✔
2059
    rule_runner.write_files({"docker/test/BUILD": f"docker_image(context_root={context_root!r})"})
1✔
2060
    build_context_files = ("docker/test/Dockerfile", *build_context_files)
1✔
2061
    build_context_snapshot = rule_runner.make_snapshot_of_empty_files(build_context_files)
1✔
2062

2063
    # Step 1: Get the build process
2064
    tgt = rule_runner.get_target(Address("docker/test"))
1✔
2065
    address = Address("docker/test")
1✔
2066

2067
    build_context_mock = _create_build_context_mock(
1✔
2068
        rule_runner, address, build_context_snapshot, copy_sources, (), ()
2069
    )
2070
    docker_options = _setup_docker_options(rule_runner, {"suggest_renames": suggest_renames})
1✔
2071
    global_options = rule_runner.request(GlobalOptions, [])
1✔
2072

2073
    # Get image refs
2074
    repository = address.target_name
1✔
2075
    image_tags = tgt.get(DockerImageTagsField).value
1✔
2076
    tags_to_use = ("latest",) if image_tags is None else image_tags
1✔
2077
    image_refs = DockerImageRefs(
1✔
2078
        [
2079
            ImageRefRegistry(
2080
                registry=None,
2081
                repository=repository,
2082
                tags=tuple(
2083
                    ImageRefTag(
2084
                        template=tag,
2085
                        formatted=tag,
2086
                        full_name=f"{repository}:{tag}",
2087
                        uses_local_alias=False,
2088
                    )
2089
                    for tag in tags_to_use
2090
                ),
2091
            )
2092
        ]
2093
    )
2094

2095
    # Step 2: Create the build process with the get_docker_image_build_process rule
2096
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
2097
    docker = DockerBinary("/dummy/docker")
1✔
2098
    build_process = run_rule_with_mocks(
1✔
2099
        get_docker_image_build_process,
2100
        rule_args=[
2101
            under_test_fs,
2102
            docker_options,
2103
        ],
2104
        mock_calls={
2105
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
2106
            "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt),
2107
            "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs,
2108
            "pants.backend.docker.util_rules.binaries.get_docker": lambda *args: docker,
2109
        },
2110
        show_warnings=False,
2111
    )
2112

2113
    # Step 3: Test that build_docker_image handles the failure properly
2114
    def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
1✔
2115
        assert field_set == under_test_fs
1✔
2116
        return build_process
1✔
2117

2118
    def mock_execute_process(_process: Process) -> FallibleProcessResult:
1✔
2119
        # Simulate Docker build failure
2120
        return FallibleProcessResult(
1✔
2121
            exit_code=1,
2122
            stdout=b"stdout",
2123
            stderr=b"stderr",
2124
            stdout_digest=EMPTY_FILE_DIGEST,
2125
            stderr_digest=EMPTY_FILE_DIGEST,
2126
            output_digest=EMPTY_DIGEST,
2127
            metadata=ProcessResultMetadata(
2128
                0,
2129
                ProcessExecutionEnvironment(
2130
                    environment_name=None,
2131
                    platform=Platform.create_for_localhost().value,
2132
                    docker_image=None,
2133
                    remote_execution=False,
2134
                    remote_execution_extra_platform_properties=[],
2135
                    execute_in_workspace=False,
2136
                    keep_sandboxes="never",
2137
                ),
2138
                "ran_locally",
2139
                0,
2140
            ),
2141
        )
2142

2143
    with pytest.raises(ProcessExecutionFailure):
1✔
2144
        run_rule_with_mocks(
1✔
2145
            build_docker_image,
2146
            rule_args=[
2147
                under_test_fs,
2148
                docker_options,
2149
                global_options,
2150
                KeepSandboxes.never,
2151
            ],
2152
            mock_calls={
2153
                "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
2154
                "pants.engine.intrinsics.execute_process": mock_execute_process,
2155
            },
2156
            show_warnings=False,
2157
        )
2158

2159
    assert_logged(caplog, expect_logged)
1✔
2160
    for msg in fail_log_contains:
1✔
2161
        if suggest_renames:
1✔
2162
            assert msg in caplog.records[0].message
1✔
2163
        else:
2164
            assert msg not in caplog.records[0].message
1✔
2165

2166

2167
@pytest.mark.parametrize(
1✔
2168
    "expected_target, options",
2169
    [
2170
        ("dev", None),
2171
        ("prod", {"build_target_stage": "prod", "default_repository": "{name}"}),
2172
    ],
2173
)
2174
def test_build_target_stage(
1✔
2175
    rule_runner: RuleRunner, options: dict | None, expected_target: str
2176
) -> None:
2177
    rule_runner.write_files(
1✔
2178
        {
2179
            "BUILD": "docker_image(name='image', target_stage='dev')",
2180
            "Dockerfile": dedent(
2181
                """\
2182
                FROM base as build
2183
                FROM build as dev
2184
                FROM build as prod
2185
                """
2186
            ),
2187
        }
2188
    )
2189

2190
    def check_build_process(result: DockerImageBuildProcess):
1✔
2191
        assert result.process.argv == (
1✔
2192
            "/dummy/docker",
2193
            "build",
2194
            "--pull=False",
2195
            f"--target={expected_target}",
2196
            "--tag",
2197
            "image:latest",
2198
            "--file",
2199
            "Dockerfile",
2200
            ".",
2201
        )
2202

2203
    assert_build_process(
1✔
2204
        rule_runner,
2205
        Address("", target_name="image"),
2206
        options=options,
2207
        build_process_assertions=check_build_process,
2208
        version_tags=("build latest", "dev latest", "prod latest"),
2209
    )
2210

2211

2212
def test_invalid_build_target_stage(rule_runner: RuleRunner) -> None:
1✔
2213
    """An invalid target_stage is passed through to Docker which will report the error."""
2214
    rule_runner.write_files(
1✔
2215
        {
2216
            "BUILD": "docker_image(name='image', target_stage='bad')",
2217
            "Dockerfile": dedent(
2218
                """\
2219
                FROM base as build
2220
                FROM build as dev
2221
                FROM build as prod
2222
                """
2223
            ),
2224
        }
2225
    )
2226

2227
    def check_build_process(result: DockerImageBuildProcess):
1✔
2228
        assert result.process.argv == (
1✔
2229
            "/dummy/docker",
2230
            "build",
2231
            "--pull=False",
2232
            "--target=bad",
2233
            "--tag",
2234
            "image:latest",
2235
            "--file",
2236
            "Dockerfile",
2237
            ".",
2238
        )
2239

2240
    assert_build_process(
1✔
2241
        rule_runner,
2242
        Address("", target_name="image"),
2243
        build_process_assertions=check_build_process,
2244
        version_tags=("build latest", "dev latest", "prod latest"),
2245
    )
2246

2247

2248
@pytest.mark.parametrize(
1✔
2249
    "default_context_root, context_root, expected_context_root",
2250
    [
2251
        ("", None, "."),
2252
        (".", None, "."),
2253
        ("src", None, "src"),
2254
        (
2255
            "/",
2256
            None,
2257
            pytest.raises(
2258
                InvalidFieldException,
2259
                match=r"Use '' for a path relative to the build root, or '\./' for",
2260
            ),
2261
        ),
2262
        (
2263
            "/src",
2264
            None,
2265
            pytest.raises(
2266
                InvalidFieldException,
2267
                match=(
2268
                    r"The `context_root` field in target src/docker:image must be a relative path, "
2269
                    r"but was '/src'\. Use 'src' for a path relative to the build root, or '\./src' "
2270
                    r"for a path relative to the BUILD file \(i\.e\. 'src/docker/src'\)\."
2271
                ),
2272
            ),
2273
        ),
2274
        ("./", None, "src/docker"),
2275
        ("./build/context/", None, "src/docker/build/context"),
2276
        (".build/context/", None, ".build/context"),
2277
        ("ignored", "", "."),
2278
        ("ignored", ".", "."),
2279
        ("ignored", "src/context/", "src/context"),
2280
        ("ignored", "./", "src/docker"),
2281
        ("ignored", "src", "src"),
2282
        ("ignored", "./build/context", "src/docker/build/context"),
2283
    ],
2284
)
2285
def test_get_context_root(
1✔
2286
    context_root: str | None, default_context_root: str, expected_context_root: str | ContextManager
2287
) -> None:
2288
    if isinstance(expected_context_root, str):
1✔
2289
        raises = cast("ContextManager", no_exception())
1✔
2290
    else:
2291
        raises = expected_context_root
1✔
2292

2293
    with raises:
1✔
2294
        docker_options = create_subsystem(
1✔
2295
            DockerOptions,
2296
            default_context_root=default_context_root,
2297
        )
2298
        address = Address("src/docker", target_name="image")
1✔
2299
        tgt = DockerImageTarget({"context_root": context_root}, address)
1✔
2300
        fs = DockerPackageFieldSet.create(tgt)
1✔
2301
        actual_context_root = fs.get_context_root(docker_options.default_context_root)
1✔
2302
        assert actual_context_root == expected_context_root
1✔
2303

2304

2305
@pytest.mark.parametrize(
1✔
2306
    "docker, expected, stdout, stderr",
2307
    [
2308
        (
2309
            DockerBinary("/bin/docker", "1234"),
2310
            None,
2311
            "",
2312
            "",
2313
        ),
2314
        # Docker
2315
        (
2316
            DockerBinary("/bin/docker", "1234"),
2317
            "0e09b442b572",
2318
            "",
2319
            dedent(
2320
                """\
2321
                Step 22/22 : LABEL job-url="https://jenkins.example.net/job/python_artefactsapi_pipeline/"
2322
                 ---> Running in ae5c3eac5c0b
2323
                Removing intermediate container ae5c3eac5c0b
2324
                 ---> 0e09b442b572
2325
                Successfully built 0e09b442b572
2326
                Successfully tagged docker.example.net/artefactsapi/master:3.6.5
2327
                """
2328
            ),
2329
        ),
2330
        # Buildkit without step duration
2331
        (
2332
            DockerBinary("/bin/docker", "1234"),
2333
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
2334
            dedent(
2335
                """\
2336
                #7 [2/2] COPY testprojects.src.python.hello.main/main.pex /hello
2337
                #7 sha256:843d0c804a7eb5ba08b0535b635d5f98a3e56bc43a3fbe7d226a8024176f00d1
2338
                #7 DONE 0.1s
2339

2340
                #8 exporting to image
2341
                #8 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
2342
                #8 exporting layers 0.0s done
2343
                #8 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 done
2344
                #8 naming to docker.io/library/test-example-synth:1.2.5 done
2345
                #8 DONE 0.0s
2346

2347
                Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
2348

2349
                """
2350
            ),
2351
            "",
2352
        ),
2353
        # Buildkit with step duration
2354
        (
2355
            DockerBinary("/bin/docker", "1234"),
2356
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
2357
            dedent(
2358
                """\
2359
                #5 [2/2] RUN sleep 1
2360
                #5 DONE 1.1s
2361

2362
                #6 exporting to image
2363
                #6 exporting layers
2364
                #6 exporting layers 0.7s done
2365
                #6 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 0.0s done
2366
                #6 naming to docker.io/library/my-docker-image:latest 0.1s done
2367
                #6 DONE 1.1s
2368
                """
2369
            ),
2370
            "",
2371
        ),
2372
        # Buildkit with containerd-snapshotter 0.12.1
2373
        (
2374
            DockerBinary("/bin/docker", "1234"),
2375
            "sha256:b2b51838586286a9e544ddb31b3dbf7f6a99654d275b6e56b5f69f90138b4c0e",
2376
            dedent(
2377
                """\
2378
                #9 exporting to image
2379
                #9 exporting layers done
2380
                #9 exporting manifest sha256:7802087e8e0801f6451d862a00a6ce8af3e4829b09bc890dea0dd2659c11b25a done
2381
                #9 exporting config sha256:c83bed954709ba0c546d66d8f29afaac87c597f01b03fec158f3b21977c3e143 done
2382
                #9 exporting attestation manifest sha256:399891f9628cfafaba9e034599bdd55675ac0a3bad38151ed1ebf03993669545 done
2383
                #9 exporting manifest list sha256:b2b51838586286a9e544ddb31b3dbf7f6a99654d275b6e56b5f69f90138b4c0e done
2384
                #9 naming to myhost.com/my_app:latest done
2385
                #9 unpacking to myhost.com/my_app:latest done
2386
                #9 DONE 0.0s
2387
                """
2388
            ),
2389
            "",
2390
        ),
2391
        # Buildkit with containerd-snapshotter and cross platform 0.12.1
2392
        (
2393
            DockerBinary("/bin/docker", "1234"),
2394
            "sha256:3c72de0e05bb75247e68e124e6500700f6e0597425db2ee9f08fd59ef28cea0f",
2395
            dedent(
2396
                """\
2397
                #12 exporting to image
2398
                #12 exporting layers done
2399
                #12 exporting manifest sha256:452598369b55c27d752c45736cf26c0339612077f17df31fb0cdd79c5145d081 done
2400
                #12 exporting config sha256:6fbcebfde0ec24b487045516c3b5ffd3f0633e756a6d5808c2e5ad75809e0ca6 done
2401
                #12 exporting attestation manifest sha256:32fcf615e85bc9c2f606f863e8db3ca16dd77613a1e175e5972f39267e106dfb done
2402
                #12 exporting manifest sha256:bcb911a3efbec48e3c58c2acfd38fe92321eed731c53253f0b5c883918420187 done
2403
                #12 exporting config sha256:86e7fd0c4fa2356430d4ca188ed9e86497b8d03996ccba426d92c7e145e69990 done
2404
                #12 exporting attestation manifest sha256:66f9e7af29dd04e6264b8e113571f7b653f1681ba124a386530145fb39ff0102 done
2405
                #12 exporting manifest list sha256:3c72de0e05bb75247e68e124e6500700f6e0597425db2ee9f08fd59ef28cea0f done
2406
                #12 naming to myhost.com/my_app:latest done
2407
                #12 unpacking to myhost.com/my_app:latest done
2408
                #12 DONE 0.0s
2409
                """
2410
            ),
2411
            "",
2412
        ),
2413
        # Buildkit with containerd-snapshotter 0.13.1
2414
        (
2415
            DockerBinary("/bin/docker", "1234"),
2416
            "sha256:d15432046b4feaebb70370fad4710151dd8f0b9741cb8bc4d20c08ed8847f17a",
2417
            dedent(
2418
                """\
2419
                #13 exporting to image
2420
                #13 exporting layers
2421
                #13 exporting layers done
2422
                #13 exporting manifest sha256:2f161cf7c511874936d99995adeb53c6ac2262279a606bc1b70756ca1367ceb5 done
2423
                #13 exporting config sha256:23bf9de65f90e11ab7bb6bad0e1fb5c7eee3df2050aa902e8a53684fbd539eb9 done
2424
                #13 exporting attestation manifest sha256:5ff8bf97d8ad78a119d95d2b887400b3482a9026192ca7fb70307dfe290c93bf 0.0s done
2425
                #13 exporting manifest sha256:bf37d968d569812df393c7b6a48eab143066fa56a001905d9a70ec7acf3d34f4 done
2426
                #13 exporting config sha256:7c99f317cfae97e79dc12096279b71036a60129314e670920475665d466c821f done
2427
                #13 exporting attestation manifest sha256:4b3176781bb62e51cce743d4428e84e3559c9a23c328d6dfbfacac67f282cf70 0.0s done
2428
                #13 exporting manifest list sha256:d15432046b4feaebb70370fad4710151dd8f0b9741cb8bc4d20c08ed8847f17a 0.0s done
2429
                #13 naming to my-host.com/repo:latest done
2430
                #13 unpacking to my-host.com/repo:latest done
2431
                #13 DONE 0.1s
2432
                """
2433
            ),
2434
            "",
2435
        ),
2436
        # Buildkit with containerd-snapshotter 0.17.1 and disabled attestations
2437
        (
2438
            DockerBinary("/bin/docker", "1234"),
2439
            "sha256:6c3aff6414781126578b3e7b4a217682e89c616c0eac864d5b3ea7c87f1094d0",
2440
            dedent(
2441
                """\
2442
                    #24 exporting to image
2443
                    #24 exporting layers done
2444
                    #24 preparing layers for inline cache
2445
                    #24 preparing layers for inline cache 0.4s done
2446
                    #24 exporting manifest sha256:6c3aff6414781126578b3e7b4a217682e89c616c0eac864d5b3ea7c87f1094d0 0.0s done
2447
                    #24 exporting config sha256:af716170542d95134cb41b56e2dfea2c000b05b6fc4f440158ed9834ff96d1b4 0.0s done
2448
                    #24 naming to REDACTED:latest done
2449
                    #24 unpacking to REDACTED:latest 0.0s done
2450
                    #24 DONE 0.5s
2451

2452
                    """
2453
            ),
2454
            "",
2455
        ),
2456
    ],
2457
)
2458
def test_parse_image_id_from_docker_build_output(
1✔
2459
    docker: DockerBinary, expected: str | None, stdout: str, stderr: str
2460
) -> None:
2461
    assert expected == parse_image_id_from_buildkit_output(stdout.encode(), stderr.encode())
1✔
2462

2463

2464
def test_parse_image_id_from_podman_build_output() -> None:
1✔
2465
    stdout = dedent(
1✔
2466
        """\
2467
        STEP 5/5: COPY ./ .
2468
        COMMIT example
2469
        --> a85499e9039a
2470
        Successfully tagged localhost/example:latest
2471
        a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee
2472
        """
2473
    )
2474
    assert (
1✔
2475
        "a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee"
2476
        == parse_image_id_from_podman_build_output(stdout.encode(), b"")
2477
    )
2478

2479

2480
ImageRefTest = namedtuple(
1✔
2481
    "ImageRefTest",
2482
    "docker_image, registries, default_repository, expect_refs, expect_error",
2483
    defaults=({}, {}, "{name}", (), None),
2484
)
2485

2486

2487
@pytest.mark.parametrize(
1✔
2488
    "test",
2489
    [
2490
        ImageRefTest(
2491
            docker_image=dict(name="lowercase"),
2492
            expect_refs=(
2493
                ImageRefRegistry(
2494
                    registry=None,
2495
                    repository="lowercase",
2496
                    tags=(
2497
                        ImageRefTag(
2498
                            template="latest",
2499
                            formatted="latest",
2500
                            uses_local_alias=False,
2501
                            full_name="lowercase:latest",
2502
                        ),
2503
                    ),
2504
                ),
2505
            ),
2506
        ),
2507
        ImageRefTest(
2508
            docker_image=dict(name="CamelCase"),
2509
            expect_refs=(
2510
                ImageRefRegistry(
2511
                    registry=None,
2512
                    repository="camelcase",
2513
                    tags=(
2514
                        ImageRefTag(
2515
                            template="latest",
2516
                            formatted="latest",
2517
                            uses_local_alias=False,
2518
                            full_name="camelcase:latest",
2519
                        ),
2520
                    ),
2521
                ),
2522
            ),
2523
        ),
2524
        ImageRefTest(
2525
            docker_image=dict(image_tags=["CamelCase"]),
2526
            expect_refs=(
2527
                ImageRefRegistry(
2528
                    registry=None,
2529
                    repository="image",
2530
                    tags=(
2531
                        ImageRefTag(
2532
                            template="CamelCase",
2533
                            formatted="CamelCase",
2534
                            uses_local_alias=False,
2535
                            full_name="image:CamelCase",
2536
                        ),
2537
                    ),
2538
                ),
2539
            ),
2540
        ),
2541
        ImageRefTest(
2542
            docker_image=dict(image_tags=["{val1}", "prefix-{val2}"]),
2543
            expect_refs=(
2544
                ImageRefRegistry(
2545
                    registry=None,
2546
                    repository="image",
2547
                    tags=(
2548
                        ImageRefTag(
2549
                            template="{val1}",
2550
                            formatted="first-value",
2551
                            uses_local_alias=False,
2552
                            full_name="image:first-value",
2553
                        ),
2554
                        ImageRefTag(
2555
                            template="prefix-{val2}",
2556
                            formatted="prefix-second-value",
2557
                            uses_local_alias=False,
2558
                            full_name="image:prefix-second-value",
2559
                        ),
2560
                    ),
2561
                ),
2562
            ),
2563
        ),
2564
        ImageRefTest(
2565
            docker_image=dict(registries=["REG1.example.net"]),
2566
            expect_refs=(
2567
                ImageRefRegistry(
2568
                    registry=DockerRegistryOptions(address="REG1.example.net"),
2569
                    repository="image",
2570
                    tags=(
2571
                        ImageRefTag(
2572
                            template="latest",
2573
                            formatted="latest",
2574
                            uses_local_alias=False,
2575
                            full_name="REG1.example.net/image:latest",
2576
                        ),
2577
                    ),
2578
                ),
2579
            ),
2580
        ),
2581
        ImageRefTest(
2582
            docker_image=dict(registries=["docker.io", "@private"], repository="our-the/pkg"),
2583
            registries=dict(private={"address": "our.registry", "repository": "the/pkg"}),
2584
            expect_refs=(
2585
                ImageRefRegistry(
2586
                    registry=DockerRegistryOptions(address="docker.io"),
2587
                    repository="our-the/pkg",
2588
                    tags=(
2589
                        ImageRefTag(
2590
                            template="latest",
2591
                            formatted="latest",
2592
                            uses_local_alias=False,
2593
                            full_name="docker.io/our-the/pkg:latest",
2594
                        ),
2595
                    ),
2596
                ),
2597
                ImageRefRegistry(
2598
                    registry=DockerRegistryOptions(
2599
                        alias="private", address="our.registry", repository="the/pkg"
2600
                    ),
2601
                    repository="the/pkg",
2602
                    tags=(
2603
                        ImageRefTag(
2604
                            template="latest",
2605
                            formatted="latest",
2606
                            uses_local_alias=False,
2607
                            full_name="our.registry/the/pkg:latest",
2608
                        ),
2609
                    ),
2610
                ),
2611
            ),
2612
        ),
2613
        ImageRefTest(
2614
            docker_image=dict(
2615
                registries=["docker.io", "@private"],
2616
                repository="{parent_directory}/{default_repository}",
2617
            ),
2618
            registries=dict(
2619
                private={"address": "our.registry", "repository": "{target_repository}/the/pkg"}
2620
            ),
2621
            expect_refs=(
2622
                ImageRefRegistry(
2623
                    registry=DockerRegistryOptions(address="docker.io"),
2624
                    repository="test/image",
2625
                    tags=(
2626
                        ImageRefTag(
2627
                            template="latest",
2628
                            formatted="latest",
2629
                            uses_local_alias=False,
2630
                            full_name="docker.io/test/image:latest",
2631
                        ),
2632
                    ),
2633
                ),
2634
                ImageRefRegistry(
2635
                    registry=DockerRegistryOptions(
2636
                        alias="private",
2637
                        address="our.registry",
2638
                        repository="{target_repository}/the/pkg",
2639
                    ),
2640
                    repository="test/image/the/pkg",
2641
                    tags=(
2642
                        ImageRefTag(
2643
                            template="latest",
2644
                            formatted="latest",
2645
                            uses_local_alias=False,
2646
                            full_name="our.registry/test/image/the/pkg:latest",
2647
                        ),
2648
                    ),
2649
                ),
2650
            ),
2651
        ),
2652
        ImageRefTest(
2653
            docker_image=dict(registries=["@private"], image_tags=["prefix-{val1}"]),
2654
            registries=dict(
2655
                private={"address": "our.registry", "extra_image_tags": ["{val2}-suffix"]}
2656
            ),
2657
            expect_refs=(
2658
                ImageRefRegistry(
2659
                    registry=DockerRegistryOptions(
2660
                        alias="private",
2661
                        address="our.registry",
2662
                        extra_image_tags=("{val2}-suffix",),
2663
                    ),
2664
                    repository="image",
2665
                    tags=(
2666
                        ImageRefTag(
2667
                            template="prefix-{val1}",
2668
                            formatted="prefix-first-value",
2669
                            uses_local_alias=False,
2670
                            full_name="our.registry/image:prefix-first-value",
2671
                        ),
2672
                        ImageRefTag(
2673
                            template="{val2}-suffix",
2674
                            formatted="second-value-suffix",
2675
                            uses_local_alias=False,
2676
                            full_name="our.registry/image:second-value-suffix",
2677
                        ),
2678
                    ),
2679
                ),
2680
            ),
2681
        ),
2682
        ImageRefTest(
2683
            docker_image=dict(repository="{default_repository}/a"),
2684
            default_repository="{target_repository}/b",
2685
            expect_error=pytest.raises(
2686
                InterpolationError,
2687
                match=(
2688
                    r"Invalid value for the `repository` field of the `docker_image` target at "
2689
                    r"src/test/docker:image: '\{default_repository\}/a'\.\n\n"
2690
                    r"The formatted placeholders recurse too deep\.\n"
2691
                    r"'\{default_repository\}/a' => '\{target_repository\}/b/a' => "
2692
                    r"'\{default_repository\}/a/b/a'"
2693
                ),
2694
            ),
2695
        ),
2696
        ImageRefTest(
2697
            # Test registry `use_local_alias` (#16354)
2698
            docker_image=dict(registries=["docker.io", "@private"], repository="our-the/pkg"),
2699
            registries=dict(
2700
                private={
2701
                    "address": "our.registry",
2702
                    "repository": "the/pkg",
2703
                    "use_local_alias": True,
2704
                }
2705
            ),
2706
            expect_refs=(
2707
                ImageRefRegistry(
2708
                    registry=DockerRegistryOptions(address="docker.io"),
2709
                    repository="our-the/pkg",
2710
                    tags=(
2711
                        ImageRefTag(
2712
                            template="latest",
2713
                            formatted="latest",
2714
                            uses_local_alias=False,
2715
                            full_name="docker.io/our-the/pkg:latest",
2716
                        ),
2717
                    ),
2718
                ),
2719
                ImageRefRegistry(
2720
                    registry=DockerRegistryOptions(
2721
                        alias="private",
2722
                        address="our.registry",
2723
                        repository="the/pkg",
2724
                        use_local_alias=True,
2725
                    ),
2726
                    repository="the/pkg",
2727
                    tags=(
2728
                        ImageRefTag(
2729
                            template="latest",
2730
                            formatted="latest",
2731
                            uses_local_alias=False,
2732
                            full_name="our.registry/the/pkg:latest",
2733
                        ),
2734
                        ImageRefTag(
2735
                            template="latest",
2736
                            formatted="latest",
2737
                            uses_local_alias=True,
2738
                            full_name="private/the/pkg:latest",
2739
                        ),
2740
                    ),
2741
                ),
2742
            ),
2743
        ),
2744
    ],
2745
)
2746
def test_image_ref_formatting(test: ImageRefTest) -> None:
1✔
2747
    address = Address("src/test/docker", target_name=test.docker_image.pop("name", "image"))
1✔
2748
    tgt = DockerImageTarget(test.docker_image, address)
1✔
2749
    field_set = DockerPackageFieldSet.create(tgt)
1✔
2750
    registries = DockerRegistries.from_dict(test.registries)
1✔
2751
    interpolation_context = InterpolationContext.from_dict(
1✔
2752
        {"val1": "first-value", "val2": "second-value"}
2753
    )
2754
    with test.expect_error or no_exception():
1✔
2755
        image_refs = field_set.image_refs(
1✔
2756
            test.default_repository, registries, interpolation_context
2757
        )
2758
        assert tuple(image_refs) == test.expect_refs
1✔
2759

2760

2761
@pytest.mark.parametrize(
1✔
2762
    "BUILD, plugin_tags, tag_flags",
2763
    [
2764
        (
2765
            'docker_image(name="plugin")',
2766
            ("1.2.3",),
2767
            (
2768
                "--tag",
2769
                "plugin:latest",
2770
                "--tag",
2771
                "plugin:1.2.3",
2772
            ),
2773
        ),
2774
        (
2775
            'docker_image(name="plugin", image_tags=[])',
2776
            ("1.2.3",),
2777
            (
2778
                "--tag",
2779
                "plugin:1.2.3",
2780
            ),
2781
        ),
2782
    ],
2783
)
2784
def test_docker_image_tags_from_plugin_hook(
1✔
2785
    rule_runner: RuleRunner, BUILD: str, plugin_tags: tuple[str, ...], tag_flags: tuple[str, ...]
2786
) -> None:
2787
    rule_runner.write_files({"docker/test/BUILD": BUILD})
1✔
2788

2789
    refs = assert_get_image_refs(
1✔
2790
        rule_runner,
2791
        Address("docker/test", target_name="plugin"),
2792
        plugin_tags=plugin_tags,
2793
    )
2794

2795
    def check_build_process(result: DockerImageBuildProcess):
1✔
2796
        assert result.process.argv == (
1✔
2797
            "/dummy/docker",
2798
            "build",
2799
            "--pull=False",
2800
            *tag_flags,
2801
            "--file",
2802
            "docker/test/Dockerfile",
2803
            ".",
2804
        )
2805

2806
    assert_build_process(
1✔
2807
        rule_runner,
2808
        Address("docker/test", target_name="plugin"),
2809
        build_process_assertions=check_build_process,
2810
        image_refs=refs,
2811
    )
2812

2813

2814
def test_docker_image_tags_defined(rule_runner: RuleRunner) -> None:
1✔
2815
    rule_runner.write_files({"docker/test/BUILD": 'docker_image(name="no-tags", image_tags=[])'})
1✔
2816

2817
    err = "The `image_tags` field in target docker/test:no-tags must not be empty, unless"
1✔
2818
    with pytest.raises(InvalidFieldException, match=err):
1✔
2819
        assert_build_process(
1✔
2820
            rule_runner,
2821
            Address("docker/test", target_name="no-tags"),
2822
        )
2823

2824

2825
def test_docker_info_serialize() -> None:
1✔
2826
    image_id = "abc123"
1✔
2827
    # image refs with unique strings (i.e. not actual templates/names etc.), to make sure they're
2828
    # ending up in the right place in the JSON
2829
    image_refs = (
1✔
2830
        ImageRefRegistry(
2831
            registry=None,
2832
            repository="repo",
2833
            tags=(
2834
                ImageRefTag(
2835
                    template="repo tag1 template",
2836
                    formatted="repo tag1 formatted",
2837
                    uses_local_alias=False,
2838
                    full_name="repo tag1 full name",
2839
                ),
2840
                ImageRefTag(
2841
                    template="repo tag2 template",
2842
                    formatted="repo tag2 formatted",
2843
                    uses_local_alias=False,
2844
                    full_name="repo tag2 full name",
2845
                ),
2846
            ),
2847
        ),
2848
        ImageRefRegistry(
2849
            registry=DockerRegistryOptions(address="address"),
2850
            repository="address repo",
2851
            tags=(
2852
                ImageRefTag(
2853
                    template="address tag template",
2854
                    formatted="address tag formatted",
2855
                    uses_local_alias=False,
2856
                    full_name="address tag full name",
2857
                ),
2858
            ),
2859
        ),
2860
        ImageRefRegistry(
2861
            registry=DockerRegistryOptions(
2862
                address="alias address", alias="alias", repository="alias registry repo"
2863
            ),
2864
            repository="alias repo",
2865
            tags=(
2866
                ImageRefTag(
2867
                    template="alias tag (address) template",
2868
                    formatted="alias tag (address) formatted",
2869
                    uses_local_alias=False,
2870
                    full_name="alias tag (address) full name",
2871
                ),
2872
                ImageRefTag(
2873
                    template="alias tag (local alias) template",
2874
                    formatted="alias tag (local alias) formatted",
2875
                    uses_local_alias=True,
2876
                    full_name="alias tag (local alias) full name",
2877
                ),
2878
            ),
2879
        ),
2880
    )
2881

2882
    expected = dict(
1✔
2883
        version=1,
2884
        image_id=image_id,
2885
        registries=[
2886
            dict(
2887
                alias=None,
2888
                address=None,
2889
                repository="repo",
2890
                tags=[
2891
                    dict(
2892
                        template="repo tag1 template",
2893
                        tag="repo tag1 formatted",
2894
                        uses_local_alias=False,
2895
                        name="repo tag1 full name",
2896
                    ),
2897
                    dict(
2898
                        template="repo tag2 template",
2899
                        tag="repo tag2 formatted",
2900
                        uses_local_alias=False,
2901
                        name="repo tag2 full name",
2902
                    ),
2903
                ],
2904
            ),
2905
            dict(
2906
                alias=None,
2907
                address="address",
2908
                repository="address repo",
2909
                tags=[
2910
                    dict(
2911
                        template="address tag template",
2912
                        tag="address tag formatted",
2913
                        uses_local_alias=False,
2914
                        name="address tag full name",
2915
                    )
2916
                ],
2917
            ),
2918
            dict(
2919
                alias="alias",
2920
                address="alias address",
2921
                repository="alias repo",
2922
                tags=[
2923
                    dict(
2924
                        template="alias tag (address) template",
2925
                        tag="alias tag (address) formatted",
2926
                        uses_local_alias=False,
2927
                        name="alias tag (address) full name",
2928
                    ),
2929
                    dict(
2930
                        template="alias tag (local alias) template",
2931
                        tag="alias tag (local alias) formatted",
2932
                        uses_local_alias=True,
2933
                        name="alias tag (local alias) full name",
2934
                    ),
2935
                ],
2936
            ),
2937
        ],
2938
    )
2939

2940
    result = DockerInfoV1.serialize(image_refs, image_id)
1✔
2941
    assert json.loads(result) == expected
1✔
2942

2943

2944
@pytest.mark.parametrize(
1✔
2945
    ("output", "expected"),
2946
    [({"type": "image", "push": "true"}, True), ({"type": "registry"}, True), (None, False)],
2947
)
2948
def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> None:
1✔
2949
    rule_runner = RuleRunner(target_types=[DockerImageTarget])
1✔
2950
    output_str = f", output={output}" if output else ""
1✔
2951
    rule_runner.write_files(
1✔
2952
        {"BUILD": f"docker_image(name='image', source='Dockerfile'{output_str})"}
2953
    )
2954
    field_set = DockerPackageFieldSet.create(
1✔
2955
        rule_runner.get_target(Address("", target_name="image"))
2956
    )
2957
    assert field_set.pushes_on_package() is expected
1✔
2958

2959

2960
def test_docker_build_process_buildctl_engine(rule_runner: RuleRunner) -> None:
1✔
2961
    rule_runner.write_files(
1✔
2962
        {
2963
            "docker/test/BUILD": dedent(
2964
                """\
2965
                docker_image(
2966
                  name="img1",
2967
                  secrets={
2968
                    "system-secret": "/var/run/secrets/mysecret",
2969
                    "target-secret": "./mysecret",
2970
                  },
2971
                  ssh=["default"],
2972
                  image_labels={
2973
                    "version": "1.0",
2974
                  },
2975
                  build_platform=["linux/amd64", "linux/arm64"],
2976
                )
2977
                """
2978
            ),
2979
        }
2980
    )
2981

2982
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
2983
        assert result.process.argv == (
1✔
2984
            "/dummy/buildctl",
2985
            "build",
2986
            "--frontend",
2987
            "dockerfile.v0",
2988
            "--local",
2989
            "context=.",
2990
            "--local",
2991
            "dockerfile=docker/test",
2992
            "--opt",
2993
            "filename=Dockerfile",
2994
            "--opt",
2995
            "platform=linux/amd64,linux/arm64",
2996
            "--opt",
2997
            "label:version=1.0",
2998
            "--secret",
2999
            "id=system-secret,src=/var/run/secrets/mysecret",
3000
            "--secret",
3001
            f"id=target-secret,src={rule_runner.build_root}/docker/test/mysecret",
3002
            "--ssh",
3003
            "default",
3004
            "--output",
3005
            "type=image,name=img1:latest",
3006
        )
3007

3008
    assert_build_process(
1✔
3009
        rule_runner,
3010
        Address("docker/test", target_name="img1"),
3011
        options=dict(engine=DockerEngines(build=DockerBuildEngine.BUILDCTL)),
3012
        binary=BuildctlBinary("/dummy/buildctl"),
3013
        build_process_assertions=check_build_process,
3014
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc