Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/unit/customizations/cloudformation/test_deployer.py
1569 views
1
import botocore.session
2
3
from botocore.stub import Stubber
4
from awscli.testutils import mock, unittest
5
from awscli.customizations.cloudformation.deployer import Deployer, ChangeSetResult
6
from awscli.customizations.cloudformation import exceptions
7
8
9
class TestDeployer(unittest.TestCase):
10
11
def setUp(self):
12
client = botocore.session.get_session().create_client('cloudformation',
13
region_name="us-east-1")
14
self.stub_client = Stubber(client)
15
16
self.changeset_prefix = "some-changeset-prefix"
17
self.deployer = Deployer(client, self.changeset_prefix)
18
19
def test_has_stack_success(self):
20
stack_name = "stack_name"
21
22
expected_params = {
23
"StackName": stack_name
24
}
25
26
response = {
27
"Stacks": [
28
make_stack_obj(stack_name)
29
]
30
}
31
32
self.stub_client.add_response('describe_stacks', response,
33
expected_params)
34
35
with self.stub_client:
36
response = self.deployer.has_stack(stack_name)
37
self.assertTrue(response)
38
39
def test_has_stack_no_stack(self):
40
stack_name = "stack_name"
41
expected_params = {
42
"StackName": stack_name
43
}
44
45
# Response contains NO stack
46
no_stack_response = {
47
"Stacks": []
48
}
49
self.stub_client.add_response('describe_stacks', no_stack_response,
50
expected_params)
51
with self.stub_client:
52
response = self.deployer.has_stack(stack_name)
53
self.assertFalse(response)
54
55
# Response is a ClientError with a message that the stack does not exist
56
self.stub_client.add_client_error('describe_stacks', "ClientError",
57
"Stack with id {0} does not exist"
58
.format(stack_name))
59
with self.stub_client:
60
response = self.deployer.has_stack(stack_name)
61
self.assertFalse(response)
62
63
def test_has_stack_review_in_progress(self):
64
stack_name = "stack_name"
65
expected_params = {
66
"StackName": stack_name
67
}
68
69
# Response contains NO stack
70
review_in_progress_response = {
71
"Stacks": [make_stack_obj(stack_name, "REVIEW_IN_PROGRESS")]
72
}
73
self.stub_client.add_response('describe_stacks',
74
review_in_progress_response,
75
expected_params)
76
with self.stub_client:
77
response = self.deployer.has_stack(stack_name)
78
self.assertFalse(response)
79
80
def test_has_stack_exception(self):
81
self.stub_client.add_client_error('describe_stacks', "ValidationError",
82
"Service is bad")
83
with self.stub_client:
84
with self.assertRaises(botocore.exceptions.ClientError):
85
self.deployer.has_stack("stack_name")
86
87
def test_create_changeset_success(self):
88
stack_name = "stack_name"
89
template = "template"
90
parameters = [
91
{"ParameterKey": "Key1", "ParameterValue": "Value"},
92
{"ParameterKey": "Key2", "UsePreviousValue": True},
93
{"ParameterKey": "Key3", "UsePreviousValue": False},
94
]
95
# Parameters that Use Previous Value will be removed on stack creation
96
# to either force CloudFormation to use the Default value, or ask user to specify a parameter
97
filtered_parameters = [
98
{"ParameterKey": "Key1", "ParameterValue": "Value"},
99
{"ParameterKey": "Key3", "UsePreviousValue": False},
100
]
101
capabilities = ["capabilities"]
102
role_arn = "arn:aws:iam::1234567890:role"
103
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
104
s3_uploader = None
105
106
tags = [{"Key":"key1", "Value": "val1"}]
107
108
# Case 1: Stack DOES NOT exist
109
self.deployer.has_stack = mock.Mock()
110
self.deployer.has_stack.return_value = False
111
112
expected_params = {
113
"ChangeSetName": botocore.stub.ANY,
114
"StackName": stack_name,
115
"TemplateBody": template,
116
"ChangeSetType": "CREATE",
117
"Parameters": filtered_parameters,
118
"Capabilities": capabilities,
119
"Description": botocore.stub.ANY,
120
"RoleARN": role_arn,
121
"NotificationARNs": notification_arns,
122
"Tags": tags
123
}
124
125
response = {
126
"Id": "changeset ID"
127
}
128
129
self.stub_client.add_response("create_change_set", response,
130
expected_params)
131
with self.stub_client:
132
result = self.deployer.create_changeset(
133
stack_name, template, parameters, capabilities, role_arn,
134
notification_arns, s3_uploader, tags)
135
self.assertEqual(response["Id"], result.changeset_id)
136
self.assertEqual("CREATE", result.changeset_type)
137
138
# Case 2: Stack exists. We are updating it
139
self.deployer.has_stack.return_value = True
140
self.stub_client.add_response("get_template_summary",
141
{"Parameters": [{"ParameterKey": parameter["ParameterKey"]}
142
for parameter in parameters]},
143
{"StackName": stack_name})
144
expected_params["ChangeSetType"] = "UPDATE"
145
expected_params["Parameters"] = parameters
146
self.stub_client.add_response("create_change_set", response,
147
expected_params)
148
# template has new parameter but should not be included in
149
# expected_params as no previous value
150
parameters = list(parameters) + \
151
[{"ParameterKey": "New", "UsePreviousValue": True}]
152
with self.stub_client:
153
result = self.deployer.create_changeset(
154
stack_name, template, parameters, capabilities, role_arn,
155
notification_arns, s3_uploader, tags)
156
self.assertEqual(response["Id"], result.changeset_id)
157
self.assertEqual("UPDATE", result.changeset_type)
158
159
def test_create_changeset_success_s3_bucket(self):
160
stack_name = "stack_name"
161
template = "template"
162
template_url = "https://s3.amazonaws.com/bucket/file"
163
parameters = [
164
{"ParameterKey": "Key1", "ParameterValue": "Value"},
165
{"ParameterKey": "Key2", "UsePreviousValue": True},
166
{"ParameterKey": "Key3", "UsePreviousValue": False},
167
]
168
# Parameters that Use Previous Value will be removed on stack creation
169
# to either force CloudFormation to use the Default value, or ask user to specify a parameter
170
filtered_parameters = [
171
{"ParameterKey": "Key1", "ParameterValue": "Value"},
172
{"ParameterKey": "Key3", "UsePreviousValue": False},
173
]
174
capabilities = ["capabilities"]
175
role_arn = "arn:aws:iam::1234567890:role"
176
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
177
178
s3_uploader = mock.Mock()
179
def to_path_style_s3_url(some_string, Version=None):
180
return "https://s3.amazonaws.com/bucket/file"
181
s3_uploader.to_path_style_s3_url = to_path_style_s3_url
182
def upload_with_dedup(filename,extension):
183
return "s3://bucket/file"
184
s3_uploader.upload_with_dedup = upload_with_dedup
185
186
# Case 1: Stack DOES NOT exist
187
self.deployer.has_stack = mock.Mock()
188
self.deployer.has_stack.return_value = False
189
190
expected_params = {
191
"ChangeSetName": botocore.stub.ANY,
192
"StackName": stack_name,
193
"TemplateURL": template_url,
194
"ChangeSetType": "CREATE",
195
"Parameters": filtered_parameters,
196
"Capabilities": capabilities,
197
"Description": botocore.stub.ANY,
198
"RoleARN": role_arn,
199
"Tags": [],
200
"NotificationARNs": notification_arns
201
}
202
203
response = {
204
"Id": "changeset ID"
205
}
206
207
self.stub_client.add_response("create_change_set", response,
208
expected_params)
209
with self.stub_client:
210
result = self.deployer.create_changeset(
211
stack_name, template, parameters, capabilities, role_arn,
212
notification_arns, s3_uploader, [])
213
self.assertEqual(response["Id"], result.changeset_id)
214
self.assertEqual("CREATE", result.changeset_type)
215
216
# Case 2: Stack exists. We are updating it
217
self.deployer.has_stack.return_value = True
218
self.stub_client.add_response("get_template_summary",
219
{"Parameters": [{"ParameterKey": parameter["ParameterKey"]}
220
for parameter in parameters]},
221
{"StackName": stack_name})
222
expected_params["ChangeSetType"] = "UPDATE"
223
expected_params["Parameters"] = parameters
224
# template has new parameter but should not be included in
225
# expected_params as no previous value
226
parameters = list(parameters) + \
227
[{"ParameterKey": "New", "UsePreviousValue": True}]
228
self.stub_client.add_response("create_change_set", response,
229
expected_params)
230
with self.stub_client:
231
result = self.deployer.create_changeset(
232
stack_name, template, parameters, capabilities, role_arn,
233
notification_arns, s3_uploader, [])
234
self.assertEqual(response["Id"], result.changeset_id)
235
self.assertEqual("UPDATE", result.changeset_type)
236
237
def test_create_changeset_exception(self):
238
stack_name = "stack_name"
239
template = "template"
240
parameters = [{"ParameterKey": "Key1", "ParameterValue": "Value",
241
"UsePreviousValue": True}]
242
capabilities = ["capabilities"]
243
role_arn = "arn:aws:iam::1234567890:role"
244
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
245
s3_uploader = None
246
tags = [{"Key":"key1", "Value": "val1"}]
247
248
self.deployer.has_stack = mock.Mock()
249
self.deployer.has_stack.return_value = False
250
251
self.stub_client.add_client_error(
252
'create_change_set', "Somethign is wrong", "Service is bad")
253
with self.stub_client:
254
with self.assertRaises(botocore.exceptions.ClientError):
255
self.deployer.create_changeset(stack_name, template, parameters,
256
capabilities, role_arn, notification_arns, None, tags)
257
258
def test_execute_changeset(self):
259
stack_name = "stack_name"
260
changeset_id = "changeset_id"
261
262
expected_params = {
263
"ChangeSetName": changeset_id,
264
"StackName": stack_name,
265
"DisableRollback": False
266
}
267
268
self.stub_client.add_response("execute_change_set", {}, expected_params)
269
with self.stub_client:
270
self.deployer.execute_changeset(changeset_id, stack_name)
271
272
def test_execute_changeset_disable_rollback(self):
273
stack_name = "stack_name"
274
changeset_id = "changeset_id"
275
disable_rollback = True
276
277
expected_params = {
278
"ChangeSetName": changeset_id,
279
"StackName": stack_name,
280
"DisableRollback": disable_rollback
281
}
282
283
self.stub_client.add_response("execute_change_set", {}, expected_params)
284
with self.stub_client:
285
self.deployer.execute_changeset(changeset_id, stack_name,
286
disable_rollback)
287
288
def test_execute_changeset_exception(self):
289
stack_name = "stack_name"
290
changeset_id = "changeset_id"
291
292
self.stub_client.add_client_error(
293
'execute_change_set', "Somethign is wrong", "Service is bad")
294
with self.stub_client:
295
with self.assertRaises(botocore.exceptions.ClientError):
296
self.deployer.execute_changeset(changeset_id, stack_name)
297
298
def test_create_and_wait_for_changeset_successful(self):
299
stack_name = "stack_name"
300
template = "template"
301
parameters = [{"ParameterKey": "Key1", "ParameterValue": "Value",
302
"UsePreviousValue": True}]
303
capabilities = ["capabilities"]
304
changeset_id = "changeset id"
305
changeset_type = "changeset type"
306
role_arn = "arn:aws:iam::1234567890:role"
307
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
308
s3_uploader = None
309
tags = [{"Key":"key1", "Value": "val1"}]
310
311
self.deployer.create_changeset = mock.Mock()
312
self.deployer.create_changeset.return_value = ChangeSetResult(changeset_id, changeset_type)
313
314
self.deployer.wait_for_changeset = mock.Mock()
315
316
result = self.deployer.create_and_wait_for_changeset(
317
stack_name, template, parameters, capabilities, role_arn,
318
notification_arns, s3_uploader, tags)
319
self.assertEqual(result.changeset_id, changeset_id)
320
self.assertEqual(result.changeset_type, changeset_type)
321
322
def test_create_and_wait_for_changeset_error_waiting_for_changeset(self):
323
stack_name = "stack_name"
324
template = "template"
325
parameters = [{"ParameterKey": "Key1", "ParameterValue": "Value",
326
"UsePreviousValue": True}]
327
capabilities = ["capabilities"]
328
changeset_id = "changeset id"
329
changeset_type = "changeset type"
330
role_arn = "arn:aws:iam::1234567890:role"
331
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
332
s3_uploader = None
333
tags = [{"Key":"key1", "Value": "val1"}]
334
335
self.deployer.create_changeset = mock.Mock()
336
self.deployer.create_changeset.return_value = ChangeSetResult(changeset_id, changeset_type)
337
338
self.deployer.wait_for_changeset = mock.Mock()
339
self.deployer.wait_for_changeset.side_effect = RuntimeError
340
341
with self.assertRaises(RuntimeError):
342
result = self.deployer.create_and_wait_for_changeset(
343
stack_name, template, parameters, capabilities, role_arn,
344
notification_arns, s3_uploader, tags)
345
346
def test_wait_for_changeset_no_changes(self):
347
stack_name = "stack_name"
348
changeset_id = "changeset-id"
349
350
mock_client = mock.Mock()
351
mock_deployer = Deployer(mock_client)
352
mock_waiter = mock.Mock()
353
mock_client.get_waiter.return_value = mock_waiter
354
355
response = {
356
"Status": "FAILED",
357
"StatusReason": "The submitted information didn't contain changes."
358
}
359
360
waiter_error = botocore.exceptions.WaiterError(name="name",
361
reason="reason",
362
last_response=response)
363
mock_waiter.wait.side_effect = waiter_error
364
365
with self.assertRaises(exceptions.ChangeEmptyError):
366
mock_deployer.wait_for_changeset(changeset_id, stack_name)
367
368
waiter_config = {'Delay': 5}
369
mock_waiter.wait.assert_called_once_with(ChangeSetName=changeset_id,
370
StackName=stack_name,
371
WaiterConfig=waiter_config)
372
373
mock_client.get_waiter.assert_called_once_with(
374
"change_set_create_complete")
375
376
def test_wait_for_changeset_no_changes_with_another_error_msg(self):
377
stack_name = "stack_name"
378
changeset_id = "changeset-id"
379
380
mock_client = mock.Mock()
381
mock_deployer = Deployer(mock_client)
382
mock_waiter = mock.Mock()
383
mock_client.get_waiter.return_value = mock_waiter
384
385
response = {
386
"Status": "FAILED",
387
"StatusReason": "No updates are to be performed"
388
}
389
390
waiter_error = botocore.exceptions.WaiterError(name="name",
391
reason="reason",
392
last_response=response)
393
mock_waiter.wait.side_effect = waiter_error
394
395
with self.assertRaises(exceptions.ChangeEmptyError):
396
mock_deployer.wait_for_changeset(changeset_id, stack_name)
397
398
waiter_config = {'Delay': 5}
399
mock_waiter.wait.assert_called_once_with(ChangeSetName=changeset_id,
400
StackName=stack_name,
401
WaiterConfig=waiter_config)
402
403
mock_client.get_waiter.assert_called_once_with(
404
"change_set_create_complete")
405
406
def test_wait_for_changeset_failed_to_create_changeset(self):
407
stack_name = "stack_name"
408
changeset_id = "changeset-id"
409
410
mock_client = mock.Mock()
411
mock_deployer = Deployer(mock_client)
412
mock_waiter = mock.Mock()
413
mock_client.get_waiter.return_value = mock_waiter
414
415
response = {
416
"Status": "FAILED",
417
"StatusReason": "some reason"
418
}
419
420
waiter_error = botocore.exceptions.WaiterError(name="name",
421
reason="reason",
422
last_response=response)
423
mock_waiter.wait.side_effect = waiter_error
424
425
with self.assertRaises(RuntimeError):
426
mock_deployer.wait_for_changeset(changeset_id, stack_name)
427
428
waiter_config = {'Delay': 5}
429
mock_waiter.wait.assert_called_once_with(ChangeSetName=changeset_id,
430
StackName=stack_name,
431
WaiterConfig=waiter_config)
432
433
mock_client.get_waiter.assert_called_once_with(
434
"change_set_create_complete")
435
436
def test_wait_for_execute_no_changes(self):
437
stack_name = "stack_name"
438
changeset_type = "CREATE"
439
440
mock_client = mock.Mock()
441
mock_deployer = Deployer(mock_client)
442
mock_waiter = mock.Mock()
443
mock_client.get_waiter.return_value = mock_waiter
444
445
waiter_error = botocore.exceptions.WaiterError(name="name",
446
reason="reason",
447
last_response={})
448
mock_waiter.wait.side_effect = waiter_error
449
450
with self.assertRaises(exceptions.DeployFailedError):
451
mock_deployer.wait_for_execute(stack_name, changeset_type)
452
453
waiter_config = {
454
'Delay': 30,
455
'MaxAttempts': 120,
456
}
457
mock_waiter.wait.assert_called_once_with(StackName=stack_name,
458
WaiterConfig=waiter_config)
459
460
mock_client.get_waiter.assert_called_once_with(
461
"stack_create_complete")
462
463
464
def make_stack_obj(stack_name, status="CREATE_COMPLETE"):
465
return {
466
"StackId": stack_name,
467
"StackName": stack_name,
468
"CreationTime": "2013-08-23T01:02:15.422Z",
469
"StackStatus": status
470
}
471
472