-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstack_diff.py
More file actions
134 lines (106 loc) · 3.81 KB
/
stack_diff.py
File metadata and controls
134 lines (106 loc) · 3.81 KB
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
from functools import cached_property
from sys import stdout
from typing import IO, Optional
from ansiscape import green, heavy, yellow
from boto3.session import Session
from differently import render
from tabulate import tabulate
class StackDiff:
"""
Visualises the changes described by an Amazon Web Services CloudFormation
change set.
Arguments:
change: ARN, ID or name of the CloudFormation change set to visualise
session: boto3 session (defaults to a new session)
stack: ARN, ID or name of the change set's CloudFormation stack
"""
def __init__(
self,
change: str,
stack: str,
session: Optional[Session] = None,
) -> None:
session = session or Session()
self.change = change
self.client = session.client(
"cloudformation"
) # pyright: reportUnknownMemberType=false
self.stack = stack
def _make_action(self, action: str, replacement: str) -> str:
if action != "Modify":
return action
rl = replacement.lower()
if rl == "true":
return "Replace 🔥"
if rl == "false":
return "Update"
return "Conditionally 🔥"
@cached_property
def change_template(self) -> str:
"""Gets the change set's proposed template."""
response = self.client.get_template(
ChangeSetName=self.change,
StackName=self.stack,
TemplateStage="Original",
)
return response.get("TemplateBody", "")
def render_changes(self, writer: Optional[IO[str]] = None) -> None:
"""
Renders a visualisation of the changes that CloudFormation would apply
if the change set was executed.
Arguments:
writer: Writer (defaults to ``stdout``)
"""
response = self.client.describe_change_set(
ChangeSetName=self.change,
StackName=self.stack,
)
rows = [
[
heavy("Logical ID").encoded,
heavy("Physical ID").encoded,
heavy("Resource Type").encoded,
heavy("Action").encoded,
]
]
for change in response["Changes"]:
rc = change.get("ResourceChange", None)
if not rc:
continue
if rc["Action"] == "Add":
color = green
else:
color = yellow
action = self._make_action(
action=rc["Action"],
replacement=rc.get("Replacement", "False"),
)
# PhysicalResourceId is not present for additions:
physical_id = rc.get("PhysicalResourceId", "")
fmt_physical_id = color(physical_id).encoded if physical_id else ""
rows.append(
[
color(rc["LogicalResourceId"]).encoded,
fmt_physical_id,
color(rc["ResourceType"]).encoded,
color(action).encoded,
]
)
t = tabulate(rows, headers="firstrow", tablefmt="plain")
(writer or stdout).write(t + "\n")
def render_differences(self, writer: Optional[IO[str]] = None) -> None:
"""
Renders a visualisation of the differences between the stack's current
template and the change set's proposed template.
Arguments:
writer: Writer (defaults to ``stdout``)
"""
render(self.stack_template, self.change_template, writer or stdout)
@cached_property
def stack_template(self) -> str:
"""Gets the stack's current template."""
response = self.client.get_template(
StackName=self.stack,
TemplateStage="Original",
)
return response.get("TemplateBody", "")