Add validation-cleanup spec
Change-Id: Ibf682f7bd8641d61dc6fa3758cdedcef53e91b7c Blueprint: validation-cleanup
This commit is contained in:
parent
81d8ccb090
commit
eda0ae7528
|
@ -0,0 +1,337 @@
|
|||
..
|
||||
|
||||
This work is licensed under a Creative Commons Attribution 3.0 Unported License.
|
||||
http://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
|
||||
=============================
|
||||
Centralize Validation Logic
|
||||
=============================
|
||||
|
||||
https://blueprints.launchpad.net/designate/+spec/validation-cleanup
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
Today, validations are duplicated between the V1 and V2 APIs, and validations
|
||||
will be required in additional places going forward (Inbound AXFR, DynamicDNS
|
||||
etc). Centralizing these validations into the Designate Objects provides a
|
||||
single re-usable home for all entry points to use.
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
Centralizing this logic will require a number of phases to complete:
|
||||
|
||||
1. Implement an Object Registry
|
||||
2. Implement Object Validation
|
||||
3. Implement an "Adaptor" layer, replacing the V2 APIs Views
|
||||
4. Migrate schemas from `designate/resources/schemas` into the Objects
|
||||
5. Update the API layer (both V1 and V2) to use the new Validations and
|
||||
Adaptors
|
||||
|
||||
Object Registry
|
||||
---------------
|
||||
|
||||
The object registry will allow for looking up a reference to any
|
||||
DesignateObject's class via the class name. This will allow an object's schema
|
||||
to reference other object easily.
|
||||
|
||||
.. note:: The Object Registry will **not** replace the standard and existing
|
||||
method of retrieving an Object class by importing it. The Registry
|
||||
provides an **alternative** method of obtaining a class reference
|
||||
using only the class name. This is useful for scenarios where we need
|
||||
to reference an Object outside of python code. For example, in a
|
||||
JSON-Schema, or in JSON messages passed from service to service with
|
||||
oslo.messaging.
|
||||
|
||||
To implement the registry, the `DesignateObjectMetaclass` class will be updated
|
||||
to track a reference to each of the Object classes as they are constructed.
|
||||
Theese references will be stored in a dictionary attached to the
|
||||
`DesignateObject` base class.
|
||||
|
||||
.. note:: The `DesignateObjectMetaclass` code is executed while the object
|
||||
classes are constructed, rather than when the object instances are
|
||||
created. This ensures the code is only executed once upon startup of
|
||||
the Designate services.
|
||||
|
||||
Registry lookups will be performed via a new
|
||||
`DesignateObject.obj_cls_from_name()` method, which will accept a single
|
||||
string argument for the Object Name.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DesignateObject(object):
|
||||
@classmethod
|
||||
def obj_cls_from_name(cls, name):
|
||||
pass
|
||||
|
||||
An example usage of the registry:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class RecordSet(DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {},
|
||||
'name': {},
|
||||
}
|
||||
|
||||
RecordSet = DesignateObject.obj_cls_from_name('RecordSet')
|
||||
|
||||
my_recordset = RecordSet(id='12345', name='example.org.')
|
||||
|
||||
|
||||
Object Validation
|
||||
-----------------
|
||||
|
||||
Object Validation rules will continue to use JSON-Schema, implemented on a
|
||||
per-field level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ValidatableObject(DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'required': True,
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid'
|
||||
}
|
||||
},
|
||||
'ttl': {
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
'maximum': 100
|
||||
}
|
||||
},
|
||||
'recursive': {
|
||||
'schema': {
|
||||
'$ref': 'obj://ValidatableObject/#',
|
||||
}
|
||||
},
|
||||
'nested': {
|
||||
'schema': {
|
||||
'$ref': 'obj://AnotherObject/#',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
To construct the final and complete scheme, and instanciate the schema
|
||||
validator, the `DesignateObjectMetaclass` class will be updated
|
||||
to call a `make_class_validator(cls)` method, implemented similarily to
|
||||
the `make_class_properties(cls)` method.
|
||||
|
||||
This `make_class_validator` method will assemble the per-field schema fragments
|
||||
into a full JSON Schema, with the necessary boilerplate being generated.
|
||||
Additionally, this method will construct the python-jsonschema Validator
|
||||
instance and attach it to the objects class as cls._obj_validator.
|
||||
|
||||
Finally, three new methods will be added to the base `DesignateObject` class:
|
||||
|
||||
1. A `obj_get_schema(cls)` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DesignateObject(object):
|
||||
@classmethod
|
||||
def obj_get_schema(cls):
|
||||
"""Returns the JSON Schema for this Object."""
|
||||
|
||||
2. A `is_valid(self)` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DesignateObject(object):
|
||||
def is_valid(self):
|
||||
"""Returns True if the Object is valid."""
|
||||
|
||||
3. A `validate(self)` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DesignateObject(object):
|
||||
def validate(self):
|
||||
"""
|
||||
Raises an InvalidObject exception if the Object is invalid
|
||||
|
||||
Attached to the `errors` attribute of exception will be a
|
||||
`ValidationErrorList` object containing the details of the
|
||||
failures.
|
||||
"""
|
||||
|
||||
An example usage of the validation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class RecordSet(DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'required': True,
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid'
|
||||
}
|
||||
},
|
||||
'ttl': {
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
'maximum': 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my_recordset = RecordSet(id='12345', ttl=50)
|
||||
|
||||
# Returns False, as the 12345 is NOT a UUID.
|
||||
my_recordset.is_valid()
|
||||
|
||||
try:
|
||||
# Raises an InvalidObject exception, as the 12345 is NOT a UUID.
|
||||
my_recordset.validate()
|
||||
except InvalidObject as e:
|
||||
LOG.warning('Invalid Object, Errors below:')
|
||||
|
||||
for error in e.errors:
|
||||
LOG.warning('Error at path %s, Message: %s', e.absolute_path,
|
||||
e.message)
|
||||
|
||||
|
||||
Object Adaptors
|
||||
---------------
|
||||
|
||||
Object Adaptors will replace the current V2 API Views, allowing for a
|
||||
structured way to convert from Object to V1 or V2 API formats. This will
|
||||
include renaming of fields in standard output, rendered JSON Schemas,
|
||||
as well as in ValidationError messages, and will support hiding fields which
|
||||
should not be visible in the matching API version.
|
||||
|
||||
.. note:: Below is a WIP mockup - Expect changes!
|
||||
|
||||
Example usage of the Object Adaptors:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Standard Object Definition
|
||||
class Domain(DesignateObject):
|
||||
FIELDS = {
|
||||
'id': {
|
||||
'required': True,
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'uuid'
|
||||
}
|
||||
},
|
||||
'name': {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'pattern': 'domainname'
|
||||
}
|
||||
},
|
||||
'ttl': {
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
'maximum': 100
|
||||
}
|
||||
},
|
||||
'version': {
|
||||
'schema': {
|
||||
'type': 'integer',
|
||||
'minimum': 0,
|
||||
'maximum': 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Define the V2 API Adaptor for the Domain Object above
|
||||
class DomainAdaptorV2(DesignateObjectAdaptorV2):
|
||||
obj_cls = Domain
|
||||
obj_list_cls = DomainList
|
||||
|
||||
# Any fields NOT specificed will not be returned by the API.
|
||||
FIELDS = {
|
||||
'id': {
|
||||
# No V2 Specific Customization Needed
|
||||
}
|
||||
'ttl': {
|
||||
# Let's rename "ttl" to "default_ttl" in V2
|
||||
'name': 'default_ttl'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Use the Adaptor in the API
|
||||
class ZonesController(rest.RestController):
|
||||
_adaptor = DomainAdaptorV2()
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
@utils.validate_uuid('zone_id')
|
||||
def get_one(self, zone_id):
|
||||
"""Get Zone"""
|
||||
|
||||
# Real life would Fetch a zone from designate-central
|
||||
domain = Domain(id='2b9e1b86-d4f1-42d2-88ff-b888f2dd068a'
|
||||
name='example.com.',
|
||||
ttl=50)
|
||||
|
||||
return self._adaptor.render(domain, single=True)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def post_all(self):
|
||||
"""Create Zone"""
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
|
||||
# The Adaptor class will parse the incoming JSON into an
|
||||
# approperiate Object instance, trigger validation, and raise
|
||||
# an exception if there are any failures. The
|
||||
# `FaultWrapperMiddleware` will catch and render this exception.
|
||||
domain = self._adaptor.parse(request.body_dict, single=True)
|
||||
|
||||
# Create the Domain
|
||||
domain = self.central_api.create_domain(context, domain)
|
||||
|
||||
# Prepare the response headers and status
|
||||
response.status_int = 201
|
||||
response.headers['Location'] = '<url for new zone>'
|
||||
|
||||
# Send the response
|
||||
return self._adaptor.render(domain)
|
||||
|
||||
|
||||
Other Changes
|
||||
-------------
|
||||
|
||||
Any other changes to Designate, broken down by which sub system is being
|
||||
changed
|
||||
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Primary assignee:
|
||||
kiall
|
||||
|
||||
Milestones
|
||||
----------
|
||||
|
||||
Target Milestone for completion:
|
||||
Kilo-1
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
Work items are as per the Proposed change section.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
- No known dependencies
|
Loading…
Reference in New Issue