8000 Add custom validator to blueprint variables by phobologic · Pull Request #218 · cloudtools/stacker · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add custom validator to blueprint variables #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/blueprints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ supports the following optional keys:
**description:**
A string that describes the purpose of the variable.

**validator:**
An optional function that can do custom validation of the variable. A
validator function should take a single argument, the value being validated,
and should return the value if validation is successful. If there is an
issue validating the value, an exception (``ValueError``, ``TypeError``, etc)
should be raised by the function.

**no_echo:**
Only valid for variables whose type subclasses ``CFNType``. Whether to
mask the parameter value whenever anyone makes a call that describes the
Expand Down Expand Up @@ -81,6 +88,7 @@ supports the following optional keys:
that explains the constraint when the constraint is violated for the
CloudFormation Parameter.


Variable Types
==============

Expand Down
138 changes: 105 additions & 33 deletions stacker/blueprints/base.py
8000
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from ..exceptions import (
MissingLocalParameterException,
MissingVariable,
UnresolvedVariables,
UnresolvedVariable,
UnresolvedVariables,
ValidatorError,
VariableTypeRequired,
)
from .variables.types import CFNType

Expand Down Expand Up @@ -130,6 +132,95 @@ def build_parameter(name, properties):
return p


def validate_variable_type(var_name, var_type, value):
"""Ensures the value is the correct variable type.

Args:
var_name (str): The name of the defined variable on a blueprint.
var_type (type): The type that the value should be.
value (obj): The object representing the value provided for the
variable

Returns:
object: A python object of type `var_type` based on the provided
`value`.

Raises:
ValueError: If the `value` isn't of `var_type` and can't be cast as
that type, this is raised.
"""

if isinstance(var_type, CFNType):
value = CFNParameter(name=var_name, value=value)
else:
if not isinstance(value, var_type):
try:
value = var_type(value)
except ValueError:
raise ValueError("Variable %s must be %s.",
var_name, var_type)
return value


def resolve_variable(var_name, var_def, provided_variable, blueprint_name):
"""Resolve a provided variable value against the variable definition.

Args:
var_name (str): The name of the defined variable on a blueprint.
var_def (dict): A dictionary representing the defined variables
attributes.
provided_variable (:class:`stacker.variables.Variable`): The variable
value provided to the blueprint.
blueprint_name (str): The name of the blueprint that the variable is
being applied to.

Returns:
object: The resolved variable value, could be any python object.

Raises:
MissingVariable: Raised when a variable with no default is not
provided a value.
UnresolvedVariable: Raised when the provided variable is not already
resolved.
ValueError: Raised when the value is not the right type and cannot be
cast as the correct type. Raised by
:func:`stacker.blueprints.base.validate_variable_type`
ValidatorError: Raised when a validator raises an exception. Wraps the
original exception.
"""

try:
var_type = var_def["type"]
except KeyError:
raise VariableTypeRequired(blueprint_name, var_name)

if provided_variable:
if not provided_variable.resolved:
raise UnresolvedVariable(blueprint_name, provided_variable)
if provided_variable.value is not None:
value = provided_variable.value
else:
# Variable value not provided, try using the default, if it exists
# in the definition
try:
value = var_def["default"]
except KeyError:
raise MissingVariable(blueprint_name, var_name)

# If no validator, return the value as is, otherwise apply validator
validator = var_def.get("validator", lambda v: v)
try:
value = validator(value)
except Exception as exc:
raise ValidatorError(var_name, validator.__name__, value, exc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add this to docstring above


# Ensure that the resulting value is the correct type
var_type = var_def.get("type")
value = validate_variable_type(var_name, var_type, value)

return value


class Blueprint(object):
"""Base implementation for dealing with a troposphere template.

Expand Down Expand Up @@ -229,9 +320,11 @@ def get_variables(self):
Returns:
dict: variables available to the template

Raises:

"""
if self.resolved_variables is None:
raise UnresolvedVariables(self)
raise UnresolvedVariables(self.name)
return self.resolved_variables

def get_cfn_parameters(self):
Expand All @@ -249,48 +342,27 @@ def get_cfn_parameters(self):
output[key] = value.to_parameter_value()
return output

def resolve_variables(self, variables):
def resolve_variables(self, provided_variables):
"""Resolve the values of the blueprint variables.

This will resolve the values of the `VARIABLES` with values from the
env file, the config, and any lookups resolved.

Args:
variables (list of :class:`stacker.variables.Variable`): list of
variables
provided_variables (list of :class:`stacker.variables.Variable`):
list of provided variables

"""
self.resolved_variables = {}
defined_variables = self.defined_variables()
variable_dict = dict((var.name, var) for var in variables)
variable_dict = dict((var.name, var) for var in provided_variables)
for var_name, var_def in defined_variables.iteritems():
value = var_def.get("default")
if value is None and var_name not in variable_dict:
raise MissingVariable(self, var_name)

variable = variable_dict.get(var_name)
if variable:
if not variable.resolved:
raise UnresolvedVariable(self, variable)
if variable.value is not None:
value = variable.value

if value is None:
logger.debug("Got `None` value for variable %s, ignoring it. "
"Default value should be used.", var_name)
continue

var_type = var_def.get("type")
if var_type:
if isinstance(var_type, CFNType):
value = CFNParameter(name=var_name, value=value)
else:
if not isinstance(value, var_type):
try:
value = var_type(value)
except ValueError:
raise ValueError("Variable %s must be %s.",
var_name, var_type)
value = resolve_variable(
var_name,
var_def,
variable_dict.get(var_name),
self.name
)
self.resolved_variables[var_name] = value

def import_mappings(self):
Expand Down
45 changes: 38 additions & 7 deletions stacker/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,41 @@ def __init__(self, lookup, *args, **kwargs):

class UnresolvedVariables(Exception):

def __init__(self, blueprint, *args, **kwargs):
def __init__(self, blueprint_name, *args, **kwargs):
message = "Blueprint: \"%s\" hasn't resolved it's variables" % (
blueprint.name)
blueprint_name)
super(UnresolvedVariables, self).__init__(message, *args, **kwargs)


class UnresolvedVariable(Exception):

def __init__(self, blueprint, variable, *args, **kwargs):
def __init__(self, blueprint_name, variable, *args, **kwargs):
message = (
"Variable \"%s\" in blueprint \"%s\" hasn't been resolved"
) % (variable.name, blueprint.name)
"Variable \"%s\" in blueprint \"%s\" hasn't been resolved" % (
variable.name, blueprint_name
)
)
super(UnresolvedVariable, self).__init__(message, *args, **kwargs)


class MissingVariable(Exception):

def __init__(self, blueprint, variable_name, *args, **kwargs):
def __init__(self, blueprint_name, variable_name, *args, **kwargs):
message = "Variable \"%s\" in blueprint \"%s\" is missing" % (
variable_name, blueprint.name)
variable_name, blueprint_name)
super(MissingVariable, self).__init__(message, *args, **kwargs)


class VariableTypeRequired(Exception):

def __init__(self, blueprint_name, variable_name, *args, **kwargs):
message = (
"Variable \"%s\" in blueprint \"%s\" does not have a type" % (
variable_name, blueprint_name)
)
super(VariableTypeRequired, self).__init__(message, *args, **kwargs)


class StackDoesNotExist(Exception):

def __init__(self, stack_name, *args, **kwargs):
Expand Down Expand Up @@ -106,3 +118,22 @@ class StackDidNotChange(Exception):

class CancelExecution(Exception):
"""Exception raised when we want to cancel executing the plan."""


class ValidatorError(Exception):
"""Used for errors raised by custom validators of blueprint variables.
"""
def __init__(self, variable, validator, value, exception=None):
self.variable = variable
self.validator = validator
self.value = value
self.exception = exception
self.message = ("Validator '%s' failed for variable '%s' with value "
"'%s'") % (self.validator, self.variable, self.value)

if self.exception:
self.message += ": %s: %s" % (self.exception.__class__.__name__,
str(self.exception))

def __str__(self):
return self.message
2 changes: 1 addition & 1 deletion stacker/lookups/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def extract_lookups_from_string(value):
value (str): string value we're extracting lookups from

Returns:
list: list of lookups if any
list: list of :class:`stacker.lookups.Lookup` if any

"""
lookups = set()
Expand Down
Loading
0