class TestDeployCommand(unittest.TestCase): def setUp(self): self.session = mock.Mock() self.session.get_scoped_config.return_value = {} self.parsed_args = FakeArgs( template_file='./foo', stack_name="some_stack_name", parameter_overrides=["Key1=Value1", "Key2=Value2"], no_execute_changeset=False, execute_changeset=True, capabilities=None, role_arn=None, notification_arns=[], fail_on_empty_changeset=True, s3_bucket=None, s3_prefix="some prefix", kms_key_id="some kms key id", force_upload=True, tags=["tagkey1=tagvalue1"]) self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None, verify_ssl=None) self.deploy_command = DeployCommand(self.session) self.deployer = Deployer(Mock()) self.deployer.create_and_wait_for_changeset = Mock() self.deployer.execute_changeset = Mock() self.deployer.wait_for_execute = Mock() @patch("awscli.customizations.cloudformation.deploy.yaml_parse") def test_command_invoked(self, mock_yaml_parse): """ Tests that deploy method is invoked when command is run """ fake_parameter_overrides = [] fake_tags_dict = {"tagkey1": "tagvalue1"} fake_tags = [{"Key": "tagkey1", "Value": "tagvalue1"}] fake_parameters = "some return value" template_str = "some template" with tempfile.NamedTemporaryFile() as handle: file_path = handle.name open_mock = mock.mock_open() # Patch the file open method to return template string with patch("awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: fake_template = get_example_template() mock_yaml_parse.return_value = fake_template self.deploy_command.deploy = MagicMock() self.deploy_command.deploy.return_value = 0 self.deploy_command.parse_key_value_arg = Mock() self.deploy_command.parse_key_value_arg.side_effect = [ fake_parameter_overrides, fake_tags_dict ] self.deploy_command.merge_parameters = MagicMock( return_value=fake_parameters) self.parsed_args.template_file = file_path result = self.deploy_command._run_main( self.parsed_args, parsed_globals=self.parsed_globals) self.assertEquals(0, result) open_mock.assert_called_once_with(file_path, "r") self.deploy_command.deploy.assert_called_once_with( mock.ANY, 'some_stack_name', mock.ANY, fake_parameters, None, not self.parsed_args.no_execute_changeset, None, [], None, fake_tags, True) self.deploy_command.parse_key_value_arg.assert_has_calls([ call(self.parsed_args.parameter_overrides, "parameter-overrides"), call(self.parsed_args.tags, "tags") ]) self.deploy_command.merge_parameters.assert_called_once_with( fake_template, fake_parameter_overrides) self.assertEquals(1, mock_yaml_parse.call_count) def test_invalid_template_file(self): self.parsed_args.template_file = "sometemplate" with self.assertRaises(exceptions.InvalidTemplatePathError): result = self.deploy_command._run_main( self.parsed_args, parsed_globals=self.parsed_globals) @patch('awscli.customizations.cloudformation.deploy.os.path.isfile') @patch('awscli.customizations.cloudformation.deploy.yaml_parse') @patch('awscli.customizations.cloudformation.deploy.os.path.getsize') def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_parse, mock_isfile): """ Tests that large templates are detected prior to deployment """ template_str = get_example_template() mock_getsize.return_value = 51201 mock_isfile.return_value = True mock_yaml_parse.return_value = template_str open_mock = mock.mock_open() with patch("awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: with self.assertRaises(exceptions.DeployBucketRequiredError): result = self.deploy_command._run_main( self.parsed_args, parsed_globals=self.parsed_globals) @patch('awscli.customizations.cloudformation.deploy.os.path.isfile') @patch('awscli.customizations.cloudformation.deploy.yaml_parse') @patch('awscli.customizations.cloudformation.deploy.os.path.getsize') @patch('awscli.customizations.cloudformation.deploy.DeployCommand.deploy') @patch('awscli.customizations.cloudformation.deploy.S3Uploader') def test_s3_uploader_is_configured_properly(self, s3UploaderMock, deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile): """ Tests that large templates are detected prior to deployment """ bucket_name = "mybucket" template_str = get_example_template() mock_getsize.return_value = 1024 mock_isfile.return_value = True mock_yaml_parse.return_value = template_str open_mock = mock.mock_open() with patch("awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: self.parsed_args.s3_bucket = bucket_name s3UploaderObject = Mock() s3UploaderMock.return_value = s3UploaderObject result = self.deploy_command._run_main( self.parsed_args, parsed_globals=self.parsed_globals) self.deploy_command.deploy.assert_called_once_with( mock.ANY, self.parsed_args.stack_name, mock.ANY, mock.ANY, None, not self.parsed_args.no_execute_changeset, None, [], s3UploaderObject, [{ "Key": "tagkey1", "Value": "tagvalue1" }], True) s3UploaderMock.assert_called_once_with( mock.ANY, bucket_name, self.parsed_args.s3_prefix, self.parsed_args.kms_key_id, self.parsed_args.force_upload) def test_deploy_success(self): """ Tests that we call the deploy command """ stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True changeset_type = "CREATE" role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None tags = [{"Key": "key1", "Value": "val1"}] # Set the mock to return this fake changeset_id self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult( changeset_id, changeset_type) rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with( stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities, role_arn=role_arn, notification_arns=notification_arns, s3_uploader=s3_uploader, tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_called_once_with( changeset_id, stack_name) self.deployer.wait_for_execute.assert_called_once_with( stack_name, changeset_type) def test_deploy_no_execute(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = False role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None tags = [{"Key": "key1", "Value": "val1"}] self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult( changeset_id, "CREATE") rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with( stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities, role_arn=role_arn, notification_arns=notification_arns, s3_uploader=s3_uploader, tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_not_called() self.deployer.wait_for_execute.assert_not_called() def test_deploy_raise_exception(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None tags = [{"Key": "key1", "Value": "val1"}] self.deployer.wait_for_execute.side_effect = RuntimeError("Some error") with self.assertRaises(RuntimeError): self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags) def test_deploy_raises_exception_on_empty_changeset(self): stack_name = "stack_name" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] tags = [] empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name) changeset_func = self.deployer.create_and_wait_for_changeset changeset_func.side_effect = empty_changeset with self.assertRaises(exceptions.ChangeEmptyError): self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, None, tags) def test_deploy_does_not_raise_exception_on_empty_changeset(self): stack_name = "stack_name" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name) changeset_func = self.deployer.create_and_wait_for_changeset changeset_func.side_effect = empty_changeset self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader=None, tags=[], fail_on_empty_changeset=False) def test_parse_key_value_arg_success(self): """ Tests that we can parse parameter arguments provided in proper format Expected format: ["Key=Value", "Key=Value"] :return: """ argname = "parameter-overrides" data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}'] output = { "Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}' } result = self.deploy_command.parse_key_value_arg(data, argname) self.assertEqual(result, output) # Empty input should return empty output result = self.deploy_command.parse_key_value_arg([], argname) self.assertEqual(result, {}) def test_parse_key_value_arg_invalid_input(self): # non-list input argname = "parameter-overrides" with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError): self.deploy_command.parse_key_value_arg("hello=world", argname) # missing equal to sign with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError): self.deploy_command.parse_key_value_arg(["hello world"], argname) def test_merge_parameters_success(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": { "Type": "String" }, "Key2": { "Type": "String" }, "Key3": "Something", "Key4": { "Type": "Number" }, "KeyWithDefaultValue": { "Type": "String", "Default": "something" }, "KeyWithDefaultValueButOverridden": { "Type": "String", "Default": "something" } } } overrides = { "Key1": "Value1", "Key3": "Value3", "KeyWithDefaultValueButOverridden": "Value4" } expected_result = [ # Overriden values { "ParameterKey": "Key1", "ParameterValue": "Value1" }, { "ParameterKey": "Key3", "ParameterValue": "Value3" }, # Parameter contains default value, but overridden with new value { "ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4" }, # non-overriden values { "ParameterKey": "Key2", "UsePreviousValue": True }, { "ParameterKey": "Key4", "UsePreviousValue": True }, # Parameter with default value but NOT overridden. # Use previous value, but this gets removed later when we are creating stack for first time { "ParameterKey": "KeyWithDefaultValue", "UsePreviousValue": True }, ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result) def test_merge_parameters_success_nothing_to_override(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": { "Type": "String" }, "Key2": { "Type": "String" }, "Key3": "Something", "Key4": { "Type": "Number" }, } } overrides = { # Key5 is not in the template. We will simply skip this key "Key5": "Value5" } expected_result = [ { "ParameterKey": "Key1", "UsePreviousValue": True }, { "ParameterKey": "Key2", "UsePreviousValue": True }, { "ParameterKey": "Key3", "UsePreviousValue": True }, { "ParameterKey": "Key4", "UsePreviousValue": True }, ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result) # Parameters definition is empty. Nothing to override result = self.deploy_command.merge_parameters({"Parameters": {}}, overrides) self.assertEqual(result, []) def test_merge_parameters_invalid_input(self): # Template does not contain "Parameters" definition result = self.deploy_command.merge_parameters({}, {"Key": "Value"}) self.assertEqual(result, []) # Parameters definition is invalid result = self.deploy_command.merge_parameters({"Parameters": "foo"}, {"Key": "Value"}) self.assertEqual(result, [])
class TestDeployCommand(unittest.TestCase): def setUp(self): self.session = mock.Mock() self.session.get_scoped_config.return_value = {} self.parsed_args = FakeArgs(template_file='./foo', stack_name="some_stack_name", parameter_overrides=["Key1=Value1", "Key2=Value2"], no_execute_changeset=False, execute_changeset=True, capabilities=None, role_arn=None, notification_arns=[], fail_on_empty_changeset=True, s3_bucket=None, s3_prefix="some prefix", kms_key_id="some kms key id", force_upload=True, tags=["tagkey1=tagvalue1"]) self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None, verify_ssl=None) self.deploy_command = DeployCommand(self.session) self.deployer = Deployer(Mock()) self.deployer.create_and_wait_for_changeset = Mock() self.deployer.execute_changeset = Mock() self.deployer.wait_for_execute = Mock() @patch("awscli.customizations.cloudformation.deploy.yaml_parse") def test_command_invoked(self, mock_yaml_parse): """ Tests that deploy method is invoked when command is run """ fake_parameter_overrides = [] fake_tags_dict = {"tagkey1": "tagvalue1"} fake_tags = [{"Key": "tagkey1", "Value": "tagvalue1"}] fake_parameters = "some return value" template_str = "some template" with tempfile.NamedTemporaryFile() as handle: file_path = handle.name open_mock = mock.mock_open() # Patch the file open method to return template string with patch( "awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: fake_template = get_example_template() mock_yaml_parse.return_value = fake_template self.deploy_command.deploy = MagicMock() self.deploy_command.deploy.return_value = 0 self.deploy_command.parse_key_value_arg = Mock() self.deploy_command.parse_key_value_arg.side_effect = [ fake_parameter_overrides, fake_tags_dict] self.deploy_command.merge_parameters = MagicMock( return_value=fake_parameters) self.parsed_args.template_file = file_path result = self.deploy_command._run_main(self.parsed_args, parsed_globals=self.parsed_globals) self.assertEquals(0, result) open_mock.assert_called_once_with(file_path, "r") self.deploy_command.deploy.assert_called_once_with( mock.ANY, 'some_stack_name', mock.ANY, fake_parameters, None, not self.parsed_args.no_execute_changeset, None, [], None, fake_tags, True ) self.deploy_command.parse_key_value_arg.assert_has_calls([ call( self.parsed_args.parameter_overrides, "parameter-overrides" ), call( self.parsed_args.tags, "tags" ) ]) self.deploy_command.merge_parameters.assert_called_once_with( fake_template, fake_parameter_overrides) self.assertEquals(1, mock_yaml_parse.call_count) def test_invalid_template_file(self): self.parsed_args.template_file = "sometemplate" with self.assertRaises(exceptions.InvalidTemplatePathError): result = self.deploy_command._run_main(self.parsed_args, parsed_globals=self.parsed_globals) @patch('awscli.customizations.cloudformation.deploy.os.path.isfile') @patch('awscli.customizations.cloudformation.deploy.yaml_parse') @patch('awscli.customizations.cloudformation.deploy.os.path.getsize') def test_s3_upload_required_but_missing_bucket(self, mock_getsize, mock_yaml_parse, mock_isfile): """ Tests that large templates are detected prior to deployment """ template_str = get_example_template() mock_getsize.return_value = 51201 mock_isfile.return_value = True mock_yaml_parse.return_value = template_str open_mock = mock.mock_open() with patch( "awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: with self.assertRaises(exceptions.DeployBucketRequiredError): result = self.deploy_command._run_main(self.parsed_args, parsed_globals=self.parsed_globals) @patch('awscli.customizations.cloudformation.deploy.os.path.isfile') @patch('awscli.customizations.cloudformation.deploy.yaml_parse') @patch('awscli.customizations.cloudformation.deploy.os.path.getsize') @patch('awscli.customizations.cloudformation.deploy.DeployCommand.deploy') @patch('awscli.customizations.cloudformation.deploy.S3Uploader') def test_s3_uploader_is_configured_properly(self, s3UploaderMock, deploy_method_mock, mock_getsize, mock_yaml_parse, mock_isfile): """ Tests that large templates are detected prior to deployment """ bucket_name = "mybucket" template_str = get_example_template() mock_getsize.return_value = 1024 mock_isfile.return_value = True mock_yaml_parse.return_value = template_str open_mock = mock.mock_open() with patch( "awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: self.parsed_args.s3_bucket = bucket_name s3UploaderObject = Mock() s3UploaderMock.return_value = s3UploaderObject result = self.deploy_command._run_main(self.parsed_args, parsed_globals=self.parsed_globals) self.deploy_command.deploy.assert_called_once_with( mock.ANY, self.parsed_args.stack_name, mock.ANY, mock.ANY, None, not self.parsed_args.no_execute_changeset, None, [], s3UploaderObject, [{"Key": "tagkey1", "Value": "tagvalue1"}], True ) s3UploaderMock.assert_called_once_with(mock.ANY, bucket_name, mock.ANY, self.parsed_args.s3_prefix, self.parsed_args.kms_key_id, self.parsed_args.force_upload) def test_deploy_success(self): """ Tests that we call the deploy command """ stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True changeset_type = "CREATE" role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None tags = [{"Key":"key1", "Value": "val1"}] # Set the mock to return this fake changeset_id self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities, role_arn=role_arn, notification_arns=notification_arns, s3_uploader=s3_uploader, tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name) self.deployer.wait_for_execute.assert_called_once_with(stack_name, changeset_type) def test_deploy_no_execute(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = False role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None tags = [{"Key":"key1", "Value": "val1"}] self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE") rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities, role_arn=role_arn, notification_arns=notification_arns, s3_uploader=s3_uploader, tags=tags) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_not_called() self.deployer.wait_for_execute.assert_not_called() def test_deploy_raise_exception(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] s3_uploader = None tags = [{"Key":"key1", "Value": "val1"}] self.deployer.wait_for_execute.side_effect = RuntimeError("Some error") with self.assertRaises(RuntimeError): self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader, tags) def test_deploy_raises_exception_on_empty_changeset(self): stack_name = "stack_name" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] tags = [] empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name) changeset_func = self.deployer.create_and_wait_for_changeset changeset_func.side_effect = empty_changeset with self.assertRaises(exceptions.ChangeEmptyError): self.deploy_command.deploy( self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, None, tags) def test_deploy_does_not_raise_exception_on_empty_changeset(self): stack_name = "stack_name" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True role_arn = "arn:aws:iam::1234567890:role" notification_arns = ["arn:aws:sns:region:1234567890:notify"] empty_changeset = exceptions.ChangeEmptyError(stack_name=stack_name) changeset_func = self.deployer.create_and_wait_for_changeset changeset_func.side_effect = empty_changeset self.deploy_command.deploy( self.deployer, stack_name, template, parameters, capabilities, execute_changeset, role_arn, notification_arns, s3_uploader=None, tags=[], fail_on_empty_changeset=False ) def test_parse_key_value_arg_success(self): """ Tests that we can parse parameter arguments provided in proper format Expected format: ["Key=Value", "Key=Value"] :return: """ argname = "parameter-overrides" data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}'] output = {"Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}'} result = self.deploy_command.parse_key_value_arg(data, argname) self.assertEqual(result, output) # Empty input should return empty output result = self.deploy_command.parse_key_value_arg([], argname) self.assertEqual(result, {}) def test_parse_key_value_arg_invalid_input(self): # non-list input argname = "parameter-overrides" with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError): self.deploy_command.parse_key_value_arg("hello=world", argname) # missing equal to sign with self.assertRaises(exceptions.InvalidKeyValuePairArgumentError): self.deploy_command.parse_key_value_arg(["hello world"], argname) def test_merge_parameters_success(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": {"Type": "String"}, "Key2": {"Type": "String"}, "Key3": "Something", "Key4": {"Type": "Number"}, "KeyWithDefaultValue": {"Type": "String", "Default": "something"}, "KeyWithDefaultValueButOverridden": {"Type": "String", "Default": "something"} } } overrides = { "Key1": "Value1", "Key3": "Value3", "KeyWithDefaultValueButOverridden": "Value4" } expected_result = [ # Overriden values {"ParameterKey": "Key1", "ParameterValue": "Value1"}, {"ParameterKey": "Key3", "ParameterValue": "Value3"}, # Parameter contains default value, but overridden with new value {"ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4"}, # non-overriden values {"ParameterKey": "Key2", "UsePreviousValue": True}, {"ParameterKey": "Key4", "UsePreviousValue": True}, # Parameter with default value but NOT overridden. # Use previous value, but this gets removed later when we are creating stack for first time {"ParameterKey": "KeyWithDefaultValue", "UsePreviousValue": True}, ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result ) def test_merge_parameters_success_nothing_to_override(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": {"Type": "String"}, "Key2": {"Type": "String"}, "Key3": "Something", "Key4": {"Type": "Number"}, } } overrides = { # Key5 is not in the template. We will simply skip this key "Key5": "Value5" } expected_result = [ {"ParameterKey": "Key1", "UsePreviousValue": True}, {"ParameterKey": "Key2", "UsePreviousValue": True}, {"ParameterKey": "Key3", "UsePreviousValue": True}, {"ParameterKey": "Key4", "UsePreviousValue": True}, ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result ) # Parameters definition is empty. Nothing to override result = self.deploy_command.merge_parameters({"Parameters": {}}, overrides) self.assertEqual(result, []) def test_merge_parameters_invalid_input(self): # Template does not contain "Parameters" definition result = self.deploy_command.merge_parameters({}, {"Key": "Value"}) self.assertEqual(result, []) # Parameters definition is invalid result = self.deploy_command.merge_parameters({"Parameters": "foo"}, {"Key": "Value"}) self.assertEqual(result, [])
class TestDeployCommand(unittest.TestCase): def setUp(self): self.session = mock.Mock() self.session.get_scoped_config.return_value = {} self.parsed_args = FakeArgs(template_file='./foo', stack_name="some_stack_name", parameter_overrides=["Key1=Value1", "Key2=Value2"], no_execute_changeset=False, execute_changeset=True, capabilities=None) self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None, verify_ssl=None) self.deploy_command = DeployCommand(self.session) self.deployer = Deployer(Mock()) self.deployer.create_and_wait_for_changeset = Mock() self.deployer.execute_changeset = Mock() self.deployer.wait_for_execute = Mock() @patch("awscli.customizations.cloudformation.deploy.yaml_parse") def test_command_invoked(self, mock_yaml_parse): """ Tests that deploy method is invoked when command is run """ fake_parameter_overrides = [] fake_parameters = "some return value" template_str = "some template" with tempfile.NamedTemporaryFile() as handle: file_path = handle.name open_mock = mock.mock_open() # Patch the file open method to return template string with patch( "awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: fake_template = get_example_template() mock_yaml_parse.return_value = fake_template self.deploy_command.deploy = MagicMock() self.deploy_command.deploy.return_value = 0 self.deploy_command.parse_parameter_arg = MagicMock( return_value=fake_parameter_overrides) self.deploy_command.merge_parameters = MagicMock( return_value=fake_parameters) self.parsed_args.template_file = file_path result = self.deploy_command._run_main(self.parsed_args, parsed_globals=self.parsed_globals) self.assertEquals(0, result) open_mock.assert_called_once_with(file_path, "r") self.deploy_command.deploy.assert_called_once_with( mock.ANY, self.parsed_args.stack_name, mock.ANY, fake_parameters, None, not self.parsed_args.no_execute_changeset) self.deploy_command.parse_parameter_arg.assert_called_once_with( self.parsed_args.parameter_overrides) self.deploy_command.merge_parameters.assert_called_once_with( fake_template, fake_parameter_overrides) self.assertEquals(1, mock_yaml_parse.call_count) def test_invalid_template_file(self): self.parsed_args.template_file = "sometemplate" with self.assertRaises(exceptions.InvalidTemplatePathError): result = self.deploy_command._run_main(self.parsed_args, parsed_globals=self.parsed_globals) def test_deploy_success(self): """ Tests that we call the deploy command """ stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True changeset_type = "CREATE" # Set the mock to return this fake changeset_id self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, changeset_type) rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_called_once_with(changeset_id, stack_name) self.deployer.wait_for_execute.assert_called_once_with(stack_name, changeset_type) def test_deploy_no_execute(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = False self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult(changeset_id, "CREATE") rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with(stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_not_called() self.deployer.wait_for_execute.assert_not_called() def test_deploy_raise_exception(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True self.deployer.wait_for_execute.side_effect = RuntimeError("Some error") with self.assertRaises(RuntimeError): self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset) def test_parse_parameter_arg_success(self): """ Tests that we can parse parameter arguments provided in proper format Expected format: ["Key=Value", "Key=Value"] :return: """ data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}'] output = {"Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}'} result = self.deploy_command.parse_parameter_arg(data) self.assertEqual(result, output) # Empty input should return empty output result = self.deploy_command.parse_parameter_arg([]) self.assertEqual(result, {}) def test_parse_parameter_arg_invalid_input(self): # non-list input with self.assertRaises(exceptions.InvalidParameterOverrideArgumentError): self.deploy_command.parse_parameter_arg("hello=world") # missing equal to sign with self.assertRaises(exceptions.InvalidParameterOverrideArgumentError): self.deploy_command.parse_parameter_arg(["hello world"]) def test_merge_parameters_success(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": {"Type": "String"}, "Key2": {"Type": "String"}, "Key3": "Something", "Key4": {"Type": "Number"}, "KeyWithDefaultValue": {"Type": "String", "Default": "something"}, "KeyWithDefaultValueButOverridden": {"Type": "String", "Default": "something"} } } overrides = { "Key1": "Value1", "Key3": "Value3", "KeyWithDefaultValueButOverridden": "Value4" } expected_result = [ # Overriden values {"ParameterKey": "Key1", "ParameterValue": "Value1"}, {"ParameterKey": "Key3", "ParameterValue": "Value3"}, # Parameter contains default value, but overridden with new value {"ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4"}, # non-overriden values {"ParameterKey": "Key2", "UsePreviousValue": True}, {"ParameterKey": "Key4", "UsePreviousValue": True}, # Parameter with default value is NOT sent to CloudFormation ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result ) def test_merge_parameters_success_nothing_to_override(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": {"Type": "String"}, "Key2": {"Type": "String"}, "Key3": "Something", "Key4": {"Type": "Number"}, } } overrides = { # Key5 is not in the template. We will simply skip this key "Key5": "Value5" } expected_result = [ {"ParameterKey": "Key1", "UsePreviousValue": True}, {"ParameterKey": "Key2", "UsePreviousValue": True}, {"ParameterKey": "Key3", "UsePreviousValue": True}, {"ParameterKey": "Key4", "UsePreviousValue": True}, ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result ) # Parameters definition is empty. Nothing to override result = self.deploy_command.merge_parameters({"Parameters": {}}, overrides) self.assertEqual(result, []) def test_merge_parameters_invalid_input(self): # Template does not contain "Parameters" definition result = self.deploy_command.merge_parameters({}, {"Key": "Value"}) self.assertEqual(result, []) # Parameters definition is invalid result = self.deploy_command.merge_parameters({"Parameters": "foo"}, {"Key": "Value"}) self.assertEqual(result, [])
class TestDeployCommand(unittest.TestCase): def setUp(self): self.session = mock.Mock() self.session.get_scoped_config.return_value = {} self.parsed_args = FakeArgs( template_file='./foo', stack_name="some_stack_name", parameter_overrides=["Key1=Value1", "Key2=Value2"], no_execute_changeset=False, execute_changeset=True, capabilities=None) self.parsed_globals = FakeArgs(region="us-east-1", endpoint_url=None, verify_ssl=None) self.deploy_command = DeployCommand(self.session) self.deployer = Deployer(Mock()) self.deployer.create_and_wait_for_changeset = Mock() self.deployer.execute_changeset = Mock() self.deployer.wait_for_execute = Mock() @patch("awscli.customizations.cloudformation.deploy.yaml_parse") def test_command_invoked(self, mock_yaml_parse): """ Tests that deploy method is invoked when command is run """ fake_parameter_overrides = [] fake_parameters = "some return value" template_str = "some template" with tempfile.NamedTemporaryFile() as handle: file_path = handle.name open_mock = mock.mock_open() # Patch the file open method to return template string with patch("awscli.customizations.cloudformation.deploy.open", open_mock(read_data=template_str)) as open_mock: fake_template = get_example_template() mock_yaml_parse.return_value = fake_template self.deploy_command.deploy = MagicMock() self.deploy_command.deploy.return_value = 0 self.deploy_command.parse_parameter_arg = MagicMock( return_value=fake_parameter_overrides) self.deploy_command.merge_parameters = MagicMock( return_value=fake_parameters) self.parsed_args.template_file = file_path result = self.deploy_command._run_main( self.parsed_args, parsed_globals=self.parsed_globals) self.assertEquals(0, result) open_mock.assert_called_once_with(file_path, "r") self.deploy_command.deploy.assert_called_once_with( mock.ANY, self.parsed_args.stack_name, mock.ANY, fake_parameters, None, not self.parsed_args.no_execute_changeset) self.deploy_command.parse_parameter_arg.assert_called_once_with( self.parsed_args.parameter_overrides) self.deploy_command.merge_parameters.assert_called_once_with( fake_template, fake_parameter_overrides) self.assertEquals(1, mock_yaml_parse.call_count) def test_invalid_template_file(self): self.parsed_args.template_file = "sometemplate" with self.assertRaises(exceptions.InvalidTemplatePathError): result = self.deploy_command._run_main( self.parsed_args, parsed_globals=self.parsed_globals) def test_deploy_success(self): """ Tests that we call the deploy command """ stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True changeset_type = "CREATE" # Set the mock to return this fake changeset_id self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult( changeset_id, changeset_type) rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with( stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_called_once_with( changeset_id, stack_name) self.deployer.wait_for_execute.assert_called_once_with( stack_name, changeset_type) def test_deploy_no_execute(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = False self.deployer.create_and_wait_for_changeset.return_value = ChangeSetResult( changeset_id, "CREATE") rc = self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset) self.assertEqual(rc, 0) self.deployer.create_and_wait_for_changeset.assert_called_once_with( stack_name=stack_name, cfn_template=template, parameter_values=parameters, capabilities=capabilities) # since execute_changeset is set to True, deploy() will execute changeset self.deployer.execute_changeset.assert_not_called() self.deployer.wait_for_execute.assert_not_called() def test_deploy_raise_exception(self): stack_name = "stack_name" changeset_id = "some changeset" parameters = ["a", "b"] template = "cloudformation template" capabilities = ["foo", "bar"] execute_changeset = True self.deployer.wait_for_execute.side_effect = RuntimeError("Some error") with self.assertRaises(RuntimeError): self.deploy_command.deploy(self.deployer, stack_name, template, parameters, capabilities, execute_changeset) def test_parse_parameter_arg_success(self): """ Tests that we can parse parameter arguments provided in proper format Expected format: ["Key=Value", "Key=Value"] :return: """ data = ["Key1=Value1", 'Key2=[1,2,3]', 'Key3={"a":"val", "b": 2}'] output = { "Key1": "Value1", "Key2": '[1,2,3]', "Key3": '{"a":"val", "b": 2}' } result = self.deploy_command.parse_parameter_arg(data) self.assertEqual(result, output) # Empty input should return empty output result = self.deploy_command.parse_parameter_arg([]) self.assertEqual(result, {}) def test_parse_parameter_arg_invalid_input(self): # non-list input with self.assertRaises( exceptions.InvalidParameterOverrideArgumentError): self.deploy_command.parse_parameter_arg("hello=world") # missing equal to sign with self.assertRaises( exceptions.InvalidParameterOverrideArgumentError): self.deploy_command.parse_parameter_arg(["hello world"]) def test_merge_parameters_success(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": { "Type": "String" }, "Key2": { "Type": "String" }, "Key3": "Something", "Key4": { "Type": "Number" }, "KeyWithDefaultValue": { "Type": "String", "Default": "something" }, "KeyWithDefaultValueButOverridden": { "Type": "String", "Default": "something" } } } overrides = { "Key1": "Value1", "Key3": "Value3", "KeyWithDefaultValueButOverridden": "Value4" } expected_result = [ # Overriden values { "ParameterKey": "Key1", "ParameterValue": "Value1" }, { "ParameterKey": "Key3", "ParameterValue": "Value3" }, # Parameter contains default value, but overridden with new value { "ParameterKey": "KeyWithDefaultValueButOverridden", "ParameterValue": "Value4" }, # non-overriden values { "ParameterKey": "Key2", "UsePreviousValue": True }, { "ParameterKey": "Key4", "UsePreviousValue": True }, # Parameter with default value is NOT sent to CloudFormation ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result) def test_merge_parameters_success_nothing_to_override(self): """ Tests that we can merge parameters specified in CloudFormation template with override values specified as commandline arguments """ template = { "Parameters": { "Key1": { "Type": "String" }, "Key2": { "Type": "String" }, "Key3": "Something", "Key4": { "Type": "Number" }, } } overrides = { # Key5 is not in the template. We will simply skip this key "Key5": "Value5" } expected_result = [ { "ParameterKey": "Key1", "UsePreviousValue": True }, { "ParameterKey": "Key2", "UsePreviousValue": True }, { "ParameterKey": "Key3", "UsePreviousValue": True }, { "ParameterKey": "Key4", "UsePreviousValue": True }, ] self.assertItemsEqual( self.deploy_command.merge_parameters(template, overrides), expected_result) # Parameters definition is empty. Nothing to override result = self.deploy_command.merge_parameters({"Parameters": {}}, overrides) self.assertEqual(result, []) def test_merge_parameters_invalid_input(self): # Template does not contain "Parameters" definition result = self.deploy_command.merge_parameters({}, {"Key": "Value"}) self.assertEqual(result, []) # Parameters definition is invalid result = self.deploy_command.merge_parameters({"Parameters": "foo"}, {"Key": "Value"}) self.assertEqual(result, [])