Repository: pantsbuild/pants

Issue:
Allow multiple registry configurations with the same address
**Is your feature request related to a problem? Please describe.**

I am pushing images to a central AWS account, the registry address is the same for all repositories in the account: $ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com.

I have some repositories that engineers can push to directly, and others that limit writes to the CI principals. 

For example, I have a docker image that we use as an environment for some build steps. I would like to build this image in CI and make it available to engineers, but they should not be able to push new versions.


**Describe the solution you'd like**

I would like to have two registry configurations, one exclusively for CI and one for tinkering, pointing to the same address. That allows me to mark some repositories as `skip_push` in pants.toml, but enable push from pants.ci.toml

**Describe alternatives you've considered**
I can just mark each image as skip_push and then push them manually from the CI host, but it's ungainly.
I could set up a DNS name for the protected registry so that they're disambiguated.
I'm probably going to add a tag to the targets that should only run on CI and disable them in pants.toml

**Additional context**
There's a repo [here](https://github.com/bobthemighty/pants-docker-registry-collision) showing the problem, from what I can tell,  the addresses collide because they're used to match the registry with tags [here](https://github.com/pantsbuild/pants/blob/main/src/python/pants/backend/docker/goals/publish.py#L87) in the publish goal.
 


Discussion:
I'm unsure this *is* a bug. I think my mental model was wrong here. In the sample repo, the two toml entries clearly point to the same actual registry. I'm trying to use the "registry" as an arbitrary group of docker targets, but idiomatically I think that should be a target tag.

With my proposal, the end user will still build the unpushed images locally with `pants package ::`, which clearly isn't what I want.

I think this might be better handled with an error from the config validator to assert that registry addresses must be unique, and maybe a helpful hint in the docs about target selection.

 I'm happy to make that change with some guidance on warnings vs errors for breaking changes.
> I'm happy to make that change with some guidance on warnings vs errors for breaking changes.

Awesome.

Good points. To solve this by actively rejecting duplicated registry addresses in the configuration works for me.

I think we can safely error in this case as if there are users out there with duplicated addresses in their configuration, they're broken any way.

Resolving patch:
diff --git a/src/python/pants/backend/docker/registries.py b/src/python/pants/backend/docker/registries.py
--- a/src/python/pants/backend/docker/registries.py
+++ b/src/python/pants/backend/docker/registries.py
@@ -8,6 +8,7 @@
 
 from pants.option.parser import Parser
 from pants.util.frozendict import FrozenDict
+from pants.util.strutil import softwrap
 
 ALL_DEFAULT_REGISTRIES = "<all default registries>"
 
@@ -24,6 +25,18 @@ def __init__(self, message):
         )
 
 
+class DockerRegistryAddressCollisionError(DockerRegistryError):
+    def __init__(self, first, second):
+        message = softwrap(
+            f"""
+            Duplicated docker registry address for aliases: {first.alias}, {second.alias}.
+            Each registry `address` in `[docker].registries` must be unique.
+            """
+        )
+
+        super().__init__(message)
+
+
 @dataclass(frozen=True)
 class DockerRegistryOptions:
     address: str
@@ -49,6 +62,9 @@ def from_dict(cls, alias: str, d: dict[str, Any]) -> DockerRegistryOptions:
         )
 
     def register(self, registries: dict[str, DockerRegistryOptions]) -> None:
+        if self.address in registries:
+            collision = registries[self.address]
+            raise DockerRegistryAddressCollisionError(collision, self)
         registries[self.address] = self
         if self.alias:
             registries[f"@{self.alias}"] = self


Test patch:
diff --git a/src/python/pants/backend/docker/registries_test.py b/src/python/pants/backend/docker/registries_test.py
--- a/src/python/pants/backend/docker/registries_test.py
+++ b/src/python/pants/backend/docker/registries_test.py
@@ -5,6 +5,7 @@
 
 from pants.backend.docker.registries import (
     DockerRegistries,
+    DockerRegistryAddressCollisionError,
     DockerRegistryOptions,
     DockerRegistryOptionsNotFoundError,
 )
@@ -96,3 +97,15 @@ def test_repository() -> None:
     )
     (reg1,) = registries.get("@reg1")
     assert reg1.repository == "{name}/foo"
+
+
+def test_registries_must_be_unique() -> None:
+    with pytest.raises(DockerRegistryAddressCollisionError) as e:
+        DockerRegistries.from_dict(
+            {
+                "reg1": {"address": "mydomain:port"},
+                "reg2": {"address": "mydomain:port"},
+            }
+        )
+
+    assert e.match("Duplicated docker registry address for aliases: reg1, reg2.")