Example #1
0
class DeployContext:

    MSG_SHOWCASE_CHANGESET = "\nChangeset created successfully. {changeset_id}\n"

    MSG_EXECUTE_SUCCESS = "\nSuccessfully created/updated stack - {stack_name} in {region}\n"

    MSG_CONFIRM_CHANGESET = "Deploy this changeset?"
    MSG_CONFIRM_CHANGESET_HEADER = "\nPreviewing CloudFormation changeset before deployment"

    def __init__(
        self,
        template_file,
        stack_name,
        s3_bucket,
        force_upload,
        s3_prefix,
        kms_key_id,
        parameter_overrides,
        capabilities,
        no_execute_changeset,
        role_arn,
        notification_arns,
        fail_on_empty_changeset,
        tags,
        region,
        profile,
        confirm_changeset,
    ):
        self.template_file = template_file
        self.stack_name = stack_name
        self.s3_bucket = s3_bucket
        self.force_upload = force_upload
        self.s3_prefix = s3_prefix
        self.kms_key_id = kms_key_id
        self.parameter_overrides = parameter_overrides
        self.capabilities = capabilities
        self.no_execute_changeset = no_execute_changeset
        self.role_arn = role_arn
        self.notification_arns = notification_arns
        self.fail_on_empty_changeset = fail_on_empty_changeset
        self.tags = tags
        self.region = region
        self.profile = profile
        self.s3_uploader = None
        self.deployer = None
        self.confirm_changeset = confirm_changeset

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass

    def run(self):

        # Parse parameters
        with open(self.template_file, "r") as handle:
            template_str = handle.read()

        template_dict = yaml_parse(template_str)

        if not isinstance(template_dict, dict):
            raise deploy_exceptions.DeployFailedError(
                stack_name=self.stack_name,
                msg="{} not in required format".format(self.template_file))

        parameters = self.merge_parameters(template_dict,
                                           self.parameter_overrides)

        template_size = os.path.getsize(self.template_file)
        if template_size > 51200 and not self.s3_bucket:
            raise deploy_exceptions.DeployBucketRequiredError()

        session = boto3.Session(
            profile_name=self.profile if self.profile else None)
        cloudformation_client = session.client(
            "cloudformation", region_name=self.region if self.region else None)

        s3_client = None
        if self.s3_bucket:
            s3_client = session.client(
                "s3", region_name=self.region if self.region else None)

            self.s3_uploader = S3Uploader(s3_client, self.s3_bucket,
                                          self.s3_prefix, self.kms_key_id,
                                          self.force_upload)

        self.deployer = Deployer(cloudformation_client)

        region = s3_client._client_config.region_name if s3_client else self.region  # pylint: disable=W0212

        return self.deploy(
            self.stack_name,
            template_str,
            parameters,
            self.capabilities,
            self.no_execute_changeset,
            self.role_arn,
            self.notification_arns,
            self.s3_uploader,
            [{
                "Key": key,
                "Value": value
            } for key, value in self.tags.items()] if self.tags else [],
            region,
            self.fail_on_empty_changeset,
            self.confirm_changeset,
        )

    def deploy(
        self,
        stack_name,
        template_str,
        parameters,
        capabilities,
        no_execute_changeset,
        role_arn,
        notification_arns,
        s3_uploader,
        tags,
        region,
        fail_on_empty_changeset=True,
        confirm_changeset=False,
    ):
        try:
            result, changeset_type = self.deployer.create_and_wait_for_changeset(
                stack_name=stack_name,
                cfn_template=template_str,
                parameter_values=parameters,
                capabilities=capabilities,
                role_arn=role_arn,
                notification_arns=notification_arns,
                s3_uploader=s3_uploader,
                tags=tags,
            )
            click.echo(
                self.MSG_SHOWCASE_CHANGESET.format(changeset_id=result["Id"]))

            if no_execute_changeset:
                return

            if confirm_changeset:
                click.secho(self.MSG_CONFIRM_CHANGESET_HEADER, fg="yellow")
                click.secho("=" * len(self.MSG_CONFIRM_CHANGESET_HEADER),
                            fg="yellow")
                if not click.confirm(f"{self.MSG_CONFIRM_CHANGESET}",
                                     default=False):
                    return

            self.deployer.execute_changeset(result["Id"], stack_name)
            self.deployer.wait_for_execute(stack_name, changeset_type)
            click.echo(
                self.MSG_EXECUTE_SUCCESS.format(stack_name=stack_name,
                                                region=region))

        except deploy_exceptions.ChangeEmptyError as ex:
            if fail_on_empty_changeset:
                raise
            click.echo(str(ex))

    def merge_parameters(self, template_dict, parameter_overrides):
        """
        CloudFormation CreateChangeset requires a value for every parameter
        from the template, either specifying a new value or use previous value.
        For convenience, this method will accept new parameter values and
        generates a dict of all parameters in a format that ChangeSet API
        will accept

        :param parameter_overrides:
        :return:
        """
        parameter_values = []

        if not isinstance(template_dict.get("Parameters", None), dict):
            return parameter_values

        for key, _ in template_dict["Parameters"].items():

            obj = {"ParameterKey": key}

            if key in parameter_overrides:
                obj["ParameterValue"] = parameter_overrides[key]
            else:
                obj["UsePreviousValue"] = True

            parameter_values.append(obj)

        return parameter_values
Example #2
0
class TestDeployer(TestCase):
    def setUp(self):
        self.session = MagicMock()
        self.cloudformation_client = self.session.client("cloudformation")
        self.s3_client = self.session.client("s3")
        self.deployer = Deployer(self.cloudformation_client)

    def test_deployer_init(self):
        self.assertEqual(self.deployer._client, self.cloudformation_client)
        self.assertEqual(self.deployer.changeset_prefix, "samcli-deploy")

    def test_deployer_has_no_stack(self):
        self.deployer._client.describe_stacks = MagicMock(
            return_value={"Stacks": []})
        self.assertEqual(self.deployer.has_stack("test"), False)

    def test_deployer_has_stack_in_review(self):
        self.deployer._client.describe_stacks = MagicMock(
            return_value={"Stacks": [{
                "StackStatus": "REVIEW_IN_PROGRESS"
            }]})
        self.assertEqual(self.deployer.has_stack("test"), False)

    def test_deployer_has_stack_exception_non_exsistent(self):
        self.deployer._client.describe_stacks = MagicMock(
            side_effect=ClientError(
                error_response={
                    "Error": {
                        "Message": "Stack with id test does not exist"
                    }
                },
                operation_name="stack_status",
            ))
        self.assertEqual(self.deployer.has_stack("test"), False)

    def test_deployer_has_stack_exception(self):
        self.deployer._client.describe_stacks = MagicMock(
            side_effect=Exception())
        with self.assertRaises(Exception):
            self.deployer.has_stack("test")

    def test_deployer_has_stack_exception_botocore(self):
        self.deployer._client.describe_stacks = MagicMock(
            side_effect=BotoCoreError())
        with self.assertRaises(DeployFailedError):
            self.deployer.has_stack("test")

    def test_create_changeset(self):
        self.deployer.has_stack = MagicMock(return_value=False)
        self.deployer.create_changeset(
            stack_name="test",
            cfn_template=" ",
            parameter_values=[
                {
                    "ParameterKey": "a",
                    "ParameterValue": "b"
                },
                {
                    "ParameterKey": "c",
                    "UsePreviousValue": True
                },
            ],
            capabilities=["CAPABILITY_IAM"],
            role_arn="role-arn",
            notification_arns=[],
            s3_uploader=S3Uploader(s3_client=self.s3_client,
                                   bucket_name="test_bucket"),
            tags={"unit": "true"},
        )

        self.assertEqual(self.deployer._client.create_change_set.call_count, 1)
        self.deployer._client.create_change_set.assert_called_with(
            Capabilities=["CAPABILITY_IAM"],
            ChangeSetName=ANY,
            ChangeSetType="CREATE",
            Description=ANY,
            NotificationARNs=[],
            Parameters=[{
                "ParameterKey": "a",
                "ParameterValue": "b"
            }],
            RoleARN="role-arn",
            StackName="test",
            Tags={"unit": "true"},
            TemplateURL=ANY,
        )

    def test_update_changeset(self):
        self.deployer.has_stack = MagicMock(return_value=True)
        self.deployer.create_changeset(
            stack_name="test",
            cfn_template=" ",
            parameter_values=[
                {
                    "ParameterKey": "a",
                    "ParameterValue": "b"
                },
                {
                    "ParameterKey": "c",
                    "UsePreviousValue": True
                },
            ],
            capabilities=["CAPABILITY_IAM"],
            role_arn="role-arn",
            notification_arns=[],
            s3_uploader=S3Uploader(s3_client=self.s3_client,
                                   bucket_name="test_bucket"),
            tags={"unit": "true"},
        )

        self.assertEqual(self.deployer._client.create_change_set.call_count, 1)
        self.deployer._client.create_change_set.assert_called_with(
            Capabilities=["CAPABILITY_IAM"],
            ChangeSetName=ANY,
            ChangeSetType="UPDATE",
            Description=ANY,
            NotificationARNs=[],
            Parameters=[{
                "ParameterKey": "a",
                "ParameterValue": "b"
            }],
            RoleARN="role-arn",
            StackName="test",
            Tags={"unit": "true"},
            TemplateURL=ANY,
        )

    def test_create_changeset_exception(self):
        self.deployer.has_stack = MagicMock(return_value=False)
        self.deployer._client.create_change_set = MagicMock(
            side_effect=Exception)
        with self.assertRaises(ChangeSetError):
            self.deployer.create_changeset(
                stack_name="test",
                cfn_template=" ",
                parameter_values=[
                    {
                        "ParameterKey": "a",
                        "ParameterValue": "b"
                    },
                    {
                        "ParameterKey": "c",
                        "UsePreviousValue": True
                    },
                ],
                capabilities=["CAPABILITY_IAM"],
                role_arn="role-arn",
                notification_arns=[],
                s3_uploader=S3Uploader(s3_client=self.s3_client,
                                       bucket_name="test_bucket"),
                tags={"unit": "true"},
            )

    def test_create_changeset_ClientErrorException(self):
        error_message = (
            "An error occurred (ValidationError) when calling the CreateChangeSet "
            "operation: S3 error: The bucket you are attempting to access must be "
            "addressed using the specified endpoint. "
            "Please send all future requests to this "
            "endpoint.\nFor more information "
            "check http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html"
        )
        self.deployer.has_stack = MagicMock(return_value=False)
        self.deployer._client.create_change_set = MagicMock(
            side_effect=ClientError(
                error_response={"Error": {
                    "Message": error_message
                }},
                operation_name="create_changeset"))
        with self.assertRaises(DeployBucketInDifferentRegionError):
            self.deployer.create_changeset(
                stack_name="test",
                cfn_template=" ",
                parameter_values=[
                    {
                        "ParameterKey": "a",
                        "ParameterValue": "b"
                    },
                    {
                        "ParameterKey": "c",
                        "UsePreviousValue": True
                    },
                ],
                capabilities=["CAPABILITY_IAM"],
                role_arn="role-arn",
                notification_arns=[],
                s3_uploader=S3Uploader(s3_client=self.s3_client,
                                       bucket_name="test_bucket"),
                tags={"unit": "true"},
            )

    def test_create_changeset_ClientErrorException_generic(self):
        self.deployer.has_stack = MagicMock(return_value=False)
        self.deployer._client.create_change_set = MagicMock(
            side_effect=ClientError(
                error_response={"Error": {
                    "Message": "Message"
                }},
                operation_name="create_changeset"))
        with self.assertRaises(ChangeSetError):
            self.deployer.create_changeset(
                stack_name="test",
                cfn_template=" ",
                parameter_values=[
                    {
                        "ParameterKey": "a",
                        "ParameterValue": "b"
                    },
                    {
                        "ParameterKey": "c",
                        "UsePreviousValue": True
                    },
                ],
                capabilities=["CAPABILITY_IAM"],
                role_arn="role-arn",
                notification_arns=[],
                s3_uploader=S3Uploader(s3_client=self.s3_client,
                                       bucket_name="test_bucket"),
                tags={"unit": "true"},
            )

    def test_describe_changeset_with_changes(self):
        response = [
            {
                "Changes": [{
                    "ResourceChange": {
                        "LogicalResourceId": "resource_id1",
                        "ResourceType": "s3",
                        "Action": "Add"
                    }
                }]
            },
            {
                "Changes": [{
                    "ResourceChange": {
                        "LogicalResourceId": "resource_id2",
                        "ResourceType": "kms",
                        "Action": "Add"
                    }
                }]
            },
            {
                "Changes": [{
                    "ResourceChange": {
                        "LogicalResourceId": "resource_id3",
                        "ResourceType": "lambda",
                        "Action": "Add"
                    }
                }]
            },
        ]
        self.deployer._client.get_paginator = MagicMock(
            return_value=MockPaginator(resp=response))
        changes = self.deployer.describe_changeset("change_id", "test")
        self.assertEqual(
            changes,
            {
                "Add": [
                    {
                        "LogicalResourceId": "resource_id1",
                        "ResourceType": "s3"
                    },
                    {
                        "LogicalResourceId": "resource_id2",
                        "ResourceType": "kms"
                    },
                    {
                        "LogicalResourceId": "resource_id3",
                        "ResourceType": "lambda"
                    },
                ],
                "Modify": [],
                "Remove": [],
            },
        )

    def test_describe_changeset_with_no_changes(self):
        response = [{"Changes": []}]
        self.deployer._client.get_paginator = MagicMock(
            return_value=MockPaginator(resp=response))
        changes = self.deployer.describe_changeset("change_id", "test")
        self.assertEqual(changes, {"Add": [], "Modify": [], "Remove": []})

    def test_wait_for_changeset(self):
        self.deployer._client.get_waiter = MagicMock(
            return_value=MockChangesetWaiter())
        self.deployer.wait_for_changeset("test-id", "test-stack")

    def test_wait_for_changeset_exception_ChangeEmpty(self):
        self.deployer._client.get_waiter = MagicMock(
            return_value=MockChangesetWaiter(ex=WaiterError(
                name="wait_for_changeset",
                reason="unit-test",
                last_response={
                    "Status": "Failed",
                    "StatusReason": "It's a unit test"
                },
            )))
        with self.assertRaises(ChangeSetError):
            self.deployer.wait_for_changeset("test-id", "test-stack")

    def test_execute_changeset(self):
        self.deployer.execute_changeset("id", "test")
        self.deployer._client.execute_change_set.assert_called_with(
            ChangeSetName="id", StackName="test")

    def test_execute_changeset_exception(self):
        self.deployer._client.execute_change_set = MagicMock(
            side_effect=ClientError(
                error_response={"Error": {
                    "Message": "Error"
                }},
                operation_name="execute_changeset"))
        with self.assertRaises(DeployFailedError):
            self.deployer.execute_changeset("id", "test")

    def test_get_last_event_time(self):
        timestamp = datetime.utcnow()
        self.deployer._client.describe_stack_events = MagicMock(
            return_value={"StackEvents": [{
                "Timestamp": timestamp
            }]})
        self.assertEqual(self.deployer.get_last_event_time("test"),
                         utc_to_timestamp(timestamp))

    def test_get_last_event_time_unknown_last_time(self):
        current_timestamp = datetime.utcnow()
        self.deployer._client.describe_stack_events = MagicMock(
            side_effect=KeyError)
        # Convert to milliseconds from seconds
        last_stack_event_timestamp = to_datetime(
            self.deployer.get_last_event_time("test") * 1000)
        self.assertEqual(last_stack_event_timestamp.year,
                         current_timestamp.year)
        self.assertEqual(last_stack_event_timestamp.month,
                         current_timestamp.month)
        self.assertEqual(last_stack_event_timestamp.day, current_timestamp.day)
        self.assertEqual(last_stack_event_timestamp.hour,
                         current_timestamp.hour)
        self.assertEqual(last_stack_event_timestamp.minute,
                         current_timestamp.minute)
        self.assertEqual(last_stack_event_timestamp.second,
                         current_timestamp.second)

    @patch("time.sleep")
    def test_describe_stack_events(self, patched_time):
        current_timestamp = datetime.utcnow()

        self.deployer._client.describe_stacks = MagicMock(side_effect=[
            {
                "Stacks": [{
                    "StackStatus": "CREATE_IN_PROGRESS"
                }]
            },
            {
                "Stacks": [{
                    "StackStatus": "CREATE_IN_PROGRESS"
                }]
            },
            {
                "Stacks": [{
                    "StackStatus": "CREATE_COMPLETE_CLEANUP_IN_PROGRESS"
                }]
            },
            {
                "Stacks": [{
                    "StackStatus": "CREATE_COMPLETE"
                }]
            },
        ])
        self.deployer._client.get_paginator = MagicMock(
            return_value=MockPaginator([
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_IN_PROGRESS",
                        "ResourceType": "s3",
                        "LogicalResourceId": "mybucket",
                    }]
                },
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_IN_PROGRESS",
                        "ResourceType": "kms",
                        "LogicalResourceId": "mykms",
                    }]
                },
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "s3",
                        "LogicalResourceId": "mybucket",
                    }]
                },
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "kms",
                        "LogicalResourceId": "mykms",
                    }]
                },
            ]))

        self.deployer.describe_stack_events("test", time.time() - 1)

    @patch("time.sleep")
    def test_describe_stack_events_exceptions(self, patched_time):

        self.deployer._client.describe_stacks = MagicMock(side_effect=[
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
        ])
        with self.assertRaises(ClientError):
            self.deployer.describe_stack_events("test", time.time())

    @patch("time.sleep")
    def test_describe_stack_events_resume_after_exceptions(self, patched_time):
        current_timestamp = datetime.utcnow()

        self.deployer._client.describe_stacks = MagicMock(side_effect=[
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
            ClientError(error_response={"Error": {
                "Message": "Rate Exceeded"
            }},
                        operation_name="describe_stack_events"),
            {
                "Stacks": [{
                    "StackStatus": "CREATE_IN_PROGRESS"
                }]
            },
            {
                "Stacks": [{
                    "StackStatus": "CREATE_IN_PROGRESS"
                }]
            },
            {
                "Stacks": [{
                    "StackStatus": "CREATE_COMPLETE_CLEANUP_IN_PROGRESS"
                }]
            },
            {
                "Stacks": [{
                    "StackStatus": "CREATE_COMPLETE"
                }]
            },
        ])

        self.deployer._client.get_paginator = MagicMock(
            return_value=MockPaginator([
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_IN_PROGRESS",
                        "ResourceType": "s3",
                        "LogicalResourceId": "mybucket",
                    }]
                },
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_IN_PROGRESS",
                        "ResourceType": "kms",
                        "LogicalResourceId": "mykms",
                    }]
                },
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "s3",
                        "LogicalResourceId": "mybucket",
                    }]
                },
                {
                    "StackEvents": [{
                        "EventId": str(uuid.uuid4()),
                        "Timestamp": current_timestamp,
                        "ResourceStatus": "CREATE_COMPLETE",
                        "ResourceType": "kms",
                        "LogicalResourceId": "mykms",
                    }]
                },
            ]))

        self.deployer.describe_stack_events("test", time.time())

    def test_check_stack_status(self):
        self.assertEqual(
            self.deployer._check_stack_complete("CREATE_COMPLETE"), True)
        self.assertEqual(self.deployer._check_stack_complete("CREATE_FAILED"),
                         False)
        self.assertEqual(
            self.deployer._check_stack_complete("CREATE_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete("DELETE_COMPLETE"), True)
        self.assertEqual(self.deployer._check_stack_complete("DELETE_FAILED"),
                         False)
        self.assertEqual(
            self.deployer._check_stack_complete("DELETE_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete("REVIEW_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete("ROLLBACK_COMPLETE"), True)
        self.assertEqual(
            self.deployer._check_stack_complete("ROLLBACK_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete("UPDATE_COMPLETE"), True)
        self.assertEqual(
            self.deployer._check_stack_complete(
                "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete("UPDATE_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete(
                "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"), False)
        self.assertEqual(
            self.deployer._check_stack_complete("UPDATE_ROLLBACK_FAILED"),
            False)
        self.assertEqual(
            self.deployer._check_stack_complete("UPDATE_ROLLBACK_IN_PROGRESS"),
            False)

    @patch("time.sleep")
    def test_wait_for_execute(self, patched_time):
        self.deployer.describe_stack_events = MagicMock()
        self.deployer._client.get_waiter = MagicMock(
            return_value=MockCreateUpdateWaiter())
        self.deployer.wait_for_execute("test", "CREATE")
        self.deployer.wait_for_execute("test", "UPDATE")
        with self.assertRaises(RuntimeError):
            self.deployer.wait_for_execute("test", "DESTRUCT")

        self.deployer._client.get_waiter = MagicMock(
            return_value=MockCreateUpdateWaiter(ex=WaiterError(
                name="create_changeset",
                reason="unit-test",
                last_response={
                    "Status": "Failed",
                    "StatusReason": "It's a unit test"
                },
            )))
        with self.assertRaises(DeployFailedError):
            self.deployer.wait_for_execute("test", "CREATE")

    def test_create_and_wait_for_changeset(self):
        self.deployer.create_changeset = MagicMock(return_value=({
            "Id": "test"
        }, "create"))
        self.deployer.wait_for_changeset = MagicMock()
        self.deployer.describe_changeset = MagicMock()

        result = self.deployer.create_and_wait_for_changeset(
            stack_name="test",
            cfn_template=" ",
            parameter_values=[
                {
                    "ParameterKey": "a",
                    "ParameterValue": "b"
                },
                {
                    "ParameterKey": "c",
                    "UsePreviousValue": True
                },
            ],
            capabilities=["CAPABILITY_IAM"],
            role_arn="role-arn",
            notification_arns=[],
            s3_uploader=S3Uploader(s3_client=self.s3_client,
                                   bucket_name="test_bucket"),
            tags={"unit": "true"},
        )

        self.assertEqual(result, ({"Id": "test"}, "create"))

    def test_create_and_wait_for_changeset_exception(self):
        self.deployer.create_changeset = MagicMock(side_effect=ClientError(
            error_response={"Error": {
                "Message": "Something Wrong"
            }},
            operation_name="create_changeset"))
        with self.assertRaises(DeployFailedError):
            self.deployer.create_and_wait_for_changeset(
                stack_name="test",
                cfn_template=" ",
                parameter_values=[
                    {
                        "ParameterKey": "a",
                        "ParameterValue": "b"
                    },
                    {
                        "ParameterKey": "c",
                        "UsePreviousValue": True
                    },
                ],
                capabilities=["CAPABILITY_IAM"],
                role_arn="role-arn",
                notification_arns=[],
                s3_uploader=S3Uploader(s3_client=self.s3_client,
                                       bucket_name="test_bucket"),
                tags={"unit": "true"},
            )

    def test_get_stack_outputs(self):
        outputs = {
            "Stacks": [{
                "Outputs": [
                    {
                        "OutputKey": "Key1",
                        "OutputValue": "Value1",
                        "Description": "output for s3"
                    },
                    {
                        "OutputKey": "Key2",
                        "OutputValue": "Value2",
                        "Description": "output for kms"
                    },
                ]
            }]
        }
        self.deployer._client.describe_stacks = MagicMock(return_value=outputs)

        self.assertEqual(outputs["Stacks"][0]["Outputs"],
                         self.deployer.get_stack_outputs(stack_name="test"))
        self.deployer._client.describe_stacks.assert_called_with(
            StackName="test")

    @patch("samcli.lib.deploy.deployer.pprint_columns")
    def test_get_stack_outputs_no_echo(self, mock_pprint_columns):
        outputs = {
            "Stacks": [{
                "Outputs": [
                    {
                        "OutputKey": "Key1",
                        "OutputValue": "Value1",
                        "Description": "output for s3"
                    },
                    {
                        "OutputKey": "Key2",
                        "OutputValue": "Value2",
                        "Description": "output for kms"
                    },
                ]
            }]
        }
        self.deployer._client.describe_stacks = MagicMock(return_value=outputs)

        self.assertEqual(
            outputs["Stacks"][0]["Outputs"],
            self.deployer.get_stack_outputs(stack_name="test", echo=False))
        self.deployer._client.describe_stacks.assert_called_with(
            StackName="test")
        self.assertEqual(mock_pprint_columns.call_count, 0)

    def test_get_stack_outputs_no_outputs_no_exception(self):
        outputs = {"Stacks": [{"SomeOtherKey": "Value"}]}
        self.deployer._client.describe_stacks = MagicMock(return_value=outputs)

        self.assertEqual(None,
                         self.deployer.get_stack_outputs(stack_name="test"))
        self.deployer._client.describe_stacks.assert_called_with(
            StackName="test")

    def test_get_stack_outputs_exception(self):
        self.deployer._client.describe_stacks = MagicMock(
            side_effect=ClientError(
                error_response={"Error": {
                    "Message": "Error"
                }},
                operation_name="describe_stacks"))

        with self.assertRaises(DeployStackOutPutFailedError):
            self.deployer.get_stack_outputs(stack_name="test")
Example #3
0
class DeployContext:

    MSG_SHOWCASE_CHANGESET = "\nChangeset created successfully. {changeset_id}\n"

    MSG_EXECUTE_SUCCESS = "\nSuccessfully created/updated stack - {stack_name} in {region}\n"

    MSG_CONFIRM_CHANGESET = "Deploy this changeset?"
    MSG_CONFIRM_CHANGESET_HEADER = "\nPreviewing CloudFormation changeset before deployment"

    def __init__(
        self,
        template_file,
        stack_name,
        s3_bucket,
        image_repository,
        image_repositories,
        force_upload,
        no_progressbar,
        s3_prefix,
        kms_key_id,
        parameter_overrides,
        capabilities,
        no_execute_changeset,
        role_arn,
        notification_arns,
        fail_on_empty_changeset,
        tags,
        region,
        profile,
        confirm_changeset,
        signing_profiles,
    ):
        self.template_file = template_file
        self.stack_name = stack_name
        self.s3_bucket = s3_bucket
        self.image_repository = image_repository
        self.image_repositories = image_repositories
        self.force_upload = force_upload
        self.no_progressbar = no_progressbar
        self.s3_prefix = s3_prefix
        self.kms_key_id = kms_key_id
        self.parameter_overrides = parameter_overrides
        self.capabilities = capabilities
        self.no_execute_changeset = no_execute_changeset
        self.role_arn = role_arn
        self.notification_arns = notification_arns
        self.fail_on_empty_changeset = fail_on_empty_changeset
        self.tags = tags
        self.region = region
        self.profile = profile
        self.s3_uploader = None
        self.deployer = None
        self.confirm_changeset = confirm_changeset
        self.signing_profiles = signing_profiles

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass

    def run(self):
        """
        Execute deployment based on the argument provided by customers and samconfig.toml.
        """

        # Parse parameters
        with open(self.template_file, "r") as handle:
            template_str = handle.read()

        template_dict = yaml_parse(template_str)

        if not isinstance(template_dict, dict):
            raise deploy_exceptions.DeployFailedError(
                stack_name=self.stack_name,
                msg="{} not in required format".format(self.template_file))

        parameters = self.merge_parameters(template_dict,
                                           self.parameter_overrides)

        template_size = os.path.getsize(self.template_file)
        if template_size > 51200 and not self.s3_bucket:
            raise deploy_exceptions.DeployBucketRequiredError()
        boto_config = get_boto_config_with_user_agent()
        cloudformation_client = boto3.client(
            "cloudformation",
            region_name=self.region if self.region else None,
            config=boto_config)

        s3_client = None
        if self.s3_bucket:
            s3_client = boto3.client(
                "s3",
                region_name=self.region if self.region else None,
                config=boto_config)

            self.s3_uploader = S3Uploader(s3_client, self.s3_bucket,
                                          self.s3_prefix, self.kms_key_id,
                                          self.force_upload,
                                          self.no_progressbar)

        self.deployer = Deployer(cloudformation_client)

        region = s3_client._client_config.region_name if s3_client else self.region  # pylint: disable=W0212
        display_parameter_overrides = hide_noecho_parameter_overrides(
            template_dict, self.parameter_overrides)
        print_deploy_args(
            self.stack_name,
            self.s3_bucket,
            self.image_repositories if isinstance(
                self.image_repositories, dict) else self.image_repository,
            region,
            self.capabilities,
            display_parameter_overrides,
            self.confirm_changeset,
            self.signing_profiles,
        )
        return self.deploy(
            self.stack_name,
            template_str,
            parameters,
            self.capabilities,
            self.no_execute_changeset,
            self.role_arn,
            self.notification_arns,
            self.s3_uploader,
            [{
                "Key": key,
                "Value": value
            } for key, value in self.tags.items()] if self.tags else [],
            region,
            self.fail_on_empty_changeset,
            self.confirm_changeset,
        )

    def deploy(
        self,
        stack_name,
        template_str,
        parameters,
        capabilities,
        no_execute_changeset,
        role_arn,
        notification_arns,
        s3_uploader,
        tags,
        region,
        fail_on_empty_changeset=True,
        confirm_changeset=False,
    ):
        """
        Deploy the stack to cloudformation.
        - if changeset needs confirmation, it will prompt for customers to confirm.
        - if no_execute_changeset is True, the changeset won't be executed.

        Parameters
        ----------
        stack_name : str
            name of the stack
        template_str : str
            the string content of the template
        parameters : List[Dict]
            List of parameters
        capabilities : List[str]
            List of capabilities
        no_execute_changeset : bool
            A bool indicating whether to execute changeset
        role_arn : str
            the Arn of the role to create changeset
        notification_arns : List[str]
            Arns for sending notifications
        s3_uploader : S3Uploader
            S3Uploader object to upload files to S3 buckets
        tags : List[str]
            List of tags passed to CloudFormation
        region : str
            AWS region to deploy the stack to
        fail_on_empty_changeset : bool
            Should fail when changeset is empty
        confirm_changeset : bool
            Should wait for customer's confirm before executing the changeset
        """
        stacks, _ = SamLocalStackProvider.get_stacks(
            self.template_file,
            parameter_overrides=sanitize_parameter_overrides(
                self.parameter_overrides))
        auth_required_per_resource = auth_per_resource(stacks)

        for resource, authorization_required in auth_required_per_resource:
            if not authorization_required:
                click.secho(f"{resource} may not have authorization defined.",
                            fg="yellow")

        try:
            result, changeset_type = self.deployer.create_and_wait_for_changeset(
                stack_name=stack_name,
                cfn_template=template_str,
                parameter_values=parameters,
                capabilities=capabilities,
                role_arn=role_arn,
                notification_arns=notification_arns,
                s3_uploader=s3_uploader,
                tags=tags,
            )
            click.echo(
                self.MSG_SHOWCASE_CHANGESET.format(changeset_id=result["Id"]))

            if no_execute_changeset:
                return

            if confirm_changeset:
                click.secho(self.MSG_CONFIRM_CHANGESET_HEADER, fg="yellow")
                click.secho("=" * len(self.MSG_CONFIRM_CHANGESET_HEADER),
                            fg="yellow")
                if not click.confirm(f"{self.MSG_CONFIRM_CHANGESET}",
                                     default=False):
                    return

            self.deployer.execute_changeset(result["Id"], stack_name)
            self.deployer.wait_for_execute(stack_name, changeset_type)
            click.echo(
                self.MSG_EXECUTE_SUCCESS.format(stack_name=stack_name,
                                                region=region))

        except deploy_exceptions.ChangeEmptyError as ex:
            if fail_on_empty_changeset:
                raise
            click.echo(str(ex))

    @staticmethod
    def merge_parameters(template_dict: Dict,
                         parameter_overrides: Dict) -> List[Dict]:
        """
        CloudFormation CreateChangeset requires a value for every parameter
        from the template, either specifying a new value or use previous value.
        For convenience, this method will accept new parameter values and
        generates a dict of all parameters in a format that ChangeSet API
        will accept

        :param template_dict:
        :param parameter_overrides:
        :return:
        """
        parameter_values: List[Dict] = []

        if not isinstance(template_dict.get("Parameters", None), dict):
            return parameter_values

        for key, _ in template_dict["Parameters"].items():

            obj = {"ParameterKey": key}

            if key in parameter_overrides:
                obj["ParameterValue"] = parameter_overrides[key]
            else:
                obj["UsePreviousValue"] = True

            parameter_values.append(obj)

        return parameter_values