Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: testcontainers/testcontainers-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: testcontainers-v4.7.1
Choose a base ref
...
head repository: testcontainers/testcontainers-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: testcontainers-v4.7.2
Choose a head ref
  • 5 commits
  • 16 files changed
  • 5 contributors

Commits on Jul 4, 2024

  1. fix(kafka): add a flag to limit to first hostname for use with networ…

    …ks (#638)
    
    fix #637
    alexanderankin authored Jul 4, 2024

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    0ce4fec View commit details

Commits on Jul 15, 2024

  1. fix: Add container Trino (#642)

    Resolve #641
    grieve54706 authored Jul 15, 2024

    Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    49ce5a5 View commit details
  2. fix(modules): Mailpit container base API URL helper method (#643)

    Minimal change to expose some helper methods on the `MailpitContainer`
    oliverlambson authored Jul 15, 2024
    Copy the full SHA
    df07586 View commit details
  3. fix(core): Improve private registry support (tolerate not implemented…

    … fields in DOCKER_AUTH_CONFIG) (#647)
    
    Continuing #562, got some feedback regarding an issue with unsupported
    use cases.
    In this PR we will try to:
    1. Map the use cases
    2. Raise a warning regarding unsupported uses cases (hopefully they will
    be added later)
    3. Address/Fix the issue where unsupported JSON schema for
    `DOCKER_AUTH_CONFIG` leads to an error
    
    As always any feedback will be much appreciated.
    _Please note this PR does not implement all use-cases just does a better
    job at preparing and handling them for now_
    Tranquility2 authored Jul 15, 2024
    Copy the full SHA
    766c382 View commit details
  4. chore(main): release testcontainers 4.7.2 (#639)

    🤖 I have created a release *beep* *boop*
    ---
    
    
    ##
    [4.7.2](testcontainers-v4.7.1...testcontainers-v4.7.2)
    (2024-07-15)
    
    
    ### Bug Fixes
    
    * Add container Trino
    ([#642](#642))
    ([49ce5a5](49ce5a5)),
    closes
    [#641](#641)
    * **core:** Improve private registry support (tolerate not implemented
    fields in DOCKER_AUTH_CONFIG)
    ([#647](#647))
    ([766c382](766c382))
    * **kafka:** add a flag to limit to first hostname for use with networks
    ([#638](#638))
    ([0ce4fec](0ce4fec)),
    closes
    [#637](#637)
    * **modules:** Mailpit container base API URL helper method
    ([#643](#643))
    ([df07586](df07586))
    
    ---
    This PR was generated with [Release
    Please](https://github.com/googleapis/release-please). See
    [documentation](https://github.com/googleapis/release-please#release-please).
    
    Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
    github-actions[bot] authored Jul 15, 2024
    Copy the full SHA
    068c431 View commit details
2 changes: 1 addition & 1 deletion .github/.release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "4.7.1"
".": "4.7.2"
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [4.7.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.7.1...testcontainers-v4.7.2) (2024-07-15)


### Bug Fixes

* Add container Trino ([#642](https://github.com/testcontainers/testcontainers-python/issues/642)) ([49ce5a5](https://github.com/testcontainers/testcontainers-python/commit/49ce5a5ff2ac46cf51920e16c5e39684886b699a)), closes [#641](https://github.com/testcontainers/testcontainers-python/issues/641)
* **core:** Improve private registry support (tolerate not implemented fields in DOCKER_AUTH_CONFIG) ([#647](https://github.com/testcontainers/testcontainers-python/issues/647)) ([766c382](https://github.com/testcontainers/testcontainers-python/commit/766c382a3aee4eb512ee0f482d6595d3412097c3))
* **kafka:** add a flag to limit to first hostname for use with networks ([#638](https://github.com/testcontainers/testcontainers-python/issues/638)) ([0ce4fec](https://github.com/testcontainers/testcontainers-python/commit/0ce4fecb2872620fd4cb96313abcba4353442cfd)), closes [#637](https://github.com/testcontainers/testcontainers-python/issues/637)
* **modules:** Mailpit container base API URL helper method ([#643](https://github.com/testcontainers/testcontainers-python/issues/643)) ([df07586](https://github.com/testcontainers/testcontainers-python/commit/df07586d8844c757db62ac0f8b7914c67fd96e05))

## [4.7.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.7.0...testcontainers-v4.7.1) (2024-07-02)


88 changes: 88 additions & 0 deletions core/testcontainers/core/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import base64 as base64
import json as json
from collections import namedtuple
from logging import warning
from typing import Optional

DockerAuthInfo = namedtuple("DockerAuthInfo", ["registry", "username", "password"])

_AUTH_WARNINGS = {
"credHelpers": "DOCKER_AUTH_CONFIG is experimental, credHelpers not supported yet",
"credsStore": "DOCKER_AUTH_CONFIG is experimental, credsStore not supported yet",
}


def process_docker_auth_config_encoded(auth_config_dict: dict) -> list[DockerAuthInfo]:
"""
Process the auths config.
Example:
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
}
}
}
Returns a list of DockerAuthInfo objects.
"""
auth_info: list[DockerAuthInfo] = []

auths = auth_config_dict.get("auths")
for registry, auth in auths.items():
auth_str = auth.get("auth")
auth_str = base64.b64decode(auth_str).decode("utf-8")
username, password = auth_str.split(":")
auth_info.append(DockerAuthInfo(registry, username, password))

return auth_info


def process_docker_auth_config_cred_helpers(auth_config_dict: dict) -> None:
"""
Process the credHelpers config.
Example:
{
"credHelpers": {
"<aws_account_id>.dkr.ecr.<region>.amazonaws.com": "ecr-login"
}
}
This is not supported yet.
"""
if "credHelpers" in _AUTH_WARNINGS:
warning(_AUTH_WARNINGS.pop("credHelpers"))


def process_docker_auth_config_store(auth_config_dict: dict) -> None:
"""
Process the credsStore config.
Example:
{
"credsStore": "ecr-login"
}
This is not supported yet.
"""
if "credsStore" in _AUTH_WARNINGS:
warning(_AUTH_WARNINGS.pop("credsStore"))


def parse_docker_auth_config(auth_config: str) -> Optional[list[DockerAuthInfo]]:
"""Parse the docker auth config from a string and handle the different formats."""
try:
auth_config_dict: dict = json.loads(auth_config)
if "credHelpers" in auth_config:
process_docker_auth_config_cred_helpers(auth_config_dict)
if "credsStore" in auth_config:
process_docker_auth_config_store(auth_config_dict)
if "auths" in auth_config:
return process_docker_auth_config_encoded(auth_config_dict)

except (json.JSONDecodeError, KeyError, ValueError) as exp:
raise ValueError("Could not parse docker auth config") from exp

return None
11 changes: 7 additions & 4 deletions core/testcontainers/core/docker_client.py
Original file line number Diff line number Diff line change
@@ -24,9 +24,10 @@
from docker.models.images import Image, ImageCollection
from typing_extensions import ParamSpec

from testcontainers.core.auth import DockerAuthInfo, parse_docker_auth_config
from testcontainers.core.config import testcontainers_config as c
from testcontainers.core.labels import SESSION_ID, create_labels
from testcontainers.core.utils import default_gateway_ip, inside_container, parse_docker_auth_config, setup_logger
from testcontainers.core.utils import default_gateway_ip, inside_container, setup_logger

LOGGER = setup_logger(__name__)

@@ -67,8 +68,11 @@ def __init__(self, **kwargs) -> None:
self.client.api.headers["x-tc-sid"] = SESSION_ID
self.client.api.headers["User-Agent"] = "tc-python/" + importlib.metadata.version("testcontainers")

# Verify if we have a docker auth config and login if we do
if docker_auth_config := get_docker_auth_config():
self.login(docker_auth_config)
LOGGER.debug(f"DOCKER_AUTH_CONFIG found: {docker_auth_config}")
if auth_config := parse_docker_auth_config(docker_auth_config):
self.login(auth_config[0]) # Only using the first auth config)

@_wrapped_container_collection
def run(
@@ -203,11 +207,10 @@ def host(self) -> str:
return ip_address
return "localhost"

def login(self, docker_auth_config: str) -> None:
def login(self, auth_config: DockerAuthInfo) -> None:
"""
Login to a docker registry using the given auth config.
"""
auth_config = parse_docker_auth_config(docker_auth_config)[0] # Only using the first auth config
login_info = self.client.login(**auth_config._asdict())
LOGGER.debug(f"logged in using {login_info}")

31 changes: 0 additions & 31 deletions core/testcontainers/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import base64
import json
import logging
import os
import platform
import subprocess
import sys
from collections import namedtuple

LINUX = "linux"
MAC = "mac"
WIN = "win"

DockerAuthInfo = namedtuple("DockerAuthInfo", ["registry", "username", "password"])


def setup_logger(name: str) -> logging.Logger:
logger = logging.getLogger(name)
@@ -82,29 +77,3 @@ def raise_for_deprecated_parameter(kwargs: dict, name: str, replacement: str) ->
if kwargs.pop(name, None):
raise ValueError(f"Use `{replacement}` instead of `{name}`")
return kwargs


def parse_docker_auth_config(auth_config: str) -> list[DockerAuthInfo]:
"""
Parse the docker auth config from a string.
Example:
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
}
}
}
"""
auth_info: list[DockerAuthInfo] = []
try:
auth_config_dict: dict = json.loads(auth_config).get("auths")
for registry, auth in auth_config_dict.items():
auth_str = auth.get("auth")
auth_str = base64.b64decode(auth_str).decode("utf-8")
username, password = auth_str.split(":")
auth_info.append(DockerAuthInfo(registry, username, password))
return auth_info
except (json.JSONDecodeError, KeyError, ValueError) as exp:
raise ValueError("Could not parse docker auth config") from exp
84 changes: 84 additions & 0 deletions core/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import json
import pytest

from testcontainers.core.auth import parse_docker_auth_config, DockerAuthInfo


def test_parse_docker_auth_config_encoded():
auth_config_json = '{"auths":{"https://index.docker.io/v1/":{"auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}}'
auth_info = parse_docker_auth_config(auth_config_json)
assert len(auth_info) == 1
assert auth_info[0] == DockerAuthInfo(
registry="https://index.docker.io/v1/",
username="username",
password="password",
)


def test_parse_docker_auth_config_cred_helpers():
auth_dict = {"credHelpers": {"<aws_account_id>.dkr.ecr.<region>.amazonaws.com": "ecr-login"}}
auth_config_json = json.dumps(auth_dict)
assert parse_docker_auth_config(auth_config_json) is None


def test_parse_docker_auth_config_store():
auth_dict = {"credsStore": "ecr-login"}
auth_config_json = json.dumps(auth_dict)
assert parse_docker_auth_config(auth_config_json) is None


def test_parse_docker_auth_config_encoded_multiple():
auth_dict = {
"auths": {
"localhost:5000": {"auth": "dXNlcjE6cGFzczE=="},
"https://example.com": {"auth": "dXNlcl9uZXc6cGFzc19uZXc=="},
"example2.com": {"auth": "YWJjOjEyMw==="},
}
}
auth_config_json = json.dumps(auth_dict)
auth_info = parse_docker_auth_config(auth_config_json)
assert len(auth_info) == 3
assert auth_info[0] == DockerAuthInfo(
registry="localhost:5000",
username="user1",
password="pass1",
)
assert auth_info[1] == DockerAuthInfo(
registry="https://example.com",
username="user_new",
password="pass_new",
)
assert auth_info[2] == DockerAuthInfo(
registry="example2.com",
username="abc",
password="123",
)


def test_parse_docker_auth_config_unknown():
auth_config_str = '{"key": "value"}'
assert parse_docker_auth_config(auth_config_str) is None


def test_parse_docker_auth_config_error():
auth_config_str = "bad//string"
with pytest.raises(ValueError):
parse_docker_auth_config(auth_config_str)


def test_parse_docker_auth_all():
test_dict = {
"auths": {
"localhost:5000": {"auth": "dXNlcjE6cGFzczE=="},
},
"credHelpers": {"<aws_account_id>.dkr.ecr.<region>.amazonaws.com": "ecr-login"},
"credsStore": "ecr-login",
}
auth_config_json = json.dumps(test_dict)
assert parse_docker_auth_config(auth_config_json) == [
DockerAuthInfo(
registry="localhost:5000",
username="user1",
password="pass1",
)
]
54 changes: 53 additions & 1 deletion core/tests/test_docker_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import json
from collections import namedtuple
from unittest import mock
from unittest.mock import MagicMock, patch
@@ -8,9 +9,11 @@
from testcontainers.core.config import testcontainers_config as c
from testcontainers.core.container import DockerContainer
from testcontainers.core.docker_client import DockerClient
from testcontainers.core.utils import parse_docker_auth_config
from testcontainers.core.auth import parse_docker_auth_config
from testcontainers.core.image import DockerImage

from pytest import mark


def test_docker_client_from_env():
test_kwargs = {"test_kw": "test_value"}
@@ -48,6 +51,55 @@ def test_docker_client_login():
mock_docker.from_env.return_value.login.assert_called_with(**{"value": "test"})


def test_docker_client_login_empty_get_docker_auth_config():
mock_docker = MagicMock(spec=docker)
mock_get_docker_auth_config = MagicMock()
mock_get_docker_auth_config.return_value = None

with (
mock.patch.object(c, "_docker_auth_config", "test"),
patch("testcontainers.core.docker_client.docker", mock_docker),
patch("testcontainers.core.docker_client.get_docker_auth_config", mock_get_docker_auth_config),
):
DockerClient()

mock_docker.from_env.return_value.login.assert_not_called()


def test_docker_client_login_empty_parse_docker_auth_config():
mock_docker = MagicMock(spec=docker)
mock_parse_docker_auth_config = MagicMock(spec=parse_docker_auth_config)
mock_utils = MagicMock()
mock_utils.parse_docker_auth_config = mock_parse_docker_auth_config
mock_parse_docker_auth_config.return_value = None

with (
mock.patch.object(c, "_docker_auth_config", "test"),
patch("testcontainers.core.docker_client.docker", mock_docker),
patch("testcontainers.core.docker_client.parse_docker_auth_config", mock_parse_docker_auth_config),
):
DockerClient()

mock_docker.from_env.return_value.login.assert_not_called()


# This is used to make sure we don't fail (nor try to login) when we have unsupported auth config
@mark.parametrize("auth_config_sample", [{"credHelpers": {"test": "login"}}, {"credsStore": "login"}])
def test_docker_client_login_unsupported_auth_config(auth_config_sample):
mock_docker = MagicMock(spec=docker)
mock_get_docker_auth_config = MagicMock()
mock_get_docker_auth_config.return_value = json.dumps(auth_config_sample)

with (
mock.patch.object(c, "_docker_auth_config", "test"),
patch("testcontainers.core.docker_client.docker", mock_docker),
patch("testcontainers.core.docker_client.get_docker_auth_config", mock_get_docker_auth_config),
):
DockerClient()

mock_docker.from_env.return_value.login.assert_not_called()


def test_container_docker_client_kw():
test_kwargs = {"test_kw": "test_value"}
mock_docker = MagicMock(spec=docker)
42 changes: 0 additions & 42 deletions core/tests/test_utils.py

This file was deleted.

Loading