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 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")
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")
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, {})
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")
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))
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")
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)
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)
def setUp(self): self.capturedOutput = io.StringIO() sys.stdout = self.capturedOutput cloudformation = Cloudformation() self.cloudformationargs = CloudformationArgs(cloudformation)
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")