Path: blob/develop/tests/unit/customizations/cloudformation/test_deploy.py
2624 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,122True,123False124)125126self.deploy_command.parse_key_value_arg.assert_has_calls([127mock.call(128self.parsed_args.parameter_overrides,129"parameter-overrides"130),131mock.call(132self.parsed_args.tags,133"tags"134)135])136137self.deploy_command.merge_parameters.assert_called_once_with(138fake_template, fake_parameter_overrides)139140self.assertEqual(1, mock_yaml_parse.call_count)141142def test_invalid_template_file(self):143self.parsed_args.template_file = "sometemplate"144with self.assertRaises(exceptions.InvalidTemplatePathError):145result = self.deploy_command._run_main(self.parsed_args,146parsed_globals=self.parsed_globals)147148@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')151def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_parse, mock_isfile):152"""153Tests that large templates are detected prior to deployment154"""155template_str = get_example_template()156157mock_getsize.return_value = 51201158mock_isfile.return_value = True159mock_yaml_parse.return_value = template_str160open_mock = mock.mock_open()161162with mock.patch(163"awscli.customizations.cloudformation.deploy.open",164open_mock(read_data=template_str)) as open_mock:165with self.assertRaises(exceptions.DeployBucketRequiredError):166result = self.deploy_command._run_main(self.parsed_args,167parsed_globals=self.parsed_globals)168169@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')174def test_s3_uploader_is_configured_properly(self, s3UploaderMock,175deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile):176"""177Tests that large templates are detected prior to deployment178"""179bucket_name = "mybucket"180template_str = get_example_template()181182mock_getsize.return_value = 1024183mock_isfile.return_value = True184mock_yaml_parse.return_value = template_str185open_mock = mock.mock_open()186187with mock.patch(188"awscli.customizations.cloudformation.deploy.open",189open_mock(read_data=template_str)) as open_mock:190191self.parsed_args.s3_bucket = bucket_name192s3UploaderObject = mock.Mock()193s3UploaderMock.return_value = s3UploaderObject194195result = self.deploy_command._run_main(self.parsed_args,196parsed_globals=self.parsed_globals)197198self.deploy_command.deploy.assert_called_once_with(199mock.ANY,200self.parsed_args.stack_name,201mock.ANY,202mock.ANY,203None,204not self.parsed_args.no_execute_changeset,205None,206[],207s3UploaderObject,208[{"Key": "tagkey1", "Value": "tagvalue1"}],209True,210True,211False212)213214s3UploaderMock.assert_called_once_with(mock.ANY,215bucket_name,216self.parsed_args.s3_prefix,217self.parsed_args.kms_key_id,218self.parsed_args.force_upload)219220def test_deploy_success(self):221"""222Tests that we call the deploy command223"""224225stack_name = "stack_name"226changeset_id = "some changeset"227parameters = ["a", "b"]228template = "cloudformation template"229capabilities = ["foo", "bar"]230execute_changeset = True231changeset_type = "CREATE"232role_arn = "arn:aws:iam::1234567890:role"233notification_arns = ["arn:aws:sns:region:1234567890:notify"]234s3_uploader = None235tags = [{"Key":"key1", "Value": "val1"}]236237# Set the mock to return this fake changeset_id238self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type)239240rc = self.deploy_command.deploy(self.deployer,241stack_name,242template,243parameters,244capabilities,245execute_changeset,246role_arn,247notification_arns,248s3_uploader,249tags)250self.assertEqual(rc, 0)251252253self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,254cfn_template=template,255parameter_values=parameters,256capabilities=capabilities,257role_arn=role_arn,258notification_arns=notification_arns,259s3_uploader=s3_uploader,260tags=tags)261262# since execute_changeset is set to True, deploy() will execute changeset263self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name, False)264self.deployer.wait_for_execute.assert_called_once_with(stack_name, changeset_type)265266267def test_deploy_no_execute(self):268stack_name = "stack_name"269changeset_id = "some changeset"270parameters = ["a", "b"]271template = "cloudformation template"272capabilities = ["foo", "bar"]273execute_changeset = False274role_arn = "arn:aws:iam::1234567890:role"275notification_arns = ["arn:aws:sns:region:1234567890:notify"]276s3_uploader = None277tags = [{"Key":"key1", "Value": "val1"}]278279280self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE")281rc = self.deploy_command.deploy(self.deployer,282stack_name,283template,284parameters,285capabilities,286execute_changeset,287role_arn,288notification_arns,289s3_uploader,290tags)291self.assertEqual(rc, 0)292293self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name,294cfn_template=template,295parameter_values=parameters,296capabilities=capabilities,297role_arn=role_arn,298notification_arns=notification_arns,299s3_uploader=s3_uploader,300tags=tags)301302# since execute_changeset is set to True, deploy() will execute changeset303self.deployer.execute_changeset.assert_not_called()304self.deployer.wait_for_execute.assert_not_called()305306def test_deploy_raise_exception(self):307stack_name = "stack_name"308changeset_id = "some changeset"309parameters = ["a", "b"]310template = "cloudformation template"311capabilities = ["foo", "bar"]312execute_changeset = True313role_arn = "arn:aws:iam::1234567890:role"314notification_arns = ["arn:aws:sns:region:1234567890:notify"]315s3_uploader = None316tags = [{"Key":"key1", "Value": "val1"}]317318self.deployer.wait_for_execute.side_effect = RuntimeError("Some error")319with self.assertRaises(RuntimeError):320self.deploy_command.deploy(self.deployer,321stack_name,322template,323parameters,324capabilities,325execute_changeset,326role_arn,327notification_arns,328s3_uploader,329tags)330331def test_deploy_raises_exception_on_empty_changeset(self):332stack_name = "stack_name"333parameters = ["a", "b"]334template = "cloudformation template"335capabilities = ["foo", "bar"]336execute_changeset = True337role_arn = "arn:aws:iam::1234567890:role"338notification_arns = ["arn:aws:sns:region:1234567890:notify"]339tags = []340341empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)342changeset_func = self.deployer.create_and_wait_for_changeset343changeset_func.side_effect = empty_changeset344with self.assertRaises(exceptions.ChangeEmptyError):345self.deploy_command.deploy(346self.deployer, stack_name, template, parameters, capabilities,347execute_changeset, role_arn, notification_arns,348None, tags)349350def test_deploy_does_not_raise_exception_on_empty_changeset(self):351stack_name = "stack_name"352parameters = ["a", "b"]353template = "cloudformation template"354capabilities = ["foo", "bar"]355execute_changeset = True356role_arn = "arn:aws:iam::1234567890:role"357notification_arns = ["arn:aws:sns:region:1234567890:notify"]358359empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name)360changeset_func = self.deployer.create_and_wait_for_changeset361changeset_func.side_effect = empty_changeset362self.deploy_command.deploy(363self.deployer, stack_name, template, parameters, capabilities,364execute_changeset, role_arn, notification_arns,365s3_uploader=None, tags=[],366fail_on_empty_changeset=False367)368369def test_parse_key_value_arg_success(self):370"""371Tests that we can parse parameter arguments provided in proper format372Expected format: ["Key=Value", "Key=Value"]373:return:374"""375argname = "parameter-overrides"376data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}']377output = {"Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}'}378379result = self.deploy_command.parse_key_value_arg(data, argname)380self.assertEqual(result, output)381382# Empty input should return empty output383result = self.deploy_command.parse_key_value_arg([], argname)384self.assertEqual(result, {})385386def test_parse_key_value_arg_invalid_input(self):387# non-list input388argname = "parameter-overrides"389with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):390self.deploy_command.parse_key_value_arg("hello=world", argname)391392# missing equal to sign393with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError):394self.deploy_command.parse_key_value_arg(["hello world"], argname)395396def test_merge_parameters_success(self):397"""398Tests that we can merge parameters specified in CloudFormation template399with override values specified as commandline arguments400"""401template = {402"Parameters": {403"Key1": {"Type": "String"},404"Key2": {"Type": "String"},405"Key3": "Something",406"Key4": {"Type": "Number"},407"KeyWithDefaultValue": {"Type": "String", "Default": "something"},408"KeyWithDefaultValueButOverridden": {"Type": "String", "Default": "something"}409}410}411overrides = {412"Key1": "Value1",413"Key3": "Value3",414"KeyWithDefaultValueButOverridden": "Value4"415}416417expected_result = [418# Overridden values419{"ParameterKey": "Key1", "ParameterValue": "Value1"},420{"ParameterKey": "Key3", "ParameterValue": "Value3"},421422# Parameter contains default value, but overridden with new value423{"ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4"},424425# non-overridden values426{"ParameterKey": "Key2", "UsePreviousValue": True},427{"ParameterKey": "Key4", "UsePreviousValue": True},428429# Parameter with default value but NOT overridden.430# Use previous value, but this gets removed later when we are creating stack for first time431{"ParameterKey": "KeyWithDefaultValue", "UsePreviousValue": True},432]433434self.assertItemsEqual(435self.deploy_command.merge_parameters(template, overrides),436expected_result437)438439def test_merge_parameters_success_nothing_to_override(self):440"""441Tests that we can merge parameters specified in CloudFormation template442with override values specified as commandline arguments443"""444template = {445"Parameters": {446"Key1": {"Type": "String"}, "Key2": {"Type": "String"},447"Key3": "Something", "Key4": {"Type": "Number"},448}449}450overrides = {451# Key5 is not in the template. We will simply skip this key452"Key5": "Value5"453}454455expected_result = [456{"ParameterKey": "Key1", "UsePreviousValue": True},457{"ParameterKey": "Key2", "UsePreviousValue": True},458{"ParameterKey": "Key3", "UsePreviousValue": True},459{"ParameterKey": "Key4", "UsePreviousValue": True},460]461462self.assertItemsEqual(463self.deploy_command.merge_parameters(template, overrides),464expected_result465)466467# Parameters definition is empty. Nothing to override468result = self.deploy_command.merge_parameters({"Parameters": {}},469overrides)470self.assertEqual(result, [])471472def test_merge_parameters_invalid_input(self):473474# Template does not contain "Parameters" definition475result = self.deploy_command.merge_parameters({}, {"Key": "Value"})476self.assertEqual(result, [])477478# Parameters definition is invalid479result = self.deploy_command.merge_parameters({"Parameters": "foo"},480{"Key": "Value"})481self.assertEqual(result, [])482483484