Repository: aws-cloudformation/cloudformation-cli

Issue:
cfn CLI needs a way to specify --profile
We use a lot of profiles (multiple gov profiles, seller profiles, commercial profiles etc.)

Since cfn command can only use default profile, we have to manually edit credentials file to switch between profiles.
This makes it hard to automate testing and publishing. This also makes it hard to test multiple things

Ability to specify profile will be very helpful. AWS Cli also provides following argument
       --profile (string)       Use a specific profile from your credential file.


Discussion:
ammokhov: Thanks for the contribution, we should take a look at your pull-request soon and get back
rrhodes: Hi @Brianwithay21, @MalikAtalla-AWS, anything further required before this is eligible for merging? I don't appear to have permission to merge this.
MalikAtalla-AWS: Hey @rrhodes, thanks for the reminder. I'll ping the team who owns this repo.
rrhodes: > Hey @rrhodes, thanks for the reminder. I'll ping the team who owns this repo.

No trouble, thanks @MalikAtalla-AWS 🙂 

Patch:
diff --git a/doc_source/resource-type-cli-invoke.md b/doc_source/resource-type-cli-invoke.md
index c79b008..3289a35 100755
--- a/doc_source/resource-type-cli-invoke.md
+++ b/doc_source/resource-type-cli-invoke.md
@@ -10,6 +10,7 @@ Performs contract tests on the specified handler of a resource type\.
   cfn invoke
 [--endpoint <value>]
 [--function-name <value>]
+[--profile <value>]
 [--region <value>]
 [--max-reinvoke <value>]
 action
@@ -30,6 +31,10 @@ The logical Lambda function name in the SAM template\. Alternately, you can also
 
 Default: `TypeFunction`
 
+`--profile <value>`
+
+The AWS profile to use\. If no profile is specified, the client applies credentials specified in the [Boto3 credentials chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html)\.
+
 `--region <value>`
 
 The region to configure the client to interact with\.
diff --git a/doc_source/resource-type-cli-submit.md b/doc_source/resource-type-cli-submit.md
index a97ee4c..0add620 100755
--- a/doc_source/resource-type-cli-submit.md
+++ b/doc_source/resource-type-cli-submit.md
@@ -22,6 +22,7 @@ The user registering the extension must be able to access the schema handler pac
   cfn submit
 [--dry-run]
 [--endpoint-url <value>]
+[--profile <value>]
 [--region <value>]
 [--role-arn <value>]
 [--no-role]
@@ -42,6 +43,10 @@ The CloudFormation endpoint to use\.
 
 The AWS Region in which to register the extension\. If no Region is specified, the extension is registered in the default region\.
 
+`--profile <value>`
+
+The AWS profile to use\. If no profile is specified, the CloudFormation CLI applies credentials specified in the [Boto3 credentials chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html)\.
+
 `--role-arn <value>`
 
 A specific IAM role to use when invoking handler operations\.
diff --git a/src/rpdk/core/boto_helpers.py b/src/rpdk/core/boto_helpers.py
index f70273a..c1dfa21 100644
--- a/src/rpdk/core/boto_helpers.py
+++ b/src/rpdk/core/boto_helpers.py
@@ -14,14 +14,14 @@ BOTO_CRED_KEYS = ("aws_access_key_id", "aws_secret_access_key", "aws_session_tok
 LOWER_CAMEL_CRED_KEYS = ("accessKeyId", "secretAccessKey", "sessionToken")
 
 
-def create_sdk_session(region_name=None):
+def create_sdk_session(region_name=None, profile_name=None):
     def _known_error(msg):
         raise CLIMisconfiguredError(
             msg + ". Please ensure your AWS CLI is configured correctly: "
             "https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html"
         )
 
-    session = Boto3Session(region_name=region_name)
+    session = Boto3Session(profile_name=profile_name, region_name=region_name)
 
     if session.region_name is None:
         _known_error("No region specified")
diff --git a/src/rpdk/core/contract/hook_client.py b/src/rpdk/core/contract/hook_client.py
index 4a3a4e2..74b8ad6 100644
--- a/src/rpdk/core/contract/hook_client.py
+++ b/src/rpdk/core/contract/hook_client.py
@@ -59,9 +59,10 @@ class HookClient:  # pylint: disable=too-many-instance-attributes
         docker_image=None,
         executable_entrypoint=None,
         target_info=None,
-    ):  # pylint: disable=too-many-arguments
+        profile=None,
+    ):  # pylint: disable=too-many-arguments,too-many-locals
         self._schema = schema
-        self._session = create_sdk_session(region)
+        self._session = create_sdk_session(region, profile)
         self._role_arn = role_arn
         self._type_name = type_name
         self._log_group_name = log_group_name
diff --git a/src/rpdk/core/contract/resource_client.py b/src/rpdk/core/contract/resource_client.py
index 93d42ec..13399a7 100644
--- a/src/rpdk/core/contract/resource_client.py
+++ b/src/rpdk/core/contract/resource_client.py
@@ -169,8 +169,9 @@ class ResourceClient:  # pylint: disable=too-many-instance-attributes
         log_role_arn=None,
         docker_image=None,
         executable_entrypoint=None,
+        profile=None,
     ):  # pylint: disable=too-many-arguments
-        self._session = create_sdk_session(region)
+        self._session = create_sdk_session(region, profile)
         self._role_arn = role_arn
         self._type_name = type_name
         self._log_group_name = log_group_name
diff --git a/src/rpdk/core/generate.py b/src/rpdk/core/generate.py
index 4c4a1ef..5838305 100644
--- a/src/rpdk/core/generate.py
+++ b/src/rpdk/core/generate.py
@@ -13,7 +13,11 @@ def generate(args):
     project = Project()
     project.load()
     project.generate(
-        args.endpoint_url, args.region, args.local_only, args.target_schemas
+        args.endpoint_url,
+        args.region,
+        args.local_only,
+        args.target_schemas,
+        args.profile,
     )
     project.generate_docs()
 
@@ -33,3 +37,4 @@ def setup_subparser(subparsers, parents):
     parser.add_argument(
         "--target-schemas", help="Path to target schemas.", nargs="*", default=[]
     )
+    parser.add_argument("--profile", help="AWS profile to use.")
diff --git a/src/rpdk/core/hook/init_hook.py b/src/rpdk/core/hook/init_hook.py
index 55dee3d..308719b 100644
--- a/src/rpdk/core/hook/init_hook.py
+++ b/src/rpdk/core/hook/init_hook.py
@@ -31,7 +31,7 @@ def init_hook(args, project):
     }
 
     project.init_hook(type_name, language, settings)
-    project.generate(args.endpoint_url, args.region, args.target_schemas)
+    project.generate(args.endpoint_url, args.region, args.target_schemas, args.profile)
     project.generate_docs()
 
 
diff --git a/src/rpdk/core/init.py b/src/rpdk/core/init.py
index ca00d77..11141a4 100644
--- a/src/rpdk/core/init.py
+++ b/src/rpdk/core/init.py
@@ -199,3 +199,5 @@ def setup_subparser(subparsers, parents):
         default=[],
         type=lambda s: [i.strip() for i in s.split(",")],
     )
+
+    parser.add_argument("--profile", help="AWS profile to use.")
diff --git a/src/rpdk/core/invoke.py b/src/rpdk/core/invoke.py
index 4f99835..c162071 100644
--- a/src/rpdk/core/invoke.py
+++ b/src/rpdk/core/invoke.py
@@ -58,6 +58,7 @@ def get_contract_client(args, project):
             {},
             executable_entrypoint=project.executable_entrypoint,
             docker_image=args.docker_image,
+            profile=args.profile,
         )
 
     return ResourceClient(
@@ -68,6 +69,7 @@ def get_contract_client(args, project):
         {},
         executable_entrypoint=project.executable_entrypoint,
         docker_image=args.docker_image,
+        profile=args.profile,
     )
 
 
diff --git a/src/rpdk/core/project.py b/src/rpdk/core/project.py
index 4b6ce0f..ae36784 100644
--- a/src/rpdk/core/project.py
+++ b/src/rpdk/core/project.py
@@ -478,8 +478,13 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 LOG.info("File already exists, not overwriting '%s'", path)
 
     def generate(
-        self, endpoint_url=None, region_name=None, local_only=False, target_schemas=None
-    ):
+        self,
+        endpoint_url=None,
+        region_name=None,
+        local_only=False,
+        target_schemas=None,
+        profile_name=None,
+    ):  # pylint: disable=too-many-arguments
         if self.artifact_type == ARTIFACT_TYPE_MODULE:
             return  # for Modules, the schema is already generated in cfn validate
 
@@ -536,6 +541,7 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 region_name,
                 type_schemas=target_schemas,
                 local_only=local_only,
+                profile_name=profile_name,
             )
 
         self._plugin.generate(self)
@@ -607,7 +613,14 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
         template_fragment.validate_fragments()
 
     def submit(
-        self, dry_run, endpoint_url, region_name, role_arn, use_role, set_default
+        self,
+        dry_run,
+        endpoint_url,
+        region_name,
+        role_arn,
+        use_role,
+        set_default,
+        profile_name,
     ):  # pylint: disable=too-many-arguments
         context_mgr = self._create_context_manager(dry_run)
 
@@ -631,7 +644,9 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 if self.artifact_type == ARTIFACT_TYPE_MODULE:
                     self._add_modules_content_to_zip(zip_file)
                 elif self.artifact_type == ARTIFACT_TYPE_HOOK:
-                    self._add_hooks_content_to_zip(zip_file, endpoint_url, region_name)
+                    self._add_hooks_content_to_zip(
+                        zip_file, endpoint_url, region_name, profile_name
+                    )
                 else:
                     self._add_resources_content_to_zip(zip_file)
 
@@ -642,7 +657,13 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
             else:
                 f.seek(0)
                 self._upload(
-                    f, endpoint_url, region_name, role_arn, use_role, set_default
+                    f,
+                    endpoint_url,
+                    region_name,
+                    role_arn,
+                    use_role,
+                    set_default,
+                    profile_name,
                 )
 
     def _add_overrides_file_to_zip(self, zip_file):
@@ -672,7 +693,9 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
         cli_metadata["cli-version"] = __version__
         zip_file.writestr(CFN_METADATA_FILENAME, json.dumps(cli_metadata))
 
-    def _add_hooks_content_to_zip(self, zip_file, endpoint_url=None, region_name=None):
+    def _add_hooks_content_to_zip(
+        self, zip_file, endpoint_url=None, region_name=None, profile_name=None
+    ):
         zip_file.write(self.schema_path, SCHEMA_UPLOAD_FILENAME)
         if os.path.isdir(self.inputs_path):
             for filename in os.listdir(self.inputs_path):
@@ -687,7 +710,9 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
             target_info = (
                 self.target_info
                 if self.target_info
-                else self._load_target_info(endpoint_url, region_name)
+                else self._load_target_info(
+                    endpoint_url, region_name, profile_name=profile_name
+                )
             )
         except RPDKBaseException as e:
             LOG.warning("Failed to load target info, attempting local...", exc_info=e)
@@ -1039,10 +1064,17 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
         return prop
 
     def _upload(
-        self, fileobj, endpoint_url, region_name, role_arn, use_role, set_default
+        self,
+        fileobj,
+        endpoint_url,
+        region_name,
+        role_arn,
+        use_role,
+        set_default,
+        profile_name,
     ):  # pylint: disable=too-many-arguments, too-many-locals
         LOG.debug("Packaging complete, uploading...")
-        session = create_sdk_session(region_name)
+        session = create_sdk_session(region_name, profile_name)
         LOG.debug("Uploading to region '%s'", session.region_name)
         cfn_client = session.client("cloudformation", endpoint_url=endpoint_url)
         s3_client = session.client("s3")
@@ -1137,8 +1169,13 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
     # flake8: noqa: C901
     # pylint: disable=R0914
     def _load_target_info(
-        self, endpoint_url, region_name, type_schemas=None, local_only=False
-    ):
+        self,
+        endpoint_url,
+        region_name,
+        type_schemas=None,
+        local_only=False,
+        profile_name=None,
+    ):  # pylint: disable=too-many-arguments
         if self.artifact_type != ARTIFACT_TYPE_HOOK or not self.schema:
             return {}
 
@@ -1175,7 +1212,7 @@ class Project:  # pylint: disable=too-many-instance-attributes,too-many-public-m
             )
             loader = TypeSchemaLoader(None, None, local_only=local_only)
         else:
-            session = create_sdk_session(region_name)
+            session = create_sdk_session(region_name, profile_name)
             cfn_client = session.client("cloudformation", endpoint_url=endpoint_url)
             s3_client = session.client("s3")
 
diff --git a/src/rpdk/core/submit.py b/src/rpdk/core/submit.py
index 6d31838..d9b521f 100644
--- a/src/rpdk/core/submit.py
+++ b/src/rpdk/core/submit.py
@@ -19,6 +19,7 @@ def submit(args):
         args.role_arn,
         args.use_role,
         args.set_default,
+        args.profile,
     )
 
 
@@ -37,6 +38,7 @@ def setup_subparser(subparsers, parents):
         action="store_true",
         help="If registration is successful, set submitted version to the default.",
     )
+    parser.add_argument("--profile", help="AWS profile to use.")
     role_group = parser.add_mutually_exclusive_group()
     role_group.add_argument(
         "--role-arn",


Test patch:
diff --git a/doc_source/resource-type-cli-test.md b/doc_source/resource-type-cli-test.md
index a4279c2..6b7d76b 100755
--- a/doc_source/resource-type-cli-test.md
+++ b/doc_source/resource-type-cli-test.md
@@ -10,6 +10,7 @@ Performs contract tests on the handlers of a resource type\.
   cfn test
 [--endpoint <value>]
 [--function-name <value>]
+[--profile <value>]
 [--region <value>]
 [--role-arn <value>]
 ```
@@ -28,6 +29,10 @@ The logical Lambda function name in the SAM template\. Alternately, you can also
 
 Default: `TestEntrypoint`
 
+`--profile <value>`
+
+The AWS profile to use\. If no profile is specified, the contract tests apply credentials specified in the [Boto3 credentials chain](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html)\.
+
 `--region <value>`
 
 The region to use for temporary credentials\.
diff --git a/src/rpdk/core/test.py b/src/rpdk/core/test.py
index f2fa952..19a5143 100644
--- a/src/rpdk/core/test.py
+++ b/src/rpdk/core/test.py
@@ -32,6 +32,7 @@ LOG = logging.getLogger(__name__)
 
 DEFAULT_ENDPOINT = "http://127.0.0.1:3001"
 DEFAULT_FUNCTION = "TypeFunction"
+DEFAULT_PROFILE = None
 DEFAULT_REGION = "us-east-1"
 DEFAULT_TIMEOUT = "240"
 INPUTS = "inputs"
@@ -101,8 +102,8 @@ def temporary_ini_file():
         yield str(path)
 
 
-def get_cloudformation_exports(region_name, endpoint_url, role_arn):
-    session = create_sdk_session(region_name)
+def get_cloudformation_exports(region_name, endpoint_url, role_arn, profile_name):
+    session = create_sdk_session(region_name, profile_name)
     temp_credentials = get_temporary_credentials(session, role_arn=role_arn)
     cfn_client = session.client(
         "cloudformation", endpoint_url=endpoint_url, **temp_credentials
@@ -115,12 +116,14 @@ def get_cloudformation_exports(region_name, endpoint_url, role_arn):
     return exports
 
 
-def render_jinja(overrides_string, region_name, endpoint_url, role_arn):
+def render_jinja(overrides_string, region_name, endpoint_url, role_arn, profile_name):
     env = Environment(autoescape=True)
     parsed_content = env.parse(overrides_string)
     variables = meta.find_undeclared_variables(parsed_content)
     if variables:
-        exports = get_cloudformation_exports(region_name, endpoint_url, role_arn)
+        exports = get_cloudformation_exports(
+            region_name, endpoint_url, role_arn, profile_name
+        )
         invalid_exports = variables - exports.keys()
         if len(invalid_exports) > 0:
             invalid_exports_message = (
@@ -148,14 +151,16 @@ def filter_overrides(overrides, project):
     return overrides
 
 
-def get_overrides(root, region_name, endpoint_url, role_arn):
+def get_overrides(root, region_name, endpoint_url, role_arn, profile_name):
     if not root:
         return empty_override()
 
     path = root / "overrides.json"
     try:
         with path.open("r", encoding="utf-8") as f:
-            overrides_raw = render_jinja(f.read(), region_name, endpoint_url, role_arn)
+            overrides_raw = render_jinja(
+                f.read(), region_name, endpoint_url, role_arn, profile_name
+            )
     except FileNotFoundError:
         LOG.debug("Override file '%s' not found. No overrides will be applied", path)
         return empty_override()
@@ -183,14 +188,16 @@ def get_overrides(root, region_name, endpoint_url, role_arn):
 
 # pylint: disable=R0914
 # flake8: noqa: C901
-def get_hook_overrides(root, region_name, endpoint_url, role_arn):
+def get_hook_overrides(root, region_name, endpoint_url, role_arn, profile_name):
     if not root:
         return empty_hook_override()
 
     path = root / "overrides.json"
     try:
         with path.open("r", encoding="utf-8") as f:
-            overrides_raw = render_jinja(f.read(), region_name, endpoint_url, role_arn)
+            overrides_raw = render_jinja(
+                f.read(), region_name, endpoint_url, role_arn, profile_name
+            )
     except FileNotFoundError:
         LOG.debug("Override file '%s' not found. No overrides will be applied", path)
         return empty_hook_override()
@@ -235,8 +242,8 @@ def get_hook_overrides(root, region_name, endpoint_url, role_arn):
     return overrides
 
 
-# pylint: disable=R0914
-def get_inputs(root, region_name, endpoint_url, value, role_arn):
+# pylint: disable=R0914,too-many-arguments
+def get_inputs(root, region_name, endpoint_url, value, role_arn, profile_name):
     inputs = {}
     if not root:
         return None
@@ -258,7 +265,7 @@ def get_inputs(root, region_name, endpoint_url, value, role_arn):
                 file_path = path / file
                 with file_path.open("r", encoding="utf-8") as f:
                     overrides_raw = render_jinja(
-                        f.read(), region_name, endpoint_url, role_arn
+                        f.read(), region_name, endpoint_url, role_arn, profile_name
                     )
                 overrides = {}
                 for pointer, obj in overrides_raw.items():
@@ -338,6 +345,7 @@ def get_contract_plugin_client(args, project, overrides, inputs):
             target_info=project._load_target_info(  # pylint: disable=protected-access
                 args.cloudformation_endpoint_url, args.region
             ),
+            profile=args.profile,
         )
         LOG.debug("Setup plugin for HOOK type")
         return plugin_clients
@@ -356,6 +364,7 @@ def get_contract_plugin_client(args, project, overrides, inputs):
         args.log_role_arn,
         executable_entrypoint=project.executable_entrypoint,
         docker_image=args.docker_image,
+        profile=args.profile,
     )
     LOG.debug("Setup plugin for RESOURCE type")
     return plugin_clients
@@ -371,11 +380,19 @@ def test(args):
 
     if project.artifact_type == ARTIFACT_TYPE_HOOK:
         overrides = get_hook_overrides(
-            project.root, args.region, args.cloudformation_endpoint_url, args.role_arn
+            project.root,
+            args.region,
+            args.cloudformation_endpoint_url,
+            args.role_arn,
+            args.profile,
         )
     else:
         overrides = get_overrides(
-            project.root, args.region, args.cloudformation_endpoint_url, args.role_arn
+            project.root,
+            args.region,
+            args.cloudformation_endpoint_url,
+            args.role_arn,
+            args.profile,
         )
         filter_overrides(overrides, project)
 
@@ -387,6 +404,7 @@ def test(args):
             args.cloudformation_endpoint_url,
             index,
             args.role_arn,
+            args.profile,
         )
         if not inputs:
             break
@@ -484,6 +502,14 @@ def _sam_arguments(parser):
             "The region used for temporary credentials " f"(Default: {DEFAULT_REGION})"
         ),
     )
+    parser.add_argument(
+        "--profile",
+        default=DEFAULT_PROFILE,
+        help=(
+            "The profile used for temporary credentials "
+            f"(Default: {DEFAULT_PROFILE})"
+        ),
+    )
 
 
 def _validate_sam_args(args):
diff --git a/tests/test_boto_helpers.py b/tests/test_boto_helpers.py
index 0cc2464..2414e6d 100644
--- a/tests/test_boto_helpers.py
+++ b/tests/test_boto_helpers.py
@@ -25,7 +25,7 @@ def test_create_sdk_session_region():
 
     assert boto3_session is mock_boto3.return_value
 
-    mock_boto3.assert_called_once_with(region_name=None)
+    mock_boto3.assert_called_once_with(profile_name=None, region_name=None)
     mock_boto3.return_value.get_credentials.assert_called_once_with()
 
 
@@ -37,7 +37,7 @@ def test_create_sdk_session_no_region():
         with pytest.raises(CLIMisconfiguredError):
             create_sdk_session()
 
-    mock_boto3.assert_called_once_with(region_name=None)
+    mock_boto3.assert_called_once_with(profile_name=None, region_name=None)
 
 
 def test_create_sdk_session_no_creds():
@@ -49,7 +49,7 @@ def test_create_sdk_session_no_creds():
         with pytest.raises(CLIMisconfiguredError):
             create_sdk_session()
 
-    mock_boto3.assert_called_once_with(region_name=None)
+    mock_boto3.assert_called_once_with(profile_name=None, region_name=None)
 
 
 def test_get_temporary_credentials_has_token():
diff --git a/tests/test_generate.py b/tests/test_generate.py
index 82e92ad..af79c4b 100644
--- a/tests/test_generate.py
+++ b/tests/test_generate.py
@@ -2,6 +2,7 @@ from unittest.mock import Mock, patch
 
 from rpdk.core.cli import main
 from rpdk.core.project import Project
+from rpdk.core.test import DEFAULT_PROFILE
 
 
 def test_generate_command_generate(capsys):
@@ -12,7 +13,9 @@ def test_generate_command_generate(capsys):
         main(args_in=["generate"])
 
     mock_project.load.assert_called_once_with()
-    mock_project.generate.assert_called_once_with(None, None, False, [])
+    mock_project.generate.assert_called_once_with(
+        None, None, False, [], DEFAULT_PROFILE
+    )
     mock_project.generate_docs.assert_called_once_with()
 
     out, err = capsys.readouterr()
@@ -44,6 +47,7 @@ def test_generate_command_generate_with_args(capsys):
         "us-east-1",
         False,
         ["/files/target-schema.json", "/files/other-target-schema"],
+        DEFAULT_PROFILE,
     )
     mock_project.generate_docs.assert_called_once_with()
 
diff --git a/tests/test_init.py b/tests/test_init.py
index 133d10c..c76e757 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -19,6 +19,7 @@ from rpdk.core.init import (
 from rpdk.core.module.init_module import input_typename as input_typename_module
 from rpdk.core.project import Project
 from rpdk.core.resource.init_resource import input_typename as input_typename_resource
+from rpdk.core.test import DEFAULT_PROFILE
 
 from .utils import add_dummy_language_plugin, dummy_parser, get_args, get_mock_project
 
@@ -60,6 +61,7 @@ def test_init_resource_method_interactive():
             "endpoint_url": None,
             "region": None,
             "target_schemas": [],
+            "profile": DEFAULT_PROFILE,
         },
     )
     mock_project.generate.assert_called_once_with()
@@ -123,9 +125,10 @@ def test_init_hook_method_interactive():
             "endpoint_url": None,
             "region": None,
             "target_schemas": [],
+            "profile": DEFAULT_PROFILE,
         },
     )
-    mock_project.generate.assert_called_once_with(None, None, [])
+    mock_project.generate.assert_called_once_with(None, None, [], DEFAULT_PROFILE)
 
 
 def test_init_resource_method_noninteractive():
@@ -169,6 +172,7 @@ def test_init_resource_method_noninteractive():
             "endpoint_url": None,
             "region": None,
             "target_schemas": [],
+            "profile": DEFAULT_PROFILE,
         },
     )
     mock_project.generate.assert_called_once_with()
@@ -215,9 +219,10 @@ def test_init_hook_method_noninteractive():
             "endpoint_url": None,
             "region": None,
             "target_schemas": [],
+            "profile": DEFAULT_PROFILE,
         },
     )
-    mock_project.generate.assert_called_once_with(None, None, [])
+    mock_project.generate.assert_called_once_with(None, None, [], DEFAULT_PROFILE)
 
 
 def test_init_resource_method_noninteractive_invalid_type_name():
@@ -268,6 +273,7 @@ def test_init_resource_method_noninteractive_invalid_type_name():
             "endpoint_url": None,
             "region": None,
             "target_schemas": [],
+            "profile": DEFAULT_PROFILE,
         },
     )
     mock_project.generate.assert_called_once_with()
@@ -319,9 +325,10 @@ def test_init_hook_method_noninteractive_invalid_type_name():
             "endpoint_url": None,
             "region": None,
             "target_schemas": [],
+            "profile": DEFAULT_PROFILE,
         },
     )
-    mock_project.generate.assert_called_once_with(None, None, [])
+    mock_project.generate.assert_called_once_with(None, None, [], DEFAULT_PROFILE)
 
 
 def test_init_hook_method_noninteractive_target_schemas():
@@ -370,10 +377,14 @@ def test_init_hook_method_noninteractive_target_schemas():
                 "/files/target-schema.json",
                 "/files/other-target-schema.json",
             ],
+            "profile": DEFAULT_PROFILE,
         },
     )
     mock_project.generate.assert_called_once_with(
-        None, None, ["/files/target-schema.json", "/files/other-target-schema.json"]
+        None,
+        None,
+        ["/files/target-schema.json", "/files/other-target-schema.json"],
+        DEFAULT_PROFILE,
     )
 
 
diff --git a/tests/test_project.py b/tests/test_project.py
index 1f3c7af..965fd54 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -51,6 +51,7 @@ TYPE_NAME = "AWS::Color::Red"
 MODULE_TYPE_NAME = "AWS::Color::Red::MODULE"
 HOOK_TYPE_NAME = "AWS::CFN::HOOK"
 REGION = "us-east-1"
+PROFILE = "sandbox"
 ENDPOINT = "cloudformation.beta.com"
 RUNTIME = random.choice(
     [
@@ -1228,7 +1229,8 @@ def test_submit_dry_run(project, is_type_configuration_available):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=False
+            set_default=False,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1328,7 +1330,8 @@ def test_submit_dry_run_modules(project):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=False
+            set_default=False,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1393,7 +1396,8 @@ def test_submit_dry_run_hooks(project):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=False
+            set_default=False,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1518,7 +1522,8 @@ def test_submit_dry_run_hooks_with_target_info(project, session):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=False
+            set_default=False,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1596,7 +1601,8 @@ def test_submit_live_run(project):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=True
+            set_default=True,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1614,6 +1620,7 @@ def test_submit_live_run(project):
         role_arn=None,
         use_role=True,
         set_default=True,
+        profile_name=PROFILE,
     )
 
     assert temp_file._was_closed
@@ -1648,7 +1655,8 @@ def test_submit_live_run_for_module(project):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=True
+            set_default=True,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1690,7 +1698,8 @@ def test_submit_live_run_for_hooks(project):
             region_name=REGION,
             role_arn=None,
             use_role=True,
-            set_default=True
+            set_default=True,
+            profile_name=PROFILE
         )
     # fmt: on
 
@@ -1708,6 +1717,7 @@ def test_submit_live_run_for_hooks(project):
         role_arn=None,
         use_role=True,
         set_default=True,
+        profile_name=PROFILE,
     )
 
     assert temp_file._was_closed
@@ -1744,9 +1754,10 @@ def test__upload_good_path_create_role_and_set_default(project):
                 role_arn=None,
                 use_role=True,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_exec_role_method.assert_called_once_with(
         project.root / "resource-role.yaml", project.hypenated_name
     )
@@ -1797,9 +1808,10 @@ def test__upload_good_path_create_role_and_set_default_hook(project):
                 role_arn=None,
                 use_role=True,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_exec_role_method.assert_called_once_with(
         project.root / "hook-role.yaml", project.hypenated_name
     )
@@ -1853,9 +1865,10 @@ def test__upload_good_path_skip_role_creation(
                 role_arn="someArn",
                 use_role=use_role,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_upload_method.assert_called_once_with(project.hypenated_name, fileobj)
     mock_role_arn_method.assert_called_once_with()
     mock_uuid.assert_called_once_with()
@@ -1907,9 +1920,10 @@ def test__upload_good_path_skip_role_creation_hook(
                 role_arn="someArn",
                 use_role=use_role,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_upload_method.assert_called_once_with(project.hypenated_name, fileobj)
     mock_role_arn_method.assert_called_once_with()
     mock_uuid.assert_called_once_with()
@@ -1957,9 +1971,10 @@ def test__upload_clienterror(project):
                 role_arn=None,
                 use_role=False,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_upload_method.assert_called_once_with(project.hypenated_name, fileobj)
     mock_role_arn_method.assert_called_once_with()
     mock_uuid.assert_called_once_with()
@@ -2004,9 +2019,10 @@ def test__upload_clienterror_module(project):
                 role_arn=None,
                 use_role=False,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_upload_method.assert_called_once_with(project.hypenated_name, fileobj)
     mock_role_arn_method.assert_called_once_with()
     mock_uuid.assert_called_once_with()
@@ -2051,9 +2067,10 @@ def test__upload_clienterror_hook(project):
                 role_arn=None,
                 use_role=False,
                 set_default=True,
+                profile_name=None,
             )
 
-    mock_sdk.assert_called_once_with(None)
+    mock_sdk.assert_called_once_with(region_name=None, profile_name=None)
     mock_upload_method.assert_called_once_with(project.hypenated_name, fileobj)
     mock_role_arn_method.assert_called_once_with()
     mock_uuid.assert_called_once_with()
@@ -2287,7 +2304,9 @@ def test__load_target_info_for_resource(project):
     project.artifact_type = ARTIFACT_TYPE_RESOURCE
     project.schema = {"handlers": {}}
 
-    target_info = project._load_target_info(endpoint_url=None, region_name=None)
+    target_info = project._load_target_info(
+        endpoint_url=None, region_name=None, profile_name=None
+    )
 
     assert not target_info
 
@@ -2355,6 +2374,7 @@ def test__load_target_info_for_hooks(project):
                 "/files/list-of-target-schemas.json",
                 "/files/file-of-valid-json-array-with-a-target-schema.json",
             ],
+            profile_name=None,
         )
 
     assert target_info == {
diff --git a/tests/test_test.py b/tests/test_test.py
index 82ba4b8..35dfe07 100644
--- a/tests/test_test.py
+++ b/tests/test_test.py
@@ -20,6 +20,7 @@ from rpdk.core.project import (
 from rpdk.core.test import (
     DEFAULT_ENDPOINT,
     DEFAULT_FUNCTION,
+    DEFAULT_PROFILE,
     DEFAULT_REGION,
     _validate_sam_args,
     empty_hook_override,
@@ -139,22 +140,47 @@ def create_invalid_input_file(base):
 @pytest.mark.parametrize(
     "args_in,pytest_args,plugin_args",
     [
-        ([], [], [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, DEFAULT_REGION, "240"]),
-        (["--endpoint", "foo"], [], [DEFAULT_FUNCTION, "foo", DEFAULT_REGION, "240"]),
+        (
+            [],
+            [],
+            [
+                DEFAULT_FUNCTION,
+                DEFAULT_ENDPOINT,
+                DEFAULT_REGION,
+                "240",
+                DEFAULT_PROFILE,
+            ],
+        ),
+        (
+            ["--endpoint", "foo"],
+            [],
+            [DEFAULT_FUNCTION, "foo", DEFAULT_REGION, "240", DEFAULT_PROFILE],
+        ),
         (
             ["--function-name", "bar", "--enforce-timeout", "60"],
             [],
-            ["bar", DEFAULT_ENDPOINT, DEFAULT_REGION, "60"],
+            ["bar", DEFAULT_ENDPOINT, DEFAULT_REGION, "60", DEFAULT_PROFILE],
         ),
         (
             ["--", "-k", "create"],
             ["-k", "create"],
-            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, DEFAULT_REGION, "240"],
+            [
+                DEFAULT_FUNCTION,
+                DEFAULT_ENDPOINT,
+                DEFAULT_REGION,
+                "240",
+                DEFAULT_PROFILE,
+            ],
         ),
         (
             ["--region", "us-west-2", "--", "--collect-only"],
             ["--collect-only"],
-            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, "us-west-2", "240"],
+            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, "us-west-2", "240", DEFAULT_PROFILE],
+        ),
+        (
+            ["--profile", "sandbox"],
+            [],
+            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, DEFAULT_REGION, "240", "sandbox"],
         ),
     ],
 )
@@ -188,7 +214,7 @@ def test_test_command_happy_path_resource(
     # fmt: on
 
     mock_project.load.assert_called_once_with()
-    function_name, endpoint, region, enforce_timeout = plugin_args
+    function_name, endpoint, region, enforce_timeout, profile = plugin_args
     mock_resource_client.assert_called_once_with(
         function_name,
         endpoint,
@@ -201,8 +227,9 @@ def test_test_command_happy_path_resource(
         mock_project.type_name,
         None,
         None,
-        None,
-        None,
+        executable_entrypoint=None,
+        docker_image=None,
+        profile=profile,
     )
     mock_plugin.assert_called_once_with(
         {"resource_client": mock_resource_client.return_value}
@@ -220,22 +247,47 @@ def test_test_command_happy_path_resource(
 @pytest.mark.parametrize(
     "args_in,pytest_args,plugin_args",
     [
-        ([], [], [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, DEFAULT_REGION, "240"]),
-        (["--endpoint", "foo"], [], [DEFAULT_FUNCTION, "foo", DEFAULT_REGION, "240"]),
+        (
+            [],
+            [],
+            [
+                DEFAULT_FUNCTION,
+                DEFAULT_ENDPOINT,
+                DEFAULT_REGION,
+                "240",
+                DEFAULT_PROFILE,
+            ],
+        ),
+        (
+            ["--endpoint", "foo"],
+            [],
+            [DEFAULT_FUNCTION, "foo", DEFAULT_REGION, "240", DEFAULT_PROFILE],
+        ),
         (
             ["--function-name", "bar", "--enforce-timeout", "60"],
             [],
-            ["bar", DEFAULT_ENDPOINT, DEFAULT_REGION, "60"],
+            ["bar", DEFAULT_ENDPOINT, DEFAULT_REGION, "60", DEFAULT_PROFILE],
         ),
         (
             ["--", "-k", "create"],
             ["-k", "create"],
-            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, DEFAULT_REGION, "240"],
+            [
+                DEFAULT_FUNCTION,
+                DEFAULT_ENDPOINT,
+                DEFAULT_REGION,
+                "240",
+                DEFAULT_PROFILE,
+            ],
         ),
         (
             ["--region", "us-west-2", "--", "--collect-only"],
             ["--collect-only"],
-            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, "us-west-2", "240"],
+            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, "us-west-2", "240", DEFAULT_PROFILE],
+        ),
+        (
+            ["--profile", "sandbox"],
+            [],
+            [DEFAULT_FUNCTION, DEFAULT_ENDPOINT, DEFAULT_REGION, "240", "sandbox"],
         ),
     ],
 )
@@ -269,7 +321,7 @@ def test_test_command_happy_path_hook(
     # fmt: on
 
     mock_project.load.assert_called_once_with()
-    function_name, endpoint, region, enforce_timeout = plugin_args
+    function_name, endpoint, region, enforce_timeout, profile = plugin_args
     mock_hook_client.assert_called_once_with(
         function_name,
         endpoint,
@@ -282,9 +334,10 @@ def test_test_command_happy_path_hook(
         mock_project.type_name,
         None,
         None,
-        None,
-        None,
-        HOOK_TARGET_INFO,
+        executable_entrypoint=None,
+        docker_image=None,
+        target_info=HOOK_TARGET_INFO,
+        profile=profile,
     )
     mock_plugin.assert_called_once_with({"hook_client": mock_hook_client.return_value})
     mock_ini.assert_called_once_with()
@@ -345,7 +398,10 @@ def test_temporary_ini_file():
 
 
 def test_get_overrides_no_root():
-    assert get_overrides(None, DEFAULT_REGION, "", None) == EMPTY_RESOURCE_OVERRIDE
+    assert (
+        get_overrides(None, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_RESOURCE_OVERRIDE
+    )
 
 
 def test_get_overrides_file_not_found(base):
@@ -354,20 +410,29 @@ def test_get_overrides_file_not_found(base):
         path.unlink()
     except FileNotFoundError:
         pass
-    assert get_overrides(path, DEFAULT_REGION, "", None) == EMPTY_RESOURCE_OVERRIDE
+    assert (
+        get_overrides(path, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_RESOURCE_OVERRIDE
+    )
 
 
 def test_get_overrides_invalid_file(base):
     path = base / "overrides.json"
     path.write_text("{}")
-    assert get_overrides(base, DEFAULT_REGION, "", None) == EMPTY_RESOURCE_OVERRIDE
+    assert (
+        get_overrides(base, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_RESOURCE_OVERRIDE
+    )
 
 
 def test_get_overrides_empty_overrides(base):
     path = base / "overrides.json"
     with path.open("w", encoding="utf-8") as f:
         json.dump(EMPTY_RESOURCE_OVERRIDE, f)
-    assert get_overrides(base, DEFAULT_REGION, "", None) == EMPTY_RESOURCE_OVERRIDE
+    assert (
+        get_overrides(base, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_RESOURCE_OVERRIDE
+    )
 
 
 def test_get_overrides_invalid_pointer_skipped(base):
@@ -377,7 +442,10 @@ def test_get_overrides_invalid_pointer_skipped(base):
     path = base / "overrides.json"
     with path.open("w", encoding="utf-8") as f:
         json.dump(overrides, f)
-    assert get_overrides(base, DEFAULT_REGION, "", None) == EMPTY_RESOURCE_OVERRIDE
+    assert (
+        get_overrides(base, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_RESOURCE_OVERRIDE
+    )
 
 
 def test_get_overrides_good_path(base):
@@ -387,13 +455,16 @@ def test_get_overrides_good_path(base):
     path = base / "overrides.json"
     with path.open("w", encoding="utf-8") as f:
         json.dump(overrides, f)
-    assert get_overrides(base, DEFAULT_REGION, "", None) == {
+    assert get_overrides(base, DEFAULT_REGION, "", None, DEFAULT_PROFILE) == {
         "CREATE": {("foo", "bar"): {}}
     }
 
 
 def test_get_hook_overrides_no_root():
-    assert get_hook_overrides(None, DEFAULT_REGION, "", None) == EMPTY_HOOK_OVERRIDE
+    assert (
+        get_hook_overrides(None, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_HOOK_OVERRIDE
+    )
 
 
 def test_get_hook_overrides_file_not_found(base):
@@ -402,13 +473,19 @@ def test_get_hook_overrides_file_not_found(base):
         path.unlink()
     except FileNotFoundError:
         pass
-    assert get_hook_overrides(path, DEFAULT_REGION, "", None) == EMPTY_HOOK_OVERRIDE
+    assert (
+        get_hook_overrides(path, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_HOOK_OVERRIDE
+    )
 
 
 def test_get_hook_overrides_invalid_file(base):
     path = base / "overrides.json"
     path.write_text("{}")
-    assert get_hook_overrides(base, DEFAULT_REGION, "", None) == EMPTY_HOOK_OVERRIDE
+    assert (
+        get_hook_overrides(base, DEFAULT_REGION, "", None, DEFAULT_PROFILE)
+        == EMPTY_HOOK_OVERRIDE
+    )
 
 
 def test_get_hook_overrides_good_path(base):
@@ -420,7 +497,7 @@ def test_get_hook_overrides_good_path(base):
     path = base / "overrides.json"
     with path.open("w", encoding="utf-8") as f:
         json.dump(overrides, f)
-    assert get_hook_overrides(base, DEFAULT_REGION, "", None) == {
+    assert get_hook_overrides(base, DEFAULT_REGION, "", None, DEFAULT_PROFILE) == {
         "CREATE_PRE_PROVISION": {
             "My::Example::Resource": {"resourceProperties": {("foo", "bar"): {}}}
         }
@@ -477,7 +554,7 @@ def test_get_overrides_with_jinja(
             mock_cfn_client,
             Mock(),
         ]
-        result = get_overrides(base, DEFAULT_REGION, None, None)
+        result = get_overrides(base, DEFAULT_REGION, None, None, DEFAULT_PROFILE)
 
     assert result == expected_overrides
 
@@ -518,8 +595,7 @@ def test_get_marker_options(schema, expected_marker_keywords):
         )
     ],
 )
-# pylint: disable=R0913
-# pylint: disable=R0914
+# pylint: disable=R0913,R0914
 def test_with_inputs(
     base,
     create_string,
@@ -544,7 +620,7 @@ def test_with_inputs(
             mock_cfn_client,
             Mock(),
         ]
-        result = get_inputs(base, DEFAULT_REGION, None, 1, None)
+        result = get_inputs(base, DEFAULT_REGION, None, 1, None, DEFAULT_PROFILE)
 
     assert result == expected_inputs
 
@@ -568,23 +644,23 @@ def test_with_inputs_invalid(base):
             mock_cfn_client,
             Mock(),
         ]
-        result = get_inputs(base, DEFAULT_REGION, None, 1, None)
+        result = get_inputs(base, DEFAULT_REGION, None, 1, None, DEFAULT_PROFILE)
 
     assert not result
 
 
 def test_get_input_invalid_root():
-    assert not get_inputs("", DEFAULT_REGION, "", 1, None)
+    assert not get_inputs("", DEFAULT_REGION, "", 1, None, DEFAULT_PROFILE)
 
 
 def test_get_input_input_folder_does_not_exist(base):
-    assert not get_inputs(base, DEFAULT_REGION, "", 1, None)
+    assert not get_inputs(base, DEFAULT_REGION, "", 1, None, DEFAULT_PROFILE)
 
 
 def test_get_input_file_not_found(base):
     path = base / "inputs"
     os.mkdir(path, mode=0o777)
-    assert not get_inputs(base, DEFAULT_REGION, "", 1, None)
+    assert not get_inputs(base, DEFAULT_REGION, "", 1, None, DEFAULT_PROFILE)
 
 
 def test_use_both_sam_and_docker_arguments():