diff --git a/docker/api/build.py b/docker/api/build.py index 439f4dc3..9c8b4e6a 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -129,13 +129,16 @@ class BuildApiMixin: raise errors.DockerException( 'Can not use custom encoding if gzip is enabled' ) - + if tag is not None: + if not utils.match_tag(tag): + raise errors.DockerException( + f"invalid tag '{tag}': invalid reference format" + ) for key in container_limits.keys(): if key not in constants.CONTAINER_LIMITS_KEYS: raise errors.DockerException( - f'Invalid container_limits key {key}' + f"invalid tag '{tag}': invalid reference format" ) - if custom_context: if not fileobj: raise TypeError("You must specify fileobj with custom_context") diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 944c6e65..b4bef7d4 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -1,5 +1,5 @@ -from .build import create_archive, exclude_paths, mkbuildcontext, tar +from .build import match_tag, create_archive, exclude_paths, mkbuildcontext, tar from .decorators import check_resource, minimum_version, update_headers from .utils import ( compare_version, convert_port_bindings, convert_volume_binds, diff --git a/docker/utils/build.py b/docker/utils/build.py index 8d18c2be..a5c4b0c2 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -9,6 +9,14 @@ from ..constants import IS_WINDOWS_PLATFORM _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/') +_TAG = re.compile( + r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" \ + + "(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$" +) + + +def match_tag(tag: str) -> bool: + return bool(_TAG.match(tag)) def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False): diff --git a/tests/unit/api_build_test.py b/tests/unit/api_build_test.py index cbecd1e5..01958c3e 100644 --- a/tests/unit/api_build_test.py +++ b/tests/unit/api_build_test.py @@ -2,181 +2,206 @@ import gzip import io import shutil -import docker -from docker import auth -from docker.api.build import process_dockerfile - import pytest +import docker +from docker import auth, errors +from docker.api.build import process_dockerfile + from ..helpers import make_tree from .api_test import BaseAPIClientTest, fake_request, url_prefix class BuildTest(BaseAPIClientTest): def test_build_container(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) self.client.build(fileobj=script) def test_build_container_pull(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) self.client.build(fileobj=script, pull=True) def test_build_container_custom_context(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) context = docker.utils.mkbuildcontext(script) self.client.build(fileobj=context, custom_context=True) def test_build_container_custom_context_gzip(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) + script = io.BytesIO( + "\n".join( + [ + "FROM busybox", + "RUN mkdir -p /tmp/test", + "EXPOSE 8080", + "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz" + " /tmp/silence.tar.gz", + ] + ).encode("ascii") + ) context = docker.utils.mkbuildcontext(script) gz_context = gzip.GzipFile(fileobj=context) - self.client.build( - fileobj=gz_context, - custom_context=True, - encoding="gzip" - ) + self.client.build(fileobj=gz_context, custom_context=True, encoding="gzip") def test_build_remote_with_registry_auth(self): - self.client._auth_configs = auth.AuthConfig({ - 'auths': { - 'https://example.com': { - 'user': 'example', - 'password': 'example', - 'email': 'example@example.com' + self.client._auth_configs = auth.AuthConfig( + { + "auths": { + "https://example.com": { + "user": "example", + "password": "example", + "email": "example@example.com", + } } } - }) + ) - expected_params = {'t': None, 'q': False, 'dockerfile': None, - 'rm': False, 'nocache': False, 'pull': False, - 'forcerm': False, - 'remote': 'https://github.com/docker-library/mongo'} + expected_params = { + "t": None, + "q": False, + "dockerfile": None, + "rm": False, + "nocache": False, + "pull": False, + "forcerm": False, + "remote": "https://github.com/docker-library/mongo", + } expected_headers = { - 'X-Registry-Config': auth.encode_header( - self.client._auth_configs.auths - ) + "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths) } - self.client.build(path='https://github.com/docker-library/mongo') + self.client.build(path="https://github.com/docker-library/mongo") fake_request.assert_called_with( - 'POST', + "POST", f"{url_prefix}build", stream=True, data=None, headers=expected_headers, params=expected_params, - timeout=None + timeout=None, ) def test_build_container_with_named_dockerfile(self): - self.client.build('.', dockerfile='nameddockerfile') + self.client.build(".", dockerfile="nameddockerfile") + + def test_build_with_invalid_tag(self): + with pytest.raises(errors.DockerException): + self.client.build(".", tag="https://example.com") def test_build_container_with_container_limits(self): - self.client.build('.', container_limits={ - 'memory': 1024 * 1024, - 'cpusetcpus': 1, - 'cpushares': 1000, - 'memswap': 1024 * 1024 * 8 - }) + self.client.build( + ".", + container_limits={ + "memory": 1024 * 1024, + "cpusetcpus": 1, + "cpushares": 1000, + "memswap": 1024 * 1024 * 8, + }, + ) def test_build_container_invalid_container_limits(self): with pytest.raises(docker.errors.DockerException): - self.client.build('.', container_limits={ - 'foo': 'bar' - }) + self.client.build(".", container_limits={"foo": "bar"}) def test_set_auth_headers_with_empty_dict_and_auth_configs(self): - self.client._auth_configs = auth.AuthConfig({ - 'auths': { - 'https://example.com': { - 'user': 'example', - 'password': 'example', - 'email': 'example@example.com' + self.client._auth_configs = auth.AuthConfig( + { + "auths": { + "https://example.com": { + "user": "example", + "password": "example", + "email": "example@example.com", + } } } - }) + ) headers = {} expected_headers = { - 'X-Registry-Config': auth.encode_header( - self.client._auth_configs.auths - ) + "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths) } self.client._set_auth_headers(headers) assert headers == expected_headers def test_set_auth_headers_with_dict_and_auth_configs(self): - self.client._auth_configs = auth.AuthConfig({ - 'auths': { - 'https://example.com': { - 'user': 'example', - 'password': 'example', - 'email': 'example@example.com' + self.client._auth_configs = auth.AuthConfig( + { + "auths": { + "https://example.com": { + "user": "example", + "password": "example", + "email": "example@example.com", + } } } - }) + ) - headers = {'foo': 'bar'} + headers = {"foo": "bar"} expected_headers = { - 'X-Registry-Config': auth.encode_header( - self.client._auth_configs.auths - ), - 'foo': 'bar' + "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths), + "foo": "bar", } self.client._set_auth_headers(headers) assert headers == expected_headers def test_set_auth_headers_with_dict_and_no_auth_configs(self): - headers = {'foo': 'bar'} - expected_headers = { - 'foo': 'bar' - } + headers = {"foo": "bar"} + expected_headers = {"foo": "bar"} self.client._set_auth_headers(headers) assert headers == expected_headers @pytest.mark.skipif( - not docker.constants.IS_WINDOWS_PLATFORM, - reason='Windows-specific syntax') + not docker.constants.IS_WINDOWS_PLATFORM, reason="Windows-specific syntax" + ) def test_process_dockerfile_win_longpath_prefix(self): dirs = [ - 'foo', 'foo/bar', 'baz', + "foo", + "foo/bar", + "baz", ] files = [ - 'Dockerfile', 'foo/Dockerfile.foo', 'foo/bar/Dockerfile.bar', - 'baz/Dockerfile.baz', + "Dockerfile", + "foo/Dockerfile.foo", + "foo/bar/Dockerfile.bar", + "baz/Dockerfile.baz", ] base = make_tree(dirs, files) @@ -186,40 +211,42 @@ class BuildTest(BaseAPIClientTest): return docker.constants.WINDOWS_LONGPATH_PREFIX + path assert process_dockerfile(None, pre(base)) == (None, None) - assert process_dockerfile('Dockerfile', pre(base)) == ( - 'Dockerfile', None + assert process_dockerfile("Dockerfile", pre(base)) == ("Dockerfile", None) + assert process_dockerfile("foo/Dockerfile.foo", pre(base)) == ( + "foo/Dockerfile.foo", + None, ) - assert process_dockerfile('foo/Dockerfile.foo', pre(base)) == ( - 'foo/Dockerfile.foo', None + assert process_dockerfile("../Dockerfile", pre(f"{base}\\foo"))[1] is not None + assert process_dockerfile("../baz/Dockerfile.baz", pre(f"{base}/baz")) == ( + "../baz/Dockerfile.baz", + None, ) - assert process_dockerfile( - '../Dockerfile', pre(f"{base}\\foo") - )[1] is not None - assert process_dockerfile( - '../baz/Dockerfile.baz', pre(f"{base}/baz") - ) == ('../baz/Dockerfile.baz', None) def test_process_dockerfile(self): dirs = [ - 'foo', 'foo/bar', 'baz', + "foo", + "foo/bar", + "baz", ] files = [ - 'Dockerfile', 'foo/Dockerfile.foo', 'foo/bar/Dockerfile.bar', - 'baz/Dockerfile.baz', + "Dockerfile", + "foo/Dockerfile.foo", + "foo/bar/Dockerfile.bar", + "baz/Dockerfile.baz", ] base = make_tree(dirs, files) self.addCleanup(shutil.rmtree, base) assert process_dockerfile(None, base) == (None, None) - assert process_dockerfile('Dockerfile', base) == ('Dockerfile', None) - assert process_dockerfile('foo/Dockerfile.foo', base) == ( - 'foo/Dockerfile.foo', None + assert process_dockerfile("Dockerfile", base) == ("Dockerfile", None) + assert process_dockerfile("foo/Dockerfile.foo", base) == ( + "foo/Dockerfile.foo", + None, ) - assert process_dockerfile( - '../Dockerfile', f"{base}/foo" - )[1] is not None - assert process_dockerfile('../baz/Dockerfile.baz', f"{base}/baz") == ( - '../baz/Dockerfile.baz', None + assert process_dockerfile("../Dockerfile", f"{base}/foo")[1] is not None + assert process_dockerfile("../baz/Dockerfile.baz", f"{base}/baz") == ( + "../baz/Dockerfile.baz", + None, )