Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ngfgrant authored Aug 15, 2019
2 parents ae94b8e + 234da22 commit 35b3a92
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 31 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Categories: Added, Removed, Changed, Fixed, Nonfunctional, Deprecated

## Unreleased

- Fix recursion in sceptre_user_data becoming infinite

## 2.1.5 (2019.06.28)

### Fixed
Expand Down
51 changes: 43 additions & 8 deletions sceptre/resolvers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# -*- coding: utf-8 -*-
import abc
import six
import logging
from contextlib import contextmanager

import six
from sceptre.helpers import _call_func_on_values


class RecursiveGet(Exception):
pass


@six.add_metaclass(abc.ABCMeta)
class Resolver:
"""
Expand Down Expand Up @@ -58,6 +63,7 @@ class ResolvableProperty(object):
def __init__(self, name):
self.name = "_" + name
self.logger = logging.getLogger(__name__)
self._get_in_progress = False

def __get__(self, instance, type):
"""
Expand All @@ -67,13 +73,19 @@ def __get__(self, instance, type):
:return: The attribute stored with the suffix ``name`` in the instance.
:rtype: dict or list
"""
def resolve(attr, key, value):
attr[key] = value.resolve()

if hasattr(instance, self.name):
return _call_func_on_values(
resolve, getattr(instance, self.name), Resolver
)
with self._no_recursive_get():
def resolve(attr, key, value):
try:
attr[key] = value.resolve()
except RecursiveGet:
attr[key] = self.ResolveLater(instance, self.name, key,
lambda: value.resolve())

if hasattr(instance, self.name):
retval = _call_func_on_values(
resolve, getattr(instance, self.name), Resolver
)
return retval

def __set__(self, instance, value):
"""
Expand All @@ -87,3 +99,26 @@ def setup(attr, key, value):

_call_func_on_values(setup, value, Resolver)
setattr(instance, self.name, value)

class ResolveLater(object):
"""Represents a value that could not yet be resolved but can be resolved in the future."""
def __init__(self, instance, name, key, resolution_function):
self._instance = instance
self._name = name
self._key = key
self._resolution_function = resolution_function

def __call__(self):
"""Resolve the value."""
attr = getattr(self._instance, self._name)
attr[self._key] = self._resolution_function()

@contextmanager
def _no_recursive_get(self):
if self._get_in_progress:
raise RecursiveGet()
self._get_in_progress = True
try:
yield
finally:
self._get_in_progress = False
34 changes: 29 additions & 5 deletions sceptre/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
"""

import logging
from typing import Mapping, Sequence

from sceptre.connection_manager import ConnectionManager
from sceptre.template import Template
from sceptre.helpers import get_external_stack_name
from sceptre.helpers import get_external_stack_name, sceptreise_path
from sceptre.hooks import HookProperty
from sceptre.resolvers import ResolvableProperty
from sceptre.helpers import sceptreise_path
from sceptre.template import Template


class Stack(object):
Expand Down Expand Up @@ -103,7 +103,7 @@ class Stack(object):
"""

parameters = ResolvableProperty("parameters")
sceptre_user_data = ResolvableProperty("sceptre_user_data")
_sceptre_user_data = ResolvableProperty("_sceptre_user_data")
notifications = ResolvableProperty("notifications")
hooks = HookProperty("hooks")

Expand Down Expand Up @@ -139,7 +139,8 @@ def __init__(
self.profile = profile
self.hooks = hooks or {}
self.parameters = parameters or {}
self.sceptre_user_data = sceptre_user_data or {}
self._sceptre_user_data = sceptre_user_data or {}
self._sceptre_user_data_is_resolved = False
self.notifications = notifications or []
self.stack_group_config = stack_group_config or {}

Expand Down Expand Up @@ -237,6 +238,17 @@ def connection_manager(self):

return self._connection_manager

@property
def sceptre_user_data(self):
"""Returns sceptre_user_data after ensuring that it is fully resolved.
:rtype: dict or list or None
"""
if not self._sceptre_user_data_is_resolved:
self._sceptre_user_data_is_resolved = True
self._resolve_sceptre_user_data()
return self._sceptre_user_data

@property
def template(self):
"""
Expand All @@ -253,3 +265,15 @@ def template(self):
connection_manager=self.connection_manager
)
return self._template

def _resolve_sceptre_user_data(self):
data = self._sceptre_user_data
if isinstance(data, Mapping):
iterator = data.values()
elif isinstance(data, Sequence):
iterator = data
else:
return
for value in iterator:
if isinstance(value, ResolvableProperty.ResolveLater):
value()
75 changes: 58 additions & 17 deletions sceptre/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import os
import sys
import threading
import traceback

import botocore
import jinja2
Expand Down Expand Up @@ -63,6 +64,41 @@ def __repr__(self):
)
)

def _print_template_traceback(self):
"""
Prints a stack trace, including only files which are inside a
'templates' directory. The function is intended to give the operator
instant feedback about why their templates are failing to compile.
:rtype: None
"""
def _print_frame(filename, line, fcn, line_text):
self.logger.error("{}:{}: Template error in '{}'\n=> `{}`".format(
filename, line, fcn, line_text))

try:
_, _, tb = sys.exc_info()
stack_trace = traceback.extract_tb(tb)
search_string = os.path.join('', 'templates', '')
if search_string in self.path:
template_path = self.path.split(search_string)[0] + search_string
else:
return
for frame in stack_trace:
if isinstance(frame, tuple):
# Python 2 / Old style stack frame
if template_path in frame[0]:
_print_frame(frame[0], frame[1], frame[2], frame[3])
else:
if template_path in frame.filename:
_print_frame(frame.filename, frame.lineno, frame.name, frame.line)
except Exception as tb_exception:
self.logger.error(
'A template error occured. ' +
'Additionally, a traceback exception occured. Exception: %s',
tb_exception
)

@property
def body(self):
"""
Expand All @@ -74,24 +110,29 @@ def body(self):
if self._body is None:
file_extension = os.path.splitext(self.path)[1]

if file_extension in {".json", ".yaml", ".template"}:
with open(self.path) as template_file:
self._body = template_file.read()
elif file_extension == ".j2":
self._body = self._render_jinja_template(
os.path.dirname(self.path),
os.path.basename(self.path),
{"sceptre_user_data": self.sceptre_user_data}
)
elif file_extension == ".py":
self._body = self._call_sceptre_handler()
try:
if file_extension in {".json", ".yaml", ".template"}:
with open(self.path) as template_file:
self._body = template_file.read()
elif file_extension == ".j2":
self._body = self._render_jinja_template(
os.path.dirname(self.path),
os.path.basename(self.path),
{"sceptre_user_data": self.sceptre_user_data}
)
elif file_extension == ".py":
self._body = self._call_sceptre_handler()

else:
raise UnsupportedTemplateFileTypeError(
"Template has file extension %s. Only .py, .yaml, "
".template, .json and .j2 are supported.",
os.path.splitext(self.path)[1]
)
except Exception as e:
self._print_template_traceback()
raise e

else:
raise UnsupportedTemplateFileTypeError(
"Template has file extension %s. Only .py, .yaml, "
".template, .json and .j2 are supported.",
os.path.splitext(self.path)[1]
)
return self._body

def _call_sceptre_handler(self):
Expand Down
9 changes: 9 additions & 0 deletions tests/test_resolvers/test_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-


class TestResolverCache(object):
def setup_method(self, setup_method):
pass

def test_singleton_cache(self):
pass
71 changes: 70 additions & 1 deletion tests/test_stack.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
# -*- coding: utf-8 -*-

import importlib
from mock import sentinel, MagicMock

from mock import MagicMock, sentinel
from sceptre.resolvers import Resolver
from sceptre.stack import Stack
from sceptre.template import Template


def stack_factory(**kwargs):
call_kwargs = {
'name': 'dev/app/stack',
'project_code': sentinel.project_code,
'template_bucket_name': sentinel.template_bucket_name,
'template_key_prefix': sentinel.template_key_prefix,
'required_version': sentinel.required_version,
'template_path': sentinel.template_path,
'region': sentinel.region,
'profile': sentinel.profile,
'parameters': {"key1": "val1"},
'sceptre_user_data': sentinel.sceptre_user_data,
'hooks': {},
's3_details': None,
'dependencies': sentinel.dependencies,
'role_arn': sentinel.role_arn,
'protected': False,
'tags': {"tag1": "val1"},
'external_name': sentinel.external_name,
'notifications': [sentinel.notification],
'on_failure': sentinel.on_failure,
'stack_timeout': sentinel.stack_timeout,
'stack_group_config': {}
}
call_kwargs.update(kwargs)
return Stack(**call_kwargs)


class TestStack(object):

def setup_method(self, test_method):
Expand Down Expand Up @@ -95,3 +124,43 @@ def test_repr_can_eval_correctly(self):
)
assert isinstance(evaluated_stack, Stack)
assert evaluated_stack.__eq__(self.stack)


class TestStackSceptreUserData(object):
def test_user_data_is_accessible(self):
"""
.sceptre_user_data is a property. Let's make sure it accesses the right
data.
"""
stack = stack_factory(sceptre_user_data={'test_key': sentinel.test_value})
assert stack.sceptre_user_data['test_key'] is sentinel.test_value

def test_user_data_gets_resolved(self):
class TestResolver(Resolver):
def setup(self):
pass

def resolve(self):
return sentinel.resolved_value

stack = stack_factory(sceptre_user_data={'test_key': TestResolver()})
assert stack.sceptre_user_data['test_key'] is sentinel.resolved_value

def test_recursive_user_data_gets_resolved(self):
"""
.sceptre_user_data can have resolvers that refer to .sceptre_user_data itself.
Those must be instantiated before the attribute can be used.
"""
class TestResolver(Resolver):
def setup(self):
pass

def resolve(self):
return self.stack.sceptre_user_data['primitive']

stack = stack_factory()
stack._sceptre_user_data = {
'primitive': sentinel.primitive_value,
'resolved': TestResolver(stack=stack),
}
assert stack.sceptre_user_data['resolved'] == sentinel.primitive_value

0 comments on commit 35b3a92

Please sign in to comment.