diff --git a/dockerfile_parse/parser.py b/dockerfile_parse/parser.py index 343ef30..64bbe4e 100644 --- a/dockerfile_parse/parser.py +++ b/dockerfile_parse/parser.py @@ -401,7 +401,7 @@ def parent_images(self, parents): lines[instr['startline']:instr['endline']+1] = [instr['content']] self.lines = lines - + @property def is_multistage(self): return len(self.parent_images) > 1 @@ -428,6 +428,21 @@ def baseimage(self, new_image): raise RuntimeError('No stage defined to set base image on') images[-1] = new_image self.parent_images = images + + @property + def basetag(self): + """ + :return: tag of base image, i.e. tag of base image + """ + _, tag = tag_from(self.baseimage) + return tag + + @basetag.setter + def basetag(self, new_tag): + """ + only change the tag of the final stage FROM instruction + """ + self.baseimage = tag_to(self.baseimage, new_tag) @property def cmd(self): @@ -882,7 +897,42 @@ def image_from(from_value): match = re.match(regex, from_value) return match.group('image', 'name') if match else (None, None) - +def tag_from(from_value): + """ + :param from_value: string like "registry:port/image:tag AS name" + :return: tuple of the image and tag e.g. ("image", "tag") + """ + + image, _ = image_from(from_value) + bare, _, tag = image.rpartition(":") if image and ":" in image else (None, None, None) + + # check if a tag was actually present + if not valid_tag(tag) or not bare: + return (image, None) + + return (bare, tag) + +def valid_tag(tag): + """ + :param tag to be checked for validity + :return: true or false + """ + regex = re.compile(r"""(?x) # readable, case-insensitive regex + ^(?P[a-zA-Z0-9\_][a-zA-Z0-9\.\_\-]*)$ # valid tag format (alphanumeric characters, numbers . _ and - (. and - not leading)) + """) + match = re.match(regex, tag) if tag else None + return True if match and match.group('tag') and len(match.group('tag')) < 128 else False + +def tag_to(image, new_tag): + """ + :param image: string like "image:tag" or "image" + :param tag: string like "latest" + :return: string like "image:new_tag" or "image" if no tag was given + """ + + bare, _ = tag_from(image) + return ":".join(filter(None, [bare.strip() if bare else None, new_tag.strip() if new_tag else None])) + def _endline(line): """ Make sure the line ends with a single newline. diff --git a/tests/test_parser.py b/tests/test_parser.py index fda75ab..a872252 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -20,6 +20,9 @@ from dockerfile_parse import DockerfileParser from dockerfile_parse.parser import image_from +from dockerfile_parse.parser import tag_from +from dockerfile_parse.parser import tag_to +from dockerfile_parse.parser import valid_tag from dockerfile_parse.constants import COMMENT_INSTRUCTION from dockerfile_parse.util import b2u, u2b, Context from tests.fixtures import dfparser, instruction @@ -312,11 +315,22 @@ def test_get_baseimg_from_df(self, dfparser): "LABEL a b\n"] assert dfparser.baseimage == 'fedora:latest' + def test_get_basetag_from_df(self,dfparser): + dfparser.lines = ["From fedora:latest\n", + "LABEL a b\n"] + assert dfparser.basetag == 'latest' + def test_get_baseimg_from_arg(self, dfparser): dfparser.lines = ["ARG BASE=fedora:latest\n", "FROM $BASE\n", "LABEL a b\n"] assert dfparser.baseimage == 'fedora:latest' + + def test_get_basetag_from_arg(self, dfparser): + dfparser.lines = ["ARG BASE=fedora:latest\n", + "FROM $BASE\n", + "LABEL a b\n"] + assert dfparser.basetag == 'latest' def test_get_baseimg_from_build_arg(self, tmpdir): tmpdir_path = str(tmpdir.realpath()) @@ -328,6 +342,16 @@ def test_get_baseimg_from_build_arg(self, tmpdir): assert dfp.baseimage == 'fedora:latest' assert not dfp.args + def test_get_basetag_from_build_arg(self, tmpdir): + tmpdir_path = str(tmpdir.realpath()) + b_args = {"BASE": "fedora:latest"} + dfp = DockerfileParser(tmpdir_path, env_replace=True, build_args=b_args) + dfp.lines = ["ARG BASE=centos:latest\n", + "FROM $BASE\n", + "LABEL a b\n"] + assert dfp.basetag == 'latest' + assert not dfp.args + def test_set_no_baseimage(self, dfparser): dfparser.lines = [] with pytest.raises(RuntimeError): @@ -468,6 +492,114 @@ def test_image_from(self, from_value, expect): result = image_from(from_value) assert result == expect + @pytest.mark.parametrize(('from_value', 'expect'), [ + ( + "", + (None, None), + ), + ( + " ", + (None, None), + ), ( + " foo", + ('foo', None), + ), ( + "foo:bar as baz ", + ('foo', 'bar'), + ), ( + "foo as baz", + ('foo', None), + ), ( + "foo and some other junk", # we won't judge + ('foo', None), + ), ( + "registry.example.com:5000/foo/bar", + ('registry.example.com:5000/foo/bar', None), + ), ( + "registry.example.com:5000/foo/bar:baz", + ('registry.example.com:5000/foo/bar', "baz"), + ), ( + "localhost:5000/foo/bar:baz", + ('localhost:5000/foo/bar', "baz"), + ) + ]) + def test_tag_from(self, from_value, expect): + result = tag_from(from_value) + assert result == expect + + @pytest.mark.parametrize(('from_image', 'from_tag', 'expect'), [ + ( + " ", + " ", + "", + ),( + "foo", + None, + 'foo', + ), ( + "foo", + "bar", + 'foo:bar', + ), ( + "foo", + "", + 'foo', + ), ( + "foo:bar", + "baz", + 'foo:baz', + ), ( + "registry.example.com:5000/foo/bar", + "baz", + 'registry.example.com:5000/foo/bar:baz', + ), + ( + "localhost:5000/foo/bar", + "baz", + 'localhost:5000/foo/bar:baz', + ), + ( + "nonvalid1@%registry.example.com:5000/foo/bar", + "baz", + 'nonvalid1@%registry.example.com:5000/foo/bar:baz', + ), + ( + "registry.example.com:5000/foo/bar", + "baz", + 'registry.example.com:5000/foo/bar:baz', + ),( + "registry.example.com:5000/foo/bar:baz", + "bap", + 'registry.example.com:5000/foo/bar:bap', + ) + ]) + def test_tag_to(self, from_image, from_tag, expect): + result = tag_to(from_image, from_tag) + assert result == expect + + + @pytest.mark.parametrize(('tag', 'expect'), [ + ( + "Tag", + True + ),( + "tAg.", + True + ), ( + "tag-tag", + True + ), ( + ".notTag", + False + ), ( + "not/tag", + False + ) + ]) + def test_valid_tag(self, tag, expect): + result = valid_tag(tag) + assert result == expect + def test_parent_images(self, dfparser): FROM = ('my-builder:latest', 'rhel7:7.5') template = dedent("""\ @@ -507,8 +639,9 @@ def test_parent_images_missing_from(self, dfparser): assert dfparser.content.count('FROM') == 4 def test_modify_instruction(self, dfparser): - FROM = ('ubuntu', 'fedora:❤') + FROM = ('ubuntu', 'fedora:theBest') CMD = ('old❤cmd', 'new❤command') + TAG = ('theBest', 'newtag') df_content = dedent("""\ FROM {0} CMD {1}""").format(FROM[0], CMD[0]) @@ -518,6 +651,10 @@ def test_modify_instruction(self, dfparser): assert dfparser.baseimage == FROM[0] dfparser.baseimage = FROM[1] assert dfparser.baseimage == FROM[1] + + assert dfparser.basetag == TAG[0] + dfparser.basetag = TAG[1] + assert dfparser.basetag == TAG[1] assert dfparser.cmd == CMD[0] dfparser.cmd = CMD[1]