예제 #1
0
 def setUp(self):
     self.capturedOutput = io.StringIO()
     sys.stdout = self.capturedOutput
     self.cloudformation = Cloudformation()
     fileloader = FileLoader()
     config_path = Path(__file__).resolve().parent.joinpath(
         "../data/fzfaws.yml")
     fileloader.load_config_file(config_path=str(config_path))
예제 #2
0
def wait_drift_result(cloudformation: Cloudformation, drift_id: str) -> None:
    """Wait for the drift detection result.

    Since aws doesn't provide wait condition, creating a custom waiter.

    :param cloudformation: Cloudformation instance
    :type cloudformation: Cloudformation
    :param drift_id: the id of the drift detection
    :type drift_id: str
    """
    delay, max_attempts = cloudformation._get_waiter_config()
    attempts: int = 0
    response = None
    with Spinner.spin(message="Wating for drift detection to complete ..."):
        while attempts <= max_attempts:
            time.sleep(delay)
            response = cloudformation.client.describe_stack_drift_detection_status(
                StackDriftDetectionId=drift_id)
            if response.get("DetectionStatus") != "DETECTION_IN_PROGRESS":
                break
    if response is not None:
        response.pop("ResponseMetadata", None)
        print(json.dumps(response, indent=4, default=str))
        print(80 * "-")
        if response["DetectionStatus"] == "DETECTION_COMPLETE":
            print("StackDriftStatus: %s" %
                  response.get("StackResourceDriftStatus"))
            print("DriftedStackResourceCount: %s" %
                  response.get("DriftedStackResourceCount"))
        else:
            print("Drift detection failed")
    else:
        print("Waiter failed: Max attempts exceeded")
예제 #3
0
def delete_stack(
    profile: Union[str, bool] = False,
    region: Union[str, bool] = False,
    wait: bool = False,
    iam: Union[str, bool] = False,
) -> None:
    """Handle deletion of the stack.

    Two situation, normal deletion and retained deletion.
    When the selected stack is already in a 'DELETE_FAILED' state, extra
    fzf operation would be triggered for user to select logical id to retain
    in order for deletion to be success.

    :param profile: use a different profile for this operation
    :type profile: Union[str, bool], optional
    :param region: use a different region for this operation
    :type region: Union[str, bool], optional
    :param wait: pause the function and wait for stack delete complete
    :type wait: bool, optional
    :param iam: specify a iam arn to delete this stack
    :type iam: Union[str, bool]
    """
    cloudformation = Cloudformation(profile, region)
    cloudformation.set_stack()

    logical_id_list: List[str] = []
    if cloudformation.stack_details["StackStatus"] == "DELETE_FAILED":
        header: str = "stack is in the failed state, specify any resource to skip during deletion"
        logical_id_list = cloudformation.get_stack_resources(empty_allow=True,
                                                             header=header)

    cloudformation_args: Dict[str, Any] = {
        "StackName": cloudformation.stack_name
    }
    if logical_id_list:
        cloudformation_args["RetainResources"] = logical_id_list

    if iam and type(iam) == str:
        cloudformation_args["RoleARN"] = iam
    elif iam and type(iam) == bool:
        iam_instance = IAM(profile=cloudformation.profile)
        iam_instance.set_arns(
            header=
            "select a iam role with permissions to delete the current stack",
            service="cloudformation.amazonaws.com",
        )
        if iam_instance.arns[0]:
            cloudformation_args["RoleARN"] = iam_instance.arns[0]

    if not get_confirmation("Are you sure you want to delete the stack '%s'?" %
                            cloudformation.stack_name):
        sys.exit(1)

    cloudformation.client.delete_stack(**cloudformation_args)
    print("Stack deletion initiated")

    if wait:
        cloudformation.wait("stack_delete_complete",
                            "Wating for stack to be deleted ...")
        print("Stack deleted")
예제 #4
0
    def test_constructor(self):
        self.assertEqual(self.cloudformation.region, None)
        self.assertEqual(self.cloudformation.profile, None)
        self.assertEqual(self.cloudformation.stack_name, "")
        self.assertEqual(self.cloudformation.stack_details, {})

        cloudformation = Cloudformation(profile="root", region="us-east-1")
        self.assertEqual(cloudformation.region, "us-east-1")
        self.assertEqual(cloudformation.profile, "root")
        self.assertEqual(cloudformation.stack_name, "")
        self.assertEqual(cloudformation.stack_details, {})
예제 #5
0
def create_stack(
    profile: Union[str, bool] = False,
    region: Union[str, bool] = False,
    local_path: Union[str, bool] = False,
    root: bool = False,
    wait: bool = False,
    extra: bool = False,
    bucket: str = None,
    version: Union[str, bool] = False,
) -> None:
    """Handle the creation of the cloudformation stack.

    :param profile: use a different profile for this operation
    :type profile: Union[bool, str], optional
    :param region: use a different region for this operation
    :type region: Union[bool, str], optional
    :param local_path: Select a template from local machine
    :type local_path: Union[bool, str], optional
    :param root: Search local file from root directory
    :type root: bool, optional
    :param wait: wait for stack to be completed before exiting the program
    :type wait: bool, optional
    :param extra: configure extra options for the stack, (tags, IAM, termination protection etc..)
    :type extra: bool, optional
    :param bucket: specify a bucket/bucketpath to skip s3 selection
    :type bucket: str, optional
    :param version: use a previous version of the template
    :type version: Union[bool, str], optional
    :raises NoNameEntered: when the new stack receive empty string as stack_name
    """
    cloudformation = Cloudformation(profile, region)

    if local_path:
        if type(local_path) != str:
            fzf = Pyfzf()
            local_path = str(
                fzf.get_local_file(search_from_root=root, cloudformation=True))
        cloudformation_args = construct_local_creation_args(
            cloudformation, str(local_path))
    else:
        cloudformation_args = construct_s3_creation_args(
            cloudformation, bucket, version)

    if extra:
        extra_args = CloudformationArgs(cloudformation)
        extra_args.set_extra_args(search_from_root=root)
        cloudformation_args.update(extra_args.extra_args)
    response = cloudformation.execute_with_capabilities(**cloudformation_args)

    response.pop("ResponseMetadata", None)
    print(json.dumps(response, indent=4, default=str))
    print(80 * "-")
    print("Stack creation initiated")

    if wait:
        cloudformation.stack_name = cloudformation_args["StackName"]
        cloudformation.wait("stack_create_complete",
                            "Waiting for stack to be ready ...")
        print("Stack created")
예제 #6
0
def validate_stack(
    profile: Optional[Union[str, bool]] = False,
    region: Optional[Union[str, bool]] = False,
    local_path: Union[str, bool] = False,
    root: bool = False,
    bucket: str = None,
    version: Union[str, bool] = False,
    no_print: bool = False,
) -> None:
    """Validate the selected cloudformation template using boto3 api.

    This is also used internally by create_stack and update_stack
    operations.

    :param profile: Use a different profile for this operation
    :type profile: Union[bool, str], optional
    :param region: Use a different region for this operation
    :type region: Union[bool, str], optional
    :param local_path: Select a template from local machine
    :type local_path: Union[bool, str], optional
    :param root: Search local file from root directory
    :type root: bool, optional
    :param bucket: specify a bucket/bucketpath to skip s3 selection
    :type bucket: str, optional
    :param version: use a previous version of the template
    :type version: Union[bool, str], optional
    :param no_print: Don't print the response, only check excpetion
    :type no_print: bool, optional
    """
    cloudformation = Cloudformation(profile, region)

    if local_path:
        if type(local_path) != str:
            fzf = Pyfzf()
            local_path = str(
                fzf.get_local_file(
                    search_from_root=root,
                    cloudformation=True,
                    header="select a cloudformation template to validate",
                ))
        check_is_valid(local_path)
        with open(local_path, "r") as file_body:
            response = cloudformation.client.validate_template(
                TemplateBody=file_body.read())
    else:
        s3 = S3(profile, region)
        s3.set_bucket_and_path(bucket)
        if not s3.bucket_name:
            s3.set_s3_bucket(
                header="select a bucket which contains the template")
        if not s3.path_list[0]:
            s3.set_s3_object()

        check_is_valid(s3.path_list[0])

        if version == True:
            version = s3.get_object_version(s3.bucket_name,
                                            s3.path_list[0])[0].get(
                                                "VersionId", False)

        template_body_loacation = s3.get_object_url(
            "" if not version else str(version))
        response = cloudformation.client.validate_template(
            TemplateURL=template_body_loacation)

    if not no_print:
        response.pop("ResponseMetadata", None)
        print(json.dumps(response, indent=4, default=str))
예제 #7
0
class TestCloudformation(unittest.TestCase):
    def setUp(self):
        self.capturedOutput = io.StringIO()
        sys.stdout = self.capturedOutput
        self.cloudformation = Cloudformation()
        fileloader = FileLoader()
        config_path = Path(__file__).resolve().parent.joinpath(
            "../data/fzfaws.yml")
        fileloader.load_config_file(config_path=str(config_path))

    def tearDown(self):
        sys.stdout = sys.__stdout__

    def test_constructor(self):
        self.assertEqual(self.cloudformation.region, None)
        self.assertEqual(self.cloudformation.profile, None)
        self.assertEqual(self.cloudformation.stack_name, "")
        self.assertEqual(self.cloudformation.stack_details, {})

        cloudformation = Cloudformation(profile="root", region="us-east-1")
        self.assertEqual(cloudformation.region, "us-east-1")
        self.assertEqual(cloudformation.profile, "root")
        self.assertEqual(cloudformation.stack_name, "")
        self.assertEqual(cloudformation.stack_details, {})

    @patch.object(Paginator, "paginate")
    @patch.object(Pyfzf, "process_list")
    @patch.object(Pyfzf, "execute_fzf")
    def test_set_stack(self, mocked_execute, mocked_list, mocked_page):
        data_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "../data/cloudformation_stacks.json",
        )
        with open(data_path, "r") as file:
            response = json.load(file)

        mocked_page.return_value = response
        mocked_execute.return_value = "dotbare-cicd"
        self.cloudformation.set_stack()
        mocked_list.assert_called_once_with(response[0]["Stacks"], "StackName",
                                            "StackStatus", "Description")
        mocked_execute.assert_called_once_with(empty_allow=False)
        self.assertEqual(
            self.cloudformation.stack_details,
            {
                "Capabilities": ["CAPABILITY_NAMED_IAM"],
                "Description":
                "CodeBuild template for dotbare, webhook trigger from Github "
                "only on Master push",
                "DisableRollback":
                False,
                "DriftInformation": {
                    "StackDriftStatus": "IN_SYNC"
                },
                "NotificationARNs": [],
                "RollbackConfiguration": {
                    "RollbackTriggers": []
                },
                "StackId":
                "arn:aws:cloudformation:ap-southeast-2:1111111:stack/dotbare-cicd/0ae5ef60-9651-11ea-b6d0-0223bf2782f0",
                "StackName":
                "dotbare-cicd",
                "StackStatus":
                "UPDATE_COMPLETE",
                "Tags": [],
            },
        )
        self.assertEqual(self.cloudformation.stack_name, "dotbare-cicd")

        mocked_list.reset_mock()
        mocked_execute.reset_mock()
        mocked_execute.return_value = "hellotesting"
        self.cloudformation.set_stack()
        mocked_list.assert_called_once_with(response[0]["Stacks"], "StackName",
                                            "StackStatus", "Description")
        mocked_execute.assert_called_once_with(empty_allow=False)
        self.assertEqual(
            self.cloudformation.stack_details,
            {
                "Description":
                "testing purposes only",
                "DisableRollback":
                False,
                "DriftInformation": {
                    "StackDriftStatus": "IN_SYNC"
                },
                "NotificationARNs": [],
                "Outputs": [{
                    "Description":
                    "The security group id for EC2 import reference",
                    "ExportName": "hellotesting-SecurityGroupId",
                    "OutputKey": "SecurityGroupId",
                    "OutputValue": "sg-006ae18653dc5acd7",
                }],
                "Parameters": [
                    {
                        "ParameterKey": "SSHLocation",
                        "ParameterValue": "0.0.0.0/0"
                    },
                    {
                        "ParameterKey": "Hello",
                        "ParameterValue": "i-0a23663d658dcee1c"
                    },
                    {
                        "ParameterKey": "WebServer",
                        "ParameterValue": "No"
                    },
                ],
                "RoleARN":
                "arn:aws:iam::1111111:role/admincloudformaitontest",
                "RollbackConfiguration": {},
                "StackId":
                "arn:aws:cloudformation:ap-southeast-2:1111111:stack/hellotesting/05feb330-88f3-11ea-ae79-0aa5d4eec80a",
                "StackName":
                "hellotesting",
                "StackStatus":
                "UPDATE_COMPLETE",
                "Tags": [{
                    "Key": "hasdf",
                    "Value": "asdfa"
                }],
            },
        )
        self.assertEqual(self.cloudformation.stack_name, "hellotesting")

    @patch.object(Paginator, "paginate")
    @patch.object(Pyfzf, "execute_fzf")
    @patch.object(Pyfzf, "process_list")
    def test_get_stack_resources(self, mocked_process, mocked_execute,
                                 mocked_page):
        data_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "../data/cloudformation_resources.json",
        )
        with open(data_path, "r") as file:
            response = json.load(file)

        mocked_page.return_value = response
        mocked_execute.return_value = ["CodeBuild"]
        result = self.cloudformation.get_stack_resources()
        self.assertEqual(result, ["CodeBuild"])
        mocked_process.assert_called_once_with(
            [
                {
                    "LogicalResourceId": "CodeBuild",
                    "PhysicalResourceId": "dotbare",
                    "ResourceType": "AWS::CodeBuild::Project",
                    "ResourceStatus": "UPDATE_COMPLETE",
                    "DriftInformation": {
                        "StackResourceDriftStatus": "NOT_CHECKED"
                    },
                    "Drift": "NOT_CHECKED",
                },
                {
                    "LogicalResourceId": "ParameterStorePolicy",
                    "PhysicalResourceId": "dotba-Para-1G3Z5VTARYKOM",
                    "ResourceType": "AWS::IAM::Policy",
                    "ResourceStatus": "UPDATE_COMPLETE",
                    "DriftInformation": {
                        "StackResourceDriftStatus": "NOT_CHECKED"
                    },
                    "Drift": "NOT_CHECKED",
                },
                {
                    "LogicalResourceId": "ServiceRole",
                    "PhysicalResourceId": "dotbare-cicd-codebuild",
                    "ResourceType": "AWS::IAM::Role",
                    "ResourceStatus": "CREATE_COMPLETE",
                    "DriftInformation": {
                        "StackResourceDriftStatus": "IN_SYNC"
                    },
                    "Drift": "IN_SYNC",
                },
            ],
            "LogicalResourceId",
            "ResourceType",
            "Drift",
        )
        mocked_execute.assert_called_once_with(multi_select=True,
                                               header=None,
                                               empty_allow=False)

        mocked_process.reset_mock()
        mocked_execute.reset_mock()
        mocked_execute.return_value = ["hello"]
        result = self.cloudformation.get_stack_resources(empty_allow=True,
                                                         header="hello")
        self.assertEqual(result, ["hello"])
        mocked_process.assert_called_once()
        mocked_execute.assert_called_once_with(multi_select=True,
                                               header="hello",
                                               empty_allow=True)

    @patch.object(Waiter, "wait")
    def test_wait(self, mocked_wait):
        self.cloudformation.stack_name = "dotbare-cicd"
        self.cloudformation.wait(waiter_name="stack_create_complete",
                                 message="hello")
        mocked_wait.assert_called_once_with(
            ANY,
            StackName="dotbare-cicd",
            WaiterConfig={
                "Delay": 30,
                "MaxAttempts": 120
            },
        )

        # test no config for watier
        mocked_wait.reset_mock()
        del os.environ["FZFAWS_CLOUDFORMATION_WAITER"]
        self.cloudformation.stack_name = "fooboo"
        self.cloudformation.wait(waiter_name="stack_create_complete",
                                 message="hello")
        mocked_wait.assert_called_once_with(
            ANY,
            StackName="fooboo",
            WaiterConfig={
                "Delay": 15,
                "MaxAttempts": 40
            },
        )

        self.capturedOutput.truncate(0)
        self.capturedOutput.seek(0)
        # test no global waiter
        mocked_wait.reset_mock()
        del os.environ["FZFAWS_GLOBAL_WAITER"]
        self.cloudformation.stack_name = "yes"
        self.cloudformation.wait(waiter_name="stack_create_complete",
                                 message="hello",
                                 foo="boo")
        mocked_wait.assert_called_once_with(
            ANY,
            StackName="yes",
            WaiterConfig={
                "Delay": 30,
                "MaxAttempts": 120
            },
            foo="boo",
        )
        self.assertRegex(self.capturedOutput.getvalue(), r"hello")

    @patch("fzfaws.cloudformation.cloudformation.get_confirmation")
    def test_execute_with_capabilities(self, mocked_confirm):
        def hello(**kwargs):
            return {**kwargs}

        mocked_confirm.return_value = True

        self.capturedOutput.truncate(0)
        self.capturedOutput.seek(0)
        result = self.cloudformation.execute_with_capabilities(hello,
                                                               foo="boo",
                                                               lol="yes")
        self.assertEqual(result, {"foo": "boo", "lol": "yes"})
        self.assertEqual(
            self.capturedOutput.getvalue(),
            json.dumps({
                "foo": "boo",
                "lol": "yes"
            }, indent=4) + "\n",
        )

        mocked_confirm.return_value = False
        self.assertRaises(SystemExit,
                          self.cloudformation.execute_with_capabilities)

    @patch.object(Pyfzf, "execute_fzf")
    @patch.object(Pyfzf, "append_fzf")
    def test_get_capabilities(self, mocked_append, mocked_execute):
        mocked_execute.return_value = ["CAPABILITY_IAM"]
        result = self.cloudformation._get_capabilities(message="lol")
        mocked_append.assert_has_calls([
            call("CAPABILITY_IAM\n"),
            call("CAPABILITY_NAMED_IAM\n"),
            call("CAPABILITY_AUTO_EXPAND"),
        ])
        mocked_execute.assert_called_once_with(
            empty_allow=True,
            print_col=1,
            multi_select=True,
            header=
            "lol\nPlease select the capabilities to acknowledge and proceed\nMore information: https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html",
        )
        self.assertEqual(result, ["CAPABILITY_IAM"])

        mocked_execute.reset_mock()
        mocked_append.reset_mock()
        mocked_execute.return_value = [
            "CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"
        ]
        result = self.cloudformation._get_capabilities()
        mocked_append.assert_has_calls([
            call("CAPABILITY_IAM\n"),
            call("CAPABILITY_NAMED_IAM\n"),
            call("CAPABILITY_AUTO_EXPAND"),
        ])
        mocked_execute.assert_called_once_with(
            empty_allow=True,
            print_col=1,
            multi_select=True,
            header=
            "\nPlease select the capabilities to acknowledge and proceed\nMore information: https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html",
        )
        self.assertEqual(result, ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"])

    def test_get_stack_generator(self):
        self.capturedOutput.truncate(0)
        self.capturedOutput.seek(0)
        data = [{"Stacks": [{"foo": "boo"}]}, {"Stacks": [{"hello": "world"}]}]
        generator = self.cloudformation._get_stack_generator(data)
        for item in generator:
            print(item)
        self.assertEqual(self.capturedOutput.getvalue(),
                         "{'foo': 'boo'}\n{'hello': 'world'}\n")
예제 #8
0
def changeset_stack(
    profile: Union[str, bool] = False,
    region: Union[str, bool] = False,
    replace: bool = False,
    local_path: Union[str, bool] = False,
    root: bool = False,
    wait: bool = False,
    info: bool = False,
    execute: bool = False,
    delete: bool = False,
    extra: bool = False,
    bucket: str = None,
    version: Union[str, bool] = False,
) -> None:
    """Handle changeset actions.

    Main function to interacte with changeset, use argument
    to control the actions.

    This function is using update_stack to handle all the dirty
    works as both functions are processing cloudformation arguments
    and having the same arguments.

    :param profile: use a different profile for this operation
    :type profile: Union[bool, str], optional
    :param region: use a different region for this operation
    :type region: Union[bool, str], optional
    :param replace: replace the template during update
    :type replace: bool, optional
    :param local_path: Select a template from local machine
    :type local_path: Union[bool, str], optional
    :param root: Search local file from root directory
    :type root: bool, optional
    :param wait: wait for stack to be completed before exiting the program
    :type wait: bool, optional
    :param info: display result of a changeset
    :type info: bool, optional
    :param execute: execute changeset
    :type execute: bool, optional
    :param delete: delete changeset
    :type delete: bool, optional
    :param extra: configure extra options for the stack, (tags, IAM, termination protection etc..)
    :type extra: bool, optional
    :param bucket: specify a bucket/bucketpath to skip s3 selection
    :type bucket: str, optional
    :param version: use previous version of template in s3 bucket
    :type version: Union[bool, str], optional
    :raises NoNameEntered: If no changset name is entered
    """
    cloudformation = Cloudformation(profile, region)
    cloudformation.set_stack()

    # if not creating new changeset
    if info or execute or delete:
        fzf = Pyfzf()
        response: Dict[str, Any] = cloudformation.client.list_change_sets(
            StackName=cloudformation.stack_name)
        # get the changeset name
        fzf.process_list(
            response.get("Summaries", []),
            "ChangeSetName",
            "StackName",
            "ExecutionStatus",
            "Status",
            "Description",
        )

        if info:
            selected_changeset = str(fzf.execute_fzf())
            describe_changes(cloudformation, selected_changeset)

        # execute the change set
        elif execute:
            selected_changeset = fzf.execute_fzf()
            if get_confirmation("Execute changeset %s?" % selected_changeset):
                response = cloudformation.client.execute_change_set(
                    ChangeSetName=selected_changeset,
                    StackName=cloudformation.stack_name,
                )
                cloudformation.wait("stack_update_complete",
                                    "Wating for stack to be updated ...")
                print("Stack updated")

        elif delete:
            selected_changeset = fzf.execute_fzf(multi_select=True)
            for changeset in selected_changeset:
                print("(dryrun) Delete changeset %s" % changeset)
            if get_confirmation("Confirm?"):
                for changeset in selected_changeset:
                    cloudformation.client.delete_change_set(
                        ChangeSetName=changeset,
                        StackName=cloudformation.stack_name)

    else:
        changeset_name = input("Enter name of this changeset: ")
        if not changeset_name:
            raise NoNameEntered("No changeset name specified")
        changeset_description = input("Description: ")
        # since is almost same operation as update stack
        # let update_stack handle it, but return update details instead of execute
        cloudformation_args = update_stack(
            cloudformation.profile,
            cloudformation.region,
            replace,
            local_path,
            root,
            wait,
            extra,
            bucket,
            version,
            dryrun=True,
            cloudformation=cloudformation,
        )
        cloudformation_args[
            "cloudformation_action"] = cloudformation.client.create_change_set
        cloudformation_args["ChangeSetName"] = changeset_name
        if changeset_description:
            cloudformation_args["Description"] = changeset_description

        response = cloudformation.execute_with_capabilities(
            **cloudformation_args)

        response.pop("ResponseMetadata", None)
        print(json.dumps(response, indent=4, default=str))
        print(80 * "-")
        print("Changeset create initiated")

        if wait:
            cloudformation.wait(
                "change_set_create_complete",
                "Wating for changset to be created ...",
                ChangeSetName=changeset_name,
            )
            print("Changeset created")
            describe_changes(cloudformation, changeset_name)
예제 #9
0
def drift_stack(
    profile: Union[str, bool] = False,
    region: Union[str, bool] = False,
    info: bool = False,
    select: bool = False,
    wait: bool = False,
) -> None:
    """Perform actions on stack drift.

    Info: print drift info.

    Select: select resource and detect its drift.

    Default: init and wait for the drift result of the entire stack.

    :param profile: use a different profile for the operation
    :type profile: Union[str, bool], optional
    :param region: use a different region for this operation
    :type region: Union[str, bool], optional
    :param info: display drift status instead of initiate a drift detection
    :type info: bool, optional
    :param select: select individual iresource and detect drift, otherwise, it will perform stack level check
    :type select: bool, optional
    :param wait: wait for the drfit detection
    :type wait: bool, optional
    """
    cloudformation = Cloudformation(profile, region)
    cloudformation.set_stack()

    print(
        json.dumps(cloudformation.stack_details["DriftInformation"],
                   indent=4,
                   default=str))
    print(80 * "-")

    if info:
        response = cloudformation.client.describe_stack_resource_drifts(
            StackName=cloudformation.stack_name, )
        response.pop("ResponseMetadata", None)
        print(json.dumps(response, indent=4, default=str))
    elif not select:
        response = cloudformation.client.detect_stack_drift(
            StackName=cloudformation.stack_name)
        drift_id: str = response["StackDriftDetectionId"]
        print("Drift detection initiated")
        print("DriftDetectionId: %s" % drift_id)
        if wait:
            wait_drift_result(cloudformation, drift_id)

    else:
        logical_id_list: List[str] = cloudformation.get_stack_resources()

        if len(logical_id_list) == 1:
            # get individual resource drift status
            response = cloudformation.client.detect_stack_resource_drift(
                StackName=cloudformation.stack_name,
                LogicalResourceId=logical_id_list[0],
            )
            print(
                json.dumps(response["StackResourceDrift"],
                           indent=4,
                           default=str))
            print(80 * "-")
            print("LogicalResourceId: %s" %
                  response["StackResourceDrift"]["LogicalResourceId"])
            print("StackResourceDriftStatus: %s" %
                  response["StackResourceDrift"]["StackResourceDriftStatus"])

        else:
            # get all selected resource status
            response = cloudformation.client.detect_stack_drift(
                StackName=cloudformation.stack_name,
                LogicalResourceIds=logical_id_list)
            drift_id: str = response["StackDriftDetectionId"]
            print("Drift detection initiated")
            print("DriftDetectionId: %s" % drift_id)
            if wait:
                wait_drift_result(cloudformation, drift_id)
예제 #10
0
 def setUp(self):
     self.capturedOutput = io.StringIO()
     sys.stdout = self.capturedOutput
     cloudformation = Cloudformation()
     self.cloudformationargs = CloudformationArgs(cloudformation)
예제 #11
0
def update_stack(
    profile: Optional[Union[str, bool]] = False,
    region: Optional[Union[str, bool]] = False,
    replace: bool = False,
    local_path: Union[str, bool] = False,
    root: bool = False,
    wait: bool = False,
    extra: bool = False,
    bucket: str = None,
    version: Union[str, bool] = False,
    dryrun: bool = False,
    cloudformation: Optional[Cloudformation] = None,
) -> Union[None, dict]:
    """Handle the update of cloudformation stacks.

    This is also used by changeset_stack to create its argument.
    The dryrun and cloudformation argument in the function is only
    used by changeset_stack.

    :param profile: use a different profile for this operation
    :type profile: Union[bool, str], optional
    :param region: use a different region for this operation
    :type region: Union[bool, str], optional
    :param replace: replace the template during update
    :type replace: bool, optional
    :param local_path: Select a template from local machine
    :type local_path: Union[bool, str], optional
    :param root: Search local file from root directory
    :type root: bool, optional
    :param wait: wait for stack to be completed before exiting the program
    :type wait: bool, optional
    :param extra: configure extra options for the stack, (tags, IAM, termination protection etc..)
    :type extra: bool, optional
    :param bucket: specify a bucket/bucketpath to skip s3 selection
    :type bucket: str, optional
    :param version: use a previous version of the template on s3 bucket
    :type version: Union[str, bool], optional
    :param dryrun: don't update, rather return update information, used for changeset_stack()
    :type dryrun: bool, optional
    :param cloudformation: a cloudformation instance, when calling from changeset_stack(), pass cloudformation in
    :type cloudformation: Cloudformation, optional
    :return: If dryrun is set, return all the update details as dict {'Parameters': value, 'Tags': value...}
    :rtype: Union[None, dict]
    """
    if not cloudformation:
        cloudformation = Cloudformation(profile, region)
        cloudformation.set_stack()

    extra_args = CloudformationArgs(cloudformation)

    if not replace:
        # non replacing update, just update the parameter
        cloudformation_args = non_replacing_update(cloudformation)

    else:
        # replace existing template
        if local_path:
            # template provided in local machine
            if type(local_path) != str:
                fzf = Pyfzf()
                local_path = str(
                    fzf.get_local_file(search_from_root=root,
                                       cloudformation=True))
            cloudformation_args = local_replacing_update(
                cloudformation, str(local_path))

        else:
            # template provided in s3
            cloudformation_args = s3_replacing_update(cloudformation, bucket,
                                                      version)

    if extra:
        extra_args.set_extra_args(update=True,
                                  search_from_root=root,
                                  dryrun=dryrun)
        cloudformation_args.update(extra_args.extra_args)

    if dryrun:
        return cloudformation_args

    response = cloudformation.execute_with_capabilities(**cloudformation_args)

    response.pop("ResponseMetadata", None)
    print(json.dumps(response, indent=4, default=str))
    print(80 * "-")
    print("Stack update initiated")

    if wait:
        cloudformation.wait("stack_update_complete",
                            "Wating for stack to be updated ...")
        print("Stack updated")