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

pantsbuild / pants / 21457998286

28 Jan 2026 10:32PM UTC coverage: 80.281% (+0.01%) from 80.269%
21457998286

Pull #23037

github

web-flow
Merge 43b38d939 into 0fdb40370
Pull Request #23037: Enable publish without package 2

275 of 328 new or added lines in 13 files covered. (83.84%)

46 existing lines in 9 files now uncovered.

78960 of 98355 relevant lines covered (80.28%)

3.36 hits per line

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

99.7
/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 ContextManager, cast
1✔
13

14
import pytest
1✔
15

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

84

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

100

101
class DockerImageTagsRequestPlugin(DockerImageTagsRequest):
1✔
102
    pass
1✔
103

104

105
def _create_build_context_mock(
1✔
106
    rule_runner: RuleRunner,
107
    address: Address,
108
    build_context_snapshot: Snapshot,
109
    copy_sources: tuple[str, ...],
110
    copy_build_args,
111
    version_tags: tuple[str, ...],
112
):
113
    """Create a mock function for create_docker_build_context."""
114
    tgt = rule_runner.get_target(address)
1✔
115

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

134
    return build_context_mock
1✔
135

136

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

157

158
def _create_union_membership() -> UnionMembership:
1✔
159
    """Create union membership for Docker image tags plugin."""
160
    return UnionMembership.from_rules(
1✔
161
        [UnionRule(DockerImageTagsRequest, DockerImageTagsRequestPlugin)]
162
    )
163

164

165
def assert_build(
1✔
166
    rule_runner: RuleRunner,
167
    address: Address,
168
    *extra_log_lines: str,
169
    options: dict | None = None,
170
    process_assertions: Callable[[Process], None] | None = None,
171
    exit_code: int = 0,
172
    copy_sources: tuple[str, ...] = (),
173
    copy_build_args=(),
174
    build_context_snapshot: Snapshot = EMPTY_SNAPSHOT,
175
    version_tags: tuple[str, ...] = (),
176
    expected_registries_metadata: None | list = None,
177
    image_refs: DockerImageRefs | None = None,
178
) -> None:
179
    tgt = rule_runner.get_target(address)
1✔
180
    metadata_file_path: list[str] = []
1✔
181
    metadata_file_contents: list[bytes] = []
1✔
182

183
    if image_refs is None:
1✔
184
        repository = address.target_name
1✔
185
        image_tags = tgt.get(DockerImageTagsField).value
1✔
186
        tags_to_use = ("latest",) if image_tags is None else image_tags
1✔
187
        image_refs = DockerImageRefs(
1✔
188
            [
189
                ImageRefRegistry(
190
                    registry=None,
191
                    repository=repository,
192
                    tags=tuple(
193
                        ImageRefTag(
194
                            template=tag,
195
                            formatted=tag,
196
                            full_name=f"{repository}:{tag}",
197
                            uses_local_alias=False,
198
                        )
199
                        for tag in tags_to_use
200
                    ),
201
                )
202
            ]
203
        )
204

205
    build_context_mock = _create_build_context_mock(
1✔
206
        rule_runner, address, build_context_snapshot, copy_sources, copy_build_args, version_tags
207
    )
208
    docker_options = _setup_docker_options(rule_runner, options)
1✔
209
    global_options = rule_runner.request(GlobalOptions, [])
1✔
210

211
    def run_process_mock(process: Process) -> FallibleProcessResult:
1✔
212
        if process_assertions:
1✔
213
            process_assertions(process)
1✔
214

215
        return FallibleProcessResult(
1✔
216
            exit_code=exit_code,
217
            stdout=b"stdout",
218
            stdout_digest=EMPTY_FILE_DIGEST,
219
            stderr=b"stderr",
220
            stderr_digest=EMPTY_FILE_DIGEST,
221
            output_digest=EMPTY_DIGEST,
222
            metadata=ProcessResultMetadata(
223
                0,
224
                ProcessExecutionEnvironment(
225
                    environment_name=None,
226
                    platform=Platform.create_for_localhost().value,
227
                    docker_image=None,
228
                    remote_execution=False,
229
                    remote_execution_extra_platform_properties=[],
230
                    execute_in_workspace=False,
231
                    keep_sandboxes="never",
232
                ),
233
                "ran_locally",
234
                0,
235
            ),
236
        )
237

238
    def mock_get_info_file(request: CreateDigest) -> Digest:
1✔
239
        assert len(request) == 1
1✔
240
        assert isinstance(request[0], FileContent)
1✔
241
        metadata_file_path.append(request[0].path)
1✔
242
        metadata_file_contents.append(request[0].content)
1✔
243
        return EMPTY_DIGEST
1✔
244

245
    union_membership = _create_union_membership()
1✔
246
    result = run_rule_with_mocks(
1✔
247
        build_docker_image,
248
        rule_args=[
249
            DockerPackageFieldSet.create(tgt),
250
            docker_options,
251
            global_options,
252
            DockerBinary("/dummy/docker"),
253
            KeepSandboxes.never,
254
        ],
255
        mock_calls={
256
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
257
            "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt),
258
            "pants.backend.docker.goals.package_image.get_image_refs": lambda _: image_refs,
259
            "pants.engine.intrinsics.execute_process": run_process_mock,
260
            "pants.engine.intrinsics.create_digest": mock_get_info_file,
261
        },
262
        union_membership=union_membership,
263
        show_warnings=False,
264
    )
265

266
    assert result.digest == EMPTY_DIGEST
1✔
267
    assert len(result.artifacts) == 1
1✔
268
    assert len(metadata_file_path) == len(metadata_file_contents) == 1
1✔
269
    assert result.artifacts[0].relpath == metadata_file_path[0]
1✔
270

271
    metadata = json.loads(metadata_file_contents[0])
1✔
272
    assert metadata["version"] == 1
1✔
273
    assert metadata["image_id"] == "<unknown>"
1✔
274
    assert isinstance(metadata["registries"], list)
1✔
275
    if expected_registries_metadata is not None:
1✔
276
        assert metadata["registries"] == expected_registries_metadata
1✔
277

278
    for log_line in extra_log_lines:
1✔
UNCOV
279
        assert log_line in result.artifacts[0].extra_log_lines
×
280

281

282
def assert_get_image_refs(
1✔
283
    rule_runner: RuleRunner,
284
    address: Address,
285
    *,
286
    options: dict | None = None,
287
    expected_refs: DockerImageRefs | None = None,
288
    version_tags: tuple[str, ...] = (),
289
    plugin_tags: tuple[str, ...] = (),
290
    copy_sources: tuple[str, ...] = (),
291
    copy_build_args=(),
292
    build_context_snapshot: Snapshot = EMPTY_SNAPSHOT,
293
    build_upstream_images: bool = True,
294
) -> DockerImageRefs:
295
    """Test helper for get_image_refs rule.
296

297
    Returns DockerImageRefs for validation. Optionally asserts against expected_refs.
298
    """
299
    tgt = rule_runner.get_target(address)
1✔
300

301
    build_context_mock = _create_build_context_mock(
1✔
302
        rule_runner, address, build_context_snapshot, copy_sources, copy_build_args, version_tags
303
    )
304
    docker_options = _setup_docker_options(rule_runner, options)
1✔
305
    union_membership = _create_union_membership()
1✔
306

307
    field_set = DockerPackageFieldSet.create(tgt)
1✔
308
    result = run_rule_with_mocks(
1✔
309
        get_image_refs,
310
        rule_args=[
311
            GetImageRefsRequest(
312
                field_set=field_set,
313
                build_upstream_images=build_upstream_images,
314
            ),
315
            docker_options,
316
            union_membership,
317
        ],
318
        mock_calls={
319
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
320
            "pants.engine.internals.graph.resolve_target": lambda *_, **__: WrappedTarget(tgt),
321
            "pants.backend.docker.target_types.get_docker_image_tags": lambda *_,
322
            **__: DockerImageTags(plugin_tags),
323
        },
324
        union_membership=union_membership,
325
    )
326

327
    if expected_refs is not None:
1✔
328
        assert result == expected_refs
1✔
329

330
    return result
1✔
331

332

333
def test_get_image_refs(rule_runner: RuleRunner) -> None:
1✔
334
    rule_runner.write_files(
1✔
335
        {
336
            "docker/test/BUILD": dedent(
337
                """\
338

339
                docker_image(
340
                  name="test1",
341
                  image_tags=["1.2.3"],
342
                  repository="{directory}/{name}",
343
                )
344
                docker_image(
345
                  name="test2",
346
                  image_tags=["1.2.3"],
347
                )
348
                docker_image(
349
                  name="test3",
350
                  image_tags=["1.2.3"],
351
                  repository="{parent_directory}/{directory}/{name}",
352
                )
353
                docker_image(
354
                  name="test4",
355
                  image_tags=["1.2.3"],
356
                  repository="{directory}/four/test-four",
357
                )
358
                docker_image(
359
                  name="test5",
360
                  image_tags=["latest", "alpha-1.0", "alpha-1"],
361
                )
362
                docker_image(
363
                  name="test6",
364
                  image_tags=["1.2.3"],
365
                  repository="xyz/{full_directory}/{name}",
366
                )
367
                docker_image(
368
                  name="err1",
369
                  repository="{bad_template}",
370
                )
371
                """
372
            ),
373
            "docker/test/Dockerfile": "FROM python:3.8",
374
        }
375
    )
376

377
    assert_get_image_refs(
1✔
378
        rule_runner,
379
        Address("docker/test", target_name="test1"),
380
        expected_refs=DockerImageRefs(
381
            [
382
                ImageRefRegistry(
383
                    registry=None,
384
                    repository="test/test1",
385
                    tags=(
386
                        ImageRefTag(
387
                            template="1.2.3",
388
                            formatted="1.2.3",
389
                            full_name="test/test1:1.2.3",
390
                            uses_local_alias=False,
391
                        ),
392
                    ),
393
                ),
394
            ]
395
        ),
396
    )
397
    assert_get_image_refs(
1✔
398
        rule_runner,
399
        Address("docker/test", target_name="test2"),
400
        expected_refs=DockerImageRefs(
401
            [
402
                ImageRefRegistry(
403
                    registry=None,
404
                    repository="test2",
405
                    tags=(
406
                        ImageRefTag(
407
                            template="1.2.3",
408
                            formatted="1.2.3",
409
                            full_name="test2:1.2.3",
410
                            uses_local_alias=False,
411
                        ),
412
                    ),
413
                ),
414
            ]
415
        ),
416
    )
417
    assert_get_image_refs(
1✔
418
        rule_runner,
419
        Address("docker/test", target_name="test3"),
420
        expected_refs=DockerImageRefs(
421
            [
422
                ImageRefRegistry(
423
                    registry=None,
424
                    repository="docker/test/test3",
425
                    tags=(
426
                        ImageRefTag(
427
                            template="1.2.3",
428
                            formatted="1.2.3",
429
                            full_name="docker/test/test3:1.2.3",
430
                            uses_local_alias=False,
431
                        ),
432
                    ),
433
                ),
434
            ]
435
        ),
436
    )
437

438
    assert_get_image_refs(
1✔
439
        rule_runner,
440
        Address("docker/test", target_name="test4"),
441
        expected_refs=DockerImageRefs(
442
            [
443
                ImageRefRegistry(
444
                    registry=None,
445
                    repository="test/four/test-four",
446
                    tags=(
447
                        ImageRefTag(
448
                            template="1.2.3",
449
                            formatted="1.2.3",
450
                            full_name="test/four/test-four:1.2.3",
451
                            uses_local_alias=False,
452
                        ),
453
                    ),
454
                ),
455
            ]
456
        ),
457
    )
458

459
    assert_get_image_refs(
1✔
460
        rule_runner,
461
        Address("docker/test", target_name="test5"),
462
        options=dict(default_repository="{directory}/{name}"),
463
        expected_refs=DockerImageRefs(
464
            [
465
                ImageRefRegistry(
466
                    registry=None,
467
                    repository="test/test5",
468
                    tags=(
469
                        ImageRefTag(
470
                            template="latest",
471
                            formatted="latest",
472
                            full_name="test/test5:latest",
473
                            uses_local_alias=False,
474
                        ),
475
                        ImageRefTag(
476
                            template="alpha-1.0",
477
                            formatted="alpha-1.0",
478
                            full_name="test/test5:alpha-1.0",
479
                            uses_local_alias=False,
480
                        ),
481
                        ImageRefTag(
482
                            template="alpha-1",
483
                            formatted="alpha-1",
484
                            full_name="test/test5:alpha-1",
485
                            uses_local_alias=False,
486
                        ),
487
                    ),
488
                ),
489
            ]
490
        ),
491
    )
492

493
    assert_get_image_refs(
1✔
494
        rule_runner,
495
        Address("docker/test", target_name="test6"),
496
        expected_refs=DockerImageRefs(
497
            [
498
                ImageRefRegistry(
499
                    registry=None,
500
                    repository="xyz/docker/test/test6",
501
                    tags=(
502
                        ImageRefTag(
503
                            template="1.2.3",
504
                            formatted="1.2.3",
505
                            full_name="xyz/docker/test/test6:1.2.3",
506
                            uses_local_alias=False,
507
                        ),
508
                    ),
509
                ),
510
            ]
511
        ),
512
    )
513

514
    err1 = (
1✔
515
        r"Invalid value for the `repository` field of the `docker_image` target at "
516
        r"docker/test:err1: '{bad_template}'\.\n\nThe placeholder 'bad_template' is unknown\. "
517
        r"Try with one of: build_args, default_repository, directory, full_directory, name, "
518
        r"pants, parent_directory, tags, target_repository\."
519
    )
520
    with pytest.raises(DockerRepositoryNameError, match=err1):
1✔
521
        assert_get_image_refs(
1✔
522
            rule_runner,
523
            Address("docker/test", target_name="err1"),
524
        )
525

526

527
def test_get_image_refs_with_registries(rule_runner: RuleRunner) -> None:
1✔
528
    rule_runner.write_files(
1✔
529
        {
530
            "docker/test/BUILD": dedent(
531
                """\
532
                docker_image(name="addr1", image_tags=["1.2.3"], registries=["myregistry1domain:port"])
533
                docker_image(name="addr2", image_tags=["1.2.3"], registries=["myregistry2domain:port"])
534
                docker_image(name="addr3", image_tags=["1.2.3"], registries=["myregistry3domain:port"])
535
                docker_image(name="alias1", image_tags=["1.2.3"], registries=["@reg1"])
536
                docker_image(name="alias2", image_tags=["1.2.3"], registries=["@reg2"])
537
                docker_image(name="alias3", image_tags=["1.2.3"], registries=["reg3"])
538
                docker_image(name="unreg", image_tags=["1.2.3"], registries=[])
539
                docker_image(name="def", image_tags=["1.2.3"])
540
                docker_image(name="multi", image_tags=["1.2.3"], registries=["@reg2", "@reg1"])
541
                docker_image(name="extra_tags", image_tags=["1.2.3"], registries=["@reg1", "@extra"])
542
                """
543
            ),
544
            "docker/test/Dockerfile": "FROM python:3.8",
545
        }
546
    )
547

548
    options = {
1✔
549
        "default_repository": "{name}",
550
        "registries": {
551
            "reg1": {"address": "myregistry1domain:port"},
552
            "reg2": {"address": "myregistry2domain:port", "default": True},
553
            "extra": {"address": "extra", "extra_image_tags": ["latest"]},
554
        },
555
    }
556

557
    assert_get_image_refs(
1✔
558
        rule_runner,
559
        Address("docker/test", target_name="addr1"),
560
        options=options,
561
        expected_refs=DockerImageRefs(
562
            [
563
                ImageRefRegistry(
564
                    registry=DockerRegistryOptions(address="myregistry1domain:port", alias="reg1"),
565
                    repository="addr1",
566
                    tags=(
567
                        ImageRefTag(
568
                            template="1.2.3",
569
                            formatted="1.2.3",
570
                            full_name="myregistry1domain:port/addr1:1.2.3",
571
                            uses_local_alias=False,
572
                        ),
573
                    ),
574
                ),
575
            ]
576
        ),
577
    )
578
    assert_get_image_refs(
1✔
579
        rule_runner,
580
        Address("docker/test", target_name="addr2"),
581
        options=options,
582
        expected_refs=DockerImageRefs(
583
            [
584
                ImageRefRegistry(
585
                    registry=DockerRegistryOptions(
586
                        address="myregistry2domain:port", alias="reg2", default=True
587
                    ),
588
                    repository="addr2",
589
                    tags=(
590
                        ImageRefTag(
591
                            template="1.2.3",
592
                            formatted="1.2.3",
593
                            full_name="myregistry2domain:port/addr2:1.2.3",
594
                            uses_local_alias=False,
595
                        ),
596
                    ),
597
                ),
598
            ]
599
        ),
600
    )
601

602
    assert_get_image_refs(
1✔
603
        rule_runner,
604
        Address("docker/test", target_name="addr3"),
605
        options=options,
606
        expected_refs=DockerImageRefs(
607
            [
608
                ImageRefRegistry(
609
                    registry=DockerRegistryOptions(address="myregistry3domain:port"),
610
                    repository="addr3",
611
                    tags=(
612
                        ImageRefTag(
613
                            template="1.2.3",
614
                            formatted="1.2.3",
615
                            full_name="myregistry3domain:port/addr3:1.2.3",
616
                            uses_local_alias=False,
617
                        ),
618
                    ),
619
                ),
620
            ]
621
        ),
622
    )
623

624
    assert_get_image_refs(
1✔
625
        rule_runner,
626
        Address("docker/test", target_name="alias1"),
627
        options=options,
628
        expected_refs=DockerImageRefs(
629
            [
630
                ImageRefRegistry(
631
                    registry=DockerRegistryOptions(alias="reg1", address="myregistry1domain:port"),
632
                    repository="alias1",
633
                    tags=(
634
                        ImageRefTag(
635
                            template="1.2.3",
636
                            formatted="1.2.3",
637
                            full_name="myregistry1domain:port/alias1:1.2.3",
638
                            uses_local_alias=False,
639
                        ),
640
                    ),
641
                ),
642
            ]
643
        ),
644
    )
645

646
    assert_get_image_refs(
1✔
647
        rule_runner,
648
        Address("docker/test", target_name="alias2"),
649
        options=options,
650
        expected_refs=DockerImageRefs(
651
            [
652
                ImageRefRegistry(
653
                    registry=DockerRegistryOptions(
654
                        address="myregistry2domain:port", alias="reg2", default=True
655
                    ),
656
                    repository="alias2",
657
                    tags=(
658
                        ImageRefTag(
659
                            template="1.2.3",
660
                            formatted="1.2.3",
661
                            full_name="myregistry2domain:port/alias2:1.2.3",
662
                            uses_local_alias=False,
663
                        ),
664
                    ),
665
                ),
666
            ]
667
        ),
668
    )
669

670
    assert_get_image_refs(
1✔
671
        rule_runner,
672
        Address("docker/test", target_name="alias3"),
673
        options=options,
674
        expected_refs=DockerImageRefs(
675
            [
676
                ImageRefRegistry(
677
                    registry=DockerRegistryOptions(address="reg3"),
678
                    repository="alias3",
679
                    tags=(
680
                        ImageRefTag(
681
                            template="1.2.3",
682
                            formatted="1.2.3",
683
                            full_name="reg3/alias3:1.2.3",
684
                            uses_local_alias=False,
685
                        ),
686
                    ),
687
                ),
688
            ]
689
        ),
690
    )
691

692
    assert_get_image_refs(
1✔
693
        rule_runner,
694
        Address("docker/test", target_name="unreg"),
695
        options=options,
696
        expected_refs=DockerImageRefs(
697
            [
698
                ImageRefRegistry(
699
                    registry=None,
700
                    repository="unreg",
701
                    tags=(
702
                        ImageRefTag(
703
                            template="1.2.3",
704
                            formatted="1.2.3",
705
                            full_name="unreg:1.2.3",
706
                            uses_local_alias=False,
707
                        ),
708
                    ),
709
                ),
710
            ]
711
        ),
712
    )
713

714
    assert_get_image_refs(
1✔
715
        rule_runner,
716
        Address("docker/test", target_name="def"),
717
        options=options,
718
        expected_refs=DockerImageRefs(
719
            [
720
                ImageRefRegistry(
721
                    registry=DockerRegistryOptions(
722
                        address="myregistry2domain:port", alias="reg2", default=True
723
                    ),
724
                    repository="def",
725
                    tags=(
726
                        ImageRefTag(
727
                            template="1.2.3",
728
                            formatted="1.2.3",
729
                            full_name="myregistry2domain:port/def:1.2.3",
730
                            uses_local_alias=False,
731
                        ),
732
                    ),
733
                ),
734
            ]
735
        ),
736
    )
737
    assert_get_image_refs(
1✔
738
        rule_runner,
739
        Address("docker/test", target_name="multi"),
740
        options=options,
741
        expected_refs=DockerImageRefs(
742
            [
743
                ImageRefRegistry(
744
                    registry=DockerRegistryOptions(
745
                        address="myregistry2domain:port", alias="reg2", default=True
746
                    ),
747
                    repository="multi",
748
                    tags=(
749
                        ImageRefTag(
750
                            template="1.2.3",
751
                            formatted="1.2.3",
752
                            full_name="myregistry2domain:port/multi:1.2.3",
753
                            uses_local_alias=False,
754
                        ),
755
                    ),
756
                ),
757
                ImageRefRegistry(
758
                    registry=DockerRegistryOptions(alias="reg1", address="myregistry1domain:port"),
759
                    repository="multi",
760
                    tags=(
761
                        ImageRefTag(
762
                            template="1.2.3",
763
                            formatted="1.2.3",
764
                            full_name="myregistry1domain:port/multi:1.2.3",
765
                            uses_local_alias=False,
766
                        ),
767
                    ),
768
                ),
769
            ]
770
        ),
771
    )
772

773
    assert_get_image_refs(
1✔
774
        rule_runner,
775
        Address("docker/test", target_name="extra_tags"),
776
        options=options,
777
        expected_refs=DockerImageRefs(
778
            [
779
                ImageRefRegistry(
780
                    registry=DockerRegistryOptions(address="myregistry1domain:port", alias="reg1"),
781
                    repository="extra_tags",
782
                    tags=(
783
                        ImageRefTag(
784
                            template="1.2.3",
785
                            formatted="1.2.3",
786
                            full_name="myregistry1domain:port/extra_tags:1.2.3",
787
                            uses_local_alias=False,
788
                        ),
789
                    ),
790
                ),
791
                ImageRefRegistry(
792
                    registry=DockerRegistryOptions(
793
                        alias="extra", address="extra", extra_image_tags=("latest",)
794
                    ),
795
                    repository="extra_tags",
796
                    tags=(
797
                        ImageRefTag(
798
                            template="1.2.3",
799
                            formatted="1.2.3",
800
                            full_name="extra/extra_tags:1.2.3",
801
                            uses_local_alias=False,
802
                        ),
803
                        ImageRefTag(
804
                            template="latest",
805
                            formatted="latest",
806
                            full_name="extra/extra_tags:latest",
807
                            uses_local_alias=False,
808
                        ),
809
                    ),
810
                ),
811
            ]
812
        ),
813
    )
814

815

816
def test_dynamic_image_version(rule_runner: RuleRunner) -> None:
1✔
817
    interpolation_context = InterpolationContext.from_dict(
1✔
818
        {
819
            "baseimage": {"tag": "3.8"},
820
            "stage0": {"tag": "3.8"},
821
            "interim": {"tag": "latest"},
822
            "stage2": {"tag": "latest"},
823
            "output": {"tag": "1-1"},
824
        }
825
    )
826

827
    def assert_tags(name: str, *expect_tags: str) -> None:
1✔
828
        tgt = rule_runner.get_target(Address("docker/test", target_name=name))
1✔
829
        fs = DockerPackageFieldSet.create(tgt)
1✔
830
        image_refs = fs.image_refs(
1✔
831
            "image",
832
            DockerRegistries.from_dict({}),
833
            interpolation_context,
834
        )
835
        tags = tuple(t.full_name for r in image_refs for t in r.tags)
1✔
836
        assert expect_tags == tags
1✔
837

838
    rule_runner.write_files(
1✔
839
        {
840
            "docker/test/BUILD": dedent(
841
                """\
842
                docker_image(name="ver_1")
843
                docker_image(
844
                  name="ver_2",
845
                  image_tags=["{baseimage.tag}-{stage2.tag}", "beta"]
846
                )
847
                docker_image(name="err_1", image_tags=["{unknown_stage}"])
848
                docker_image(name="err_2", image_tags=["{stage0.unknown_value}"])
849
                """
850
            ),
851
        }
852
    )
853

854
    assert_tags("ver_1", "image:latest")
1✔
855
    assert_tags("ver_2", "image:3.8-latest", "image:beta")
1✔
856

857
    err_1 = (
1✔
858
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
859
        r"docker/test:err_1: '{unknown_stage}'\.\n\n"
860
        r"The placeholder 'unknown_stage' is unknown\. Try with one of: baseimage, interim, "
861
        r"output, stage0, stage2\."
862
    )
863
    with pytest.raises(DockerImageTagValueError, match=err_1):
1✔
864
        assert_tags("err_1")
1✔
865

866
    err_2 = (
1✔
867
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
868
        r"docker/test:err_2: '{stage0.unknown_value}'\.\n\n"
869
        r"The placeholder 'unknown_value' is unknown\. Try with one of: tag\."
870
    )
871
    with pytest.raises(DockerImageTagValueError, match=err_2):
1✔
872
        assert_tags("err_2")
1✔
873

874

875
def test_docker_build_process_environment(rule_runner: RuleRunner) -> None:
1✔
876
    rule_runner.write_files(
1✔
877
        {"docker/test/BUILD": 'docker_image(name="env1", image_tags=["1.2.3"])'}
878
    )
879
    rule_runner.set_options(
1✔
880
        [],
881
        env={
882
            "INHERIT": "from Pants env",
883
            "PANTS_DOCKER_ENV_VARS": '["VAR=value", "INHERIT"]',
884
        },
885
    )
886

887
    def check_docker_proc(process: Process):
1✔
888
        assert process.argv == (
1✔
889
            "/dummy/docker",
890
            "build",
891
            "--pull=False",
892
            "--tag",
893
            "env1:1.2.3",
894
            "--file",
895
            "docker/test/Dockerfile",
896
            ".",
897
        )
898
        assert process.env == FrozenDict(
1✔
899
            {
900
                "INHERIT": "from Pants env",
901
                "VAR": "value",
902
                "__UPSTREAM_IMAGE_IDS": "",
903
            }
904
        )
905

906
    assert_build(
1✔
907
        rule_runner,
908
        Address("docker/test", target_name="env1"),
909
        process_assertions=check_docker_proc,
910
    )
911

912

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

916
    def check_docker_proc(process: Process):
1✔
917
        assert process.argv == (
1✔
918
            "/dummy/docker",
919
            "build",
920
            "--pull=True",
921
            "--tag",
922
            "args1:latest",
923
            "--file",
924
            "docker/test/Dockerfile",
925
            ".",
926
        )
927

928
    assert_build(
1✔
929
        rule_runner,
930
        Address("docker/test", target_name="args1"),
931
        process_assertions=check_docker_proc,
932
    )
933

934

935
def test_docker_build_squash(rule_runner: RuleRunner) -> None:
1✔
936
    rule_runner.write_files(
1✔
937
        {
938
            "docker/test/BUILD": dedent(
939
                """\
940
            docker_image(name="args1", squash=True)
941
            docker_image(name="args2", squash=False)
942
            """
943
            )
944
        }
945
    )
946

947
    def check_docker_proc(process: Process):
1✔
948
        assert process.argv == (
1✔
949
            "/dummy/docker",
950
            "build",
951
            "--pull=False",
952
            "--squash",
953
            "--tag",
954
            "args1:latest",
955
            "--file",
956
            "docker/test/Dockerfile",
957
            ".",
958
        )
959

960
    def check_docker_proc_no_squash(process: Process):
1✔
961
        assert process.argv == (
1✔
962
            "/dummy/docker",
963
            "build",
964
            "--pull=False",
965
            "--tag",
966
            "args2:latest",
967
            "--file",
968
            "docker/test/Dockerfile",
969
            ".",
970
        )
971

972
    assert_build(
1✔
973
        rule_runner,
974
        Address("docker/test", target_name="args1"),
975
        process_assertions=check_docker_proc,
976
    )
977
    assert_build(
1✔
978
        rule_runner,
979
        Address("docker/test", target_name="args2"),
980
        process_assertions=check_docker_proc_no_squash,
981
    )
982

983

984
def test_docker_build_args(rule_runner: RuleRunner) -> None:
1✔
985
    rule_runner.write_files(
1✔
986
        {"docker/test/BUILD": 'docker_image(name="args1", image_tags=["1.2.3"])'}
987
    )
988
    rule_runner.set_options(
1✔
989
        [],
990
        env={
991
            "INHERIT": "from Pants env",
992
            "PANTS_DOCKER_BUILD_ARGS": '["VAR=value", "INHERIT"]',
993
        },
994
    )
995

996
    def check_docker_proc(process: Process):
1✔
997
        assert process.argv == (
1✔
998
            "/dummy/docker",
999
            "build",
1000
            "--pull=False",
1001
            "--tag",
1002
            "args1:1.2.3",
1003
            "--build-arg",
1004
            "INHERIT",
1005
            "--build-arg",
1006
            "VAR=value",
1007
            "--file",
1008
            "docker/test/Dockerfile",
1009
            ".",
1010
        )
1011

1012
        # Check that we pull in name only args via env.
1013
        assert process.env == FrozenDict(
1✔
1014
            {
1015
                "INHERIT": "from Pants env",
1016
                "__UPSTREAM_IMAGE_IDS": "",
1017
            }
1018
        )
1019

1020
    assert_build(
1✔
1021
        rule_runner,
1022
        Address("docker/test", target_name="args1"),
1023
        process_assertions=check_docker_proc,
1024
    )
1025

1026

1027
def test_docker_image_version_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
1028
    rule_runner.write_files(
1✔
1029
        {"docker/test/BUILD": 'docker_image(name="ver1", image_tags=["{build_args.VERSION}"])'}
1030
    )
1031
    rule_runner.set_options(
1✔
1032
        [],
1033
        env={
1034
            "PANTS_DOCKER_BUILD_ARGS": '["VERSION=1.2.3"]',
1035
        },
1036
    )
1037

1038
    refs = assert_get_image_refs(
1✔
1039
        rule_runner,
1040
        Address("docker/test", target_name="ver1"),
1041
    )
1042
    assert len(refs) == 1
1✔
1043
    assert refs[0].registry is None
1✔
1044
    assert refs[0].repository == "ver1"
1✔
1045
    assert len(refs[0].tags) == 1
1✔
1046
    assert refs[0].tags[0].template == "{build_args.VERSION}"
1✔
1047
    assert refs[0].tags[0].formatted == "1.2.3"
1✔
1048
    assert refs[0].tags[0].full_name == "ver1:1.2.3"
1✔
1049

1050

1051
def test_docker_repository_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
1052
    rule_runner.write_files(
1✔
1053
        {"docker/test/BUILD": 'docker_image(name="image", repository="{build_args.REPO}")'}
1054
    )
1055
    rule_runner.set_options(
1✔
1056
        [],
1057
        env={
1058
            "PANTS_DOCKER_BUILD_ARGS": '["REPO=test/image"]',
1059
        },
1060
    )
1061

1062
    refs = assert_get_image_refs(
1✔
1063
        rule_runner,
1064
        Address("docker/test", target_name="image"),
1065
    )
1066
    assert refs[0].repository == "test/image"
1✔
1067
    assert refs[0].tags[0].formatted == "latest"
1✔
1068
    assert refs[0].tags[0].full_name == "test/image:latest"
1✔
1069

1070

1071
def test_docker_extra_build_args_field(rule_runner: RuleRunner) -> None:
1✔
1072
    rule_runner.write_files(
1✔
1073
        {
1074
            "docker/test/BUILD": dedent(
1075
                """\
1076
                docker_image(
1077
                  name="img1",
1078
                  extra_build_args=[
1079
                    "FROM_ENV",
1080
                    "SET=value",
1081
                    "DEFAULT2=overridden",
1082
                  ]
1083
                )
1084
                """
1085
            ),
1086
        }
1087
    )
1088
    rule_runner.set_options(
1✔
1089
        [
1090
            "--docker-build-args=DEFAULT1=global1",
1091
            "--docker-build-args=DEFAULT2=global2",
1092
        ],
1093
        env={
1094
            "FROM_ENV": "env value",
1095
            "SET": "no care",
1096
        },
1097
    )
1098

1099
    def check_docker_proc(process: Process):
1✔
1100
        assert process.argv == (
1✔
1101
            "/dummy/docker",
1102
            "build",
1103
            "--pull=False",
1104
            "--tag",
1105
            "img1:latest",
1106
            "--build-arg",
1107
            "DEFAULT1=global1",
1108
            "--build-arg",
1109
            "DEFAULT2=overridden",
1110
            "--build-arg",
1111
            "FROM_ENV",
1112
            "--build-arg",
1113
            "SET=value",
1114
            "--file",
1115
            "docker/test/Dockerfile",
1116
            ".",
1117
        )
1118

1119
        assert process.env == FrozenDict(
1✔
1120
            {
1121
                "FROM_ENV": "env value",
1122
                "__UPSTREAM_IMAGE_IDS": "",
1123
            }
1124
        )
1125

1126
    assert_build(
1✔
1127
        rule_runner,
1128
        Address("docker/test", target_name="img1"),
1129
        process_assertions=check_docker_proc,
1130
    )
1131

1132

1133
def test_docker_build_secrets_option(rule_runner: RuleRunner) -> None:
1✔
1134
    rule_runner.write_files(
1✔
1135
        {
1136
            "docker/test/BUILD": dedent(
1137
                """\
1138
                docker_image(
1139
                  name="img1",
1140
                  secrets={
1141
                    "system-secret": "/var/run/secrets/mysecret",
1142
                    "project-secret": "secrets/mysecret",
1143
                    "target-secret": "./mysecret",
1144
                  }
1145
                )
1146
                """
1147
            ),
1148
        }
1149
    )
1150

1151
    def check_docker_proc(process: Process):
1✔
1152
        assert process.argv == (
1✔
1153
            "/dummy/docker",
1154
            "build",
1155
            "--pull=False",
1156
            "--secret",
1157
            "id=system-secret,src=/var/run/secrets/mysecret",
1158
            "--secret",
1159
            f"id=project-secret,src={rule_runner.build_root}/secrets/mysecret",
1160
            "--secret",
1161
            f"id=target-secret,src={rule_runner.build_root}/docker/test/mysecret",
1162
            "--tag",
1163
            "img1:latest",
1164
            "--file",
1165
            "docker/test/Dockerfile",
1166
            ".",
1167
        )
1168

1169
    assert_build(
1✔
1170
        rule_runner,
1171
        Address("docker/test", target_name="img1"),
1172
        process_assertions=check_docker_proc,
1173
    )
1174

1175

1176
def test_docker_build_ssh_option(rule_runner: RuleRunner) -> None:
1✔
1177
    rule_runner.write_files(
1✔
1178
        {
1179
            "docker/test/BUILD": dedent(
1180
                """\
1181
                docker_image(
1182
                  name="img1",
1183
                  ssh=["default"],
1184
                )
1185
                """
1186
            ),
1187
        }
1188
    )
1189

1190
    def check_docker_proc(process: Process):
1✔
1191
        assert process.argv == (
1✔
1192
            "/dummy/docker",
1193
            "build",
1194
            "--pull=False",
1195
            "--ssh",
1196
            "default",
1197
            "--tag",
1198
            "img1:latest",
1199
            "--file",
1200
            "docker/test/Dockerfile",
1201
            ".",
1202
        )
1203

1204
    assert_build(
1✔
1205
        rule_runner,
1206
        Address("docker/test", target_name="img1"),
1207
        process_assertions=check_docker_proc,
1208
    )
1209

1210

1211
def test_docker_build_no_cache_option(rule_runner: RuleRunner) -> None:
1✔
1212
    rule_runner.set_options(
1✔
1213
        [],
1214
        env={
1215
            "PANTS_DOCKER_BUILD_NO_CACHE": "true",
1216
        },
1217
    )
1218
    rule_runner.write_files(
1✔
1219
        {
1220
            "docker/test/BUILD": dedent(
1221
                """\
1222
                docker_image(
1223
                  name="img1",
1224
                )
1225
                """
1226
            ),
1227
        }
1228
    )
1229

1230
    def check_docker_proc(process: Process):
1✔
1231
        assert process.argv == (
1✔
1232
            "/dummy/docker",
1233
            "build",
1234
            "--pull=False",
1235
            "--no-cache",
1236
            "--tag",
1237
            "img1:latest",
1238
            "--file",
1239
            "docker/test/Dockerfile",
1240
            ".",
1241
        )
1242

1243
    assert_build(
1✔
1244
        rule_runner,
1245
        Address("docker/test", target_name="img1"),
1246
        process_assertions=check_docker_proc,
1247
    )
1248

1249

1250
def test_docker_build_hosts_option(rule_runner: RuleRunner) -> None:
1✔
1251
    rule_runner.set_options(
1✔
1252
        [],
1253
        env={
1254
            "PANTS_DOCKER_BUILD_HOSTS": '{"global": "9.9.9.9"}',
1255
        },
1256
    )
1257
    rule_runner.write_files(
1✔
1258
        {
1259
            "docker/test/BUILD": dedent(
1260
                """\
1261
                docker_image(
1262
                  name="img1",
1263
                  extra_build_hosts={"docker": "10.180.0.1", "docker2": "10.180.0.2"},
1264
                )
1265
                """
1266
            ),
1267
        }
1268
    )
1269

1270
    def check_docker_proc(process: Process):
1✔
1271
        assert process.argv == (
1✔
1272
            "/dummy/docker",
1273
            "build",
1274
            "--add-host",
1275
            "global:9.9.9.9",
1276
            "--add-host",
1277
            "docker:10.180.0.1",
1278
            "--add-host",
1279
            "docker2:10.180.0.2",
1280
            "--pull=False",
1281
            "--tag",
1282
            "img1:latest",
1283
            "--file",
1284
            "docker/test/Dockerfile",
1285
            ".",
1286
        )
1287

1288
    assert_build(
1✔
1289
        rule_runner,
1290
        Address("docker/test", target_name="img1"),
1291
        process_assertions=check_docker_proc,
1292
    )
1293

1294

1295
def test_docker_cache_to_option(rule_runner: RuleRunner) -> None:
1✔
1296
    rule_runner.write_files(
1✔
1297
        {
1298
            "docker/test/BUILD": dedent(
1299
                """\
1300
                docker_image(
1301
                  name="img1",
1302
                  cache_to={"type": "local", "dest": "/tmp/docker/pants-test-cache"},
1303
                )
1304
                """
1305
            ),
1306
        }
1307
    )
1308

1309
    def check_docker_proc(process: Process):
1✔
1310
        assert process.argv == (
1✔
1311
            "/dummy/docker",
1312
            "buildx",
1313
            "build",
1314
            "--cache-to=type=local,dest=/tmp/docker/pants-test-cache",
1315
            "--output=type=docker",
1316
            "--pull=False",
1317
            "--tag",
1318
            "img1:latest",
1319
            "--file",
1320
            "docker/test/Dockerfile",
1321
            ".",
1322
        )
1323

1324
    assert_build(
1✔
1325
        rule_runner,
1326
        Address("docker/test", target_name="img1"),
1327
        process_assertions=check_docker_proc,
1328
        options=dict(use_buildx=True),
1329
    )
1330

1331

1332
def test_docker_cache_from_option(rule_runner: RuleRunner) -> None:
1✔
1333
    rule_runner.write_files(
1✔
1334
        {
1335
            "docker/test/BUILD": dedent(
1336
                """\
1337
                docker_image(
1338
                  name="img1",
1339
                  cache_from=[{"type": "local", "dest": "/tmp/docker/pants-test-cache1"}, {"type": "local", "dest": "/tmp/docker/pants-test-cache2"}],
1340
                )
1341
                """
1342
            ),
1343
        }
1344
    )
1345

1346
    def check_docker_proc(process: Process):
1✔
1347
        assert process.argv == (
1✔
1348
            "/dummy/docker",
1349
            "buildx",
1350
            "build",
1351
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache1",
1352
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache2",
1353
            "--output=type=docker",
1354
            "--pull=False",
1355
            "--tag",
1356
            "img1:latest",
1357
            "--file",
1358
            "docker/test/Dockerfile",
1359
            ".",
1360
        )
1361

1362
    assert_build(
1✔
1363
        rule_runner,
1364
        Address("docker/test", target_name="img1"),
1365
        process_assertions=check_docker_proc,
1366
        options=dict(use_buildx=True),
1367
    )
1368

1369

1370
def test_docker_output_option(rule_runner: RuleRunner) -> None:
1✔
1371
    """Testing non-default output type 'image'.
1372

1373
    Default output type 'docker' tested implicitly in other scenarios
1374
    """
1375
    rule_runner.write_files(
1✔
1376
        {
1377
            "docker/test/BUILD": dedent(
1378
                """\
1379
                docker_image(
1380
                  name="img1",
1381
                  output={"type": "image"}
1382
                )
1383
                """
1384
            ),
1385
        }
1386
    )
1387

1388
    def check_docker_proc(process: Process):
1✔
1389
        assert process.argv == (
1✔
1390
            "/dummy/docker",
1391
            "buildx",
1392
            "build",
1393
            "--output=type=image",
1394
            "--pull=False",
1395
            "--tag",
1396
            "img1:latest",
1397
            "--file",
1398
            "docker/test/Dockerfile",
1399
            ".",
1400
        )
1401

1402
    assert_build(
1✔
1403
        rule_runner,
1404
        Address("docker/test", target_name="img1"),
1405
        process_assertions=check_docker_proc,
1406
        options=dict(use_buildx=True),
1407
    )
1408

1409

1410
def test_docker_output_option_raises_when_no_buildkit(rule_runner: RuleRunner) -> None:
1✔
1411
    rule_runner.write_files(
1✔
1412
        {
1413
            "docker/test/BUILD": dedent(
1414
                """\
1415
                docker_image(
1416
                  name="img1",
1417
                  output={"type": "image"}
1418
                )
1419
                """
1420
            ),
1421
        }
1422
    )
1423

1424
    with pytest.raises(
1✔
1425
        DockerImageOptionValueError,
1426
        match=r"Buildx must be enabled via the Docker subsystem options in order to use this field.",
1427
    ):
1428
        assert_build(
1✔
1429
            rule_runner,
1430
            Address("docker/test", target_name="img1"),
1431
        )
1432

1433

1434
def test_docker_build_network_option(rule_runner: RuleRunner) -> None:
1✔
1435
    rule_runner.write_files(
1✔
1436
        {
1437
            "docker/test/BUILD": dedent(
1438
                """\
1439
                docker_image(
1440
                  name="img1",
1441
                  build_network="host",
1442
                )
1443
                """
1444
            ),
1445
        }
1446
    )
1447

1448
    def check_docker_proc(process: Process):
1✔
1449
        assert process.argv == (
1✔
1450
            "/dummy/docker",
1451
            "build",
1452
            "--network=host",
1453
            "--pull=False",
1454
            "--tag",
1455
            "img1:latest",
1456
            "--file",
1457
            "docker/test/Dockerfile",
1458
            ".",
1459
        )
1460

1461
    assert_build(
1✔
1462
        rule_runner,
1463
        Address("docker/test", target_name="img1"),
1464
        process_assertions=check_docker_proc,
1465
    )
1466

1467

1468
def test_docker_build_platform_option(rule_runner: RuleRunner) -> None:
1✔
1469
    rule_runner.write_files(
1✔
1470
        {
1471
            "docker/test/BUILD": dedent(
1472
                """\
1473
                docker_image(
1474
                  name="img1",
1475
                  build_platform=["linux/amd64", "linux/arm64", "linux/arm/v7"],
1476
                )
1477
                """
1478
            ),
1479
        }
1480
    )
1481

1482
    def check_docker_proc(process: Process):
1✔
1483
        assert process.argv == (
1✔
1484
            "/dummy/docker",
1485
            "build",
1486
            "--platform=linux/amd64,linux/arm64,linux/arm/v7",
1487
            "--pull=False",
1488
            "--tag",
1489
            "img1:latest",
1490
            "--file",
1491
            "docker/test/Dockerfile",
1492
            ".",
1493
        )
1494

1495
    assert_build(
1✔
1496
        rule_runner,
1497
        Address("docker/test", target_name="img1"),
1498
        process_assertions=check_docker_proc,
1499
    )
1500

1501

1502
def test_docker_build_labels_option(rule_runner: RuleRunner) -> None:
1✔
1503
    rule_runner.write_files(
1✔
1504
        {
1505
            "docker/test/BUILD": dedent(
1506
                """\
1507
                docker_image(
1508
                  name="img1",
1509
                  extra_build_args=[
1510
                    "BUILD_SLAVE=tbs06",
1511
                    "BUILD_NUMBER=13934",
1512
                  ],
1513
                  image_labels={
1514
                    "build.host": "{build_args.BUILD_SLAVE}",
1515
                    "build.job": "{build_args.BUILD_NUMBER}",
1516
                  }
1517
                )
1518
                """
1519
            ),
1520
        }
1521
    )
1522

1523
    def check_docker_proc(process: Process):
1✔
1524
        assert process.argv == (
1✔
1525
            "/dummy/docker",
1526
            "build",
1527
            "--label",
1528
            "build.host=tbs06",
1529
            "--label",
1530
            "build.job=13934",
1531
            "--pull=False",
1532
            "--tag",
1533
            "img1:latest",
1534
            "--build-arg",
1535
            "BUILD_NUMBER=13934",
1536
            "--build-arg",
1537
            "BUILD_SLAVE=tbs06",
1538
            "--file",
1539
            "docker/test/Dockerfile",
1540
            ".",
1541
        )
1542

1543
    assert_build(
1✔
1544
        rule_runner,
1545
        Address("docker/test", target_name="img1"),
1546
        process_assertions=check_docker_proc,
1547
    )
1548

1549

1550
@pytest.mark.parametrize("suggest_renames", [True, False])
1✔
1551
@pytest.mark.parametrize(
1✔
1552
    "context_root, copy_sources, build_context_files, expect_logged, fail_log_contains",
1553
    [
1554
        (
1555
            None,
1556
            ("src/project/bin.pex",),
1557
            ("src.project/binary.pex", "src/project/app.py"),
1558
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1559
            [
1560
                "suggested renames:\n\n  * src/project/bin.pex => src.project/binary.pex\n\n",
1561
                "There are files in the Docker build context that were not referenced by ",
1562
                "  * src/project/app.py\n\n",
1563
            ],
1564
        ),
1565
        (
1566
            "./",
1567
            ("config.txt",),
1568
            ("docker/test/conf/config.txt",),
1569
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1570
            [
1571
                "suggested renames:\n\n  * config.txt => conf/config.txt\n\n",
1572
            ],
1573
        ),
1574
        (
1575
            "./",
1576
            ("conf/config.txt",),
1577
            (
1578
                "docker/test/conf/config.txt",
1579
                "src.project/binary.pex",
1580
            ),
1581
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1582
            [
1583
                "There are unreachable files in these directories, excluded from the build context "
1584
                "due to `context_root` being 'docker/test':\n\n"
1585
                "  * src.project\n\n"
1586
                "Suggested `context_root` setting is '' in order to include all files in the "
1587
                "build context, otherwise relocate the files to be part of the current "
1588
                "`context_root` 'docker/test'."
1589
            ],
1590
        ),
1591
        (
1592
            "./config",
1593
            (),
1594
            (
1595
                "docker/test/config/..unusal-name",
1596
                "docker/test/config/.rc",
1597
                "docker/test/config/.a",
1598
                "docker/test/config/.conf.d/b",
1599
            ),
1600
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1601
            [
1602
                "There are files in the Docker build context that were not referenced by "
1603
                "any `COPY` instruction (this is not an error):\n"
1604
                "\n"
1605
                "  * ..unusal-name\n"
1606
                "  * .a\n"
1607
                "  * .conf.d/b\n"
1608
                "  * .rc\n"
1609
            ],
1610
        ),
1611
    ],
1612
)
1613
def test_docker_build_fail_logs(
1✔
1614
    rule_runner: RuleRunner,
1615
    caplog,
1616
    context_root: str | None,
1617
    copy_sources: tuple[str, ...],
1618
    build_context_files: tuple[str, ...],
1619
    expect_logged: list[tuple[int, str]] | None,
1620
    fail_log_contains: list[str],
1621
    suggest_renames: bool,
1622
) -> None:
1623
    caplog.set_level(logging.INFO)
1✔
1624
    rule_runner.write_files({"docker/test/BUILD": f"docker_image(context_root={context_root!r})"})
1✔
1625
    build_context_files = ("docker/test/Dockerfile", *build_context_files)
1✔
1626
    build_context_snapshot = rule_runner.make_snapshot_of_empty_files(build_context_files)
1✔
1627
    suggest_renames_arg = (
1✔
1628
        "--docker-suggest-renames" if suggest_renames else "--no-docker-suggest-renames"
1629
    )
1630
    rule_runner.set_options([suggest_renames_arg])
1✔
1631
    with pytest.raises(ProcessExecutionFailure):
1✔
1632
        assert_build(
1✔
1633
            rule_runner,
1634
            Address("docker/test"),
1635
            exit_code=1,
1636
            copy_sources=copy_sources,
1637
            build_context_snapshot=build_context_snapshot,
1638
        )
1639

1640
    assert_logged(caplog, expect_logged)
1✔
1641
    for msg in fail_log_contains:
1✔
1642
        if suggest_renames:
1✔
1643
            assert msg in caplog.records[0].message
1✔
1644
        else:
1645
            assert msg not in caplog.records[0].message
1✔
1646

1647

1648
@pytest.mark.parametrize(
1✔
1649
    "expected_target, options",
1650
    [
1651
        ("dev", None),
1652
        ("prod", {"build_target_stage": "prod", "default_repository": "{name}"}),
1653
    ],
1654
)
1655
def test_build_target_stage(
1✔
1656
    rule_runner: RuleRunner, options: dict | None, expected_target: str
1657
) -> None:
1658
    rule_runner.write_files(
1✔
1659
        {
1660
            "BUILD": "docker_image(name='image', target_stage='dev')",
1661
            "Dockerfile": dedent(
1662
                """\
1663
                FROM base as build
1664
                FROM build as dev
1665
                FROM build as prod
1666
                """
1667
            ),
1668
        }
1669
    )
1670

1671
    def check_docker_proc(process: Process):
1✔
1672
        assert process.argv == (
1✔
1673
            "/dummy/docker",
1674
            "build",
1675
            "--pull=False",
1676
            "--target",
1677
            expected_target,
1678
            "--tag",
1679
            "image:latest",
1680
            "--file",
1681
            "Dockerfile",
1682
            ".",
1683
        )
1684

1685
    assert_build(
1✔
1686
        rule_runner,
1687
        Address("", target_name="image"),
1688
        options=options,
1689
        process_assertions=check_docker_proc,
1690
        version_tags=("build latest", "dev latest", "prod latest"),
1691
        expected_registries_metadata=[
1692
            dict(
1693
                address=None,
1694
                alias=None,
1695
                repository="image",
1696
                tags=[
1697
                    dict(
1698
                        template="latest", tag="latest", uses_local_alias=False, name="image:latest"
1699
                    )
1700
                ],
1701
            )
1702
        ],
1703
    )
1704

1705

1706
def test_invalid_build_target_stage(rule_runner: RuleRunner) -> None:
1✔
1707
    rule_runner.write_files(
1✔
1708
        {
1709
            "BUILD": "docker_image(name='image', target_stage='bad')",
1710
            "Dockerfile": dedent(
1711
                """\
1712
                FROM base as build
1713
                FROM build as dev
1714
                FROM build as prod
1715
                """
1716
            ),
1717
        }
1718
    )
1719

1720
    err = (
1✔
1721
        r"The 'target_stage' field in `docker_image` //:image was set to 'bad', but there is no "
1722
        r"such stage in `Dockerfile`\. Available stages: build, dev, prod\."
1723
    )
1724
    with pytest.raises(DockerBuildTargetStageError, match=err):
1✔
1725
        assert_build(
1✔
1726
            rule_runner,
1727
            Address("", target_name="image"),
1728
            version_tags=("build latest", "dev latest", "prod latest"),
1729
        )
1730

1731

1732
@pytest.mark.parametrize(
1✔
1733
    "default_context_root, context_root, expected_context_root",
1734
    [
1735
        ("", None, "."),
1736
        (".", None, "."),
1737
        ("src", None, "src"),
1738
        (
1739
            "/",
1740
            None,
1741
            pytest.raises(
1742
                InvalidFieldException,
1743
                match=r"Use '' for a path relative to the build root, or '\./' for",
1744
            ),
1745
        ),
1746
        (
1747
            "/src",
1748
            None,
1749
            pytest.raises(
1750
                InvalidFieldException,
1751
                match=(
1752
                    r"The `context_root` field in target src/docker:image must be a relative path, "
1753
                    r"but was '/src'\. Use 'src' for a path relative to the build root, or '\./src' "
1754
                    r"for a path relative to the BUILD file \(i\.e\. 'src/docker/src'\)\."
1755
                ),
1756
            ),
1757
        ),
1758
        ("./", None, "src/docker"),
1759
        ("./build/context/", None, "src/docker/build/context"),
1760
        (".build/context/", None, ".build/context"),
1761
        ("ignored", "", "."),
1762
        ("ignored", ".", "."),
1763
        ("ignored", "src/context/", "src/context"),
1764
        ("ignored", "./", "src/docker"),
1765
        ("ignored", "src", "src"),
1766
        ("ignored", "./build/context", "src/docker/build/context"),
1767
    ],
1768
)
1769
def test_get_context_root(
1✔
1770
    context_root: str | None, default_context_root: str, expected_context_root: str | ContextManager
1771
) -> None:
1772
    if isinstance(expected_context_root, str):
1✔
1773
        raises = cast("ContextManager", no_exception())
1✔
1774
    else:
1775
        raises = expected_context_root
1✔
1776

1777
    with raises:
1✔
1778
        docker_options = create_subsystem(
1✔
1779
            DockerOptions,
1780
            default_context_root=default_context_root,
1781
        )
1782
        address = Address("src/docker", target_name="image")
1✔
1783
        tgt = DockerImageTarget({"context_root": context_root}, address)
1✔
1784
        fs = DockerPackageFieldSet.create(tgt)
1✔
1785
        actual_context_root = fs.get_context_root(docker_options.default_context_root)
1✔
1786
        assert actual_context_root == expected_context_root
1✔
1787

1788

1789
@pytest.mark.parametrize(
1✔
1790
    "docker, expected, stdout, stderr",
1791
    [
1792
        (
1793
            DockerBinary("/bin/docker", "1234", is_podman=False),
1794
            "<unknown>",
1795
            "",
1796
            "",
1797
        ),
1798
        # Docker
1799
        (
1800
            DockerBinary("/bin/docker", "1234", is_podman=False),
1801
            "0e09b442b572",
1802
            "",
1803
            dedent(
1804
                """\
1805
                Step 22/22 : LABEL job-url="https://jenkins.example.net/job/python_artefactsapi_pipeline/"
1806
                 ---> Running in ae5c3eac5c0b
1807
                Removing intermediate container ae5c3eac5c0b
1808
                 ---> 0e09b442b572
1809
                Successfully built 0e09b442b572
1810
                Successfully tagged docker.example.net/artefactsapi/master:3.6.5
1811
                """
1812
            ),
1813
        ),
1814
        # Buildkit without step duration
1815
        (
1816
            DockerBinary("/bin/docker", "1234", is_podman=False),
1817
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
1818
            dedent(
1819
                """\
1820
                #7 [2/2] COPY testprojects.src.python.hello.main/main.pex /hello
1821
                #7 sha256:843d0c804a7eb5ba08b0535b635d5f98a3e56bc43a3fbe7d226a8024176f00d1
1822
                #7 DONE 0.1s
1823

1824
                #8 exporting to image
1825
                #8 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
1826
                #8 exporting layers 0.0s done
1827
                #8 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 done
1828
                #8 naming to docker.io/library/test-example-synth:1.2.5 done
1829
                #8 DONE 0.0s
1830

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

1833
                """
1834
            ),
1835
            "",
1836
        ),
1837
        # Buildkit with step duration
1838
        (
1839
            DockerBinary("/bin/docker", "1234", is_podman=False),
1840
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
1841
            dedent(
1842
                """\
1843
                #5 [2/2] RUN sleep 1
1844
                #5 DONE 1.1s
1845

1846
                #6 exporting to image
1847
                #6 exporting layers
1848
                #6 exporting layers 0.7s done
1849
                #6 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 0.0s done
1850
                #6 naming to docker.io/library/my-docker-image:latest 0.1s done
1851
                #6 DONE 1.1s
1852
                """
1853
            ),
1854
            "",
1855
        ),
1856
        # Buildkit with containerd-snapshotter 0.12.1
1857
        (
1858
            DockerBinary("/bin/docker", "1234", is_podman=False),
1859
            "sha256:b2b51838586286a9e544ddb31b3dbf7f6a99654d275b6e56b5f69f90138b4c0e",
1860
            dedent(
1861
                """\
1862
                #9 exporting to image
1863
                #9 exporting layers done
1864
                #9 exporting manifest sha256:7802087e8e0801f6451d862a00a6ce8af3e4829b09bc890dea0dd2659c11b25a done
1865
                #9 exporting config sha256:c83bed954709ba0c546d66d8f29afaac87c597f01b03fec158f3b21977c3e143 done
1866
                #9 exporting attestation manifest sha256:399891f9628cfafaba9e034599bdd55675ac0a3bad38151ed1ebf03993669545 done
1867
                #9 exporting manifest list sha256:b2b51838586286a9e544ddb31b3dbf7f6a99654d275b6e56b5f69f90138b4c0e done
1868
                #9 naming to myhost.com/my_app:latest done
1869
                #9 unpacking to myhost.com/my_app:latest done
1870
                #9 DONE 0.0s
1871
                """
1872
            ),
1873
            "",
1874
        ),
1875
        # Buildkit with containerd-snapshotter and cross platform 0.12.1
1876
        (
1877
            DockerBinary("/bin/docker", "1234", is_podman=False),
1878
            "sha256:3c72de0e05bb75247e68e124e6500700f6e0597425db2ee9f08fd59ef28cea0f",
1879
            dedent(
1880
                """\
1881
                #12 exporting to image
1882
                #12 exporting layers done
1883
                #12 exporting manifest sha256:452598369b55c27d752c45736cf26c0339612077f17df31fb0cdd79c5145d081 done
1884
                #12 exporting config sha256:6fbcebfde0ec24b487045516c3b5ffd3f0633e756a6d5808c2e5ad75809e0ca6 done
1885
                #12 exporting attestation manifest sha256:32fcf615e85bc9c2f606f863e8db3ca16dd77613a1e175e5972f39267e106dfb done
1886
                #12 exporting manifest sha256:bcb911a3efbec48e3c58c2acfd38fe92321eed731c53253f0b5c883918420187 done
1887
                #12 exporting config sha256:86e7fd0c4fa2356430d4ca188ed9e86497b8d03996ccba426d92c7e145e69990 done
1888
                #12 exporting attestation manifest sha256:66f9e7af29dd04e6264b8e113571f7b653f1681ba124a386530145fb39ff0102 done
1889
                #12 exporting manifest list sha256:3c72de0e05bb75247e68e124e6500700f6e0597425db2ee9f08fd59ef28cea0f done
1890
                #12 naming to myhost.com/my_app:latest done
1891
                #12 unpacking to myhost.com/my_app:latest done
1892
                #12 DONE 0.0s
1893
                """
1894
            ),
1895
            "",
1896
        ),
1897
        # Buildkit with containerd-snapshotter 0.13.1
1898
        (
1899
            DockerBinary("/bin/docker", "1234", is_podman=False),
1900
            "sha256:d15432046b4feaebb70370fad4710151dd8f0b9741cb8bc4d20c08ed8847f17a",
1901
            dedent(
1902
                """\
1903
                #13 exporting to image
1904
                #13 exporting layers
1905
                #13 exporting layers done
1906
                #13 exporting manifest sha256:2f161cf7c511874936d99995adeb53c6ac2262279a606bc1b70756ca1367ceb5 done
1907
                #13 exporting config sha256:23bf9de65f90e11ab7bb6bad0e1fb5c7eee3df2050aa902e8a53684fbd539eb9 done
1908
                #13 exporting attestation manifest sha256:5ff8bf97d8ad78a119d95d2b887400b3482a9026192ca7fb70307dfe290c93bf 0.0s done
1909
                #13 exporting manifest sha256:bf37d968d569812df393c7b6a48eab143066fa56a001905d9a70ec7acf3d34f4 done
1910
                #13 exporting config sha256:7c99f317cfae97e79dc12096279b71036a60129314e670920475665d466c821f done
1911
                #13 exporting attestation manifest sha256:4b3176781bb62e51cce743d4428e84e3559c9a23c328d6dfbfacac67f282cf70 0.0s done
1912
                #13 exporting manifest list sha256:d15432046b4feaebb70370fad4710151dd8f0b9741cb8bc4d20c08ed8847f17a 0.0s done
1913
                #13 naming to my-host.com/repo:latest done
1914
                #13 unpacking to my-host.com/repo:latest done
1915
                #13 DONE 0.1s
1916
                """
1917
            ),
1918
            "",
1919
        ),
1920
        # Buildkit with containerd-snapshotter 0.17.1 and disabled attestations
1921
        (
1922
            DockerBinary("/bin/docker", "1234", is_podman=False),
1923
            "sha256:6c3aff6414781126578b3e7b4a217682e89c616c0eac864d5b3ea7c87f1094d0",
1924
            dedent(
1925
                """\
1926
                    #24 exporting to image
1927
                    #24 exporting layers done
1928
                    #24 preparing layers for inline cache
1929
                    #24 preparing layers for inline cache 0.4s done
1930
                    #24 exporting manifest sha256:6c3aff6414781126578b3e7b4a217682e89c616c0eac864d5b3ea7c87f1094d0 0.0s done
1931
                    #24 exporting config sha256:af716170542d95134cb41b56e2dfea2c000b05b6fc4f440158ed9834ff96d1b4 0.0s done
1932
                    #24 naming to REDACTED:latest done
1933
                    #24 unpacking to REDACTED:latest 0.0s done
1934
                    #24 DONE 0.5s
1935

1936
                    """
1937
            ),
1938
            "",
1939
        ),
1940
        # Podman
1941
        (
1942
            DockerBinary("/bin/podman", "abcd", is_podman=True),
1943
            "a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee",
1944
            dedent(
1945
                """\
1946
                STEP 5/5: COPY ./ .
1947
                COMMIT example
1948
                --> a85499e9039a
1949
                Successfully tagged localhost/example:latest
1950
                a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee
1951
                """
1952
            ),
1953
            "",
1954
        ),
1955
    ],
1956
)
1957
def test_parse_image_id_from_docker_build_output(
1✔
1958
    docker: DockerBinary, expected: str, stdout: str, stderr: str
1959
) -> None:
1960
    assert expected == parse_image_id_from_docker_build_output(
1✔
1961
        docker, stdout.encode(), stderr.encode()
1962
    )
1963

1964

1965
ImageRefTest = namedtuple(
1✔
1966
    "ImageRefTest",
1967
    "docker_image, registries, default_repository, expect_refs, expect_error",
1968
    defaults=({}, {}, "{name}", (), None),
1969
)
1970

1971

1972
@pytest.mark.parametrize(
1✔
1973
    "test",
1974
    [
1975
        ImageRefTest(
1976
            docker_image=dict(name="lowercase"),
1977
            expect_refs=(
1978
                ImageRefRegistry(
1979
                    registry=None,
1980
                    repository="lowercase",
1981
                    tags=(
1982
                        ImageRefTag(
1983
                            template="latest",
1984
                            formatted="latest",
1985
                            uses_local_alias=False,
1986
                            full_name="lowercase:latest",
1987
                        ),
1988
                    ),
1989
                ),
1990
            ),
1991
        ),
1992
        ImageRefTest(
1993
            docker_image=dict(name="CamelCase"),
1994
            expect_refs=(
1995
                ImageRefRegistry(
1996
                    registry=None,
1997
                    repository="camelcase",
1998
                    tags=(
1999
                        ImageRefTag(
2000
                            template="latest",
2001
                            formatted="latest",
2002
                            uses_local_alias=False,
2003
                            full_name="camelcase:latest",
2004
                        ),
2005
                    ),
2006
                ),
2007
            ),
2008
        ),
2009
        ImageRefTest(
2010
            docker_image=dict(image_tags=["CamelCase"]),
2011
            expect_refs=(
2012
                ImageRefRegistry(
2013
                    registry=None,
2014
                    repository="image",
2015
                    tags=(
2016
                        ImageRefTag(
2017
                            template="CamelCase",
2018
                            formatted="CamelCase",
2019
                            uses_local_alias=False,
2020
                            full_name="image:CamelCase",
2021
                        ),
2022
                    ),
2023
                ),
2024
            ),
2025
        ),
2026
        ImageRefTest(
2027
            docker_image=dict(image_tags=["{val1}", "prefix-{val2}"]),
2028
            expect_refs=(
2029
                ImageRefRegistry(
2030
                    registry=None,
2031
                    repository="image",
2032
                    tags=(
2033
                        ImageRefTag(
2034
                            template="{val1}",
2035
                            formatted="first-value",
2036
                            uses_local_alias=False,
2037
                            full_name="image:first-value",
2038
                        ),
2039
                        ImageRefTag(
2040
                            template="prefix-{val2}",
2041
                            formatted="prefix-second-value",
2042
                            uses_local_alias=False,
2043
                            full_name="image:prefix-second-value",
2044
                        ),
2045
                    ),
2046
                ),
2047
            ),
2048
        ),
2049
        ImageRefTest(
2050
            docker_image=dict(registries=["REG1.example.net"]),
2051
            expect_refs=(
2052
                ImageRefRegistry(
2053
                    registry=DockerRegistryOptions(address="REG1.example.net"),
2054
                    repository="image",
2055
                    tags=(
2056
                        ImageRefTag(
2057
                            template="latest",
2058
                            formatted="latest",
2059
                            uses_local_alias=False,
2060
                            full_name="REG1.example.net/image:latest",
2061
                        ),
2062
                    ),
2063
                ),
2064
            ),
2065
        ),
2066
        ImageRefTest(
2067
            docker_image=dict(registries=["docker.io", "@private"], repository="our-the/pkg"),
2068
            registries=dict(private={"address": "our.registry", "repository": "the/pkg"}),
2069
            expect_refs=(
2070
                ImageRefRegistry(
2071
                    registry=DockerRegistryOptions(address="docker.io"),
2072
                    repository="our-the/pkg",
2073
                    tags=(
2074
                        ImageRefTag(
2075
                            template="latest",
2076
                            formatted="latest",
2077
                            uses_local_alias=False,
2078
                            full_name="docker.io/our-the/pkg:latest",
2079
                        ),
2080
                    ),
2081
                ),
2082
                ImageRefRegistry(
2083
                    registry=DockerRegistryOptions(
2084
                        alias="private", address="our.registry", repository="the/pkg"
2085
                    ),
2086
                    repository="the/pkg",
2087
                    tags=(
2088
                        ImageRefTag(
2089
                            template="latest",
2090
                            formatted="latest",
2091
                            uses_local_alias=False,
2092
                            full_name="our.registry/the/pkg:latest",
2093
                        ),
2094
                    ),
2095
                ),
2096
            ),
2097
        ),
2098
        ImageRefTest(
2099
            docker_image=dict(
2100
                registries=["docker.io", "@private"],
2101
                repository="{parent_directory}/{default_repository}",
2102
            ),
2103
            registries=dict(
2104
                private={"address": "our.registry", "repository": "{target_repository}/the/pkg"}
2105
            ),
2106
            expect_refs=(
2107
                ImageRefRegistry(
2108
                    registry=DockerRegistryOptions(address="docker.io"),
2109
                    repository="test/image",
2110
                    tags=(
2111
                        ImageRefTag(
2112
                            template="latest",
2113
                            formatted="latest",
2114
                            uses_local_alias=False,
2115
                            full_name="docker.io/test/image:latest",
2116
                        ),
2117
                    ),
2118
                ),
2119
                ImageRefRegistry(
2120
                    registry=DockerRegistryOptions(
2121
                        alias="private",
2122
                        address="our.registry",
2123
                        repository="{target_repository}/the/pkg",
2124
                    ),
2125
                    repository="test/image/the/pkg",
2126
                    tags=(
2127
                        ImageRefTag(
2128
                            template="latest",
2129
                            formatted="latest",
2130
                            uses_local_alias=False,
2131
                            full_name="our.registry/test/image/the/pkg:latest",
2132
                        ),
2133
                    ),
2134
                ),
2135
            ),
2136
        ),
2137
        ImageRefTest(
2138
            docker_image=dict(registries=["@private"], image_tags=["prefix-{val1}"]),
2139
            registries=dict(
2140
                private={"address": "our.registry", "extra_image_tags": ["{val2}-suffix"]}
2141
            ),
2142
            expect_refs=(
2143
                ImageRefRegistry(
2144
                    registry=DockerRegistryOptions(
2145
                        alias="private",
2146
                        address="our.registry",
2147
                        extra_image_tags=("{val2}-suffix",),
2148
                    ),
2149
                    repository="image",
2150
                    tags=(
2151
                        ImageRefTag(
2152
                            template="prefix-{val1}",
2153
                            formatted="prefix-first-value",
2154
                            uses_local_alias=False,
2155
                            full_name="our.registry/image:prefix-first-value",
2156
                        ),
2157
                        ImageRefTag(
2158
                            template="{val2}-suffix",
2159
                            formatted="second-value-suffix",
2160
                            uses_local_alias=False,
2161
                            full_name="our.registry/image:second-value-suffix",
2162
                        ),
2163
                    ),
2164
                ),
2165
            ),
2166
        ),
2167
        ImageRefTest(
2168
            docker_image=dict(repository="{default_repository}/a"),
2169
            default_repository="{target_repository}/b",
2170
            expect_error=pytest.raises(
2171
                InterpolationError,
2172
                match=(
2173
                    r"Invalid value for the `repository` field of the `docker_image` target at "
2174
                    r"src/test/docker:image: '\{default_repository\}/a'\.\n\n"
2175
                    r"The formatted placeholders recurse too deep\.\n"
2176
                    r"'\{default_repository\}/a' => '\{target_repository\}/b/a' => "
2177
                    r"'\{default_repository\}/a/b/a'"
2178
                ),
2179
            ),
2180
        ),
2181
        ImageRefTest(
2182
            # Test registry `use_local_alias` (#16354)
2183
            docker_image=dict(registries=["docker.io", "@private"], repository="our-the/pkg"),
2184
            registries=dict(
2185
                private={
2186
                    "address": "our.registry",
2187
                    "repository": "the/pkg",
2188
                    "use_local_alias": True,
2189
                }
2190
            ),
2191
            expect_refs=(
2192
                ImageRefRegistry(
2193
                    registry=DockerRegistryOptions(address="docker.io"),
2194
                    repository="our-the/pkg",
2195
                    tags=(
2196
                        ImageRefTag(
2197
                            template="latest",
2198
                            formatted="latest",
2199
                            uses_local_alias=False,
2200
                            full_name="docker.io/our-the/pkg:latest",
2201
                        ),
2202
                    ),
2203
                ),
2204
                ImageRefRegistry(
2205
                    registry=DockerRegistryOptions(
2206
                        alias="private",
2207
                        address="our.registry",
2208
                        repository="the/pkg",
2209
                        use_local_alias=True,
2210
                    ),
2211
                    repository="the/pkg",
2212
                    tags=(
2213
                        ImageRefTag(
2214
                            template="latest",
2215
                            formatted="latest",
2216
                            uses_local_alias=False,
2217
                            full_name="our.registry/the/pkg:latest",
2218
                        ),
2219
                        ImageRefTag(
2220
                            template="latest",
2221
                            formatted="latest",
2222
                            uses_local_alias=True,
2223
                            full_name="private/the/pkg:latest",
2224
                        ),
2225
                    ),
2226
                ),
2227
            ),
2228
        ),
2229
    ],
2230
)
2231
def test_image_ref_formatting(test: ImageRefTest) -> None:
1✔
2232
    address = Address("src/test/docker", target_name=test.docker_image.pop("name", "image"))
1✔
2233
    tgt = DockerImageTarget(test.docker_image, address)
1✔
2234
    field_set = DockerPackageFieldSet.create(tgt)
1✔
2235
    registries = DockerRegistries.from_dict(test.registries)
1✔
2236
    interpolation_context = InterpolationContext.from_dict(
1✔
2237
        {"val1": "first-value", "val2": "second-value"}
2238
    )
2239
    with test.expect_error or no_exception():
1✔
2240
        image_refs = field_set.image_refs(
1✔
2241
            test.default_repository, registries, interpolation_context
2242
        )
2243
        assert tuple(image_refs) == test.expect_refs
1✔
2244

2245

2246
@pytest.mark.parametrize(
1✔
2247
    "BUILD, plugin_tags, tag_flags",
2248
    [
2249
        (
2250
            'docker_image(name="plugin")',
2251
            ("1.2.3",),
2252
            (
2253
                "--tag",
2254
                "plugin:latest",
2255
                "--tag",
2256
                "plugin:1.2.3",
2257
            ),
2258
        ),
2259
        (
2260
            'docker_image(name="plugin", image_tags=[])',
2261
            ("1.2.3",),
2262
            (
2263
                "--tag",
2264
                "plugin:1.2.3",
2265
            ),
2266
        ),
2267
    ],
2268
)
2269
def test_docker_image_tags_from_plugin_hook(
1✔
2270
    rule_runner: RuleRunner, BUILD: str, plugin_tags: tuple[str, ...], tag_flags: tuple[str, ...]
2271
) -> None:
2272
    rule_runner.write_files({"docker/test/BUILD": BUILD})
1✔
2273

2274
    refs = assert_get_image_refs(
1✔
2275
        rule_runner,
2276
        Address("docker/test", target_name="plugin"),
2277
        plugin_tags=plugin_tags,
2278
    )
2279

2280
    def check_docker_proc(process: Process):
1✔
2281
        assert process.argv == (
1✔
2282
            "/dummy/docker",
2283
            "build",
2284
            "--pull=False",
2285
            *tag_flags,
2286
            "--file",
2287
            "docker/test/Dockerfile",
2288
            ".",
2289
        )
2290

2291
    assert_build(
1✔
2292
        rule_runner,
2293
        Address("docker/test", target_name="plugin"),
2294
        process_assertions=check_docker_proc,
2295
        image_refs=refs,
2296
    )
2297

2298

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

2302
    err = "The `image_tags` field in target docker/test:no-tags must not be empty, unless"
1✔
2303
    with pytest.raises(InvalidFieldException, match=err):
1✔
2304
        assert_build(
1✔
2305
            rule_runner,
2306
            Address("docker/test", target_name="no-tags"),
2307
        )
2308

2309

2310
def test_docker_info_serialize() -> None:
1✔
2311
    image_id = "abc123"
1✔
2312
    # image refs with unique strings (i.e. not actual templates/names etc.), to make sure they're
2313
    # ending up in the right place in the JSON
2314
    image_refs = (
1✔
2315
        ImageRefRegistry(
2316
            registry=None,
2317
            repository="repo",
2318
            tags=(
2319
                ImageRefTag(
2320
                    template="repo tag1 template",
2321
                    formatted="repo tag1 formatted",
2322
                    uses_local_alias=False,
2323
                    full_name="repo tag1 full name",
2324
                ),
2325
                ImageRefTag(
2326
                    template="repo tag2 template",
2327
                    formatted="repo tag2 formatted",
2328
                    uses_local_alias=False,
2329
                    full_name="repo tag2 full name",
2330
                ),
2331
            ),
2332
        ),
2333
        ImageRefRegistry(
2334
            registry=DockerRegistryOptions(address="address"),
2335
            repository="address repo",
2336
            tags=(
2337
                ImageRefTag(
2338
                    template="address tag template",
2339
                    formatted="address tag formatted",
2340
                    uses_local_alias=False,
2341
                    full_name="address tag full name",
2342
                ),
2343
            ),
2344
        ),
2345
        ImageRefRegistry(
2346
            registry=DockerRegistryOptions(
2347
                address="alias address", alias="alias", repository="alias registry repo"
2348
            ),
2349
            repository="alias repo",
2350
            tags=(
2351
                ImageRefTag(
2352
                    template="alias tag (address) template",
2353
                    formatted="alias tag (address) formatted",
2354
                    uses_local_alias=False,
2355
                    full_name="alias tag (address) full name",
2356
                ),
2357
                ImageRefTag(
2358
                    template="alias tag (local alias) template",
2359
                    formatted="alias tag (local alias) formatted",
2360
                    uses_local_alias=True,
2361
                    full_name="alias tag (local alias) full name",
2362
                ),
2363
            ),
2364
        ),
2365
    )
2366

2367
    expected = dict(
1✔
2368
        version=1,
2369
        image_id=image_id,
2370
        registries=[
2371
            dict(
2372
                alias=None,
2373
                address=None,
2374
                repository="repo",
2375
                tags=[
2376
                    dict(
2377
                        template="repo tag1 template",
2378
                        tag="repo tag1 formatted",
2379
                        uses_local_alias=False,
2380
                        name="repo tag1 full name",
2381
                    ),
2382
                    dict(
2383
                        template="repo tag2 template",
2384
                        tag="repo tag2 formatted",
2385
                        uses_local_alias=False,
2386
                        name="repo tag2 full name",
2387
                    ),
2388
                ],
2389
            ),
2390
            dict(
2391
                alias=None,
2392
                address="address",
2393
                repository="address repo",
2394
                tags=[
2395
                    dict(
2396
                        template="address tag template",
2397
                        tag="address tag formatted",
2398
                        uses_local_alias=False,
2399
                        name="address tag full name",
2400
                    )
2401
                ],
2402
            ),
2403
            dict(
2404
                alias="alias",
2405
                address="alias address",
2406
                repository="alias repo",
2407
                tags=[
2408
                    dict(
2409
                        template="alias tag (address) template",
2410
                        tag="alias tag (address) formatted",
2411
                        uses_local_alias=False,
2412
                        name="alias tag (address) full name",
2413
                    ),
2414
                    dict(
2415
                        template="alias tag (local alias) template",
2416
                        tag="alias tag (local alias) formatted",
2417
                        uses_local_alias=True,
2418
                        name="alias tag (local alias) full name",
2419
                    ),
2420
                ],
2421
            ),
2422
        ],
2423
    )
2424

2425
    result = DockerInfoV1.serialize(image_refs, image_id)
1✔
2426
    assert json.loads(result) == expected
1✔
2427

2428

2429
@pytest.mark.parametrize(
1✔
2430
    ("output", "expected"),
2431
    [({"type": "image", "push": "true"}, True), ({"type": "registry"}, True), (None, False)],
2432
)
2433
def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> None:
1✔
2434
    rule_runner = RuleRunner(target_types=[DockerImageTarget])
1✔
2435
    output_str = f", output={output}" if output else ""
1✔
2436
    rule_runner.write_files(
1✔
2437
        {"BUILD": f"docker_image(name='image', source='Dockerfile'{output_str})"}
2438
    )
2439
    field_set = DockerPackageFieldSet.create(
1✔
2440
        rule_runner.get_target(Address("", target_name="image"))
2441
    )
2442
    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