Path: blob/develop/tests/unit/customizations/cloudformation/test_deploy.py
1569 views
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0e7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12import tempfile13import collections1415from awscli.testutils import mock, unittest16from awscli.customizations.cloudformation.deploy import DeployCommand17from awscli.customizations.cloudformation.deployer import Deployer18from awscli.customizations.cloudformation.yamlhelper import yaml_parse19from awscli.customizations.cloudformation import exceptions202122class FakeArgs(object):23def __init__(self, **kwargs):24self.__dict__.update(kwargs)2526def __contains__(self, key):27return key in self.__dict__282930def get_example_template():31return {32"Parameters": {33"Key1": "Value1"34},35"Resources": {36"Resource1": {}37}38}3940ChangeSetResult = collections.namedtuple("ChangeSetResult", ["changeset_id", "changeset_type"])4142class TestDeployCommand(unittest.TestCase):4344def setUp(self):45self.session = mock.Mock()46self.session.get_scoped_config.return_value = {}47self.parsed_args = FakeArgs(template_file='./foo',48stack_name="some_stack_name",49parameter_overrides=["Key1=Value1",50"Key2=Value2"],51no_execute_changeset=False,52execute_changeset=True,53disable_rollback=True,54capabilities=None,55role_arn=None,56notification_arns=[],57fail_on_empty_changeset=True,58s3_bucket=None,59s3_prefix="some prefix",60kms_key_id="some kms key id",61force_upload=True,62tags=["tagkey1=tagvalue1"])63self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None,64verify_ssl=None)65self.deploy_command = DeployCommand(self.session)6667self.deployer = Deployer(mock.Mock())68self.deployer.create_and_wait_for_changeset = mock.Mock()69self.deployer.execute_changeset = mock.Mock()70self.deployer.wait_for_execute = mock.Mock()7172@mock.patch("awscli.customizations.cloudformation.deploy.yaml_parse")73def test_command_invoked(self, mock_yaml_parse):74"""75Tests that deploy method is invoked when command is run76"""77fake_parameter_overrides = []78fake_tags_dict = {"tagkey1": "tagvalue1"}79fake_tags = [{"Key": "tagkey1", "Value": "tagvalue1"}]80fake_parameters = "some return value"81template_str = "some template"8283with tempfile.NamedTemporaryFile() as handle:84file_path = handle.name8586open_mock = mock.mock_open()87# Patch the file open method to return template string88with mock.patch(89"awscli.customizations.cloudformation.deploy.open",90open_mock(read_data=template_str)) as open_mock:9192fake_template = get_example_template()93mock_yaml_parse.return_value = fake_template9495self.deploy_command.deploy = mock.MagicMock()96self.deploy_command.deploy.return_value = 097self.deploy_command.parse_key_value_arg = mock.Mock()98self.deploy_command.parse_key_value_arg.side_effect = [99fake_parameter_overrides, fake_tags_dict]100self.deploy_command.merge_parameters = mock.MagicMock(101return_value=fake_parameters)102103self.parsed_args.template_file = file_path104result = self.deploy_command._run_main(self.parsed_args,105parsed_globals=self.parsed_globals)106self.assertEqual(0, result)107108open_mock.assert_called_once_with(file_path, "r")109110self.deploy_command.deploy.assert_called_once_with(111mock.ANY,112'some_stack_name',113mock.ANY,114fake_parameters,115None,116not self.parsed_args.no_execute_changeset,117None,118[],119None,120fake_tags,121True,122True123)124125self.deploy_command.parse_key_value_arg.assert_has_calls([126mock.call(127self.parsed_args.parameter_overrides,128"parameter-overrides"129),130mock.call(131self.parsed_args.tags,132"tags"133)134])135136self.deploy_command.merge_parameters.assert_called_once_with(137fake_template, fake_parameter_overrides)138139self.assertEqual(1, mock_yaml_parse.call_count)140141def test_invalid_template_file(self):142self.parsed_args.template_file = "sometemplate"143with self.assertRaises(exceptions.InvalidTemplatePathError):144result = self.deploy_command._run_main(self.parsed_args,145parsed_globals=self.parsed_globals)146147@mock.patch('awscli.customizations.cloudformation.deploy.os.path.isfile')148@mock.patch('awscli.customizations.cloudformation.deploy.yaml_parse')149@mock.patch('awscli.customizations.cloudformation.deploy.os.path.getsize')150def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_parse, mock_isfile):151"""152Tests that large templates are detected prior to deployment153"""154template_str = get_example_template()155156mock_getsize.return_value = 51201157mock_isfile.return_value = True158mock_yaml_parse.return_value = template_str159open_mock = mock.mock_open()160161with mock.patch(162"awscli.customizations.cloudformation.deploy.open",163open_mock(read_data=template_str)) as open_mock:164with self.assertRaises(exceptions.DeployBucketRequiredError):165result = self.deploy_command._run_main(self.parsed_args,166parsed_globals=self.parsed_globals)167168@mock.patch('awscli.customizations.cloudformation.deploy.os.path.isfile')169@mock.patch('awscli.customizations.cloudformation.deploy.yaml_parse')170@mock.patch('awscli.customizations.cloudformation.deploy.os.path.getsize')171@mock.patch('awscli.customizations.cloudformation.deploy.DeployCommand.deploy')172@mock.patch('awscli.customizations.cloudformation.deploy.S3Uploader')173def test_s3_uploader_is_configured_properly(self, s3UploaderMock,174deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile):175"""176Tests that large templates are detected prior to deployment177"""178bucket_name = "mybucket"179template_str = get_example_template()180181mock_getsize.return_value = 1024182mock_isfile.return_value = True183mock_yaml_parse.return_value = template_str184open_mock = mock.mock_open()185186with mock.patch(187"awscli.customizations.cloudformation.deploy.open",188open_mock(read_data=template_str)) as open_mock:189190self.parsed_args.s3_bucket = bucket_name191s3UploaderObject = mock.Mock()192s3UploaderMock.return_value = s3UploaderObject193194result = self.deploy_command._run_main(self.parsed_args,195parsed_globals=self.parsed_globals)196197self.deploy_command.deploy.assert_called_once_with(198mock.ANY,199self.parsed_args.stack_name,200mock.ANY,201mock.ANY,202None,203not self.parsed_args.no_execute_changeset,204None,205[],206s3UploaderObject,207[{"Key": "tagkey1", "Value": "tagvalue1"}],208True,209True210)211212s3UploaderMock.assert_called_once_with(mock.ANY,213bucket_name,214self.parsed_args.s3_prefix,215self.parsed_args.kms_key_id,216self.parsed_args.force_upload)217218def test_deploy_success(self):219"""220Tests that we call the deploy command221"""222223stack_name = "stack_name"224changeset_id = "some changeset"225parameters = ["a", "b"]226template = "cloudformation template"227capabilities = ["foo", "bar"]228execute_changeset = True229changeset_type = "CREATE"230role_arn = "arn:aws:iam::1234567890:role"231notification_arns = ["arn:aws:sns:region:1234567890:notify"]232s3_uploader = None233tags = [{"Key":"key1", "Value": "val1"}]234235# Set the mock to return this fake changeset_id236self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type)237238rc = self.deploy_command.deploy(self.deployer,239stack_name,240template,241parameters,242capabilities,243execute_changeset,244role_arn,245notification_arns,246s3_uploader,247tags)248self.assertEqual(rc, 0)249250251self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,252cfn_template=template,253parameter_values=parameters,254capabilities=capabilities,255role_arn=role_arn,256notification_arns=notification_arns,257s3_uploader=s3_uploader,258tags=tags)259260# since execute_changeset is set to True, deploy() will execute changeset261self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name, False)262self.deployer.wait_for_execute.assert_called_once_with(stack_name, changeset_type)263264265def test_deploy_no_execute(self):266stack_name = "stack_name"267changeset_id = "some changeset"268parameters = ["a", "b"]269template = "cloudformation template"270capabilities = ["foo", "bar"]271execute_changeset = False272role_arn = "arn:aws:iam::1234567890:role"273notification_arns = ["arn:aws:sns:region:1234567890:notify"]274s3_uploader = None275tags = [{"Key":"key1", "Value": "val1"}]276277278self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE")279rc = self.deploy_command.deploy(self.deployer,280stack_name,281template,282parameters,283capabilities,284execute_changeset,285role_arn,286notification_arns,287s3_uploader,288tags)289self.assertEqual(rc, 0)290291self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,292cfn_template=template,293parameter_values=parameters,294capabilities=capabilities,295role_arn=role_arn,296notification_arns=notification_arns,297s3_uploader=s3_uploader,298tags=tags)299300# since execute_changeset is set to True, deploy() will execute changeset301self.deployer.execute_changeset.assert_not_called()302self.deployer.wait_for_execute.assert_not_called()303304def test_deploy_raise_exception(self):305stack_name = "stack_name"306changeset_id = "some changeset"307parameters = ["a", "b"]308template = "cloudformation template"309capabilities = ["foo", "bar"]310execute_changeset = True311role_arn = "arn:aws:iam::1234567890:role"312notification_arns = ["arn:aws:sns:region:1234567890:notify"]313s3_uploader = None314tags = [{"Key":"key1", "Value": "val1"}]315316self.deployer.wait_for_execute.side_effect = RuntimeError("Some error")317with self.assertRaises(RuntimeError):318self.deploy_command.deploy(self.deployer,319stack_name,320template,321parameters,322capabilities,323execute_changeset,324role_arn,325notification_arns,326s3_uploader,327tags)328329def test_deploy_raises_exception_on_empty_changeset(self):330stack_name = "stack_name"331parameters = ["a", "b"]332template = "cloudformation template"333capabilities = ["foo", "bar"]334execute_changeset = True335role_arn = "arn:aws:iam::1234567890:role"336notification_arns = ["arn:aws:sns:region:1234567890:notify"]337tags = []338339empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)340changeset_func = self.deployer.create_and_wait_for_changeset341changeset_func.side_effect = empty_changeset342with self.assertRaises(exceptions.ChangeEmptyError):343self.deploy_command.deploy(344self.deployer, stack_name, template, parameters, capabilities,345execute_changeset, role_arn, notification_arns,346None, tags)347348def test_deploy_does_not_raise_exception_on_empty_changeset(self):349stack_name = "stack_name"350parameters = ["a", "b"]351template = "cloudformation template"352capabilities = ["foo", "bar"]353execute_changeset = True354role_arn = "arn:aws:iam::1234567890:role"355notification_arns = ["arn:aws:sns:region:1234567890:notify"]356357empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)358changeset_func = self.deployer.create_and_wait_for_changeset359changeset_func.side_effect = empty_changeset360self.deploy_command.deploy(361self.deployer, stack_name, template, parameters, capabilities,362execute_changeset, role_arn, notification_arns,363s3_uploader=None, tags=[],364fail_on_empty_changeset=False365)366367def test_parse_key_value_arg_success(self):368"""369Tests that we can parse parameter arguments provided in proper format370Expected format: ["Key=Value", "Key=Value"]371:return:372"""373argname = "parameter-overrides"374data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}']375output = {"Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}'}376377result = self.deploy_command.parse_key_value_arg(data, argname)378self.assertEqual(result, output)379380# Empty input should return empty output381result = self.deploy_command.parse_key_value_arg([], argname)382self.assertEqual(result, {})383384def test_parse_key_value_arg_invalid_input(self):385# non-list input386argname = "parameter-overrides"387with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):388self.deploy_command.parse_key_value_arg("hello=world", argname)389390# missing equal to sign391with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):392self.deploy_command.parse_key_value_arg(["hello world"], argname)393394def test_merge_parameters_success(self):395"""396Tests that we can merge parameters specified in CloudFormation template397with override values specified as commandline arguments398"""399template = {400"Parameters": {401"Key1": {"Type": "String"},402"Key2": {"Type": "String"},403"Key3": "Something",404"Key4": {"Type": "Number"},405"KeyWithDefaultValue": {"Type": "String", "Default": "something"},406"KeyWithDefaultValueButOverridden": {"Type": "String", "Default": "something"}407}408}409overrides = {410"Key1": "Value1",411"Key3": "Value3",412"KeyWithDefaultValueButOverridden": "Value4"413}414415expected_result = [416# Overridden values417{"ParameterKey": "Key1", "ParameterValue": "Value1"},418{"ParameterKey": "Key3", "ParameterValue": "Value3"},419420# Parameter contains default value, but overridden with new value421{"ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4"},422423# non-overridden values424{"ParameterKey": "Key2", "UsePreviousValue": True},425{"ParameterKey": "Key4", "UsePreviousValue": True},426427# Parameter with default value but NOT overridden.428# Use previous value, but this gets removed later when we are creating stack for first time429{"ParameterKey": "KeyWithDefaultValue", "UsePreviousValue": True},430]431432self.assertItemsEqual(433self.deploy_command.merge_parameters(template, overrides),434expected_result435)436437def test_merge_parameters_success_nothing_to_override(self):438"""439Tests that we can merge parameters specified in CloudFormation template440with override values specified as commandline arguments441"""442template = {443"Parameters": {444"Key1": {"Type": "String"}, "Key2": {"Type": "String"},445"Key3": "Something", "Key4": {"Type": "Number"},446}447}448overrides = {449# Key5 is not in the template. We will simply skip this key450"Key5": "Value5"451}452453expected_result = [454{"ParameterKey": "Key1", "UsePreviousValue": True},455{"ParameterKey": "Key2", "UsePreviousValue": True},456{"ParameterKey": "Key3", "UsePreviousValue": True},457{"ParameterKey": "Key4", "UsePreviousValue": True},458]459460self.assertItemsEqual(461self.deploy_command.merge_parameters(template, overrides),462expected_result463)464465# Parameters definition is empty. Nothing to override466result = self.deploy_command.merge_parameters({"Parameters": {}},467overrides)468self.assertEqual(result, [])469470def test_merge_parameters_invalid_input(self):471472# Template does not contain "Parameters" definition473result = self.deploy_command.merge_parameters({}, {"Key": "Value"})474self.assertEqual(result, [])475476# Parameters definition is invalid477result = self.deploy_command.merge_parameters({"Parameters": "foo"},478{"Key": "Value"})479self.assertEqual(result, [])480481482