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

pantsbuild / pants / 23505168284

24 Mar 2026 06:11PM UTC coverage: 92.618% (-0.3%) from 92.918%
23505168284

Pull #23133

github

web-flow
Merge e8047d851 into b84b29b9b
Pull Request #23133: Add buildctl engine

215 of 296 new or added lines in 13 files covered. (72.64%)

233 existing lines in 12 files now uncovered.

91363 of 98645 relevant lines covered (92.62%)

4.05 hits per line

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

77.06
/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.goals.package_image import (
1✔
17
    DockerBuildTargetStageError,
18
    DockerImageBuildProcess,
19
    DockerImageOptionValueError,
20
    DockerImageRefs,
21
    DockerImageTagValueError,
22
    DockerInfoV1,
23
    DockerPackageFieldSet,
24
    DockerRepositoryNameError,
25
    GetImageRefsRequest,
26
    ImageRefRegistry,
27
    ImageRefTag,
28
    build_docker_image,
29
    get_docker_image_build_process,
30
    get_image_refs,
31
    parse_image_id_from_buildkit_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 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:
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("use_buildx", False)
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
) -> DockerImageBuildProcess:
182
    """Test helper for get_docker_image_build_process rule.
183

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

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

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

217
    result = run_rule_with_mocks(
1✔
218
        get_docker_image_build_process,
219
        rule_args=[
220
            DockerPackageFieldSet.create(tgt),
221
            docker_options,
222
            DockerBinary("/dummy/docker"),
223
        ],
224
        mock_calls={
225
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
226
            "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt),
227
            "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs,
228
        },
229
        union_membership=_create_union_membership(),
230
        show_warnings=False,
231
    )
232

233
    # Run optional assertions
UNCOV
234
    if build_process_assertions:
×
UNCOV
235
        build_process_assertions(result)
×
236

UNCOV
237
    return result
×
238

239

240
def assert_get_image_refs(
1✔
241
    rule_runner: RuleRunner,
242
    address: Address,
243
    *,
244
    options: dict | None = None,
245
    expected_refs: DockerImageRefs | None = None,
246
    version_tags: tuple[str, ...] = (),
247
    plugin_tags: tuple[str, ...] = (),
248
    copy_sources: tuple[str, ...] = (),
249
    copy_build_args=(),
250
    build_context_snapshot: Snapshot = EMPTY_SNAPSHOT,
251
    build_upstream_images: bool = True,
252
) -> DockerImageRefs:
253
    """Test helper for get_image_refs rule.
254

255
    Returns DockerImageRefs for validation. Optionally asserts against expected_refs.
256
    """
257
    tgt = rule_runner.get_target(address)
1✔
258

259
    build_context_mock = _create_build_context_mock(
1✔
260
        rule_runner, address, build_context_snapshot, copy_sources, copy_build_args, version_tags
261
    )
262
    docker_options = _setup_docker_options(rule_runner, options)
1✔
263
    union_membership = _create_union_membership()
1✔
264

265
    field_set = DockerPackageFieldSet.create(tgt)
1✔
266
    result = run_rule_with_mocks(
1✔
267
        get_image_refs,
268
        rule_args=[
269
            GetImageRefsRequest(
270
                field_set=field_set,
271
                build_upstream_images=build_upstream_images,
272
            ),
273
            docker_options,
274
            union_membership,
275
        ],
276
        mock_calls={
277
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
278
            "pants.engine.internals.graph.resolve_target": lambda *_, **__: WrappedTarget(tgt),
279
            "pants.backend.docker.target_types.get_docker_image_tags": lambda *_,
280
            **__: DockerImageTags(plugin_tags),
281
        },
282
        union_membership=union_membership,
283
    )
284

285
    if expected_refs is not None:
1✔
286
        assert result == expected_refs
1✔
287

288
    return result
1✔
289

290

291
def test_get_image_refs(rule_runner: RuleRunner) -> None:
1✔
292
    rule_runner.write_files(
1✔
293
        {
294
            "docker/test/BUILD": dedent(
295
                """\
296

297
                docker_image(
298
                  name="test1",
299
                  image_tags=["1.2.3"],
300
                  repository="{directory}/{name}",
301
                )
302
                docker_image(
303
                  name="test2",
304
                  image_tags=["1.2.3"],
305
                )
306
                docker_image(
307
                  name="test3",
308
                  image_tags=["1.2.3"],
309
                  repository="{parent_directory}/{directory}/{name}",
310
                )
311
                docker_image(
312
                  name="test4",
313
                  image_tags=["1.2.3"],
314
                  repository="{directory}/four/test-four",
315
                )
316
                docker_image(
317
                  name="test5",
318
                  image_tags=["latest", "alpha-1.0", "alpha-1"],
319
                )
320
                docker_image(
321
                  name="test6",
322
                  image_tags=["1.2.3"],
323
                  repository="xyz/{full_directory}/{name}",
324
                )
325
                docker_image(
326
                  name="err1",
327
                  repository="{bad_template}",
328
                )
329
                """
330
            ),
331
            "docker/test/Dockerfile": "FROM python:3.8",
332
        }
333
    )
334

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

396
    assert_get_image_refs(
1✔
397
        rule_runner,
398
        Address("docker/test", target_name="test4"),
399
        expected_refs=DockerImageRefs(
400
            [
401
                ImageRefRegistry(
402
                    registry=None,
403
                    repository="test/four/test-four",
404
                    tags=(
405
                        ImageRefTag(
406
                            template="1.2.3",
407
                            formatted="1.2.3",
408
                            full_name="test/four/test-four:1.2.3",
409
                            uses_local_alias=False,
410
                        ),
411
                    ),
412
                ),
413
            ]
414
        ),
415
    )
416

417
    assert_get_image_refs(
1✔
418
        rule_runner,
419
        Address("docker/test", target_name="test5"),
420
        options=dict(default_repository="{directory}/{name}"),
421
        expected_refs=DockerImageRefs(
422
            [
423
                ImageRefRegistry(
424
                    registry=None,
425
                    repository="test/test5",
426
                    tags=(
427
                        ImageRefTag(
428
                            template="latest",
429
                            formatted="latest",
430
                            full_name="test/test5:latest",
431
                            uses_local_alias=False,
432
                        ),
433
                        ImageRefTag(
434
                            template="alpha-1.0",
435
                            formatted="alpha-1.0",
436
                            full_name="test/test5:alpha-1.0",
437
                            uses_local_alias=False,
438
                        ),
439
                        ImageRefTag(
440
                            template="alpha-1",
441
                            formatted="alpha-1",
442
                            full_name="test/test5:alpha-1",
443
                            uses_local_alias=False,
444
                        ),
445
                    ),
446
                ),
447
            ]
448
        ),
449
    )
450

451
    assert_get_image_refs(
1✔
452
        rule_runner,
453
        Address("docker/test", target_name="test6"),
454
        expected_refs=DockerImageRefs(
455
            [
456
                ImageRefRegistry(
457
                    registry=None,
458
                    repository="xyz/docker/test/test6",
459
                    tags=(
460
                        ImageRefTag(
461
                            template="1.2.3",
462
                            formatted="1.2.3",
463
                            full_name="xyz/docker/test/test6:1.2.3",
464
                            uses_local_alias=False,
465
                        ),
466
                    ),
467
                ),
468
            ]
469
        ),
470
    )
471

472
    err1 = (
1✔
473
        r"Invalid value for the `repository` field of the `docker_image` target at "
474
        r"docker/test:err1: '{bad_template}'\.\n\nThe placeholder 'bad_template' is unknown\. "
475
        r"Try with one of: build_args, default_repository, directory, full_directory, name, "
476
        r"pants, parent_directory, tags, target_repository\."
477
    )
478
    with pytest.raises(DockerRepositoryNameError, match=err1):
1✔
479
        assert_get_image_refs(
1✔
480
            rule_runner,
481
            Address("docker/test", target_name="err1"),
482
        )
483

484

485
def test_get_image_refs_with_registries(rule_runner: RuleRunner) -> None:
1✔
486
    rule_runner.write_files(
1✔
487
        {
488
            "docker/test/BUILD": dedent(
489
                """\
490
                docker_image(name="addr1", image_tags=["1.2.3"], registries=["myregistry1domain:port"])
491
                docker_image(name="addr2", image_tags=["1.2.3"], registries=["myregistry2domain:port"])
492
                docker_image(name="addr3", image_tags=["1.2.3"], registries=["myregistry3domain:port"])
493
                docker_image(name="alias1", image_tags=["1.2.3"], registries=["@reg1"])
494
                docker_image(name="alias2", image_tags=["1.2.3"], registries=["@reg2"])
495
                docker_image(name="alias3", image_tags=["1.2.3"], registries=["reg3"])
496
                docker_image(name="unreg", image_tags=["1.2.3"], registries=[])
497
                docker_image(name="def", image_tags=["1.2.3"])
498
                docker_image(name="multi", image_tags=["1.2.3"], registries=["@reg2", "@reg1"])
499
                docker_image(name="extra_tags", image_tags=["1.2.3"], registries=["@reg1", "@extra"])
500
                """
501
            ),
502
            "docker/test/Dockerfile": "FROM python:3.8",
503
        }
504
    )
505

506
    options = {
1✔
507
        "default_repository": "{name}",
508
        "registries": {
509
            "reg1": {"address": "myregistry1domain:port"},
510
            "reg2": {"address": "myregistry2domain:port", "default": True},
511
            "extra": {"address": "extra", "extra_image_tags": ["latest"]},
512
        },
513
    }
514

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

560
    assert_get_image_refs(
1✔
561
        rule_runner,
562
        Address("docker/test", target_name="addr3"),
563
        options=options,
564
        expected_refs=DockerImageRefs(
565
            [
566
                ImageRefRegistry(
567
                    registry=DockerRegistryOptions(address="myregistry3domain:port"),
568
                    repository="addr3",
569
                    tags=(
570
                        ImageRefTag(
571
                            template="1.2.3",
572
                            formatted="1.2.3",
573
                            full_name="myregistry3domain:port/addr3:1.2.3",
574
                            uses_local_alias=False,
575
                        ),
576
                    ),
577
                ),
578
            ]
579
        ),
580
    )
581

582
    assert_get_image_refs(
1✔
583
        rule_runner,
584
        Address("docker/test", target_name="alias1"),
585
        options=options,
586
        expected_refs=DockerImageRefs(
587
            [
588
                ImageRefRegistry(
589
                    registry=DockerRegistryOptions(alias="reg1", address="myregistry1domain:port"),
590
                    repository="alias1",
591
                    tags=(
592
                        ImageRefTag(
593
                            template="1.2.3",
594
                            formatted="1.2.3",
595
                            full_name="myregistry1domain:port/alias1:1.2.3",
596
                            uses_local_alias=False,
597
                        ),
598
                    ),
599
                ),
600
            ]
601
        ),
602
    )
603

604
    assert_get_image_refs(
1✔
605
        rule_runner,
606
        Address("docker/test", target_name="alias2"),
607
        options=options,
608
        expected_refs=DockerImageRefs(
609
            [
610
                ImageRefRegistry(
611
                    registry=DockerRegistryOptions(
612
                        address="myregistry2domain:port", alias="reg2", default=True
613
                    ),
614
                    repository="alias2",
615
                    tags=(
616
                        ImageRefTag(
617
                            template="1.2.3",
618
                            formatted="1.2.3",
619
                            full_name="myregistry2domain:port/alias2:1.2.3",
620
                            uses_local_alias=False,
621
                        ),
622
                    ),
623
                ),
624
            ]
625
        ),
626
    )
627

628
    assert_get_image_refs(
1✔
629
        rule_runner,
630
        Address("docker/test", target_name="alias3"),
631
        options=options,
632
        expected_refs=DockerImageRefs(
633
            [
634
                ImageRefRegistry(
635
                    registry=DockerRegistryOptions(address="reg3"),
636
                    repository="alias3",
637
                    tags=(
638
                        ImageRefTag(
639
                            template="1.2.3",
640
                            formatted="1.2.3",
641
                            full_name="reg3/alias3:1.2.3",
642
                            uses_local_alias=False,
643
                        ),
644
                    ),
645
                ),
646
            ]
647
        ),
648
    )
649

650
    assert_get_image_refs(
1✔
651
        rule_runner,
652
        Address("docker/test", target_name="unreg"),
653
        options=options,
654
        expected_refs=DockerImageRefs(
655
            [
656
                ImageRefRegistry(
657
                    registry=None,
658
                    repository="unreg",
659
                    tags=(
660
                        ImageRefTag(
661
                            template="1.2.3",
662
                            formatted="1.2.3",
663
                            full_name="unreg:1.2.3",
664
                            uses_local_alias=False,
665
                        ),
666
                    ),
667
                ),
668
            ]
669
        ),
670
    )
671

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

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

773

774
def test_dynamic_image_version(rule_runner: RuleRunner) -> None:
1✔
775
    interpolation_context = InterpolationContext.from_dict(
1✔
776
        {
777
            "baseimage": {"tag": "3.8"},
778
            "stage0": {"tag": "3.8"},
779
            "interim": {"tag": "latest"},
780
            "stage2": {"tag": "latest"},
781
            "output": {"tag": "1-1"},
782
        }
783
    )
784

785
    def assert_tags(name: str, *expect_tags: str) -> None:
1✔
786
        tgt = rule_runner.get_target(Address("docker/test", target_name=name))
1✔
787
        fs = DockerPackageFieldSet.create(tgt)
1✔
788
        image_refs = fs.image_refs(
1✔
789
            "image",
790
            DockerRegistries.from_dict({}),
791
            interpolation_context,
792
        )
793
        tags = tuple(t.full_name for r in image_refs for t in r.tags)
1✔
794
        assert expect_tags == tags
1✔
795

796
    rule_runner.write_files(
1✔
797
        {
798
            "docker/test/BUILD": dedent(
799
                """\
800
                docker_image(name="ver_1")
801
                docker_image(
802
                  name="ver_2",
803
                  image_tags=["{baseimage.tag}-{stage2.tag}", "beta"]
804
                )
805
                docker_image(name="err_1", image_tags=["{unknown_stage}"])
806
                docker_image(name="err_2", image_tags=["{stage0.unknown_value}"])
807
                """
808
            ),
809
        }
810
    )
811

812
    assert_tags("ver_1", "image:latest")
1✔
813
    assert_tags("ver_2", "image:3.8-latest", "image:beta")
1✔
814

815
    err_1 = (
1✔
816
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
817
        r"docker/test:err_1: '{unknown_stage}'\.\n\n"
818
        r"The placeholder 'unknown_stage' is unknown\. Try with one of: baseimage, interim, "
819
        r"output, stage0, stage2\."
820
    )
821
    with pytest.raises(DockerImageTagValueError, match=err_1):
1✔
822
        assert_tags("err_1")
1✔
823

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

832

833
def test_docker_build_process_environment(rule_runner: RuleRunner) -> None:
1✔
834
    rule_runner.write_files(
1✔
835
        {"docker/test/BUILD": 'docker_image(name="env1", image_tags=["1.2.3"])'}
836
    )
837
    rule_runner.set_options(
1✔
838
        [],
839
        env={
840
            "INHERIT": "from Pants env",
841
            "PANTS_DOCKER_ENV_VARS": '["VAR=value", "INHERIT"]',
842
        },
843
    )
844

845
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
846
        assert result.process.argv == (
×
847
            "/dummy/docker",
848
            "build",
849
            "--pull=False",
850
            "--tag",
851
            "env1:1.2.3",
852
            "--file",
853
            "docker/test/Dockerfile",
854
            ".",
855
        )
UNCOV
856
        assert result.process.env == FrozenDict(
×
857
            {
858
                "INHERIT": "from Pants env",
859
                "VAR": "value",
860
                "__UPSTREAM_IMAGE_IDS": "",
861
            }
862
        )
863

864
    assert_build_process(
1✔
865
        rule_runner,
866
        Address("docker/test", target_name="env1"),
867
        build_process_assertions=check_build_process,
868
    )
869

870

871
def test_build_docker_image(rule_runner: RuleRunner) -> None:
1✔
872
    """Test build_docker_image rule orchestration and metadata creation."""
873
    rule_runner.write_files(
1✔
874
        {"docker/test/BUILD": 'docker_image(name="img1", image_tags=["1.2.3"])'}
875
    )
876

877
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
1✔
878
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
879
    metadata_file_path: list[str] = []
1✔
880
    metadata_file_contents: list[bytes] = []
1✔
881

882
    # Create mock DockerImageBuildProcess
883
    image_refs = DockerImageRefs(
1✔
884
        [
885
            ImageRefRegistry(
886
                registry=None,
887
                repository="img1",
888
                tags=(
889
                    ImageRefTag(
890
                        template="1.2.3",
891
                        formatted="1.2.3",
892
                        full_name="img1:1.2.3",
893
                        uses_local_alias=False,
894
                    ),
895
                ),
896
            )
897
        ]
898
    )
899

900
    process = Process(
1✔
901
        argv=(
902
            "/dummy/docker",
903
            "build",
904
            "--tag",
905
            "img1:1.2.3",
906
            "--pull=False",
907
            "--file",
908
            "docker/test/Dockerfile",
909
            ".",
910
        ),
911
        description="docker build",
912
        input_digest=EMPTY_DIGEST,
913
    )
914

915
    build_context = DockerBuildContext.create(
1✔
916
        snapshot=EMPTY_SNAPSHOT,
917
        upstream_image_ids=[],
918
        dockerfile_info=DockerfileInfo(
919
            tgt.address,
920
            digest=EMPTY_DIGEST,
921
            source="docker/test/Dockerfile",
922
        ),
923
        build_args=DockerBuildArgs(()),
924
        build_env=DockerBuildEnvironment.create({}),
925
    )
926

927
    mock_build_process = DockerImageBuildProcess(
1✔
928
        process=process,
929
        context=build_context,
930
        context_root=".",
931
        image_refs=image_refs,
932
        tags=("img1:1.2.3",),
933
    )
934

935
    # Mock get_docker_image_build_process to return our mock
936
    def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
1✔
UNCOV
937
        assert field_set == under_test_fs
×
UNCOV
938
        return mock_build_process
×
939

940
    # Mock execute_process to return success with image ID
941
    def mock_execute_process(_process: Process) -> FallibleProcessResult:
1✔
UNCOV
942
        return FallibleProcessResult(
×
943
            exit_code=0,
944
            stdout=b"Successfully built abc123\n",
945
            stderr=b"",
946
            stdout_digest=EMPTY_FILE_DIGEST,
947
            stderr_digest=EMPTY_FILE_DIGEST,
948
            output_digest=EMPTY_DIGEST,
949
            metadata=ProcessResultMetadata(
950
                0,
951
                ProcessExecutionEnvironment(
952
                    environment_name=None,
953
                    platform=Platform.create_for_localhost().value,
954
                    docker_image=None,
955
                    remote_execution=False,
956
                    remote_execution_extra_platform_properties=[],
957
                    execute_in_workspace=False,
958
                    keep_sandboxes="never",
959
                ),
960
                "ran_locally",
961
                0,
962
            ),
963
        )
964

965
    # Mock create_digest to capture metadata
966
    def mock_create_digest(request: CreateDigest) -> Digest:
1✔
UNCOV
967
        assert len(request) == 1
×
UNCOV
968
        assert isinstance(request[0], FileContent)
×
UNCOV
969
        metadata_file_path.append(request[0].path)
×
UNCOV
970
        metadata_file_contents.append(request[0].content)
×
UNCOV
971
        return EMPTY_DIGEST
×
972

973
    docker_options = _setup_docker_options(rule_runner, None)
1✔
974
    global_options = rule_runner.request(GlobalOptions, [])
1✔
975

976
    # Execute the rule
977
    result = run_rule_with_mocks(
1✔
978
        build_docker_image,
979
        rule_args=[
980
            under_test_fs,
981
            docker_options,
982
            global_options,
983
            DockerBinary("/dummy/docker"),
984
            KeepSandboxes.never,
985
        ],
986
        mock_calls={
987
            "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
988
            "pants.engine.intrinsics.execute_process": mock_execute_process,
989
            "pants.engine.intrinsics.create_digest": mock_create_digest,
990
        },
991
        show_warnings=False,
992
    )
993

994
    # Validate BuiltPackage result
UNCOV
995
    assert result.digest == EMPTY_DIGEST
×
UNCOV
996
    assert len(result.artifacts) == 1
×
UNCOV
997
    assert len(metadata_file_path) == 1
×
UNCOV
998
    assert result.artifacts[0].relpath == metadata_file_path[0]
×
999

1000
    # Validate metadata file content
UNCOV
1001
    metadata = json.loads(metadata_file_contents[0])
×
UNCOV
1002
    assert metadata["version"] == 1
×
UNCOV
1003
    assert metadata["image_id"] == "abc123"
×
UNCOV
1004
    assert isinstance(metadata["registries"], list)
×
UNCOV
1005
    assert len(metadata["registries"]) == 1
×
UNCOV
1006
    assert metadata["registries"][0]["repository"] == "img1"
×
UNCOV
1007
    assert metadata["registries"][0]["tags"][0]["tag"] == "1.2.3"
×
1008

1009

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

1013
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1014
        assert result.process.argv == (
×
1015
            "/dummy/docker",
1016
            "build",
1017
            "--pull=True",
1018
            "--tag",
1019
            "args1:latest",
1020
            "--file",
1021
            "docker/test/Dockerfile",
1022
            ".",
1023
        )
1024

1025
    assert_build_process(
1✔
1026
        rule_runner,
1027
        Address("docker/test", target_name="args1"),
1028
        build_process_assertions=check_build_process,
1029
    )
1030

1031

1032
def test_docker_build_squash(rule_runner: RuleRunner) -> None:
1✔
1033
    rule_runner.write_files(
1✔
1034
        {
1035
            "docker/test/BUILD": dedent(
1036
                """\
1037
            docker_image(name="args1", squash=True)
1038
            docker_image(name="args2", squash=False)
1039
            """
1040
            )
1041
        }
1042
    )
1043

1044
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1045
        assert result.process.argv == (
×
1046
            "/dummy/docker",
1047
            "build",
1048
            "--pull=False",
1049
            "--squash",
1050
            "--tag",
1051
            "args1:latest",
1052
            "--file",
1053
            "docker/test/Dockerfile",
1054
            ".",
1055
        )
1056

1057
    def check_build_process_no_squash(result: DockerImageBuildProcess):
1✔
UNCOV
1058
        assert result.process.argv == (
×
1059
            "/dummy/docker",
1060
            "build",
1061
            "--pull=False",
1062
            "--tag",
1063
            "args2:latest",
1064
            "--file",
1065
            "docker/test/Dockerfile",
1066
            ".",
1067
        )
1068

1069
    assert_build_process(
1✔
1070
        rule_runner,
1071
        Address("docker/test", target_name="args1"),
1072
        build_process_assertions=check_build_process,
1073
    )
UNCOV
1074
    assert_build_process(
×
1075
        rule_runner,
1076
        Address("docker/test", target_name="args2"),
1077
        build_process_assertions=check_build_process_no_squash,
1078
    )
1079

1080

1081
def test_docker_build_args(rule_runner: RuleRunner) -> None:
1✔
1082
    rule_runner.write_files(
1✔
1083
        {"docker/test/BUILD": 'docker_image(name="args1", image_tags=["1.2.3"])'}
1084
    )
1085
    rule_runner.set_options(
1✔
1086
        [],
1087
        env={
1088
            "INHERIT": "from Pants env",
1089
            "PANTS_DOCKER_BUILD_ARGS": '["VAR=value", "INHERIT"]',
1090
        },
1091
    )
1092

1093
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1094
        assert result.process.argv == (
×
1095
            "/dummy/docker",
1096
            "build",
1097
            "--pull=False",
1098
            "--tag",
1099
            "args1:1.2.3",
1100
            "--build-arg",
1101
            "INHERIT",
1102
            "--build-arg",
1103
            "VAR=value",
1104
            "--file",
1105
            "docker/test/Dockerfile",
1106
            ".",
1107
        )
1108

1109
        # Check that we pull in name only args via env.
UNCOV
1110
        assert result.process.env == FrozenDict(
×
1111
            {
1112
                "INHERIT": "from Pants env",
1113
                "__UPSTREAM_IMAGE_IDS": "",
1114
            }
1115
        )
1116

1117
    assert_build_process(
1✔
1118
        rule_runner,
1119
        Address("docker/test", target_name="args1"),
1120
        build_process_assertions=check_build_process,
1121
    )
1122

1123

1124
def test_docker_image_version_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
1125
    rule_runner.write_files(
1✔
1126
        {"docker/test/BUILD": 'docker_image(name="ver1", image_tags=["{build_args.VERSION}"])'}
1127
    )
1128
    rule_runner.set_options(
1✔
1129
        [],
1130
        env={
1131
            "PANTS_DOCKER_BUILD_ARGS": '["VERSION=1.2.3"]',
1132
        },
1133
    )
1134

1135
    refs = assert_get_image_refs(
1✔
1136
        rule_runner,
1137
        Address("docker/test", target_name="ver1"),
1138
    )
1139
    assert len(refs) == 1
1✔
1140
    assert refs[0].registry is None
1✔
1141
    assert refs[0].repository == "ver1"
1✔
1142
    assert len(refs[0].tags) == 1
1✔
1143
    assert refs[0].tags[0].template == "{build_args.VERSION}"
1✔
1144
    assert refs[0].tags[0].formatted == "1.2.3"
1✔
1145
    assert refs[0].tags[0].full_name == "ver1:1.2.3"
1✔
1146

1147

1148
def test_docker_repository_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
1149
    rule_runner.write_files(
1✔
1150
        {"docker/test/BUILD": 'docker_image(name="image", repository="{build_args.REPO}")'}
1151
    )
1152
    rule_runner.set_options(
1✔
1153
        [],
1154
        env={
1155
            "PANTS_DOCKER_BUILD_ARGS": '["REPO=test/image"]',
1156
        },
1157
    )
1158

1159
    refs = assert_get_image_refs(
1✔
1160
        rule_runner,
1161
        Address("docker/test", target_name="image"),
1162
    )
1163
    assert refs[0].repository == "test/image"
1✔
1164
    assert refs[0].tags[0].formatted == "latest"
1✔
1165
    assert refs[0].tags[0].full_name == "test/image:latest"
1✔
1166

1167

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

1196
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1197
        assert result.process.argv == (
×
1198
            "/dummy/docker",
1199
            "build",
1200
            "--pull=False",
1201
            "--tag",
1202
            "img1:latest",
1203
            "--build-arg",
1204
            "DEFAULT1=global1",
1205
            "--build-arg",
1206
            "DEFAULT2=overridden",
1207
            "--build-arg",
1208
            "FROM_ENV",
1209
            "--build-arg",
1210
            "SET=value",
1211
            "--file",
1212
            "docker/test/Dockerfile",
1213
            ".",
1214
        )
1215

UNCOV
1216
        assert result.process.env == FrozenDict(
×
1217
            {
1218
                "FROM_ENV": "env value",
1219
                "__UPSTREAM_IMAGE_IDS": "",
1220
            }
1221
        )
1222

1223
    assert_build_process(
1✔
1224
        rule_runner,
1225
        Address("docker/test", target_name="img1"),
1226
        build_process_assertions=check_build_process,
1227
    )
1228

1229

1230
def test_docker_build_secrets_option(rule_runner: RuleRunner) -> None:
1✔
1231
    rule_runner.write_files(
1✔
1232
        {
1233
            "docker/test/BUILD": dedent(
1234
                """\
1235
                docker_image(
1236
                  name="img1",
1237
                  secrets={
1238
                    "system-secret": "/var/run/secrets/mysecret",
1239
                    "project-secret": "secrets/mysecret",
1240
                    "target-secret": "./mysecret",
1241
                  }
1242
                )
1243
                """
1244
            ),
1245
        }
1246
    )
1247

1248
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1249
        assert result.process.argv == (
×
1250
            "/dummy/docker",
1251
            "build",
1252
            "--pull=False",
1253
            "--secret",
1254
            "id=system-secret,src=/var/run/secrets/mysecret",
1255
            "--secret",
1256
            f"id=project-secret,src={rule_runner.build_root}/secrets/mysecret",
1257
            "--secret",
1258
            f"id=target-secret,src={rule_runner.build_root}/docker/test/mysecret",
1259
            "--tag",
1260
            "img1:latest",
1261
            "--file",
1262
            "docker/test/Dockerfile",
1263
            ".",
1264
        )
1265

1266
    assert_build_process(
1✔
1267
        rule_runner,
1268
        Address("docker/test", target_name="img1"),
1269
        build_process_assertions=check_build_process,
1270
    )
1271

1272

1273
def test_docker_build_ssh_option(rule_runner: RuleRunner) -> None:
1✔
1274
    rule_runner.write_files(
1✔
1275
        {
1276
            "docker/test/BUILD": dedent(
1277
                """\
1278
                docker_image(
1279
                  name="img1",
1280
                  ssh=["default"],
1281
                )
1282
                """
1283
            ),
1284
        }
1285
    )
1286

1287
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1288
        assert result.process.argv == (
×
1289
            "/dummy/docker",
1290
            "build",
1291
            "--pull=False",
1292
            "--ssh",
1293
            "default",
1294
            "--tag",
1295
            "img1:latest",
1296
            "--file",
1297
            "docker/test/Dockerfile",
1298
            ".",
1299
        )
1300

1301
    assert_build_process(
1✔
1302
        rule_runner,
1303
        Address("docker/test", target_name="img1"),
1304
        build_process_assertions=check_build_process,
1305
    )
1306

1307

1308
def test_docker_build_no_cache_option(rule_runner: RuleRunner) -> None:
1✔
1309
    rule_runner.set_options(
1✔
1310
        [],
1311
        env={
1312
            "PANTS_DOCKER_BUILD_NO_CACHE": "true",
1313
        },
1314
    )
1315
    rule_runner.write_files(
1✔
1316
        {
1317
            "docker/test/BUILD": dedent(
1318
                """\
1319
                docker_image(
1320
                  name="img1",
1321
                )
1322
                """
1323
            ),
1324
        }
1325
    )
1326

1327
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1328
        assert result.process.argv == (
×
1329
            "/dummy/docker",
1330
            "build",
1331
            "--pull=False",
1332
            "--no-cache",
1333
            "--tag",
1334
            "img1:latest",
1335
            "--file",
1336
            "docker/test/Dockerfile",
1337
            ".",
1338
        )
1339

1340
    assert_build_process(
1✔
1341
        rule_runner,
1342
        Address("docker/test", target_name="img1"),
1343
        build_process_assertions=check_build_process,
1344
    )
1345

1346

1347
def test_docker_build_hosts_option(rule_runner: RuleRunner) -> None:
1✔
1348
    rule_runner.set_options(
1✔
1349
        [],
1350
        env={
1351
            "PANTS_DOCKER_BUILD_HOSTS": '{"global": "9.9.9.9"}',
1352
        },
1353
    )
1354
    rule_runner.write_files(
1✔
1355
        {
1356
            "docker/test/BUILD": dedent(
1357
                """\
1358
                docker_image(
1359
                  name="img1",
1360
                  extra_build_hosts={"docker": "10.180.0.1", "docker2": "10.180.0.2"},
1361
                )
1362
                """
1363
            ),
1364
        }
1365
    )
1366

1367
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1368
        assert result.process.argv == (
×
1369
            "/dummy/docker",
1370
            "build",
1371
            "--add-host",
1372
            "global:9.9.9.9",
1373
            "--add-host",
1374
            "docker:10.180.0.1",
1375
            "--add-host",
1376
            "docker2:10.180.0.2",
1377
            "--pull=False",
1378
            "--tag",
1379
            "img1:latest",
1380
            "--file",
1381
            "docker/test/Dockerfile",
1382
            ".",
1383
        )
1384

1385
    assert_build_process(
1✔
1386
        rule_runner,
1387
        Address("docker/test", target_name="img1"),
1388
        build_process_assertions=check_build_process,
1389
    )
1390

1391

1392
def test_docker_cache_to_option(rule_runner: RuleRunner) -> None:
1✔
1393
    rule_runner.write_files(
1✔
1394
        {
1395
            "docker/test/BUILD": dedent(
1396
                """\
1397
                docker_image(
1398
                  name="img1",
1399
                  cache_to={"type": "local", "dest": "/tmp/docker/pants-test-cache"},
1400
                )
1401
                """
1402
            ),
1403
        }
1404
    )
1405

1406
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1407
        assert result.process.argv == (
×
1408
            "/dummy/docker",
1409
            "buildx",
1410
            "build",
1411
            "--cache-to=type=local,dest=/tmp/docker/pants-test-cache",
1412
            "--output=type=docker",
1413
            "--pull=False",
1414
            "--tag",
1415
            "img1:latest",
1416
            "--file",
1417
            "docker/test/Dockerfile",
1418
            ".",
1419
        )
1420

1421
    assert_build_process(
1✔
1422
        rule_runner,
1423
        Address("docker/test", target_name="img1"),
1424
        build_process_assertions=check_build_process,
1425
        options=dict(use_buildx=True),
1426
    )
1427

1428

1429
def test_docker_cache_from_option(rule_runner: RuleRunner) -> None:
1✔
1430
    rule_runner.write_files(
1✔
1431
        {
1432
            "docker/test/BUILD": dedent(
1433
                """\
1434
                docker_image(
1435
                  name="img1",
1436
                  cache_from=[{"type": "local", "dest": "/tmp/docker/pants-test-cache1"}, {"type": "local", "dest": "/tmp/docker/pants-test-cache2"}],
1437
                )
1438
                """
1439
            ),
1440
        }
1441
    )
1442

1443
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1444
        assert result.process.argv == (
×
1445
            "/dummy/docker",
1446
            "buildx",
1447
            "build",
1448
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache1",
1449
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache2",
1450
            "--output=type=docker",
1451
            "--pull=False",
1452
            "--tag",
1453
            "img1:latest",
1454
            "--file",
1455
            "docker/test/Dockerfile",
1456
            ".",
1457
        )
1458

1459
    assert_build_process(
1✔
1460
        rule_runner,
1461
        Address("docker/test", target_name="img1"),
1462
        build_process_assertions=check_build_process,
1463
        options=dict(use_buildx=True),
1464
    )
1465

1466

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

1480
    Default output type 'docker' tested implicitly in other scenarios
1481
    """
1482
    output_str = f"output={repr(output)}," if output else ""
1✔
1483
    rule_runner.write_files(
1✔
1484
        {
1485
            "docker/test/BUILD": dedent(
1486
                f"""\
1487
                docker_image(
1488
                  name="img1",
1489
                  {output_str}
1490
                )
1491
                """
1492
            ),
1493
        }
1494
    )
1495

1496
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
UNCOV
1497
        assert result.process.argv == (
×
1498
            "/dummy/docker",
1499
            "buildx",
1500
            "build",
1501
            expected_output_arg,
1502
            "--pull=False",
1503
            "--tag",
1504
            "img1:latest",
1505
            "--file",
1506
            "docker/test/Dockerfile",
1507
            ".",
1508
        )
1509

1510
    assert_build_process(
1✔
1511
        rule_runner,
1512
        Address("docker/test", target_name="img1"),
1513
        build_process_assertions=check_build_process,
1514
        options=dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.ALLOW),
1515
    )
1516

1517

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

1546
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
1✔
1547
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
1548

1549
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
UNCOV
1550
        assert result.process.argv == (
×
1551
            "/dummy/docker",
1552
            "buildx",
1553
            "build",
1554
            expected_output_arg,
1555
            "--pull=False",
1556
            "--tag",
1557
            "img1:latest",
1558
            "--file",
1559
            "docker/test/Dockerfile",
1560
            ".",
1561
        )
1562

1563
    build_process = assert_build_process(
1✔
1564
        rule_runner,
1565
        Address("docker/test", target_name="img1"),
1566
        build_process_assertions=check_build_process if expected_output_arg else None,
1567
        options=dict(use_buildx=True),
1568
    )
1569

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

UNCOV
1595
    def mock_create_digest(request: CreateDigest) -> Digest:
×
UNCOV
1596
        return EMPTY_DIGEST
×
1597

UNCOV
1598
    def mock_get_build_process_success(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
×
UNCOV
1599
        assert field_set == under_test_fs
×
UNCOV
1600
        return build_process
×
1601

UNCOV
1602
    docker_options = _setup_docker_options(
×
1603
        rule_runner, dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.ERROR)
1604
    )
UNCOV
1605
    global_options = rule_runner.request(GlobalOptions, [])
×
1606

UNCOV
1607
    try:
×
UNCOV
1608
        run_rule_with_mocks(
×
1609
            build_docker_image,
1610
            rule_args=[
1611
                under_test_fs,
1612
                docker_options,
1613
                global_options,
1614
                DockerBinary("/dummy/docker"),
1615
                KeepSandboxes.never,
1616
            ],
1617
            mock_calls={
1618
                "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process_success,
1619
                "pants.engine.intrinsics.execute_process": mock_execute_process,
1620
                "pants.engine.intrinsics.create_digest": mock_create_digest,
1621
            },
1622
            show_warnings=False,
1623
        )
UNCOV
1624
    except DockerPushOnPackageException:
×
UNCOV
1625
        assert expect_error
×
1626
    else:
UNCOV
1627
        assert not expect_error
×
1628

1629

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

1667
    # Step 1: Validate Process construction using assert_build_process
1668
    def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
UNCOV
1669
        assert result.process.argv == (
×
1670
            "/dummy/docker",
1671
            "buildx",
1672
            "build",
1673
            expected_output_arg,
1674
            "--pull=False",
1675
            "--tag",
1676
            "img1:latest",
1677
            "--file",
1678
            "docker/test/Dockerfile",
1679
            ".",
1680
        )
1681

1682
    build_process = assert_build_process(
1✔
1683
        rule_runner,
1684
        Address("docker/test", target_name="img1"),
1685
        build_process_assertions=check_build_process,
1686
        options=dict(use_buildx=True),
1687
    )
1688

1689
    # Step 2: Test build_docker_image with WARN behavior
UNCOV
1690
    tgt = rule_runner.get_target(Address("docker/test", target_name="img1"))
×
UNCOV
1691
    under_test_fs = DockerPackageFieldSet.create(tgt)
×
1692

UNCOV
1693
    def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
×
UNCOV
1694
        assert field_set == under_test_fs
×
UNCOV
1695
        return build_process
×
1696

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

UNCOV
1721
    def mock_create_digest(_request: CreateDigest) -> Digest:
×
UNCOV
1722
        return EMPTY_DIGEST
×
1723

UNCOV
1724
    docker_options = _setup_docker_options(
×
1725
        rule_runner, dict(use_buildx=True, push_on_package=DockerPushOnPackageBehavior.WARN)
1726
    )
UNCOV
1727
    global_options = rule_runner.request(GlobalOptions, [])
×
1728

UNCOV
1729
    caplog.set_level(logging.WARNING)
×
1730

UNCOV
1731
    run_rule_with_mocks(
×
1732
        build_docker_image,
1733
        rule_args=[
1734
            under_test_fs,
1735
            docker_options,
1736
            global_options,
1737
            DockerBinary("/dummy/docker"),
1738
            KeepSandboxes.never,
1739
        ],
1740
        mock_calls={
1741
            "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
1742
            "pants.engine.intrinsics.execute_process": mock_execute_process,
1743
            "pants.engine.intrinsics.create_digest": mock_create_digest,
1744
        },
1745
        show_warnings=False,
1746
    )
1747

1748
    # Validate warning was logged
UNCOV
1749
    has_message = expected_message in [
×
1750
        record.message for record in caplog.records if record.levelno == logging.WARNING
1751
    ]
UNCOV
1752
    assert has_message is (expected_message is not None)
×
1753

1754

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

1786
    if expected_output_arg:
1✔
1787
        # Step 1: Validate Process construction using assert_build_process
1788
        def check_build_process(result: DockerImageBuildProcess) -> None:
1✔
UNCOV
1789
            assert result.process.argv == (
×
1790
                "/dummy/docker",
1791
                "buildx",
1792
                "build",
1793
                expected_output_arg,
1794
                "--pull=False",
1795
                "--tag",
1796
                "img1:latest",
1797
                "--file",
1798
                "docker/test/Dockerfile",
1799
                ".",
1800
            )
1801

1802
        build_process = assert_build_process(
1✔
1803
            rule_runner,
1804
            Address("docker/test", target_name="img1"),
1805
            build_process_assertions=check_build_process,
1806
            options=dict(use_buildx=True),
1807
        )
1808

UNCOV
1809
        def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
×
UNCOV
1810
            assert field_set == under_test_fs
×
UNCOV
1811
            return build_process
×
1812

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

UNCOV
1838
        def mock_create_digest(_request: CreateDigest) -> Digest:
×
UNCOV
1839
            return EMPTY_DIGEST
×
1840

UNCOV
1841
        mock_calls: dict[str, Callable[..., Any]] | None = {
×
1842
            "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
1843
            "pants.engine.intrinsics.execute_process": mock_execute_process,
1844
            "pants.engine.intrinsics.create_digest": mock_create_digest,
1845
        }
1846
    else:
1847
        mock_calls = None
1✔
1848

1849
    result = run_rule_with_mocks(
1✔
1850
        build_docker_image,
1851
        rule_args=[
1852
            under_test_fs,
1853
            docker_options,
1854
            global_options,
1855
            DockerBinary("/dummy/docker"),
1856
            KeepSandboxes.never,
1857
        ],
1858
        mock_calls=mock_calls,
1859
        show_warnings=False,
1860
    )
1861

UNCOV
1862
    assert result.digest == EMPTY_DIGEST
×
UNCOV
1863
    assert len(result.artifacts) == (1 if expected_output_arg else 0)
×
1864

1865

1866
def test_docker_output_option_raises_when_no_buildkit(rule_runner: RuleRunner) -> None:
1✔
1867
    rule_runner.write_files(
1✔
1868
        {
1869
            "docker/test/BUILD": dedent(
1870
                """\
1871
                docker_image(
1872
                  name="img1",
1873
                  output={"type": "image"}
1874
                )
1875
                """
1876
            ),
1877
        }
1878
    )
1879

1880
    with pytest.raises(
1✔
1881
        DockerImageOptionValueError,
1882
        match=r"Buildx must be enabled via the Docker subsystem options in order to use this field.",
1883
    ):
1884
        assert_build_process(
1✔
1885
            rule_runner,
1886
            Address("docker/test", target_name="img1"),
1887
        )
1888

1889

1890
def test_docker_build_network_option(rule_runner: RuleRunner) -> None:
1✔
1891
    rule_runner.write_files(
1✔
1892
        {
1893
            "docker/test/BUILD": dedent(
1894
                """\
1895
                docker_image(
1896
                  name="img1",
1897
                  build_network="host",
1898
                )
1899
                """
1900
            ),
1901
        }
1902
    )
1903

1904
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1905
        assert result.process.argv == (
×
1906
            "/dummy/docker",
1907
            "build",
1908
            "--network=host",
1909
            "--pull=False",
1910
            "--tag",
1911
            "img1:latest",
1912
            "--file",
1913
            "docker/test/Dockerfile",
1914
            ".",
1915
        )
1916

1917
    assert_build_process(
1✔
1918
        rule_runner,
1919
        Address("docker/test", target_name="img1"),
1920
        build_process_assertions=check_build_process,
1921
    )
1922

1923

1924
def test_docker_build_platform_option(rule_runner: RuleRunner) -> None:
1✔
1925
    rule_runner.write_files(
1✔
1926
        {
1927
            "docker/test/BUILD": dedent(
1928
                """\
1929
                docker_image(
1930
                  name="img1",
1931
                  build_platform=["linux/amd64", "linux/arm64", "linux/arm/v7"],
1932
                )
1933
                """
1934
            ),
1935
        }
1936
    )
1937

1938
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1939
        assert result.process.argv == (
×
1940
            "/dummy/docker",
1941
            "build",
1942
            "--platform=linux/amd64,linux/arm64,linux/arm/v7",
1943
            "--pull=False",
1944
            "--tag",
1945
            "img1:latest",
1946
            "--file",
1947
            "docker/test/Dockerfile",
1948
            ".",
1949
        )
1950

1951
    assert_build_process(
1✔
1952
        rule_runner,
1953
        Address("docker/test", target_name="img1"),
1954
        build_process_assertions=check_build_process,
1955
    )
1956

1957

1958
def test_docker_build_labels_option(rule_runner: RuleRunner) -> None:
1✔
1959
    rule_runner.write_files(
1✔
1960
        {
1961
            "docker/test/BUILD": dedent(
1962
                """\
1963
                docker_image(
1964
                  name="img1",
1965
                  extra_build_args=[
1966
                    "BUILD_SLAVE=tbs06",
1967
                    "BUILD_NUMBER=13934",
1968
                  ],
1969
                  image_labels={
1970
                    "build.host": "{build_args.BUILD_SLAVE}",
1971
                    "build.job": "{build_args.BUILD_NUMBER}",
1972
                  }
1973
                )
1974
                """
1975
            ),
1976
        }
1977
    )
1978

1979
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
1980
        assert result.process.argv == (
×
1981
            "/dummy/docker",
1982
            "build",
1983
            "--label",
1984
            "build.host=tbs06",
1985
            "--label",
1986
            "build.job=13934",
1987
            "--pull=False",
1988
            "--tag",
1989
            "img1:latest",
1990
            "--build-arg",
1991
            "BUILD_NUMBER=13934",
1992
            "--build-arg",
1993
            "BUILD_SLAVE=tbs06",
1994
            "--file",
1995
            "docker/test/Dockerfile",
1996
            ".",
1997
        )
1998

1999
    assert_build_process(
1✔
2000
        rule_runner,
2001
        Address("docker/test", target_name="img1"),
2002
        build_process_assertions=check_build_process,
2003
    )
2004

2005

2006
@pytest.mark.parametrize("suggest_renames", [True, False])
1✔
2007
@pytest.mark.parametrize(
1✔
2008
    "context_root, copy_sources, build_context_files, expect_logged, fail_log_contains",
2009
    [
2010
        (
2011
            None,
2012
            ("src/project/bin.pex",),
2013
            ("src.project/binary.pex", "src/project/app.py"),
2014
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2015
            [
2016
                "suggested renames:\n\n  * src/project/bin.pex => src.project/binary.pex\n\n",
2017
                "There are files in the Docker build context that were not referenced by ",
2018
                "  * src/project/app.py\n\n",
2019
            ],
2020
        ),
2021
        (
2022
            "./",
2023
            ("config.txt",),
2024
            ("docker/test/conf/config.txt",),
2025
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2026
            [
2027
                "suggested renames:\n\n  * config.txt => conf/config.txt\n\n",
2028
            ],
2029
        ),
2030
        (
2031
            "./",
2032
            ("conf/config.txt",),
2033
            (
2034
                "docker/test/conf/config.txt",
2035
                "src.project/binary.pex",
2036
            ),
2037
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2038
            [
2039
                "There are unreachable files in these directories, excluded from the build context "
2040
                "due to `context_root` being 'docker/test':\n\n"
2041
                "  * src.project\n\n"
2042
                "Suggested `context_root` setting is '' in order to include all files in the "
2043
                "build context, otherwise relocate the files to be part of the current "
2044
                "`context_root` 'docker/test'."
2045
            ],
2046
        ),
2047
        (
2048
            "./config",
2049
            (),
2050
            (
2051
                "docker/test/config/..unusal-name",
2052
                "docker/test/config/.rc",
2053
                "docker/test/config/.a",
2054
                "docker/test/config/.conf.d/b",
2055
            ),
2056
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
2057
            [
2058
                "There are files in the Docker build context that were not referenced by "
2059
                "any `COPY` instruction (this is not an error):\n"
2060
                "\n"
2061
                "  * ..unusal-name\n"
2062
                "  * .a\n"
2063
                "  * .conf.d/b\n"
2064
                "  * .rc\n"
2065
            ],
2066
        ),
2067
    ],
2068
)
2069
def test_docker_build_fail_logs(
1✔
2070
    rule_runner: RuleRunner,
2071
    caplog,
2072
    context_root: str | None,
2073
    copy_sources: tuple[str, ...],
2074
    build_context_files: tuple[str, ...],
2075
    expect_logged: list[tuple[int, str]] | None,
2076
    fail_log_contains: list[str],
2077
    suggest_renames: bool,
2078
) -> None:
2079
    caplog.set_level(logging.INFO)
1✔
2080
    rule_runner.write_files({"docker/test/BUILD": f"docker_image(context_root={context_root!r})"})
1✔
2081
    build_context_files = ("docker/test/Dockerfile", *build_context_files)
1✔
2082
    build_context_snapshot = rule_runner.make_snapshot_of_empty_files(build_context_files)
1✔
2083
    suggest_renames_arg = (
1✔
2084
        "--docker-suggest-renames" if suggest_renames else "--no-docker-suggest-renames"
2085
    )
2086
    rule_runner.set_options([suggest_renames_arg])
1✔
2087

2088
    # Step 1: Get the build process
2089
    tgt = rule_runner.get_target(Address("docker/test"))
1✔
2090
    address = Address("docker/test")
1✔
2091

2092
    build_context_mock = _create_build_context_mock(
1✔
2093
        rule_runner, address, build_context_snapshot, copy_sources, (), ()
2094
    )
2095
    docker_options = _setup_docker_options(rule_runner, None)
1✔
2096
    global_options = rule_runner.request(GlobalOptions, [])
1✔
2097

2098
    # Get image refs
2099
    repository = address.target_name
1✔
2100
    image_tags = tgt.get(DockerImageTagsField).value
1✔
2101
    tags_to_use = ("latest",) if image_tags is None else image_tags
1✔
2102
    image_refs = DockerImageRefs(
1✔
2103
        [
2104
            ImageRefRegistry(
2105
                registry=None,
2106
                repository=repository,
2107
                tags=tuple(
2108
                    ImageRefTag(
2109
                        template=tag,
2110
                        formatted=tag,
2111
                        full_name=f"{repository}:{tag}",
2112
                        uses_local_alias=False,
2113
                    )
2114
                    for tag in tags_to_use
2115
                ),
2116
            )
2117
        ]
2118
    )
2119

2120
    # Step 2: Create the build process with the get_docker_image_build_process rule
2121
    under_test_fs = DockerPackageFieldSet.create(tgt)
1✔
2122
    build_process = run_rule_with_mocks(
1✔
2123
        get_docker_image_build_process,
2124
        rule_args=[
2125
            under_test_fs,
2126
            docker_options,
2127
            DockerBinary("/dummy/docker"),
2128
        ],
2129
        mock_calls={
2130
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
2131
            "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt),
2132
            "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs,
2133
        },
2134
        show_warnings=False,
2135
    )
2136

2137
    # Step 3: Test that build_docker_image handles the failure properly
UNCOV
2138
    def mock_get_build_process(field_set: DockerPackageFieldSet) -> DockerImageBuildProcess:
×
UNCOV
2139
        assert field_set == under_test_fs
×
UNCOV
2140
        return build_process
×
2141

UNCOV
2142
    def mock_execute_process(_process: Process) -> FallibleProcessResult:
×
2143
        # Simulate Docker build failure
UNCOV
2144
        return FallibleProcessResult(
×
2145
            exit_code=1,
2146
            stdout=b"stdout",
2147
            stderr=b"stderr",
2148
            stdout_digest=EMPTY_FILE_DIGEST,
2149
            stderr_digest=EMPTY_FILE_DIGEST,
2150
            output_digest=EMPTY_DIGEST,
2151
            metadata=ProcessResultMetadata(
2152
                0,
2153
                ProcessExecutionEnvironment(
2154
                    environment_name=None,
2155
                    platform=Platform.create_for_localhost().value,
2156
                    docker_image=None,
2157
                    remote_execution=False,
2158
                    remote_execution_extra_platform_properties=[],
2159
                    execute_in_workspace=False,
2160
                    keep_sandboxes="never",
2161
                ),
2162
                "ran_locally",
2163
                0,
2164
            ),
2165
        )
2166

UNCOV
2167
    with pytest.raises(ProcessExecutionFailure):
×
UNCOV
2168
        run_rule_with_mocks(
×
2169
            build_docker_image,
2170
            rule_args=[
2171
                under_test_fs,
2172
                docker_options,
2173
                global_options,
2174
                DockerBinary("/dummy/docker"),
2175
                KeepSandboxes.never,
2176
            ],
2177
            mock_calls={
2178
                "pants.backend.docker.goals.package_image.get_docker_image_build_process": mock_get_build_process,
2179
                "pants.engine.intrinsics.execute_process": mock_execute_process,
2180
            },
2181
            show_warnings=False,
2182
        )
2183

UNCOV
2184
    assert_logged(caplog, expect_logged)
×
UNCOV
2185
    for msg in fail_log_contains:
×
UNCOV
2186
        if suggest_renames:
×
UNCOV
2187
            assert msg in caplog.records[0].message
×
2188
        else:
UNCOV
2189
            assert msg not in caplog.records[0].message
×
2190

2191

2192
@pytest.mark.parametrize(
1✔
2193
    "expected_target, options",
2194
    [
2195
        ("dev", None),
2196
        ("prod", {"build_target_stage": "prod", "default_repository": "{name}"}),
2197
    ],
2198
)
2199
def test_build_target_stage(
1✔
2200
    rule_runner: RuleRunner, options: dict | None, expected_target: str
2201
) -> None:
2202
    rule_runner.write_files(
1✔
2203
        {
2204
            "BUILD": "docker_image(name='image', target_stage='dev')",
2205
            "Dockerfile": dedent(
2206
                """\
2207
                FROM base as build
2208
                FROM build as dev
2209
                FROM build as prod
2210
                """
2211
            ),
2212
        }
2213
    )
2214

2215
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
2216
        assert result.process.argv == (
×
2217
            "/dummy/docker",
2218
            "build",
2219
            "--pull=False",
2220
            "--target",
2221
            expected_target,
2222
            "--tag",
2223
            "image:latest",
2224
            "--file",
2225
            "Dockerfile",
2226
            ".",
2227
        )
2228

2229
    assert_build_process(
1✔
2230
        rule_runner,
2231
        Address("", target_name="image"),
2232
        options=options,
2233
        build_process_assertions=check_build_process,
2234
        version_tags=("build latest", "dev latest", "prod latest"),
2235
    )
2236

2237

2238
def test_invalid_build_target_stage(rule_runner: RuleRunner) -> None:
1✔
2239
    rule_runner.write_files(
1✔
2240
        {
2241
            "BUILD": "docker_image(name='image', target_stage='bad')",
2242
            "Dockerfile": dedent(
2243
                """\
2244
                FROM base as build
2245
                FROM build as dev
2246
                FROM build as prod
2247
                """
2248
            ),
2249
        }
2250
    )
2251

2252
    err = (
1✔
2253
        r"The 'target_stage' field in `docker_image` //:image was set to 'bad', but there is no "
2254
        r"such stage in `Dockerfile`\. Available stages: build, dev, prod\."
2255
    )
2256
    with pytest.raises(DockerBuildTargetStageError, match=err):
1✔
2257
        assert_build_process(
1✔
2258
            rule_runner,
2259
            Address("", target_name="image"),
2260
            version_tags=("build latest", "dev latest", "prod latest"),
2261
        )
2262

2263

2264
@pytest.mark.parametrize(
1✔
2265
    "default_context_root, context_root, expected_context_root",
2266
    [
2267
        ("", None, "."),
2268
        (".", None, "."),
2269
        ("src", None, "src"),
2270
        (
2271
            "/",
2272
            None,
2273
            pytest.raises(
2274
                InvalidFieldException,
2275
                match=r"Use '' for a path relative to the build root, or '\./' for",
2276
            ),
2277
        ),
2278
        (
2279
            "/src",
2280
            None,
2281
            pytest.raises(
2282
                InvalidFieldException,
2283
                match=(
2284
                    r"The `context_root` field in target src/docker:image must be a relative path, "
2285
                    r"but was '/src'\. Use 'src' for a path relative to the build root, or '\./src' "
2286
                    r"for a path relative to the BUILD file \(i\.e\. 'src/docker/src'\)\."
2287
                ),
2288
            ),
2289
        ),
2290
        ("./", None, "src/docker"),
2291
        ("./build/context/", None, "src/docker/build/context"),
2292
        (".build/context/", None, ".build/context"),
2293
        ("ignored", "", "."),
2294
        ("ignored", ".", "."),
2295
        ("ignored", "src/context/", "src/context"),
2296
        ("ignored", "./", "src/docker"),
2297
        ("ignored", "src", "src"),
2298
        ("ignored", "./build/context", "src/docker/build/context"),
2299
    ],
2300
)
2301
def test_get_context_root(
1✔
2302
    context_root: str | None, default_context_root: str, expected_context_root: str | ContextManager
2303
) -> None:
2304
    if isinstance(expected_context_root, str):
1✔
2305
        raises = cast("ContextManager", no_exception())
1✔
2306
    else:
2307
        raises = expected_context_root
1✔
2308

2309
    with raises:
1✔
2310
        docker_options = create_subsystem(
1✔
2311
            DockerOptions,
2312
            default_context_root=default_context_root,
2313
        )
2314
        address = Address("src/docker", target_name="image")
1✔
2315
        tgt = DockerImageTarget({"context_root": context_root}, address)
1✔
2316
        fs = DockerPackageFieldSet.create(tgt)
1✔
2317
        actual_context_root = fs.get_context_root(docker_options.default_context_root)
1✔
2318
        assert actual_context_root == expected_context_root
1✔
2319

2320

2321
@pytest.mark.parametrize(
1✔
2322
    "docker, expected, stdout, stderr",
2323
    [
2324
        (
2325
            DockerBinary("/bin/docker", "1234"),
2326
            "<unknown>",
2327
            "",
2328
            "",
2329
        ),
2330
        # Docker
2331
        (
2332
            DockerBinary("/bin/docker", "1234"),
2333
            "0e09b442b572",
2334
            "",
2335
            dedent(
2336
                """\
2337
                Step 22/22 : LABEL job-url="https://jenkins.example.net/job/python_artefactsapi_pipeline/"
2338
                 ---> Running in ae5c3eac5c0b
2339
                Removing intermediate container ae5c3eac5c0b
2340
                 ---> 0e09b442b572
2341
                Successfully built 0e09b442b572
2342
                Successfully tagged docker.example.net/artefactsapi/master:3.6.5
2343
                """
2344
            ),
2345
        ),
2346
        # Buildkit without step duration
2347
        (
2348
            DockerBinary("/bin/docker", "1234"),
2349
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
2350
            dedent(
2351
                """\
2352
                #7 [2/2] COPY testprojects.src.python.hello.main/main.pex /hello
2353
                #7 sha256:843d0c804a7eb5ba08b0535b635d5f98a3e56bc43a3fbe7d226a8024176f00d1
2354
                #7 DONE 0.1s
2355

2356
                #8 exporting to image
2357
                #8 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
2358
                #8 exporting layers 0.0s done
2359
                #8 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 done
2360
                #8 naming to docker.io/library/test-example-synth:1.2.5 done
2361
                #8 DONE 0.0s
2362

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

2365
                """
2366
            ),
2367
            "",
2368
        ),
2369
        # Buildkit with step duration
2370
        (
2371
            DockerBinary("/bin/docker", "1234"),
2372
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
2373
            dedent(
2374
                """\
2375
                #5 [2/2] RUN sleep 1
2376
                #5 DONE 1.1s
2377

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

2468
                    """
2469
            ),
2470
            "",
2471
        ),
2472
        # Podman
2473
        (
2474
            PodmanBinary("/bin/podman", "abcd"),
2475
            "a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee",
2476
            dedent(
2477
                """\
2478
                STEP 5/5: COPY ./ .
2479
                COMMIT example
2480
                --> a85499e9039a
2481
                Successfully tagged localhost/example:latest
2482
                a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee
2483
                """
2484
            ),
2485
            "",
2486
        ),
2487
    ],
2488
)
2489
def test_parse_image_id_from_docker_build_output(
1✔
2490
    docker: DockerBinary, expected: str, stdout: str, stderr: str
2491
) -> None:
2492
    assert expected == parse_image_id_from_buildkit_output(stdout.encode(), stderr.encode())
1✔
2493

2494

2495
ImageRefTest = namedtuple(
1✔
2496
    "ImageRefTest",
2497
    "docker_image, registries, default_repository, expect_refs, expect_error",
2498
    defaults=({}, {}, "{name}", (), None),
2499
)
2500

2501

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

2775

2776
@pytest.mark.parametrize(
1✔
2777
    "BUILD, plugin_tags, tag_flags",
2778
    [
2779
        (
2780
            'docker_image(name="plugin")',
2781
            ("1.2.3",),
2782
            (
2783
                "--tag",
2784
                "plugin:latest",
2785
                "--tag",
2786
                "plugin:1.2.3",
2787
            ),
2788
        ),
2789
        (
2790
            'docker_image(name="plugin", image_tags=[])',
2791
            ("1.2.3",),
2792
            (
2793
                "--tag",
2794
                "plugin:1.2.3",
2795
            ),
2796
        ),
2797
    ],
2798
)
2799
def test_docker_image_tags_from_plugin_hook(
1✔
2800
    rule_runner: RuleRunner, BUILD: str, plugin_tags: tuple[str, ...], tag_flags: tuple[str, ...]
2801
) -> None:
2802
    rule_runner.write_files({"docker/test/BUILD": BUILD})
1✔
2803

2804
    refs = assert_get_image_refs(
1✔
2805
        rule_runner,
2806
        Address("docker/test", target_name="plugin"),
2807
        plugin_tags=plugin_tags,
2808
    )
2809

2810
    def check_build_process(result: DockerImageBuildProcess):
1✔
UNCOV
2811
        assert result.process.argv == (
×
2812
            "/dummy/docker",
2813
            "build",
2814
            "--pull=False",
2815
            *tag_flags,
2816
            "--file",
2817
            "docker/test/Dockerfile",
2818
            ".",
2819
        )
2820

2821
    assert_build_process(
1✔
2822
        rule_runner,
2823
        Address("docker/test", target_name="plugin"),
2824
        build_process_assertions=check_build_process,
2825
        image_refs=refs,
2826
    )
2827

2828

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

2832
    err = "The `image_tags` field in target docker/test:no-tags must not be empty, unless"
1✔
2833
    with pytest.raises(InvalidFieldException, match=err):
1✔
2834
        assert_build_process(
1✔
2835
            rule_runner,
2836
            Address("docker/test", target_name="no-tags"),
2837
        )
2838

2839

2840
def test_docker_info_serialize() -> None:
1✔
2841
    image_id = "abc123"
1✔
2842
    # image refs with unique strings (i.e. not actual templates/names etc.), to make sure they're
2843
    # ending up in the right place in the JSON
2844
    image_refs = (
1✔
2845
        ImageRefRegistry(
2846
            registry=None,
2847
            repository="repo",
2848
            tags=(
2849
                ImageRefTag(
2850
                    template="repo tag1 template",
2851
                    formatted="repo tag1 formatted",
2852
                    uses_local_alias=False,
2853
                    full_name="repo tag1 full name",
2854
                ),
2855
                ImageRefTag(
2856
                    template="repo tag2 template",
2857
                    formatted="repo tag2 formatted",
2858
                    uses_local_alias=False,
2859
                    full_name="repo tag2 full name",
2860
                ),
2861
            ),
2862
        ),
2863
        ImageRefRegistry(
2864
            registry=DockerRegistryOptions(address="address"),
2865
            repository="address repo",
2866
            tags=(
2867
                ImageRefTag(
2868
                    template="address tag template",
2869
                    formatted="address tag formatted",
2870
                    uses_local_alias=False,
2871
                    full_name="address tag full name",
2872
                ),
2873
            ),
2874
        ),
2875
        ImageRefRegistry(
2876
            registry=DockerRegistryOptions(
2877
                address="alias address", alias="alias", repository="alias registry repo"
2878
            ),
2879
            repository="alias repo",
2880
            tags=(
2881
                ImageRefTag(
2882
                    template="alias tag (address) template",
2883
                    formatted="alias tag (address) formatted",
2884
                    uses_local_alias=False,
2885
                    full_name="alias tag (address) full name",
2886
                ),
2887
                ImageRefTag(
2888
                    template="alias tag (local alias) template",
2889
                    formatted="alias tag (local alias) formatted",
2890
                    uses_local_alias=True,
2891
                    full_name="alias tag (local alias) full name",
2892
                ),
2893
            ),
2894
        ),
2895
    )
2896

2897
    expected = dict(
1✔
2898
        version=1,
2899
        image_id=image_id,
2900
        registries=[
2901
            dict(
2902
                alias=None,
2903
                address=None,
2904
                repository="repo",
2905
                tags=[
2906
                    dict(
2907
                        template="repo tag1 template",
2908
                        tag="repo tag1 formatted",
2909
                        uses_local_alias=False,
2910
                        name="repo tag1 full name",
2911
                    ),
2912
                    dict(
2913
                        template="repo tag2 template",
2914
                        tag="repo tag2 formatted",
2915
                        uses_local_alias=False,
2916
                        name="repo tag2 full name",
2917
                    ),
2918
                ],
2919
            ),
2920
            dict(
2921
                alias=None,
2922
                address="address",
2923
                repository="address repo",
2924
                tags=[
2925
                    dict(
2926
                        template="address tag template",
2927
                        tag="address tag formatted",
2928
                        uses_local_alias=False,
2929
                        name="address tag full name",
2930
                    )
2931
                ],
2932
            ),
2933
            dict(
2934
                alias="alias",
2935
                address="alias address",
2936
                repository="alias repo",
2937
                tags=[
2938
                    dict(
2939
                        template="alias tag (address) template",
2940
                        tag="alias tag (address) formatted",
2941
                        uses_local_alias=False,
2942
                        name="alias tag (address) full name",
2943
                    ),
2944
                    dict(
2945
                        template="alias tag (local alias) template",
2946
                        tag="alias tag (local alias) formatted",
2947
                        uses_local_alias=True,
2948
                        name="alias tag (local alias) full name",
2949
                    ),
2950
                ],
2951
            ),
2952
        ],
2953
    )
2954

2955
    result = DockerInfoV1.serialize(image_refs, image_id)
1✔
2956
    assert json.loads(result) == expected
1✔
2957

2958

2959
@pytest.mark.parametrize(
1✔
2960
    ("output", "expected"),
2961
    [({"type": "image", "push": "true"}, True), ({"type": "registry"}, True), (None, False)],
2962
)
2963
def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> None:
1✔
2964
    rule_runner = RuleRunner(target_types=[DockerImageTarget])
1✔
2965
    output_str = f", output={output}" if output else ""
1✔
2966
    rule_runner.write_files(
1✔
2967
        {"BUILD": f"docker_image(name='image', source='Dockerfile'{output_str})"}
2968
    )
2969
    field_set = DockerPackageFieldSet.create(
1✔
2970
        rule_runner.get_target(Address("", target_name="image"))
2971
    )
2972
    assert field_set.pushes_on_package() is expected
1✔
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