diff --git a/docs/_source/docs/faq.rst b/docs/_source/docs/faq.rst index e01d91d7d..6563b0cce 100644 --- a/docs/_source/docs/faq.rst +++ b/docs/_source/docs/faq.rst @@ -90,14 +90,14 @@ How do I call AWS services or use AWS-based tools in my custom hook/resolver/tem In order to call AWS services in your custom hook/resolver/template handler properly, you should use the IAM configurations of the stack where the resolver is being used (unless you need to use a different configuration for a specific reason). This means your hook/resolver/handler should honor the -``profile``, ``region``, and ``iam_role`` configurations as set for your project and/or Stack Config. +``profile``, ``region``, and ``sceptre_role`` configurations as set for your project and/or Stack Config. Simply invoking ``boto3.client('s3')`` is _not_ going to regard those and could end up using the wrong credentials or not even working. There is a simple interface available for doing this properly, the :py:class:`sceptre.connection_manager.ConnectionManager`. The ConnectionManager is an interface that -will be pre-configured with each stack's profile, region, and iam_role and will be ready for you to use. -If you are using an `iam_role`, it will automatically assume that role via STS for making calls to +will be pre-configured with each stack's profile, region, and sceptre_role and will be ready for you to use. +If you are using an ``sceptre_role``, it will automatically assume that role via STS for making calls to AWS so you can just use it the way you want. It is accessible on hooks and resolvers via ``self.stack.connection_manager`` and on template_handlers via ``self.connection_manager``. diff --git a/docs/_source/docs/permissions.rst b/docs/_source/docs/permissions.rst index f3af67246..44c9faded 100644 --- a/docs/_source/docs/permissions.rst +++ b/docs/_source/docs/permissions.rst @@ -20,14 +20,14 @@ Permissions Configurations -------------------------- There are three main configurations for Sceptre that can modify default permissions behavior to -provide more flexibility, control, and safety within an organization: **role_arn**, **iam_role**, and -**profile**. These can be applied in a very targeted way, on a stack by stack basis or can be applied -broadly to a whole StackGroup. +provide more flexibility, control, and safety within an organization: **cloudformation_service_role**, +**sceptre_role**, and **profile**. These can be applied in a very targeted way, on a stack by stack +basis or can be applied broadly to a whole StackGroup. -.. _role_arn_permissions: +.. _cloudformation_service_role_permissions: -role_arn -^^^^^^^^ +cloudformation_service_role +^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is the **CloudFormation service role** that will be attached to a given CloudFormation stack. This IAM role needs to be able to be assumed by **CloudFormation**, and must provide all the necessary permissions for all create/read/update/delete operations for all resources defined in that @@ -53,14 +53,14 @@ the use case. For more information on using CloudFormation service roles, see the `AWS documentation `_. -As a resolvable property, Sceptre allows you to use a resolver to populate the ``role_arn`` for a +As a resolvable property, Sceptre allows you to use a resolver to populate the ``cloudformation_service_role`` for a Stack or StackGroup Config. This means you could define that role within your project, output its ARN, and then reference it using `!stack_output`. -.. _iam_role_permissions: +.. _sceptre_role_permissions: -iam_role -^^^^^^^^ +sceptre_role +^^^^^^^^^^^^ This is a **role that Sceptre will assume** when taking any actions on the Stack. It is not a service role for CloudFormation. Instead, this is simply a role that the current user assumes to execute @@ -69,20 +69,20 @@ any Sceptre actions on that stack. This has some benefits over a CloudFormation * This is not permanently attached to the Stack after you've used it once. If you ever *don't* want to assume this role, you could comment it out or remove it from the Stack Config and Sceptre simply won't use it. This is useful if the user executing Sceptre already has the right permissions to - take those actions. In other words, it doesn't lock you in (unlike using ``role_arn``). + take those actions. In other words, it doesn't lock you in (unlike using ``cloudformation_service_role``). * CloudFormation can continue to use it's default behavior of executing Stack actions with the - permissions of the current user, but it interprets the current user to hold the indicated ``iam_role``, + permissions of the current user, but it interprets the current user to hold the indicated ``sceptre_role``, which could grant additional permissions. -Using the ``iam_role`` configuration on a Stack or StackGroup Config allows the user to *temporarily* +Using the ``sceptre_role`` configuration on a Stack or StackGroup Config allows the user to *temporarily* "step into" a different set of permissions in order to execute Sceptre actions on Stack(s) in the project without having to permanently hold those permissions. -In order to use an ``iam_role`` on a Sceptre Stack Config, that role needs to have an +In order to use an ``sceptre_role`` on a Sceptre Stack Config, that role needs to have an AssumeRolePolicyDocument that allows the current user to assume it and permissions to perform all deployment actions on the stack and all its resources. -As a resolvable property, Sceptre allows you to use a resolver to populate the ``iam_role`` for a +As a resolvable property, Sceptre allows you to use a resolver to populate the ``sceptre_role`` for a Stack or StackGroup Config. This means you could define that role within your project, output its ARN, and then reference it using `!stack_output`. @@ -91,7 +91,7 @@ ARN, and then reference it using `!stack_output`. profile ^^^^^^^ -This is different from ``role_arn`` and ``iam_role``, as both of those cause CloudFormation or +This is different from ``cloudformation_service_role`` and ``sceptre_role``, as both of those cause CloudFormation or Sceptre to *assume* a different role with different permissions than the permissions the current user has. @@ -107,7 +107,7 @@ Tips for working with Sceptre, IAM, and a CI/CD system * Rather than giving your CI/CD system blanket, admin-level permissions, you can define an IAM role with Sceptre to use for deploying the rest of your infrastructure, outputing its ARN in the template. - Then, in the rest of your project's stacks, you can set the ``iam_role`` using ``!stack_output`` + Then, in the rest of your project's stacks, you can set the ``sceptre_role`` using ``!stack_output`` to get that role's arn. This will mean your CI/CD system will temporarily "step into" that role when using Sceptre to interact with those specific stacks. It will also establish a dependency on your deployment role stack with every other stack in your project. @@ -131,7 +131,7 @@ Tips for working with Sceptre, IAM, and a CI/CD system condition applied. * If you define your deployment role (and any other related resources) using Sceptre and then - reference it on all *other* stacks using ``iam_role: !stack_output ...``, this means that your + reference it on all *other* stacks using ``sceptre_role: !stack_output ...``, this means that your CI/CD system will not be able to deploy changes to the deployment role or its resources, but that every deployment will depend on those. This is good! It means that, so long as those resources remain unchanged, automated deployment can proceed without issue. It also means that the scope of diff --git a/docs/_source/docs/resolvers.rst b/docs/_source/docs/resolvers.rst index 8561dc1f7..8c93b1fea 100644 --- a/docs/_source/docs/resolvers.rst +++ b/docs/_source/docs/resolvers.rst @@ -117,13 +117,13 @@ nested values in dicts and lists using "." to separate key/index segments. For e - "some random value" - "the value we want to select" - iam_role: !stack_output roles.yaml::RoleArn + sceptre_role: !stack_output roles.yaml::RoleArn parameters: # This will pass the value of "the value we want to select" for my_parameter my_parameter: !stack_attr sceptre_user_data.key.1 # You can also access the value of another resolvable property like this: - use_role_arn: !stack_attr iam_role + use_role: !stack_attr sceptre_role stack_output @@ -325,7 +325,7 @@ Resolver arguments can be a simple string or a complex data structure. Resolving to nothing ^^^^^^^^^^^^^^^^^^^^ When a resolver returns ``None``, this means that it resolves to "nothing". For resolvers set for -single values (such as for ``template_bucket_name`` or ``role_arn``), this just means the value is +single values (such as for ``template_bucket_name`` or ``cloudformation_service_role``), this just means the value is ``None`` and treated like those values aren't actually set. But for resolvers inside of containers like lists or dicts, when they resolve to "nothing", that item gets completely removed from their containing list or dict. @@ -365,7 +365,7 @@ A few examples... "pretty" as the sort of placeholder used for stack parameters, but the use of sceptre_user_data is broader, so it placeholder values can only be alphanumeric to reduce chances of it breaking the template. -* Resolvable properties that are *always* used when performing template operations (like ``iam_role`` +* Resolvable properties that are *always* used when performing template operations (like ``sceptre_role`` and ``template_bucket_name``) will resolve to ``None`` and not be used for those operations if they cannot be resolved. diff --git a/docs/_source/docs/stack_config.rst b/docs/_source/docs/stack_config.rst index 01a56a40a..ae3401006 100644 --- a/docs/_source/docs/stack_config.rst +++ b/docs/_source/docs/stack_config.rst @@ -24,8 +24,11 @@ particular Stack. The available keys are listed below. - `parameters`_ *(optional)* - `protected`_ *(optional)* - `role_arn`_ *(optional)* +- `cloudformation_service_role`_ *(optional)* - `iam_role`_ *(optional)* +- `sceptre_role`_ (*optional)* - `iam_role_session_duration`_ *(optional)* +- `sceptre_role_session_duration`_ *(optional)* - `sceptre_user_data`_ *(optional)* - `stack_name`_ *(optional)* - `stack_tags`_ *(optional)* @@ -47,7 +50,7 @@ from the Stack config filename. .. warning:: - This key is deprecated in favor of the `template`_ key. + This key is deprecated in favor of the `template`_ key. It will be removed in version 5.0.0. template ~~~~~~~~ @@ -312,9 +315,19 @@ role_arn * Can be inherited from StackGroup: Yes * Inheritance strategy: Overrides parent if set +.. warning:: + This field is deprecated as of v4.0.0 and will be removed in v5.0.0. It has been renamed to + `cloudformation_service_role`_ as a clearer name for its purpose. + +cloudformation_service_role +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* Resolvable: Yes +* Can be inherited from StackGroup: Yes +* Inheritance strategy: Overrides parent if set + The ARN of a `CloudFormation Service Role`_ that is assumed by *CloudFormation* (not Sceptre) to create, update or delete resources. For more information on this configuration, its implications, -and its uses see :ref:`Sceptre and IAM: role_arn `. +and its uses see :ref:`Sceptre and IAM: cloudformation_service_role `. iam_role ~~~~~~~~ @@ -322,21 +335,32 @@ iam_role * Can be inherited from StackGroup: Yes * Inheritance strategy: Overrides parent if set +.. warning:: + This field is deprecated as of v4.0.0 and will be removed in v5.0.0. It has been renamed to + `sceptre_role`_ as a clearer name for its purpose. + +sceptre_role +~~~~~~~~~~~~ +* Resolvable: Yes +* Can be inherited from StackGroup: Yes +* Inheritance strategy: Overrides parent if set + This is the IAM Role ARN that **Sceptre** should *assume* using AWS STS when executing any actions on the Stack. -This is different from the ``role_arn`` option, which sets a CloudFormation service role for the -stack. The ``iam_role`` configuration does not configure anything on the stack itself. +This is different from the ``cloudformation_service_role`` option, which sets a CloudFormation +service role for the stack. The ``sceptre_role`` configuration does not configure anything on the +stack itself. .. warning:: - If you set the value of ``iam_role`` with ``!stack_output``, that ``iam_role`` + If you set the value of ``sceptre_role`` with ``!stack_output``, that ``sceptre_role`` will not actually be used to obtain the stack_output, but it *WILL* be used for all subsequent stack actions. Therefore, it is important that the user executing the stack action have permissions to get - stack outputs for the stack outputting the ``iam_role``. + stack outputs for the stack outputting the ``sceptre_role``. For more information on this configuration, its implications, and its uses, see -:ref:`Sceptre and IAM: iam_role `. +:ref:`Sceptre and IAM: sceptre_role `. iam_role_session_duration ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -344,17 +368,24 @@ iam_role_session_duration * Can be inherited from StackGroup: Yes * Inheritance strategy: Overrides parent if set -This is the session duration when **Sceptre** *assumes* the **iam_role** IAM Role using AWS STS when +.. warning:: + This field is deprecated as of v4.0.0 and will be removed in v5.0.0. It has been renamed to + `sceptre_role_session_duration`_ as a clearer name for its purpose. + +sceptre_role_session_duration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* Resolvable: No +* Can be inherited from StackGroup: Yes +* Inheritance strategy: Overrides parent if set + +This is the session duration when **Sceptre** *assumes* the **sceptre_role** IAM Role using AWS STS when executing any actions on the Stack. .. warning:: - If you set the value of ``iam_role_session_duration`` to a number that *GREATER* than 3600, you - will need to make sure that the ``iam_role`` has a configuration of ``MaxSessionDuration``, and - its value is *GREATER* than or equal to the value of ``iam_role_session_duration``. - -For more information on this configuration, its implications, and its uses, see -:ref:`Sceptre and IAM: iam_role_session_duration `. + If you set the value of ``sceptre_role_session_duration`` to a number that *GREATER* than 3600, you + will need to make sure that the ``sceptre_role`` has a configuration of ``MaxSessionDuration``, and + its value is *GREATER* than or equal to the value of ``sceptre_role_session_duration``. sceptre_user_data ~~~~~~~~~~~~~~~~~ diff --git a/docs/_source/docs/stack_group_config.rst b/docs/_source/docs/stack_group_config.rst index 4a9a9bd4c..3da527e4b 100644 --- a/docs/_source/docs/stack_group_config.rst +++ b/docs/_source/docs/stack_group_config.rst @@ -195,8 +195,8 @@ and concerns of the project. These include: * The S3 bucket where templates are uploaded to and then referenced from for stack actions (i.e. the ``template_bucket_name`` config key). * The CloudFormation service role added to the stack(s) that CloudFormation uses to execute stack - actions (i.e. the ``role_arn`` config key). -* The role that Sceptre will assume to execute stack actions (i.e. the ``iam_role`` config key). + actions (i.e. the ``cloudformation_service_role`` config key). +* The role that Sceptre will assume to execute stack actions (i.e. the ``sceptre_role`` config key). * SNS topics that cloudformation will notify with the results of stack actions (i.e. the ``notifications`` config key). @@ -212,7 +212,7 @@ dependencies. .. warning:: You might have already considered that this might cause a circular dependency for those - dependency stacks, the ones that output the template bucket name, role arn, iam_role, or topic arns. + dependency stacks, the ones that output the template bucket name, role arn, sceptre_role, or topic arns. In order to avoid the circular dependency issue, you can either: 1. Set the value of those configurations to ``!no_value`` in the actual stacks that define those diff --git a/integration-tests/sceptre-project/config/1/A.yaml b/integration-tests/sceptre-project/config/1/A.yaml index a35368f14..4bea28a07 100644 --- a/integration-tests/sceptre-project/config/1/A.yaml +++ b/integration-tests/sceptre-project/config/1/A.yaml @@ -1,2 +1,3 @@ stack_timeout: 1 -template_path: malformed_template.json +template: + path: malformed_template.json diff --git a/integration-tests/sceptre-project/config/10/A.yaml b/integration-tests/sceptre-project/config/10/A.yaml index 5842590f4..77f67b7c7 100644 --- a/integration-tests/sceptre-project/config/10/A.yaml +++ b/integration-tests/sceptre-project/config/10/A.yaml @@ -1 +1,2 @@ -template_path: sam_template.yaml +template: + path: sam_template.yaml diff --git a/integration-tests/sceptre-project/config/11/A.yaml b/integration-tests/sceptre-project/config/11/A.yaml index a7ec2dcea..6a97eac6c 100644 --- a/integration-tests/sceptre-project/config/11/A.yaml +++ b/integration-tests/sceptre-project/config/11/A.yaml @@ -1 +1,2 @@ -template_path: sam_updated_template.yaml +template: + path: sam_updated_template.yaml diff --git a/integration-tests/sceptre-project/config/12/1/2/3/C.yaml b/integration-tests/sceptre-project/config/12/1/2/3/C.yaml index d57a8d3ed..4c17329ad 100644 --- a/integration-tests/sceptre-project/config/12/1/2/3/C.yaml +++ b/integration-tests/sceptre-project/config/12/1/2/3/C.yaml @@ -1,4 +1,5 @@ -template_path: valid_template.json +template: + path: valid_template.json stack_tags: Project: '{{ project }}' Key: '{{ keyC }}' diff --git a/integration-tests/sceptre-project/config/12/1/2/B.yaml b/integration-tests/sceptre-project/config/12/1/2/B.yaml index 345f45f7b..1e9de974b 100644 --- a/integration-tests/sceptre-project/config/12/1/2/B.yaml +++ b/integration-tests/sceptre-project/config/12/1/2/B.yaml @@ -1,4 +1,5 @@ -template_path: valid_template.json +template: + path: valid_template.json stack_tags: Project: '{{ project }}' Key: '{{ keyB }}' diff --git a/integration-tests/sceptre-project/config/12/1/A.yaml b/integration-tests/sceptre-project/config/12/1/A.yaml index d7ba8b4e3..dcb3a0c8c 100644 --- a/integration-tests/sceptre-project/config/12/1/A.yaml +++ b/integration-tests/sceptre-project/config/12/1/A.yaml @@ -1,4 +1,5 @@ -template_path: valid_template.json +template: + path: valid_template.json stack_tags: Key: '{{ keyA }}' Project: '{{ project }}' diff --git a/integration-tests/sceptre-project/config/2/A.yaml b/integration-tests/sceptre-project/config/2/A.yaml index 87cf59424..b0e4f9065 100644 --- a/integration-tests/sceptre-project/config/2/A.yaml +++ b/integration-tests/sceptre-project/config/2/A.yaml @@ -1 +1,2 @@ -template_path: updated_template.json +template: + path: updated_template.json diff --git a/integration-tests/sceptre-project/config/2/B.yaml b/integration-tests/sceptre-project/config/2/B.yaml index 87cf59424..b0e4f9065 100644 --- a/integration-tests/sceptre-project/config/2/B.yaml +++ b/integration-tests/sceptre-project/config/2/B.yaml @@ -1 +1,2 @@ -template_path: updated_template.json +template: + path: updated_template.json diff --git a/integration-tests/sceptre-project/config/2/C.yaml b/integration-tests/sceptre-project/config/2/C.yaml index 87cf59424..b0e4f9065 100644 --- a/integration-tests/sceptre-project/config/2/C.yaml +++ b/integration-tests/sceptre-project/config/2/C.yaml @@ -1 +1,2 @@ -template_path: updated_template.json +template: + path: updated_template.json diff --git a/integration-tests/sceptre-project/config/3/A.yaml b/integration-tests/sceptre-project/config/3/A.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/3/A.yaml +++ b/integration-tests/sceptre-project/config/3/A.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/3/B.yaml b/integration-tests/sceptre-project/config/3/B.yaml index 514c5e3ea..ebe85fdbc 100644 --- a/integration-tests/sceptre-project/config/3/B.yaml +++ b/integration-tests/sceptre-project/config/3/B.yaml @@ -1,3 +1,4 @@ dependencies: - 3/A.yaml -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/3/C.yaml b/integration-tests/sceptre-project/config/3/C.yaml index f9e932a62..980d0690c 100644 --- a/integration-tests/sceptre-project/config/3/C.yaml +++ b/integration-tests/sceptre-project/config/3/C.yaml @@ -1,3 +1,4 @@ dependencies: - 3/B.yaml -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/4/A.yaml b/integration-tests/sceptre-project/config/4/A.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/4/A.yaml +++ b/integration-tests/sceptre-project/config/4/A.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/4/B.yaml b/integration-tests/sceptre-project/config/4/B.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/4/B.yaml +++ b/integration-tests/sceptre-project/config/4/B.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/4/C.yaml b/integration-tests/sceptre-project/config/4/C.yaml index d776e67d9..30842a6fe 100644 --- a/integration-tests/sceptre-project/config/4/C.yaml +++ b/integration-tests/sceptre-project/config/4/C.yaml @@ -1,3 +1,4 @@ -template_path: valid_template.json +template: + path: valid_template.json dependencies: - 3/A.yaml diff --git a/integration-tests/sceptre-project/config/5/1/A.yaml b/integration-tests/sceptre-project/config/5/1/A.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/5/1/A.yaml +++ b/integration-tests/sceptre-project/config/5/1/A.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/5/1/B.yaml b/integration-tests/sceptre-project/config/5/1/B.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/5/1/B.yaml +++ b/integration-tests/sceptre-project/config/5/1/B.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/5/1/C.yaml b/integration-tests/sceptre-project/config/5/1/C.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/5/1/C.yaml +++ b/integration-tests/sceptre-project/config/5/1/C.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/5/2/A.yaml b/integration-tests/sceptre-project/config/5/2/A.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/5/2/A.yaml +++ b/integration-tests/sceptre-project/config/5/2/A.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/5/2/B.yaml b/integration-tests/sceptre-project/config/5/2/B.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/5/2/B.yaml +++ b/integration-tests/sceptre-project/config/5/2/B.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/5/2/C.yaml b/integration-tests/sceptre-project/config/5/2/C.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/5/2/C.yaml +++ b/integration-tests/sceptre-project/config/5/2/C.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/6/1/A.yaml b/integration-tests/sceptre-project/config/6/1/A.yaml index 70c25a81c..42dd6738a 100644 --- a/integration-tests/sceptre-project/config/6/1/A.yaml +++ b/integration-tests/sceptre-project/config/6/1/A.yaml @@ -1 +1,2 @@ -template_path: dependencies/independent_template.json +template: + path: dependencies/independent_template.json diff --git a/integration-tests/sceptre-project/config/6/1/B.yaml b/integration-tests/sceptre-project/config/6/1/B.yaml index fbd458ec8..4bf13facf 100644 --- a/integration-tests/sceptre-project/config/6/1/B.yaml +++ b/integration-tests/sceptre-project/config/6/1/B.yaml @@ -1,3 +1,4 @@ -template_path: dependencies/dependent_template.json +template: + path: dependencies/dependent_template.json parameters: DependentStackName: !stack_output 6/1/A.yaml::StackName diff --git a/integration-tests/sceptre-project/config/6/1/C.yaml b/integration-tests/sceptre-project/config/6/1/C.yaml index c2c5560e9..533e16a94 100644 --- a/integration-tests/sceptre-project/config/6/1/C.yaml +++ b/integration-tests/sceptre-project/config/6/1/C.yaml @@ -1,3 +1,4 @@ -template_path: dependencies/dependent_template.json +template: + path: dependencies/dependent_template.json parameters: DependentStackName: !stack_output 6/1/B.yaml::StackName diff --git a/integration-tests/sceptre-project/config/6/2/A.yaml b/integration-tests/sceptre-project/config/6/2/A.yaml index 42549a093..7832e2386 100644 --- a/integration-tests/sceptre-project/config/6/2/A.yaml +++ b/integration-tests/sceptre-project/config/6/2/A.yaml @@ -1,2 +1,3 @@ -template_path: dependencies/independent_template.json +template: + path: dependencies/independent_template.json region: eu-west-1 diff --git a/integration-tests/sceptre-project/config/6/2/B.yaml b/integration-tests/sceptre-project/config/6/2/B.yaml index 03a4ce3c0..a0c604bc6 100644 --- a/integration-tests/sceptre-project/config/6/2/B.yaml +++ b/integration-tests/sceptre-project/config/6/2/B.yaml @@ -1,4 +1,5 @@ -template_path: dependencies/dependent_template_local_export.json +template: + path: dependencies/dependent_template_local_export.json region: eu-west-2 parameters: DependentStackName: !stack_output 6/2/A.yaml::StackName diff --git a/integration-tests/sceptre-project/config/6/2/C.yaml b/integration-tests/sceptre-project/config/6/2/C.yaml index d898ebd51..712f8f1c3 100644 --- a/integration-tests/sceptre-project/config/6/2/C.yaml +++ b/integration-tests/sceptre-project/config/6/2/C.yaml @@ -1,4 +1,5 @@ -template_path: dependencies/dependent_template_local_export.json +template: + path: dependencies/dependent_template_local_export.json region: eu-west-3 parameters: DependentStackName: !stack_output 6/2/B.yaml::StackName diff --git a/integration-tests/sceptre-project/config/6/3/A.yaml b/integration-tests/sceptre-project/config/6/3/A.yaml index fbb2444bf..1cf4462fc 100644 --- a/integration-tests/sceptre-project/config/6/3/A.yaml +++ b/integration-tests/sceptre-project/config/6/3/A.yaml @@ -1,3 +1,4 @@ -template_path: output_template.json +template: + path: output_template.json parameters: Input: "TestValue" diff --git a/integration-tests/sceptre-project/config/6/3/B.yaml b/integration-tests/sceptre-project/config/6/3/B.yaml index 18a4193ce..1f2bb13a9 100644 --- a/integration-tests/sceptre-project/config/6/3/B.yaml +++ b/integration-tests/sceptre-project/config/6/3/B.yaml @@ -1,3 +1,4 @@ -template_path: input_output_template.json +template: + path: input_output_template.json parameters: Input: !stack_output 6/3/A.yaml::Output diff --git a/integration-tests/sceptre-project/config/6/3/C.yaml b/integration-tests/sceptre-project/config/6/3/C.yaml index 615cb3311..00ff1711a 100644 --- a/integration-tests/sceptre-project/config/6/3/C.yaml +++ b/integration-tests/sceptre-project/config/6/3/C.yaml @@ -1,3 +1,4 @@ -template_path: input_output_template.json +template: + path: input_output_template.json parameters: Input: !stack_output 6/3/B.yaml::Output diff --git a/integration-tests/sceptre-project/config/6/4/1/A.yaml b/integration-tests/sceptre-project/config/6/4/1/A.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/6/4/1/A.yaml +++ b/integration-tests/sceptre-project/config/6/4/1/A.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/6/4/1/B.yaml b/integration-tests/sceptre-project/config/6/4/1/B.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/6/4/1/B.yaml +++ b/integration-tests/sceptre-project/config/6/4/1/B.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/6/4/1/C.yaml b/integration-tests/sceptre-project/config/6/4/1/C.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/6/4/1/C.yaml +++ b/integration-tests/sceptre-project/config/6/4/1/C.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/6/4/2/A.yaml b/integration-tests/sceptre-project/config/6/4/2/A.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/6/4/2/A.yaml +++ b/integration-tests/sceptre-project/config/6/4/2/A.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/6/4/2/B.yaml b/integration-tests/sceptre-project/config/6/4/2/B.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/6/4/2/B.yaml +++ b/integration-tests/sceptre-project/config/6/4/2/B.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/6/4/2/C.yaml b/integration-tests/sceptre-project/config/6/4/2/C.yaml index c74618e06..0d905dfe0 100644 --- a/integration-tests/sceptre-project/config/6/4/2/C.yaml +++ b/integration-tests/sceptre-project/config/6/4/2/C.yaml @@ -1 +1,2 @@ -template_path: valid_template.json +template: + path: valid_template.json diff --git a/integration-tests/sceptre-project/config/7/A.yaml b/integration-tests/sceptre-project/config/7/A.yaml index cb292d02f..c94ce5bb6 100644 --- a/integration-tests/sceptre-project/config/7/A.yaml +++ b/integration-tests/sceptre-project/config/7/A.yaml @@ -1,3 +1,4 @@ sceptre_user_data: type: AWS::CloudFormation::WaitConditionHandle -template_path: invalid_template_missing_attr.j2 +template: + path: invalid_template_missing_attr.j2 diff --git a/integration-tests/sceptre-project/config/8/A.yaml b/integration-tests/sceptre-project/config/8/A.yaml index ba1f80440..ae471013d 100644 --- a/integration-tests/sceptre-project/config/8/A.yaml +++ b/integration-tests/sceptre-project/config/8/A.yaml @@ -1 +1,2 @@ -template_path: invalid_template.json +template: + path: invalid_template.json diff --git a/integration-tests/sceptre-project/config/8/B.yaml b/integration-tests/sceptre-project/config/8/B.yaml index a804a2ff3..42fee7e6c 100644 --- a/integration-tests/sceptre-project/config/8/B.yaml +++ b/integration-tests/sceptre-project/config/8/B.yaml @@ -1,2 +1,3 @@ on_failure: DO_NOTHING -template_path: invalid_template.json +template: + path: invalid_template.json diff --git a/integration-tests/sceptre-project/config/8/C.yaml b/integration-tests/sceptre-project/config/8/C.yaml index 6728bff2f..15ee72a33 100644 --- a/integration-tests/sceptre-project/config/8/C.yaml +++ b/integration-tests/sceptre-project/config/8/C.yaml @@ -1,2 +1,3 @@ stack_timeout: 1 -template_path: valid_template_wait_300.json +template: + path: valid_template_wait_300.json diff --git a/integration-tests/sceptre-project/config/9/A.yaml b/integration-tests/sceptre-project/config/9/A.yaml index 42549a093..7832e2386 100644 --- a/integration-tests/sceptre-project/config/9/A.yaml +++ b/integration-tests/sceptre-project/config/9/A.yaml @@ -1,2 +1,3 @@ -template_path: dependencies/independent_template.json +template: + path: dependencies/independent_template.json region: eu-west-1 diff --git a/integration-tests/sceptre-project/config/9/B.yaml b/integration-tests/sceptre-project/config/9/B.yaml index ad179dacc..a6e1ee08e 100644 --- a/integration-tests/sceptre-project/config/9/B.yaml +++ b/integration-tests/sceptre-project/config/9/B.yaml @@ -1,4 +1,5 @@ -template_path: dependencies/dependent_template_local_export.json +template: + path: dependencies/dependent_template_local_export.json region: eu-west-1 parameters: DependentStackName: !stack_output_external "{project_code}-9-A::StackName default::eu-west-1" diff --git a/integration-tests/sceptre-project/config/project-deps/main-project/config.yaml b/integration-tests/sceptre-project/config/project-deps/main-project/config.yaml index 73b93541d..f03ad5867 100644 --- a/integration-tests/sceptre-project/config/project-deps/main-project/config.yaml +++ b/integration-tests/sceptre-project/config/project-deps/main-project/config.yaml @@ -2,8 +2,8 @@ template_bucket_name: !stack_output project-deps/dependencies/bucket.yaml::Bucke notifications: - !stack_output project-deps/dependencies/topic.yaml::TopicArn -iam_role: !stack_output project-deps/dependencies/assumed-role.yaml::RoleArn -iam_role_session_duration: 1800 +sceptre_role: !stack_output project-deps/dependencies/assumed-role.yaml::RoleArn +sceptre_role_session_duration: 1800 stack_tags: greeting: !rcmd "echo 'hello' | tr -d '\n'" nonexistant: !no_value diff --git a/requirements/prod.txt b/requirements/prod.txt index 737a30b57..a094c4fef 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -3,6 +3,7 @@ click>=7.0,<9.0 colorama>=0.2.5,<0.4.4 cfn-flip>=1.2.3,<2.0 deepdiff>=5.5.0,<6.0 +deprecation>=2.0.0,<3.0 Jinja2>=3.0,<4 jsonschema>=3.2,<3.3 networkx>=2.6,<2.7 diff --git a/sceptre/__init__.py b/sceptre/__init__.py index a292dfc95..22f0adabd 100644 --- a/sceptre/__init__.py +++ b/sceptre/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging +import sys import warnings @@ -16,7 +17,7 @@ def emit(self, record): pass -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) +if not sys.warnoptions: + warnings.filterwarnings("default", category=DeprecationWarning, module="sceptre") logging.getLogger("sceptre").addHandler(NullHandler()) diff --git a/sceptre/cli/diff.py b/sceptre/cli/diff.py index dc34a0fc2..303b156c5 100644 --- a/sceptre/cli/diff.py +++ b/sceptre/cli/diff.py @@ -88,7 +88,7 @@ def diff_command( \b * parameters * notifications - * role_arn + * cloudformation_service_role * stack_tags Important: There are resolvers (notably !stack_output) that rely on other stacks diff --git a/sceptre/config/reader.py b/sceptre/config/reader.py index 2bd664263..cc85d29ba 100644 --- a/sceptre/config/reader.py +++ b/sceptre/config/reader.py @@ -42,7 +42,9 @@ "dependencies": strategies.list_join, "hooks": strategies.child_wins, "iam_role": strategies.child_wins, + "sceptre_role": strategies.child_wins, "iam_role_session_duration": strategies.child_wins, + "sceptre_role_session_duration": strategies.child_wins, "notifications": strategies.child_wins, "on_failure": strategies.child_wins, "parameters": strategies.child_wins, @@ -52,14 +54,15 @@ "region": strategies.child_wins, "required_version": strategies.child_wins, "role_arn": strategies.child_wins, + "cloudformation_service_role": strategies.child_wins, "sceptre_user_data": strategies.child_wins, "stack_name": strategies.child_wins, "stack_tags": strategies.child_wins, "stack_timeout": strategies.child_wins, "template_bucket_name": strategies.child_wins, "template_key_value": strategies.child_wins, - "template_path": strategies.child_wins, "template": strategies.child_wins, + "template_path": strategies.child_wins, "ignore": strategies.child_wins, "obsolete": strategies.child_wins, } @@ -82,13 +85,16 @@ "dependencies", "hooks", "iam_role", + "sceptre_role", "iam_role_session_duration", + "sceptre_role_session_duration", "notifications", "on_failure", "parameters", "profile", "protect", "role_arn", + "cloudformation_service_role", "sceptre_user_data", "stack_name", "stack_tags", @@ -581,7 +587,9 @@ def _construct_stack(self, rel_path, stack_group_config=None): template_bucket_name=config.get("template_bucket_name"), template_key_prefix=config.get("template_key_prefix"), required_version=config.get("required_version"), + sceptre_role=config.get("sceptre_role"), iam_role=config.get("iam_role"), + sceptre_role_session_duration=config.get("sceptre_role_session_duration"), iam_role_session_duration=config.get("iam_role_session_duration"), profile=config.get("profile"), parameters=config.get("parameters", {}), @@ -590,6 +598,7 @@ def _construct_stack(self, rel_path, stack_group_config=None): s3_details=s3_details, dependencies=config.get("dependencies", []), role_arn=config.get("role_arn"), + cloudformation_service_role=config.get("cloudformation_service_role"), protected=config.get("protect", False), tags=config.get("stack_tags", {}), external_name=config.get("stack_name"), diff --git a/sceptre/connection_manager.py b/sceptre/connection_manager.py index fabb8c8fa..dbc83b927 100644 --- a/sceptre/connection_manager.py +++ b/sceptre/connection_manager.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - """ sceptre.connection_manager @@ -13,14 +12,16 @@ import random import threading import time +import warnings from typing import Optional, Dict import boto3 +import deprecation from botocore.credentials import Credentials from botocore.exceptions import ClientError from sceptre.exceptions import InvalidAWSCredentialsError, RetryLimitExceededError -from sceptre.helpers import mask_key +from sceptre.helpers import mask_key, create_deprecated_alias_property def _retry_boto_call(func): @@ -82,10 +83,10 @@ class ConnectionManager(object): the various AWS services that Sceptre needs to interact with. :param profile: The AWS credentials profile that should be used. - :param iam_role: The iam_role that should be assumed in the account. + :param sceptre_role: The sceptre_role that should be assumed in the account. :param stack_name: The CloudFormation stack name for this connection. :param region: The region to use. - :param iam_role_session_duration: The duration to assume the specified iam_role per session. + :param sceptre_role_session_duration: The duration to assume the specified sceptre_role per session. """ _session_lock = threading.Lock() @@ -94,13 +95,21 @@ class ConnectionManager(object): _clients = {} _stack_keys = {} + iam_role = create_deprecated_alias_property( + "iam_role", "sceptre_role", "4.0.0", "5.0.0" + ) + sceptre_role_session_duration = 0 + iam_role_session_duration = create_deprecated_alias_property( + "iam_role_session_duration", "sceptre_role_session_duration", "4.0.0", "5.0.0" + ) + def __init__( self, region: str, profile: Optional[str] = None, stack_name: Optional[str] = None, - iam_role: Optional[str] = None, - iam_role_session_duration: Optional[int] = None, + sceptre_role: Optional[str] = None, + sceptre_role_session_duration: Optional[int] = None, *, session_class=boto3.Session, get_envs_func=lambda: os.environ, @@ -111,11 +120,11 @@ def __init__( self.region = region self.profile = profile self.stack_name = stack_name - self.iam_role = iam_role - self.iam_role_session_duration = iam_role_session_duration + self.sceptre_role = sceptre_role + self.sceptre_role_session_duration = sceptre_role_session_duration if stack_name: - self._stack_keys[stack_name] = (region, profile, iam_role) + self._stack_keys[stack_name] = (region, profile, sceptre_role) self._session_class = session_class self._get_envs = get_envs_func @@ -123,12 +132,12 @@ def __init__( def __repr__(self): return ( "sceptre.connection_manager.ConnectionManager(region='{0}', " - "profile='{1}', stack_name='{2}', iam_role='{3}', iam_role_session_duration='{4}')".format( + "profile='{1}', stack_name='{2}', sceptre_role='{3}', sceptre_role_session_duration='{4}')".format( self.region, self.profile, self.stack_name, - self.iam_role, - self.iam_role_session_duration, + self.sceptre_role, + self.sceptre_role_session_duration, ) ) @@ -136,12 +145,14 @@ def get_session( self, profile: Optional[str] = STACK_DEFAULT, region: Optional[str] = STACK_DEFAULT, + sceptre_role: Optional[str] = STACK_DEFAULT, + *, iam_role: Optional[str] = STACK_DEFAULT, ) -> boto3.Session: """ - Returns a boto3 session for the targeted profile, region, and iam_role. + Returns a boto3 session for the targeted profile, region, and sceptre_role. - For each of profile, region, and iam_role, these values will default to the ConnectionManager's + For each of profile, region, and sceptre_role, these values will default to the ConnectionManager's configured default values (which correspond to the Stack's configuration). These values can be overridden, however, by passing them explicitly. @@ -150,23 +161,40 @@ def get_session( configured profile (if there is one). :param region: The AWS Region the session should be configured with. Defaults to the ConnectionManager's configured region. - :param iam_role: The IAM role ARN that is assumed using STS to create the session. Passing + :param sceptre_role: The IAM role ARN that is assumed using STS to create the session. Passing None will result in no IAM role being assumed. Defaults to the ConnectionManager's - configured iam_role (if there is one). + configured sceptre_role (if there is one). + :param iam_role: An alias for sceptre_role; Deprecated in v4.0.0 and will be removed in + v5.0.0. :returns: The Boto3 session. :raises: botocore.exceptions.ClientError """ profile = self.profile if profile == STACK_DEFAULT else profile region = self.region if region == STACK_DEFAULT else region - iam_role = self.iam_role if iam_role == STACK_DEFAULT else iam_role - return self._get_session(profile, region, iam_role) + sceptre_role = ( + self.sceptre_role if sceptre_role == STACK_DEFAULT else sceptre_role + ) + if sceptre_role == STACK_DEFAULT and iam_role != STACK_DEFAULT: + self._emit_iam_role_deprecation_warning() + sceptre_role = iam_role + + return self._get_session(profile, region, sceptre_role) + + def _emit_iam_role_deprecation_warning(self): + warnings.warn( + deprecation.DeprecatedWarning( + "The iam_role parameter", "4.0.0", "5.0.0", "Use sceptre_role instead" + ), + DeprecationWarning, + stacklevel=3, + ) def create_session_environment_variables( self, profile: Optional[str] = STACK_DEFAULT, region: Optional[str] = STACK_DEFAULT, - iam_role: Optional[str] = STACK_DEFAULT, + sceptre_role: Optional[str] = STACK_DEFAULT, include_system_envs: bool = True, ) -> Dict[str, str]: """Creates the standard AWS environment variables that would need to be passed to a @@ -175,7 +203,7 @@ def create_session_environment_variables( The environment variables returned by this method should be everything needed for subprocesses to properly interact with AWS using the ConnectionManager's configurations for - profile, iam_role, and region. By default, they include the other process environment + profile, sceptre_role, and region. By default, they include the other process environment variables, such as PATH and any others. If you do not want the other environment variables, you can toggle these off via include_system_envs=False. @@ -192,9 +220,9 @@ def create_session_environment_variables( configured profile (if there is one). :param region: The AWS Region the session should be configured with. Defaults to the ConnectionManager's configured region. - :param iam_role: The IAM role ARN that is assumed using STS to create the session. Passing + :param sceptre_role: The IAM role ARN that is assumed using STS to create the session. Passing None will result in no IAM role being assumed. Defaults to the ConnectionManager's - configured iam_role (if there is one). + configured sceptre_role (if there is one). :param include_system_envs: If True, will return a dict with all the system environment variables included. This is useful for creating a complete dict of environment variables to pass to a subprocess. If set to False, this method will ONLY return the relevant AWS @@ -202,7 +230,7 @@ def create_session_environment_variables( :returns: A dict of environment variables with the appropriate credentials available for use. """ - session = self.get_session(profile, region, iam_role) + session = self.get_session(profile, region, sceptre_role) # Set aws environment variables specific to whatever AWS configuration has been set on the # stack's connection manager. credentials: Credentials = session.get_credentials() @@ -232,11 +260,20 @@ def create_session_environment_variables( return envs def _get_session( - self, profile: Optional[str], region: Optional[str], iam_role: Optional[str] + self, + profile: Optional[str], + region: Optional[str], + sceptre_role: Optional[str], + *, + iam_role: Optional[str] = None, ) -> boto3.Session: + if iam_role is not None: + self._emit_iam_role_deprecation_warning() + sceptre_role = iam_role + with self._session_lock: self.logger.debug("Getting Boto3 session") - key = (region, profile, iam_role) + key = (region, profile, sceptre_role) if self._boto_sessions.get(key) is None: self.logger.debug("No Boto3 session found, creating one...") @@ -261,18 +298,18 @@ def _get_session( ) ) - if iam_role: + if sceptre_role: sts_client = session.client("sts") # maximum session name length is 64 chars. 56 + "-session" = 64 - session_name = f'{iam_role.split("/")[-1][:56]}-session' + session_name = f'{sceptre_role.split("/")[-1][:56]}-session' assume_role_kwargs = { - "RoleArn": iam_role, + "RoleArn": sceptre_role, "RoleSessionName": session_name, } - if self.iam_role_session_duration: + if self.sceptre_role_session_duration: assume_role_kwargs[ "DurationSeconds" - ] = self.iam_role_session_duration + ] = self.sceptre_role_session_duration sts_response = sts_client.assume_role(**assume_role_kwargs) credentials = sts_response["Credentials"] @@ -286,7 +323,7 @@ def _get_session( if session.get_credentials() is None: raise InvalidAWSCredentialsError( "Session credentials were not found. Role: {0}. Region: {1}.".format( - iam_role, region + sceptre_role, region ) ) @@ -308,7 +345,7 @@ def _get_session( return self._boto_sessions[key] - def _get_client(self, service, region, profile, stack_name, iam_role): + def _get_client(self, service, region, profile, stack_name, sceptre_role): """ Returns the Boto3 client associated with . @@ -321,11 +358,11 @@ def _get_client(self, service, region, profile, stack_name, iam_role): :rtype: boto3.client.Client """ with self._client_lock: - key = (service, region, profile, stack_name, iam_role) + key = (service, region, profile, stack_name, sceptre_role) if self._clients.get(key) is None: self.logger.debug("No %s client found, creating one...", service) self._clients[key] = self._get_session( - profile, region, iam_role + profile, region, sceptre_role ).client(service) return self._clients[key] @@ -339,6 +376,8 @@ def call( profile=None, region=None, stack_name=None, + sceptre_role=None, + *, iam_role=None, ): """ @@ -353,18 +392,21 @@ def call( :param kwargs: The keyword arguments to supply to . :type kwargs: dict :returns: The response from the Boto3 call. - :rtype: dict """ - if region is None and profile is None and iam_role is None: + if iam_role is not None: + self._emit_iam_role_deprecation_warning() + sceptre_role = iam_role + + if region is None and profile is None and sceptre_role is None: if stack_name and stack_name in self._stack_keys: - region, profile, iam_role = self._stack_keys[stack_name] + region, profile, sceptre_role = self._stack_keys[stack_name] else: region = self.region profile = self.profile - iam_role = self.iam_role + sceptre_role = self.sceptre_role if kwargs is None: # pragma: no cover kwargs = {} - client = self._get_client(service, region, profile, stack_name, iam_role) + client = self._get_client(service, region, profile, stack_name, sceptre_role) return getattr(client, command)(**kwargs) diff --git a/sceptre/diffing/stack_differ.py b/sceptre/diffing/stack_differ.py index 577a4592e..eeb942e83 100644 --- a/sceptre/diffing/stack_differ.py +++ b/sceptre/diffing/stack_differ.py @@ -34,7 +34,7 @@ class StackConfiguration(NamedTuple): parameters: Dict[str, Union[str, List[str]]] stack_tags: Dict[str, str] notifications: List[str] - role_arn: Optional[str] + cloudformation_service_role: Optional[str] class StackDiff(NamedTuple): @@ -153,7 +153,7 @@ def _create_generated_config(self, stack: Stack) -> StackConfiguration: parameters=parameters, stack_tags=stack.tags, notifications=stack.notifications, - role_arn=stack.role_arn, + cloudformation_service_role=stack.cloudformation_service_role, ) return stack_configuration @@ -193,7 +193,7 @@ def _create_deployed_stack_config( stack_tags={tag["Key"]: tag["Value"] for tag in stack["Tags"]}, stack_name=stack["StackName"], notifications=stack["NotificationARNs"], - role_arn=stack.get("RoleARN"), + cloudformation_service_role=stack.get("RoleARN"), ) def _handle_special_parameter_situations( diff --git a/sceptre/helpers.py b/sceptre/helpers.py index 3392f29b9..266d6acfa 100644 --- a/sceptre/helpers.py +++ b/sceptre/helpers.py @@ -5,8 +5,10 @@ from typing import Optional, Any, List import dateutil.parser +import deprecation from sceptre.exceptions import PathConversionError +from sceptre import __version__ def get_external_stack_name(project_code, stack_name): @@ -152,3 +154,44 @@ def gen_repr(instance: Any, class_label: str = None, attributes: List[str] = []) [f"{a}={repr(instance.__getattribute__(a))}" for a in attributes] ) return f"{class_label}({attr_str})" + + +def create_deprecated_alias_property( + alias_from: str, alias_to: str, deprecated_in: str, removed_in: Optional[str] +) -> property: + """Creates a property object with a deprecated getter and a deprecated setter that emit warnings + when used, aliasing to their renamed property names. + + :param alias_from: The name of the attribute that is deprecated and that needs to be aliased + :param alias_to: The name of the attribute to alias the deprecated field to. + :param deprecated_in: The version in which the property is deprecated. + :param removed_in: The version when it will be removed, after which the alias will no longer work. + This value can be None, indicating that removal is not yet planned. + :return: A property object to be assigned directly onto a class. + """ + + def getter(self): + return getattr(self, alias_to) + + getter.__name__ = alias_from + + def setter(self, value): + setattr(self, alias_to, value) + + setter.__name__ = alias_from + + deprecation_kwargs = dict( + deprecated_in=deprecated_in, + removed_in=removed_in, + current_version=__version__, + details=( + f'It is being renamed to "{alias_to}". You should migrate all uses of "{alias_from}" to ' + f"that in order to avoid future breakage." + ), + ) + + deprecated_getter = deprecation.deprecated(**deprecation_kwargs)(getter) + deprecated_setter = deprecation.deprecated(**deprecation_kwargs)(setter) + + deprecated_property = property(deprecated_getter, deprecated_setter) + return deprecated_property diff --git a/sceptre/plan/actions.py b/sceptre/plan/actions.py index d174f9243..bbcacdf53 100644 --- a/sceptre/plan/actions.py +++ b/sceptre/plan/actions.py @@ -37,7 +37,7 @@ from sceptre.diffing.stack_differ import StackDiff, StackDiffer -class StackActions(object): +class StackActions: """ StackActions stores the operations a Stack can take, such as creating or deleting the Stack. @@ -54,8 +54,8 @@ def __init__(self, stack: Stack): self.stack.region, self.stack.profile, self.stack.external_name, - self.stack.iam_role, - self.stack.iam_role_session_duration, + self.stack.sceptre_role, + self.stack.sceptre_role_session_duration, ) @add_stack_hooks @@ -719,8 +719,8 @@ def _get_role_arn(self): :returns: The a Role ARN :rtype: dict """ - if self.stack.role_arn: - return {"RoleARN": self.stack.role_arn} + if self.stack.cloudformation_service_role: + return {"RoleARN": self.stack.cloudformation_service_role} else: return {} diff --git a/sceptre/resolvers/stack_output.py b/sceptre/resolvers/stack_output.py index 2dedcdaa8..72d9a1ad1 100644 --- a/sceptre/resolvers/stack_output.py +++ b/sceptre/resolvers/stack_output.py @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): super(StackOutputBase, self).__init__(*args, **kwargs) def _get_output_value( - self, stack_name, output_key, profile=None, region=None, iam_role=None + self, stack_name, output_key, profile=None, region=None, sceptre_role=None ): """ Attempts to get the Stack output named by ``output_key`` @@ -39,7 +39,7 @@ def _get_output_value( :rtype: str :raises: sceptre.exceptions.DependencyStackMissingOutputError """ - outputs = self._get_stack_outputs(stack_name, profile, region, iam_role) + outputs = self._get_stack_outputs(stack_name, profile, region, sceptre_role) try: return outputs[output_key] @@ -50,7 +50,9 @@ def _get_output_value( ) ) - def _get_stack_outputs(self, stack_name, profile=None, region=None, iam_role=None): + def _get_stack_outputs( + self, stack_name, profile=None, region=None, sceptre_role=None + ): """ Communicates with AWS CloudFormation to fetch outputs from a specific Stack. @@ -72,7 +74,7 @@ def _get_stack_outputs(self, stack_name, profile=None, region=None, iam_role=Non profile=profile, region=region, stack_name=stack_name, - iam_role=iam_role, + sceptre_role=sceptre_role, ) except ClientError as e: if "does not exist" in e.response["Error"]["Message"]: @@ -138,7 +140,7 @@ def resolve(self): self.output_key, profile=stack.profile, region=stack.region, - iam_role=stack.iam_role, + sceptre_role=stack.sceptre_role, ) @@ -165,13 +167,13 @@ def resolve(self): profile = None region = None - iam_role = None + sceptre_role = None arguments = shlex.split(self.argument) stack_argument = arguments[0] if len(arguments) > 1: extra_args = arguments[1].split("::", 2) - profile, region, iam_role = extra_args + (3 - len(extra_args)) * [None] + profile, region, sceptre_role = extra_args + (3 - len(extra_args)) * [None] dependency_stack_name, output_key = stack_argument.split("::") return self._get_output_value( @@ -179,5 +181,5 @@ def resolve(self): output_key, profile or None, region or None, - iam_role or None, + sceptre_role or None, ) diff --git a/sceptre/stack.py b/sceptre/stack.py index a83056791..b953dcb94 100644 --- a/sceptre/stack.py +++ b/sceptre/stack.py @@ -8,11 +8,18 @@ """ import logging -from typing import List, Any +from typing import List, Any, Optional +import deprecation + +from sceptre import __version__ from sceptre.connection_manager import ConnectionManager from sceptre.exceptions import InvalidConfigFileError -from sceptre.helpers import get_external_stack_name, sceptreise_path +from sceptre.helpers import ( + get_external_stack_name, + sceptreise_path, + create_deprecated_alias_property, +) from sceptre.hooks import Hook, HookProperty from sceptre.resolvers import ( ResolvableContainerProperty, @@ -23,7 +30,7 @@ from sceptre.template import Template -class Stack(object): +class Stack: """ Stack stores information about a particular CloudFormation Stack. @@ -32,9 +39,10 @@ class Stack(object): :param project_code: A code which is prepended to the Stack names\ of all Stacks built by Sceptre. - :param template_path: The relative path to the CloudFormation, Jinja2\ + :param template_path: The relative path to the CloudFormation, Jinja2, or Python template to build the Stack from. If this is filled, - `template_handler_config` should not be filled. + `template_handler_config` should not be filled. This field has been deprecated since + version 4.0.0 and will be removed in version 5.0.0. :param template_handler_config: Configuration for a Template Handler that can resolve its arguments to a template string. Should contain the `type` property to specify @@ -60,19 +68,19 @@ class Stack(object): :param hooks: A list of arbitrary shell or python commands or scripts to\ run. - :param s3_details: + :param s3_details: Details used for uploading templates to S3. :param dependencies: The relative path to the Stack, including the file\ extension of the Stack. - :param role_arn: The ARN of a CloudFormation Service Role that is assumed\ + :param cloudformation_service_role: The ARN of a CloudFormation Service Role that is assumed\ by CloudFormation to create, update or delete resources. :param protected: Stack protection against execution. :param tags: CloudFormation Tags to be applied to the Stack. - :param external_name: + :param external_name: The real stack name used for CloudFormation :param notifications: SNS topic ARNs to publish Stack related events to.\ A maximum of 5 ARNs can be specified per Stack. @@ -82,10 +90,20 @@ class Stack(object): :param disable_rollback: If True, cloudformation will not rollback on deployment failures - :param iam_role: The ARN of a role for Sceptre to assume before interacting\ + :param iam_role: The ARN of a role for Sceptre to assume before interacting + with the environment. If not supplied, Sceptre uses the user's AWS CLI + credentials. This field has been deprecated since version 4.0.0 and will be removed in + version 5.0.0. + + :param sceptre_role: The ARN of a role for Sceptre to assume before interacting\ with the environment. If not supplied, Sceptre uses the user's AWS CLI\ credentials. + :param iam_role_session_duration: The duration in seconds of the assumed IAM role session. + This field has been deprecated since version 4.0.0 and will be removed in version 5.0.0. + + :param sceptre_role_session_duration: The duration in seconds of the assumed IAM role session. + :param profile: The name of the profile as defined in ~/.aws/config and\ ~/.aws/credentials. @@ -101,11 +119,10 @@ class Stack(object): also be deleted if the prune command is invoked or the --prune option is used with the launch command. - :param iam_role_session_duration: The session duration when Scetre assumes a role.\ + :param sceptre_role_session_duration: The session duration when Scetre assumes a role.\ If not supplied, Sceptre uses default value (3600 seconds) :param stack_group_config: The StackGroup config for the Stack - """ parameters = ResolvableContainerProperty("parameters") @@ -126,14 +143,25 @@ class Stack(object): template_bucket_name = ResolvableValueProperty( "template_bucket_name", PlaceholderType.none ) - # Similarly, the placeholder_override=None for iam_role means that actions that would otherwise - # use the iam_role will act as if there was no iam role when the iam_role stack has not been + # Similarly, the placeholder_override=None for sceptre_role means that actions that would otherwise + # use the sceptre_role will act as if there was no iam role when the sceptre_role stack has not been # deployed for commands that allow placeholders (like validate). - iam_role = ResolvableValueProperty("iam_role", PlaceholderType.none) - role_arn = ResolvableValueProperty("role_arn") + sceptre_role = ResolvableValueProperty("sceptre_role", PlaceholderType.none) + cloudformation_service_role = ResolvableValueProperty("cloudformation_service_role") hooks = HookProperty("hooks") + iam_role = create_deprecated_alias_property( + "iam_role", "sceptre_role", "4.0.0", "5.0.0" + ) + role_arn = create_deprecated_alias_property( + "role_arn", "cloudformation_service_role", "4.0.0", "5.0.0" + ) + sceptre_role_session_duration = None + iam_role_session_duration = create_deprecated_alias_property( + "iam_role_session_duration", "sceptre_role_session_duration", "4.0.0", "5.0.0" + ) + def __init__( self, name: str, @@ -148,8 +176,10 @@ def __init__( sceptre_user_data: dict = None, hooks: Hook = None, s3_details: dict = None, + sceptre_role: str = None, iam_role: str = None, dependencies: List["Stack"] = None, + cloudformation_service_role: str = None, role_arn: str = None, protected: bool = False, tags: dict = None, @@ -159,23 +189,14 @@ def __init__( disable_rollback=False, profile: str = None, stack_timeout: int = 0, - iam_role_session_duration: int = 0, + sceptre_role_session_duration: Optional[int] = None, + iam_role_session_duration: Optional[int] = None, ignore=False, obsolete=False, stack_group_config: dict = {}, ): self.logger = logging.getLogger(__name__) - if template_path and template_handler_config: - raise InvalidConfigFileError( - "Both 'template_path' and 'template' are set, specify one or the other" - ) - - if not template_path and not template_handler_config: - raise InvalidConfigFileError( - "Neither 'template_path' nor 'template' is set" - ) - self.name = sceptreise_path(name) self.project_code = project_code self.region = region @@ -183,7 +204,6 @@ def __init__( self.external_name = external_name or get_external_stack_name( self.project_code, self.name ) - self.template_path = template_path self.dependencies = dependencies or [] self.protected = protected self.on_failure = on_failure @@ -194,7 +214,12 @@ def __init__( self.stack_timeout = stack_timeout self.profile = profile self.template_key_prefix = template_key_prefix - self.iam_role_session_duration = iam_role_session_duration + self._set_field_with_deprecated_alias( + "sceptre_role_session_duration", + sceptre_role_session_duration, + "iam_role_session_duration", + iam_role_session_duration, + ) self.ignore = self._ensure_boolean("ignore", ignore) self.obsolete = self._ensure_boolean("obsolete", obsolete) @@ -203,11 +228,25 @@ def __init__( # Resolvers and hooks need to be assigned last self.s3_details = s3_details - self.iam_role = iam_role + self._set_field_with_deprecated_alias( + "sceptre_role", sceptre_role, "iam_role", iam_role + ) self.tags = tags or {} - self.role_arn = role_arn + self._set_field_with_deprecated_alias( + "cloudformation_service_role", + cloudformation_service_role, + "role_arn", + role_arn, + ) self.template_bucket_name = template_bucket_name - self.template_handler_config = template_handler_config + self._set_field_with_deprecated_alias( + "template_handler_config", + template_handler_config, + "template_path", + template_path, + required=True, + preferred_config_name="template", + ) self.s3_details = s3_details self.parameters = parameters or {} @@ -228,21 +267,20 @@ def __repr__(self): "sceptre.stack.Stack(" f"name='{self.name}', " f"project_code={self.project_code}, " - f"template_path={self.template_path}, " f"template_handler_config={self.template_handler_config}, " f"region={self.region}, " f"template_bucket_name={self.template_bucket_name}, " f"template_key_prefix={self.template_key_prefix}, " f"required_version={self.required_version}, " - f"iam_role={self.iam_role}, " - f"iam_role_session_duration={self.iam_role_session_duration}, " + f"sceptre_role={self.sceptre_role}, " + f"sceptre_role_session_duration={self.sceptre_role_session_duration}, " f"profile={self.profile}, " f"sceptre_user_data={self.sceptre_user_data}, " f"parameters={self.parameters}, " f"hooks={self.hooks}, " f"s3_details={self.s3_details}, " f"dependencies={self.dependencies}, " - f"role_arn={self.role_arn}, " + f"cloudformation_service_role={self.cloudformation_service_role}, " f"protected={self.protected}, " f"tags={self.tags}, " f"external_name={self.external_name}, " @@ -271,7 +309,8 @@ def __eq__(self, stack): and self.region == stack.region and self.template_key_prefix == stack.template_key_prefix and self.required_version == stack.required_version - and self.iam_role_session_duration == stack.iam_role_session_duration + and self.sceptre_role_session_duration + == stack.sceptre_role_session_duration and self.profile == stack.profile and self.dependencies == stack.dependencies and self.protected == stack.protected @@ -294,28 +333,28 @@ def connection_manager(self) -> ConnectionManager: if self._connection_manager is None: cache_connection_manager = True try: - iam_role = self.iam_role + sceptre_role = self.sceptre_role except RecursiveResolve: - # This would be the case when iam_role is set with a resolver (especially stack_output) + # This would be the case when sceptre_role is set with a resolver (especially stack_output) # that uses the stack's connection manager. This creates a temporary condition where # you need the iam role to get the iam role. To get around this, it will temporarily - # use None as the iam_role but will re-attempt to resolve the value in future accesses. + # use None as the sceptre_role but will re-attempt to resolve the value in future accesses. # Since the Stack Output resolver (the most likely culprit) uses the target stack's - # iam_role rather than the current stack's one anyway, it actually doesn't matter, - # since the stack defining that iam_role won't actually be using that iam_role. + # sceptre_role rather than the current stack's one anyway, it actually doesn't matter, + # since the stack defining that sceptre_role won't actually be using that sceptre_role. self.logger.debug( - "Resolving iam_role requires the Stack connection manager. Temporarily setting " - "the iam_role to None until it can be fully resolved." + "Resolving sceptre_role requires the Stack connection manager. Temporarily setting " + "the sceptre_role to None until it can be fully resolved." ) - iam_role = None + sceptre_role = None cache_connection_manager = False connection_manager = ConnectionManager( self.region, self.profile, self.external_name, - iam_role, - self.iam_role_session_duration, + sceptre_role, + self.sceptre_role_session_duration, ) if cache_connection_manager: self._connection_manager = connection_manager @@ -333,17 +372,61 @@ def template(self): :rtype: Template """ if self._template is None: - if self.template_path: - handler_config = {"type": "file", "path": self.template_path} - else: - handler_config = self.template_handler_config - self._template = Template( name=self.name, - handler_config=handler_config, + handler_config=self.template_handler_config, sceptre_user_data=self.sceptre_user_data, stack_group_config=self.stack_group_config, s3_details=self.s3_details, connection_manager=self.connection_manager, ) return self._template + + @property + @deprecation.deprecated( + "4.0.0", "5.0.0", __version__, "Use the template Stack Config key instead." + ) + def template_path(self) -> str: + """The path argument from the template_handler config. This field is deprecated as of v4.0.0 + and will be removed in v5.0.0. + """ + return self.template_handler_config["path"] + + @template_path.setter + @deprecation.deprecated( + "4.0.0", "5.0.0", __version__, "Use the template Stack Config key instead." + ) + def template_path(self, value: str): + self.template_handler_config = {"type": "file", "path": value} + + def _set_field_with_deprecated_alias( + self, + preferred_attribute_name, + preferred_value, + deprecated_attribute_name, + deprecated_value, + *, + required=False, + preferred_config_name=None, + deprecated_config_name=None, + ): + # This is a generic truthiness check. All current default values are falsy, so this should work. + # If we ever use this function where the default value is NOT falsy, this will be a problem. + preferred_config_name = preferred_config_name or preferred_attribute_name + deprecated_config_name = deprecated_config_name or deprecated_attribute_name + + if preferred_value and deprecated_value: + raise InvalidConfigFileError( + f"Both '{preferred_config_name}' and '{deprecated_config_name}' are set; You should only set a " + f"value for {preferred_config_name} because {deprecated_config_name} is deprecated." + ) + elif preferred_value: + setattr(self, preferred_attribute_name, preferred_value) + elif deprecated_value: + setattr(self, deprecated_attribute_name, deprecated_value) + elif required: + raise InvalidConfigFileError( + f"{preferred_config_name} is a required Stack Config." + ) + else: # In case they're both falsy, we should just set the value using the preferred value. + setattr(self, preferred_attribute_name, preferred_value) diff --git a/setup.py b/setup.py index 4b0548128..48073e2f9 100755 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ def get_version(rel_path): "click>=7.0,<9.0", "cfn-flip>=1.2.3,<2.0", "deepdiff>=5.5.0,<6.0", + "deprecation>=2.0.0,<3.0", "PyYaml>=5.1,<6.0", "Jinja2>=3.0,<4", "jsonschema>=3.2,<3.3", diff --git a/tests/fixtures-vpc/config/account/stack-group/region/vpc.yaml b/tests/fixtures-vpc/config/account/stack-group/region/vpc.yaml index 97aa110ed..889e79ded 100644 --- a/tests/fixtures-vpc/config/account/stack-group/region/vpc.yaml +++ b/tests/fixtures-vpc/config/account/stack-group/region/vpc.yaml @@ -1,4 +1,5 @@ -template_path: path/to/template +template: + path: path/to/template parameters: param1: val1 dependencies: diff --git a/tests/fixtures-vpc/config/top/level.yaml b/tests/fixtures-vpc/config/top/level.yaml index 460977c19..0457dab1e 100644 --- a/tests/fixtures-vpc/config/top/level.yaml +++ b/tests/fixtures-vpc/config/top/level.yaml @@ -1 +1,2 @@ -template_path: somethingelse.py +template: + path: somethingelse.py diff --git a/tests/fixtures/config/account/stack-group/region/construct_nodes.yaml b/tests/fixtures/config/account/stack-group/region/construct_nodes.yaml index 1c06705ec..928a89402 100644 --- a/tests/fixtures/config/account/stack-group/region/construct_nodes.yaml +++ b/tests/fixtures/config/account/stack-group/region/construct_nodes.yaml @@ -1,4 +1,5 @@ -template_path: path/to/template +template: + path: path/to/template parameters: param1: !environment_variable example param2: diff --git a/tests/test_actions.py b/tests/test_actions.py index fab4071fb..7bc4e5514 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -37,7 +37,7 @@ def setup_method(self, test_method): hooks={}, s3_details=None, dependencies=sentinel.dependencies, - role_arn=sentinel.role_arn, + cloudformation_service_role=sentinel.cloudformation_service_role, protected=False, tags={"tag1": "val1"}, external_name=sentinel.external_name, @@ -121,7 +121,7 @@ def test_create_sends_correct_request( "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], "OnFailure": sentinel.on_failure, @@ -154,7 +154,7 @@ def test_create_disable_rollback_overrides_on_failure( "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], "DisableRollback": True, @@ -186,7 +186,7 @@ def test_create_sends_correct_request_no_notifications( "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [], "Tags": [{"Key": "tag1", "Value": "val1"}], "OnFailure": sentinel.on_failure, @@ -217,7 +217,7 @@ def test_create_sends_correct_request_with_no_failure_no_timeout( "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], }, @@ -265,7 +265,7 @@ def test_update_sends_correct_request(self, mock_wait_for_completion): "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], }, @@ -296,7 +296,7 @@ def test_update_cancels_after_timeout(self, mock_wait_for_completion): "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], }, @@ -335,7 +335,7 @@ def test_update_sends_correct_request_no_notification( "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND", ], - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [], "Tags": [{"Key": "tag1", "Value": "val1"}], }, @@ -458,7 +458,10 @@ def test_delete_with_created_stack(self, mock_get_status, mock_wait_for_completi self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="delete_stack", - kwargs={"StackName": sentinel.external_name, "RoleARN": sentinel.role_arn}, + kwargs={ + "StackName": sentinel.external_name, + "RoleARN": sentinel.cloudformation_service_role, + }, ) @patch("sceptre.plan.actions.StackActions._wait_for_completion") @@ -570,7 +573,10 @@ def test_continue_update_rollback_sends_correct_request(self): self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="continue_update_rollback", - kwargs={"StackName": sentinel.external_name, "RoleARN": sentinel.role_arn}, + kwargs={ + "StackName": sentinel.external_name, + "RoleARN": sentinel.cloudformation_service_role, + }, ) def test_set_stack_policy_sends_correct_request(self): @@ -624,7 +630,7 @@ def test_create_change_set_sends_correct_request(self): "CAPABILITY_AUTO_EXPAND", ], "ChangeSetName": sentinel.change_set_name, - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], }, @@ -651,7 +657,7 @@ def test_create_change_set_sends_correct_request_no_notifications(self): "CAPABILITY_AUTO_EXPAND", ], "ChangeSetName": sentinel.change_set_name, - "RoleARN": sentinel.role_arn, + "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [], "Tags": [{"Key": "tag1", "Value": "val1"}], }, @@ -913,12 +919,14 @@ def test_get_status_with_unknown_clinet_error(self, mock_describe): with pytest.raises(ClientError): self.actions.get_status() - def test_get_role_arn_without_role(self): - self.actions.stack.role_arn = None + def test_get_cloudformation_service_role_without_role(self): + self.actions.stack.cloudformation_service_role = None assert self.actions._get_role_arn() == {} def test_get_role_arn_with_role(self): - assert self.actions._get_role_arn() == {"RoleARN": sentinel.role_arn} + assert self.actions._get_role_arn() == { + "RoleARN": sentinel.cloudformation_service_role + } def test_protect_execution_without_protection(self): # Function should do nothing if protect == False diff --git a/tests/test_config_reader.py b/tests/test_config_reader.py index be427729f..64fc1a168 100644 --- a/tests/test_config_reader.py +++ b/tests/test_config_reader.py @@ -283,8 +283,8 @@ def test_construct_stacks_constructs_stack( mock_Stack.assert_any_call( name="account/stack-group/region/vpc", project_code="account_project_code", - template_path="path/to/template", - template_handler_config=None, + template_path=None, + template_handler_config={"path": "path/to/template"}, region="region_region", profile="account_profile", parameters={"param1": "val1"}, @@ -293,8 +293,11 @@ def test_construct_stacks_constructs_stack( s3_details=sentinel.s3_details, dependencies=["child/level", "top/level"], iam_role=None, + sceptre_role=None, iam_role_session_duration=None, + sceptre_role_session_duration=None, role_arn=None, + cloudformation_service_role=None, protected=False, tags={}, external_name=None, @@ -381,7 +384,9 @@ def test_construct_stacks_with_valid_config( config = { "region": "region", "project_code": "project_code", - "template_path": rel_path, + "template": { + "path": rel_path, + }, } abs_path = os.path.join(config_dir, rel_path) @@ -402,7 +407,7 @@ def test_construct_stacks_with_disable_rollback_command_param(self): config = { "region": "region", "project_code": "project_code", - "template_path": rel_path, + "template": {"path": rel_path}, } abs_path = os.path.join(config_dir, rel_path) @@ -420,7 +425,7 @@ def test_construct_stacks_with_disable_rollback_in_stack_config(self): config = { "region": "region", "project_code": "project_code", - "template_path": rel_path, + "template": {"path": rel_path}, "disable_rollback": True, } @@ -481,7 +486,7 @@ def test_existing_dependency(self, filepaths, dependency): config = { "project_code": "project_code", "region": "region", - "template_path": rel_path, + "template": {"path": rel_path}, "dependencies": [dependency], } @@ -512,7 +517,7 @@ def test_missing_dependency(self, filepaths, dependency): config = { "project_code": "project_code", "region": "region", - "template_path": rel_path, + "template": {"path": rel_path}, "dependencies": [dependency], } @@ -551,7 +556,7 @@ def test_inherited_dependency_already_resolved( config = { "project_code": "project_code", "region": "region", - "template_path": rel_path, + "template": {"path": rel_path}, } abs_path = os.path.join(config_dir, rel_path) diff --git a/tests/test_connection_manager.py b/tests/test_connection_manager.py index 9795436b3..e1dec1c06 100644 --- a/tests/test_connection_manager.py +++ b/tests/test_connection_manager.py @@ -3,6 +3,7 @@ from typing import Union from unittest.mock import Mock, patch, sentinel, create_autospec +import deprecation import pytest from boto3.session import Session from botocore.exceptions import ClientError @@ -20,8 +21,8 @@ class TestConnectionManager(object): def setup_method(self, test_method): self.stack_name = None self.profile = None - self.iam_role = None - self.iam_role_session_duration = 3600 + self.sceptre_role = None + self.sceptre_role_session_duration = 3600 self.region = "eu-west-1" self.environment_variables = { @@ -39,7 +40,7 @@ def setup_method(self, test_method): region=self.region, stack_name=self.stack_name, profile=self.profile, - iam_role=self.iam_role, + sceptre_role=self.sceptre_role, session_class=self.session_class, get_envs_func=lambda: self.environment_variables, ) @@ -59,44 +60,44 @@ def test_connection_manager_initialised_with_all_parameters(self): region=self.region, stack_name="stack", profile="profile", - iam_role="iam_role", - iam_role_session_duration=21600, + sceptre_role="sceptre_role", + sceptre_role_session_duration=21600, ) assert connection_manager.stack_name == "stack" assert connection_manager.profile == "profile" - assert connection_manager.iam_role == "iam_role" - assert connection_manager.iam_role_session_duration == 21600 + assert connection_manager.sceptre_role == "sceptre_role" + assert connection_manager.sceptre_role_session_duration == 21600 assert connection_manager.region == self.region assert connection_manager._boto_sessions == {} assert connection_manager._clients == {} assert connection_manager._stack_keys == { - "stack": (self.region, "profile", "iam_role") + "stack": (self.region, "profile", "sceptre_role") } def test_repr(self): self.connection_manager.stack_name = "stack" self.connection_manager.profile = "profile" self.connection_manager.region = "region" - self.connection_manager.iam_role = "iam_role" + self.connection_manager.sceptre_role = "sceptre_role" response = self.connection_manager.__repr__() assert ( response == "sceptre.connection_manager.ConnectionManager(" "region='region', profile='profile', stack_name='stack', " - "iam_role='iam_role', iam_role_session_duration='None')" + "sceptre_role='sceptre_role', sceptre_role_session_duration='None')" ) - def test_repr_with_iam_role_session_duration(self): + def test_repr_with_sceptre_role_session_duration(self): self.connection_manager.stack_name = "stack" self.connection_manager.profile = "profile" self.connection_manager.region = "region" - self.connection_manager.iam_role = "iam_role" - self.connection_manager.iam_role_session_duration = 21600 + self.connection_manager.sceptre_role = "sceptre_role" + self.connection_manager.sceptre_role_session_duration = 21600 response = self.connection_manager.__repr__() assert ( response == "sceptre.connection_manager.ConnectionManager(" "region='region', profile='profile', stack_name='stack', " - "iam_role='iam_role', iam_role_session_duration='21600')" + "sceptre_role='sceptre_role', sceptre_role_session_duration='21600')" ) def test_boto_session_with_cache(self): @@ -107,7 +108,7 @@ def test_boto_session_with_cache(self): def test__get_session__no_args__no_defaults__makes_boto_session_with_defaults(self): self.connection_manager.profile = None - self.connection_manager.iam_role = None + self.connection_manager.sceptre_role = None boto_session = self.connection_manager.get_session() @@ -122,7 +123,7 @@ def test__get_session__no_args__no_defaults__makes_boto_session_with_defaults(se def test_get_session__no_args__connection_manager_has_profile__uses_profile(self): self.connection_manager.profile = "fancy" - self.connection_manager.iam_role = None + self.connection_manager.sceptre_role = None boto_session = self.connection_manager.get_session() @@ -167,19 +168,21 @@ def test_get_session__none_for_profile_passed__connection_manager_has_default_pr ) assert boto_session == self.mock_session - def test_get_session__no_iam_role_passed__no_iam_role_on_connection_manager__does_not_assume_role( + def test_get_session__no_sceptre_role_passed__no_sceptre_role_on_connection_manager__does_not_assume_role( self, ): - self.connection_manager.iam_role = None + self.connection_manager.sceptre_role = None self.connection_manager.get_session() self.mock_session.client.assert_not_called() - def test_get_session__none_passed_for_iam_role__iam_role_on_connection_manager__does_not_assume_role( + def test_get_session__none_passed_for_sceptre_role__sceptre_role_on_connection_manager__does_not_assume_role( self, ): - self.connection_manager.iam_role = "arn:aws:iam::123456:role/my-path/other-role" - self.connection_manager.get_session(iam_role=None) + self.connection_manager.sceptre_role = ( + "arn:aws:iam::123456:role/my-path/other-role" + ) + self.connection_manager.get_session(sceptre_role=None) self.mock_session.client.assert_not_called() @@ -198,12 +201,14 @@ def test_get_session__none_passed_for_iam_role__iam_role_on_connection_manager__ ), ], ) - def test_get_session__iam_role__assumes_that_role(self, connection_manager, arg): - self.connection_manager.iam_role = connection_manager + def test_get_session__sceptre_role__assumes_that_role( + self, connection_manager, arg + ): + self.connection_manager.sceptre_role = connection_manager kwargs = {} if arg != STACK_DEFAULT: - kwargs["iam_role"] = arg + kwargs["sceptre_role"] = arg self.connection_manager.get_session(**kwargs) @@ -222,44 +227,44 @@ def test_get_session__iam_role__assumes_that_role(self, connection_manager, arg) aws_session_token=credentials["SessionToken"], ) - def test_get_session__iam_role_and_session_duration_on_connection_manager__uses_session_duration( + def test_get_session__sceptre_role_and_session_duration_on_connection_manager__uses_session_duration( self, ): - self.connection_manager.iam_role = "iam_role" - self.connection_manager.iam_role_session_duration = 21600 + self.connection_manager.sceptre_role = "sceptre_role" + self.connection_manager.sceptre_role_session_duration = 21600 self.connection_manager.get_session() self.mock_session.client.return_value.assume_role.assert_called_once_with( - RoleArn=self.connection_manager.iam_role, + RoleArn=self.connection_manager.sceptre_role, RoleSessionName="{0}-session".format( - self.connection_manager.iam_role.split("/")[-1] + self.connection_manager.sceptre_role.split("/")[-1] ), DurationSeconds=21600, ) - def test_get_session__with_iam_role__returning_empty_credentials__raises_invalid_aws_credentials_error( + def test_get_session__with_sceptre_role__returning_empty_credentials__raises_invalid_aws_credentials_error( self, ): self.connection_manager._boto_sessions = {} - self.connection_manager.iam_role = "iam_role" + self.connection_manager.sceptre_role = "sceptre_role" self.mock_session.get_credentials.return_value = None with pytest.raises(InvalidAWSCredentialsError): self.connection_manager.get_session( - self.profile, self.region, self.connection_manager.iam_role + self.profile, self.region, self.connection_manager.sceptre_role ) def test_get_client_with_no_pre_existing_clients(self): service = "s3" region = "eu-west-1" profile = None - iam_role = None + sceptre_role = None stack = self.stack_name client = self.connection_manager._get_client( - service, region, profile, stack, iam_role + service, region, profile, stack, sceptre_role ) expected_client = self.mock_session.client.return_value assert client == expected_client @@ -268,15 +273,15 @@ def test_get_client_with_no_pre_existing_clients(self): def test_get_client_with_existing_client(self): service = "cloudformation" region = "eu-west-1" - iam_role = None + sceptre_role = None profile = None stack = self.stack_name client_1 = self.connection_manager._get_client( - service, region, profile, stack, iam_role + service, region, profile, stack, sceptre_role ) client_2 = self.connection_manager._get_client( - service, region, profile, stack, iam_role + service, region, profile, stack, sceptre_role ) assert client_1 == client_2 assert self.mock_session.client.call_count == 1 @@ -287,16 +292,16 @@ def test_get_client_with_existing_client_and_profile_none( ): service = "cloudformation" region = "eu-west-1" - iam_role = None + sceptre_role = None profile = None stack = self.stack_name self.connection_manager.profile = None client_1 = self.connection_manager._get_client( - service, region, profile, stack, iam_role + service, region, profile, stack, sceptre_role ) client_2 = self.connection_manager._get_client( - service, region, profile, stack, iam_role + service, region, profile, stack, sceptre_role ) assert client_1 == client_2 @@ -419,6 +424,23 @@ def test_create_session_environment_variables__include_system_envs_false__does_n } assert expected == result + @deprecation.fail_if_not_removed + def test_iam_role__is_removed_on_removal_version(self): + self.connection_manager.iam_role + + @deprecation.fail_if_not_removed + def test_iam_role_session_duration__is_removed_on_removal_version(self): + self.connection_manager.iam_role_session_duration + + def test_init__iam_role_fields_resolve_to_sceptre_role_fields(self): + connection_manager = ConnectionManager( + region="us-west-2", + sceptre_role="sceptre_role", + sceptre_role_session_duration=123456, + ) + assert connection_manager.iam_role == "sceptre_role" + assert connection_manager.iam_role_session_duration == 123456 + class TestRetry: def test_retry_boto_call_returns_response_correctly(self): diff --git a/tests/test_diffing/test_diff_writer.py b/tests/test_diffing/test_diff_writer.py index 29d051790..f2e8e7e82 100644 --- a/tests/test_diffing/test_diff_writer.py +++ b/tests/test_diffing/test_diff_writer.py @@ -67,7 +67,7 @@ def generated_config(self): parameters={}, stack_tags={}, notifications=[], - role_arn=None, + cloudformation_service_role=None, ) @property @@ -211,7 +211,7 @@ def setup_method(self, method): parameters={}, stack_tags={}, notifications=[], - role_arn=None, + cloudformation_service_role=None, ) self.config2 = deepcopy(self.config1) @@ -339,7 +339,7 @@ def setup_method(self, method): parameters={}, stack_tags={}, notifications=[], - role_arn=None, + cloudformation_service_role=None, ) self.config2 = deepcopy(self.config1) @@ -414,7 +414,7 @@ def setup_method(self, method): parameters={}, stack_tags={}, notifications=[], - role_arn=None, + cloudformation_service_role=None, ) self.template1 = "foo" diff --git a/tests/test_diffing/test_stack_differ.py b/tests/test_diffing/test_stack_differ.py index 8a664c357..0329195e2 100644 --- a/tests/test_diffing/test_stack_differ.py +++ b/tests/test_diffing/test_stack_differ.py @@ -38,7 +38,7 @@ class TestStackDiffer: def setup_method(self, method): self.name = "my/stack" self.external_name = "full-stack-name" - self.role_arn = "role_arn" + self.cloudformation_service_role = "cloudformation_service_role" self.parameters_on_stack_config = {"param": "some_value"} self.tags = {"tag_name": "tag_value"} self.notifications = ["notification_arn1"] @@ -51,7 +51,7 @@ def setup_method(self, method): self.local_no_echo_parameters = [] self.deployed_tags = dict(self.tags) self.deployed_notification_arns = list(self.notifications) - self.deployed_role_arn = self.role_arn + self.deployed_cloudformation_service_role = self.cloudformation_service_role self.command_capturer = Mock() self.differ = ImplementedStackDiffer(self.command_capturer) @@ -74,7 +74,7 @@ def stack(self) -> Union[Stack, Mock]: spec=Stack, external_name=self.external_name, _parameters=self.parameters_on_stack, - role_arn=self.role_arn, + cloudformation_service_role=self.cloudformation_service_role, tags=self.tags, notifications=self.notifications, __sceptre_user_data=self.sceptre_user_data, @@ -114,7 +114,7 @@ def describe_stack(self): ], "StackStatus": self.stack_status, "NotificationARNs": self.deployed_notification_arns, - "RoleARN": self.deployed_role_arn, + "RoleARN": self.deployed_cloudformation_service_role, "Tags": [ {"Key": key, "Value": value} for key, value in self.deployed_tags.items() @@ -161,7 +161,7 @@ def expected_generated_config(self): parameters=self.parameters_on_stack_config, stack_tags=deepcopy(self.tags), notifications=deepcopy(self.notifications), - role_arn=self.role_arn, + cloudformation_service_role=self.cloudformation_service_role, ) @property @@ -171,7 +171,7 @@ def expected_deployed_config(self): parameters=self.deployed_parameters, stack_tags=deepcopy(self.deployed_tags), notifications=deepcopy(self.deployed_notification_arns), - role_arn=self.deployed_role_arn, + cloudformation_service_role=self.deployed_cloudformation_service_role, ) def test_diff__compares_deployed_template_to_generated_template(self): @@ -444,7 +444,7 @@ def setup_method(self, method): parameters={"pk1": "pv1"}, stack_tags={"tk1": "tv1"}, notifications=["notification"], - role_arn=None, + cloudformation_service_role=None, ) self.config2 = StackConfiguration( @@ -452,7 +452,7 @@ def setup_method(self, method): parameters={"pk1": "pv1", "pk2": "pv2"}, stack_tags={"tk1": "tv1"}, notifications=["notification"], - role_arn="new_role", + cloudformation_service_role="new_role", ) self.template_dict_1 = { @@ -544,7 +544,7 @@ def setup_method(self, method): parameters={"pk1": "pv1"}, stack_tags={"tk1": "tv1"}, notifications=["notification"], - role_arn=None, + cloudformation_service_role=None, ) self.config2 = StackConfiguration( @@ -552,7 +552,7 @@ def setup_method(self, method): parameters={"pk1": "pv1", "pk2": "pv2"}, stack_tags={"tk1": "tv1"}, notifications=["notification"], - role_arn="new_role", + cloudformation_service_role="new_role", ) self.template_dict_1 = { @@ -632,7 +632,7 @@ def test_compare_stack_configurations__deployed_is_none__all_configs_are_falsey_ parameters={}, stack_tags={}, notifications=[], - role_arn=None, + cloudformation_service_role=None, ) comparison = self.differ.compare_stack_configurations(None, empty_config) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 911d02c5a..4d69c1e13 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- +import warnings +import deprecation import pytest from os.path import join, sep from datetime import datetime, timezone, timedelta from sceptre.exceptions import PathConversionError -from sceptre.helpers import get_external_stack_name +from sceptre.helpers import get_external_stack_name, create_deprecated_alias_property from sceptre.helpers import normalise_path from sceptre.helpers import sceptreise_path from sceptre.helpers import extract_datetime_from_aws_response_headers, gen_repr +from sceptre import __version__ class SomeClass(object): @@ -112,3 +115,59 @@ def test_repr__two_attrs__correct_order(self): def test_repr__override_label__correct_name(self): i = SomeClass("q", 123) assert gen_repr(i, class_label="My.Class") == "My.Class()" + + def test_create_deprecated_alias_property__alias_getter_returns_alias_target_value( + self, + ): + class MyClass: + target = "winner" + alias = create_deprecated_alias_property( + "alias", "target", __version__, None + ) + + obj = MyClass() + + assert obj.alias == obj.target + + def test_create_deprecated_alias_property__alias_setter_returns_alias_target_value( + self, + ): + class MyClass: + target = "loser" + alias = create_deprecated_alias_property( + "alias", "target", __version__, None + ) + + obj = MyClass() + obj.alias = expected = "winner" + assert obj.target == expected + + def test_create_deprecated_alias_property__emits_warning_when_getting_value(self): + class MyClass: + target = "winner" + alias = create_deprecated_alias_property( + "alias", "target", __version__, None + ) + + obj = MyClass() + + with warnings.catch_warnings(record=True) as messages: + obj.alias + + assert len(messages) == 1 + assert messages[0].category == deprecation.DeprecatedWarning + + def test_create_deprecated_alias_property__emits_warning_when_setting_value(self): + class MyClass: + target = "loser" + alias = create_deprecated_alias_property( + "alias", "target", __version__, None + ) + + obj = MyClass() + + with warnings.catch_warnings(record=True) as messages: + obj.alias = "winner" + + assert len(messages) == 1 + assert messages[0].category == deprecation.DeprecatedWarning diff --git a/tests/test_plan.py b/tests/test_plan.py index 44d6767ad..9e92ba9ab 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -13,7 +13,7 @@ def setup_method(self, test_method): self.stack = Stack( name="dev/app/stack", project_code=sentinel.project_code, - template_path=sentinel.template_path, + template_handler_config={"path": "/path/to/thing"}, region=sentinel.region, profile=sentinel.profile, parameters={"key1": "val1"}, @@ -21,7 +21,7 @@ def setup_method(self, test_method): hooks={}, s3_details=None, dependencies=sentinel.dependencies, - role_arn=sentinel.role_arn, + cloudformation_service_role=sentinel.cloudformation_service_role, protected=False, tags={"tag1": "val1"}, external_name=sentinel.external_name, diff --git a/tests/test_resolvers/test_stack_output.py b/tests/test_resolvers/test_stack_output.py index 6242fb80c..61347375f 100644 --- a/tests/test_resolvers/test_stack_output.py +++ b/tests/test_resolvers/test_stack_output.py @@ -29,7 +29,7 @@ def test_resolver(self, mock_get_output_value): dependency.name = "account/dev/vpc" dependency.profile = "dependency_profile" dependency.region = "dependency_region" - dependency.iam_role = "dependency_iam_role" + dependency.sceptre_role = "dependency_sceptre_role" mock_get_output_value.return_value = "output_value" @@ -46,7 +46,7 @@ def test_resolver(self, mock_get_output_value): "VpcId", profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role", + sceptre_role="dependency_sceptre_role", ) @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") @@ -61,7 +61,7 @@ def test_resolver_with_existing_dependencies(self, mock_get_output_value): dependency.name = "account/dev/vpc" dependency.profile = "dependency_profile" dependency.region = "dependency_region" - dependency.iam_role = "dependency_iam_role" + dependency.sceptre_role = "dependency_sceptre_role" mock_get_output_value.return_value = "output_value" @@ -78,7 +78,7 @@ def test_resolver_with_existing_dependencies(self, mock_get_output_value): "VpcId", profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role", + sceptre_role="dependency_sceptre_role", ) @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") @@ -94,7 +94,7 @@ def test_resolve_with_implicit_stack_reference(self, mock_get_output_value): dependency.name = "account/dev/vpc" dependency.profile = "dependency_profile" dependency.region = "dependency_region" - dependency.iam_role = "dependency_iam_role" + dependency.sceptre_role = "dependency_sceptre_role" mock_get_output_value.return_value = "output_value" @@ -111,7 +111,7 @@ def test_resolve_with_implicit_stack_reference(self, mock_get_output_value): "VpcId", profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role", + sceptre_role="dependency_sceptre_role", ) @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") @@ -129,7 +129,7 @@ def test_resolve_with_implicit_stack_reference_top_level( dependency.name = "vpc" dependency.profile = "dependency_profile" dependency.region = "dependency_region" - dependency.iam_role = "dependency_iam_role" + dependency.sceptre_role = "dependency_sceptre_role" mock_get_output_value.return_value = "output_value" @@ -146,7 +146,7 @@ def test_resolve_with_implicit_stack_reference_top_level( "VpcId", profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role", + sceptre_role="dependency_sceptre_role", ) @@ -172,12 +172,12 @@ def test_resolve_with_args(self, mock_get_output_value): stack.dependencies = [] stack._connection_manager = MagicMock(spec=ConnectionManager) stack_output_external_resolver = StackOutputExternal( - "another/account-vpc::VpcId region::profile::iam_role", stack + "another/account-vpc::VpcId region::profile::sceptre_role", stack ) mock_get_output_value.return_value = "output_value" stack_output_external_resolver.resolve() mock_get_output_value.assert_called_once_with( - "another/account-vpc", "VpcId", "region", "profile", "iam_role" + "another/account-vpc", "VpcId", "region", "profile", "sceptre_role" ) assert stack.dependencies == [] diff --git a/tests/test_stack.py b/tests/test_stack.py index 635c0f014..4683d0073 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, sentinel +import deprecation import pytest from sceptre.exceptions import InvalidConfigFileError @@ -62,8 +63,8 @@ def setup_method(self, test_method): notifications=[sentinel.notification], on_failure=sentinel.on_failure, disable_rollback=False, - iam_role=sentinel.iam_role, - iam_role_session_duration=sentinel.iam_role_session_duration, + sceptre_role=sentinel.sceptre_role, + sceptre_role_session_duration=sentinel.sceptre_role_session_duration, stack_timeout=sentinel.stack_timeout, stack_group_config={}, ) @@ -90,11 +91,14 @@ def test_initialize_stack_with_template_path(self): assert stack.parameters == {} assert stack.sceptre_user_data == {} assert stack.template_path == sentinel.template_path - assert stack.template_handler_config is None + assert stack.template_handler_config == { + "path": sentinel.template_path, + "type": "file", + } assert stack.s3_details is None assert stack._template is None assert stack.protected is False - assert stack.iam_role is None + assert stack.sceptre_role is None assert stack.role_arn is None assert stack.dependencies == [] assert stack.tags == {} @@ -104,10 +108,14 @@ def test_initialize_stack_with_template_path(self): assert stack.stack_group_config == {} def test_initialize_stack_with_template_handler(self): + expected_template_handler_config = { + "type": "file", + "path": sentinel.template_path, + } stack = Stack( name="dev/stack/app", project_code=sentinel.project_code, - template_handler_config=sentinel.template_handler_config, + template_handler_config=expected_template_handler_config, template_bucket_name=sentinel.template_bucket_name, template_key_prefix=sentinel.template_key_prefix, required_version=sentinel.required_version, @@ -123,12 +131,12 @@ def test_initialize_stack_with_template_handler(self): assert stack.hooks == {} assert stack.parameters == {} assert stack.sceptre_user_data == {} - assert stack.template_path is None - assert stack.template_handler_config == sentinel.template_handler_config + assert stack.template_path == sentinel.template_path + assert stack.template_handler_config == expected_template_handler_config assert stack.s3_details is None assert stack._template is None assert stack.protected is False - assert stack.iam_role is None + assert stack.sceptre_role is None assert stack.role_arn is None assert stack.dependencies == [] assert stack.tags == {} @@ -180,21 +188,20 @@ def test_stack_repr(self): self.stack.__repr__() == "sceptre.stack.Stack(" "name='dev/app/stack', " "project_code=sentinel.project_code, " - "template_path=sentinel.template_path, " - "template_handler_config=None, " + "template_handler_config={'type': 'file', 'path': sentinel.template_path}, " "region=sentinel.region, " "template_bucket_name=sentinel.template_bucket_name, " "template_key_prefix=sentinel.template_key_prefix, " "required_version=sentinel.required_version, " - "iam_role=sentinel.iam_role, " - "iam_role_session_duration=sentinel.iam_role_session_duration, " + "sceptre_role=sentinel.sceptre_role, " + "sceptre_role_session_duration=sentinel.sceptre_role_session_duration, " "profile=sentinel.profile, " "sceptre_user_data=sentinel.sceptre_user_data, " "parameters={'key1': 'val1'}, " "hooks={}, " "s3_details=None, " "dependencies=sentinel.dependencies, " - "role_arn=sentinel.role_arn, " + "cloudformation_service_role=sentinel.role_arn, " "protected=False, " "tags={'tag1': 'val1'}, " "external_name=sentinel.external_name, " @@ -208,19 +215,19 @@ def test_stack_repr(self): ")" ) - def test_configuration_manager__iam_role_raises_recursive_resolve__returns_connection_manager_with_no_role( + def test_configuration_manager__sceptre_role_raises_recursive_resolve__returns_connection_manager_with_no_role( self, ): class FakeResolver(Resolver): def resolve(self): - return self.stack.iam_role + return self.stack.sceptre_role - self.stack.iam_role = FakeResolver() + self.stack.sceptre_role = FakeResolver() connection_manager = self.stack.connection_manager - assert connection_manager.iam_role is None + assert connection_manager.sceptre_role is None - def test_configuration_manager__iam_role_returns_value_second_access__returns_value_on_second_access( + def test_configuration_manager__sceptre_role_returns_value_second_access__returns_value_on_second_access( self, ): class FakeResolver(Resolver): @@ -229,26 +236,52 @@ class FakeResolver(Resolver): def resolve(self): if self.access_count == 0: self.access_count += 1 - return self.stack.iam_role + return self.stack.sceptre_role else: return "role" - self.stack.iam_role = FakeResolver() + self.stack.sceptre_role = FakeResolver() - assert self.stack.connection_manager.iam_role is None - assert self.stack.connection_manager.iam_role == "role" + assert self.stack.connection_manager.sceptre_role is None + assert self.stack.connection_manager.sceptre_role == "role" - def test_configuration_manager__iam_role_returns_value__returns_connection_manager_with_that_role( + def test_configuration_manager__sceptre_role_returns_value__returns_connection_manager_with_that_role( self, ): class FakeResolver(Resolver): def resolve(self): return "role" - self.stack.iam_role = FakeResolver() + self.stack.sceptre_role = FakeResolver() connection_manager = self.stack.connection_manager - assert connection_manager.iam_role == "role" + assert connection_manager.sceptre_role == "role" + + @deprecation.fail_if_not_removed + def test_iam_role__is_removed_on_removal_version(self): + self.stack.iam_role + + @deprecation.fail_if_not_removed + def test_role_arn__is_removed_on_removal_version(self): + self.stack.role_arn + + @deprecation.fail_if_not_removed + def test_iam_role_session_duration__is_removed_on_removal_version(self): + self.stack.iam_role_session_duration + + def test_init__iam_role_set_resolves_to_sceptre_role(self): + stack = Stack("test", "test", "test", "test", iam_role="fancy") + assert stack.sceptre_role == "fancy" + + def test_init__role_arn_set_resolves_to_cloudformation_service_role(self): + stack = Stack("test", "test", "test", "test", role_arn="fancy") + assert stack.cloudformation_service_role == "fancy" + + def test_init__iam_role_session_duration_set_resolves_to_sceptre_role_session_duration( + self, + ): + stack = Stack("test", "test", "test", "test", iam_role_session_duration=123456) + assert stack.sceptre_role_session_duration == 123456 class TestStackSceptreUserData(object):