Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/unit/customizations/cloudformation/test_artifact_exporter.py
1569 views
1
import botocore.session
2
import tempfile
3
import os
4
import string
5
import random
6
import zipfile
7
8
import pytest
9
10
from contextlib import contextmanager, closing
11
from botocore.stub import Stubber
12
from awscli.testutils import mock, unittest, FileCreator
13
from awscli.customizations.cloudformation import exceptions
14
from awscli.customizations.cloudformation.artifact_exporter \
15
import is_s3_url, parse_s3_url, is_local_file, is_local_folder, \
16
upload_local_artifacts, zip_folder, make_abs_path, make_zip, \
17
Template, Resource, ResourceWithS3UrlDict, ServerlessApiResource, \
18
ServerlessFunctionResource, GraphQLSchemaResource, \
19
LambdaFunctionResource, ApiGatewayRestApiResource, \
20
ElasticBeanstalkApplicationVersion, CloudFormationStackResource, \
21
ServerlessApplicationResource, LambdaLayerVersionResource, \
22
copy_to_temp_dir, include_transform_export_handler, GLOBAL_EXPORT_DICT, \
23
ServerlessLayerVersionResource, ServerlessRepoApplicationLicense, \
24
ServerlessRepoApplicationReadme, \
25
AppSyncResolverRequestTemplateResource, \
26
AppSyncResolverResponseTemplateResource, \
27
AppSyncFunctionConfigurationRequestTemplateResource, \
28
AppSyncFunctionConfigurationResponseTemplateResource, \
29
GlueJobCommandScriptLocationResource, \
30
StepFunctionsStateMachineDefinitionResource, \
31
ServerlessStateMachineDefinitionResource, \
32
CodeCommitRepositoryS3Resource
33
34
35
VALID_CASES = [
36
"s3://foo/bar",
37
"s3://foo/bar/baz/cat/dog",
38
"s3://foo/bar?versionId=abc",
39
"s3://foo/bar/baz?versionId=abc&versionId=123",
40
"s3://foo/bar/baz?versionId=abc",
41
"s3://www.amazon.com/foo/bar",
42
"s3://my-new-bucket/foo/bar?a=1&a=2&a=3&b=1",
43
]
44
45
INVALID_CASES = [
46
# For purposes of exporter, we need S3 URLs to point to an object
47
# and not a bucket
48
"s3://foo",
49
50
# two versionIds is invalid
51
"https://s3-eu-west-1.amazonaws.com/bucket/key",
52
"https://www.amazon.com"
53
]
54
55
56
@pytest.mark.parametrize(
57
"url",
58
VALID_CASES
59
)
60
def test_is_valid_s3_url(url):
61
assert is_s3_url(url), f"{url} should be valid"
62
63
64
@pytest.mark.parametrize(
65
"url",
66
INVALID_CASES
67
)
68
def test_is_invalid_s3_url(url):
69
assert not is_s3_url(url), f"{url} should be invalid"
70
71
72
UPLOADED_S3_URL = "s3://foo/bar?versionId=baz"
73
74
RESOURCE_EXPORT_TEST_CASES = [
75
{
76
"class": ServerlessFunctionResource,
77
"expected_result": UPLOADED_S3_URL
78
},
79
80
{
81
"class": ServerlessApiResource,
82
"expected_result": UPLOADED_S3_URL
83
},
84
85
{
86
"class": GraphQLSchemaResource,
87
"expected_result": UPLOADED_S3_URL
88
},
89
90
{
91
"class": AppSyncResolverRequestTemplateResource,
92
"expected_result": UPLOADED_S3_URL
93
},
94
95
{
96
"class": AppSyncResolverResponseTemplateResource,
97
"expected_result": UPLOADED_S3_URL
98
},
99
100
{
101
"class": AppSyncFunctionConfigurationRequestTemplateResource,
102
"expected_result": UPLOADED_S3_URL
103
},
104
105
{
106
"class": AppSyncFunctionConfigurationResponseTemplateResource,
107
"expected_result": UPLOADED_S3_URL
108
},
109
110
{
111
"class": ApiGatewayRestApiResource,
112
"expected_result": {
113
"Bucket": "foo", "Key": "bar", "Version": "baz"
114
}
115
},
116
117
{
118
"class": LambdaFunctionResource,
119
"expected_result": {
120
"S3Bucket": "foo", "S3Key": "bar", "S3ObjectVersion": "baz"
121
}
122
},
123
124
{
125
"class": ElasticBeanstalkApplicationVersion,
126
"expected_result": {
127
"S3Bucket": "foo", "S3Key": "bar"
128
}
129
},
130
{
131
"class": LambdaLayerVersionResource,
132
"expected_result": {
133
"S3Bucket": "foo", "S3Key": "bar", "S3ObjectVersion": "baz"
134
}
135
},
136
{
137
"class": ServerlessLayerVersionResource,
138
"expected_result": UPLOADED_S3_URL
139
},
140
{
141
"class": ServerlessRepoApplicationReadme,
142
"expected_result": UPLOADED_S3_URL
143
},
144
{
145
"class": ServerlessRepoApplicationLicense,
146
"expected_result": UPLOADED_S3_URL
147
},
148
{
149
"class": ServerlessRepoApplicationLicense,
150
"expected_result": UPLOADED_S3_URL
151
},
152
{
153
"class": GlueJobCommandScriptLocationResource,
154
"expected_result": {
155
"ScriptLocation": UPLOADED_S3_URL
156
}
157
},
158
{
159
"class": StepFunctionsStateMachineDefinitionResource,
160
"expected_result": {
161
"Bucket": "foo", "Key": "bar", "Version": "baz"
162
}
163
},
164
{
165
"class": ServerlessStateMachineDefinitionResource,
166
"expected_result": {
167
"Bucket": "foo", "Key": "bar", "Version": "baz"
168
}
169
},
170
]
171
172
173
@pytest.mark.parametrize(
174
"test",
175
RESOURCE_EXPORT_TEST_CASES
176
)
177
def test_all_resources_export(test):
178
mock_path = (
179
"awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts"
180
)
181
with mock.patch(mock_path) as upload_local_artifacts_mock:
182
_helper_verify_export_resources(
183
test["class"], upload_local_artifacts_mock, test["expected_result"]
184
)
185
186
187
def _helper_verify_export_resources(
188
test_class, upload_local_artifacts_mock, expected_result
189
):
190
191
s3_uploader_mock = mock.Mock()
192
upload_local_artifacts_mock.reset_mock()
193
194
resource_id = "id"
195
parent_dir = "dir"
196
197
if '.' in test_class.PROPERTY_NAME:
198
reversed_property_names = test_class.PROPERTY_NAME.split('.')
199
reversed_property_names.reverse()
200
property_dict = {
201
reversed_property_names[0]: "foo"
202
}
203
for sub_property_name in reversed_property_names[1:]:
204
property_dict = {
205
sub_property_name: property_dict
206
}
207
resource_dict = property_dict
208
else:
209
resource_dict = {
210
test_class.PROPERTY_NAME: "foo"
211
}
212
213
upload_local_artifacts_mock.return_value = UPLOADED_S3_URL
214
resource_obj = test_class(s3_uploader_mock)
215
resource_obj.export(resource_id, resource_dict, parent_dir)
216
217
upload_local_artifacts_mock.assert_called_once_with(
218
resource_id, resource_dict, test_class.PROPERTY_NAME,
219
parent_dir, s3_uploader_mock
220
)
221
if '.' in test_class.PROPERTY_NAME:
222
top_level_property_name = test_class.PROPERTY_NAME.split('.')[0]
223
result = resource_dict[top_level_property_name]
224
else:
225
result = resource_dict[test_class.PROPERTY_NAME]
226
227
assert result == expected_result
228
229
230
class TestArtifactExporter(unittest.TestCase):
231
232
def setUp(self):
233
self.s3_uploader_mock = mock.Mock()
234
self.s3_uploader_mock.s3.meta.endpoint_url = "https://s3.some-valid-region.amazonaws.com"
235
236
def test_parse_s3_url(self):
237
238
valid = [
239
{
240
"url": "s3://foo/bar",
241
"result": {"Bucket": "foo", "Key": "bar"}
242
},
243
{
244
"url": "s3://foo/bar/cat/dog",
245
"result": {"Bucket": "foo", "Key": "bar/cat/dog"}
246
},
247
{
248
"url": "s3://foo/bar/baz?versionId=abc&param1=val1&param2=val2",
249
"result": {"Bucket": "foo", "Key": "bar/baz", "VersionId": "abc"}
250
},
251
{
252
# VersionId is not returned if there are more than one versionId
253
# keys in query parameter
254
"url": "s3://foo/bar/baz?versionId=abc&versionId=123",
255
"result": {"Bucket": "foo", "Key": "bar/baz"}
256
}
257
]
258
259
invalid = [
260
261
# For purposes of exporter, we need S3 URLs to point to an object
262
# and not a bucket
263
"s3://foo",
264
265
# two versionIds is invalid
266
"https://s3-eu-west-1.amazonaws.com/bucket/key",
267
"https://www.amazon.com"
268
]
269
270
for config in valid:
271
result = parse_s3_url(config["url"],
272
bucket_name_property="Bucket",
273
object_key_property="Key",
274
version_property="VersionId")
275
276
self.assertEqual(result, config["result"])
277
278
for url in invalid:
279
with self.assertRaises(ValueError):
280
parse_s3_url(url)
281
282
def test_is_local_file(self):
283
with tempfile.NamedTemporaryFile() as handle:
284
self.assertTrue(is_local_file(handle.name))
285
self.assertFalse(is_local_folder(handle.name))
286
287
def test_is_local_folder(self):
288
with self.make_temp_dir() as filename:
289
self.assertTrue(is_local_folder(filename))
290
self.assertFalse(is_local_file(filename))
291
292
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
293
def test_upload_local_artifacts_local_file(self, zip_and_upload_mock):
294
# Case 1: Artifact path is a relative path
295
# Verifies that we package local artifacts appropriately
296
property_name = "property"
297
resource_id = "resource_id"
298
expected_s3_url = "s3://foo/bar?versionId=baz"
299
300
self.s3_uploader_mock.upload_with_dedup.return_value = expected_s3_url
301
302
with tempfile.NamedTemporaryFile() as handle:
303
# Artifact is a file in the temporary directory
304
artifact_path = handle.name
305
parent_dir = tempfile.gettempdir()
306
307
resource_dict = {property_name: artifact_path}
308
result = upload_local_artifacts(resource_id,
309
resource_dict,
310
property_name,
311
parent_dir,
312
self.s3_uploader_mock)
313
self.assertEqual(result, expected_s3_url)
314
315
# Internally the method would convert relative paths to absolute
316
# path, with respect to the parent directory
317
absolute_artifact_path = make_abs_path(parent_dir, artifact_path)
318
self.s3_uploader_mock.upload_with_dedup.assert_called_with(absolute_artifact_path)
319
320
zip_and_upload_mock.assert_not_called()
321
322
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
323
def test_upload_local_artifacts_local_file_abs_path(self, zip_and_upload_mock):
324
# Case 2: Artifact path is an absolute path
325
# Verifies that we package local artifacts appropriately
326
property_name = "property"
327
resource_id = "resource_id"
328
expected_s3_url = "s3://foo/bar?versionId=baz"
329
330
self.s3_uploader_mock.upload_with_dedup.return_value = expected_s3_url
331
332
with tempfile.NamedTemporaryFile() as handle:
333
parent_dir = tempfile.gettempdir()
334
artifact_path = make_abs_path(parent_dir, handle.name)
335
336
resource_dict = {property_name: artifact_path}
337
result = upload_local_artifacts(resource_id,
338
resource_dict,
339
property_name,
340
parent_dir,
341
self.s3_uploader_mock)
342
self.assertEqual(result, expected_s3_url)
343
344
self.s3_uploader_mock.upload_with_dedup.assert_called_with(artifact_path)
345
zip_and_upload_mock.assert_not_called()
346
347
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
348
def test_upload_local_artifacts_local_folder(self, zip_and_upload_mock):
349
property_name = "property"
350
resource_id = "resource_id"
351
expected_s3_url = "s3://foo/bar?versionId=baz"
352
353
zip_and_upload_mock.return_value = expected_s3_url
354
355
# Artifact path is a Directory
356
with self.make_temp_dir() as artifact_path:
357
# Artifact is a file in the temporary directory
358
parent_dir = tempfile.gettempdir()
359
resource_dict = {property_name: artifact_path}
360
361
result = upload_local_artifacts(resource_id,
362
resource_dict,
363
property_name,
364
parent_dir,
365
mock.Mock())
366
self.assertEqual(result, expected_s3_url)
367
368
absolute_artifact_path = make_abs_path(parent_dir, artifact_path)
369
370
zip_and_upload_mock.assert_called_once_with(absolute_artifact_path,
371
mock.ANY)
372
373
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
374
def test_upload_local_artifacts_no_path(self, zip_and_upload_mock):
375
property_name = "property"
376
resource_id = "resource_id"
377
expected_s3_url = "s3://foo/bar?versionId=baz"
378
379
zip_and_upload_mock.return_value = expected_s3_url
380
381
# If you don't specify a path, we will default to Current Working Dir
382
resource_dict = {}
383
parent_dir = tempfile.gettempdir()
384
385
result = upload_local_artifacts(resource_id,
386
resource_dict,
387
property_name,
388
parent_dir,
389
self.s3_uploader_mock)
390
self.assertEqual(result, expected_s3_url)
391
392
zip_and_upload_mock.assert_called_once_with(parent_dir, mock.ANY)
393
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
394
395
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
396
def test_upload_local_artifacts_s3_url(self,
397
zip_and_upload_mock):
398
property_name = "property"
399
resource_id = "resource_id"
400
object_s3_url = "s3://foo/bar?versionId=baz"
401
402
# If URL is already S3 URL, this will be returned without zip/upload
403
resource_dict = {property_name: object_s3_url}
404
parent_dir = tempfile.gettempdir()
405
406
result = upload_local_artifacts(resource_id,
407
resource_dict,
408
property_name,
409
parent_dir,
410
self.s3_uploader_mock)
411
self.assertEqual(result, object_s3_url)
412
413
zip_and_upload_mock.assert_not_called()
414
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
415
416
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
417
def test_upload_local_artifacts_invalid_value(self, zip_and_upload_mock):
418
property_name = "property"
419
resource_id = "resource_id"
420
parent_dir = tempfile.gettempdir()
421
422
with self.assertRaises(exceptions.InvalidLocalPathError):
423
non_existent_file = "some_random_filename"
424
resource_dict = {property_name: non_existent_file}
425
upload_local_artifacts(resource_id,
426
resource_dict,
427
property_name,
428
parent_dir,
429
self.s3_uploader_mock)
430
431
with self.assertRaises(exceptions.InvalidLocalPathError):
432
non_existent_file = ["invalid datatype"]
433
resource_dict = {property_name: non_existent_file}
434
upload_local_artifacts(resource_id,
435
resource_dict,
436
property_name,
437
parent_dir,
438
self.s3_uploader_mock)
439
440
zip_and_upload_mock.assert_not_called()
441
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
442
443
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.make_zip")
444
def test_zip_folder(self, make_zip_mock):
445
zip_file_name = "name.zip"
446
make_zip_mock.return_value = zip_file_name
447
448
with self.make_temp_dir() as dirname:
449
with zip_folder(dirname) as actual_zip_file_name:
450
self.assertEqual(actual_zip_file_name, zip_file_name)
451
452
make_zip_mock.assert_called_once_with(mock.ANY, dirname)
453
454
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts")
455
def test_resource(self, upload_local_artifacts_mock):
456
# Property value is a path to file
457
458
class MockResource(Resource):
459
PROPERTY_NAME = "foo"
460
461
resource = MockResource(self.s3_uploader_mock)
462
463
resource_id = "id"
464
resource_dict = {}
465
resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
466
parent_dir = "dir"
467
s3_url = "s3://foo/bar"
468
469
upload_local_artifacts_mock.return_value = s3_url
470
471
resource.export(resource_id, resource_dict, parent_dir)
472
473
upload_local_artifacts_mock.assert_called_once_with(resource_id,
474
resource_dict,
475
resource.PROPERTY_NAME,
476
parent_dir,
477
self.s3_uploader_mock)
478
479
self.assertEqual(resource_dict[resource.PROPERTY_NAME], s3_url)
480
481
@mock.patch("shutil.rmtree")
482
@mock.patch("zipfile.is_zipfile")
483
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.copy_to_temp_dir")
484
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
485
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
486
def test_resource_with_force_zip_on_regular_file(self, is_local_file_mock, \
487
zip_and_upload_mock, copy_to_temp_dir_mock, is_zipfile_mock, rmtree_mock):
488
# Property value is a path to file and FORCE_ZIP is True
489
490
class MockResource(Resource):
491
PROPERTY_NAME = "foo"
492
FORCE_ZIP = True
493
494
resource = MockResource(self.s3_uploader_mock)
495
496
resource_id = "id"
497
resource_dict = {}
498
original_path = "/path/to/file"
499
resource_dict[resource.PROPERTY_NAME] = original_path
500
parent_dir = "dir"
501
s3_url = "s3://foo/bar"
502
503
zip_and_upload_mock.return_value = s3_url
504
is_local_file_mock.return_value = True
505
506
with self.make_temp_dir() as tmp_dir:
507
508
copy_to_temp_dir_mock.return_value = tmp_dir
509
510
# This is not a zip file
511
is_zipfile_mock.return_value = False
512
513
resource.export(resource_id, resource_dict, parent_dir)
514
515
zip_and_upload_mock.assert_called_once_with(tmp_dir, mock.ANY)
516
rmtree_mock.assert_called_once_with(tmp_dir)
517
is_zipfile_mock.assert_called_once_with(original_path)
518
assert resource_dict[resource.PROPERTY_NAME] == s3_url
519
520
@mock.patch("shutil.rmtree")
521
@mock.patch("zipfile.is_zipfile")
522
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.copy_to_temp_dir")
523
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
524
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
525
def test_resource_with_force_zip_on_zip_file(self, is_local_file_mock, \
526
zip_and_upload_mock, copy_to_temp_dir_mock, is_zipfile_mock, rmtree_mock):
527
# Property value is a path to zip file and FORCE_ZIP is True
528
# We should *not* re-zip an existing zip
529
530
class MockResource(Resource):
531
PROPERTY_NAME = "foo"
532
FORCE_ZIP = True
533
534
resource = MockResource(self.s3_uploader_mock)
535
536
resource_id = "id"
537
resource_dict = {}
538
original_path = "/path/to/zip_file"
539
resource_dict[resource.PROPERTY_NAME] = original_path
540
parent_dir = "dir"
541
s3_url = "s3://foo/bar"
542
543
# When the file is actually a zip-file, no additional zipping has to happen
544
is_zipfile_mock.return_value = True
545
is_local_file_mock.return_value = True
546
zip_and_upload_mock.return_value = s3_url
547
self.s3_uploader_mock.upload_with_dedup.return_value = s3_url
548
549
resource.export(resource_id, resource_dict, parent_dir)
550
551
copy_to_temp_dir_mock.assert_not_called()
552
zip_and_upload_mock.assert_not_called()
553
rmtree_mock.assert_not_called()
554
is_zipfile_mock.assert_called_once_with(original_path)
555
assert resource_dict[resource.PROPERTY_NAME] == s3_url
556
557
@mock.patch("shutil.rmtree")
558
@mock.patch("zipfile.is_zipfile")
559
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.copy_to_temp_dir")
560
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.zip_and_upload")
561
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
562
def test_resource_without_force_zip(self, is_local_file_mock, \
563
zip_and_upload_mock, copy_to_temp_dir_mock, is_zipfile_mock, rmtree_mock):
564
565
class MockResourceNoForceZip(Resource):
566
PROPERTY_NAME = "foo"
567
568
resource = MockResourceNoForceZip(self.s3_uploader_mock)
569
570
resource_id = "id"
571
resource_dict = {}
572
original_path = "/path/to/file"
573
resource_dict[resource.PROPERTY_NAME] = original_path
574
parent_dir = "dir"
575
s3_url = "s3://foo/bar"
576
577
# This is not a zip file, but a valid local file. Since FORCE_ZIP is NOT set, this will not be zipped
578
is_zipfile_mock.return_value = False
579
is_local_file_mock.return_value = True
580
zip_and_upload_mock.return_value = s3_url
581
self.s3_uploader_mock.upload_with_dedup.return_value = s3_url
582
583
resource.export(resource_id, resource_dict, parent_dir)
584
585
copy_to_temp_dir_mock.assert_not_called()
586
zip_and_upload_mock.assert_not_called()
587
rmtree_mock.assert_not_called()
588
is_zipfile_mock.assert_called_once_with(original_path)
589
assert resource_dict[resource.PROPERTY_NAME] == s3_url
590
591
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts")
592
def test_resource_empty_property_value(self, upload_local_artifacts_mock):
593
# Property value is empty
594
595
class MockResource(Resource):
596
PROPERTY_NAME = "foo"
597
resource = MockResource(self.s3_uploader_mock)
598
599
resource_id = "id"
600
resource_dict = {}
601
resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
602
parent_dir = "dir"
603
s3_url = "s3://foo/bar"
604
605
upload_local_artifacts_mock.return_value = s3_url
606
resource_dict = {}
607
resource.export(resource_id, resource_dict, parent_dir)
608
upload_local_artifacts_mock.assert_called_once_with(resource_id,
609
resource_dict,
610
resource.PROPERTY_NAME,
611
parent_dir,
612
self.s3_uploader_mock)
613
self.assertEqual(resource_dict[resource.PROPERTY_NAME], s3_url)
614
615
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts")
616
def test_resource_property_value_dict(self, upload_local_artifacts_mock):
617
# Property value is a dictionary. Export should not upload anything
618
619
class MockResource(Resource):
620
PROPERTY_NAME = "foo"
621
622
resource = MockResource(self.s3_uploader_mock)
623
resource_id = "id"
624
resource_dict = {}
625
resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
626
parent_dir = "dir"
627
s3_url = "s3://foo/bar"
628
629
upload_local_artifacts_mock.return_value = s3_url
630
resource_dict = {}
631
resource_dict[resource.PROPERTY_NAME] = {"a": "b"}
632
resource.export(resource_id, resource_dict, parent_dir)
633
upload_local_artifacts_mock.assert_not_called()
634
self.assertEqual(resource_dict, {"foo": {"a": "b"}})
635
636
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts")
637
def test_resource_has_package_null_property_to_false(self, upload_local_artifacts_mock):
638
# Should not upload anything if PACKAGE_NULL_PROPERTY is set to False
639
640
class MockResource(Resource):
641
PROPERTY_NAME = "foo"
642
PACKAGE_NULL_PROPERTY = False
643
644
resource = MockResource(self.s3_uploader_mock)
645
resource_id = "id"
646
resource_dict = {}
647
parent_dir = "dir"
648
s3_url = "s3://foo/bar"
649
650
upload_local_artifacts_mock.return_value = s3_url
651
652
resource.export(resource_id, resource_dict, parent_dir)
653
654
upload_local_artifacts_mock.assert_not_called()
655
self.assertNotIn(resource.PROPERTY_NAME, resource_dict)
656
657
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts")
658
def test_resource_export_fails(self, upload_local_artifacts_mock):
659
660
class MockResource(Resource):
661
PROPERTY_NAME = "foo"
662
663
resource = MockResource(self.s3_uploader_mock)
664
resource_id = "id"
665
resource_dict = {}
666
resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
667
parent_dir = "dir"
668
s3_url = "s3://foo/bar"
669
670
upload_local_artifacts_mock.side_effect = RuntimeError
671
resource_dict = {}
672
673
with self.assertRaises(exceptions.ExportFailedError):
674
resource.export(resource_id, resource_dict, parent_dir)
675
676
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.upload_local_artifacts")
677
def test_resource_with_s3_url_dict(self, upload_local_artifacts_mock):
678
"""
679
Checks if we properly export from the Resource classc
680
:return:
681
"""
682
683
self.assertTrue(issubclass(ResourceWithS3UrlDict, Resource))
684
685
class MockResource(ResourceWithS3UrlDict):
686
PROPERTY_NAME = "foo"
687
BUCKET_NAME_PROPERTY = "b"
688
OBJECT_KEY_PROPERTY = "o"
689
VERSION_PROPERTY = "v"
690
691
resource = MockResource(self.s3_uploader_mock)
692
693
# Case 1: Property value is a path to file
694
resource_id = "id"
695
resource_dict = {}
696
resource_dict[resource.PROPERTY_NAME] = "/path/to/file"
697
parent_dir = "dir"
698
s3_url = "s3://bucket/key1/key2?versionId=SomeVersionNumber"
699
700
upload_local_artifacts_mock.return_value = s3_url
701
702
resource.export(resource_id, resource_dict, parent_dir)
703
704
upload_local_artifacts_mock.assert_called_once_with(resource_id,
705
resource_dict,
706
resource.PROPERTY_NAME,
707
parent_dir,
708
self.s3_uploader_mock)
709
710
self.assertEqual(resource_dict[resource.PROPERTY_NAME], {
711
"b": "bucket",
712
"o": "key1/key2",
713
"v": "SomeVersionNumber"
714
})
715
716
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.Template")
717
def test_export_cloudformation_stack(self, TemplateMock):
718
stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
719
720
resource_id = "id"
721
property_name = stack_resource.PROPERTY_NAME
722
exported_template_dict = {"foo": "bar"}
723
result_s3_url = "s3://hello/world"
724
result_path_style_s3_url = "http://s3.amazonws.com/hello/world"
725
726
template_instance_mock = mock.Mock()
727
TemplateMock.return_value = template_instance_mock
728
template_instance_mock.export.return_value = exported_template_dict
729
730
self.s3_uploader_mock.upload_with_dedup.return_value = result_s3_url
731
self.s3_uploader_mock.to_path_style_s3_url.return_value = result_path_style_s3_url
732
733
with tempfile.NamedTemporaryFile() as handle:
734
template_path = handle.name
735
resource_dict = {property_name: template_path}
736
parent_dir = tempfile.gettempdir()
737
738
stack_resource.export(resource_id, resource_dict, parent_dir)
739
740
self.assertEqual(resource_dict[property_name], result_path_style_s3_url)
741
742
TemplateMock.assert_called_once_with(template_path, parent_dir, self.s3_uploader_mock)
743
template_instance_mock.export.assert_called_once_with()
744
self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(mock.ANY, "template")
745
self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with("world", None)
746
747
def _assert_stack_template_url_is_not_changed(self, s3_url):
748
stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
749
resource_id = "id"
750
property_name = stack_resource.PROPERTY_NAME
751
resource_dict = {property_name: s3_url}
752
753
# Case 1: Path is already S3 url
754
stack_resource.export(resource_id, resource_dict, "dir")
755
self.assertEqual(resource_dict[property_name], s3_url)
756
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
757
758
def test_export_cloudformation_stack_no_upload_path_is_s3url(self):
759
s3_url = "s3://hello/world"
760
self._assert_stack_template_url_is_not_changed(s3_url)
761
762
def test_export_cloudformation_stack_no_upload_path_is_httpsurl(self):
763
s3_url = "https://s3.amazonaws.com/hello/world"
764
self._assert_stack_template_url_is_not_changed(s3_url)
765
766
def test_export_cloudformation_stack_no_upload_path_is_s3_region_httpsurl(self):
767
s3_url = "https://s3.some-valid-region.amazonaws.com/hello/world"
768
self._assert_stack_template_url_is_not_changed(s3_url)
769
770
def test_export_cloudformation_stack_no_upload_path_is_virtual(self):
771
s3_url = "https://bucket.s3.amazonaws.com/key"
772
self._assert_stack_template_url_is_not_changed(s3_url)
773
774
def test_export_cloudformation_stack_no_upload_path_is_virtual_region(self):
775
s3_url = "https://bucket.s3.some-region.amazonaws.com/key"
776
self._assert_stack_template_url_is_not_changed(s3_url)
777
778
def test_export_cloudformation_stack_no_upload_path_is_empty(self):
779
stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
780
resource_id = "id"
781
property_name = stack_resource.PROPERTY_NAME
782
s3_url = "s3://hello/world"
783
resource_dict = {property_name: s3_url}
784
785
# Case 2: Path is empty
786
resource_dict = {}
787
stack_resource.export(resource_id, resource_dict, "dir")
788
self.assertEqual(resource_dict, {})
789
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
790
791
def test_export_cloudformation_stack_no_upload_path_not_file(self):
792
stack_resource = CloudFormationStackResource(self.s3_uploader_mock)
793
resource_id = "id"
794
property_name = stack_resource.PROPERTY_NAME
795
s3_url = "s3://hello/world"
796
797
# Case 3: Path is not a file
798
with self.make_temp_dir() as dirname:
799
resource_dict = {property_name: dirname}
800
with self.assertRaises(exceptions.ExportFailedError):
801
stack_resource.export(resource_id, resource_dict, "dir")
802
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
803
804
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.Template")
805
def test_export_serverless_application(self, TemplateMock):
806
stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
807
808
resource_id = "id"
809
property_name = stack_resource.PROPERTY_NAME
810
exported_template_dict = {"foo": "bar"}
811
result_s3_url = "s3://hello/world"
812
result_path_style_s3_url = "http://s3.amazonws.com/hello/world"
813
814
template_instance_mock = mock.Mock()
815
TemplateMock.return_value = template_instance_mock
816
template_instance_mock.export.return_value = exported_template_dict
817
818
self.s3_uploader_mock.upload_with_dedup.return_value = result_s3_url
819
self.s3_uploader_mock.to_path_style_s3_url.return_value = result_path_style_s3_url
820
821
with tempfile.NamedTemporaryFile() as handle:
822
template_path = handle.name
823
resource_dict = {property_name: template_path}
824
parent_dir = tempfile.gettempdir()
825
826
stack_resource.export(resource_id, resource_dict, parent_dir)
827
828
self.assertEqual(resource_dict[property_name], result_path_style_s3_url)
829
830
TemplateMock.assert_called_once_with(template_path, parent_dir, self.s3_uploader_mock)
831
template_instance_mock.export.assert_called_once_with()
832
self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(mock.ANY, "template")
833
self.s3_uploader_mock.to_path_style_s3_url.assert_called_once_with("world", None)
834
835
def test_export_serverless_application_no_upload_path_is_s3url(self):
836
stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
837
resource_id = "id"
838
property_name = stack_resource.PROPERTY_NAME
839
s3_url = "s3://hello/world"
840
resource_dict = {property_name: s3_url}
841
842
# Case 1: Path is already S3 url
843
stack_resource.export(resource_id, resource_dict, "dir")
844
self.assertEqual(resource_dict[property_name], s3_url)
845
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
846
847
def test_export_serverless_application_no_upload_path_is_httpsurl(self):
848
stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
849
resource_id = "id"
850
property_name = stack_resource.PROPERTY_NAME
851
s3_url = "https://s3.amazonaws.com/hello/world"
852
resource_dict = {property_name: s3_url}
853
854
# Case 1: Path is already S3 url
855
stack_resource.export(resource_id, resource_dict, "dir")
856
self.assertEqual(resource_dict[property_name], s3_url)
857
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
858
859
def test_export_serverless_application_no_upload_path_is_empty(self):
860
stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
861
resource_id = "id"
862
property_name = stack_resource.PROPERTY_NAME
863
864
# Case 2: Path is empty
865
resource_dict = {}
866
stack_resource.export(resource_id, resource_dict, "dir")
867
self.assertEqual(resource_dict, {})
868
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
869
870
def test_export_serverless_application_no_upload_path_not_file(self):
871
stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
872
resource_id = "id"
873
property_name = stack_resource.PROPERTY_NAME
874
875
# Case 3: Path is not a file
876
with self.make_temp_dir() as dirname:
877
resource_dict = {property_name: dirname}
878
with self.assertRaises(exceptions.ExportFailedError):
879
stack_resource.export(resource_id, resource_dict, "dir")
880
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
881
882
def test_export_serverless_application_no_upload_path_is_dictionary(self):
883
stack_resource = ServerlessApplicationResource(self.s3_uploader_mock)
884
resource_id = "id"
885
property_name = stack_resource.PROPERTY_NAME
886
887
# Case 4: Path is dictionary
888
location = {"ApplicationId": "id", "SemanticVersion": "1.0.1"}
889
resource_dict = {property_name: location}
890
stack_resource.export(resource_id, resource_dict, "dir")
891
self.assertEqual(resource_dict[property_name], location)
892
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
893
894
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.yaml_parse")
895
def test_template_export_metadata(self, yaml_parse_mock):
896
parent_dir = os.path.abspath(os.path.sep)
897
template_dir = os.path.join(parent_dir, 'foo', 'bar')
898
template_path = os.path.join(template_dir, 'path')
899
template_str = self.example_yaml_template()
900
901
metadata_type1_class = mock.Mock()
902
metadata_type1_class.RESOURCE_TYPE = "metadata_type1"
903
metadata_type1_class.PROPERTY_NAME = "property_1"
904
metadata_type1_instance = mock.Mock()
905
metadata_type1_class.return_value = metadata_type1_instance
906
907
metadata_type2_class = mock.Mock()
908
metadata_type2_class.RESOURCE_TYPE = "metadata_type2"
909
metadata_type2_class.PROPERTY_NAME = "property_2"
910
metadata_type2_instance = mock.Mock()
911
metadata_type2_class.return_value = metadata_type2_instance
912
913
metadata_to_export = [
914
metadata_type1_class,
915
metadata_type2_class
916
]
917
918
template_dict = {
919
"Metadata": {
920
"metadata_type1": {
921
"property_1": "abc"
922
},
923
"metadata_type2": {
924
"property_2": "def"
925
}
926
}
927
}
928
open_mock = mock.mock_open()
929
yaml_parse_mock.return_value = template_dict
930
931
# Patch the file open method to return template string
932
with mock.patch(
933
"awscli.customizations.cloudformation.artifact_exporter.open",
934
open_mock(read_data=template_str)) as open_mock:
935
936
template_exporter = Template(
937
template_path, parent_dir, self.s3_uploader_mock,
938
metadata_to_export=metadata_to_export)
939
exported_template = template_exporter.export()
940
self.assertEqual(exported_template, template_dict)
941
942
open_mock.assert_called_once_with(
943
make_abs_path(parent_dir, template_path), "r")
944
945
self.assertEqual(1, yaml_parse_mock.call_count)
946
947
metadata_type1_class.assert_called_once_with(self.s3_uploader_mock)
948
metadata_type1_instance.export.assert_called_once_with(
949
"metadata_type1", mock.ANY, template_dir)
950
metadata_type2_class.assert_called_once_with(self.s3_uploader_mock)
951
metadata_type2_instance.export.assert_called_once_with(
952
"metadata_type2", mock.ANY, template_dir)
953
954
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.yaml_parse")
955
def test_template_export(self, yaml_parse_mock):
956
parent_dir = os.path.abspath(os.path.sep)
957
template_dir = os.path.join(parent_dir, 'foo', 'bar')
958
template_path = os.path.join(template_dir, 'path')
959
template_str = self.example_yaml_template()
960
961
resource_type1_class = mock.Mock()
962
resource_type1_class.RESOURCE_TYPE = "resource_type1"
963
resource_type1_instance = mock.Mock()
964
resource_type1_class.return_value = resource_type1_instance
965
resource_type2_class = mock.Mock()
966
resource_type2_class.RESOURCE_TYPE = "resource_type2"
967
resource_type2_instance = mock.Mock()
968
resource_type2_class.return_value = resource_type2_instance
969
970
resources_to_export = [
971
resource_type1_class,
972
resource_type2_class
973
]
974
975
properties = {"foo": "bar"}
976
template_dict = {
977
"Resources": {
978
"Resource1": {
979
"Type": "resource_type1",
980
"Properties": properties
981
},
982
"Resource2": {
983
"Type": "resource_type2",
984
"Properties": properties
985
},
986
"Resource3": {
987
"Type": "some-other-type",
988
"Properties": properties
989
}
990
}
991
}
992
993
open_mock = mock.mock_open()
994
yaml_parse_mock.return_value = template_dict
995
996
# Patch the file open method to return template string
997
with mock.patch(
998
"awscli.customizations.cloudformation.artifact_exporter.open",
999
open_mock(read_data=template_str)) as open_mock:
1000
1001
template_exporter = Template(
1002
template_path, parent_dir, self.s3_uploader_mock,
1003
resources_to_export)
1004
exported_template = template_exporter.export()
1005
self.assertEqual(exported_template, template_dict)
1006
1007
open_mock.assert_called_once_with(
1008
make_abs_path(parent_dir, template_path), "r")
1009
1010
self.assertEqual(1, yaml_parse_mock.call_count)
1011
1012
resource_type1_class.assert_called_once_with(self.s3_uploader_mock)
1013
resource_type1_instance.export.assert_called_once_with(
1014
"Resource1", mock.ANY, template_dir)
1015
resource_type2_class.assert_called_once_with(self.s3_uploader_mock)
1016
resource_type2_instance.export.assert_called_once_with(
1017
"Resource2", mock.ANY, template_dir)
1018
1019
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.yaml_parse")
1020
def test_template_export_foreach_valid(self, yaml_parse_mock):
1021
parent_dir = os.path.abspath(os.path.sep)
1022
template_dir = os.path.join(parent_dir, 'foo', 'bar')
1023
template_path = os.path.join(template_dir, 'path')
1024
template_str = self.example_yaml_template()
1025
1026
resource_type1_class = mock.Mock()
1027
resource_type1_class.RESOURCE_TYPE = "resource_type1"
1028
resource_type1_instance = mock.Mock()
1029
resource_type1_class.return_value = resource_type1_instance
1030
resource_type2_class = mock.Mock()
1031
resource_type2_class.RESOURCE_TYPE = "resource_type2"
1032
resource_type2_instance = mock.Mock()
1033
resource_type2_class.return_value = resource_type2_instance
1034
1035
resources_to_export = [
1036
resource_type1_class,
1037
resource_type2_class
1038
]
1039
1040
properties = {"foo": "bar"}
1041
template_dict = {
1042
"Resources": {
1043
"Resource1": {
1044
"Type": "resource_type1",
1045
"Properties": properties
1046
},
1047
"Resource2": {
1048
"Type": "resource_type2",
1049
"Properties": properties
1050
},
1051
"Resource3": {
1052
"Type": "some-other-type",
1053
"Properties": properties
1054
},
1055
"Fn::ForEach::OuterLoopName": [
1056
"Identifier1",
1057
["4", "5"],
1058
{
1059
"Fn::ForEach::InnerLoopName": [
1060
"Identifier2",
1061
["6", "7"],
1062
{
1063
"Resource${Identifier1}${Identifier2}": {
1064
"Type": "resource_type2",
1065
"Properties": properties
1066
}
1067
}
1068
],
1069
"Resource${Identifier1}": {
1070
"Type": "resource_type1",
1071
"Properties": properties
1072
}
1073
}
1074
]
1075
}
1076
}
1077
1078
open_mock = mock.mock_open()
1079
yaml_parse_mock.return_value = template_dict
1080
1081
# Patch the file open method to return template string
1082
with mock.patch(
1083
"awscli.customizations.cloudformation.artifact_exporter.open",
1084
open_mock(read_data=template_str)) as open_mock:
1085
1086
template_exporter = Template(
1087
template_path, parent_dir, self.s3_uploader_mock,
1088
resources_to_export)
1089
exported_template = template_exporter.export()
1090
self.assertEqual(exported_template, template_dict)
1091
1092
open_mock.assert_called_once_with(
1093
make_abs_path(parent_dir, template_path), "r")
1094
1095
self.assertEqual(1, yaml_parse_mock.call_count)
1096
1097
resource_type1_class.assert_called_with(self.s3_uploader_mock)
1098
self.assertEqual(
1099
resource_type1_instance.export.call_args_list,
1100
[
1101
mock.call("Resource1", properties, template_dir),
1102
mock.call("Resource${Identifier1}", properties, template_dir)
1103
]
1104
)
1105
resource_type2_class.assert_called_with(self.s3_uploader_mock)
1106
self.assertEqual(
1107
resource_type2_instance.export.call_args_list,
1108
[
1109
mock.call("Resource2", properties, template_dir),
1110
mock.call("Resource${Identifier1}${Identifier2}", properties, template_dir)
1111
]
1112
)
1113
1114
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.yaml_parse")
1115
def test_template_export_foreach_invalid(self, yaml_parse_mock):
1116
parent_dir = os.path.abspath(os.path.sep)
1117
template_dir = os.path.join(parent_dir, 'foo', 'bar')
1118
template_path = os.path.join(template_dir, 'path')
1119
template_str = self.example_yaml_template()
1120
1121
resource_type1_class = mock.Mock()
1122
resource_type1_class.RESOURCE_TYPE = "resource_type1"
1123
resource_type1_instance = mock.Mock()
1124
resource_type1_class.return_value = resource_type1_instance
1125
resource_type2_class = mock.Mock()
1126
resource_type2_class.RESOURCE_TYPE = "resource_type2"
1127
resource_type2_instance = mock.Mock()
1128
resource_type2_class.return_value = resource_type2_instance
1129
1130
resources_to_export = [
1131
resource_type1_class,
1132
resource_type2_class
1133
]
1134
1135
properties = {"foo": "bar"}
1136
template_dict = {
1137
"Resources": {
1138
"Resource1": {
1139
"Type": "resource_type1",
1140
"Properties": properties
1141
},
1142
"Resource2": {
1143
"Type": "resource_type2",
1144
"Properties": properties
1145
},
1146
"Resource3": {
1147
"Type": "some-other-type",
1148
"Properties": properties
1149
},
1150
"Fn::ForEach::OuterLoopName": [
1151
"Identifier1",
1152
{
1153
"Resource${Identifier1}": {
1154
}
1155
}
1156
]
1157
}
1158
}
1159
1160
open_mock = mock.mock_open()
1161
yaml_parse_mock.return_value = template_dict
1162
1163
# Patch the file open method to return template string
1164
with mock.patch(
1165
"awscli.customizations.cloudformation.artifact_exporter.open",
1166
open_mock(read_data=template_str)) as open_mock:
1167
template_exporter = Template(
1168
template_path, parent_dir, self.s3_uploader_mock,
1169
resources_to_export)
1170
with self.assertRaises(exceptions.InvalidForEachIntrinsicFunctionError):
1171
template_exporter.export()
1172
1173
1174
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.yaml_parse")
1175
def test_template_global_export(self, yaml_parse_mock):
1176
parent_dir = os.path.abspath(os.path.sep)
1177
template_dir = os.path.join(parent_dir, 'foo', 'bar')
1178
template_path = os.path.join(template_dir, 'path')
1179
template_str = self.example_yaml_template()
1180
1181
resource_type1_class = mock.Mock()
1182
resource_type1_instance = mock.Mock()
1183
resource_type1_class.return_value = resource_type1_instance
1184
resource_type2_class = mock.Mock()
1185
resource_type2_instance = mock.Mock()
1186
resource_type2_class.return_value = resource_type2_instance
1187
1188
resources_to_export = {
1189
"resource_type1": resource_type1_class,
1190
"resource_type2": resource_type2_class
1191
}
1192
properties1 = {"foo": "bar", "Fn::Transform": {"Name": "AWS::Include", "Parameters": {"Location": "foo.yaml"}}}
1193
properties2 = {"foo": "bar", "Fn::Transform": {"Name": "AWS::OtherTransform"}}
1194
properties_in_list = {"Fn::Transform": {"Name": "AWS::Include", "Parameters": {"Location": "bar.yaml"}}}
1195
template_dict = {
1196
"Resources": {
1197
"Resource1": {
1198
"Type": "resource_type1",
1199
"Properties": properties1
1200
},
1201
"Resource2": {
1202
"Type": "resource_type2",
1203
"Properties": properties2,
1204
}
1205
},
1206
"List": ["foo", properties_in_list]
1207
}
1208
open_mock = mock.mock_open()
1209
include_transform_export_handler_mock = mock.Mock()
1210
include_transform_export_handler_mock.return_value = {"Name": "AWS::Include", "Parameters": {"Location": "s3://foo"}}
1211
yaml_parse_mock.return_value = template_dict
1212
1213
with mock.patch(
1214
"awscli.customizations.cloudformation.artifact_exporter.open",
1215
open_mock(read_data=template_str)) as open_mock:
1216
with mock.patch.dict(GLOBAL_EXPORT_DICT, {"Fn::Transform": include_transform_export_handler_mock}):
1217
template_exporter = Template(
1218
template_path, parent_dir, self.s3_uploader_mock,
1219
resources_to_export)
1220
1221
exported_template = template_exporter.export_global_artifacts(template_exporter.template_dict)
1222
1223
first_call_args, kwargs = include_transform_export_handler_mock.call_args_list[0]
1224
second_call_args, kwargs = include_transform_export_handler_mock.call_args_list[1]
1225
third_call_args, kwargs = include_transform_export_handler_mock.call_args_list[2]
1226
call_args = [first_call_args[0], second_call_args[0], third_call_args[0]]
1227
self.assertTrue({"Name": "AWS::Include", "Parameters": {"Location": "foo.yaml"}} in call_args)
1228
self.assertTrue({"Name": "AWS::OtherTransform"} in call_args)
1229
self.assertTrue({"Name": "AWS::Include", "Parameters": {"Location": "bar.yaml"}} in call_args)
1230
self.assertTrue(template_dir in first_call_args)
1231
self.assertTrue(template_dir in second_call_args)
1232
self.assertTrue(template_dir in third_call_args)
1233
self.assertEqual(include_transform_export_handler_mock.call_count, 3)
1234
#new s3 url is added to include location
1235
self.assertEqual(exported_template["Resources"]["Resource1"]["Properties"]["Fn::Transform"], {"Name": "AWS::Include", "Parameters": {"Location": "s3://foo"}})
1236
self.assertEqual(exported_template["List"][1]["Fn::Transform"], {"Name": "AWS::Include", "Parameters": {"Location": "s3://foo"}})
1237
1238
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1239
def test_include_transform_export_handler_with_relative_file_path(self, is_local_file_mock):
1240
#exports transform
1241
parent_dir = os.path.abspath("someroot")
1242
self.s3_uploader_mock.upload_with_dedup.return_value = "s3://foo"
1243
is_local_file_mock.return_value = True
1244
abs_file_path = os.path.join(parent_dir, "foo.yaml")
1245
1246
handler_output = include_transform_export_handler({"Name": "AWS::Include", "Parameters": {"Location": "foo.yaml"}}, self.s3_uploader_mock, parent_dir)
1247
self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(abs_file_path)
1248
is_local_file_mock.assert_called_with(abs_file_path)
1249
self.assertEqual(handler_output, {'Name': 'AWS::Include', 'Parameters': {'Location': 's3://foo'}})
1250
1251
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1252
def test_include_transform_export_handler_with_absolute_file_path(self, is_local_file_mock):
1253
#exports transform
1254
parent_dir = os.path.abspath("someroot")
1255
self.s3_uploader_mock.upload_with_dedup.return_value = "s3://foo"
1256
is_local_file_mock.return_value = True
1257
abs_file_path = os.path.abspath(os.path.join("my", "file.yaml"))
1258
1259
handler_output = include_transform_export_handler({"Name": "AWS::Include", "Parameters": {"Location": abs_file_path}}, self.s3_uploader_mock, parent_dir)
1260
self.s3_uploader_mock.upload_with_dedup.assert_called_once_with(abs_file_path)
1261
is_local_file_mock.assert_called_with(abs_file_path)
1262
self.assertEqual(handler_output, {'Name': 'AWS::Include', 'Parameters': {'Location': 's3://foo'}})
1263
1264
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1265
def test_include_transform_export_handler_with_s3_uri(self, is_local_file_mock):
1266
1267
handler_output = include_transform_export_handler({"Name": "AWS::Include", "Parameters": {"Location": "s3://bucket/foo.yaml"}}, self.s3_uploader_mock, "parent_dir")
1268
# Input is returned unmodified
1269
self.assertEqual(handler_output, {"Name": "AWS::Include", "Parameters": {"Location": "s3://bucket/foo.yaml"}})
1270
1271
is_local_file_mock.assert_not_called()
1272
self.s3_uploader_mock.assert_not_called()
1273
1274
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1275
def test_include_transform_export_handler_with_no_path(self, is_local_file_mock):
1276
1277
handler_output = include_transform_export_handler({"Name": "AWS::Include", "Parameters": {"Location": ""}}, self.s3_uploader_mock, "parent_dir")
1278
# Input is returned unmodified
1279
self.assertEqual(handler_output, {"Name": "AWS::Include", "Parameters": {"Location": ""}})
1280
1281
is_local_file_mock.assert_not_called()
1282
self.s3_uploader_mock.assert_not_called()
1283
1284
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1285
def test_include_transform_export_handler_with_dict_value_for_location(self, is_local_file_mock):
1286
1287
handler_output = include_transform_export_handler(
1288
{"Name": "AWS::Include", "Parameters": {"Location": {"Fn::Sub": "${S3Bucket}/file.txt"}}},
1289
self.s3_uploader_mock,
1290
"parent_dir")
1291
# Input is returned unmodified
1292
self.assertEqual(handler_output, {"Name": "AWS::Include", "Parameters": {"Location": {"Fn::Sub": "${S3Bucket}/file.txt"}}})
1293
1294
is_local_file_mock.assert_not_called()
1295
self.s3_uploader_mock.assert_not_called()
1296
1297
1298
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1299
def test_include_transform_export_handler_non_local_file(self, is_local_file_mock):
1300
#returns unchanged template dict if transform not a local file, and not a S3 URI
1301
is_local_file_mock.return_value = False
1302
1303
with self.assertRaises(exceptions.InvalidLocalPathError):
1304
include_transform_export_handler({"Name": "AWS::Include", "Parameters": {"Location": "http://foo.yaml"}}, self.s3_uploader_mock, "parent_dir")
1305
is_local_file_mock.assert_called_with("http://foo.yaml")
1306
self.s3_uploader_mock.assert_not_called()
1307
1308
@mock.patch("awscli.customizations.cloudformation.artifact_exporter.is_local_file")
1309
def test_include_transform_export_handler_non_include_transform(self, is_local_file_mock):
1310
#ignores transform that is not aws::include
1311
handler_output = include_transform_export_handler({"Name": "AWS::OtherTransform", "Parameters": {"Location": "foo.yaml"}}, self.s3_uploader_mock, "parent_dir")
1312
self.s3_uploader_mock.upload_with_dedup.assert_not_called()
1313
self.assertEqual(handler_output, {"Name": "AWS::OtherTransform", "Parameters": {"Location": "foo.yaml"}})
1314
1315
def test_template_export_path_be_folder(self):
1316
1317
template_path = "/path/foo"
1318
# Set parent_dir to be a non-existent folder
1319
with self.assertRaises(ValueError):
1320
Template(template_path, "somefolder", self.s3_uploader_mock)
1321
1322
# Set parent_dir to be a real folder, but just a relative path
1323
with self.make_temp_dir() as dirname:
1324
with self.assertRaises(ValueError):
1325
Template(template_path, os.path.relpath(dirname), self.s3_uploader_mock)
1326
1327
def test_make_zip(self):
1328
test_file_creator = FileCreator()
1329
test_file_creator.append_file('index.js',
1330
'exports handler = (event, context, callback) => {callback(null, event);}')
1331
1332
dirname = test_file_creator.rootdir
1333
1334
expected_files = set(['index.js'])
1335
1336
random_name = ''.join(random.choice(string.ascii_letters) for _ in range(10))
1337
outfile = os.path.join(tempfile.gettempdir(), random_name)
1338
1339
zipfile_name = None
1340
try:
1341
zipfile_name = make_zip(outfile, dirname)
1342
1343
test_zip_file = zipfile.ZipFile(zipfile_name, "r")
1344
with closing(test_zip_file) as zf:
1345
files_in_zip = set()
1346
for info in zf.infolist():
1347
files_in_zip.add(info.filename)
1348
1349
self.assertEqual(files_in_zip, expected_files)
1350
1351
finally:
1352
if zipfile_name:
1353
os.remove(zipfile_name)
1354
test_file_creator.remove_all()
1355
1356
@mock.patch("shutil.copy")
1357
@mock.patch("tempfile.mkdtemp")
1358
def test_copy_to_temp_dir(self, mkdtemp_mock, copyfile_mock):
1359
temp_dir = "/tmp/foo/"
1360
filename = "test.js"
1361
mkdtemp_mock.return_value = temp_dir
1362
1363
returned_dir = copy_to_temp_dir(filename)
1364
1365
self.assertEqual(returned_dir, temp_dir)
1366
copyfile_mock.assert_called_once_with(filename, temp_dir + filename)
1367
1368
@contextmanager
1369
def make_temp_dir(self):
1370
filename = tempfile.mkdtemp()
1371
try:
1372
yield filename
1373
finally:
1374
if filename:
1375
os.rmdir(filename)
1376
1377
def example_yaml_template(self):
1378
return """
1379
AWSTemplateFormatVersion: '2010-09-09'
1380
Description: Simple CRUD webservice. State is stored in a SimpleTable (DynamoDB) resource.
1381
Resources:
1382
MyFunction:
1383
Type: AWS::Lambda::Function
1384
Properties:
1385
Code: ./handler
1386
Handler: index.get
1387
Role:
1388
Fn::GetAtt:
1389
- MyFunctionRole
1390
- Arn
1391
Timeout: 20
1392
Runtime: nodejs4.3
1393
"""
1394
1395