Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/unit/customizations/cloudformation/test_deploy.py
2624 views
1
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0e
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
import tempfile
14
import collections
15
16
from awscli.testutils import mock, unittest
17
from awscli.customizations.cloudformation.deploy import DeployCommand
18
from awscli.customizations.cloudformation.deployer import Deployer
19
from awscli.customizations.cloudformation.yamlhelper import yaml_parse
20
from awscli.customizations.cloudformation import exceptions
21
22
23
class FakeArgs(object):
24
def __init__(self, **kwargs):
25
self.__dict__.update(kwargs)
26
27
def __contains__(self, key):
28
return key in self.__dict__
29
30
31
def get_example_template():
32
return {
33
"Parameters": {
34
"Key1": "Value1"
35
},
36
"Resources": {
37
"Resource1": {}
38
}
39
}
40
41
ChangeSetResult = collections.namedtuple("ChangeSetResult", ["changeset_id", "changeset_type"])
42
43
class TestDeployCommand(unittest.TestCase):
44
45
def setUp(self):
46
self.session = mock.Mock()
47
self.session.get_scoped_config.return_value = {}
48
self.parsed_args = FakeArgs(template_file='./foo',
49
stack_name="some_stack_name",
50
parameter_overrides=["Key1=Value1",
51
"Key2=Value2"],
52
no_execute_changeset=False,
53
execute_changeset=True,
54
disable_rollback=True,
55
capabilities=None,
56
role_arn=None,
57
notification_arns=[],
58
fail_on_empty_changeset=True,
59
s3_bucket=None,
60
s3_prefix="some prefix",
61
kms_key_id="some kms key id",
62
force_upload=True,
63
tags=["tagkey1=tagvalue1"])
64
self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None,
65
verify_ssl=None)
66
self.deploy_command = DeployCommand(self.session)
67
68
self.deployer = Deployer(mock.Mock())
69
self.deployer.create_and_wait_for_changeset = mock.Mock()
70
self.deployer.execute_changeset = mock.Mock()
71
self.deployer.wait_for_execute = mock.Mock()
72
73
@mock.patch("awscli.customizations.cloudformation.deploy.yaml_parse")
74
def test_command_invoked(self, mock_yaml_parse):
75
"""
76
Tests that deploy method is invoked when command is run
77
"""
78
fake_parameter_overrides = []
79
fake_tags_dict = {"tagkey1": "tagvalue1"}
80
fake_tags = [{"Key": "tagkey1", "Value": "tagvalue1"}]
81
fake_parameters = "some return value"
82
template_str = "some template"
83
84
with tempfile.NamedTemporaryFile() as handle:
85
file_path = handle.name
86
87
open_mock = mock.mock_open()
88
# Patch the file open method to return template string
89
with mock.patch(
90
"awscli.customizations.cloudformation.deploy.open",
91
open_mock(read_data=template_str)) as open_mock:
92
93
fake_template = get_example_template()
94
mock_yaml_parse.return_value = fake_template
95
96
self.deploy_command.deploy = mock.MagicMock()
97
self.deploy_command.deploy.return_value = 0
98
self.deploy_command.parse_key_value_arg = mock.Mock()
99
self.deploy_command.parse_key_value_arg.side_effect = [
100
fake_parameter_overrides, fake_tags_dict]
101
self.deploy_command.merge_parameters = mock.MagicMock(
102
return_value=fake_parameters)
103
104
self.parsed_args.template_file = file_path
105
result = self.deploy_command._run_main(self.parsed_args,
106
parsed_globals=self.parsed_globals)
107
self.assertEqual(0, result)
108
109
open_mock.assert_called_once_with(file_path, "r")
110
111
self.deploy_command.deploy.assert_called_once_with(
112
mock.ANY,
113
'some_stack_name',
114
mock.ANY,
115
fake_parameters,
116
None,
117
not self.parsed_args.no_execute_changeset,
118
None,
119
[],
120
None,
121
fake_tags,
122
True,
123
True,
124
False
125
)
126
127
self.deploy_command.parse_key_value_arg.assert_has_calls([
128
mock.call(
129
self.parsed_args.parameter_overrides,
130
"parameter-overrides"
131
),
132
mock.call(
133
self.parsed_args.tags,
134
"tags"
135
)
136
])
137
138
self.deploy_command.merge_parameters.assert_called_once_with(
139
fake_template, fake_parameter_overrides)
140
141
self.assertEqual(1, mock_yaml_parse.call_count)
142
143
def test_invalid_template_file(self):
144
self.parsed_args.template_file = "sometemplate"
145
with self.assertRaises(exceptions.InvalidTemplatePathError):
146
result = self.deploy_command._run_main(self.parsed_args,
147
parsed_globals=self.parsed_globals)
148
149
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.isfile')
150
@mock.patch('awscli.customizations.cloudformation.deploy.yaml_parse')
151
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.getsize')
152
def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_parse, mock_isfile):
153
"""
154
Tests that large templates are detected prior to deployment
155
"""
156
template_str = get_example_template()
157
158
mock_getsize.return_value = 51201
159
mock_isfile.return_value = True
160
mock_yaml_parse.return_value = template_str
161
open_mock = mock.mock_open()
162
163
with mock.patch(
164
"awscli.customizations.cloudformation.deploy.open",
165
open_mock(read_data=template_str)) as open_mock:
166
with self.assertRaises(exceptions.DeployBucketRequiredError):
167
result = self.deploy_command._run_main(self.parsed_args,
168
parsed_globals=self.parsed_globals)
169
170
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.isfile')
171
@mock.patch('awscli.customizations.cloudformation.deploy.yaml_parse')
172
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.getsize')
173
@mock.patch('awscli.customizations.cloudformation.deploy.DeployCommand.deploy')
174
@mock.patch('awscli.customizations.cloudformation.deploy.S3Uploader')
175
def test_s3_uploader_is_configured_properly(self, s3UploaderMock,
176
deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile):
177
"""
178
Tests that large templates are detected prior to deployment
179
"""
180
bucket_name = "mybucket"
181
template_str = get_example_template()
182
183
mock_getsize.return_value = 1024
184
mock_isfile.return_value = True
185
mock_yaml_parse.return_value = template_str
186
open_mock = mock.mock_open()
187
188
with mock.patch(
189
"awscli.customizations.cloudformation.deploy.open",
190
open_mock(read_data=template_str)) as open_mock:
191
192
self.parsed_args.s3_bucket = bucket_name
193
s3UploaderObject = mock.Mock()
194
s3UploaderMock.return_value = s3UploaderObject
195
196
result = self.deploy_command._run_main(self.parsed_args,
197
parsed_globals=self.parsed_globals)
198
199
self.deploy_command.deploy.assert_called_once_with(
200
mock.ANY,
201
self.parsed_args.stack_name,
202
mock.ANY,
203
mock.ANY,
204
None,
205
not self.parsed_args.no_execute_changeset,
206
None,
207
[],
208
s3UploaderObject,
209
[{"Key": "tagkey1", "Value": "tagvalue1"}],
210
True,
211
True,
212
False
213
)
214
215
s3UploaderMock.assert_called_once_with(mock.ANY,
216
bucket_name,
217
self.parsed_args.s3_prefix,
218
self.parsed_args.kms_key_id,
219
self.parsed_args.force_upload)
220
221
def test_deploy_success(self):
222
"""
223
Tests that we call the deploy command
224
"""
225
226
stack_name = "stack_name"
227
changeset_id = "some changeset"
228
parameters = ["a", "b"]
229
template = "cloudformation template"
230
capabilities = ["foo", "bar"]
231
execute_changeset = True
232
changeset_type = "CREATE"
233
role_arn = "arn:aws:iam::1234567890:role"
234
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
235
s3_uploader = None
236
tags = [{"Key":"key1", "Value": "val1"}]
237
238
# Set the mock to return this fake changeset_id
239
self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type)
240
241
rc = self.deploy_command.deploy(self.deployer,
242
stack_name,
243
template,
244
parameters,
245
capabilities,
246
execute_changeset,
247
role_arn,
248
notification_arns,
249
s3_uploader,
250
tags)
251
self.assertEqual(rc, 0)
252
253
254
self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,
255
cfn_template=template,
256
parameter_values=parameters,
257
capabilities=capabilities,
258
role_arn=role_arn,
259
notification_arns=notification_arns,
260
s3_uploader=s3_uploader,
261
tags=tags)
262
263
# since execute_changeset is set to True, deploy() will execute changeset
264
self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name, False)
265
self.deployer.wait_for_execute.assert_called_once_with(stack_name, changeset_type)
266
267
268
def test_deploy_no_execute(self):
269
stack_name = "stack_name"
270
changeset_id = "some changeset"
271
parameters = ["a", "b"]
272
template = "cloudformation template"
273
capabilities = ["foo", "bar"]
274
execute_changeset = False
275
role_arn = "arn:aws:iam::1234567890:role"
276
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
277
s3_uploader = None
278
tags = [{"Key":"key1", "Value": "val1"}]
279
280
281
self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE")
282
rc = self.deploy_command.deploy(self.deployer,
283
stack_name,
284
template,
285
parameters,
286
capabilities,
287
execute_changeset,
288
role_arn,
289
notification_arns,
290
s3_uploader,
291
tags)
292
self.assertEqual(rc, 0)
293
294
self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,
295
cfn_template=template,
296
parameter_values=parameters,
297
capabilities=capabilities,
298
role_arn=role_arn,
299
notification_arns=notification_arns,
300
s3_uploader=s3_uploader,
301
tags=tags)
302
303
# since execute_changeset is set to True, deploy() will execute changeset
304
self.deployer.execute_changeset.assert_not_called()
305
self.deployer.wait_for_execute.assert_not_called()
306
307
def test_deploy_raise_exception(self):
308
stack_name = "stack_name"
309
changeset_id = "some changeset"
310
parameters = ["a", "b"]
311
template = "cloudformation template"
312
capabilities = ["foo", "bar"]
313
execute_changeset = True
314
role_arn = "arn:aws:iam::1234567890:role"
315
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
316
s3_uploader = None
317
tags = [{"Key":"key1", "Value": "val1"}]
318
319
self.deployer.wait_for_execute.side_effect = RuntimeError("Some error")
320
with self.assertRaises(RuntimeError):
321
self.deploy_command.deploy(self.deployer,
322
stack_name,
323
template,
324
parameters,
325
capabilities,
326
execute_changeset,
327
role_arn,
328
notification_arns,
329
s3_uploader,
330
tags)
331
332
def test_deploy_raises_exception_on_empty_changeset(self):
333
stack_name = "stack_name"
334
parameters = ["a", "b"]
335
template = "cloudformation template"
336
capabilities = ["foo", "bar"]
337
execute_changeset = True
338
role_arn = "arn:aws:iam::1234567890:role"
339
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
340
tags = []
341
342
empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)
343
changeset_func = self.deployer.create_and_wait_for_changeset
344
changeset_func.side_effect = empty_changeset
345
with self.assertRaises(exceptions.ChangeEmptyError):
346
self.deploy_command.deploy(
347
self.deployer, stack_name, template, parameters, capabilities,
348
execute_changeset, role_arn, notification_arns,
349
None, tags)
350
351
def test_deploy_does_not_raise_exception_on_empty_changeset(self):
352
stack_name = "stack_name"
353
parameters = ["a", "b"]
354
template = "cloudformation template"
355
capabilities = ["foo", "bar"]
356
execute_changeset = True
357
role_arn = "arn:aws:iam::1234567890:role"
358
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
359
360
empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)
361
changeset_func = self.deployer.create_and_wait_for_changeset
362
changeset_func.side_effect = empty_changeset
363
self.deploy_command.deploy(
364
self.deployer, stack_name, template, parameters, capabilities,
365
execute_changeset, role_arn, notification_arns,
366
s3_uploader=None, tags=[],
367
fail_on_empty_changeset=False
368
)
369
370
def test_parse_key_value_arg_success(self):
371
"""
372
Tests that we can parse parameter arguments provided in proper format
373
Expected format: ["Key=Value", "Key=Value"]
374
:return:
375
"""
376
argname = "parameter-overrides"
377
data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}']
378
output = {"Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}'}
379
380
result = self.deploy_command.parse_key_value_arg(data, argname)
381
self.assertEqual(result, output)
382
383
# Empty input should return empty output
384
result = self.deploy_command.parse_key_value_arg([], argname)
385
self.assertEqual(result, {})
386
387
def test_parse_key_value_arg_invalid_input(self):
388
# non-list input
389
argname = "parameter-overrides"
390
with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):
391
self.deploy_command.parse_key_value_arg("hello=world", argname)
392
393
# missing equal to sign
394
with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):
395
self.deploy_command.parse_key_value_arg(["hello world"], argname)
396
397
def test_merge_parameters_success(self):
398
"""
399
Tests that we can merge parameters specified in CloudFormation template
400
with override values specified as commandline arguments
401
"""
402
template = {
403
"Parameters": {
404
"Key1": {"Type": "String"},
405
"Key2": {"Type": "String"},
406
"Key3": "Something",
407
"Key4": {"Type": "Number"},
408
"KeyWithDefaultValue": {"Type": "String", "Default": "something"},
409
"KeyWithDefaultValueButOverridden": {"Type": "String", "Default": "something"}
410
}
411
}
412
overrides = {
413
"Key1": "Value1",
414
"Key3": "Value3",
415
"KeyWithDefaultValueButOverridden": "Value4"
416
}
417
418
expected_result = [
419
# Overridden values
420
{"ParameterKey": "Key1", "ParameterValue": "Value1"},
421
{"ParameterKey": "Key3", "ParameterValue": "Value3"},
422
423
# Parameter contains default value, but overridden with new value
424
{"ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4"},
425
426
# non-overridden values
427
{"ParameterKey": "Key2", "UsePreviousValue": True},
428
{"ParameterKey": "Key4", "UsePreviousValue": True},
429
430
# Parameter with default value but NOT overridden.
431
# Use previous value, but this gets removed later when we are creating stack for first time
432
{"ParameterKey": "KeyWithDefaultValue", "UsePreviousValue": True},
433
]
434
435
self.assertItemsEqual(
436
self.deploy_command.merge_parameters(template, overrides),
437
expected_result
438
)
439
440
def test_merge_parameters_success_nothing_to_override(self):
441
"""
442
Tests that we can merge parameters specified in CloudFormation template
443
with override values specified as commandline arguments
444
"""
445
template = {
446
"Parameters": {
447
"Key1": {"Type": "String"}, "Key2": {"Type": "String"},
448
"Key3": "Something", "Key4": {"Type": "Number"},
449
}
450
}
451
overrides = {
452
# Key5 is not in the template. We will simply skip this key
453
"Key5": "Value5"
454
}
455
456
expected_result = [
457
{"ParameterKey": "Key1", "UsePreviousValue": True},
458
{"ParameterKey": "Key2", "UsePreviousValue": True},
459
{"ParameterKey": "Key3", "UsePreviousValue": True},
460
{"ParameterKey": "Key4", "UsePreviousValue": True},
461
]
462
463
self.assertItemsEqual(
464
self.deploy_command.merge_parameters(template, overrides),
465
expected_result
466
)
467
468
# Parameters definition is empty. Nothing to override
469
result = self.deploy_command.merge_parameters({"Parameters": {}},
470
overrides)
471
self.assertEqual(result, [])
472
473
def test_merge_parameters_invalid_input(self):
474
475
# Template does not contain "Parameters" definition
476
result = self.deploy_command.merge_parameters({}, {"Key": "Value"})
477
self.assertEqual(result, [])
478
479
# Parameters definition is invalid
480
result = self.deploy_command.merge_parameters({"Parameters": "foo"},
481
{"Key": "Value"})
482
self.assertEqual(result, [])
483
484