From 8f4ac103619fe9e22fdea0d38ab740ddeb6bc1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 5 Nov 2020 10:39:53 +0100 Subject: [PATCH] Add login support for dockerhub (#2218) * Add login support for dockerhub * Remove registry --- supervisor/const.py | 1 + supervisor/docker/interface.py | 62 +++++++++++++++++++++++++------- tests/docker/test_credentials.py | 40 +++++++++++++++++++++ 3 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 tests/docker/test_credentials.py diff --git a/supervisor/const.py b/supervisor/const.py index a95301a4d..bb1f84f25 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -227,6 +227,7 @@ ATTR_PROVIDERS = "providers" ATTR_RATING = "rating" ATTR_REFRESH_TOKEN = "refresh_token" ATTR_REGISTRIES = "registries" +ATTR_REGISTRY = "registry" ATTR_REPOSITORIES = "repositories" ATTR_REPOSITORY = "repository" ATTR_SCHEMA = "schema" diff --git a/supervisor/docker/interface.py b/supervisor/docker/interface.py index 5d06645d4..051ec36fc 100644 --- a/supervisor/docker/interface.py +++ b/supervisor/docker/interface.py @@ -10,7 +10,13 @@ from packaging import version as pkg_version import requests from . import CommandReturn -from ..const import ATTR_PASSWORD, ATTR_USERNAME, LABEL_ARCH, LABEL_VERSION +from ..const import ( + ATTR_PASSWORD, + ATTR_REGISTRY, + ATTR_USERNAME, + LABEL_ARCH, + LABEL_VERSION, +) from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import DockerAPIError, DockerError, DockerNotFound, DockerRequestError from ..utils import process_lock @@ -19,6 +25,7 @@ from .stats import DockerStats _LOGGER: logging.Logger = logging.getLogger(__name__) IMAGE_WITH_HOST = re.compile(r"^((?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,})\/.+") +DOCKER_HUB = "hub.docker.com" class DockerInterface(CoreSysAttributes): @@ -87,17 +94,46 @@ class DockerInterface(CoreSysAttributes): """Pull docker image.""" return self.sys_run_in_executor(self._install, tag, image, latest) - def _docker_login(self, hostname: str) -> None: - """Try to log in to the registry if there are credentials available.""" - if hostname in self.sys_docker.config.registries: - credentials = self.sys_docker.config.registries[hostname] + def _get_credentials(self, image: str) -> dict: + """Return a dictionay with credentials for docker login.""" + registry = None + credentials = {} + matcher = IMAGE_WITH_HOST.match(image) - self.sys_docker.docker.login( - registry=hostname, - username=credentials[ATTR_USERNAME], - password=credentials[ATTR_PASSWORD], + # Custom registry + if matcher: + if matcher.group(1) in self.sys_docker.config.registries: + registry = matcher.group(1) + credentials[ATTR_REGISTRY] = registry + + # If no match assume "dockerhub" as registry + elif DOCKER_HUB in self.sys_docker.config.registries: + registry = DOCKER_HUB + + if registry: + stored = self.sys_docker.config.registries[registry] + credentials[ATTR_USERNAME] = stored[ATTR_USERNAME] + credentials[ATTR_PASSWORD] = stored[ATTR_PASSWORD] + + _LOGGER.debug( + "Logging in to %s as %s", + registry, + stored[ATTR_USERNAME], ) + return credentials + + def _docker_login(self, image: str) -> None: + """Try to log in to the registry if there are credentials available.""" + if not self.sys_docker.config.registries: + return + + credentials = self._get_credentials(image) + if not credentials: + return + + self.sys_docker.docker.login(**credentials) + def _install( self, tag: str, image: Optional[str] = None, latest: bool = False ) -> None: @@ -109,10 +145,10 @@ class DockerInterface(CoreSysAttributes): _LOGGER.info("Downloading docker image %s with tag %s.", image, tag) try: - # If the image name contains a path to a registry, try to log in - path = IMAGE_WITH_HOST.match(image) - if path: - self._docker_login(path.group(1)) + if self.sys_docker.config.registries: + # Try login if we have defined credentials + self._docker_login(image) + docker_image = self.sys_docker.images.pull(f"{image}:{tag}") if latest: _LOGGER.info("Tagging image %s with version %s as latest", image, tag) diff --git a/tests/docker/test_credentials.py b/tests/docker/test_credentials.py new file mode 100644 index 000000000..6b311bb64 --- /dev/null +++ b/tests/docker/test_credentials.py @@ -0,0 +1,40 @@ +"""Test docker login.""" +# pylint: disable=protected-access +from supervisor.coresys import CoreSys +from supervisor.docker.interface import DOCKER_HUB, DockerInterface + + +def test_no_credentials(coresys: CoreSys): + """Test no credentials.""" + docker = DockerInterface(coresys) + coresys.docker.config.registries = { + DOCKER_HUB: {"username": "Spongebob Squarepants", "password": "Password1!"} + } + assert not docker._get_credentials("ghcr.io/homeassistant") + assert not docker._get_credentials("ghcr.io/homeassistant/amd64-supervisor") + + +def test_no_matching_credentials(coresys: CoreSys): + """Test no matching credentials.""" + docker = DockerInterface(coresys) + coresys.docker.config.registries = { + DOCKER_HUB: {"username": "Spongebob Squarepants", "password": "Password1!"} + } + assert not docker._get_credentials("ghcr.io/homeassistant") + assert not docker._get_credentials("ghcr.io/homeassistant/amd64-supervisor") + + +def test_matching_credentials(coresys: CoreSys): + """Test no matching credentials.""" + docker = DockerInterface(coresys) + coresys.docker.config.registries = { + "ghcr.io": {"username": "Octocat", "password": "Password1!"}, + DOCKER_HUB: {"username": "Spongebob Squarepants", "password": "Password1!"}, + } + + credentials = docker._get_credentials("ghcr.io/homeassistant/amd64-supervisor") + assert credentials["registry"] == "ghcr.io" + + credentials = docker._get_credentials("homeassistant/amd64-supervisor") + assert credentials["username"] == "Spongebob Squarepants" + assert "registry" not in credentials