-
Notifications
You must be signed in to change notification settings - Fork 61
/
diff.py
216 lines (167 loc) · 6.75 KB
/
diff.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""CFNgin diff action."""
import logging
from operator import attrgetter
from .. import exceptions
from ..status import (COMPLETE, INTERRUPTED, NotSubmittedStatus,
NotUpdatedStatus, SkippedStatus)
from ..status import StackDoesNotExist as StackDoesNotExistStatus
from . import build
from .base import build_walker
LOGGER = logging.getLogger(__name__)
class DictValue(object):
"""Used to create a diff of two dictionaries."""
ADDED = "ADDED"
REMOVED = "REMOVED"
MODIFIED = "MODIFIED"
UNMODIFIED = "UNMODIFIED"
formatter = "%s%s = %s"
def __init__(self, key, old_value, new_value):
"""Instantiate class."""
self.key = key
self.old_value = old_value
self.new_value = new_value
def __eq__(self, other):
"""Compare if self is equal to another object."""
return self.__dict__ == other.__dict__
def changes(self):
"""Return changes to represent the diff between old and new value.
Returns:
List[str]: 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
def status(self):
"""Status of changes between the old value and new value."""
if self.old_value == self.new_value:
return self.UNMODIFIED
if self.old_value is None:
return self.ADDED
if self.new_value is None:
return self.REMOVED
return self.MODIFIED
def diff_dictionaries(old_dict, new_dict):
"""Calculate the diff two single dimension dictionaries.
Args:
old_dict(Dict[Any, Any]): Old dictionary.
new_dict(Dict[Any, Any]): New dictionary.
Returns:
Tuple[int, List[:class:`DictValue`]]: Number of changed records and
the :class:`DictValue` object containing the changes.
"""
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
def format_params_diff(parameter_diff):
"""Handle the formatting of differences in parameters.
Args:
parameter_diff (List[:class:`DictValue`]): A list of
:class:`DictValue` detailing the differences between two dicts
returned by :func:`diff_dictionaries`.
Returns:
str: A formatted string that represents a parameter diff
"""
params_output = '\n'.join([line for v in parameter_diff
for line in v.changes()])
return """--- Old Parameters
+++ New Parameters
******************
%s\n""" % params_output
def diff_parameters(old_params, new_params):
"""Compare the old vs. new parameters and returns a "diff".
If there are no changes, we return an empty list.
Args:
old_params(Dict[Any, Any]): old paramters
new_params(Dict[Any, Any]): new parameters
Returns:
List[:class:`DictValue`]: A list of differences.
"""
changes, diff = diff_dictionaries(old_params, new_params)
if changes == 0:
return []
return diff
class Action(build.Action):
"""Responsible for diffing CloudFormation stacks in AWS and locally.
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.
"""
DESCRIPTION = 'Diff stacks'
@property
def _stack_action(self):
"""Run against a step."""
return self._diff_stack
def _diff_stack(self, stack, **_kwargs): # pylint: disable=too-many-return-statements
"""Handle 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)
try:
stack.resolve(self.context, provider)
parameters = self.build_parameters(stack)
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))
except exceptions.StackDoesNotExist:
if self.context.persistent_graph:
return SkippedStatus('persistent graph: stack does not '
'exist, will be removed')
return StackDoesNotExistStatus()
except AttributeError as err:
if (self.context.persistent_graph and
'defined class or template path' in str(err)):
return SkippedStatus('persistent graph: will be destroyed')
raise
return COMPLETE
def run(self, **kwargs):
"""Kicks off the diffing of the stacks in the stack_definitions."""
plan = self._generate_plan(require_unlocked=False,
include_persistent_graph=True)
plan.outline(logging.DEBUG)
if plan.keys():
LOGGER.info("Diffing stacks: %s", ", ".join(plan.keys()))
else:
LOGGER.warning('WARNING: No stacks detected (error in config?)')
walker = build_walker(kwargs.get('concurrency', 0))
plan.execute(walker)
def pre_run(self, **kwargs):
"""Do nothing."""
def post_run(self, **kwargs):
"""Do nothing."""