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
1569 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
)
125
126
self.deploy_command.parse_key_value_arg.assert_has_calls([
127
mock.call(
128
self.parsed_args.parameter_overrides,
129
"parameter-overrides"
130
),
131
mock.call(
132
self.parsed_args.tags,
133
"tags"
134
)
135
])
136
137
self.deploy_command.merge_parameters.assert_called_once_with(
138
fake_template, fake_parameter_overrides)
139
140
self.assertEqual(1, mock_yaml_parse.call_count)
141
142
def test_invalid_template_file(self):
143
self.parsed_args.template_file = "sometemplate"
144
with self.assertRaises(exceptions.InvalidTemplatePathError):
145
result = self.deploy_command._run_main(self.parsed_args,
146
parsed_globals=self.parsed_globals)
147
148
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.isfile')
149
@mock.patch('awscli.customizations.cloudformation.deploy.yaml_parse')
150
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.getsize')
151
def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_parse, mock_isfile):
152
"""
153
Tests that large templates are detected prior to deployment
154
"""
155
template_str = get_example_template()
156
157
mock_getsize.return_value = 51201
158
mock_isfile.return_value = True
159
mock_yaml_parse.return_value = template_str
160
open_mock = mock.mock_open()
161
162
with mock.patch(
163
"awscli.customizations.cloudformation.deploy.open",
164
open_mock(read_data=template_str)) as open_mock:
165
with self.assertRaises(exceptions.DeployBucketRequiredError):
166
result = self.deploy_command._run_main(self.parsed_args,
167
parsed_globals=self.parsed_globals)
168
169
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.isfile')
170
@mock.patch('awscli.customizations.cloudformation.deploy.yaml_parse')
171
@mock.patch('awscli.customizations.cloudformation.deploy.os.path.getsize')
172
@mock.patch('awscli.customizations.cloudformation.deploy.DeployCommand.deploy')
173
@mock.patch('awscli.customizations.cloudformation.deploy.S3Uploader')
174
def test_s3_uploader_is_configured_properly(self, s3UploaderMock,
175
deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile):
176
"""
177
Tests that large templates are detected prior to deployment
178
"""
179
bucket_name = "mybucket"
180
template_str = get_example_template()
181
182
mock_getsize.return_value = 1024
183
mock_isfile.return_value = True
184
mock_yaml_parse.return_value = template_str
185
open_mock = mock.mock_open()
186
187
with mock.patch(
188
"awscli.customizations.cloudformation.deploy.open",
189
open_mock(read_data=template_str)) as open_mock:
190
191
self.parsed_args.s3_bucket = bucket_name
192
s3UploaderObject = mock.Mock()
193
s3UploaderMock.return_value = s3UploaderObject
194
195
result = self.deploy_command._run_main(self.parsed_args,
196
parsed_globals=self.parsed_globals)
197
198
self.deploy_command.deploy.assert_called_once_with(
199
mock.ANY,
200
self.parsed_args.stack_name,
201
mock.ANY,
202
mock.ANY,
203
None,
204
not self.parsed_args.no_execute_changeset,
205
None,
206
[],
207
s3UploaderObject,
208
[{"Key": "tagkey1", "Value": "tagvalue1"}],
209
True,
210
True
211
)
212
213
s3UploaderMock.assert_called_once_with(mock.ANY,
214
bucket_name,
215
self.parsed_args.s3_prefix,
216
self.parsed_args.kms_key_id,
217
self.parsed_args.force_upload)
218
219
def test_deploy_success(self):
220
"""
221
Tests that we call the deploy command
222
"""
223
224
stack_name = "stack_name"
225
changeset_id = "some changeset"
226
parameters = ["a", "b"]
227
template = "cloudformation template"
228
capabilities = ["foo", "bar"]
229
execute_changeset = True
230
changeset_type = "CREATE"
231
role_arn = "arn:aws:iam::1234567890:role"
232
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
233
s3_uploader = None
234
tags = [{"Key":"key1", "Value": "val1"}]
235
236
# Set the mock to return this fake changeset_id
237
self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type)
238
239
rc = self.deploy_command.deploy(self.deployer,
240
stack_name,
241
template,
242
parameters,
243
capabilities,
244
execute_changeset,
245
role_arn,
246
notification_arns,
247
s3_uploader,
248
tags)
249
self.assertEqual(rc, 0)
250
251
252
self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,
253
cfn_template=template,
254
parameter_values=parameters,
255
capabilities=capabilities,
256
role_arn=role_arn,
257
notification_arns=notification_arns,
258
s3_uploader=s3_uploader,
259
tags=tags)
260
261
# since execute_changeset is set to True, deploy() will execute changeset
262
self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name, False)
263
self.deployer.wait_for_execute.assert_called_once_with(stack_name, changeset_type)
264
265
266
def test_deploy_no_execute(self):
267
stack_name = "stack_name"
268
changeset_id = "some changeset"
269
parameters = ["a", "b"]
270
template = "cloudformation template"
271
capabilities = ["foo", "bar"]
272
execute_changeset = False
273
role_arn = "arn:aws:iam::1234567890:role"
274
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
275
s3_uploader = None
276
tags = [{"Key":"key1", "Value": "val1"}]
277
278
279
self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE")
280
rc = self.deploy_command.deploy(self.deployer,
281
stack_name,
282
template,
283
parameters,
284
capabilities,
285
execute_changeset,
286
role_arn,
287
notification_arns,
288
s3_uploader,
289
tags)
290
self.assertEqual(rc, 0)
291
292
self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,
293
cfn_template=template,
294
parameter_values=parameters,
295
capabilities=capabilities,
296
role_arn=role_arn,
297
notification_arns=notification_arns,
298
s3_uploader=s3_uploader,
299
tags=tags)
300
301
# since execute_changeset is set to True, deploy() will execute changeset
302
self.deployer.execute_changeset.assert_not_called()
303
self.deployer.wait_for_execute.assert_not_called()
304
305
def test_deploy_raise_exception(self):
306
stack_name = "stack_name"
307
changeset_id = "some changeset"
308
parameters = ["a", "b"]
309
template = "cloudformation template"
310
capabilities = ["foo", "bar"]
311
execute_changeset = True
312
role_arn = "arn:aws:iam::1234567890:role"
313
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
314
s3_uploader = None
315
tags = [{"Key":"key1", "Value": "val1"}]
316
317
self.deployer.wait_for_execute.side_effect = RuntimeError("Some error")
318
with self.assertRaises(RuntimeError):
319
self.deploy_command.deploy(self.deployer,
320
stack_name,
321
template,
322
parameters,
323
capabilities,
324
execute_changeset,
325
role_arn,
326
notification_arns,
327
s3_uploader,
328
tags)
329
330
def test_deploy_raises_exception_on_empty_changeset(self):
331
stack_name = "stack_name"
332
parameters = ["a", "b"]
333
template = "cloudformation template"
334
capabilities = ["foo", "bar"]
335
execute_changeset = True
336
role_arn = "arn:aws:iam::1234567890:role"
337
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
338
tags = []
339
340
empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)
341
changeset_func = self.deployer.create_and_wait_for_changeset
342
changeset_func.side_effect = empty_changeset
343
with self.assertRaises(exceptions.ChangeEmptyError):
344
self.deploy_command.deploy(
345
self.deployer, stack_name, template, parameters, capabilities,
346
execute_changeset, role_arn, notification_arns,
347
None, tags)
348
349
def test_deploy_does_not_raise_exception_on_empty_changeset(self):
350
stack_name = "stack_name"
351
parameters = ["a", "b"]
352
template = "cloudformation template"
353
capabilities = ["foo", "bar"]
354
execute_changeset = True
355
role_arn = "arn:aws:iam::1234567890:role"
356
notification_arns = ["arn:aws:sns:region:1234567890:notify"]
357
358
empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)
359
changeset_func = self.deployer.create_and_wait_for_changeset
360
changeset_func.side_effect = empty_changeset
361
self.deploy_command.deploy(
362
self.deployer, stack_name, template, parameters, capabilities,
363
execute_changeset, role_arn, notification_arns,
364
s3_uploader=None, tags=[],
365
fail_on_empty_changeset=False
366
)
367
368
def test_parse_key_value_arg_success(self):
369
"""
370
Tests that we can parse parameter arguments provided in proper format
371
Expected format: ["Key=Value", "Key=Value"]
372
:return:
373
"""
374
argname = "parameter-overrides"
375
data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}']
376
output = {"Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}'}
377
378
result = self.deploy_command.parse_key_value_arg(data, argname)
379
self.assertEqual(result, output)
380
381
# Empty input should return empty output
382
result = self.deploy_command.parse_key_value_arg([], argname)
383
self.assertEqual(result, {})
384
385
def test_parse_key_value_arg_invalid_input(self):
386
# non-list input
387
argname = "parameter-overrides"
388
with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):
389
self.deploy_command.parse_key_value_arg("hello=world", argname)
390
391
# missing equal to sign
392
with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):
393
self.deploy_command.parse_key_value_arg(["hello world"], argname)
394
395
def test_merge_parameters_success(self):
396
"""
397
Tests that we can merge parameters specified in CloudFormation template
398
with override values specified as commandline arguments
399
"""
400
template = {
401
"Parameters": {
402
"Key1": {"Type": "String"},
403
"Key2": {"Type": "String"},
404
"Key3": "Something",
405
"Key4": {"Type": "Number"},
406
"KeyWithDefaultValue": {"Type": "String", "Default": "something"},
407
"KeyWithDefaultValueButOverridden": {"Type": "String", "Default": "something"}
408
}
409
}
410
overrides = {
411
"Key1": "Value1",
412
"Key3": "Value3",
413
"KeyWithDefaultValueButOverridden": "Value4"
414
}
415
416
expected_result = [
417
# Overridden values
418
{"ParameterKey": "Key1", "ParameterValue": "Value1"},
419
{"ParameterKey": "Key3", "ParameterValue": "Value3"},
420
421
# Parameter contains default value, but overridden with new value
422
{"ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4"},
423
424
# non-overridden values
425
{"ParameterKey": "Key2", "UsePreviousValue": True},
426
{"ParameterKey": "Key4", "UsePreviousValue": True},
427
428
# Parameter with default value but NOT overridden.
429
# Use previous value, but this gets removed later when we are creating stack for first time
430
{"ParameterKey": "KeyWithDefaultValue", "UsePreviousValue": True},
431
]
432
433
self.assertItemsEqual(
434
self.deploy_command.merge_parameters(template, overrides),
435
expected_result
436
)
437
438
def test_merge_parameters_success_nothing_to_override(self):
439
"""
440
Tests that we can merge parameters specified in CloudFormation template
441
with override values specified as commandline arguments
442
"""
443
template = {
444
"Parameters": {
445
"Key1": {"Type": "String"}, "Key2": {"Type": "String"},
446
"Key3": "Something", "Key4": {"Type": "Number"},
447
}
448
}
449
overrides = {
450
# Key5 is not in the template. We will simply skip this key
451
"Key5": "Value5"
452
}
453
454
expected_result = [
455
{"ParameterKey": "Key1", "UsePreviousValue": True},
456
{"ParameterKey": "Key2", "UsePreviousValue": True},
457
{"ParameterKey": "Key3", "UsePreviousValue": True},
458
{"ParameterKey": "Key4", "UsePreviousValue": True},
459
]
460
461
self.assertItemsEqual(
462
self.deploy_command.merge_parameters(template, overrides),
463
expected_result
464
)
465
466
# Parameters definition is empty. Nothing to override
467
result = self.deploy_command.merge_parameters({"Parameters": {}},
468
overrides)
469
self.assertEqual(result, [])
470
471
def test_merge_parameters_invalid_input(self):
472
473
# Template does not contain "Parameters" definition
474
result = self.deploy_command.merge_parameters({}, {"Key": "Value"})
475
self.assertEqual(result, [])
476
477
# Parameters definition is invalid
478
result = self.deploy_command.merge_parameters({"Parameters": "foo"},
479
{"Key": "Value"})
480
self.assertEqual(result, [])
481
482