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

pantsbuild / pants / 21374897774

26 Jan 2026 09:37PM UTC coverage: 80.008% (-0.3%) from 80.269%
21374897774

Pull #23037

github

web-flow
Merge 4023b9eee into 09b8ecaa1
Pull Request #23037: Enable publish without package 2

105 of 178 new or added lines in 11 files covered. (58.99%)

238 existing lines in 14 files now uncovered.

78628 of 98275 relevant lines covered (80.01%)

3.35 hits per line

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

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

79

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

95

96
class DockerImageTagsRequestPlugin(DockerImageTagsRequest):
1✔
97
    pass
1✔
98

99

100
def assert_build(
1✔
101
    rule_runner: RuleRunner,
102
    address: Address,
103
    *extra_log_lines: str,
104
    options: dict | None = None,
105
    process_assertions: Callable[[Process], None] | None = None,
106
    exit_code: int = 0,
107
    copy_sources: tuple[str, ...] = (),
108
    copy_build_args=(),
109
    build_context_snapshot: Snapshot = EMPTY_SNAPSHOT,
110
    version_tags: tuple[str, ...] = (),
111
    plugin_tags: tuple[str, ...] = (),
112
    expected_registries_metadata: None | list = None,
113
) -> None:
114
    tgt = rule_runner.get_target(address)
1✔
115
    metadata_file_path: list[str] = []
1✔
116
    metadata_file_contents: list[bytes] = []
1✔
117

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

136
    def run_process_mock(process: Process) -> FallibleProcessResult:
1✔
UNCOV
137
        if process_assertions:
×
UNCOV
138
            process_assertions(process)
×
139

UNCOV
140
        return FallibleProcessResult(
×
141
            exit_code=exit_code,
142
            stdout=b"stdout",
143
            stdout_digest=EMPTY_FILE_DIGEST,
144
            stderr=b"stderr",
145
            stderr_digest=EMPTY_FILE_DIGEST,
146
            output_digest=EMPTY_DIGEST,
147
            metadata=ProcessResultMetadata(
148
                0,
149
                ProcessExecutionEnvironment(
150
                    environment_name=None,
151
                    platform=Platform.create_for_localhost().value,
152
                    docker_image=None,
153
                    remote_execution=False,
154
                    remote_execution_extra_platform_properties=[],
155
                    execute_in_workspace=False,
156
                    keep_sandboxes="never",
157
                ),
158
                "ran_locally",
159
                0,
160
            ),
161
        )
162

163
    def mock_get_info_file(request: CreateDigest) -> Digest:
1✔
UNCOV
164
        assert len(request) == 1
×
UNCOV
165
        assert isinstance(request[0], FileContent)
×
UNCOV
166
        metadata_file_path.append(request[0].path)
×
UNCOV
167
        metadata_file_contents.append(request[0].content)
×
UNCOV
168
        return EMPTY_DIGEST
×
169

170
    if options:
1✔
171
        opts = options or {}
1✔
172
        opts.setdefault("registries", {})
1✔
173
        opts.setdefault("default_repository", "{name}")
1✔
174
        opts.setdefault("default_context_root", "")
1✔
175
        opts.setdefault("build_args", [])
1✔
176
        opts.setdefault("build_target_stage", None)
1✔
177
        opts.setdefault("build_hosts", None)
1✔
178
        opts.setdefault("build_verbose", False)
1✔
179
        opts.setdefault("build_no_cache", False)
1✔
180
        opts.setdefault("use_buildx", False)
1✔
181
        opts.setdefault("env_vars", [])
1✔
182
        opts.setdefault("suggest_renames", True)
1✔
183

184
        docker_options = create_subsystem(
1✔
185
            DockerOptions,
186
            **opts,
187
        )
188
    else:
189
        docker_options = rule_runner.request(DockerOptions, [])
1✔
190

191
    global_options = rule_runner.request(GlobalOptions, [])
1✔
192

193
    union_membership = UnionMembership.from_rules(
1✔
194
        [UnionRule(DockerImageTagsRequest, DockerImageTagsRequestPlugin)]
195
    )
196
    result = run_rule_with_mocks(
1✔
197
        build_docker_image,
198
        rule_args=[
199
            DockerPackageFieldSet.create(tgt),
200
            docker_options,
201
            global_options,
202
            DockerBinary("/dummy/docker"),
203
            KeepSandboxes.never,
204
            union_membership,
205
        ],
206
        mock_calls={
207
            "pants.backend.docker.util_rules.docker_build_context.create_docker_build_context": build_context_mock,
208
            "pants.engine.internals.graph.resolve_target": lambda _: WrappedTarget(tgt),
209
            "pants.backend.docker.target_types.get_docker_image_tags": lambda __implicitly: DockerImageTags(
210
                plugin_tags
211
            ),
212
            "pants.engine.intrinsics.execute_process": run_process_mock,
213
            "pants.engine.intrinsics.create_digest": mock_get_info_file,
214
        },
215
        union_membership=union_membership,
216
        show_warnings=False,
217
    )
218

UNCOV
219
    assert result.digest == EMPTY_DIGEST
×
UNCOV
220
    assert len(result.artifacts) == 1
×
UNCOV
221
    assert len(metadata_file_path) == len(metadata_file_contents) == 1
×
UNCOV
222
    assert result.artifacts[0].relpath == metadata_file_path[0]
×
223

UNCOV
224
    metadata = json.loads(metadata_file_contents[0])
×
225
    # basic checks that we can always do
UNCOV
226
    assert metadata["version"] == 1
×
UNCOV
227
    assert metadata["image_id"] == "<unknown>"
×
UNCOV
228
    assert isinstance(metadata["registries"], list)
×
229
    # detailed checks, if the test opts in
UNCOV
230
    if expected_registries_metadata is not None:
×
UNCOV
231
        assert metadata["registries"] == expected_registries_metadata
×
232

UNCOV
233
    for log_line in extra_log_lines:
×
UNCOV
234
        assert log_line in result.artifacts[0].extra_log_lines
×
235

236

237
def test_build_docker_image(rule_runner: RuleRunner) -> None:
1✔
238
    rule_runner.write_files(
1✔
239
        {
240
            "docker/test/BUILD": dedent(
241
                """\
242

243
                docker_image(
244
                  name="test1",
245
                  image_tags=["1.2.3"],
246
                  repository="{directory}/{name}",
247
                )
248
                docker_image(
249
                  name="test2",
250
                  image_tags=["1.2.3"],
251
                )
252
                docker_image(
253
                  name="test3",
254
                  image_tags=["1.2.3"],
255
                  repository="{parent_directory}/{directory}/{name}",
256
                )
257
                docker_image(
258
                  name="test4",
259
                  image_tags=["1.2.3"],
260
                  repository="{directory}/four/test-four",
261
                )
262
                docker_image(
263
                  name="test5",
264
                  image_tags=["latest", "alpha-1.0", "alpha-1"],
265
                )
266
                docker_image(
267
                  name="test6",
268
                  image_tags=["1.2.3"],
269
                  repository="xyz/{full_directory}/{name}",
270
                )
271
                docker_image(
272
                  name="err1",
273
                  repository="{bad_template}",
274
                )
275
                """
276
            ),
277
            "docker/test/Dockerfile": "FROM python:3.8",
278
        }
279
    )
280

281
    assert_build(
1✔
282
        rule_runner,
283
        Address("docker/test", target_name="test1"),
284
        "Built docker image: test/test1:1.2.3",
285
        expected_registries_metadata=[
286
            dict(
287
                alias=None,
288
                address=None,
289
                repository="test/test1",
290
                tags=[
291
                    dict(
292
                        template="1.2.3",
293
                        tag="1.2.3",
294
                        uses_local_alias=False,
295
                        name="test/test1:1.2.3",
296
                    )
297
                ],
298
            )
299
        ],
300
    )
UNCOV
301
    assert_build(
×
302
        rule_runner,
303
        Address("docker/test", target_name="test2"),
304
        "Built docker image: test2:1.2.3",
305
        expected_registries_metadata=[
306
            dict(
307
                alias=None,
308
                address=None,
309
                repository="test2",
310
                tags=[
311
                    dict(template="1.2.3", tag="1.2.3", uses_local_alias=False, name="test2:1.2.3")
312
                ],
313
            )
314
        ],
315
    )
UNCOV
316
    assert_build(
×
317
        rule_runner,
318
        Address("docker/test", target_name="test3"),
319
        "Built docker image: docker/test/test3:1.2.3",
320
    )
UNCOV
321
    assert_build(
×
322
        rule_runner,
323
        Address("docker/test", target_name="test4"),
324
        "Built docker image: test/four/test-four:1.2.3",
325
    )
UNCOV
326
    assert_build(
×
327
        rule_runner,
328
        Address("docker/test", target_name="test5"),
329
        (
330
            "Built docker images: \n"
331
            "  * test/test5:latest\n"
332
            "  * test/test5:alpha-1.0\n"
333
            "  * test/test5:alpha-1"
334
        ),
335
        options=dict(default_repository="{directory}/{name}"),
336
        expected_registries_metadata=[
337
            dict(
338
                alias=None,
339
                address=None,
340
                repository="test/test5",
341
                tags=[
342
                    dict(
343
                        template="alpha-1",
344
                        tag="alpha-1",
345
                        uses_local_alias=False,
346
                        name="test/test5:alpha-1",
347
                    ),
348
                    dict(
349
                        template="alpha-1.0",
350
                        tag="alpha-1.0",
351
                        uses_local_alias=False,
352
                        name="test/test5:alpha-1.0",
353
                    ),
354
                    dict(
355
                        template="latest",
356
                        tag="latest",
357
                        uses_local_alias=False,
358
                        name="test/test5:latest",
359
                    ),
360
                ],
361
            )
362
        ],
363
    )
UNCOV
364
    assert_build(
×
365
        rule_runner,
366
        Address("docker/test", target_name="test6"),
367
        "Built docker image: xyz/docker/test/test6:1.2.3",
368
    )
369

UNCOV
370
    err1 = (
×
371
        r"Invalid value for the `repository` field of the `docker_image` target at "
372
        r"docker/test:err1: '{bad_template}'\.\n\nThe placeholder 'bad_template' is unknown\. "
373
        r"Try with one of: build_args, default_repository, directory, full_directory, name, "
374
        r"pants, parent_directory, tags, target_repository\."
375
    )
UNCOV
376
    with pytest.raises(DockerRepositoryNameError, match=err1):
×
UNCOV
377
        assert_build(
×
378
            rule_runner,
379
            Address("docker/test", target_name="err1"),
380
        )
381

382

383
def test_build_image_with_registries(rule_runner: RuleRunner) -> None:
1✔
384
    rule_runner.write_files(
1✔
385
        {
386
            "docker/test/BUILD": dedent(
387
                """\
388
                docker_image(name="addr1", image_tags=["1.2.3"], registries=["myregistry1domain:port"])
389
                docker_image(name="addr2", image_tags=["1.2.3"], registries=["myregistry2domain:port"])
390
                docker_image(name="addr3", image_tags=["1.2.3"], registries=["myregistry3domain:port"])
391
                docker_image(name="alias1", image_tags=["1.2.3"], registries=["@reg1"])
392
                docker_image(name="alias2", image_tags=["1.2.3"], registries=["@reg2"])
393
                docker_image(name="alias3", image_tags=["1.2.3"], registries=["reg3"])
394
                docker_image(name="unreg", image_tags=["1.2.3"], registries=[])
395
                docker_image(name="def", image_tags=["1.2.3"])
396
                docker_image(name="multi", image_tags=["1.2.3"], registries=["@reg2", "@reg1"])
397
                docker_image(name="extra_tags", image_tags=["1.2.3"], registries=["@reg1", "@extra"])
398
                """
399
            ),
400
            "docker/test/Dockerfile": "FROM python:3.8",
401
        }
402
    )
403

404
    options = {
1✔
405
        "default_repository": "{name}",
406
        "registries": {
407
            "reg1": {"address": "myregistry1domain:port"},
408
            "reg2": {"address": "myregistry2domain:port", "default": "true"},
409
            "extra": {"address": "extra", "extra_image_tags": ["latest"]},
410
        },
411
    }
412

413
    assert_build(
1✔
414
        rule_runner,
415
        Address("docker/test", target_name="addr1"),
416
        "Built docker image: myregistry1domain:port/addr1:1.2.3",
417
        options=options,
418
        expected_registries_metadata=[
419
            dict(
420
                alias="reg1",
421
                address="myregistry1domain:port",
422
                repository="addr1",
423
                tags=[
424
                    dict(
425
                        template="1.2.3",
426
                        tag="1.2.3",
427
                        uses_local_alias=False,
428
                        name="myregistry1domain:port/addr1:1.2.3",
429
                    )
430
                ],
431
            )
432
        ],
433
    )
UNCOV
434
    assert_build(
×
435
        rule_runner,
436
        Address("docker/test", target_name="addr2"),
437
        "Built docker image: myregistry2domain:port/addr2:1.2.3",
438
        options=options,
439
    )
UNCOV
440
    assert_build(
×
441
        rule_runner,
442
        Address("docker/test", target_name="addr3"),
443
        "Built docker image: myregistry3domain:port/addr3:1.2.3",
444
        options=options,
445
        expected_registries_metadata=[
446
            dict(
447
                alias=None,
448
                address="myregistry3domain:port",
449
                repository="addr3",
450
                tags=[
451
                    dict(
452
                        template="1.2.3",
453
                        tag="1.2.3",
454
                        uses_local_alias=False,
455
                        name="myregistry3domain:port/addr3:1.2.3",
456
                    )
457
                ],
458
            )
459
        ],
460
    )
UNCOV
461
    assert_build(
×
462
        rule_runner,
463
        Address("docker/test", target_name="alias1"),
464
        "Built docker image: myregistry1domain:port/alias1:1.2.3",
465
        options=options,
466
        expected_registries_metadata=[
467
            dict(
468
                alias="reg1",
469
                address="myregistry1domain:port",
470
                repository="alias1",
471
                tags=[
472
                    dict(
473
                        template="1.2.3",
474
                        tag="1.2.3",
475
                        uses_local_alias=False,
476
                        name="myregistry1domain:port/alias1:1.2.3",
477
                    )
478
                ],
479
            )
480
        ],
481
    )
UNCOV
482
    assert_build(
×
483
        rule_runner,
484
        Address("docker/test", target_name="alias2"),
485
        "Built docker image: myregistry2domain:port/alias2:1.2.3",
486
        options=options,
487
    )
UNCOV
488
    assert_build(
×
489
        rule_runner,
490
        Address("docker/test", target_name="alias3"),
491
        "Built docker image: reg3/alias3:1.2.3",
492
        options=options,
493
    )
UNCOV
494
    assert_build(
×
495
        rule_runner,
496
        Address("docker/test", target_name="unreg"),
497
        "Built docker image: unreg:1.2.3",
498
        options=options,
499
        expected_registries_metadata=[
500
            dict(
501
                alias=None,
502
                address=None,
503
                repository="unreg",
504
                tags=[
505
                    dict(template="1.2.3", tag="1.2.3", uses_local_alias=False, name="unreg:1.2.3")
506
                ],
507
            )
508
        ],
509
    )
UNCOV
510
    assert_build(
×
511
        rule_runner,
512
        Address("docker/test", target_name="def"),
513
        "Built docker image: myregistry2domain:port/def:1.2.3",
514
        options=options,
515
        expected_registries_metadata=[
516
            dict(
517
                alias="reg2",
518
                address="myregistry2domain:port",
519
                repository="def",
520
                tags=[
521
                    dict(
522
                        template="1.2.3",
523
                        tag="1.2.3",
524
                        uses_local_alias=False,
525
                        name="myregistry2domain:port/def:1.2.3",
526
                    )
527
                ],
528
            )
529
        ],
530
    )
UNCOV
531
    assert_build(
×
532
        rule_runner,
533
        Address("docker/test", target_name="multi"),
534
        (
535
            "Built docker images: \n"
536
            "  * myregistry2domain:port/multi:1.2.3\n"
537
            "  * myregistry1domain:port/multi:1.2.3"
538
        ),
539
        options=options,
540
        expected_registries_metadata=[
541
            dict(
542
                alias="reg1",
543
                address="myregistry1domain:port",
544
                repository="multi",
545
                tags=[
546
                    dict(
547
                        template="1.2.3",
548
                        tag="1.2.3",
549
                        uses_local_alias=False,
550
                        name="myregistry1domain:port/multi:1.2.3",
551
                    )
552
                ],
553
            ),
554
            dict(
555
                alias="reg2",
556
                address="myregistry2domain:port",
557
                repository="multi",
558
                tags=[
559
                    dict(
560
                        template="1.2.3",
561
                        tag="1.2.3",
562
                        uses_local_alias=False,
563
                        name="myregistry2domain:port/multi:1.2.3",
564
                    )
565
                ],
566
            ),
567
        ],
568
    )
UNCOV
569
    assert_build(
×
570
        rule_runner,
571
        Address("docker/test", target_name="extra_tags"),
572
        (
573
            "Built docker images: \n"
574
            "  * myregistry1domain:port/extra_tags:1.2.3\n"
575
            "  * extra/extra_tags:1.2.3\n"
576
            "  * extra/extra_tags:latest"
577
        ),
578
        options=options,
579
        expected_registries_metadata=[
580
            dict(
581
                alias="extra",
582
                address="extra",
583
                repository="extra_tags",
584
                tags=[
585
                    dict(
586
                        template="1.2.3",
587
                        tag="1.2.3",
588
                        uses_local_alias=False,
589
                        name="extra/extra_tags:1.2.3",
590
                    ),
591
                    dict(
592
                        template="latest",
593
                        tag="latest",
594
                        uses_local_alias=False,
595
                        name="extra/extra_tags:latest",
596
                    ),
597
                ],
598
            ),
599
            dict(
600
                alias="reg1",
601
                address="myregistry1domain:port",
602
                repository="extra_tags",
603
                tags=[
604
                    dict(
605
                        template="1.2.3",
606
                        tag="1.2.3",
607
                        uses_local_alias=False,
608
                        name="myregistry1domain:port/extra_tags:1.2.3",
609
                    )
610
                ],
611
            ),
612
        ],
613
    )
614

615

616
def test_dynamic_image_version(rule_runner: RuleRunner) -> None:
1✔
617
    interpolation_context = InterpolationContext.from_dict(
1✔
618
        {
619
            "baseimage": {"tag": "3.8"},
620
            "stage0": {"tag": "3.8"},
621
            "interim": {"tag": "latest"},
622
            "stage2": {"tag": "latest"},
623
            "output": {"tag": "1-1"},
624
        }
625
    )
626

627
    def assert_tags(name: str, *expect_tags: str) -> None:
1✔
628
        tgt = rule_runner.get_target(Address("docker/test", target_name=name))
1✔
629
        fs = DockerPackageFieldSet.create(tgt)
1✔
630
        image_refs = fs.image_refs(
1✔
631
            "image",
632
            DockerRegistries.from_dict({}),
633
            interpolation_context,
634
        )
635
        tags = tuple(t.full_name for r in image_refs for t in r.tags)
1✔
636
        assert expect_tags == tags
1✔
637

638
    rule_runner.write_files(
1✔
639
        {
640
            "docker/test/BUILD": dedent(
641
                """\
642
                docker_image(name="ver_1")
643
                docker_image(
644
                  name="ver_2",
645
                  image_tags=["{baseimage.tag}-{stage2.tag}", "beta"]
646
                )
647
                docker_image(name="err_1", image_tags=["{unknown_stage}"])
648
                docker_image(name="err_2", image_tags=["{stage0.unknown_value}"])
649
                """
650
            ),
651
        }
652
    )
653

654
    assert_tags("ver_1", "image:latest")
1✔
655
    assert_tags("ver_2", "image:3.8-latest", "image:beta")
1✔
656

657
    err_1 = (
1✔
658
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
659
        r"docker/test:err_1: '{unknown_stage}'\.\n\n"
660
        r"The placeholder 'unknown_stage' is unknown\. Try with one of: baseimage, interim, "
661
        r"output, stage0, stage2\."
662
    )
663
    with pytest.raises(DockerImageTagValueError, match=err_1):
1✔
664
        assert_tags("err_1")
1✔
665

666
    err_2 = (
1✔
667
        r"Invalid value for the `image_tags` field of the `docker_image` target at "
668
        r"docker/test:err_2: '{stage0.unknown_value}'\.\n\n"
669
        r"The placeholder 'unknown_value' is unknown\. Try with one of: tag\."
670
    )
671
    with pytest.raises(DockerImageTagValueError, match=err_2):
1✔
672
        assert_tags("err_2")
1✔
673

674

675
def test_docker_build_process_environment(rule_runner: RuleRunner) -> None:
1✔
676
    rule_runner.write_files(
1✔
677
        {"docker/test/BUILD": 'docker_image(name="env1", image_tags=["1.2.3"])'}
678
    )
679
    rule_runner.set_options(
1✔
680
        [],
681
        env={
682
            "INHERIT": "from Pants env",
683
            "PANTS_DOCKER_ENV_VARS": '["VAR=value", "INHERIT"]',
684
        },
685
    )
686

687
    def check_docker_proc(process: Process):
1✔
UNCOV
688
        assert process.argv == (
×
689
            "/dummy/docker",
690
            "build",
691
            "--pull=False",
692
            "--tag",
693
            "env1:1.2.3",
694
            "--file",
695
            "docker/test/Dockerfile",
696
            ".",
697
        )
UNCOV
698
        assert process.env == FrozenDict(
×
699
            {
700
                "INHERIT": "from Pants env",
701
                "VAR": "value",
702
                "__UPSTREAM_IMAGE_IDS": "",
703
            }
704
        )
705

706
    assert_build(
1✔
707
        rule_runner,
708
        Address("docker/test", target_name="env1"),
709
        process_assertions=check_docker_proc,
710
    )
711

712

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

716
    def check_docker_proc(process: Process):
1✔
UNCOV
717
        assert process.argv == (
×
718
            "/dummy/docker",
719
            "build",
720
            "--pull=True",
721
            "--tag",
722
            "args1:latest",
723
            "--file",
724
            "docker/test/Dockerfile",
725
            ".",
726
        )
727

728
    assert_build(
1✔
729
        rule_runner,
730
        Address("docker/test", target_name="args1"),
731
        process_assertions=check_docker_proc,
732
    )
733

734

735
def test_docker_build_squash(rule_runner: RuleRunner) -> None:
1✔
736
    rule_runner.write_files(
1✔
737
        {
738
            "docker/test/BUILD": dedent(
739
                """\
740
            docker_image(name="args1", squash=True)
741
            docker_image(name="args2", squash=False)
742
            """
743
            )
744
        }
745
    )
746

747
    def check_docker_proc(process: Process):
1✔
UNCOV
748
        assert process.argv == (
×
749
            "/dummy/docker",
750
            "build",
751
            "--pull=False",
752
            "--squash",
753
            "--tag",
754
            "args1:latest",
755
            "--file",
756
            "docker/test/Dockerfile",
757
            ".",
758
        )
759

760
    def check_docker_proc_no_squash(process: Process):
1✔
UNCOV
761
        assert process.argv == (
×
762
            "/dummy/docker",
763
            "build",
764
            "--pull=False",
765
            "--tag",
766
            "args2:latest",
767
            "--file",
768
            "docker/test/Dockerfile",
769
            ".",
770
        )
771

772
    assert_build(
1✔
773
        rule_runner,
774
        Address("docker/test", target_name="args1"),
775
        process_assertions=check_docker_proc,
776
    )
UNCOV
777
    assert_build(
×
778
        rule_runner,
779
        Address("docker/test", target_name="args2"),
780
        process_assertions=check_docker_proc_no_squash,
781
    )
782

783

784
def test_docker_build_args(rule_runner: RuleRunner) -> None:
1✔
785
    rule_runner.write_files(
1✔
786
        {"docker/test/BUILD": 'docker_image(name="args1", image_tags=["1.2.3"])'}
787
    )
788
    rule_runner.set_options(
1✔
789
        [],
790
        env={
791
            "INHERIT": "from Pants env",
792
            "PANTS_DOCKER_BUILD_ARGS": '["VAR=value", "INHERIT"]',
793
        },
794
    )
795

796
    def check_docker_proc(process: Process):
1✔
UNCOV
797
        assert process.argv == (
×
798
            "/dummy/docker",
799
            "build",
800
            "--pull=False",
801
            "--tag",
802
            "args1:1.2.3",
803
            "--build-arg",
804
            "INHERIT",
805
            "--build-arg",
806
            "VAR=value",
807
            "--file",
808
            "docker/test/Dockerfile",
809
            ".",
810
        )
811

812
        # Check that we pull in name only args via env.
UNCOV
813
        assert process.env == FrozenDict(
×
814
            {
815
                "INHERIT": "from Pants env",
816
                "__UPSTREAM_IMAGE_IDS": "",
817
            }
818
        )
819

820
    assert_build(
1✔
821
        rule_runner,
822
        Address("docker/test", target_name="args1"),
823
        process_assertions=check_docker_proc,
824
    )
825

826

827
def test_docker_image_version_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
828
    rule_runner.write_files(
1✔
829
        {"docker/test/BUILD": 'docker_image(name="ver1", image_tags=["{build_args.VERSION}"])'}
830
    )
831
    rule_runner.set_options(
1✔
832
        [],
833
        env={
834
            "PANTS_DOCKER_BUILD_ARGS": '["VERSION=1.2.3"]',
835
        },
836
    )
837

838
    assert_build(
1✔
839
        rule_runner,
840
        Address("docker/test", target_name="ver1"),
841
        "Built docker image: ver1:1.2.3",
842
        expected_registries_metadata=[
843
            dict(
844
                alias=None,
845
                address=None,
846
                repository="ver1",
847
                tags=[
848
                    dict(
849
                        template="{build_args.VERSION}",
850
                        tag="1.2.3",
851
                        uses_local_alias=False,
852
                        name="ver1:1.2.3",
853
                    )
854
                ],
855
            )
856
        ],
857
    )
858

859

860
def test_docker_repository_from_build_arg(rule_runner: RuleRunner) -> None:
1✔
861
    rule_runner.write_files(
1✔
862
        {"docker/test/BUILD": 'docker_image(name="image", repository="{build_args.REPO}")'}
863
    )
864
    rule_runner.set_options(
1✔
865
        [],
866
        env={
867
            "PANTS_DOCKER_BUILD_ARGS": '["REPO=test/image"]',
868
        },
869
    )
870

871
    assert_build(
1✔
872
        rule_runner,
873
        Address("docker/test", target_name="image"),
874
        "Built docker image: test/image:latest",
875
    )
876

877

878
def test_docker_extra_build_args_field(rule_runner: RuleRunner) -> None:
1✔
879
    rule_runner.write_files(
1✔
880
        {
881
            "docker/test/BUILD": dedent(
882
                """\
883
                docker_image(
884
                  name="img1",
885
                  extra_build_args=[
886
                    "FROM_ENV",
887
                    "SET=value",
888
                    "DEFAULT2=overridden",
889
                  ]
890
                )
891
                """
892
            ),
893
        }
894
    )
895
    rule_runner.set_options(
1✔
896
        [
897
            "--docker-build-args=DEFAULT1=global1",
898
            "--docker-build-args=DEFAULT2=global2",
899
        ],
900
        env={
901
            "FROM_ENV": "env value",
902
            "SET": "no care",
903
        },
904
    )
905

906
    def check_docker_proc(process: Process):
1✔
UNCOV
907
        assert process.argv == (
×
908
            "/dummy/docker",
909
            "build",
910
            "--pull=False",
911
            "--tag",
912
            "img1:latest",
913
            "--build-arg",
914
            "DEFAULT1=global1",
915
            "--build-arg",
916
            "DEFAULT2=overridden",
917
            "--build-arg",
918
            "FROM_ENV",
919
            "--build-arg",
920
            "SET=value",
921
            "--file",
922
            "docker/test/Dockerfile",
923
            ".",
924
        )
925

UNCOV
926
        assert process.env == FrozenDict(
×
927
            {
928
                "FROM_ENV": "env value",
929
                "__UPSTREAM_IMAGE_IDS": "",
930
            }
931
        )
932

933
    assert_build(
1✔
934
        rule_runner,
935
        Address("docker/test", target_name="img1"),
936
        process_assertions=check_docker_proc,
937
    )
938

939

940
def test_docker_build_secrets_option(rule_runner: RuleRunner) -> None:
1✔
941
    rule_runner.write_files(
1✔
942
        {
943
            "docker/test/BUILD": dedent(
944
                """\
945
                docker_image(
946
                  name="img1",
947
                  secrets={
948
                    "system-secret": "/var/run/secrets/mysecret",
949
                    "project-secret": "secrets/mysecret",
950
                    "target-secret": "./mysecret",
951
                  }
952
                )
953
                """
954
            ),
955
        }
956
    )
957

958
    def check_docker_proc(process: Process):
1✔
UNCOV
959
        assert process.argv == (
×
960
            "/dummy/docker",
961
            "build",
962
            "--pull=False",
963
            "--secret",
964
            "id=system-secret,src=/var/run/secrets/mysecret",
965
            "--secret",
966
            f"id=project-secret,src={rule_runner.build_root}/secrets/mysecret",
967
            "--secret",
968
            f"id=target-secret,src={rule_runner.build_root}/docker/test/mysecret",
969
            "--tag",
970
            "img1:latest",
971
            "--file",
972
            "docker/test/Dockerfile",
973
            ".",
974
        )
975

976
    assert_build(
1✔
977
        rule_runner,
978
        Address("docker/test", target_name="img1"),
979
        process_assertions=check_docker_proc,
980
    )
981

982

983
def test_docker_build_ssh_option(rule_runner: RuleRunner) -> None:
1✔
984
    rule_runner.write_files(
1✔
985
        {
986
            "docker/test/BUILD": dedent(
987
                """\
988
                docker_image(
989
                  name="img1",
990
                  ssh=["default"],
991
                )
992
                """
993
            ),
994
        }
995
    )
996

997
    def check_docker_proc(process: Process):
1✔
UNCOV
998
        assert process.argv == (
×
999
            "/dummy/docker",
1000
            "build",
1001
            "--pull=False",
1002
            "--ssh",
1003
            "default",
1004
            "--tag",
1005
            "img1:latest",
1006
            "--file",
1007
            "docker/test/Dockerfile",
1008
            ".",
1009
        )
1010

1011
    assert_build(
1✔
1012
        rule_runner,
1013
        Address("docker/test", target_name="img1"),
1014
        process_assertions=check_docker_proc,
1015
    )
1016

1017

1018
def test_docker_build_no_cache_option(rule_runner: RuleRunner) -> None:
1✔
1019
    rule_runner.set_options(
1✔
1020
        [],
1021
        env={
1022
            "PANTS_DOCKER_BUILD_NO_CACHE": "true",
1023
        },
1024
    )
1025
    rule_runner.write_files(
1✔
1026
        {
1027
            "docker/test/BUILD": dedent(
1028
                """\
1029
                docker_image(
1030
                  name="img1",
1031
                )
1032
                """
1033
            ),
1034
        }
1035
    )
1036

1037
    def check_docker_proc(process: Process):
1✔
UNCOV
1038
        assert process.argv == (
×
1039
            "/dummy/docker",
1040
            "build",
1041
            "--pull=False",
1042
            "--no-cache",
1043
            "--tag",
1044
            "img1:latest",
1045
            "--file",
1046
            "docker/test/Dockerfile",
1047
            ".",
1048
        )
1049

1050
    assert_build(
1✔
1051
        rule_runner,
1052
        Address("docker/test", target_name="img1"),
1053
        process_assertions=check_docker_proc,
1054
    )
1055

1056

1057
def test_docker_build_hosts_option(rule_runner: RuleRunner) -> None:
1✔
1058
    rule_runner.set_options(
1✔
1059
        [],
1060
        env={
1061
            "PANTS_DOCKER_BUILD_HOSTS": '{"global": "9.9.9.9"}',
1062
        },
1063
    )
1064
    rule_runner.write_files(
1✔
1065
        {
1066
            "docker/test/BUILD": dedent(
1067
                """\
1068
                docker_image(
1069
                  name="img1",
1070
                  extra_build_hosts={"docker": "10.180.0.1", "docker2": "10.180.0.2"},
1071
                )
1072
                """
1073
            ),
1074
        }
1075
    )
1076

1077
    def check_docker_proc(process: Process):
1✔
UNCOV
1078
        assert process.argv == (
×
1079
            "/dummy/docker",
1080
            "build",
1081
            "--add-host",
1082
            "global:9.9.9.9",
1083
            "--add-host",
1084
            "docker:10.180.0.1",
1085
            "--add-host",
1086
            "docker2:10.180.0.2",
1087
            "--pull=False",
1088
            "--tag",
1089
            "img1:latest",
1090
            "--file",
1091
            "docker/test/Dockerfile",
1092
            ".",
1093
        )
1094

1095
    assert_build(
1✔
1096
        rule_runner,
1097
        Address("docker/test", target_name="img1"),
1098
        process_assertions=check_docker_proc,
1099
    )
1100

1101

1102
def test_docker_cache_to_option(rule_runner: RuleRunner) -> None:
1✔
1103
    rule_runner.write_files(
1✔
1104
        {
1105
            "docker/test/BUILD": dedent(
1106
                """\
1107
                docker_image(
1108
                  name="img1",
1109
                  cache_to={"type": "local", "dest": "/tmp/docker/pants-test-cache"},
1110
                )
1111
                """
1112
            ),
1113
        }
1114
    )
1115

1116
    def check_docker_proc(process: Process):
1✔
UNCOV
1117
        assert process.argv == (
×
1118
            "/dummy/docker",
1119
            "buildx",
1120
            "build",
1121
            "--cache-to=type=local,dest=/tmp/docker/pants-test-cache",
1122
            "--output=type=docker",
1123
            "--pull=False",
1124
            "--tag",
1125
            "img1:latest",
1126
            "--file",
1127
            "docker/test/Dockerfile",
1128
            ".",
1129
        )
1130

1131
    assert_build(
1✔
1132
        rule_runner,
1133
        Address("docker/test", target_name="img1"),
1134
        process_assertions=check_docker_proc,
1135
        options=dict(use_buildx=True),
1136
    )
1137

1138

1139
def test_docker_cache_from_option(rule_runner: RuleRunner) -> None:
1✔
1140
    rule_runner.write_files(
1✔
1141
        {
1142
            "docker/test/BUILD": dedent(
1143
                """\
1144
                docker_image(
1145
                  name="img1",
1146
                  cache_from=[{"type": "local", "dest": "/tmp/docker/pants-test-cache1"}, {"type": "local", "dest": "/tmp/docker/pants-test-cache2"}],
1147
                )
1148
                """
1149
            ),
1150
        }
1151
    )
1152

1153
    def check_docker_proc(process: Process):
1✔
UNCOV
1154
        assert process.argv == (
×
1155
            "/dummy/docker",
1156
            "buildx",
1157
            "build",
1158
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache1",
1159
            "--cache-from=type=local,dest=/tmp/docker/pants-test-cache2",
1160
            "--output=type=docker",
1161
            "--pull=False",
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
        options=dict(use_buildx=True),
1174
    )
1175

1176

1177
def test_docker_output_option(rule_runner: RuleRunner) -> None:
1✔
1178
    """Testing non-default output type 'image'.
1179

1180
    Default output type 'docker' tested implicitly in other scenarios
1181
    """
1182
    rule_runner.write_files(
1✔
1183
        {
1184
            "docker/test/BUILD": dedent(
1185
                """\
1186
                docker_image(
1187
                  name="img1",
1188
                  output={"type": "image"}
1189
                )
1190
                """
1191
            ),
1192
        }
1193
    )
1194

1195
    def check_docker_proc(process: Process):
1✔
UNCOV
1196
        assert process.argv == (
×
1197
            "/dummy/docker",
1198
            "buildx",
1199
            "build",
1200
            "--output=type=image",
1201
            "--pull=False",
1202
            "--tag",
1203
            "img1:latest",
1204
            "--file",
1205
            "docker/test/Dockerfile",
1206
            ".",
1207
        )
1208

1209
    assert_build(
1✔
1210
        rule_runner,
1211
        Address("docker/test", target_name="img1"),
1212
        process_assertions=check_docker_proc,
1213
        options=dict(use_buildx=True),
1214
    )
1215

1216

1217
def test_docker_output_option_raises_when_no_buildkit(rule_runner: RuleRunner) -> None:
1✔
1218
    rule_runner.write_files(
1✔
1219
        {
1220
            "docker/test/BUILD": dedent(
1221
                """\
1222
                docker_image(
1223
                  name="img1",
1224
                  output={"type": "image"}
1225
                )
1226
                """
1227
            ),
1228
        }
1229
    )
1230

1231
    with pytest.raises(
1✔
1232
        DockerImageOptionValueError,
1233
        match=r"Buildx must be enabled via the Docker subsystem options in order to use this field.",
1234
    ):
1235
        assert_build(
1✔
1236
            rule_runner,
1237
            Address("docker/test", target_name="img1"),
1238
        )
1239

1240

1241
def test_docker_build_network_option(rule_runner: RuleRunner) -> None:
1✔
1242
    rule_runner.write_files(
1✔
1243
        {
1244
            "docker/test/BUILD": dedent(
1245
                """\
1246
                docker_image(
1247
                  name="img1",
1248
                  build_network="host",
1249
                )
1250
                """
1251
            ),
1252
        }
1253
    )
1254

1255
    def check_docker_proc(process: Process):
1✔
UNCOV
1256
        assert process.argv == (
×
1257
            "/dummy/docker",
1258
            "build",
1259
            "--network=host",
1260
            "--pull=False",
1261
            "--tag",
1262
            "img1:latest",
1263
            "--file",
1264
            "docker/test/Dockerfile",
1265
            ".",
1266
        )
1267

1268
    assert_build(
1✔
1269
        rule_runner,
1270
        Address("docker/test", target_name="img1"),
1271
        process_assertions=check_docker_proc,
1272
    )
1273

1274

1275
def test_docker_build_platform_option(rule_runner: RuleRunner) -> None:
1✔
1276
    rule_runner.write_files(
1✔
1277
        {
1278
            "docker/test/BUILD": dedent(
1279
                """\
1280
                docker_image(
1281
                  name="img1",
1282
                  build_platform=["linux/amd64", "linux/arm64", "linux/arm/v7"],
1283
                )
1284
                """
1285
            ),
1286
        }
1287
    )
1288

1289
    def check_docker_proc(process: Process):
1✔
UNCOV
1290
        assert process.argv == (
×
1291
            "/dummy/docker",
1292
            "build",
1293
            "--platform=linux/amd64,linux/arm64,linux/arm/v7",
1294
            "--pull=False",
1295
            "--tag",
1296
            "img1:latest",
1297
            "--file",
1298
            "docker/test/Dockerfile",
1299
            ".",
1300
        )
1301

1302
    assert_build(
1✔
1303
        rule_runner,
1304
        Address("docker/test", target_name="img1"),
1305
        process_assertions=check_docker_proc,
1306
    )
1307

1308

1309
def test_docker_build_labels_option(rule_runner: RuleRunner) -> None:
1✔
1310
    rule_runner.write_files(
1✔
1311
        {
1312
            "docker/test/BUILD": dedent(
1313
                """\
1314
                docker_image(
1315
                  name="img1",
1316
                  extra_build_args=[
1317
                    "BUILD_SLAVE=tbs06",
1318
                    "BUILD_NUMBER=13934",
1319
                  ],
1320
                  image_labels={
1321
                    "build.host": "{build_args.BUILD_SLAVE}",
1322
                    "build.job": "{build_args.BUILD_NUMBER}",
1323
                  }
1324
                )
1325
                """
1326
            ),
1327
        }
1328
    )
1329

1330
    def check_docker_proc(process: Process):
1✔
UNCOV
1331
        assert process.argv == (
×
1332
            "/dummy/docker",
1333
            "build",
1334
            "--label",
1335
            "build.host=tbs06",
1336
            "--label",
1337
            "build.job=13934",
1338
            "--pull=False",
1339
            "--tag",
1340
            "img1:latest",
1341
            "--build-arg",
1342
            "BUILD_NUMBER=13934",
1343
            "--build-arg",
1344
            "BUILD_SLAVE=tbs06",
1345
            "--file",
1346
            "docker/test/Dockerfile",
1347
            ".",
1348
        )
1349

1350
    assert_build(
1✔
1351
        rule_runner,
1352
        Address("docker/test", target_name="img1"),
1353
        process_assertions=check_docker_proc,
1354
    )
1355

1356

1357
@pytest.mark.parametrize("suggest_renames", [True, False])
1✔
1358
@pytest.mark.parametrize(
1✔
1359
    "context_root, copy_sources, build_context_files, expect_logged, fail_log_contains",
1360
    [
1361
        (
1362
            None,
1363
            ("src/project/bin.pex",),
1364
            ("src.project/binary.pex", "src/project/app.py"),
1365
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1366
            [
1367
                "suggested renames:\n\n  * src/project/bin.pex => src.project/binary.pex\n\n",
1368
                "There are files in the Docker build context that were not referenced by ",
1369
                "  * src/project/app.py\n\n",
1370
            ],
1371
        ),
1372
        (
1373
            "./",
1374
            ("config.txt",),
1375
            ("docker/test/conf/config.txt",),
1376
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1377
            [
1378
                "suggested renames:\n\n  * config.txt => conf/config.txt\n\n",
1379
            ],
1380
        ),
1381
        (
1382
            "./",
1383
            ("conf/config.txt",),
1384
            (
1385
                "docker/test/conf/config.txt",
1386
                "src.project/binary.pex",
1387
            ),
1388
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1389
            [
1390
                "There are unreachable files in these directories, excluded from the build context "
1391
                "due to `context_root` being 'docker/test':\n\n"
1392
                "  * src.project\n\n"
1393
                "Suggested `context_root` setting is '' in order to include all files in the "
1394
                "build context, otherwise relocate the files to be part of the current "
1395
                "`context_root` 'docker/test'."
1396
            ],
1397
        ),
1398
        (
1399
            "./config",
1400
            (),
1401
            (
1402
                "docker/test/config/..unusal-name",
1403
                "docker/test/config/.rc",
1404
                "docker/test/config/.a",
1405
                "docker/test/config/.conf.d/b",
1406
            ),
1407
            [(logging.WARNING, "Docker build failed for `docker_image` docker/test:test.")],
1408
            [
1409
                "There are files in the Docker build context that were not referenced by "
1410
                "any `COPY` instruction (this is not an error):\n"
1411
                "\n"
1412
                "  * ..unusal-name\n"
1413
                "  * .a\n"
1414
                "  * .conf.d/b\n"
1415
                "  * .rc\n"
1416
            ],
1417
        ),
1418
    ],
1419
)
1420
def test_docker_build_fail_logs(
1✔
1421
    rule_runner: RuleRunner,
1422
    caplog,
1423
    context_root: str | None,
1424
    copy_sources: tuple[str, ...],
1425
    build_context_files: tuple[str, ...],
1426
    expect_logged: list[tuple[int, str]] | None,
1427
    fail_log_contains: list[str],
1428
    suggest_renames: bool,
1429
) -> None:
1430
    caplog.set_level(logging.INFO)
1✔
1431
    rule_runner.write_files({"docker/test/BUILD": f"docker_image(context_root={context_root!r})"})
1✔
1432
    build_context_files = ("docker/test/Dockerfile", *build_context_files)
1✔
1433
    build_context_snapshot = rule_runner.make_snapshot_of_empty_files(build_context_files)
1✔
1434
    suggest_renames_arg = (
1✔
1435
        "--docker-suggest-renames" if suggest_renames else "--no-docker-suggest-renames"
1436
    )
1437
    rule_runner.set_options([suggest_renames_arg])
1✔
1438
    with pytest.raises(ProcessExecutionFailure):
1✔
1439
        assert_build(
1✔
1440
            rule_runner,
1441
            Address("docker/test"),
1442
            exit_code=1,
1443
            copy_sources=copy_sources,
1444
            build_context_snapshot=build_context_snapshot,
1445
        )
1446

UNCOV
1447
    assert_logged(caplog, expect_logged)
×
UNCOV
1448
    for msg in fail_log_contains:
×
UNCOV
1449
        if suggest_renames:
×
UNCOV
1450
            assert msg in caplog.records[0].message
×
1451
        else:
UNCOV
1452
            assert msg not in caplog.records[0].message
×
1453

1454

1455
@pytest.mark.parametrize(
1✔
1456
    "expected_target, options",
1457
    [
1458
        ("dev", None),
1459
        ("prod", {"build_target_stage": "prod", "default_repository": "{name}"}),
1460
    ],
1461
)
1462
def test_build_target_stage(
1✔
1463
    rule_runner: RuleRunner, options: dict | None, expected_target: str
1464
) -> None:
1465
    rule_runner.write_files(
1✔
1466
        {
1467
            "BUILD": "docker_image(name='image', target_stage='dev')",
1468
            "Dockerfile": dedent(
1469
                """\
1470
                FROM base as build
1471
                FROM build as dev
1472
                FROM build as prod
1473
                """
1474
            ),
1475
        }
1476
    )
1477

1478
    def check_docker_proc(process: Process):
1✔
UNCOV
1479
        assert process.argv == (
×
1480
            "/dummy/docker",
1481
            "build",
1482
            "--pull=False",
1483
            "--target",
1484
            expected_target,
1485
            "--tag",
1486
            "image:latest",
1487
            "--file",
1488
            "Dockerfile",
1489
            ".",
1490
        )
1491

1492
    assert_build(
1✔
1493
        rule_runner,
1494
        Address("", target_name="image"),
1495
        options=options,
1496
        process_assertions=check_docker_proc,
1497
        version_tags=("build latest", "dev latest", "prod latest"),
1498
        expected_registries_metadata=[
1499
            dict(
1500
                address=None,
1501
                alias=None,
1502
                repository="image",
1503
                tags=[
1504
                    dict(
1505
                        template="latest", tag="latest", uses_local_alias=False, name="image:latest"
1506
                    )
1507
                ],
1508
            )
1509
        ],
1510
    )
1511

1512

1513
def test_invalid_build_target_stage(rule_runner: RuleRunner) -> None:
1✔
1514
    rule_runner.write_files(
1✔
1515
        {
1516
            "BUILD": "docker_image(name='image', target_stage='bad')",
1517
            "Dockerfile": dedent(
1518
                """\
1519
                FROM base as build
1520
                FROM build as dev
1521
                FROM build as prod
1522
                """
1523
            ),
1524
        }
1525
    )
1526

1527
    err = (
1✔
1528
        r"The 'target_stage' field in `docker_image` //:image was set to 'bad', but there is no "
1529
        r"such stage in `Dockerfile`\. Available stages: build, dev, prod\."
1530
    )
1531
    with pytest.raises(DockerBuildTargetStageError, match=err):
1✔
1532
        assert_build(
1✔
1533
            rule_runner,
1534
            Address("", target_name="image"),
1535
            version_tags=("build latest", "dev latest", "prod latest"),
1536
        )
1537

1538

1539
@pytest.mark.parametrize(
1✔
1540
    "default_context_root, context_root, expected_context_root",
1541
    [
1542
        ("", None, "."),
1543
        (".", None, "."),
1544
        ("src", None, "src"),
1545
        (
1546
            "/",
1547
            None,
1548
            pytest.raises(
1549
                InvalidFieldException,
1550
                match=r"Use '' for a path relative to the build root, or '\./' for",
1551
            ),
1552
        ),
1553
        (
1554
            "/src",
1555
            None,
1556
            pytest.raises(
1557
                InvalidFieldException,
1558
                match=(
1559
                    r"The `context_root` field in target src/docker:image must be a relative path, "
1560
                    r"but was '/src'\. Use 'src' for a path relative to the build root, or '\./src' "
1561
                    r"for a path relative to the BUILD file \(i\.e\. 'src/docker/src'\)\."
1562
                ),
1563
            ),
1564
        ),
1565
        ("./", None, "src/docker"),
1566
        ("./build/context/", None, "src/docker/build/context"),
1567
        (".build/context/", None, ".build/context"),
1568
        ("ignored", "", "."),
1569
        ("ignored", ".", "."),
1570
        ("ignored", "src/context/", "src/context"),
1571
        ("ignored", "./", "src/docker"),
1572
        ("ignored", "src", "src"),
1573
        ("ignored", "./build/context", "src/docker/build/context"),
1574
    ],
1575
)
1576
def test_get_context_root(
1✔
1577
    context_root: str | None, default_context_root: str, expected_context_root: str | ContextManager
1578
) -> None:
1579
    if isinstance(expected_context_root, str):
1✔
1580
        raises = cast("ContextManager", no_exception())
1✔
1581
    else:
1582
        raises = expected_context_root
1✔
1583

1584
    with raises:
1✔
1585
        docker_options = create_subsystem(
1✔
1586
            DockerOptions,
1587
            default_context_root=default_context_root,
1588
        )
1589
        address = Address("src/docker", target_name="image")
1✔
1590
        tgt = DockerImageTarget({"context_root": context_root}, address)
1✔
1591
        fs = DockerPackageFieldSet.create(tgt)
1✔
1592
        actual_context_root = fs.get_context_root(docker_options.default_context_root)
1✔
1593
        assert actual_context_root == expected_context_root
1✔
1594

1595

1596
@pytest.mark.parametrize(
1✔
1597
    "docker, expected, stdout, stderr",
1598
    [
1599
        (
1600
            DockerBinary("/bin/docker", "1234", is_podman=False),
1601
            "<unknown>",
1602
            "",
1603
            "",
1604
        ),
1605
        # Docker
1606
        (
1607
            DockerBinary("/bin/docker", "1234", is_podman=False),
1608
            "0e09b442b572",
1609
            "",
1610
            dedent(
1611
                """\
1612
                Step 22/22 : LABEL job-url="https://jenkins.example.net/job/python_artefactsapi_pipeline/"
1613
                 ---> Running in ae5c3eac5c0b
1614
                Removing intermediate container ae5c3eac5c0b
1615
                 ---> 0e09b442b572
1616
                Successfully built 0e09b442b572
1617
                Successfully tagged docker.example.net/artefactsapi/master:3.6.5
1618
                """
1619
            ),
1620
        ),
1621
        # Buildkit without step duration
1622
        (
1623
            DockerBinary("/bin/docker", "1234", is_podman=False),
1624
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
1625
            dedent(
1626
                """\
1627
                #7 [2/2] COPY testprojects.src.python.hello.main/main.pex /hello
1628
                #7 sha256:843d0c804a7eb5ba08b0535b635d5f98a3e56bc43a3fbe7d226a8024176f00d1
1629
                #7 DONE 0.1s
1630

1631
                #8 exporting to image
1632
                #8 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
1633
                #8 exporting layers 0.0s done
1634
                #8 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 done
1635
                #8 naming to docker.io/library/test-example-synth:1.2.5 done
1636
                #8 DONE 0.0s
1637

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

1640
                """
1641
            ),
1642
            "",
1643
        ),
1644
        # Buildkit with step duration
1645
        (
1646
            DockerBinary("/bin/docker", "1234", is_podman=False),
1647
            "sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7",
1648
            dedent(
1649
                """\
1650
                #5 [2/2] RUN sleep 1
1651
                #5 DONE 1.1s
1652

1653
                #6 exporting to image
1654
                #6 exporting layers
1655
                #6 exporting layers 0.7s done
1656
                #6 writing image sha256:7805a7da5f45a70bb9e47e8de09b1f5acd8f479dda06fb144c5590b9d2b86dd7 0.0s done
1657
                #6 naming to docker.io/library/my-docker-image:latest 0.1s done
1658
                #6 DONE 1.1s
1659
                """
1660
            ),
1661
            "",
1662
        ),
1663
        # Buildkit with containerd-snapshotter 0.12.1
1664
        (
1665
            DockerBinary("/bin/docker", "1234", is_podman=False),
1666
            "sha256:b2b51838586286a9e544ddb31b3dbf7f6a99654d275b6e56b5f69f90138b4c0e",
1667
            dedent(
1668
                """\
1669
                #9 exporting to image
1670
                #9 exporting layers done
1671
                #9 exporting manifest sha256:7802087e8e0801f6451d862a00a6ce8af3e4829b09bc890dea0dd2659c11b25a done
1672
                #9 exporting config sha256:c83bed954709ba0c546d66d8f29afaac87c597f01b03fec158f3b21977c3e143 done
1673
                #9 exporting attestation manifest sha256:399891f9628cfafaba9e034599bdd55675ac0a3bad38151ed1ebf03993669545 done
1674
                #9 exporting manifest list sha256:b2b51838586286a9e544ddb31b3dbf7f6a99654d275b6e56b5f69f90138b4c0e done
1675
                #9 naming to myhost.com/my_app:latest done
1676
                #9 unpacking to myhost.com/my_app:latest done
1677
                #9 DONE 0.0s
1678
                """
1679
            ),
1680
            "",
1681
        ),
1682
        # Buildkit with containerd-snapshotter and cross platform 0.12.1
1683
        (
1684
            DockerBinary("/bin/docker", "1234", is_podman=False),
1685
            "sha256:3c72de0e05bb75247e68e124e6500700f6e0597425db2ee9f08fd59ef28cea0f",
1686
            dedent(
1687
                """\
1688
                #12 exporting to image
1689
                #12 exporting layers done
1690
                #12 exporting manifest sha256:452598369b55c27d752c45736cf26c0339612077f17df31fb0cdd79c5145d081 done
1691
                #12 exporting config sha256:6fbcebfde0ec24b487045516c3b5ffd3f0633e756a6d5808c2e5ad75809e0ca6 done
1692
                #12 exporting attestation manifest sha256:32fcf615e85bc9c2f606f863e8db3ca16dd77613a1e175e5972f39267e106dfb done
1693
                #12 exporting manifest sha256:bcb911a3efbec48e3c58c2acfd38fe92321eed731c53253f0b5c883918420187 done
1694
                #12 exporting config sha256:86e7fd0c4fa2356430d4ca188ed9e86497b8d03996ccba426d92c7e145e69990 done
1695
                #12 exporting attestation manifest sha256:66f9e7af29dd04e6264b8e113571f7b653f1681ba124a386530145fb39ff0102 done
1696
                #12 exporting manifest list sha256:3c72de0e05bb75247e68e124e6500700f6e0597425db2ee9f08fd59ef28cea0f done
1697
                #12 naming to myhost.com/my_app:latest done
1698
                #12 unpacking to myhost.com/my_app:latest done
1699
                #12 DONE 0.0s
1700
                """
1701
            ),
1702
            "",
1703
        ),
1704
        # Buildkit with containerd-snapshotter 0.13.1
1705
        (
1706
            DockerBinary("/bin/docker", "1234", is_podman=False),
1707
            "sha256:d15432046b4feaebb70370fad4710151dd8f0b9741cb8bc4d20c08ed8847f17a",
1708
            dedent(
1709
                """\
1710
                #13 exporting to image
1711
                #13 exporting layers
1712
                #13 exporting layers done
1713
                #13 exporting manifest sha256:2f161cf7c511874936d99995adeb53c6ac2262279a606bc1b70756ca1367ceb5 done
1714
                #13 exporting config sha256:23bf9de65f90e11ab7bb6bad0e1fb5c7eee3df2050aa902e8a53684fbd539eb9 done
1715
                #13 exporting attestation manifest sha256:5ff8bf97d8ad78a119d95d2b887400b3482a9026192ca7fb70307dfe290c93bf 0.0s done
1716
                #13 exporting manifest sha256:bf37d968d569812df393c7b6a48eab143066fa56a001905d9a70ec7acf3d34f4 done
1717
                #13 exporting config sha256:7c99f317cfae97e79dc12096279b71036a60129314e670920475665d466c821f done
1718
                #13 exporting attestation manifest sha256:4b3176781bb62e51cce743d4428e84e3559c9a23c328d6dfbfacac67f282cf70 0.0s done
1719
                #13 exporting manifest list sha256:d15432046b4feaebb70370fad4710151dd8f0b9741cb8bc4d20c08ed8847f17a 0.0s done
1720
                #13 naming to my-host.com/repo:latest done
1721
                #13 unpacking to my-host.com/repo:latest done
1722
                #13 DONE 0.1s
1723
                """
1724
            ),
1725
            "",
1726
        ),
1727
        # Buildkit with containerd-snapshotter 0.17.1 and disabled attestations
1728
        (
1729
            DockerBinary("/bin/docker", "1234", is_podman=False),
1730
            "sha256:6c3aff6414781126578b3e7b4a217682e89c616c0eac864d5b3ea7c87f1094d0",
1731
            dedent(
1732
                """\
1733
                    #24 exporting to image
1734
                    #24 exporting layers done
1735
                    #24 preparing layers for inline cache
1736
                    #24 preparing layers for inline cache 0.4s done
1737
                    #24 exporting manifest sha256:6c3aff6414781126578b3e7b4a217682e89c616c0eac864d5b3ea7c87f1094d0 0.0s done
1738
                    #24 exporting config sha256:af716170542d95134cb41b56e2dfea2c000b05b6fc4f440158ed9834ff96d1b4 0.0s done
1739
                    #24 naming to REDACTED:latest done
1740
                    #24 unpacking to REDACTED:latest 0.0s done
1741
                    #24 DONE 0.5s
1742

1743
                    """
1744
            ),
1745
            "",
1746
        ),
1747
        # Podman
1748
        (
1749
            DockerBinary("/bin/podman", "abcd", is_podman=True),
1750
            "a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee",
1751
            dedent(
1752
                """\
1753
                STEP 5/5: COPY ./ .
1754
                COMMIT example
1755
                --> a85499e9039a
1756
                Successfully tagged localhost/example:latest
1757
                a85499e9039a4add9712f7ea96a4aa9f0edd57d1008c6565822561ceed927eee
1758
                """
1759
            ),
1760
            "",
1761
        ),
1762
    ],
1763
)
1764
def test_parse_image_id_from_docker_build_output(
1✔
1765
    docker: DockerBinary, expected: str, stdout: str, stderr: str
1766
) -> None:
1767
    assert expected == parse_image_id_from_docker_build_output(
1✔
1768
        docker, stdout.encode(), stderr.encode()
1769
    )
1770

1771

1772
ImageRefTest = namedtuple(
1✔
1773
    "ImageRefTest",
1774
    "docker_image, registries, default_repository, expect_refs, expect_error",
1775
    defaults=({}, {}, "{name}", (), None),
1776
)
1777

1778

1779
@pytest.mark.parametrize(
1✔
1780
    "test",
1781
    [
1782
        ImageRefTest(
1783
            docker_image=dict(name="lowercase"),
1784
            expect_refs=(
1785
                ImageRefRegistry(
1786
                    registry=None,
1787
                    repository="lowercase",
1788
                    tags=(
1789
                        ImageRefTag(
1790
                            template="latest",
1791
                            formatted="latest",
1792
                            uses_local_alias=False,
1793
                            full_name="lowercase:latest",
1794
                        ),
1795
                    ),
1796
                ),
1797
            ),
1798
        ),
1799
        ImageRefTest(
1800
            docker_image=dict(name="CamelCase"),
1801
            expect_refs=(
1802
                ImageRefRegistry(
1803
                    registry=None,
1804
                    repository="camelcase",
1805
                    tags=(
1806
                        ImageRefTag(
1807
                            template="latest",
1808
                            formatted="latest",
1809
                            uses_local_alias=False,
1810
                            full_name="camelcase:latest",
1811
                        ),
1812
                    ),
1813
                ),
1814
            ),
1815
        ),
1816
        ImageRefTest(
1817
            docker_image=dict(image_tags=["CamelCase"]),
1818
            expect_refs=(
1819
                ImageRefRegistry(
1820
                    registry=None,
1821
                    repository="image",
1822
                    tags=(
1823
                        ImageRefTag(
1824
                            template="CamelCase",
1825
                            formatted="CamelCase",
1826
                            uses_local_alias=False,
1827
                            full_name="image:CamelCase",
1828
                        ),
1829
                    ),
1830
                ),
1831
            ),
1832
        ),
1833
        ImageRefTest(
1834
            docker_image=dict(image_tags=["{val1}", "prefix-{val2}"]),
1835
            expect_refs=(
1836
                ImageRefRegistry(
1837
                    registry=None,
1838
                    repository="image",
1839
                    tags=(
1840
                        ImageRefTag(
1841
                            template="{val1}",
1842
                            formatted="first-value",
1843
                            uses_local_alias=False,
1844
                            full_name="image:first-value",
1845
                        ),
1846
                        ImageRefTag(
1847
                            template="prefix-{val2}",
1848
                            formatted="prefix-second-value",
1849
                            uses_local_alias=False,
1850
                            full_name="image:prefix-second-value",
1851
                        ),
1852
                    ),
1853
                ),
1854
            ),
1855
        ),
1856
        ImageRefTest(
1857
            docker_image=dict(registries=["REG1.example.net"]),
1858
            expect_refs=(
1859
                ImageRefRegistry(
1860
                    registry=DockerRegistryOptions(address="REG1.example.net"),
1861
                    repository="image",
1862
                    tags=(
1863
                        ImageRefTag(
1864
                            template="latest",
1865
                            formatted="latest",
1866
                            uses_local_alias=False,
1867
                            full_name="REG1.example.net/image:latest",
1868
                        ),
1869
                    ),
1870
                ),
1871
            ),
1872
        ),
1873
        ImageRefTest(
1874
            docker_image=dict(registries=["docker.io", "@private"], repository="our-the/pkg"),
1875
            registries=dict(private={"address": "our.registry", "repository": "the/pkg"}),
1876
            expect_refs=(
1877
                ImageRefRegistry(
1878
                    registry=DockerRegistryOptions(address="docker.io"),
1879
                    repository="our-the/pkg",
1880
                    tags=(
1881
                        ImageRefTag(
1882
                            template="latest",
1883
                            formatted="latest",
1884
                            uses_local_alias=False,
1885
                            full_name="docker.io/our-the/pkg:latest",
1886
                        ),
1887
                    ),
1888
                ),
1889
                ImageRefRegistry(
1890
                    registry=DockerRegistryOptions(
1891
                        alias="private", address="our.registry", repository="the/pkg"
1892
                    ),
1893
                    repository="the/pkg",
1894
                    tags=(
1895
                        ImageRefTag(
1896
                            template="latest",
1897
                            formatted="latest",
1898
                            uses_local_alias=False,
1899
                            full_name="our.registry/the/pkg:latest",
1900
                        ),
1901
                    ),
1902
                ),
1903
            ),
1904
        ),
1905
        ImageRefTest(
1906
            docker_image=dict(
1907
                registries=["docker.io", "@private"],
1908
                repository="{parent_directory}/{default_repository}",
1909
            ),
1910
            registries=dict(
1911
                private={"address": "our.registry", "repository": "{target_repository}/the/pkg"}
1912
            ),
1913
            expect_refs=(
1914
                ImageRefRegistry(
1915
                    registry=DockerRegistryOptions(address="docker.io"),
1916
                    repository="test/image",
1917
                    tags=(
1918
                        ImageRefTag(
1919
                            template="latest",
1920
                            formatted="latest",
1921
                            uses_local_alias=False,
1922
                            full_name="docker.io/test/image:latest",
1923
                        ),
1924
                    ),
1925
                ),
1926
                ImageRefRegistry(
1927
                    registry=DockerRegistryOptions(
1928
                        alias="private",
1929
                        address="our.registry",
1930
                        repository="{target_repository}/the/pkg",
1931
                    ),
1932
                    repository="test/image/the/pkg",
1933
                    tags=(
1934
                        ImageRefTag(
1935
                            template="latest",
1936
                            formatted="latest",
1937
                            uses_local_alias=False,
1938
                            full_name="our.registry/test/image/the/pkg:latest",
1939
                        ),
1940
                    ),
1941
                ),
1942
            ),
1943
        ),
1944
        ImageRefTest(
1945
            docker_image=dict(registries=["@private"], image_tags=["prefix-{val1}"]),
1946
            registries=dict(
1947
                private={"address": "our.registry", "extra_image_tags": ["{val2}-suffix"]}
1948
            ),
1949
            expect_refs=(
1950
                ImageRefRegistry(
1951
                    registry=DockerRegistryOptions(
1952
                        alias="private",
1953
                        address="our.registry",
1954
                        extra_image_tags=("{val2}-suffix",),
1955
                    ),
1956
                    repository="image",
1957
                    tags=(
1958
                        ImageRefTag(
1959
                            template="prefix-{val1}",
1960
                            formatted="prefix-first-value",
1961
                            uses_local_alias=False,
1962
                            full_name="our.registry/image:prefix-first-value",
1963
                        ),
1964
                        ImageRefTag(
1965
                            template="{val2}-suffix",
1966
                            formatted="second-value-suffix",
1967
                            uses_local_alias=False,
1968
                            full_name="our.registry/image:second-value-suffix",
1969
                        ),
1970
                    ),
1971
                ),
1972
            ),
1973
        ),
1974
        ImageRefTest(
1975
            docker_image=dict(repository="{default_repository}/a"),
1976
            default_repository="{target_repository}/b",
1977
            expect_error=pytest.raises(
1978
                InterpolationError,
1979
                match=(
1980
                    r"Invalid value for the `repository` field of the `docker_image` target at "
1981
                    r"src/test/docker:image: '\{default_repository\}/a'\.\n\n"
1982
                    r"The formatted placeholders recurse too deep\.\n"
1983
                    r"'\{default_repository\}/a' => '\{target_repository\}/b/a' => "
1984
                    r"'\{default_repository\}/a/b/a'"
1985
                ),
1986
            ),
1987
        ),
1988
        ImageRefTest(
1989
            # Test registry `use_local_alias` (#16354)
1990
            docker_image=dict(registries=["docker.io", "@private"], repository="our-the/pkg"),
1991
            registries=dict(
1992
                private={
1993
                    "address": "our.registry",
1994
                    "repository": "the/pkg",
1995
                    "use_local_alias": True,
1996
                }
1997
            ),
1998
            expect_refs=(
1999
                ImageRefRegistry(
2000
                    registry=DockerRegistryOptions(address="docker.io"),
2001
                    repository="our-the/pkg",
2002
                    tags=(
2003
                        ImageRefTag(
2004
                            template="latest",
2005
                            formatted="latest",
2006
                            uses_local_alias=False,
2007
                            full_name="docker.io/our-the/pkg:latest",
2008
                        ),
2009
                    ),
2010
                ),
2011
                ImageRefRegistry(
2012
                    registry=DockerRegistryOptions(
2013
                        alias="private",
2014
                        address="our.registry",
2015
                        repository="the/pkg",
2016
                        use_local_alias=True,
2017
                    ),
2018
                    repository="the/pkg",
2019
                    tags=(
2020
                        ImageRefTag(
2021
                            template="latest",
2022
                            formatted="latest",
2023
                            uses_local_alias=False,
2024
                            full_name="our.registry/the/pkg:latest",
2025
                        ),
2026
                        ImageRefTag(
2027
                            template="latest",
2028
                            formatted="latest",
2029
                            uses_local_alias=True,
2030
                            full_name="private/the/pkg:latest",
2031
                        ),
2032
                    ),
2033
                ),
2034
            ),
2035
        ),
2036
    ],
2037
)
2038
def test_image_ref_formatting(test: ImageRefTest) -> None:
1✔
2039
    address = Address("src/test/docker", target_name=test.docker_image.pop("name", "image"))
1✔
2040
    tgt = DockerImageTarget(test.docker_image, address)
1✔
2041
    field_set = DockerPackageFieldSet.create(tgt)
1✔
2042
    registries = DockerRegistries.from_dict(test.registries)
1✔
2043
    interpolation_context = InterpolationContext.from_dict(
1✔
2044
        {"val1": "first-value", "val2": "second-value"}
2045
    )
2046
    with test.expect_error or no_exception():
1✔
2047
        image_refs = field_set.image_refs(
1✔
2048
            test.default_repository, registries, interpolation_context
2049
        )
2050
        assert tuple(image_refs) == test.expect_refs
1✔
2051

2052

2053
@pytest.mark.parametrize(
1✔
2054
    "BUILD, plugin_tags, tag_flags",
2055
    [
2056
        (
2057
            'docker_image(name="plugin")',
2058
            ("1.2.3",),
2059
            (
2060
                "--tag",
2061
                "plugin:latest",
2062
                "--tag",
2063
                "plugin:1.2.3",
2064
            ),
2065
        ),
2066
        (
2067
            'docker_image(name="plugin", image_tags=[])',
2068
            ("1.2.3",),
2069
            (
2070
                "--tag",
2071
                "plugin:1.2.3",
2072
            ),
2073
        ),
2074
    ],
2075
)
2076
def test_docker_image_tags_from_plugin_hook(
1✔
2077
    rule_runner: RuleRunner, BUILD: str, plugin_tags: tuple[str, ...], tag_flags: tuple[str, ...]
2078
) -> None:
2079
    rule_runner.write_files({"docker/test/BUILD": BUILD})
1✔
2080

2081
    def check_docker_proc(process: Process):
1✔
UNCOV
2082
        assert process.argv == (
×
2083
            "/dummy/docker",
2084
            "build",
2085
            "--pull=False",
2086
            *tag_flags,
2087
            "--file",
2088
            "docker/test/Dockerfile",
2089
            ".",
2090
        )
2091

2092
    assert_build(
1✔
2093
        rule_runner,
2094
        Address("docker/test", target_name="plugin"),
2095
        process_assertions=check_docker_proc,
2096
        plugin_tags=plugin_tags,
2097
    )
2098

2099

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

2103
    err = "The `image_tags` field in target docker/test:no-tags must not be empty, unless"
1✔
2104
    with pytest.raises(InvalidFieldException, match=err):
1✔
2105
        assert_build(
1✔
2106
            rule_runner,
2107
            Address("docker/test", target_name="no-tags"),
2108
        )
2109

2110

2111
def test_docker_info_serialize() -> None:
1✔
2112
    image_id = "abc123"
1✔
2113
    # image refs with unique strings (i.e. not actual templates/names etc.), to make sure they're
2114
    # ending up in the right place in the JSON
2115
    image_refs = (
1✔
2116
        ImageRefRegistry(
2117
            registry=None,
2118
            repository="repo",
2119
            tags=(
2120
                ImageRefTag(
2121
                    template="repo tag1 template",
2122
                    formatted="repo tag1 formatted",
2123
                    uses_local_alias=False,
2124
                    full_name="repo tag1 full name",
2125
                ),
2126
                ImageRefTag(
2127
                    template="repo tag2 template",
2128
                    formatted="repo tag2 formatted",
2129
                    uses_local_alias=False,
2130
                    full_name="repo tag2 full name",
2131
                ),
2132
            ),
2133
        ),
2134
        ImageRefRegistry(
2135
            registry=DockerRegistryOptions(address="address"),
2136
            repository="address repo",
2137
            tags=(
2138
                ImageRefTag(
2139
                    template="address tag template",
2140
                    formatted="address tag formatted",
2141
                    uses_local_alias=False,
2142
                    full_name="address tag full name",
2143
                ),
2144
            ),
2145
        ),
2146
        ImageRefRegistry(
2147
            registry=DockerRegistryOptions(
2148
                address="alias address", alias="alias", repository="alias registry repo"
2149
            ),
2150
            repository="alias repo",
2151
            tags=(
2152
                ImageRefTag(
2153
                    template="alias tag (address) template",
2154
                    formatted="alias tag (address) formatted",
2155
                    uses_local_alias=False,
2156
                    full_name="alias tag (address) full name",
2157
                ),
2158
                ImageRefTag(
2159
                    template="alias tag (local alias) template",
2160
                    formatted="alias tag (local alias) formatted",
2161
                    uses_local_alias=True,
2162
                    full_name="alias tag (local alias) full name",
2163
                ),
2164
            ),
2165
        ),
2166
    )
2167

2168
    expected = dict(
1✔
2169
        version=1,
2170
        image_id=image_id,
2171
        registries=[
2172
            dict(
2173
                alias=None,
2174
                address=None,
2175
                repository="repo",
2176
                tags=[
2177
                    dict(
2178
                        template="repo tag1 template",
2179
                        tag="repo tag1 formatted",
2180
                        uses_local_alias=False,
2181
                        name="repo tag1 full name",
2182
                    ),
2183
                    dict(
2184
                        template="repo tag2 template",
2185
                        tag="repo tag2 formatted",
2186
                        uses_local_alias=False,
2187
                        name="repo tag2 full name",
2188
                    ),
2189
                ],
2190
            ),
2191
            dict(
2192
                alias=None,
2193
                address="address",
2194
                repository="address repo",
2195
                tags=[
2196
                    dict(
2197
                        template="address tag template",
2198
                        tag="address tag formatted",
2199
                        uses_local_alias=False,
2200
                        name="address tag full name",
2201
                    )
2202
                ],
2203
            ),
2204
            dict(
2205
                alias="alias",
2206
                address="alias address",
2207
                repository="alias repo",
2208
                tags=[
2209
                    dict(
2210
                        template="alias tag (address) template",
2211
                        tag="alias tag (address) formatted",
2212
                        uses_local_alias=False,
2213
                        name="alias tag (address) full name",
2214
                    ),
2215
                    dict(
2216
                        template="alias tag (local alias) template",
2217
                        tag="alias tag (local alias) formatted",
2218
                        uses_local_alias=True,
2219
                        name="alias tag (local alias) full name",
2220
                    ),
2221
                ],
2222
            ),
2223
        ],
2224
    )
2225

2226
    result = DockerInfoV1.serialize(image_refs, image_id)
1✔
2227
    assert json.loads(result) == expected
1✔
2228

2229

2230
@pytest.mark.parametrize(
1✔
2231
    ("output", "expected"),
2232
    [({"type": "image", "push": "true"}, True), ({"type": "registry"}, True), (None, False)],
2233
)
2234
def test_field_set_pushes_on_package(output: dict | None, expected: bool) -> None:
1✔
2235
    rule_runner = RuleRunner(target_types=[DockerImageTarget])
1✔
2236
    output_str = f", output={output}" if output else ""
1✔
2237
    rule_runner.write_files(
1✔
2238
        {"BUILD": f"docker_image(name='image', source='Dockerfile'{output_str})"}
2239
    )
2240
    field_set = DockerPackageFieldSet.create(
1✔
2241
        rule_runner.get_target(Address("", target_name="image"))
2242
    )
2243
    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