from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from builtins import str
from builtins import object
import logging
from operator import attrgetter
from .base import plan, build_walker
from . import build
from .. import exceptions
from ..status import (
NotSubmittedStatus,
NotUpdatedStatus,
COMPLETE,
INTERRUPTED,
)
logger = logging.getLogger(__name__)
[docs]class DictValue(object):
ADDED = "ADDED"
REMOVED = "REMOVED"
MODIFIED = "MODIFIED"
UNMODIFIED = "UNMODIFIED"
formatter = "%s%s = %s"
def __init__(self, key, old_value, new_value):
self.key = key
self.old_value = old_value
self.new_value = new_value
def __eq__(self, other):
return self.__dict__ == other.__dict__
[docs] def changes(self):
"""Returns a list of changes to represent the diff between
old and new value.
Returns:
list: [string] representation of the change (if any)
between old and new value
"""
output = []
if self.status() is self.UNMODIFIED:
output = [self.formatter % (' ', self.key, self.old_value)]
elif self.status() is self.ADDED:
output.append(self.formatter % ('+', self.key, self.new_value))
elif self.status() is self.REMOVED:
output.append(self.formatter % ('-', self.key, self.old_value))
elif self.status() is self.MODIFIED:
output.append(self.formatter % ('-', self.key, self.old_value))
output.append(self.formatter % ('+', self.key, self.new_value))
return output
[docs] def status(self):
if self.old_value == self.new_value:
return self.UNMODIFIED
elif self.old_value is None:
return self.ADDED
elif self.new_value is None:
return self.REMOVED
else:
return self.MODIFIED
[docs]def diff_dictionaries(old_dict, new_dict):
"""Diffs two single dimension dictionaries
Returns the number of changes and an unordered list
expressing the common entries and changes.
Args:
old_dict(dict): old dictionary
new_dict(dict): new dictionary
Returns: list()
int: number of changed records
list: [DictValue]
"""
old_set = set(old_dict)
new_set = set(new_dict)
added_set = new_set - old_set
removed_set = old_set - new_set
common_set = old_set & new_set
changes = 0
output = []
for key in added_set:
changes += 1
output.append(DictValue(key, None, new_dict[key]))
for key in removed_set:
changes += 1
output.append(DictValue(key, old_dict[key], None))
for key in common_set:
output.append(DictValue(key, old_dict[key], new_dict[key]))
if str(old_dict[key]) != str(new_dict[key]):
changes += 1
output.sort(key=attrgetter("key"))
return [changes, output]
[docs]def diff_parameters(old_params, new_params):
"""Compares the old vs. new parameters and returns a "diff"
If there are no changes, we return an empty list.
Args:
old_params(dict): old paramters
new_params(dict): new parameters
Returns:
list: A list of differences
"""
[changes, diff] = diff_dictionaries(old_params, new_params)
if changes == 0:
return []
return diff
[docs]class Action(build.Action):
""" Responsible for diff'ing CF stacks in AWS and on disk
Generates the build plan based on stack dependencies (these dependencies
are determined automatically based on references to output values from
other stacks).
The plan is then used to create a changeset for a stack using a
generated template based on the current config.
"""
def _diff_stack(self, stack, **kwargs):
"""Handles the diffing a stack in CloudFormation vs our config"""
if self.cancel.wait(0):
return INTERRUPTED
if not build.should_submit(stack):
return NotSubmittedStatus()
provider = self.build_provider(stack)
if not build.should_update(stack):
stack.set_outputs(provider.get_outputs(stack.fqn))
return NotUpdatedStatus()
tags = build.build_stack_tags(stack)
stack.resolve(self.context, provider)
parameters = self.build_parameters(stack)
try:
outputs = provider.get_stack_changes(
stack, self._template(stack.blueprint), parameters, tags
)
stack.set_outputs(outputs)
except exceptions.StackDidNotChange:
logger.info('No changes: %s', stack.fqn)
stack.set_outputs(provider.get_outputs(stack.fqn))
return COMPLETE
def _generate_plan(self):
return plan(
description="Diff stacks",
stack_action=self._diff_stack,
context=self.context)
[docs] def run(self, concurrency=0, *args, **kwargs):
plan = self._generate_plan()
plan.outline(logging.DEBUG)
if plan.keys():
logger.info("Diffing stacks: %s", ", ".join(plan.keys()))
else:
logger.warn('WARNING: No stacks detected (error in config?)')
walker = build_walker(concurrency)
plan.execute(walker)
"""Don't ever do anything for pre_run or post_run"""
[docs] def pre_run(self, *args, **kwargs):
pass
[docs] def post_run(self, *args, **kwargs):
pass