mirror of
https://github.com/ansible/awx.git
synced 2026-03-21 19:07:39 -02:30
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:
24
awx/main/migrations/0147_validate_ee_image_field.py
Normal file
24
awx/main/migrations/0147_validate_ee_image_field.py
Normal 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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
from awx.main.models.base import CommonModel
|
from awx.main.models.base import CommonModel
|
||||||
|
from awx.main.validators import validate_container_image_name
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['ExecutionEnvironment']
|
__all__ = ['ExecutionEnvironment']
|
||||||
@@ -31,6 +32,7 @@ class ExecutionEnvironment(CommonModel):
|
|||||||
max_length=1024,
|
max_length=1024,
|
||||||
verbose_name=_('image location'),
|
verbose_name=_('image location'),
|
||||||
help_text=_("The full image location, including the container registry, image name, and version tag."),
|
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)
|
managed_by_tower = models.BooleanField(default=False, editable=False)
|
||||||
credential = models.ForeignKey(
|
credential = models.ForeignKey(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from awx.main.validators import (
|
|||||||
validate_certificate,
|
validate_certificate,
|
||||||
validate_ssh_private_key,
|
validate_ssh_private_key,
|
||||||
vars_validate_or_raise,
|
vars_validate_or_raise,
|
||||||
|
validate_container_image_name,
|
||||||
)
|
)
|
||||||
from awx.main.tests.data.ssh import (
|
from awx.main.tests.data.ssh import (
|
||||||
TEST_SSH_RSA1_KEY_DATA,
|
TEST_SSH_RSA1_KEY_DATA,
|
||||||
@@ -163,3 +164,39 @@ def test_valid_vars(var_str):
|
|||||||
def test_invalid_vars(var_str):
|
def test_invalid_vars(var_str):
|
||||||
with pytest.raises(RestValidationError):
|
with pytest.raises(RestValidationError):
|
||||||
vars_validate_or_raise(var_str)
|
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)
|
||||||
|
|||||||
@@ -195,3 +195,95 @@ def vars_validate_or_raise(vars_str):
|
|||||||
return vars_str
|
return vars_str
|
||||||
except ParseError as e:
|
except ParseError as e:
|
||||||
raise RestValidationError(str(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"))
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ExecutionEnvironment(HasCreate, HasCopy, base.Base):
|
|||||||
def payload(self, name='', image=None, organization=None, credential=None, pull='', **kwargs):
|
def payload(self, name='', image=None, organization=None, credential=None, pull='', **kwargs):
|
||||||
payload = PseudoNamespace(
|
payload = PseudoNamespace(
|
||||||
name=name or "EE - {}".format(random_title()),
|
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,
|
organization=organization.id if organization else None,
|
||||||
credential=credential.id if credential else None,
|
credential=credential.id if credential else None,
|
||||||
pull=pull,
|
pull=pull,
|
||||||
|
|||||||
Reference in New Issue
Block a user