Skip to content

Commit 9736cd9

Browse files
committed
Move ImageName from osbs-client to util.
Refactor to keep string behavior identical for parser. Signed-off-by: Tim van Katwijk <[email protected]>
1 parent 1d4f9fa commit 9736cd9

File tree

3 files changed

+211
-4
lines changed

3 files changed

+211
-4
lines changed

dockerfile_parse/parser.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from .constants import DOCKERFILE_FILENAME, COMMENT_INSTRUCTION
2121
from .util import (b2u, extract_key_values, get_key_val_dictionary,
22-
u2b, Context, WordSplitter)
22+
u2b, Context, WordSplitter, ImageName)
2323

2424

2525
logger = logging.getLogger(__name__)
@@ -359,7 +359,7 @@ def parent_images(self):
359359
in_stage = True
360360
image, _ = image_from(instr['value'])
361361
if image is not None:
362-
image = WordSplitter(image, args=top_args).dequote()
362+
image = WordSplitter(image.to_str(), args=top_args).dequote()
363363
parents.append(image)
364364
return parents
365365

@@ -880,7 +880,9 @@ def image_from(from_value):
880880
)?
881881
""")
882882
match = re.match(regex, from_value)
883-
return match.group('image', 'name') if match else (None, None)
883+
image = ImageName.parse(match.group('image')) if match else None
884+
name = match.group('name') if match else None
885+
return image, name
884886

885887

886888
def _endline(line):

dockerfile_parse/util.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,121 @@ def get_values(self, context_type):
332332
if context_type.upper() == "LABEL":
333333
return self.labels
334334
raise ValueError("Unexpected context type: " + context_type)
335+
336+
337+
class ImageName(object):
338+
"""Represent an image.
339+
Naming Conventions
340+
==================
341+
registry.somewhere/namespace/image_name:tag
342+
|-----------------| registry, reg_uri
343+
|---------| namespace
344+
|--------------------------------------| repository
345+
|--------------------| image name
346+
|--| tag
347+
|------------------------| image
348+
|------------------------------------------| image
349+
"""
350+
351+
def __init__(self, registry=None, namespace=None, repo=None, tag=None):
352+
self.registry = registry
353+
self.namespace = namespace
354+
self.repo = repo
355+
self.tag = tag
356+
357+
@classmethod
358+
def parse(cls, image_name):
359+
result = cls()
360+
361+
if not image_name or str(image_name).isspace():
362+
return ImageName()
363+
364+
if isinstance(image_name, cls):
365+
return image_name
366+
367+
# registry.org/namespace/repo:tag
368+
s = image_name.split('/', 2)
369+
370+
if len(s) == 2:
371+
if '.' in s[0] or ':' in s[0]:
372+
result.registry = s[0] if s[0] else None
373+
else:
374+
result.namespace = s[0]
375+
elif len(s) == 3:
376+
result.registry = s[0] if s[0] else None
377+
result.namespace = s[1]
378+
result.repo = s[-1]
379+
380+
for sep in '@:':
381+
try:
382+
result.repo, result.tag = result.repo.rsplit(sep, 1)
383+
except ValueError:
384+
continue
385+
break
386+
387+
return result
388+
389+
def to_str(self, registry=True, tag=True, explicit_tag=False,
390+
explicit_namespace=False):
391+
if self.repo is None:
392+
raise RuntimeError('No image repository specified')
393+
394+
result = self.get_repo(explicit_namespace)
395+
396+
if tag and self.tag and ':' in self.tag:
397+
result = '{0}@{1}'.format(result, self.tag)
398+
elif tag and self.tag:
399+
result = '{0}:{1}'.format(result, self.tag)
400+
elif tag and explicit_tag:
401+
result = '{0}:{1}'.format(result, 'latest')
402+
403+
if registry and self.registry:
404+
result = '{0}/{1}'.format(self.registry, result)
405+
406+
return result
407+
408+
def get_repo(self, explicit_namespace=False):
409+
result = self.repo
410+
if self.namespace:
411+
result = '{0}/{1}'.format(self.namespace, result)
412+
elif explicit_namespace:
413+
result = '{0}/{1}'.format('library', result)
414+
return result
415+
416+
def enclose(self, organization):
417+
if self.namespace == organization:
418+
return
419+
420+
repo_parts = [self.repo]
421+
if self.namespace:
422+
repo_parts.insert(0, self.namespace)
423+
424+
self.namespace = organization
425+
self.repo = '-'.join(repo_parts)
426+
427+
def __str__(self):
428+
return self.to_str(registry=True, tag=True)
429+
430+
def __repr__(self):
431+
return (
432+
"ImageName(registry={s.registry!r}, namespace={s.namespace!r},"
433+
" repo={s.repo!r}, tag={s.tag!r})"
434+
).format(s=self)
435+
436+
def __eq__(self, other):
437+
if isinstance(other, str):
438+
return self.__str__() == other
439+
elif isinstance(other, ImageName):
440+
return self.__dict__ == other.__dict__
441+
else:
442+
return NotImplemented
443+
444+
def __hash__(self):
445+
return hash(self.to_str())
446+
447+
def copy(self):
448+
return ImageName(
449+
registry=self.registry,
450+
namespace=self.namespace,
451+
repo=self.repo,
452+
tag=self.tag)

tests/test_parser.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from dockerfile_parse import DockerfileParser
2323
from dockerfile_parse.parser import image_from
2424
from dockerfile_parse.constants import COMMENT_INSTRUCTION
25-
from dockerfile_parse.util import b2u, u2b, Context
25+
from dockerfile_parse.util import b2u, u2b, Context, ImageName
2626
from tests.fixtures import dfparser, instruction
2727

2828
NON_ASCII = "žluťoučký"
@@ -31,6 +31,93 @@
3131
instruction = instruction # pylint: disable=self-assigning-variable
3232

3333

34+
@pytest.mark.parametrize(('image_string', 'dictionary'), [
35+
(
36+
" ",
37+
{"namespace": None, "registry": None, "tag": None, "repo": None},
38+
), (
39+
"registry.org/namespace/repo:tag",
40+
{"namespace": "namespace", "registry": "registry.org", "tag": "tag", "repo": "repo"},
41+
), (
42+
"/namespace/repo:tag",
43+
{"namespace": "namespace", "registry": None, "tag": "tag", "repo": "repo"},
44+
), (
45+
"registry.org/repo:tag",
46+
{"namespace": None, "registry": "registry.org", "tag": "tag", "repo": "repo"},
47+
)
48+
])
49+
class TestImageName(object):
50+
def test_util_image_name_parse(self, image_string, dictionary):
51+
image = ImageName.parse(image_string)
52+
assert image.namespace == dictionary["namespace"]
53+
assert image.registry == dictionary["registry"]
54+
assert image.tag == dictionary["tag"]
55+
assert image.repo == dictionary["repo"]
56+
57+
def test_util_image_name_get_repo(self, image_string, dictionary):
58+
image = ImageName.parse(image_string)
59+
repo = "/".join(filter(None, (dictionary["namespace"], dictionary["repo"])))
60+
assert image.get_repo() == (repo if repo != "" else None)
61+
assert image.get_repo(explicit_namespace=True) == "{0}/{1}".format(
62+
dictionary["namespace"] if dictionary["namespace"] else "library", dictionary["repo"])
63+
64+
def test_util_image_name_to_str(self, image_string, dictionary):
65+
image = ImageName.parse(image_string)
66+
if dictionary["repo"] is None:
67+
with pytest.raises(RuntimeError):
68+
image.to_str()
69+
else:
70+
assert image.to_str() == image_string.lstrip('/')
71+
72+
def test_image_name_comparison(self, image_string, dictionary):
73+
# make sure that "==" is implemented correctly on both Python major releases
74+
i1 = ImageName.parse(image_string)
75+
i2 = ImageName(registry=dictionary["registry"], namespace=dictionary["namespace"],
76+
repo=dictionary["repo"],
77+
tag=dictionary["tag"])
78+
assert i1 == i2
79+
80+
i2 = ImageName(registry='foo.com', namespace='spam', repo='bar', tag='2')
81+
# pylint: disable=unneeded-not
82+
assert not i1 == i2
83+
84+
i1 = ImageName.parse(i2)
85+
assert i1 == i2
86+
87+
i1 = i2
88+
assert i1 == i2
89+
90+
91+
@pytest.mark.parametrize(('repo', 'organization', 'enclosed_repo'), (
92+
('fedora', 'spam', 'spam/fedora'),
93+
('spam/fedora', 'spam', 'spam/fedora'),
94+
('spam/fedora', 'maps', 'maps/spam-fedora'),
95+
))
96+
@pytest.mark.parametrize('registry', (
97+
'example.registry.com',
98+
'example.registry.com:8888',
99+
None,
100+
))
101+
@pytest.mark.parametrize('tag', ('bacon', None))
102+
def test_image_name_enclose(repo, organization, enclosed_repo, registry, tag):
103+
reference = repo
104+
if tag:
105+
reference = '{}:{}'.format(repo, tag)
106+
if registry:
107+
reference = '{}/{}'.format(registry, reference)
108+
109+
image_name = ImageName.parse(reference)
110+
assert image_name.get_repo() == repo
111+
assert image_name.registry == registry
112+
assert image_name.tag == tag
113+
114+
image_name.enclose(organization)
115+
assert image_name.get_repo() == enclosed_repo
116+
# Verify that registry and tag are unaffected
117+
assert image_name.registry == registry
118+
assert image_name.tag == tag
119+
120+
34121
class TestDockerfileParser(object):
35122
def test_all_versions_match(self):
36123
def read_version(fp, regex):

0 commit comments

Comments
 (0)