Skip to content

Commit 017fc3b

Browse files
committed
Add basetag property and helper functions
Signed-off-by: Tim van Katwijk <timvankatwijk@hotmail.com>
1 parent 48f8b95 commit 017fc3b

File tree

2 files changed

+190
-3
lines changed

2 files changed

+190
-3
lines changed

dockerfile_parse/parser.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ def parent_images(self, parents):
401401
lines[instr['startline']:instr['endline']+1] = [instr['content']]
402402

403403
self.lines = lines
404-
404+
405405
@property
406406
def is_multistage(self):
407407
return len(self.parent_images) > 1
@@ -428,6 +428,21 @@ def baseimage(self, new_image):
428428
raise RuntimeError('No stage defined to set base image on')
429429
images[-1] = new_image
430430
self.parent_images = images
431+
432+
@property
433+
def basetag(self):
434+
"""
435+
:return: tag of base image, i.e. tag of base image
436+
"""
437+
_, tag = tag_from(self.baseimage)
438+
return tag
439+
440+
@basetag.setter
441+
def basetag(self, new_tag):
442+
"""
443+
only change the tag of the final stage FROM instruction
444+
"""
445+
self.baseimage = tag_to(self.baseimage, new_tag)
431446

432447
@property
433448
def cmd(self):
@@ -882,7 +897,42 @@ def image_from(from_value):
882897
match = re.match(regex, from_value)
883898
return match.group('image', 'name') if match else (None, None)
884899

885-
900+
def tag_from(from_value):
901+
"""
902+
:param from_value: string like "registry:port/image:tag AS name"
903+
:return: tuple of the image and tag e.g. ("image", "tag")
904+
"""
905+
906+
image, _ = image_from(from_value)
907+
bare, _, tag = image.rpartition(":") if image and ":" in image else (None, None, None)
908+
909+
# check if a tag was actually present
910+
if not valid_tag(tag) or not bare:
911+
return (image, None)
912+
913+
return (bare, tag)
914+
915+
def valid_tag(tag):
916+
"""
917+
:param tag to be checked for validity
918+
:return: true or false
919+
"""
920+
regex = re.compile(r"""(?x) # readable, case-insensitive regex
921+
^(?P<tag>[a-zA-Z0-9\_][a-zA-Z0-9\.\_\-]*)$ # valid tag format (alphanumeric characters, numbers . _ and - (. and - not leading))
922+
""")
923+
match = re.match(regex, tag) if tag else None
924+
return True if match and match.group('tag') and len(match.group('tag')) < 128 else False
925+
926+
def tag_to(image, new_tag):
927+
"""
928+
:param image: string like "image:tag" or "image"
929+
:param tag: string like "latest"
930+
:return: string like "image:new_tag" or "image" if no tag was given
931+
"""
932+
933+
bare, _ = tag_from(image)
934+
return ":".join(filter(None, [bare.strip() if bare else None, new_tag.strip() if new_tag else None]))
935+
886936
def _endline(line):
887937
"""
888938
Make sure the line ends with a single newline.

tests/test_parser.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
from dockerfile_parse import DockerfileParser
2222
from dockerfile_parse.parser import image_from
23+
from dockerfile_parse.parser import tag_from
24+
from dockerfile_parse.parser import tag_to
25+
from dockerfile_parse.parser import valid_tag
2326
from dockerfile_parse.constants import COMMENT_INSTRUCTION
2427
from dockerfile_parse.util import b2u, u2b, Context
2528
from tests.fixtures import dfparser, instruction
@@ -312,11 +315,22 @@ def test_get_baseimg_from_df(self, dfparser):
312315
"LABEL a b\n"]
313316
assert dfparser.baseimage == 'fedora:latest'
314317

318+
def test_get_basetag_from_df(self,dfparser):
319+
dfparser.lines = ["From fedora:latest\n",
320+
"LABEL a b\n"]
321+
assert dfparser.basetag == 'latest'
322+
315323
def test_get_baseimg_from_arg(self, dfparser):
316324
dfparser.lines = ["ARG BASE=fedora:latest\n",
317325
"FROM $BASE\n",
318326
"LABEL a b\n"]
319327
assert dfparser.baseimage == 'fedora:latest'
328+
329+
def test_get_basetag_from_arg(self, dfparser):
330+
dfparser.lines = ["ARG BASE=fedora:latest\n",
331+
"FROM $BASE\n",
332+
"LABEL a b\n"]
333+
assert dfparser.basetag == 'latest'
320334

321335
def test_get_baseimg_from_build_arg(self, tmpdir):
322336
tmpdir_path = str(tmpdir.realpath())
@@ -328,6 +342,16 @@ def test_get_baseimg_from_build_arg(self, tmpdir):
328342
assert dfp.baseimage == 'fedora:latest'
329343
assert not dfp.args
330344

345+
def test_get_basetag_from_build_arg(self, tmpdir):
346+
tmpdir_path = str(tmpdir.realpath())
347+
b_args = {"BASE": "fedora:latest"}
348+
dfp = DockerfileParser(tmpdir_path, env_replace=True, build_args=b_args)
349+
dfp.lines = ["ARG BASE=centos:latest\n",
350+
"FROM $BASE\n",
351+
"LABEL a b\n"]
352+
assert dfp.basetag == 'latest'
353+
assert not dfp.args
354+
331355
def test_set_no_baseimage(self, dfparser):
332356
dfparser.lines = []
333357
with pytest.raises(RuntimeError):
@@ -468,6 +492,114 @@ def test_image_from(self, from_value, expect):
468492
result = image_from(from_value)
469493
assert result == expect
470494

495+
@pytest.mark.parametrize(('from_value', 'expect'), [
496+
(
497+
"",
498+
(None, None),
499+
),
500+
(
501+
" ",
502+
(None, None),
503+
), (
504+
" foo",
505+
('foo', None),
506+
), (
507+
"foo:bar as baz ",
508+
('foo', 'bar'),
509+
), (
510+
"foo as baz",
511+
('foo', None),
512+
), (
513+
"foo and some other junk", # we won't judge
514+
('foo', None),
515+
), (
516+
"registry.example.com:5000/foo/bar",
517+
('registry.example.com:5000/foo/bar', None),
518+
), (
519+
"registry.example.com:5000/foo/bar:baz",
520+
('registry.example.com:5000/foo/bar', "baz"),
521+
), (
522+
"localhost:5000/foo/bar:baz",
523+
('localhost:5000/foo/bar', "baz"),
524+
)
525+
])
526+
def test_tag_from(self, from_value, expect):
527+
result = tag_from(from_value)
528+
assert result == expect
529+
530+
@pytest.mark.parametrize(('from_image', 'from_tag', 'expect'), [
531+
(
532+
" ",
533+
" ",
534+
"",
535+
),(
536+
"foo",
537+
None,
538+
'foo',
539+
), (
540+
"foo",
541+
"bar",
542+
'foo:bar',
543+
), (
544+
"foo",
545+
"",
546+
'foo',
547+
), (
548+
"foo:bar",
549+
"baz",
550+
'foo:baz',
551+
), (
552+
"registry.example.com:5000/foo/bar",
553+
"baz",
554+
'registry.example.com:5000/foo/bar:baz',
555+
),
556+
(
557+
"localhost:5000/foo/bar",
558+
"baz",
559+
'localhost:5000/foo/bar:baz',
560+
),
561+
(
562+
"nonvalid1@%registry.example.com:5000/foo/bar",
563+
"baz",
564+
'nonvalid1@%registry.example.com:5000/foo/bar:baz',
565+
),
566+
(
567+
"registry.example.com:5000/foo/bar",
568+
"baz",
569+
'registry.example.com:5000/foo/bar:baz',
570+
),(
571+
"registry.example.com:5000/foo/bar:baz",
572+
"bap",
573+
'registry.example.com:5000/foo/bar:bap',
574+
)
575+
])
576+
def test_tag_to(self, from_image, from_tag, expect):
577+
result = tag_to(from_image, from_tag)
578+
assert result == expect
579+
580+
581+
@pytest.mark.parametrize(('tag', 'expect'), [
582+
(
583+
"Tag",
584+
True
585+
),(
586+
"tAg.",
587+
True
588+
), (
589+
"tag-tag",
590+
True
591+
), (
592+
".notTag",
593+
False
594+
), (
595+
"not/tag",
596+
False
597+
)
598+
])
599+
def test_valid_tag(self, tag, expect):
600+
result = valid_tag(tag)
601+
assert result == expect
602+
471603
def test_parent_images(self, dfparser):
472604
FROM = ('my-builder:latest', 'rhel7:7.5')
473605
template = dedent("""\
@@ -507,8 +639,9 @@ def test_parent_images_missing_from(self, dfparser):
507639
assert dfparser.content.count('FROM') == 4
508640

509641
def test_modify_instruction(self, dfparser):
510-
FROM = ('ubuntu', 'fedora:')
642+
FROM = ('ubuntu', 'fedora:theBest')
511643
CMD = ('old❤cmd', 'new❤command')
644+
TAG = ('theBest', 'newtag')
512645
df_content = dedent("""\
513646
FROM {0}
514647
CMD {1}""").format(FROM[0], CMD[0])
@@ -518,6 +651,10 @@ def test_modify_instruction(self, dfparser):
518651
assert dfparser.baseimage == FROM[0]
519652
dfparser.baseimage = FROM[1]
520653
assert dfparser.baseimage == FROM[1]
654+
655+
assert dfparser.basetag == TAG[0]
656+
dfparser.basetag = TAG[1]
657+
assert dfparser.basetag == TAG[1]
521658

522659
assert dfparser.cmd == CMD[0]
523660
dfparser.cmd = CMD[1]

0 commit comments

Comments
 (0)