Add validator for ee image field name

awxkit default ee image name is now a fixed valid (but bogus) name, rather than random unicode
This commit is contained in:
Seth Foster 2021-06-14 22:51:36 -04:00
parent ef67f9c65d
commit 61846e88ca
No known key found for this signature in database
GPG Key ID: 86E90D96F7184028
5 changed files with 156 additions and 1 deletions

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.16 on 2021-06-15 02:49
import awx.main.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0146_add_insights_inventory'),
]
operations = [
migrations.AlterField(
model_name='executionenvironment',
name='image',
field=models.CharField(
help_text='The full image location, including the container registry, image name, and version tag.',
max_length=1024,
validators=[awx.main.validators.validate_container_image_name],
verbose_name='image location',
),
),
]

View File

@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from awx.api.versioning import reverse
from awx.main.models.base import CommonModel
from awx.main.validators import validate_container_image_name
__all__ = ['ExecutionEnvironment']
@ -31,6 +32,7 @@ class ExecutionEnvironment(CommonModel):
max_length=1024,
verbose_name=_('image location'),
help_text=_("The full image location, including the container registry, image name, and version tag."),
validators=[validate_container_image_name],
)
managed_by_tower = models.BooleanField(default=False, editable=False)
credential = models.ForeignKey(

View File

@ -4,6 +4,7 @@ from awx.main.validators import (
validate_certificate,
validate_ssh_private_key,
vars_validate_or_raise,
validate_container_image_name,
)
from awx.main.tests.data.ssh import (
TEST_SSH_RSA1_KEY_DATA,
@ -163,3 +164,39 @@ def test_valid_vars(var_str):
def test_invalid_vars(var_str):
with pytest.raises(RestValidationError):
vars_validate_or_raise(var_str)
@pytest.mark.parametrize(
("image_name", "is_valid"),
[
("localhost", True),
("short", True),
("simple/name", True),
("ab/ab/ab/ab", True),
("foo.com/", False),
("", False),
("localhost/foo", True),
("3asdasdf3", True),
("xn--7o8h.com/myimage", True),
("Asdf.com/foo/bar", True),
("Foo/FarB", False),
("registry.com:8080/myapp:tag", True),
("registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True),
("registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True),
("registry.com:8080/myapp@sha256:badbadbadbad", False),
("registry.com:8080/myapp:invalid~tag", False),
("bad_hostname.com:8080/myapp:tag", False),
("localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True),
("localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True),
("localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", False),
("localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", True),
("registry.com:8080/myapp@bad", False),
("registry.com:8080/myapp@2bad", False),
],
)
def test_valid_container_image_name(image_name, is_valid):
if is_valid:
validate_container_image_name(image_name)
else:
with pytest.raises(ValidationError):
validate_container_image_name(image_name)

View File

@ -195,3 +195,95 @@ def vars_validate_or_raise(vars_str):
return vars_str
except ParseError as e:
raise RestValidationError(str(e))
def validate_container_image_name(value):
"""
from https://github.com/distribution/distribution/blob/af8ac809336c2316c81b08605d92d94f8670ad15/reference/reference.go#L4
Grammar
reference := name [ ":" tag ] [ "@" digest ]
name := [domain '/'] path-component ['/' path-component]*
domain := domain-component ['.' domain-component]* [':' port-number]
domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
port-number := /[0-9]+/
path-component := alpha-numeric [separator alpha-numeric]*
alpha-numeric := /[a-z0-9]+/
separator := /[_.]|__|[-]*/
tag := /[\w][\w.-]{0,127}/
digest := digest-algorithm ":" digest-hex
digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
digest-algorithm-separator := /[+.-_]/
digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
The regex below is the printed value of the following GO variable, which represents the 'reference' line above, i.e. the full image name + tag or digest
https://github.com/distribution/distribution/blob/af8ac809336c2316c81b08605d92d94f8670ad15/reference/regexp.go#L72
"""
# fmt: off
regex = re.compile(r"""
^
( # name
(?: # domain (optional)
(?:
[a-zA-Z0-9] # domain-component
|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]
)
(?:
(?:
\. # additional domain-components, separated by a dot
(?:
[a-zA-Z0-9]
|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]
)
)+
)?
(?::[0-9]+)? # port number
/ # domain should end in slash
)?
[a-z0-9]+ # path-component
(?:
(?:
(?:
[_.]|__|[-]* # path-components can contain separators
)
[a-z0-9]+
)+
)?
(?:
(?:
/ # additional path-components, separated by a /
[a-z0-9]+
(?:
(?:
(?:
[_.]|__|[-]*
)
[a-z0-9]+
)+
)?
)+
)?
) # name end
(?:
: # tag (optional)
([\w][\w.-]{0,127}) # tag limited to 128 characters
)?
(?:
@ # digest (optional)
(
[A-Za-z][A-Za-z0-9]* # digest-algorithm-component, e.g. sha256
(?: # additional digest-alorithm components, separated by [-_+.]
[-_+.][A-Za-z][A-Za-z0-9]*
)*
[:][0-9a-fA-F]{32,} # digest-hex
)
)?
$
""", re.VERBOSE)
# fmt: on
if not regex.fullmatch(value):
raise ValidationError(_(f"The container image name {value} is not valid"))

View File

@ -40,7 +40,7 @@ class ExecutionEnvironment(HasCreate, HasCopy, base.Base):
def payload(self, name='', image=None, organization=None, credential=None, pull='', **kwargs):
payload = PseudoNamespace(
name=name or "EE - {}".format(random_title()),
image=image or random_title(10),
image=image or "example.invalid/component:tagname",
organization=organization.id if organization else None,
credential=credential.id if credential else None,
pull=pull,