diff --git a/repo2docker/app.py b/repo2docker/app.py index 5d747122..c44a9b6d 100644 --- a/repo2docker/app.py +++ b/repo2docker/app.py @@ -30,7 +30,7 @@ from .buildpacks import ( PythonBuildPack, DockerBuildPack, LegacyBinderDockerBuildPack, CondaBuildPack, JuliaBuildPack, Python2BuildPack, BaseImage ) -from .utils import execute_cmd, ByteSpecification, maybe_cleanup +from .utils import execute_cmd, ByteSpecification, maybe_cleanup, is_valid_docker_image_name from . import __version__ @@ -180,6 +180,25 @@ class Repo2Docker(Application): extra=dict(phase='failed')) sys.exit(1) + def validate_image_name(self, image_name): + """ + Validate image_name read by argparse contains only lowercase characters + + Args: + image_name (string): argument read by the argument parser + + Returns: + unmodified image_name + + Raises: + ArgumentTypeError: if image_name contains characters that are not lowercase + """ + + if not is_valid_docker_image_name(image_name): + msg = "%r is not a valid docker image name. Image name can contain only lowercase characters." % image_name + raise argparse.ArgumentTypeError(msg) + return image_name + def get_argparser(self): argparser = argparse.ArgumentParser() argparser.add_argument( @@ -204,7 +223,8 @@ class Repo2Docker(Application): argparser.add_argument( '--image-name', help=('Name of image to be built. If unspecified will be ' - 'autogenerated') + 'autogenerated'), + type=self.validate_image_name ) argparser.add_argument( diff --git a/repo2docker/utils.py b/repo2docker/utils.py index 334b2399..504f3949 100644 --- a/repo2docker/utils.py +++ b/repo2docker/utils.py @@ -2,6 +2,7 @@ from contextlib import contextmanager from functools import partial import shutil import subprocess +import re from traitlets import Integer @@ -56,6 +57,81 @@ def maybe_cleanup(path, cleanup=False): shutil.rmtree(path, ignore_errors=True) +def is_valid_docker_image_name(image_name): + """ + Function that constructs a regex representing the docker image name and tests it against the given image_name + Reference Regex definition in https://github.com/docker/distribution/blob/master/reference/regexp.go + + Args: + image_name: string representing a docker image name + + Returns: + True if image_name is valid else False + + Example: + + 'test.Com/name:latest' is a valid tag + + 'Test/name:latest' is not a valid tag + + Note: + + This function has a stricter pattern than https://github.com/docker/distribution/blob/master/reference/regexp.go + + This pattern will not allow cases like + 'TEST.com/name:latest' though docker considers it a valid tag + """ + reference_regex = re.compile(r"""^ # Anchored at start and end of string + + ( # Start capturing name + + (?: # start grouping the optional registry domain name part + + (?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]) # lowercase only '' + + (?: # start optional group + + (?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+ # multiple repetitions of pattern '.' + + )? # end optional grouping part of the '.' separated domain name + + (?::[0-9]+)?/ # '' followed by an optional '' component followed by '/' literal + + )? # end grouping the optional registry domain part + + # start + [a-z0-9]+ # must have a + (?: + (?:(?:[\._]|__|[-]*)[a-z0-9]+)+ # repeat the pattern '' + )? # optionally have multiple repetitions of the above line + # end + + (?: # start optional name components + + (?: # start multiple repetitions + + / # separate multiple name components by / + # start + [a-z0-9]+ # must have a + (?: + (?:(?:[\._]|__|[-]*)[a-z0-9]+)+ # repeat the pattern '' + )? # optionally have multiple repetitions of the above line + # end + + )+ # multiple repetitions of the pattern '/' + + )? # optionally have the above group + + ) # end capturing name + + (?::([\w][\w.-]{0,127}))? # optional capture =':' + (?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})? # optionally capture ='@' + $""", + re.VERBOSE) + + return reference_regex.match(image_name) is not None + + class ByteSpecification(Integer): """ Allow easily specifying bytes in units of 1024 with suffixes diff --git a/tests/argumentvalidation.py b/tests/argumentvalidation.py new file mode 100644 index 00000000..eb895081 --- /dev/null +++ b/tests/argumentvalidation.py @@ -0,0 +1,90 @@ +""" +Tests that runs validity checks on arguments passed in from shell +""" + +import os +import subprocess + +def does_validate_image_name(builddir, image_name): + try: + output = subprocess.check_output( + [ + 'repo2docker', + '--no-run', + '--no-build', + '--image-name', + str(image_name), + builddir + ], + stderr=subprocess.STDOUT, + ).decode() + return True + except subprocess.CalledProcessError as e: + output = e.output.decode() + if "error: argument --image-name: %r is not a valid docker image name. " \ + "Image name can contain only lowercase characters." % image_name in output: + return False + else: + raise + + +def test_image_name_fail(): + """ + Test to check if repo2docker throws image_name validation error on --image-name argument containing + uppercase characters and _ characters in incorrect positions. + """ + + builddir = os.path.dirname(__file__) + + assert not does_validate_image_name(builddir, 'Test/Invalid_name:1.0.0') + + +def test_image_name_underscore_fail(): + """ + Test to check if repo2docker throws image_name validation error on --image-name argument starts with _. + """ + + builddir = os.path.dirname(__file__) + + assert not does_validate_image_name(builddir, '_test/invalid_name:1.0.0') + + +def test_image_name_double_dot_fail(): + """ + Test to check if repo2docker throws image_name validation error on --image-name argument contains consecutive dots. + """ + + builddir = os.path.dirname(__file__) + + assert not does_validate_image_name(builddir, 'test..com/invalid_name:1.0.0') + + +def test_image_name_valid_restircted_registry_domain_name_fail(): + """ + Test to check if repo2docker throws image_name validation error on -image-name argument being invalid. Based on the + regex definitions first part of registry domain cannot contain uppercase characters + """ + + builddir = os.path.dirname(__file__) + + assert not does_validate_image_name(builddir, 'Test.com/valid_name:1.0.0') + + +def test_image_name_valid_registry_domain_name_success(): + """ + Test to check if repo2docker runs with a valid --image-name argument. + """ + + builddir = os.path.dirname(__file__) + '/dockerfile/simple/' + + assert does_validate_image_name(builddir, 'test.COM/valid_name:1.0.0') + + +def test_image_name_valid_name_success(): + """ + Test to check if repo2docker runs with a valid --image-name argument. + """ + + builddir = os.path.dirname(__file__) + '/dockerfile/simple/' + + assert does_validate_image_name(builddir, 'test.com/valid_name:1.0.0') \ No newline at end of file