Skip to content

Commit

Permalink
Rewrote Lookup-parser
Browse files Browse the repository at this point in the history
The new parser will build the entire AST to support nested lookups.
  • Loading branch information
nielslaukens committed Sep 27, 2018
1 parent 7dd6040 commit e2062a2
Show file tree
Hide file tree
Showing 7 changed files with 476 additions and 181 deletions.
42 changes: 37 additions & 5 deletions stacker/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ def __init__(self, lookup, lookups, value, *args, **kwargs):
message = (
"Lookup: \"{}\" has non-string return value, must be only lookup "
"present (not {}) in \"{}\""
).format(lookup.raw, len(lookups), value)
).format(str(lookup), len(lookups), value)
super(InvalidLookupCombination, self).__init__(message,
*args,
**kwargs)


class InvalidLookupConcatenation(Exception):
"""
Intermediary Exception to be converted to InvalidLookupCombination once it
bubbles up there
"""
def __init__(self, lookup, lookups, *args, **kwargs):
self.lookup = lookup
self.lookups = lookups
super(InvalidLookupConcatenation, self).__init__("", *args, **kwargs)


class UnknownLookupType(Exception):

def __init__(self, lookup, *args, **kwargs):
self.lookup = lookup
message = "Unknown lookup type: \"{}\"".format(lookup.type)
def __init__(self, lookup_type, *args, **kwargs):
message = "Unknown lookup type: \"{}\"".format(lookup_type)
super(UnknownLookupType, self).__init__(message, *args, **kwargs)


Expand All @@ -35,11 +45,22 @@ def __init__(self, variable_name, lookup, error, *args, **kwargs):
self.lookup = lookup
self.error = error
message = "Couldn't resolve lookup in variable `%s`, " % variable_name
message += "lookup: ${%s}: " % lookup.raw
message += "lookup: ${%s}: " % repr(lookup)
message += "(%s) %s" % (error.__class__, error)
super(FailedVariableLookup, self).__init__(message, *args, **kwargs)


class FailedLookup(Exception):
"""
Intermediary Exception to be converted to FailedVariableLookup once it
bubbles up there
"""
def __init__(self, lookup, error, *args, **kwargs):
self.lookup = lookup
self.error = error
super(FailedLookup, self).__init__("Failed lookup", *args, **kwargs)


class InvalidUserdataPlaceholder(Exception):

def __init__(self, blueprint_name, exception_message, *args, **kwargs):
Expand Down Expand Up @@ -70,6 +91,17 @@ def __init__(self, blueprint_name, variable, *args, **kwargs):
super(UnresolvedVariable, self).__init__(message, *args, **kwargs)


class UnresolvedVariableValue(Exception):
"""
Intermediary Exception to be converted to UnresolvedVariable once it
bubbles up there
"""
def __init__(self, lookup, *args, **kwargs):
self.lookup = lookup
super(UnresolvedVariableValue, self).__init__(
"Unresolved lookup", *args, **kwargs)


class MissingVariable(Exception):

def __init__(self, blueprint_name, variable_name, *args, **kwargs):
Expand Down
28 changes: 7 additions & 21 deletions stacker/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@
Variable,
resolve_variables,
)
from .lookups.handlers.output import (
TYPE_NAME as OUTPUT_LOOKUP_TYPE_NAME,
deconstruct,
)

from .blueprints.raw import RawTemplateBlueprint
from .exceptions import FailedVariableLookup


def _gather_variables(stack_def):
Expand Down Expand Up @@ -93,22 +88,13 @@ def requires(self):

# Add any dependencies based on output lookups
for variable in self.variables:
for lookup in variable.lookups:
if lookup.type == OUTPUT_LOOKUP_TYPE_NAME:

try:
d = deconstruct(lookup.input)
except ValueError as e:
raise FailedVariableLookup(self.name, lookup, e)

if d.stack_name == self.name:
message = (
"Variable %s in stack %s has a ciruclar reference "
"within lookup: %s"
) % (variable.name, self.name, lookup.raw)
raise ValueError(message)
requires.add(d.stack_name)

deps = variable.dependencies()
if self.name in deps:
message = (
"Variable %s in stack %s has a ciruclar reference"
) % (variable.name, self.name)
raise ValueError(message)
requires.update(deps)
return requires

@property
Expand Down
42 changes: 11 additions & 31 deletions stacker/tests/blueprints/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from stacker.variables import Variable
from stacker.lookups import register_lookup_handler

from ..factories import mock_lookup, mock_context
from ..factories import mock_context


def mock_lookup_handler(value, provider=None, context=None, fqn=False,
Expand Down Expand Up @@ -424,11 +424,8 @@ class TestBlueprint(Blueprint):
Variable("Param2", "${output other-stack::Output}"),
Variable("Param3", 3),
]
resolved_lookups = {
mock_lookup("other-stack::Output", "output"): "Test Output",
}
for var in variables:
var.replace(resolved_lookups)

variables[1]._value._resolve("Test Output")

blueprint.resolve_variables(variables)
self.assertEqual(blueprint.resolved_variables["Param1"], 1)
Expand All @@ -443,13 +440,8 @@ class TestBlueprint(Blueprint):

blueprint = TestBlueprint(name="test", context=MagicMock())
variables = [Variable("Param1", "${custom non-string-return-val}")]
lookup = mock_lookup("non-string-return-val", "custom",
"custom non-string-return-val")
resolved_lookups = {
lookup: ["something"],
}
for var in variables:
var.replace(resolved_lookups)
var._value._resolve(["something"])

blueprint.resolve_variables(variables)
self.assertEqual(blueprint.resolved_variables["Param1"], ["something"])
Expand All @@ -462,13 +454,8 @@ class TestBlueprint(Blueprint):

blueprint = TestBlueprint(name="test", context=MagicMock())
variables = [Variable("Param1", "${custom non-string-return-val}")]
lookup = mock_lookup("non-string-return-val", "custom",
"custom non-string-return-val")
resolved_lookups = {
lookup: Base64("test"),
}
for var in variables:
var.replace(resolved_lookups)
var._value._resolve(Base64("test"))

blueprint.resolve_variables(variables)
self.assertEqual(blueprint.resolved_variables["Param1"].data,
Expand All @@ -480,20 +467,13 @@ class TestBlueprint(Blueprint):
"Param1": {"type": list},
}

variables = [
Variable(
"Param1",
"${custom non-string-return-val},${output some-stack::Output}",
)
]
lookup = mock_lookup("non-string-return-val", "custom",
"custom non-string-return-val")
resolved_lookups = {
lookup: ["something"],
}
variable = Variable(
"Param1",
"${custom non-string-return-val},${output some-stack::Output}",
)
variable._value[0]._resolve(["something"])
with self.assertRaises(InvalidLookupCombination):
for var in variables:
var.replace(resolved_lookups)
variable.value()

def test_get_variables(self):
class TestBlueprint(Blueprint):
Expand Down
38 changes: 18 additions & 20 deletions stacker/tests/lookups/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@
from __future__ import absolute_import
import unittest

from mock import patch, MagicMock
from mock import MagicMock

from stacker.exceptions import (
UnknownLookupType,
FailedVariableLookup,
)

from stacker.lookups.registry import (
LOOKUP_HANDLERS,
resolve_lookups,
)
from stacker.lookups.registry import LOOKUP_HANDLERS

from stacker.variables import Variable
from stacker.variables import Variable, VariableValueLookup

from ..factories import (
mock_context,
Expand Down Expand Up @@ -43,31 +40,32 @@ def test_autoloaded_lookup_handlers(self):
)

def test_resolve_lookups_string_unknown_lookup(self):
variable = Variable("MyVar", "${bad_lookup foo}")

with self.assertRaises(UnknownLookupType):
resolve_lookups(variable, self.ctx, self.provider)
Variable("MyVar", "${bad_lookup foo}")

def test_resolve_lookups_list_unknown_lookup(self):
variable = Variable(
"MyVar", [
"${bad_lookup foo}", "random string",
]
)

with self.assertRaises(UnknownLookupType):
resolve_lookups(variable, self.ctx, self.provider)
Variable(
"MyVar", [
"${bad_lookup foo}", "random string",
]
)

def resolve_lookups_with_output_handler_raise_valueerror(self, variable):
"""Mock output handler to throw ValueError, then run resolve_lookups
on the given variable.
"""
mock_handler = MagicMock(side_effect=ValueError("Error"))
with patch.dict(LOOKUP_HANDLERS, {"output": mock_handler}):
with self.assertRaises(FailedVariableLookup) as cm:
resolve_lookups(variable, self.ctx, self.provider)

self.assertIsInstance(cm.exception.error, ValueError)
# find the only lookup in the variable
for value in variable._value:
if isinstance(value, VariableValueLookup):
value.handler = mock_handler

with self.assertRaises(FailedVariableLookup) as cm:
variable.resolve(self.ctx, self.provider)

self.assertIsInstance(cm.exception.error, ValueError)

def test_resolve_lookups_string_failed_variable_lookup(self):
variable = Variable("MyVar", "${output foo::bar}")
Expand Down
2 changes: 2 additions & 0 deletions stacker/tests/test_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from mock import MagicMock
import unittest

from stacker.lookups import register_lookup_handler
from stacker.context import Context
from stacker.config import Config
from stacker.stack import Stack
Expand All @@ -20,6 +21,7 @@ def setUp(self):
definition=generate_definition("vpc", 1),
context=self.context,
)
register_lookup_handler("noop", lambda **kwargs: "test")

def test_stack_requires(self):
definition = generate_definition(
Expand Down
Loading

0 comments on commit e2062a2

Please sign in to comment.