Skip to content

Commit

Permalink
Use jsonschema for EE schema validation (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shrews authored Nov 7, 2022
1 parent 8a767ce commit 567c00e
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 141 deletions.
184 changes: 184 additions & 0 deletions ansible_builder/ee_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
from jsonschema import validate, SchemaError, ValidationError

from ansible_builder.exceptions import DefinitionError


############
# Version 1
############

schema_v1 = {
"type": "object",
"additionalProperties": False,
"properties": {
"version": {
"description": "The EE schema version number",
"type": "number",
},

"ansible_config": {
"type": "string",
},

"build_arg_defaults": {
"type": "object",
"additionalProperties": False,
"properties": {
"EE_BASE_IMAGE": {
"type": "string",
},
"EE_BUILDER_IMAGE": {
"type": "string",
},
"ANSIBLE_GALAXY_CLI_COLLECTION_OPTS": {
"type": "string",
},
},
},

"dependencies": {
"description": "The dependency stuff",
"type": "object",
"additionalProperties": False,
"properties": {
"python": {
"description": "The python dependency file",
"type": "string",
},
"galaxy": {
"description": "The Galaxy dependency file",
"type": "string",
},
"system": {
"description": "The system dependency file",
"type": "string",
},
},
},

"additional_build_steps": {
"type": "object",
"additionalProperties": False,
"properties": {
"prepend": {
"anyOf": [{"type": "string"}, {"type": "array"}],
},
"append": {
"anyOf": [{"type": "string"}, {"type": "array"}],
},
},
},
},
}


############
# Version 2
############

schema_v2 = {
"type": "object",
"additionalProperties": False,
"properties": {
"version": {
"description": "The EE schema version number",
"type": "number",
},

"ansible_config": {
"type": "string",
},

"build_arg_defaults": {
"type": "object",
"additionalProperties": False,
"properties": {
"ANSIBLE_GALAXY_CLI_COLLECTION_OPTS": {
"type": "string",
},
},
},

"dependencies": {
"description": "The dependency stuff",
"type": "object",
"additionalProperties": False,
"properties": {
"python": {
"description": "The python dependency file",
"type": "string",
},
"galaxy": {
"description": "The Galaxy dependency file",
"type": "string",
},
"system": {
"description": "The system dependency file",
"type": "string",
},
},
},

"images": {
"type": "object",
"additionalProperties": False,
"properties": {
"base_image": {
"type": "object",
"properties": {
"name": {
"type": "string",
},
"signature_original_name": {
"type": "string",
},
},
},
"builder_image": {
"type": "object",
"properties": {
"name": {
"type": "string",
},
"signature_original_name": {
"type": "string",
},
},
}
},
},

"additional_build_steps": {
"type": "object",
"additionalProperties": False,
"properties": {
"prepend": {
"anyOf": [{"type": "string"}, {"type": "array"}],
},
"append": {
"anyOf": [{"type": "string"}, {"type": "array"}],
},
},
},
},
}


def validate_schema(ee_def: dict):
schema_version = 1
if 'version' in ee_def:
try:
schema_version = int(ee_def['version'])
except ValueError:
raise DefinitionError(f"Schema version not an integer: {ee_def['version']}")

if schema_version not in (1, 2):
raise DefinitionError(f"Unsupported schema version: {schema_version}")

try:
if schema_version == 1:
validate(instance=ee_def, schema=schema_v1)
elif schema_version == 2:
validate(instance=ee_def, schema=schema_v2)
except (SchemaError, ValidationError) as e:
raise DefinitionError(msg=e.message, path=e.absolute_schema_path)
6 changes: 5 additions & 1 deletion ansible_builder/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import sys

from collections import deque
from typing import Optional


class DefinitionError(RuntimeError):
# Eliminate the output of traceback before our custom error message prints out
sys.tracebacklimit = 0

def __init__(self, msg):
def __init__(self, msg: str, path: Optional[deque] = None):
super(DefinitionError, self).__init__("%s" % msg)
self.msg = msg
self.path = path
2 changes: 1 addition & 1 deletion ansible_builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def _handle_image_validation_opts(self, policy, keyring):
resolved_keyring = None

if policy is not None:
if self.version != "2":
if self.version != 2:
raise ValueError(f'--container-policy not valid with version {self.version} format')

# Require podman runtime
Expand Down
138 changes: 15 additions & 123 deletions ansible_builder/user_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from . import constants
from .exceptions import DefinitionError
from .ee_schema import validate_schema


ALLOWED_KEYS_V1 = [
Expand Down Expand Up @@ -111,12 +112,12 @@ def __init__(self, filename):
@property
def version(self):
"""
Version of the EE file.
Integer version of the EE file.
If no version is specified, assume version 1 (for backward compat).
"""
version = self.raw.get('version', 1)
return str(version)
return version

@property
def ansible_config(self):
Expand Down Expand Up @@ -180,84 +181,13 @@ def get_dep_abs_path(self, entry):

return os.path.join(self.reference_path, req_file)

def _validate_root_keys(self):
"""
Identify any invalid top-level keys in the execution environment file.
:raises: DefinitionError exception if any invalid keys are identified.
"""
def_file_dict = self.raw
yaml_keys = set(def_file_dict.keys())

valid_keys = set(ALLOWED_KEYS_V1)
if self.version >= '2':
valid_keys = valid_keys.union(set(ALLOWED_KEYS_V2))

invalid_keys = yaml_keys - valid_keys

if invalid_keys:
raise DefinitionError(textwrap.dedent(
f"""
Error: Unknown yaml key(s), {invalid_keys}, found in the definition file.\n
Allowed options are:
{valid_keys}
""")
)

def _validate_v2(self):
"""
Validate all execution environment file, version 2, keys.
:raises: DefinitionError exception if any errors are found.
"""

if self.version == "1":
return

images = self.raw.get('images', {})

# The base and builder images MUST be defined in the 'images' section only.
bad = self.raw.get('build_arg_defaults')
if bad:
if 'EE_BASE_IMAGE' in bad or 'EE_BUILDER_IMAGE' in bad:
raise DefinitionError("Error: Version 2 does not allow defining EE_BASE_IMAGE or EE_BUILDER_IMAGE in 'build_arg_defaults'")

if images:
self.base_image = ImageDescription(images, 'base_image')
if images.get('builder_image'):
self.builder_image = ImageDescription(images, 'builder_image')
self.build_arg_defaults['EE_BUILDER_IMAGE'] = self.builder_image.name

# Must set these values so that Containerfile uses the proper images
self.build_arg_defaults['EE_BASE_IMAGE'] = self.base_image.name

def _validate_v1(self):
def validate(self):
"""
Validate all execution environment file, version 1, keys.
Check that all specified keys in the definition file are valid.
:raises: DefinitionError exception if any errors are found.
"""

if self.raw.get('dependencies') is not None:
if not isinstance(self.raw.get('dependencies'), dict):
raise DefinitionError(textwrap.dedent(
f"""
Error: Unknown type {type(self.raw.get('dependencies'))} found for dependencies, must be a dict.\n
Allowed options are:
{list(constants.CONTEXT_FILES.keys())}
""")
)

dependencies_keys = set(self.raw.get('dependencies'))
invalid_dependencies_keys = dependencies_keys - set(constants.CONTEXT_FILES.keys())
if invalid_dependencies_keys:
raise DefinitionError(textwrap.dedent(
f"""
Error: Unknown yaml key(s), {invalid_dependencies_keys}, found in dependencies.\n
Allowed options are:
{list(constants.CONTEXT_FILES.keys())}
""")
)
validate_schema(self.raw)

for item in constants.CONTEXT_FILES:
# HACK: non-file deps for dynamic base/builder
Expand All @@ -271,54 +201,16 @@ def _validate_v1(self):
# Validate and set any user-specified build arguments
build_arg_defaults = self.raw.get('build_arg_defaults')
if build_arg_defaults:
if not isinstance(build_arg_defaults, dict):
raise DefinitionError(
f"Error: Unknown type {type(build_arg_defaults)} found for build_arg_defaults; "
f"must be a dict."
)
unexpected_keys = set(build_arg_defaults) - set(constants.build_arg_defaults)
if unexpected_keys:
raise DefinitionError(
f"Keys {unexpected_keys} are not allowed in 'build_arg_defaults'."
)
for key, user_value in build_arg_defaults.items():
if user_value and not isinstance(user_value, str):
raise DefinitionError(
f"Expected build_arg_defaults.{key} to be a string; "
f"Found a {type(user_value)} instead."
)
self.build_arg_defaults[key] = user_value

additional_cmds = self.get_additional_commands()
if additional_cmds:
if not isinstance(additional_cmds, dict):
raise DefinitionError(textwrap.dedent("""
Expected 'additional_build_steps' in the provided definition file to be a dictionary
with keys 'prepend' and/or 'append'; found a {0} instead.
""").format(type(additional_cmds).__name__))

expected_keys = frozenset(('append', 'prepend'))
unexpected_keys = set(additional_cmds) - expected_keys
if unexpected_keys:
raise DefinitionError(
f"Keys {*unexpected_keys,} are not allowed in 'additional_build_steps'."
)

ansible_config_path = self.raw.get('ansible_config')
if ansible_config_path:
if not isinstance(ansible_config_path, str):
raise DefinitionError(textwrap.dedent(f"""
Expected 'ansible_config' in the provided definition file to
be a string; found a {type(ansible_config_path).__name__} instead.
"""))

def validate(self):
"""
Check that all specified keys in the definition file are valid.
:raises: DefinitionError exception if any errors are found.
"""
if self.version == 2:
images = self.raw.get('images', {})
if images:
self.base_image = ImageDescription(images, 'base_image')
if images.get('builder_image'):
self.builder_image = ImageDescription(images, 'builder_image')
self.build_arg_defaults['EE_BUILDER_IMAGE'] = self.builder_image.name

self._validate_root_keys()
self._validate_v1()
self._validate_v2()
# Must set these values so that Containerfile uses the proper images
self.build_arg_defaults['EE_BASE_IMAGE'] = self.base_image.name
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PyYAML
requirements-parser
bindep
jsonschema
Loading

0 comments on commit 567c00e

Please sign in to comment.