├── .dockerignore ├── .editorconfig ├── .flake8 ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── branch-integration-tests.yaml │ ├── check.yaml │ ├── gate.yaml │ ├── integration-tests.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .yamllint ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── RELEASE.md ├── behave.ini ├── docs ├── Makefile └── _source │ ├── _templates │ └── versions.html │ ├── about.rst │ ├── apidoc │ ├── modules.rst │ ├── sceptre.cli.rst │ ├── sceptre.config.rst │ ├── sceptre.diffing.rst │ ├── sceptre.hooks.rst │ ├── sceptre.plan.rst │ ├── sceptre.resolvers.rst │ └── sceptre.rst │ ├── conf.py │ ├── docs │ ├── architecture.rst │ ├── cli.rst │ ├── faq.rst │ ├── get_started.rst │ ├── hooks.rst │ ├── install.rst │ ├── permissions.rst │ ├── resolvers.rst │ ├── stack_config.rst │ ├── stack_group_config.rst │ ├── template_handlers.rst │ ├── templates.rst │ └── terminology.rst │ └── index.rst ├── integration-tests ├── __ini__.py ├── environment.py ├── features │ ├── create-change-set.feature │ ├── create-stack.feature │ ├── delete-change-set.feature │ ├── delete-stack-group.feature │ ├── delete-stack.feature │ ├── dependency-resolution.feature │ ├── describe-change-set.feature │ ├── drift.feature │ ├── dump-template-s3.feature │ ├── dump-template.feature │ ├── execute-change-set.feature │ ├── launch-stack-group.feature │ ├── launch-stack.feature │ ├── list-change-sets.feature │ ├── lock-stack.feature │ ├── project-dependencies.feature │ ├── prune.feature │ ├── stack-diff.feature │ ├── stack-output-external-resolver.feature │ ├── stack-output-resolver.feature │ ├── unlock-stack.feature │ ├── update-stack.feature │ └── validate-template.feature ├── sceptre-project │ ├── config │ │ ├── 1 │ │ │ └── A.yaml │ │ ├── 2 │ │ │ ├── A.yaml │ │ │ ├── B.yaml │ │ │ └── C.yaml │ │ ├── 3 │ │ │ ├── A.yaml │ │ │ ├── B.yaml │ │ │ └── C.yaml │ │ ├── 4 │ │ │ ├── A.yaml │ │ │ ├── B.yaml │ │ │ └── C.yaml │ │ ├── 5 │ │ │ ├── 1 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ │ └── 2 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ ├── 6 │ │ │ ├── 1 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ │ ├── 2 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ │ ├── 3 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ │ └── 4 │ │ │ │ ├── 1 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ │ │ └── 2 │ │ │ │ ├── A.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── C.yaml │ │ ├── 7 │ │ │ └── A.yaml │ │ ├── 8 │ │ │ ├── A.yaml │ │ │ ├── B.yaml │ │ │ └── C.yaml │ │ ├── 10 │ │ │ └── A.yaml │ │ ├── 11 │ │ │ └── A.yaml │ │ ├── 12 │ │ │ └── 1 │ │ │ │ ├── 2 │ │ │ │ ├── 3 │ │ │ │ │ ├── C.yaml │ │ │ │ │ └── config.yaml │ │ │ │ ├── B.yaml │ │ │ │ └── config.yaml │ │ │ │ ├── A.yaml │ │ │ │ └── config.yaml │ │ ├── 13 │ │ │ ├── A.yaml │ │ │ ├── B.yaml │ │ │ ├── C.yaml │ │ │ └── D.yaml │ │ ├── config.yaml │ │ ├── drift-group │ │ │ ├── A.yaml │ │ │ └── B.yaml │ │ ├── drift-single │ │ │ └── A.yaml │ │ ├── external-stack-output │ │ │ ├── outputter.yaml │ │ │ └── resolver-no-profile-region.yaml │ │ ├── launch-actions │ │ │ ├── deploy.yaml │ │ │ ├── ignore.yaml │ │ │ └── obsolete.yaml │ │ ├── project-deps │ │ │ ├── dependencies │ │ │ │ ├── assumed-role.yaml │ │ │ │ ├── bucket.yaml │ │ │ │ └── topic.yaml │ │ │ └── main-project │ │ │ │ ├── config.yaml │ │ │ │ └── resource.yaml │ │ └── pruning │ │ │ ├── not-obsolete.yaml │ │ │ ├── obsolete-1.yaml │ │ │ └── obsolete-2.yaml │ └── templates │ │ ├── attribute_error.py │ │ ├── dependencies │ │ ├── dependent_template.json │ │ ├── dependent_template_local_export.json │ │ └── independent_template.json │ │ ├── input_output_template.json │ │ ├── invalid_template.json │ │ ├── invalid_template.yaml │ │ ├── jinja │ │ ├── invalid_template_missing_attr.j2 │ │ ├── invalid_template_missing_key.j2 │ │ ├── valid_template.j2 │ │ ├── valid_template.json │ │ └── valid_template.yaml │ │ ├── malformed_template.json │ │ ├── malformed_template.yaml │ │ ├── missing_sceptre_handler.py │ │ ├── output_template.json │ │ ├── project-dependencies │ │ ├── assumed-role.yaml │ │ ├── bucket.yaml │ │ └── topic.yaml │ │ ├── python │ │ └── valid_template.py │ │ ├── sam_template.yaml │ │ ├── sam_updated_template.yaml │ │ ├── template.unsupported │ │ ├── topic.yaml │ │ ├── updated_template.json │ │ ├── updated_template_wait_300.json │ │ ├── valid_template.json │ │ ├── valid_template.yaml │ │ ├── valid_template_func.yaml │ │ ├── valid_template_json.py │ │ ├── valid_template_mark.yaml │ │ ├── valid_template_wait_300.json │ │ └── valid_template_yaml.py └── steps │ ├── change_sets.py │ ├── drift.py │ ├── helpers.py │ ├── project_dependencies.py │ ├── stack_groups.py │ ├── stack_policies.py │ ├── stacks.py │ └── templates.py ├── poetry.lock ├── pyproject.toml ├── sceptre ├── __init__.py ├── cli │ ├── __init__.py │ ├── create.py │ ├── delete.py │ ├── describe.py │ ├── diff.py │ ├── drift.py │ ├── dump.py │ ├── execute.py │ ├── helpers.py │ ├── launch.py │ ├── list.py │ ├── new.py │ ├── policy.py │ ├── prune.py │ ├── status.py │ ├── template.py │ └── update.py ├── config │ ├── __init__.py │ ├── graph.py │ ├── reader.py │ └── strategies.py ├── connection_manager.py ├── context.py ├── diffing │ ├── __init__.py │ ├── diff_writer.py │ └── stack_differ.py ├── exceptions.py ├── helpers.py ├── hooks │ ├── __init__.py │ ├── asg_scaling_processes.py │ └── cmd.py ├── logging.py ├── plan │ ├── __init__.py │ ├── actions.py │ ├── executor.py │ └── plan.py ├── resolvers │ ├── __init__.py │ ├── environment_variable.py │ ├── file_contents.py │ ├── join.py │ ├── no_value.py │ ├── placeholders.py │ ├── select.py │ ├── split.py │ ├── stack_attr.py │ ├── stack_output.py │ └── sub.py ├── stack.py ├── stack_policies │ ├── lock.json │ └── unlock.json ├── stack_status.py ├── stack_status_colourer.py ├── template.py └── template_handlers │ ├── __init__.py │ ├── file.py │ ├── helper.py │ ├── http.py │ └── s3.py ├── sponsors ├── cloudreach_logo.png ├── godaddy_logo.png └── sage_bionetworks_logo.png ├── tests ├── __init__.py ├── fixtures-vpc │ ├── config │ │ ├── account │ │ │ └── stack-group │ │ │ │ ├── config.yaml │ │ │ │ └── region │ │ │ │ ├── config.yaml │ │ │ │ └── vpc.yaml │ │ ├── config.yaml │ │ └── top │ │ │ └── level.yaml │ ├── hooks │ │ └── custom_hook.py │ ├── resolvers │ │ └── custom_resolver.py │ ├── stack_policies │ │ ├── lock.json │ │ └── unlock.json │ └── templates │ │ ├── compiled_vpc.json │ │ ├── compiled_vpc_sud.json │ │ ├── sg.j2 │ │ ├── vpc.j2 │ │ ├── vpc.json │ │ ├── vpc.py │ │ ├── vpc.template │ │ ├── vpc.yaml │ │ ├── vpc.yaml.j2 │ │ ├── vpc_sgt.py │ │ ├── vpc_sud.py │ │ ├── vpc_sud_incorrect_function.py │ │ ├── vpc_sud_incorrect_handler.py │ │ └── vpc_t.py ├── fixtures │ ├── config │ │ ├── account │ │ │ └── stack-group │ │ │ │ ├── config.yaml │ │ │ │ └── region │ │ │ │ ├── config.yaml │ │ │ │ ├── construct_nodes.yaml │ │ │ │ ├── security_groups.yaml │ │ │ │ ├── subnets.yaml │ │ │ │ └── vpc.yaml │ │ ├── config.yaml │ │ └── top │ │ │ └── level.yaml │ ├── hooks │ │ └── custom_hook.py │ ├── resolvers │ │ └── custom_resolver.py │ ├── stack_policies │ │ ├── lock.json │ │ └── unlock.json │ └── templates │ │ ├── chdir.py │ │ ├── compiled_vpc.json │ │ ├── compiled_vpc.yaml │ │ ├── compiled_vpc_sud.json │ │ ├── sg.j2 │ │ ├── vpc.j2 │ │ ├── vpc.py │ │ ├── vpc.template │ │ ├── vpc.without_start_marker.yaml │ │ ├── vpc.yaml │ │ ├── vpc.yaml.j2 │ │ ├── vpc_sgt.py │ │ ├── vpc_sud.py │ │ ├── vpc_sud_incorrect_function.py │ │ ├── vpc_sud_incorrect_handler.py │ │ └── vpc_t.py ├── test_actions.py ├── test_cli │ ├── __init__.py │ ├── test_cli_commands.py │ ├── test_launch.py │ └── test_prune.py ├── test_config_reader.py ├── test_connection_manager.py ├── test_context.py ├── test_diffing │ ├── __init__.py │ ├── test_diff_writer.py │ └── test_stack_differ.py ├── test_helpers.py ├── test_hooks │ ├── __init__.py │ ├── test_asg_scaling_processes.py │ ├── test_cmd.py │ └── test_hooks.py ├── test_plan.py ├── test_resolvers │ ├── test_environment_variable.py │ ├── test_file_contents.py │ ├── test_join.py │ ├── test_placeholders.py │ ├── test_resolver.py │ ├── test_select.py │ ├── test_split.py │ ├── test_stack_attr.py │ ├── test_stack_output.py │ └── test_sub.py ├── test_stack.py ├── test_stack_status_colourer.py ├── test_template.py └── test_template_handlers │ ├── test_file.py │ ├── test_helper.py │ ├── test_http.py │ ├── test_s3.py │ └── test_template_handlers.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.pyd 4 | .git 5 | .coverage 6 | .circleci/ 7 | .github/ 8 | .gitignore 9 | *.dist-info/ 10 | sceptre/__pycache__/ 11 | sceptre/*/__pycache__/ 12 | htmlcov/ 13 | dist/ 14 | sceptre.egg-info/ 15 | docs/ 16 | integration-tests/ 17 | requirements/dev.txt 18 | tests/ 19 | Dockerfile 20 | Makefile 21 | NOTICE 22 | *.ini 23 | *.md 24 | !README.md 25 | !CHANGELOG.md 26 | MANIFEST.in 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | 10 | [*.json] 11 | indent_size = 2 12 | 13 | [*.{markdown,md}] 14 | indent_size = 4 15 | max_line_length = 80 16 | trim_trailing_whitespace = false 17 | 18 | [*.py] 19 | indent_size = 4 20 | max_line_legth = 120 21 | 22 | [*.{yaml,yml}] 23 | indent_size = 2 24 | 25 | [Makefile] 26 | indent_style = tab 27 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | __pycache__, 5 | build, 6 | dist, 7 | .tox, 8 | venv, 9 | .venv, 10 | .pytest_cache 11 | max-complexity = 12 12 | per-file-ignores = 13 | docs/_api/conf.py: E265 14 | integration-tests/steps/*: E501,F811,F403,F405 15 | extend-ignore = E201,E202,E203,E231 16 | max-line-length = 120 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | poetry.lock binary 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Subject of the issue 2 | Describe your issue here. 3 | 4 | ### Your environment 5 | * version of sceptre (sceptre --version) 6 | * version of python (python --version) 7 | * which OS/distro 8 | 9 | ### Steps to reproduce 10 | Tell us how to reproduce this issue. Please provide sceptre projct files if possible, 11 | you can use https://plnkr.co/edit/ANFHm61Ilt4mQVgF as a base. 12 | 13 | ### Expected behaviour 14 | Tell us what should happen 15 | 16 | ### Actual behaviour 17 | Tell us what happens instead 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [Your PR description here] 2 | 3 | ## PR Checklist 4 | 5 | - [ ] Wrote a good commit message & description [see guide below]. 6 | - [ ] Commit message starts with `[Resolve #issue-number]`. 7 | - [ ] Added/Updated unit tests. 8 | - [ ] Added/Updated integration tests (if applicable). 9 | - [ ] All unit tests (`poetry run tox`) are passing. 10 | - [ ] Used the same coding conventions as the rest of the project. 11 | - [ ] The new code passes pre-commit validations (`poetry run pre-commit run --all-files`). 12 | - [ ] The PR relates to _only_ one subject with a clear title. 13 | and description in grammatically correct, complete sentences. 14 | 15 | ## Approver/Reviewer Checklist 16 | 17 | - [ ] Before merge squash related commits. 18 | 19 | ## Other Information 20 | 21 | [Guide to writing a good commit](http://chris.beams.io/posts/git-commit/) 22 | -------------------------------------------------------------------------------- /.github/workflows/branch-integration-tests.yaml: -------------------------------------------------------------------------------- 1 | # The idea with this workflow is to allow core reviewers to trigger the 2 | # integration tests by pushing a branch to the sceptre repository. 3 | name: branch-integration-tests 4 | 5 | on: 6 | push: 7 | branches: 8 | - '*' # matches every branch that doesn't contain a '/' 9 | - '*/*' # matches every branch containing a single '/' 10 | - '**' # matches every branch 11 | - '!master' # excludes master 12 | 13 | jobs: 14 | integration-tests: 15 | if: ${{ github.ref != 'refs/heads/master' }} 16 | uses: "./.github/workflows/integration-tests.yaml" 17 | with: 18 | # role generated from https://github.com/Sceptre/sceptre-aws/blob/master/config/prod/gh-oidc-sceptre-tests.yaml 19 | role-to-assume: "arn:aws:iam::743644221192:role/gh-oidc-sceptre-tests" 20 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | # Execute sanity checks 2 | name: check 3 | 4 | on: 5 | push: 6 | branches: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | pre-commit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pre-commit/action@v3.0.1 18 | with: 19 | extra_args: poetry-lock --all-files 20 | 21 | packaging: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Install Poetry 26 | uses: snok/install-poetry@v1 27 | - name: build package 28 | run: poetry build 29 | 30 | documentation: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Install Poetry 35 | uses: snok/install-poetry@v1 36 | - name: Install dependencies 37 | run: poetry install --no-interaction --all-extras 38 | - name: build documentation 39 | run: poetry run make html --directory docs 40 | 41 | # use https://github.com/medmunds/tox-gh-matrix to export tox envlist to GH actions 42 | get-tox-envlist: 43 | runs-on: ubuntu-latest 44 | outputs: 45 | envlist: ${{ steps.generate-envlist.outputs.envlist }} 46 | steps: 47 | - name: Check out repository 48 | uses: actions/checkout@v4 49 | - name: Install Poetry 50 | uses: snok/install-poetry@v1 51 | - name: Install dependencies 52 | run: poetry install --no-interaction --all-extras 53 | - id: generate-envlist 54 | run: poetry run tox --gh-matrix 55 | 56 | unit-tests: 57 | needs: get-tox-envlist 58 | runs-on: ubuntu-latest 59 | strategy: 60 | fail-fast: true 61 | matrix: 62 | tox: ${{ fromJSON(needs.get-tox-envlist.outputs.envlist) }} 63 | steps: 64 | - name: Check out repository 65 | uses: actions/checkout@v4 66 | - name: Setup Python 67 | id: setup-python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: ${{ matrix.tox.python.spec }} 71 | - name: Install Poetry 72 | uses: snok/install-poetry@v1 73 | - name: Install dependencies 74 | run: poetry install --no-interaction --all-extras 75 | - name: run python tests 76 | run: poetry run tox -e ${{ matrix.tox.name }} 77 | - name: run python test report 78 | run: poetry run tox -e report 79 | 80 | docker-build: 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: Check out repository 84 | uses: actions/checkout@v4 85 | - name: Build Docker Image 86 | uses: docker/build-push-action@v5 87 | with: 88 | context: . 89 | -------------------------------------------------------------------------------- /.github/workflows/gate.yaml: -------------------------------------------------------------------------------- 1 | # Run integration tests when a PR is merged to master and publish a 2 | # docker container (with an `edge` tag) with the latest code 3 | name: gate 4 | 5 | on: 6 | workflow_run: 7 | workflows: 8 | - check 9 | types: 10 | - completed 11 | branches: 12 | - master 13 | 14 | jobs: 15 | integration-tests: 16 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 17 | uses: "./.github/workflows/integration-tests.yaml" 18 | with: 19 | # role generated from https://github.com/Sceptre/sceptre-aws/blob/master/config/prod/gh-oidc-sceptre-tests.yaml 20 | role-to-assume: "arn:aws:iam::743644221192:role/gh-oidc-sceptre-tests" 21 | 22 | docker-build-push: 23 | needs: 24 | - integration-tests 25 | if: ${{ github.ref == 'refs/heads/master' }} 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Log in to the Container registry 30 | uses: docker/login-action@v3 31 | with: 32 | username: ${{ secrets.DOCKERHUB_USERNAME }} 33 | password: ${{ secrets.DOCKERHUB_TOKEN }} 34 | - name: Extract metadata (tags, labels) for Docker 35 | id: meta 36 | uses: docker/metadata-action@v5 37 | # docker convention: edge tag refers to the very latest code 38 | - name: Build and push Docker image to sceptreorg/sceptre:${{ steps.meta.outputs.tags }} 39 | uses: docker/build-push-action@v5 40 | with: 41 | context: . 42 | push: true 43 | tags: sceptreorg/sceptre:edge 44 | labels: ${{ steps.meta.outputs.labels }} 45 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yaml: -------------------------------------------------------------------------------- 1 | name: integration-tests 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | aws-region: 7 | type: string 8 | default: us-east-1 9 | role-to-assume: 10 | required: true 11 | type: string 12 | role-duration-seconds: 13 | type: number 14 | default: 3600 15 | 16 | jobs: 17 | tests: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | id-token: write 21 | # There is only one AWS account for running integration tests and the tests are not designed 22 | # to run concurrently in one account which is why we are disabling concurrency. 23 | # The intention is to have all triggered integration tests execute serially in one queue, 24 | # all triggered integration tests should wait in the queue however github is canceling 25 | # waiting jobs in the queue. Github currently does not support the desired use case, 26 | # more info at https://github.com/orgs/community/discussions/41518 27 | concurrency: 28 | group: integration-tests 29 | cancel-in-progress: false 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Install Poetry 33 | uses: snok/install-poetry@v1 34 | - name: Install dependencies 35 | run: poetry install --no-interaction --all-extras 36 | # Update poetry for https://github.com/python-poetry/poetry/issues/7184 37 | - name: update poetry 38 | run: poetry self update --no-ansi 39 | - name: Setup Python 40 | id: setup-python 41 | uses: actions/setup-python@v5 42 | with: 43 | python-version: '3.12' 44 | cache: 'poetry' 45 | - name: Assume AWS role 46 | uses: aws-actions/configure-aws-credentials@v4 47 | with: 48 | aws-region: ${{ inputs.aws-region }} 49 | role-to-assume: ${{ inputs.role-to-assume }} 50 | role-session-name: GHA-${{ github.repository_owner }}-${{ github.event.repository.name }}-${{ github.run_id }} 51 | role-duration-seconds: ${{ inputs.role-duration-seconds }} 52 | - name: run tests 53 | run: poetry run behave integration-tests/features --junit --junit-directory build/behave 54 | env: 55 | AWS_DEFAULT_REGION: eu-west-1 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | docker-build-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Extract metadata (tags, labels) for Docker 14 | id: meta 15 | uses: docker/metadata-action@v5 16 | with: 17 | images: sceptreorg/sceptre 18 | - name: Log in to DockerHub 19 | uses: docker/login-action@v3 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | # docker convention: latest tag refers to the last stable release 24 | - name: Build and push 25 | uses: docker/build-push-action@v6 26 | with: 27 | context: . 28 | push: true 29 | tags: ${{ steps.meta.outputs.tags }} 30 | labels: ${{ steps.meta.outputs.labels }} 31 | 32 | pypi-release: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Install Poetry 37 | uses: snok/install-poetry@v1 38 | - name: Publish to pypi 39 | run: poetry publish --build -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | venv/ 13 | .venv/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | .python-version 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | test-results.xml 51 | test-reports/ 52 | test-results/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # Sceptre directories users may have for testing 68 | /config 69 | /templates 70 | 71 | ### OSX ### 72 | *.DS_Store 73 | .AppleDouble 74 | .LSOverride 75 | 76 | # Icon must end with two \r 77 | Icon 78 | 79 | 80 | # Thumbnails 81 | ._* 82 | 83 | # Files that might appear in the root of a volume 84 | .DocumentRevisions-V100 85 | .fseventsd 86 | .Spotlight-V100 87 | .TemporaryItems 88 | .Trashes 89 | .VolumeIcon.icns 90 | .com.apple.timemachine.donotpresent 91 | 92 | # Directories potentially created on remote AFP share 93 | .AppleDB 94 | .AppleDesktop 95 | Network Trash Folder 96 | Temporary Items 97 | .apdisk 98 | 99 | ### PyCharm ### 100 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 101 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 102 | 103 | # User-specific stuff: 104 | .idea/ 105 | 106 | ## File-based project format: 107 | *.iws 108 | 109 | # Jekyll settings 110 | 111 | .bundle/ 112 | docs/vendor/* 113 | _site/ 114 | .sass-cache/ 115 | .jekyll-metadata 116 | 117 | # Sphinx docs 118 | /docs/docs/api 119 | /docs/_api/_build/ 120 | /docs/_api/modules.rst 121 | /docs/_api/sceptre.rst 122 | /docs/_api/sceptre.hooks.rst 123 | /docs/_api/sceptre.resolvers.rst 124 | 125 | ### Vim ### 126 | # Swap 127 | [._]*.s[a-v][a-z] 128 | [._]*.sw[a-p] 129 | [._]s[a-rt-v][a-z] 130 | [._]ss[a-gi-z] 131 | [._]sw[a-p] 132 | 133 | # Session 134 | Session.vim 135 | 136 | # Temporary 137 | .netrwhist 138 | *~ 139 | # Auto-generated tag files 140 | tags 141 | # Persistent undo 142 | [._]*.un~ 143 | 144 | # temp files 145 | temp/ 146 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | skip: ["poetry-lock"] # work around for https://github.com/pre-commit-ci/issues/issues/241 4 | 5 | default_language_version: 6 | python: python3 7 | 8 | repos: 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v5.0.0 11 | hooks: 12 | - id: end-of-file-fixer 13 | - id: mixed-line-ending 14 | - id: trailing-whitespace 15 | - repo: https://github.com/PyCQA/flake8 16 | rev: 7.1.2 17 | hooks: 18 | - id: flake8 19 | - repo: https://github.com/adrienverge/yamllint 20 | rev: v1.37.0 21 | hooks: 22 | - id: yamllint 23 | - repo: https://github.com/awslabs/cfn-python-lint 24 | rev: v1.32.1 25 | hooks: 26 | - id: cfn-python-lint 27 | args: 28 | - "-i=E0000" 29 | - "-i=E1001" 30 | - "-i=E3012" 31 | - "-i=W6001" 32 | exclude: | 33 | (?x)( 34 | ^integration-tests/sceptre-project/config/| 35 | ^integration-tests/sceptre-project/templates/| 36 | ^tests/fixtures-vpc/config/| 37 | ^tests/fixtures/config/| 38 | ^temp/| 39 | ^.github/| 40 | ^.pre-commit-config.yaml 41 | ) 42 | - repo: https://github.com/psf/black 43 | rev: 25.1.0 44 | hooks: 45 | - id: black 46 | - repo: https://github.com/python-poetry/poetry 47 | rev: '2.1.1' 48 | hooks: 49 | - id: poetry-check 50 | - id: poetry-lock 51 | - repo: https://github.com/sirosen/check-jsonschema 52 | rev: 0.32.1 53 | hooks: 54 | - id: check-github-workflows 55 | - id: check-github-actions 56 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.12" 7 | jobs: 8 | post_install: 9 | # Install poetry 10 | # https://python-poetry.org/docs/#installing-manually 11 | - pip install poetry 12 | # Install dependencies with 'docs' dependency group 13 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 14 | # VIRTUAL_ENV needs to be set manually for now. 15 | # See https://github.com/readthedocs/readthedocs.org/pull/11152/ 16 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --all-extras 17 | 18 | sphinx: 19 | configuration: docs/_source/conf.py 20 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | braces: 7 | level: warning 8 | max-spaces-inside: 1 9 | brackets: 10 | level: warning 11 | max-spaces-inside: 1 12 | commas: 13 | level: warning 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | empty-lines: 18 | level: warning 19 | hyphens: 20 | level: warning 21 | indentation: 22 | level: warning 23 | indent-sequences: consistent 24 | line-length: disable 25 | truthy: disable 26 | new-line-at-end-of-file: 27 | level: warning 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | RUN apk add --no-cache bash 3 | WORKDIR /app 4 | COPY pyproject.toml README.md CHANGELOG.md ./ 5 | COPY sceptre/ ./sceptre 6 | RUN pip install wheel 7 | RUN pip install . 8 | WORKDIR /project 9 | ENTRYPOINT ["sceptre"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache Software License 2.0 2 | 3 | Copyright 2017 Cloudreach Europe Limited or its affiliates. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Sceptre 2 | Copyright 2017 Cloudreach Europe Limited or its affiliates. 3 | 4 | This software was developed at Cloudreach Europe Limited. 5 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Sceptre release process 2 | 3 | Poetry is used to manage versions and deployments. Follow the below steps to release a version to pypi. 4 | 5 | 1. Bump the package version (i.e. `poetry version minor`) 6 | 2. Update Changelog with details from Github commit list since last release 7 | 3. Create a PR for above changes 8 | 4. Once PR is merged, `git pull` the changes to sync the *master* branch 9 | 5. `git tag -as vX.Y.Z` 10 | 6. `git push origin vX.Y.Z` (CI/CD publishes to PyPi) 11 | 7. Get list of contributors with 12 | `git log --no-merges --format='%<(20)%an' v1.0.0..HEAD | sort | uniq`, where 13 | the tag is the last deployed version. 14 | 8. Announce release to the #sceptre channel on og-aws Slack with a link to 15 | the latest changelog and list of contributors 16 | -------------------------------------------------------------------------------- /behave.ini: -------------------------------------------------------------------------------- 1 | [behave] 2 | stderr_capture=False 3 | stdout_capture=False 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = _source 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | 21 | apidoc: 22 | @sphinx-apidoc -fM -o "$(SOURCEDIR)/apidoc" ../sceptre 23 | 24 | clean: 25 | rm -f sceptre*rst 26 | rm -f modules.rst 27 | rm -rf _build 28 | -------------------------------------------------------------------------------- /docs/_source/_templates/versions.html: -------------------------------------------------------------------------------- 1 | {% if GHPAGES %} 2 |
3 | 4 | Read the Docs 5 | v: {{ version }} 6 | 7 | 8 | 9 |
10 |
11 |
Versions
12 |
13 |
14 | {% set links = { 15 | 'Cloudreach':'https://www.cloudreach.com', 16 | 'Sceptre Home': 'https://docs.sceptre-project.org' 17 | } %} 18 |
Other Links
19 | {% for title, url in links.items() %} 20 | 21 |
22 | {{ title }} 23 |
24 | {% endfor %} 25 |
26 |
27 | Original template provided by Read the Docs. 28 |
29 | 41 |
42 | {% endif %} 43 | -------------------------------------------------------------------------------- /docs/_source/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Sceptre is a tool to drive CloudFormation_. Sceptre manages the creation, 5 | update and deletion of stacks while providing meta commands which 6 | allow users to retrieve information about their stacks. Sceptre is 7 | unopinionated, enterprise ready and designed to run as part of CI/CD pipelines. 8 | Sceptre is accessible as a CLI tool or as a Python module. 9 | 10 | Motivation 11 | ---------- 12 | 13 | CloudFormation_ lacks a robust tool to deploy and manage stacks. While the 14 | `AWS CLI`_ and Boto3_ both provide some functionality, neither offer: 15 | 16 | * Chaining one stack's outputs to another's parameters 17 | 18 | * Easy support for working with role assumption or multiple accounts 19 | 20 | All of the above are common tasks when deploying infrastructure. 21 | 22 | Sceptre was developed to produce a single tool which can be used to deploy any 23 | and all CloudFormation_. 24 | 25 | Overview 26 | -------- 27 | 28 | Sceptre is used by defining CloudFormation, Jinja2 or Python templates, with 29 | corresponding YAML configuration files. The configuration files include which 30 | account and region to use as well as the parameters to supply the templates. 31 | 32 | For a tutorial on using Sceptre, see :doc:`docs/get_started`, or find out more 33 | information about Sceptre below. 34 | 35 | Code 36 | ---- 37 | 38 | Sceptre’s source code can be found on `Github`_. 39 | 40 | Bugs and feature requests should be raised via our `Issues`_ page. 41 | 42 | Communication 43 | ------------- 44 | 45 | The Sceptre community uses a Slack channel #sceptre on the og-aws Slack for 46 | discussion. To join use this link http://slackhatesthe.cloud/ to create an 47 | account and join the #sceptre channel. 48 | 49 | .. _Github: https://github.com/Sceptre/sceptre/ 50 | .. _Issues: https://github.com/Sceptre/sceptre/issues 51 | .. _CloudFormation: https://aws.amazon.com/cloudformation/ 52 | .. _AWS CLI: https://aws.amazon.com/cli/ 53 | .. _Boto3: https://aws.amazon.com/sdk-for-python/ 54 | -------------------------------------------------------------------------------- /docs/_source/apidoc/modules.rst: -------------------------------------------------------------------------------- 1 | sceptre 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | sceptre 8 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.cli.rst: -------------------------------------------------------------------------------- 1 | sceptre.cli package 2 | =================== 3 | 4 | .. automodule:: sceptre.cli 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | sceptre.cli.create module 13 | ------------------------- 14 | 15 | .. automodule:: sceptre.cli.create 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | sceptre.cli.delete module 21 | ------------------------- 22 | 23 | .. automodule:: sceptre.cli.delete 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | sceptre.cli.diff module 29 | _______________________ 30 | 31 | .. automodule:: sceptre.cli.diff 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | sceptre.cli.describe module 37 | --------------------------- 38 | 39 | .. automodule:: sceptre.cli.describe 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | sceptre.cli.execute module 45 | -------------------------- 46 | 47 | .. automodule:: sceptre.cli.execute 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | sceptre.cli.helpers module 53 | -------------------------- 54 | 55 | .. automodule:: sceptre.cli.helpers 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | sceptre.cli.launch module 61 | ------------------------- 62 | 63 | .. automodule:: sceptre.cli.launch 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | sceptre.cli.list module 69 | ----------------------- 70 | 71 | .. automodule:: sceptre.cli.list 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | sceptre.cli.new module 77 | ---------------------- 78 | 79 | .. automodule:: sceptre.cli.new 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | 84 | sceptre.cli.policy module 85 | ------------------------- 86 | 87 | .. automodule:: sceptre.cli.policy 88 | :members: 89 | :undoc-members: 90 | :show-inheritance: 91 | 92 | sceptre.cli.status module 93 | ------------------------- 94 | 95 | .. automodule:: sceptre.cli.status 96 | :members: 97 | :undoc-members: 98 | :show-inheritance: 99 | 100 | sceptre.cli.template module 101 | --------------------------- 102 | 103 | .. automodule:: sceptre.cli.template 104 | :members: 105 | :undoc-members: 106 | :show-inheritance: 107 | 108 | sceptre.cli.update module 109 | ------------------------- 110 | 111 | .. automodule:: sceptre.cli.update 112 | :members: 113 | :undoc-members: 114 | :show-inheritance: 115 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.config.rst: -------------------------------------------------------------------------------- 1 | sceptre.config package 2 | ====================== 3 | 4 | .. automodule:: sceptre.config 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | sceptre.config.graph module 13 | --------------------------- 14 | 15 | .. automodule:: sceptre.config.graph 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | sceptre.config.reader module 21 | ---------------------------- 22 | 23 | .. automodule:: sceptre.config.reader 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | sceptre.config.strategies module 29 | -------------------------------- 30 | 31 | .. automodule:: sceptre.config.strategies 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.diffing.rst: -------------------------------------------------------------------------------- 1 | sceptre.diffing package 2 | ======================= 3 | 4 | .. automodule:: sceptre.diffing 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | sceptre.diffing.stack_differ module 13 | ----------------------------------- 14 | 15 | .. automodule:: sceptre.diffing.stack_differ 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | sceptre.diffing.diff_writer module 21 | ---------------------------------- 22 | 23 | .. automodule:: sceptre.diffing.diff_writer 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.hooks.rst: -------------------------------------------------------------------------------- 1 | sceptre.hooks package 2 | ===================== 3 | 4 | .. automodule:: sceptre.hooks 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | sceptre.hooks.asg\_scaling\_processes module 13 | -------------------------------------------- 14 | 15 | .. automodule:: sceptre.hooks.asg_scaling_processes 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | sceptre.hooks.cmd module 21 | ------------------------ 22 | 23 | .. automodule:: sceptre.hooks.cmd 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.plan.rst: -------------------------------------------------------------------------------- 1 | sceptre.plan package 2 | ==================== 3 | 4 | .. automodule:: sceptre.plan 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | sceptre.plan.actions module 13 | --------------------------- 14 | 15 | .. automodule:: sceptre.plan.actions 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | sceptre.plan.executor module 21 | ---------------------------- 22 | 23 | .. automodule:: sceptre.plan.executor 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | sceptre.plan.plan module 29 | ------------------------ 30 | 31 | .. automodule:: sceptre.plan.plan 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.resolvers.rst: -------------------------------------------------------------------------------- 1 | sceptre.resolvers package 2 | ========================= 3 | 4 | .. automodule:: sceptre.resolvers 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | sceptre.resolvers.environment\_variable module 13 | ---------------------------------------------- 14 | 15 | .. automodule:: sceptre.resolvers.environment_variable 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | sceptre.resolvers.file\_contents module 21 | --------------------------------------- 22 | 23 | .. automodule:: sceptre.resolvers.file_contents 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | sceptre.resolvers.stack\_output module 29 | -------------------------------------- 30 | 31 | .. automodule:: sceptre.resolvers.stack_output 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | -------------------------------------------------------------------------------- /docs/_source/apidoc/sceptre.rst: -------------------------------------------------------------------------------- 1 | sceptre package 2 | =============== 3 | 4 | .. automodule:: sceptre 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | 14 | sceptre.cli 15 | sceptre.config 16 | sceptre.diffing 17 | sceptre.hooks 18 | sceptre.plan 19 | sceptre.resolvers 20 | 21 | Submodules 22 | ---------- 23 | 24 | sceptre.connection\_manager module 25 | ---------------------------------- 26 | 27 | .. automodule:: sceptre.connection_manager 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | sceptre.context module 33 | ---------------------- 34 | 35 | .. automodule:: sceptre.context 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | 40 | sceptre.exceptions module 41 | ------------------------- 42 | 43 | .. automodule:: sceptre.exceptions 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | sceptre.helpers module 49 | ---------------------- 50 | 51 | .. automodule:: sceptre.helpers 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | 56 | sceptre.stack module 57 | -------------------- 58 | 59 | .. automodule:: sceptre.stack 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | sceptre.stack\_status module 65 | ---------------------------- 66 | 67 | .. automodule:: sceptre.stack_status 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | 72 | sceptre.stack\_status\_colourer module 73 | -------------------------------------- 74 | 75 | .. automodule:: sceptre.stack_status_colourer 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | sceptre.template module 81 | ----------------------- 82 | 83 | .. automodule:: sceptre.template 84 | :members: 85 | :undoc-members: 86 | :show-inheritance: 87 | -------------------------------------------------------------------------------- /docs/_source/docs/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Using pip 5 | --------- 6 | 7 | This assumes that you have Python installed. A thorough guide on installing 8 | Python can be found `here `_. We highly recommend using Sceptre from within a 9 | ``virtualenv``. Notes on installing and setting up ``virtualenv`` can be found 10 | `here `__. 11 | 12 | Install Sceptre: 13 | 14 | ``pip install sceptre`` 15 | 16 | Validate installation by printing out Sceptre’s version number: 17 | 18 | .. TODO resolve version in code 19 | 20 | ``sceptre --version`` 21 | 22 | .. TODO ask for fix from: https://github.com/sphinx-doc/sphinx/issues/3306 23 | 24 | .. parsed-literal:: 25 | 26 | Sceptre, version |version| 27 | 28 | Update Sceptre: 29 | 30 | ``pip install sceptre -U`` 31 | 32 | .. _python_install: http://docs.python-guide.org/en/latest/starting/installation/ 33 | 34 | Using Docker image 35 | ------------------ 36 | 37 | To use our Docker image follow these instructions: 38 | 39 | 1. Pull the image ``docker pull sceptreorg/sceptre:[SCEPTRE_VERSION_NUMBER]`` e.g. 40 | ``docker pull sceptreorg/sceptre:4.5.2``. Leave out the version number if you 41 | wish to run `latest`. 42 | 43 | 2. Run the image. You will need to mount the working directory where your 44 | project resides to a directory called `project`. You will also need to mount 45 | a volume with your AWS config to your docker container. E.g. 46 | 47 | ``docker run -v $(pwd):/project -v /Users/me/.aws/:/root/.aws/:ro cloudreach/sceptre:latest --help`` 48 | 49 | If you want to use a custom ENTRYPOINT simply amend the Docker command: 50 | 51 | ``docker run -ti --entrypoint='' cloudreach:test sh`` 52 | 53 | The above command will enter you into the shell of the Docker container where 54 | you can execute sceptre commands - useful for development. 55 | 56 | If you have any other environment variables in your non-docker shell you will 57 | need to pass these in on the Docker CLI using the ``-e`` flag. See Docker 58 | documentation on how to achieve this. 59 | -------------------------------------------------------------------------------- /docs/_source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: about.rst 2 | 3 | .. toctree:: 4 | :maxdepth: 3 5 | :caption: Introduction 6 | 7 | docs/install.rst 8 | docs/get_started.rst 9 | docs/terminology.rst 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | :caption: Documentation 14 | 15 | docs/cli.rst 16 | docs/stack_group_config.rst 17 | docs/stack_config.rst 18 | docs/templates.rst 19 | docs/template_handlers.rst 20 | docs/hooks.rst 21 | docs/resolvers.rst 22 | docs/architecture.rst 23 | docs/permissions.rst 24 | 25 | .. toctree:: 26 | :maxdepth: 3 27 | :caption: Support 28 | 29 | docs/faq.rst 30 | 31 | 32 | .. toctree:: 33 | :maxdepth: 3 34 | :caption: API 35 | :glob: 36 | 37 | apidoc/modules 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | -------------------------------------------------------------------------------- /integration-tests/__ini__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/integration-tests/__ini__.py -------------------------------------------------------------------------------- /integration-tests/environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import uuid 4 | import yaml 5 | import boto3 6 | import string 7 | import random 8 | 9 | 10 | def before_all(context): 11 | random_str = "".join(random.choices(string.ascii_lowercase + string.digits, k=16)) 12 | context.TEST_ARTIFACT_BUCKET_NAME = f"sceptre-test-artifacts-{random_str}" 13 | context.region = boto3.session.Session().region_name 14 | context.uuid = uuid.uuid1().hex 15 | context.project_code = "sceptre-integration-tests-{0}".format(context.uuid) 16 | 17 | sts = boto3.client("sts") 18 | account_number = sts.get_caller_identity()["Account"] 19 | context.bucket_name = "sceptre-integration-tests-templates-{}".format( 20 | account_number 21 | ) 22 | 23 | context.sceptre_dir = os.path.join( 24 | os.getcwd(), "integration-tests", "sceptre-project" 25 | ) 26 | update_config(context) 27 | context.cloudformation = boto3.resource("cloudformation") 28 | context.client = boto3.client("cloudformation") 29 | 30 | 31 | def before_scenario(context, scenario): 32 | os.environ.pop("AWS_REGION", None) 33 | os.environ.pop("AWS_CONFIG_FILE", None) 34 | context.error = None 35 | context.response = None 36 | context.output = None 37 | 38 | 39 | def update_config(context): 40 | config_path = os.path.join(context.sceptre_dir, "config", "config.yaml") 41 | with open(config_path) as config_file: 42 | stack_group_config = yaml.safe_load(config_file) 43 | 44 | stack_group_config["template_bucket_name"] = context.bucket_name 45 | stack_group_config["project_code"] = context.project_code 46 | 47 | with open(config_path, "w") as config_file: 48 | yaml.safe_dump(stack_group_config, config_file, default_flow_style=False) 49 | 50 | 51 | def after_all(context): 52 | response = context.client.describe_stacks() 53 | for stack in response["Stacks"]: 54 | if stack["StackName"].startswith(context.project_code): 55 | context.client.delete_stack(StackName=stack["StackName"]) 56 | time.sleep(2) 57 | context.project_code = "sceptre-integration-tests" 58 | context.bucket_name = "sceptre-integration-tests-templates" 59 | update_config(context) 60 | 61 | 62 | def before_feature(context, feature): 63 | """ 64 | Create a test bucket with a unique name and upload test artifact to the bucket 65 | for the S3 template handler to reference 66 | """ 67 | if "s3-template-handler" in feature.tags: 68 | bucket = boto3.resource("s3").Bucket(context.TEST_ARTIFACT_BUCKET_NAME) 69 | if bucket.creation_date is None: 70 | bucket.create( 71 | CreateBucketConfiguration={"LocationConstraint": context.region} 72 | ) 73 | 74 | 75 | def after_feature(context, feature): 76 | """ 77 | Do a full cleanup of the test artifacts and the test bucket 78 | """ 79 | if "s3-template-handler" in feature.tags: 80 | bucket = boto3.resource("s3").Bucket(context.TEST_ARTIFACT_BUCKET_NAME) 81 | if bucket.creation_date is not None: 82 | bucket.objects.all().delete() 83 | bucket.delete() 84 | -------------------------------------------------------------------------------- /integration-tests/features/create-change-set.feature: -------------------------------------------------------------------------------- 1 | Feature: Create change set 2 | 3 | Scenario: create new change set with updated template 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and the template for stack "1/A" is "updated_template.json" 6 | and stack "1/A" does not have change set "A" 7 | When the user creates change set "A" for stack "1/A" 8 | Then stack "1/A" has change set "A" in "CREATE_COMPLETE" state 9 | 10 | Scenario: create new change set with same template 11 | Given stack "1/A" exists in "CREATE_COMPLETE" state 12 | and the template for stack "1/A" is "valid_template.json" 13 | and stack "1/A" does not have change set "A" 14 | When the user creates change set "A" for stack "1/A" 15 | Then stack "1/A" has change set "A" in "FAILED" state 16 | 17 | Scenario: create new change set with stack that does not exist 18 | Given stack "1/A" does not exist 19 | and the template for stack "1/A" is "valid_template.json" 20 | When the user creates change set "A" for stack "1/A" 21 | Then stack "1/A" has change set "A" in "CREATE_COMPLETE" state 22 | 23 | Scenario: create new change set with updated template and ignore dependencies 24 | Given stack "1/A" exists in "CREATE_COMPLETE" state 25 | and the template for stack "1/A" is "updated_template.json" 26 | and stack "1/A" does not have change set "A" 27 | When the user creates change set "A" for stack "1/A" with ignore dependencies 28 | Then stack "1/A" has change set "A" in "CREATE_COMPLETE" state 29 | 30 | Scenario: create new change set with a SAM template 31 | Given stack "11/A" exists in "CREATE_COMPLETE" state 32 | and the template for stack "11/A" is "sam_updated_template.yaml" 33 | and stack "11/A" does not have change set "A" 34 | When the user creates change set "A" for stack "11/A" 35 | Then stack "11/A" has change set "A" in "CREATE_COMPLETE" state 36 | -------------------------------------------------------------------------------- /integration-tests/features/create-stack.feature: -------------------------------------------------------------------------------- 1 | Feature: Create stack 2 | 3 | Scenario: create new stack 4 | Given stack "1/A" does not exist 5 | and the template for stack "1/A" is "valid_template.json" 6 | When the user creates stack "1/A" 7 | Then stack "1/A" exists in "CREATE_COMPLETE" state 8 | 9 | Scenario: create a stack that already exists 10 | Given stack "1/A" exists in "CREATE_COMPLETE" state 11 | and the template for stack "1/A" is "valid_template.json" 12 | When the user creates stack "1/A" 13 | Then stack "1/A" exists in "CREATE_COMPLETE" state 14 | 15 | Scenario: create new stack that has previously failed 16 | Given stack "1/A" exists in "CREATE_FAILED" state 17 | and the template for stack "1/A" is "valid_template.json" 18 | When the user creates stack "1/A" 19 | Then stack "1/A" exists in "CREATE_FAILED" state 20 | 21 | Scenario: create new stack that is rolled back on failure 22 | Given stack "8/A" does not exist 23 | and the template for stack "8/A" is "invalid_template.json" 24 | When the user creates stack "8/A" 25 | Then stack "8/A" exists in "ROLLBACK_COMPLETE" state 26 | 27 | Scenario: create new stack that is retained on failure 28 | Given stack "8/B" does not exist 29 | and the template for stack "8/B" is "invalid_template.json" 30 | When the user creates stack "8/B" 31 | Then stack "8/B" exists in "CREATE_FAILED" state 32 | 33 | Scenario: create new stack that is rolled back after timeout 34 | Given stack "8/C" does not exist 35 | and the template for stack "8/C" is "valid_template_wait_300.json" 36 | and the stack_timeout for stack "8/C" is "1" 37 | When the user creates stack "8/C" 38 | Then stack "8/C" exists in "ROLLBACK_COMPLETE" state 39 | 40 | Scenario: create new stack that ignores dependencies 41 | Given stack "1/A" does not exist 42 | and the template for stack "1/A" is "valid_template.json" 43 | When the user creates stack "1/A" with ignore dependencies 44 | Then stack "1/A" exists in "CREATE_COMPLETE" state 45 | 46 | Scenario: create new stack containing a SAM template transform 47 | Given stack "10/A" does not exist 48 | and the template for stack "10/A" is "sam_template.yaml" 49 | When the user creates stack "10/A" 50 | Then stack "10/A" exists in "CREATE_COMPLETE" state 51 | 52 | Scenario: create new stack with nested config jinja resolver 53 | Given stack_group "12/1" does not exist 54 | When the user launches stack_group "12/1" 55 | Then all the stacks in stack_group "12/1" are in "CREATE_COMPLETE" 56 | and stack "12/1/A" has "Project" tag with "A" value 57 | and stack "12/1/A" has "Key" tag with "A" value 58 | and stack "12/1/2/B" has "Project" tag with "B" value 59 | and stack "12/1/2/B" has "Key" tag with "A-B" value 60 | and stack "12/1/2/3/C" has "Project" tag with "C" value 61 | and stack "12/1/2/3/C" has "Key" tag with "A-B-C" value 62 | -------------------------------------------------------------------------------- /integration-tests/features/delete-change-set.feature: -------------------------------------------------------------------------------- 1 | Feature: Delete change set 2 | 3 | Scenario: delete a change set that exists 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and stack "1/A" has change set "A" using "updated_template.json" 6 | When the user deletes change set "A" for stack "1/A" 7 | Then stack "1/A" does not have change set "A" 8 | 9 | 10 | Scenario: delete a change set that exists with ignore dependencies 11 | Given stack "1/A" exists in "CREATE_COMPLETE" state 12 | and stack "1/A" has change set "A" using "updated_template.json" 13 | When the user deletes change set "A" for stack "1/A" with ignore dependencies 14 | Then stack "1/A" does not have change set "A" 15 | 16 | # @wip 17 | # Scenario: delete a change set that does not exist 18 | # Given stack "1/A" exists in "CREATE_COMPLETE" state 19 | # and stack "1/A" does not have change set "1/A" 20 | # When the user deletes change set "1/A" for stack "1/A" 21 | # Then the user is told the change set does not exist 22 | -------------------------------------------------------------------------------- /integration-tests/features/delete-stack-group.feature: -------------------------------------------------------------------------------- 1 | Feature: Delete stack_group 2 | 3 | Scenario: delete a stack_group that does not exist 4 | Given stack_group "1" does not exist 5 | When the user deletes stack_group "2" 6 | Then all the stacks in stack_group "2" do not exist 7 | 8 | Scenario: delete a stack_group that already exists 9 | Given all the stacks in stack_group "2" are in "CREATE_COMPLETE" 10 | When the user deletes stack_group "2" 11 | Then all the stacks in stack_group "2" do not exist 12 | 13 | Scenario: delete a stack_group that partially exists 14 | Given stack "2/A" exists in "CREATE_COMPLETE" state 15 | When the user deletes stack_group "2" 16 | Then all the stacks in stack_group "2" do not exist 17 | 18 | Scenario: delete a stack_group that already exists with ignore dependencies 19 | Given all the stacks in stack_group "2" are in "CREATE_COMPLETE" 20 | When the user deletes stack_group "2" with ignore dependencies 21 | Then all the stacks in stack_group "2" do not exist 22 | -------------------------------------------------------------------------------- /integration-tests/features/delete-stack.feature: -------------------------------------------------------------------------------- 1 | Feature: Delete stack 2 | 3 | Scenario: delete a stack that exists 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | When the user deletes stack "1/A" 6 | Then stack "1/A" does not exist 7 | 8 | Scenario: delete a stack that does not exist 9 | Given stack "1/A" does not exist 10 | When the user deletes stack "1/A" 11 | Then stack "1/A" does not exist 12 | 13 | Scenario: delete a stack that exists with ignore dependencies 14 | Given stack "1/A" exists in "CREATE_COMPLETE" state 15 | When the user deletes stack "1/A" with ignore dependencies 16 | Then stack "1/A" does not exist 17 | 18 | Scenario: delete a stack that exists with dependencies ignoring dependencies 19 | Given stack "4/C" exists in "CREATE_COMPLETE" state 20 | and stack "3/A" exists in "CREATE_COMPLETE" state 21 | and stack "3/A" depends on stack "4/C" 22 | When the user deletes stack "4/C" with ignore dependencies 23 | Then stack "4/C" does not exist and stack "3/A" exists in "CREATE_COMPLETE" 24 | 25 | Scenario: delete a stack that contains !stack_output dependencies 26 | Given stack "6/1/A" exists in "CREATE_COMPLETE" state 27 | and stack "6/1/B" exists in "CREATE_COMPLETE" state 28 | and stack "6/1/C" exists in "CREATE_COMPLETE" state 29 | When the user deletes stack "6/1/A" 30 | Then stack "6/1/A" does not exist 31 | and stack "6/1/B" does not exist 32 | and stack "6/1/C" does not exist 33 | 34 | Scenario: delete a stack that contains dependencies parameter 35 | Given stack "3/A" exists in "CREATE_COMPLETE" state 36 | and stack "3/B" exists in "CREATE_COMPLETE" state 37 | and stack "3/C" exists in "CREATE_COMPLETE" state 38 | When the user deletes stack "3/A" 39 | Then stack "3/A" does not exist 40 | and stack "3/B" does not exist 41 | and stack "3/C" does not exist 42 | -------------------------------------------------------------------------------- /integration-tests/features/dependency-resolution.feature: -------------------------------------------------------------------------------- 1 | Feature: Dependency resolution 2 | 3 | Scenario: launch a stack_group with dependencies that is partially complete 4 | Given stack "3/A" exists in "CREATE_COMPLETE" state 5 | And stack "3/B" exists in "CREATE_COMPLETE" state 6 | And stack "3/C" does not exist 7 | When the user launches stack_group "3" 8 | Then all the stacks in stack_group "3" are in "CREATE_COMPLETE" 9 | And that stack "3/A" was created before "3/B" 10 | And that stack "3/B" was created before "3/C" 11 | 12 | Scenario: delete a stack_group with dependencies that is partially complete 13 | Given stack "3/A" exists in "CREATE_COMPLETE" state 14 | And stack "3/B" exists in "CREATE_COMPLETE" state 15 | And stack "3/C" does not exist 16 | When the user deletes stack_group "3" 17 | Then all the stacks in stack_group "3" do not exist 18 | 19 | Scenario: delete a stack_group with dependencies 20 | Given all the stacks in stack_group "3" are in "CREATE_COMPLETE" 21 | When the user deletes stack_group "3" 22 | Then all the stacks in stack_group "3" do not exist 23 | -------------------------------------------------------------------------------- /integration-tests/features/describe-change-set.feature: -------------------------------------------------------------------------------- 1 | Feature: Describe change sets 2 | 3 | Scenario: describe a change set that exists 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and stack "1/A" has change set "A" using "updated_template.json" 6 | When the user describes change set "A" for stack "1/A" 7 | Then change set "A" for stack "1/A" is described 8 | 9 | Scenario: describe a change set that does not exist 10 | Given stack "1/A" exists in "CREATE_COMPLETE" state 11 | and stack "1/A" has no change sets 12 | When the user describes change set "A" for stack "1/A" 13 | Then the user is told "Failed describing Change Set" 14 | 15 | Scenario: describe a change set that exists with ignore dependencies 16 | Given stack "1/A" exists in "CREATE_COMPLETE" state 17 | and stack "1/A" has change set "A" using "updated_template.json" 18 | When the user describes change set "A" for stack "1/A" with ignore dependencies 19 | Then change set "A" for stack "1/A" is described 20 | -------------------------------------------------------------------------------- /integration-tests/features/drift.feature: -------------------------------------------------------------------------------- 1 | Feature: Drift Detection 2 | Scenario: Detects no drift on a stack with no drift 3 | Given stack "drift-single/A" exists using "topic.yaml" 4 | When the user detects drift on stack "drift-single/A" 5 | Then stack drift status is "IN_SYNC" 6 | 7 | Scenario: Shows no drift on a stack that with no drift 8 | Given stack "drift-single/A" exists using "topic.yaml" 9 | When the user shows drift on stack "drift-single/A" 10 | Then stack resource drift status is "IN_SYNC" 11 | 12 | Scenario: Detects drift on a stack that has drifted 13 | Given stack "drift-single/A" exists using "topic.yaml" 14 | And a topic configuration in stack "drift-single/A" has drifted 15 | When the user detects drift on stack "drift-single/A" 16 | Then stack drift status is "DRIFTED" 17 | 18 | Scenario: Shows drift on a stack that has drifted 19 | Given stack "drift-single/A" exists using "topic.yaml" 20 | And a topic configuration in stack "drift-single/A" has drifted 21 | When the user shows drift on stack "drift-single/A" 22 | Then stack resource drift status is "MODIFIED" 23 | 24 | Scenario: Detects drift on a stack group that partially exists 25 | Given stack "drift-group/A" exists using "topic.yaml" 26 | And stack "drift-group/B" does not exist 27 | And a topic configuration in stack "drift-group/A" has drifted 28 | When the user detects drift on stack_group "drift-group" 29 | Then stack_group drift statuses are each one of "DRIFTED,STACK_DOES_NOT_EXIST" 30 | 31 | Scenario: Does not blow up on a stack group that doesn't exist 32 | Given stack_group "drift-group" does not exist 33 | When the user detects drift on stack_group "drift-group" 34 | Then stack_group drift statuses are each one of "STACK_DOES_NOT_EXIST,STACK_DOES_NOT_EXIST" 35 | -------------------------------------------------------------------------------- /integration-tests/features/dump-template-s3.feature: -------------------------------------------------------------------------------- 1 | @s3-template-handler 2 | Feature: Dump template s3 3 | 4 | Scenario: Dumping static templates with S3 template handler 5 | Given the template for stack "13/B" is "valid_template.json" 6 | When the user dumps the template for stack "13/B" 7 | Then the output is the same as the contents of "valid_template.json" template 8 | 9 | Scenario: Render jinja templates with S3 template handler 10 | Given the template for stack "13/C" is "jinja/valid_template.j2" 11 | When the user dumps the template for stack "13/C" 12 | Then the output is the same as the contents of "valid_template.json" template 13 | 14 | Scenario: Render python templates with S3 template handler 15 | Given the template for stack "13/D" is "python/valid_template.py" 16 | When the user dumps the template for stack "13/D" 17 | Then the output is the same as the contents of "valid_template.json" template 18 | -------------------------------------------------------------------------------- /integration-tests/features/execute-change-set.feature: -------------------------------------------------------------------------------- 1 | Feature: Execute change set 2 | 3 | Scenario: execute a change set that exists 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | And stack "1/A" has change set "A" using "updated_template.json" 6 | When the user executes change set "A" for stack "1/A" 7 | Then stack "1/A" does not have change set "A" 8 | And stack "1/A" was updated with change set "A" 9 | 10 | Scenario: execute a change set that does not exist 11 | Given stack "1/A" exists in "CREATE_COMPLETE" state 12 | And stack "1/A" does not have change set "A" 13 | When the user executes change set "A" for stack "1/A" 14 | Then the user is told "change set does not exist" 15 | 16 | Scenario: execute a change set that exists with ignore dependencies 17 | Given stack "1/A" exists in "CREATE_COMPLETE" state 18 | And stack "1/A" has change set "A" using "updated_template.json" 19 | When the user executes change set "A" for stack "1/A" with ignore dependencies 20 | Then stack "1/A" does not have change set "A" 21 | And stack "1/A" was updated with change set "A" 22 | 23 | Scenario: execute a change set that failed creation for no changes 24 | Given stack "2/A" exists using "valid_template.json" 25 | And stack "2/A" has change set "A" using "valid_template.json" 26 | When the user executes change set "A" for stack "2/A" 27 | Then stack "2/A" has change set "A" in "FAILED" state 28 | 29 | Scenario: execute a change set that failed creation for a SAM template with no changes 30 | Given stack "3/A" exists using "sam_template.yaml" 31 | And stack "3/A" has change set "A" using "sam_template.yaml" 32 | When the user executes change set "A" for stack "3/A" 33 | Then stack "3/A" has change set "A" in "FAILED" state 34 | -------------------------------------------------------------------------------- /integration-tests/features/launch-stack.feature: -------------------------------------------------------------------------------- 1 | Feature: Launch stack 2 | 3 | Scenario: launch a new stack 4 | Given stack "1/A" does not exist 5 | And the template for stack "1/A" is "valid_template.json" 6 | When the user launches stack "1/A" 7 | Then stack "1/A" exists in "CREATE_COMPLETE" state 8 | 9 | Scenario: launch a stack that was newly created 10 | Given stack "1/A" exists in "CREATE_COMPLETE" state 11 | And the template for stack "1/A" is "updated_template.json" 12 | When the user launches stack "1/A" 13 | Then stack "1/A" exists in "UPDATE_COMPLETE" state 14 | 15 | Scenario: launch a stack that has been previously updated 16 | Given stack "1/A" exists in "UPDATE_COMPLETE" state 17 | And the template for stack "1/A" is "valid_template.json" 18 | When the user launches stack "1/A" 19 | Then stack "1/A" exists in "UPDATE_COMPLETE" state 20 | 21 | Scenario: launch a new stack with ignore dependencies 22 | Given stack "1/A" does not exist 23 | And the template for stack "1/A" is "valid_template.json" 24 | When the user launches stack "1/A" with ignore dependencies 25 | Then stack "1/A" exists in "CREATE_COMPLETE" state 26 | 27 | Scenario: launch an obsolete stack that doesn't exist 28 | Given stack "launch-actions/obsolete" does not exist 29 | When the user launches stack "launch-actions/obsolete" 30 | Then stack "launch-actions/obsolete" does not exist 31 | 32 | Scenario: launch an obsolete stack that does exist without --prune 33 | Given stack "launch-actions/obsolete" exists using "valid_template.json" 34 | When the user launches stack "launch-actions/obsolete" 35 | Then stack "launch-actions/obsolete" exists in "CREATE_COMPLETE" state 36 | 37 | Scenario: launch an obsolete stack that does exist with --prune 38 | Given stack "launch-actions/obsolete" exists using "valid_template.json" 39 | When the user launches stack "launch-actions/obsolete" with --prune 40 | Then stack "launch-actions/obsolete" does not exist 41 | 42 | Scenario: launch an ignored stack that doesn't exist 43 | Given stack "launch-actions/ignore" does not exist 44 | When the user launches stack "launch-actions/ignore" 45 | Then stack "launch-actions/ignore" does not exist 46 | 47 | Scenario: launch an ignored stack that does exist 48 | Given stack "launch-actions/ignore" exists using "valid_template.json" 49 | When the user launches stack "launch-actions/ignore" 50 | Then stack "launch-actions/ignore" exists in "CREATE_COMPLETE" state 51 | -------------------------------------------------------------------------------- /integration-tests/features/list-change-sets.feature: -------------------------------------------------------------------------------- 1 | Feature: List change sets 2 | 3 | Scenario: list change sets on existing stack with change sets 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and stack "1/A" has change set "A" using "updated_template.json" 6 | When the user lists change sets for stack "1/A" 7 | Then the change sets for stack "1/A" are listed 8 | 9 | Scenario: list change sets on existing stack with no change sets 10 | Given stack "1/A" exists in "CREATE_COMPLETE" state 11 | and stack "1/A" has no change sets 12 | When the user lists change sets for stack "1/A" 13 | Then no change sets for stack "1/A" are listed 14 | 15 | Scenario: list change sets on existing stack with change sets with ignore dependencies 16 | Given stack "1/A" exists in "CREATE_COMPLETE" state 17 | and stack "1/A" has change set "A" using "updated_template.json" 18 | When the user lists change sets for stack "1/A" with ignore dependencies 19 | Then the change sets for stack "1/A" are listed 20 | -------------------------------------------------------------------------------- /integration-tests/features/lock-stack.feature: -------------------------------------------------------------------------------- 1 | Feature: Lock stack 2 | 3 | Scenario: lock a stack that exists with a stack policy 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and the policy for stack "1/A" is allow all 6 | When the user locks stack "1/A" 7 | Then the policy for stack "1/A" is deny all 8 | 9 | Scenario: lock a stack that does not exist 10 | Given stack "1/A" does not exist 11 | When the user locks stack "1/A" 12 | Then a "ClientError" is raised 13 | and the user is told "stack does not exist" 14 | -------------------------------------------------------------------------------- /integration-tests/features/project-dependencies.feature: -------------------------------------------------------------------------------- 1 | Feature: Project Dependencies managed within Sceptre 2 | 3 | Background: 4 | Given stack_group "project-deps" does not exist 5 | 6 | Scenario: launch stack group with dependencies 7 | Given all files in template bucket for stack "project-deps/main-project/resource" are deleted at cleanup 8 | When the user launches stack_group "project-deps" 9 | Then all the stacks in stack_group "project-deps" are in "CREATE_COMPLETE" 10 | 11 | Scenario: template_bucket_name is managed in stack group 12 | Given all files in template bucket for stack "project-deps/main-project/resource" are deleted at cleanup 13 | When the user launches stack_group "project-deps" 14 | Then the template for stack "project-deps/main-project/resource" has been uploaded 15 | 16 | Scenario: notifications are managed in stack group 17 | Given all files in template bucket for stack "project-deps/main-project/resource" are deleted at cleanup 18 | When the user launches stack_group "project-deps" 19 | Then the stack "project-deps/main-project/resource" has a notification defined by stack "project-deps/dependencies/topic" 20 | 21 | Scenario: validate a project that isn't deployed yet 22 | Given placeholders are allowed 23 | When the user validates stack_group "project-deps" 24 | Then the user is told "the template is valid" 25 | 26 | Scenario: diff a project that isn't deployed yet 27 | Given placeholders are allowed 28 | When the user diffs stack group "project-deps" with "deepdiff" 29 | Then a diff is returned with "is_deployed" = "False" 30 | 31 | Scenario: tags can be resolved 32 | Given all files in template bucket for stack "project-deps/main-project/resource" are deleted at cleanup 33 | When the user launches stack_group "project-deps" 34 | Then the tag "greeting" for stack "project-deps/main-project/resource" is "hello" 35 | And the tag "nonexistant" for stack "project-deps/main-project/resource" does not exist 36 | -------------------------------------------------------------------------------- /integration-tests/features/prune.feature: -------------------------------------------------------------------------------- 1 | Feature: Prune 2 | 3 | Scenario: Prune with no stacks marked obsolete does nothing 4 | Given stack "pruning/not-obsolete" exists using "valid_template.json" 5 | When command path "pruning/not-obsolete" is pruned 6 | Then stack "pruning/not-obsolete" exists in "CREATE_COMPLETE" state 7 | 8 | Scenario: Prune whole project deletes all obsolete stacks that exist 9 | Given all the stacks in stack_group "pruning" are in "CREATE_COMPLETE" 10 | And stack "launch-actions/obsolete" exists using "valid_template.json" 11 | When the whole project is pruned 12 | Then stack "pruning/obsolete-1" does not exist 13 | And stack "pruning/obsolete-2" does not exist 14 | And stack "launch-actions/obsolete" does not exist 15 | And stack "pruning/not-obsolete" exists in "CREATE_COMPLETE" state 16 | 17 | Scenario: Prune command path only deletes stacks on command path 18 | Given stack "pruning/obsolete-1" exists using "valid_template.json" 19 | And stack "pruning/obsolete-2" exists using "valid_template.json" 20 | When command path "pruning/obsolete-1.yaml" is pruned 21 | Then stack "pruning/obsolete-1" does not exist 22 | And stack "pruning/obsolete-2" exists in "CREATE_COMPLETE" state 23 | -------------------------------------------------------------------------------- /integration-tests/features/stack-diff.feature: -------------------------------------------------------------------------------- 1 | Feature: Stack Diff 2 | Scenario Outline: Diff on stack that exists with no changes 3 | Given stack "1/A" exists in "CREATE_COMPLETE" state 4 | And the template for stack "1/A" is "valid_template.json" 5 | When the user diffs stack "1/A" with "" 6 | Then a diff is returned with no "template" difference 7 | And a diff is returned with no "config" difference 8 | And a diff is returned with "is_deployed" = "True" 9 | 10 | Examples: DiffTypes 11 | | diff_type | 12 | | deepdiff | 13 | | difflib | 14 | 15 | Scenario Outline: Diff on stack that doesnt exist 16 | Given stack "1/A" does not exist 17 | And the template for stack "1/A" is "valid_template.json" 18 | When the user diffs stack "1/A" with "" 19 | Then a diff is returned with "is_deployed" = "False" 20 | And a diff is returned with a "template" difference 21 | And a diff is returned with a "config" difference 22 | 23 | Examples: DiffTypes 24 | | diff_type | 25 | | deepdiff | 26 | | difflib | 27 | 28 | Scenario Outline: Diff on stack that exists in non-deployed state 29 | Given stack "1/A" exists in "" state 30 | And the template for stack "1/A" is "valid_template.json" 31 | When the user diffs stack "1/A" with "" 32 | Then a diff is returned with a "template" difference 33 | And a diff is returned with a "config" difference 34 | And a diff is returned with "is_deployed" = "False" 35 | 36 | Examples: DeepDiff 37 | | diff_type | status | 38 | | deepdiff | CREATE_FAILED | 39 | | deepdiff | ROLLBACK_COMPLETE | 40 | | difflib | CREATE_FAILED | 41 | | difflib | ROLLBACK_COMPLETE | 42 | 43 | Scenario Outline: Diff on stack with only template changes 44 | Given stack "1/A" exists in "CREATE_COMPLETE" state 45 | And the template for stack "1/A" is "updated_template.json" 46 | When the user diffs stack "1/A" with "" 47 | Then a diff is returned with a "template" difference 48 | And a diff is returned with no "config" difference 49 | 50 | Examples: DiffTypes 51 | | diff_type | 52 | | deepdiff | 53 | | difflib | 54 | 55 | Scenario Outline: Diff on stack with only configuration changes 56 | Given stack "1/A" exists in "CREATE_COMPLETE" state 57 | And the template for stack "1/A" is "valid_template.json" 58 | And the stack config for stack "1/A" has changed 59 | When the user diffs stack "1/A" with "" 60 | Then a diff is returned with a "config" difference 61 | And a diff is returned with no "template" difference 62 | 63 | Examples: DiffTypes 64 | | diff_type | 65 | | deepdiff | 66 | | difflib | 67 | 68 | 69 | Scenario Outline: Diff on stack with both configuration and template changes 70 | Given stack "1/A" exists in "CREATE_COMPLETE" state 71 | And the template for stack "1/A" is "updated_template.json" 72 | And the stack config for stack "1/A" has changed 73 | When the user diffs stack "1/A" with "" 74 | Then a diff is returned with a "config" difference 75 | And a diff is returned with a "template" difference 76 | 77 | Examples: DiffTypes 78 | | diff_type | 79 | | deepdiff | 80 | | difflib | 81 | -------------------------------------------------------------------------------- /integration-tests/features/stack-output-external-resolver.feature: -------------------------------------------------------------------------------- 1 | Feature: Stack output external resolver 2 | 3 | Scenario: launch a stack referencing the external output of an existing stack without explicit region or profile 4 | Given stack "external-stack-output/outputter" exists using "dependencies/independent_template.json" 5 | And stack "external-stack-output/resolver-no-profile-region" does not exist 6 | When the user launches stack "external-stack-output/resolver-no-profile-region" 7 | Then stack "external-stack-output/resolver-no-profile-region" exists in "CREATE_COMPLETE" state 8 | -------------------------------------------------------------------------------- /integration-tests/features/stack-output-resolver.feature: -------------------------------------------------------------------------------- 1 | Feature: Stack output resolver 2 | 3 | Scenario: launch a stack referencing an output of existing stack 4 | Given stack "6/1/A" exists using "dependencies/independent_template.json" 5 | and stack "6/1/B" does not exist 6 | and stack "6/1/C" does not exist 7 | When the user launches stack "6/1/B" 8 | Then stack "6/1/B" exists in "CREATE_COMPLETE" state 9 | 10 | Scenario: launch a stack referencing an output of a non-existant stack 11 | Given stack "6/1/B" does not exist 12 | and stack "6/1/A" does not exist 13 | When the user launches stack "6/1/B" 14 | Then stack "6/1/A" exists in "CREATE_COMPLETE" state 15 | And stack "6/1/B" exists in "CREATE_COMPLETE" state 16 | And that stack "6/1/A" was created before "6/1/B" 17 | 18 | Scenario: launch a stack_group where stacks reference other stack outputs 19 | Given stack "6/1/B" does not exist 20 | and stack "6/1/A" does not exist 21 | When the user launches stack_group "6/1" 22 | Then all the stacks in stack_group "6/1" are in "CREATE_COMPLETE" 23 | and that stack "6/1/A" was created before "6/1/B" 24 | and that stack "6/1/B" was created before "6/1/C" 25 | 26 | Scenario: launch a stack_group where stacks are in different regions 27 | Given stack "6/2/A" does not exist in "eu-west-1" 28 | and stack "6/2/B" does not exist in "eu-west-2" 29 | and stack "6/2/C" does not exist in "eu-west-3" 30 | When the user launches stack_group "6/2" 31 | Then stack "6/2/A" in "eu-west-1" exists in "CREATE_COMPLETE" state 32 | and stack "6/2/B" in "eu-west-2" exists in "CREATE_COMPLETE" state 33 | and stack "6/2/C" in "eu-west-3" exists in "CREATE_COMPLETE" state 34 | 35 | Scenario: delete a stack referencing an output of existing stack 36 | Given stack "6/1/A" exists in "CREATE_COMPLETE" state 37 | and stack "6/1/B" exists in "CREATE_COMPLETE" state 38 | and stack "6/1/C" does not exist 39 | When the user deletes stack "6/1/B" 40 | Then stack "6/1/B" does not exist 41 | 42 | Scenario: delete a stack referencing an output of existing stack 43 | Given stack "6/1/A" exists in "CREATE_COMPLETE" state 44 | and stack "6/1/B" exists in "CREATE_COMPLETE" state 45 | and stack "6/1/C" exists in "CREATE_COMPLETE" state 46 | When the user deletes stack_group "6/1" 47 | Then all the stacks in stack_group "6/1" do not exist 48 | -------------------------------------------------------------------------------- /integration-tests/features/unlock-stack.feature: -------------------------------------------------------------------------------- 1 | Feature: Unlock stack 2 | 3 | Scenario: unlock a stack that exists with a stack policy 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and the policy for stack "1/A" is deny all 6 | When the user unlocks stack "1/A" 7 | Then the policy for stack "1/A" is allow all 8 | 9 | Scenario: unlock a stack that does not exist 10 | Given stack "1/A" does not exist 11 | When the user unlocks stack "1/A" 12 | Then a "ClientError" is raised 13 | and the user is told "stack does not exist" 14 | -------------------------------------------------------------------------------- /integration-tests/features/update-stack.feature: -------------------------------------------------------------------------------- 1 | Feature: Update stack 2 | 3 | Scenario: update a stack that was newly created 4 | Given stack "1/A" exists in "CREATE_COMPLETE" state 5 | and the template for stack "1/A" is "updated_template.json" 6 | When the user updates stack "1/A" 7 | Then stack "1/A" exists in "UPDATE_COMPLETE" state 8 | 9 | Scenario: update a stack that has been previously updated 10 | Given stack "1/A" exists in "UPDATE_COMPLETE" state 11 | and the template for stack "1/A" is "updated_template.json" 12 | When the user updates stack "1/A" 13 | Then stack "1/A" exists in "UPDATE_COMPLETE" state 14 | 15 | Scenario: update a stack that does not exists 16 | Given stack "1/A" does not exist 17 | and the template for stack "1/A" is "updated_template.json" 18 | When the user updates stack "1/A" 19 | Then stack "1/A" does not exist 20 | 21 | Scenario: update a stack that is rolled back after timeout 22 | Given stack "1/A" exists in "CREATE_COMPLETE" state 23 | and the template for stack "1/A" is "updated_template_wait_300.json" 24 | and the stack_timeout for stack "1/A" is "1" 25 | When the user updates stack "1/A" 26 | Then stack "1/A" exists in "UPDATE_ROLLBACK_COMPLETE" state 27 | 28 | Scenario: update a stack that was newly created with ignore dependencies 29 | Given stack "1/A" exists in "CREATE_COMPLETE" state 30 | and the template for stack "1/A" is "updated_template.json" 31 | When the user updates stack "1/A" with ignore dependencies 32 | Then stack "1/A" exists in "UPDATE_COMPLETE" state 33 | 34 | Scenario: update a stack that was newly created with a SAM template 35 | Given stack "11/A" exists in "CREATE_COMPLETE" state 36 | and the template for stack "11/A" is "sam_updated_template.yaml" 37 | When the user updates stack "11/A" 38 | Then stack "11/A" exists in "UPDATE_COMPLETE" state 39 | -------------------------------------------------------------------------------- /integration-tests/features/validate-template.feature: -------------------------------------------------------------------------------- 1 | Feature: Validate template 2 | 3 | Scenario: validate a vaild template 4 | Given the template for stack "1/A" is "valid_template.json" 5 | When the user validates the template for stack "1/A" 6 | Then the user is told "the template is valid" 7 | 8 | Scenario: validate a invaild template 9 | Given the template for stack "1/A" is "malformed_template.json" 10 | When the user validates the template for stack "1/A" 11 | Then a "ClientError" is raised 12 | and the user is told "the template is malformed" 13 | 14 | Scenario: validate a valid template with ignore dependencies 15 | Given the template for stack "1/A" is "valid_template.json" 16 | When the user validates the template for stack "1/A" with ignore dependencies 17 | Then the user is told "the template is valid" 18 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/1/A.yaml: -------------------------------------------------------------------------------- 1 | stack_timeout: 1 2 | template: 3 | path: malformed_template.json 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/10/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: sam_template.yaml 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/11/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: sam_updated_template.yaml 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/12/1/2/3/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | stack_tags: 4 | Project: '{{ project }}' 5 | Key: '{{ keyC }}' 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/12/1/2/3/config.yaml: -------------------------------------------------------------------------------- 1 | keyC: "{{ keyB }}-C" 2 | project: C 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/12/1/2/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | stack_tags: 4 | Project: '{{ project }}' 5 | Key: '{{ keyB }}' 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/12/1/2/config.yaml: -------------------------------------------------------------------------------- 1 | keyB: '{{ keyA }}-B' 2 | project: B 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/12/1/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | stack_tags: 4 | Key: '{{ keyA }}' 5 | Project: '{{ project }}' 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/12/1/config.yaml: -------------------------------------------------------------------------------- 1 | keyA: A 2 | project: A 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/13/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | type: file 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/13/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: sceptre-test-artifacts/13/valid_template.json 3 | type: s3 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/13/C.yaml: -------------------------------------------------------------------------------- 1 | sceptre_user_data: 2 | type: AWS::CloudFormation::WaitConditionHandle 3 | template: 4 | path: sceptre-test-artifacts/13/jinja/valid_template.json 5 | type: s3 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/13/D.yaml: -------------------------------------------------------------------------------- 1 | sceptre_user_data: 2 | type: AWS::CloudFormation::WaitConditionHandle 3 | template: 4 | path: sceptre-test-artifacts/13/python/valid_template.json 5 | type: s3 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/2/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: updated_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/2/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: updated_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/2/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: updated_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/3/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/3/B.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - 3/A.yaml 3 | template: 4 | path: valid_template.json 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/3/C.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - 3/B.yaml 3 | template: 4 | path: valid_template.json 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/4/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/4/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/4/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | dependencies: 4 | - 3/A.yaml 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/5/1/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/5/1/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/5/1/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/5/2/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/5/2/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/5/2/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/1/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/independent_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/1/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/dependent_template.json 3 | parameters: 4 | DependentStackName: !stack_output 6/1/A.yaml::StackName 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/1/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/dependent_template.json 3 | parameters: 4 | DependentStackName: !stack_output 6/1/B.yaml::StackName 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/2/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/independent_template.json 3 | region: eu-west-1 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/2/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/dependent_template_local_export.json 3 | region: eu-west-2 4 | parameters: 5 | DependentStackName: !stack_output 6/2/A.yaml::StackName 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/2/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/dependent_template_local_export.json 3 | region: eu-west-3 4 | parameters: 5 | DependentStackName: !stack_output 6/2/B.yaml::StackName 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/3/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: output_template.json 3 | parameters: 4 | Input: "TestValue" 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/3/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: input_output_template.json 3 | parameters: 4 | Input: !stack_output 6/3/A.yaml::Output 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/3/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: input_output_template.json 3 | parameters: 4 | Input: !stack_output 6/3/B.yaml::Output 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/4/1/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/4/1/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/4/1/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/4/2/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/4/2/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/6/4/2/C.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/7/A.yaml: -------------------------------------------------------------------------------- 1 | sceptre_user_data: 2 | type: AWS::CloudFormation::WaitConditionHandle 3 | template: 4 | path: invalid_template_missing_attr.j2 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/8/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: invalid_template.json 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/8/B.yaml: -------------------------------------------------------------------------------- 1 | on_failure: DO_NOTHING 2 | template: 3 | path: invalid_template.json 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/8/C.yaml: -------------------------------------------------------------------------------- 1 | stack_timeout: 1 2 | template: 3 | path: valid_template_wait_300.json 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/config.yaml: -------------------------------------------------------------------------------- 1 | project_code: sceptre-integration-tests 2 | region: eu-west-1 3 | template_bucket_name: sceptre-integration-tests-templates 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/drift-group/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: loggroup.yaml 3 | type: file 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/drift-group/B.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: loggroup.yaml 3 | type: file 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/drift-single/A.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: loggroup.yaml 3 | type: file 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/external-stack-output/outputter.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/independent_template.json 3 | region: eu-west-1 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/external-stack-output/resolver-no-profile-region.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: dependencies/dependent_template_local_export.json 3 | 4 | parameters: 5 | DependentStackName: !stack_output_external "{{project_code}}-external-stack-output-outputter::StackName" 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/launch-actions/deploy.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: valid_template.yaml 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/launch-actions/ignore.yaml: -------------------------------------------------------------------------------- 1 | ignore: True 2 | 3 | template: 4 | path: valid_template.yaml 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/launch-actions/obsolete.yaml: -------------------------------------------------------------------------------- 1 | obsolete: True 2 | 3 | template: 4 | path: valid_template.yaml 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/project-deps/dependencies/assumed-role.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: "project-dependencies/assumed-role.yaml" 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/project-deps/dependencies/bucket.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: project-dependencies/bucket.yaml 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/project-deps/dependencies/topic.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: project-dependencies/topic.yaml 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/project-deps/main-project/config.yaml: -------------------------------------------------------------------------------- 1 | template_bucket_name: !stack_output project-deps/dependencies/bucket.yaml::BucketName 2 | notifications: 3 | - !stack_output project-deps/dependencies/topic.yaml::TopicArn 4 | 5 | sceptre_role: !stack_output project-deps/dependencies/assumed-role.yaml::RoleArn 6 | sceptre_role_session_duration: 1800 7 | stack_tags: 8 | greeting: !rcmd "echo 'hello' | tr -d '\n'" 9 | nonexistant: !no_value 10 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/project-deps/main-project/resource.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: "valid_template.yaml" 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/pruning/not-obsolete.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: /Users/jfalkenstein/sceptre/integration-tests/sceptre-project/templates/valid_template.json 3 | type: file 4 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/pruning/obsolete-1.yaml: -------------------------------------------------------------------------------- 1 | obsolete: true 2 | template: 3 | path: /Users/jfalkenstein/sceptre/integration-tests/sceptre-project/templates/valid_template.json 4 | type: file 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/config/pruning/obsolete-2.yaml: -------------------------------------------------------------------------------- 1 | obsolete: true 2 | template: 3 | path: /Users/jfalkenstein/sceptre/integration-tests/sceptre-project/templates/valid_template.json 4 | type: file 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/attribute_error.py: -------------------------------------------------------------------------------- 1 | def sceptre_handler(scepter_user_data): 2 | raise AttributeError 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/dependencies/dependent_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "DependentStackName": { 4 | "Type": "String" 5 | } 6 | }, 7 | "Resources": { 8 | "WaitConditionHandle": { 9 | "Type": "AWS::CloudFormation::WaitConditionHandle", 10 | "Properties": {} 11 | } 12 | }, 13 | "Outputs": { 14 | "StackName": { 15 | "Value": { 16 | "Ref": "AWS::StackName" 17 | } 18 | }, 19 | "Output": { 20 | "Value": { 21 | "Fn::ImportValue": { 22 | "Fn::Sub": "${DependentStackName}-Output" 23 | } 24 | }, 25 | "Export": { 26 | "Name": { 27 | "Fn::Sub": "${AWS::StackName}-Output" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/dependencies/dependent_template_local_export.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "DependentStackName": { 4 | "Type": "String" 5 | } 6 | }, 7 | "Resources": { 8 | "WaitConditionHandle": { 9 | "Type": "AWS::CloudFormation::WaitConditionHandle", 10 | "Properties": {} 11 | } 12 | }, 13 | "Outputs": { 14 | "StackName": { 15 | "Value": { 16 | "Ref": "AWS::StackName" 17 | } 18 | }, 19 | "Output": { 20 | "Value": { 21 | "Ref": "DependentStackName" 22 | }, 23 | "Export": { 24 | "Name": { 25 | "Fn::Sub": "${AWS::StackName}-Output" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/dependencies/independent_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | } 7 | }, 8 | "Outputs": { 9 | "StackName": { 10 | "Value": { 11 | "Ref": "AWS::StackName" 12 | } 13 | }, 14 | "Output": { 15 | "Value": "HelloFromStack1", 16 | "Export": { 17 | "Name": { 18 | "Fn::Sub": "${AWS::StackName}-Output" 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/input_output_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "Input": { 4 | "Type": "String" 5 | } 6 | }, 7 | "Resources": { 8 | "WaitConditionHandle": { 9 | "Type": "AWS::CloudFormation::WaitConditionHandle", 10 | "Properties": {} 11 | } 12 | }, 13 | "Outputs": { 14 | "Output": { 15 | "Value": { 16 | "Ref": "Input" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/invalid_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "InvalidWaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": { 6 | "Invalid": "Invalid" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/invalid_template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "AWS::CloudFormation::WaitConditionHandle" 4 | Properties: 5 | Invalid: Invalid 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/jinja/invalid_template_missing_attr.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "{{ sceptre_user_data.missing_attr }}" 4 | Properties: 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/jinja/invalid_template_missing_key.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "{{ non_existant_key.type }}" 4 | Properties: 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/jinja/valid_template.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "{{ sceptre_user_data.type }}" 4 | Properties: {} 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/jinja/valid_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "{{ sceptre_user_data.type }}", 5 | "Properties": {} 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/jinja/valid_template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "{{ sceptre_user_data.type }}" 4 | Properties: 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/malformed_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Malformed": { 3 | "InvalidWaitConditionHandle": { 4 | "Properties": { 5 | "Invalid": "Invalid" 6 | }, 7 | "Type": "AWS::CloudFormation::WaitConditionHandle" 8 | }, 9 | "WaitConditionHandle": { 10 | "NotProperties": {}, 11 | "Type": "Invalid::Resource::Type" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/malformed_template.yaml: -------------------------------------------------------------------------------- 1 | Malformed: 2 | WaitConditionHandle: 3 | Type: "AWS::CloudFormation::WaitConditionHandle" 4 | Malformed: 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/missing_sceptre_handler.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | pass 3 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/output_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | } 7 | }, 8 | "Outputs": { 9 | "Output": { 10 | "Value": "SomeValue" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/project-dependencies/assumed-role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Resources: 4 | Role: 5 | Type: AWS::IAM::Role 6 | Properties: 7 | Path: /service/ 8 | AssumeRolePolicyDocument: 9 | Version: "2012-10-17" 10 | Statement: 11 | - Action: sts:AssumeRole 12 | Effect: Allow 13 | Principal: 14 | AWS: !Ref AWS::AccountId 15 | ManagedPolicyArns: 16 | - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess 17 | - arn:aws:iam::aws:policy/AmazonS3FullAccess 18 | - arn:aws:iam::aws:policy/AmazonSNSFullAccess 19 | Policies: 20 | - PolicyName: "PassRolePermissions" 21 | PolicyDocument: 22 | Version: "2012-10-17" 23 | Statement: 24 | - Action: "iam:PassRole" 25 | Effect: Allow 26 | Resource: "*" 27 | MaxSessionDuration: 43200 28 | 29 | Outputs: 30 | RoleArn: 31 | Value: !GetAtt Role.Arn 32 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/project-dependencies/bucket.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Resources: 4 | Bucket: 5 | Type: AWS::S3::Bucket 6 | Properties: { } 7 | 8 | Outputs: 9 | BucketName: 10 | Value: !Ref Bucket 11 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/project-dependencies/topic.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Resources: 4 | Topic: 5 | Type: AWS::SNS::Topic 6 | Properties: {} 7 | 8 | Outputs: 9 | TopicArn: 10 | Value: !Ref Topic 11 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/python/valid_template.py: -------------------------------------------------------------------------------- 1 | from troposphere import Template 2 | from troposphere.cloudformation import WaitConditionHandle 3 | 4 | 5 | def sceptre_handler(scepter_user_data): 6 | template = Template() 7 | template.add_resource(WaitConditionHandle("WaitConditionHandle")) 8 | return template.to_json() 9 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/sam_template.yaml: -------------------------------------------------------------------------------- 1 | Transform: 'AWS::Serverless-2016-10-31' 2 | 3 | Resources: 4 | TestFunction: 5 | Type: 'AWS::Serverless::Function' 6 | Properties: 7 | Handler: index.handler 8 | Runtime: python3.9 9 | InlineCode: "print('Hello World!')" 10 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/sam_updated_template.yaml: -------------------------------------------------------------------------------- 1 | Transform: 'AWS::Serverless-2016-10-31' 2 | 3 | Resources: 4 | TestFunction2: 5 | Type: 'AWS::Serverless::Function' 6 | Properties: 7 | Handler: index.handler 8 | Runtime: python3.9 9 | InlineCode: "print('Hello Again World!')" 10 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/template.unsupported: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/topic.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Topic: 3 | Type: AWS::SNS::Topic 4 | Properties: 5 | DisplayName: MyTopic 6 | 7 | Outputs: 8 | TopicName: 9 | Value: !Ref Topic 10 | Export: 11 | Name: !Sub "${AWS::StackName}-TopicName" 12 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/updated_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | }, 7 | "AnotherWaitConditionHandle": { 8 | "Type": "AWS::CloudFormation::WaitConditionHandle", 9 | "Properties": {} 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/updated_template_wait_300.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | }, 7 | "AnotherWaitConditionHandle": { 8 | "Type": "AWS::CloudFormation::WaitConditionHandle", 9 | "Properties": {} 10 | }, 11 | "AnotherWaitCondition" : { 12 | "Type" : "AWS::CloudFormation::WaitCondition", 13 | "Properties" : { 14 | "Count" : 1, 15 | "Handle" : { "Ref" : "AnotherWaitConditionHandle" }, 16 | "Timeout" : "300" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "AWS::CloudFormation::WaitConditionHandle" 4 | Properties: {} 5 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template_func.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | WaitConditionHandle: 3 | Type: "AWS::CloudFormation::WaitConditionHandle" 4 | WaitCondition: 5 | Type: "AWS::CloudFormation::WaitCondition" 6 | Properties: 7 | Count: 1 8 | Handle: !Ref WaitConditionHandle 9 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def sceptre_handler(scepter_user_data): 5 | template = { 6 | "Resources": { 7 | "WaitConditionHandle": { 8 | "Type": "AWS::CloudFormation::WaitConditionHandle", 9 | "Properties": {}, 10 | } 11 | } 12 | } 13 | return json.dumps(template) 14 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template_mark.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | WaitConditionHandle: 4 | Type: "AWS::CloudFormation::WaitConditionHandle" 5 | Properties: 6 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template_wait_300.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "WaitConditionHandle": { 4 | "Type": "AWS::CloudFormation::WaitConditionHandle", 5 | "Properties": {} 6 | }, 7 | "WaitCondition" : { 8 | "Type" : "AWS::CloudFormation::WaitCondition", 9 | "Properties" : { 10 | "Count" : 1, 11 | "Handle" : { "Ref" : "WaitConditionHandle" }, 12 | "Timeout" : "300" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integration-tests/sceptre-project/templates/valid_template_yaml.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | def sceptre_handler(scepter_user_data): 5 | template = { 6 | "Resources": { 7 | "WaitConditionHandle": { 8 | "Type": "AWS::CloudFormation::WaitConditionHandle", 9 | "Properties": {}, 10 | } 11 | } 12 | } 13 | return yaml.dump(template) 14 | -------------------------------------------------------------------------------- /integration-tests/steps/drift.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | from helpers import get_cloudformation_stack_name 3 | 4 | import boto3 5 | 6 | from sceptre.plan.plan import SceptrePlan 7 | from sceptre.context import SceptreContext 8 | 9 | 10 | @given('a topic configuration in stack "{stack_name}" has drifted') 11 | def step_impl(context, stack_name): 12 | full_name = get_cloudformation_stack_name(context, stack_name) 13 | topic_arn = _get_output("TopicName", full_name) 14 | client = boto3.client("sns") 15 | client.set_topic_attributes( 16 | TopicArn=topic_arn, AttributeName="DisplayName", AttributeValue="WrongName" 17 | ) 18 | 19 | 20 | def _get_output(output_name, stack_name): 21 | client = boto3.client("cloudformation") 22 | response = client.describe_stacks(StackName=stack_name) 23 | for output in response["Stacks"][0]["Outputs"]: 24 | if output["OutputKey"] == output_name: 25 | return output["OutputValue"] 26 | 27 | 28 | @when('the user detects drift on stack "{stack_name}"') 29 | def step_impl(context, stack_name): 30 | sceptre_context = SceptreContext( 31 | command_path=stack_name + ".yaml", project_path=context.sceptre_dir 32 | ) 33 | sceptre_plan = SceptrePlan(sceptre_context) 34 | values = sceptre_plan.drift_detect().values() 35 | context.output = list(values) 36 | 37 | 38 | @when('the user shows drift on stack "{stack_name}"') 39 | def step_impl(context, stack_name): 40 | sceptre_context = SceptreContext( 41 | command_path=stack_name + ".yaml", project_path=context.sceptre_dir 42 | ) 43 | sceptre_plan = SceptrePlan(sceptre_context) 44 | values = sceptre_plan.drift_show().values() 45 | context.output = list(values) 46 | 47 | 48 | @when('the user detects drift on stack_group "{stack_group_name}"') 49 | def step_impl(context, stack_group_name): 50 | sceptre_context = SceptreContext( 51 | command_path=stack_group_name, project_path=context.sceptre_dir 52 | ) 53 | sceptre_plan = SceptrePlan(sceptre_context) 54 | values = sceptre_plan.drift_detect().values() 55 | context.output = list(values) 56 | 57 | 58 | @then('stack drift status is "{desired_status}"') 59 | def step_impl(context, desired_status): 60 | assert context.output[0]["StackDriftStatus"] == desired_status 61 | 62 | 63 | @then('stack resource drift status is "{desired_status}"') 64 | def step_impl(context, desired_status): 65 | assert ( 66 | context.output[0][1]["StackResourceDrifts"][0]["StackResourceDriftStatus"] 67 | == desired_status 68 | ) 69 | 70 | 71 | @then('stack_group drift statuses are each one of "{statuses}"') 72 | def step_impl(context, statuses): 73 | status_list = [status.strip() for status in statuses.split(",")] 74 | for output in context.output: 75 | assert output["StackDriftStatus"] in status_list 76 | -------------------------------------------------------------------------------- /integration-tests/steps/helpers.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | import os 3 | import time 4 | 5 | import jinja2.exceptions 6 | 7 | from botocore.exceptions import ClientError 8 | from sceptre.exceptions import TemplateSceptreHandlerError 9 | from sceptre.exceptions import UnsupportedTemplateFileTypeError 10 | from sceptre.exceptions import StackDoesNotExistError 11 | from sceptre.exceptions import SceptreException 12 | 13 | 14 | @then('the user is told "{message}"') 15 | def step_impl(context, message): 16 | if message == "stack does not exist": 17 | msg = context.error.response["Error"]["Message"] 18 | assert msg.endswith("does not exist") 19 | elif message == "change set does not exist": 20 | assert context.log_capture.find_event("does not exist") 21 | elif message == "the template is valid": 22 | for stack, status in context.response.items(): 23 | assert status["ResponseMetadata"]["HTTPStatusCode"] == 200 24 | elif message == "the template is malformed": 25 | msg = context.error.response["Error"]["Message"] 26 | assert msg.startswith("Template format error") 27 | elif message == "Failed describing Change Set": 28 | assert context.log_capture.find_event(message) 29 | else: 30 | raise Exception("Step has incorrect message") 31 | 32 | 33 | @then("no exception is raised") 34 | def step_impl(context): 35 | assert context.error is None 36 | 37 | 38 | @then('a "{exception_type}" is raised') 39 | def step_impl(context, exception_type): 40 | if exception_type == "TemplateSceptreHandlerError": 41 | assert isinstance(context.error, TemplateSceptreHandlerError) 42 | elif exception_type == "UnsupportedTemplateFileTypeError": 43 | assert isinstance(context.error, UnsupportedTemplateFileTypeError) 44 | elif exception_type == "StackDoesNotExistError": 45 | assert isinstance(context.error, StackDoesNotExistError) 46 | elif exception_type == "ClientError": 47 | assert isinstance(context.error, ClientError) 48 | elif exception_type == "AttributeError": 49 | assert isinstance(context.error, AttributeError) 50 | elif exception_type == "UndefinedError": 51 | assert isinstance(context.error, jinja2.exceptions.UndefinedError) 52 | elif exception_type == "SceptreException": 53 | assert isinstance(context.error, SceptreException) 54 | else: 55 | raise Exception("Step has incorrect message") 56 | 57 | 58 | @given('stack_group "{stack_group}" has AWS config "{config}" set') 59 | def step_impl(context, stack_group, config): 60 | config_path = os.path.join(context.sceptre_dir, "config", stack_group, config) 61 | 62 | os.environ["AWS_CONFIG_FILE"] = config_path 63 | 64 | 65 | def read_template_file(context, template_name): 66 | path = os.path.join(context.sceptre_dir, "templates", template_name) 67 | with open(path) as template: 68 | return template.read() 69 | 70 | 71 | def get_cloudformation_stack_name(context, stack_name): 72 | return "-".join([context.project_code, stack_name.replace("/", "-")]) 73 | 74 | 75 | def retry_boto_call(func, *args, **kwargs): 76 | delay = 5 77 | max_retries = 150 78 | attempts = 0 79 | while attempts < max_retries: 80 | attempts += 1 81 | try: 82 | response = func(*args, **kwargs) 83 | return response 84 | except ClientError as e: 85 | if e.response["Error"]["Code"] == "Throttling": 86 | time.sleep(delay) 87 | else: 88 | raise e 89 | -------------------------------------------------------------------------------- /integration-tests/steps/stack_policies.py: -------------------------------------------------------------------------------- 1 | from behave import * 2 | import json 3 | 4 | from botocore.exceptions import ClientError 5 | from sceptre.plan.plan import SceptrePlan 6 | from sceptre.context import SceptreContext 7 | 8 | from helpers import get_cloudformation_stack_name, retry_boto_call 9 | 10 | 11 | @given('the policy for stack "{stack_name}" is {state}') 12 | def step_impl(context, stack_name, state): 13 | full_name = get_cloudformation_stack_name(context, stack_name) 14 | retry_boto_call( 15 | context.client.set_stack_policy, 16 | StackName=full_name, 17 | StackPolicyBody=generate_stack_policy(state), 18 | ) 19 | 20 | 21 | @when('the user unlocks stack "{stack_name}"') 22 | def step_impl(context, stack_name): 23 | sceptre_context = SceptreContext( 24 | command_path=stack_name + ".yaml", project_path=context.sceptre_dir 25 | ) 26 | 27 | sceptre_plan = SceptrePlan(sceptre_context) 28 | 29 | try: 30 | sceptre_plan.unlock() 31 | except ClientError as e: 32 | context.error = e 33 | 34 | 35 | @when('the user locks stack "{stack_name}"') 36 | def step_impl(context, stack_name): 37 | sceptre_context = SceptreContext( 38 | command_path=stack_name + ".yaml", project_path=context.sceptre_dir 39 | ) 40 | 41 | sceptre_plan = SceptrePlan(sceptre_context) 42 | try: 43 | sceptre_plan.lock() 44 | except ClientError as e: 45 | context.error = e 46 | 47 | 48 | @then('the policy for stack "{stack_name}" is {state}') 49 | def step_impl(context, stack_name, state): 50 | full_name = get_cloudformation_stack_name(context, stack_name) 51 | policy = get_stack_policy(context, full_name) 52 | 53 | if state == "not set": 54 | assert policy is None 55 | 56 | 57 | def get_stack_policy(context, stack_name): 58 | try: 59 | response = retry_boto_call( 60 | context.client.get_stack_policy, StackName=stack_name 61 | ) 62 | except ClientError as e: 63 | if e.response["Error"]["Code"] == "ValidationError" and e.response["Error"][ 64 | "Message" 65 | ].endswith("does not exist"): 66 | return None 67 | else: 68 | raise e 69 | return response.get("StackPolicyBody") 70 | 71 | 72 | def generate_stack_policy(policy_type): 73 | data = "" 74 | if policy_type == "allow all": 75 | data = { 76 | "Statement": [ 77 | { 78 | "Effect": "Allow", 79 | "Action": "Update:*", 80 | "Principal": "*", 81 | "Resource": "*", 82 | } 83 | ] 84 | } 85 | elif policy_type == "deny all": 86 | data = { 87 | "Statement": [ 88 | { 89 | "Effect": "Deny", 90 | "Action": "Update:*", 91 | "Principal": "*", 92 | "Resource": "*", 93 | } 94 | ] 95 | } 96 | 97 | return json.dumps(data, sort_keys=True, indent=4, separators=(",", ": ")) 98 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "sceptre" 3 | version = "4.5.3" 4 | packages = [{ include = "sceptre" }] 5 | readme = "README.md" 6 | homepage = "https://github.com/Sceptre/sceptre" 7 | repository = "https://github.com/Sceptre/sceptre" 8 | documentation = "https://docs.sceptre-project.org" 9 | authors = ["Sceptre "] 10 | description = "An AWS Cloud Provisioning Tool" 11 | keywords = ["aws", "cloud", "devops", "infrastructure", "tools", "cli"] 12 | license = "Apache-2.0" 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "Natural Language :: English", 17 | "Environment :: Console", 18 | ] 19 | 20 | [tool.poetry.scripts] 21 | "sceptre" = "sceptre.cli:cli" 22 | 23 | [tool.poetry.plugins."sceptre.hooks"] 24 | "asg_scheduled_actions" = "sceptre.hooks.asg_scaling_processes:ASGScalingProcesses" 25 | "asg_scaling_processes" = "sceptre.hooks.asg_scaling_processes:ASGScalingProcesses" 26 | "cmd" = "sceptre.hooks.cmd:Cmd" 27 | 28 | [tool.poetry.plugins."sceptre.resolvers"] 29 | "environment_variable" = "sceptre.resolvers.environment_variable:EnvironmentVariable" 30 | "file_contents" = "sceptre.resolvers.file_contents:FileContents" 31 | "stack_output" = "sceptre.resolvers.stack_output:StackOutput" 32 | "stack_output_external" = "sceptre.resolvers.stack_output:StackOutputExternal" 33 | "no_value" = "sceptre.resolvers.no_value:NoValue" 34 | "select" = "sceptre.resolvers.select:Select" 35 | "stack_attr" = "sceptre.resolvers.stack_attr:StackAttr" 36 | "sub" = "sceptre.resolvers.sub:Sub" 37 | "split" = "sceptre.resolvers.split:Split" 38 | "join" = "sceptre.resolvers.join:Join" 39 | 40 | [tool.poetry.plugins."sceptre.template_handlers"] 41 | "file" = "sceptre.template_handlers.file:File" 42 | "s3" = "sceptre.template_handlers.s3:S3" 43 | "http" = "sceptre.template_handlers.http:Http" 44 | 45 | [tool.poetry.dependencies] 46 | python = "^3.9" 47 | boto3 = "^1.20.27" 48 | click = ">=7.0,<9.0" 49 | cfn-flip = "^1.2.3" 50 | deepdiff = "^8.0" 51 | deprecation = "^2.0" 52 | jinja2 = "^3.0" 53 | jsonschema = "~3.2" 54 | networkx = "~2.6" 55 | packaging = ">=16.8,<25.0" # Some old tools in the Sceptre ecosystem pin packaging to 16.8. 56 | pyyaml = "^6.0" 57 | sceptre-cmd-resolver = "^2.0" 58 | sceptre-file-resolver = "^1.0" 59 | colorama = ">=0.2.5,<0.4.4" 60 | troposphere = { version = "^4", optional = true } 61 | sphinx = { version = ">=1.6.5,<=5.1.1", optional = true } 62 | sphinx-click = { version = ">=2.0.1,<4.0.0", optional = true } 63 | sphinx-rtd-theme = { version = "0.5.2", optional = true } 64 | sphinx-autodoc-typehints = { version = "1.19.2", optional = true } 65 | docutils = { version = "<0.17", optional = true } # temporary fix for sphinx-rtd-theme==0.5.2, it depends on docutils<0.17 66 | 67 | [tool.poetry.extras] 68 | troposphere = ["troposphere"] 69 | docs = [ 70 | "sphinx", 71 | "sphinx-click", 72 | "sphinx-rtd-theme", 73 | "sphinx-autodoc-typehints", 74 | "docutils" 75 | ] 76 | 77 | [tool.poetry.group.dev.dependencies] 78 | pre-commit = "^4.2" 79 | behave = "^1.2" 80 | freezegun = ">=1.5.0,<1.5.1" 81 | pygments = "^2.2" 82 | pytest = "^7.4.3" 83 | pytest-cov = "^2.11.1" 84 | pytest-sugar = "^0.9.4" 85 | readme-renderer = "^24.0" 86 | requests-mock = "^1.9.3" 87 | tox = "^3.23.0" 88 | troposphere = "^4" 89 | tox-gh-matrix = "^0.2" 90 | 91 | [build-system] 92 | requires = ["poetry-core>=1.0.0"] 93 | build-backend = "poetry.core.masonry.api" 94 | -------------------------------------------------------------------------------- /sceptre/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import warnings 5 | import sys 6 | 7 | from importlib.metadata import version 8 | 9 | __author__ = "SceptreOrg" 10 | __email__ = "sceptreorg@gmail.com" 11 | __version__ = version(__package__ or __name__) 12 | 13 | 14 | # Set up logging to ``/dev/null`` like a library is supposed to. 15 | # http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library 16 | class NullHandler(logging.Handler): # pragma: no cover 17 | def emit(self, record): 18 | pass 19 | 20 | 21 | if not sys.warnoptions: 22 | warnings.filterwarnings("default", category=DeprecationWarning, module="sceptre") 23 | 24 | logging.getLogger("sceptre").addHandler(NullHandler()) 25 | -------------------------------------------------------------------------------- /sceptre/cli/create.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from typing import Optional 4 | from sceptre.context import SceptreContext 5 | from sceptre.cli.helpers import catch_exceptions, confirmation 6 | from sceptre.plan.plan import SceptrePlan 7 | from sceptre.cli.helpers import stack_status_exit_code 8 | 9 | 10 | @click.command(name="create", short_help="Creates a stack or a change set.") 11 | @click.argument("path") 12 | @click.argument("change-set-name", required=False) 13 | @click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") 14 | @click.option( 15 | "--disable-rollback/--enable-rollback", 16 | default=None, 17 | help="Disable or enable the cloudformation automatic rollback", 18 | ) 19 | @click.pass_context 20 | @catch_exceptions 21 | def create_command(ctx, path, change_set_name, yes, disable_rollback: Optional[bool]): 22 | """ 23 | Creates a stack for a given config PATH. Or if CHANGE_SET_NAME is specified 24 | creates a change set for stack in PATH. 25 | \f 26 | 27 | :param path: Path to a Stack or StackGroup 28 | :type path: str 29 | :param change_set_name: A name of the Change Set - optional 30 | :type change_set_name: str 31 | :param yes: A flag to assume yes to all questions. 32 | :type yes: bool 33 | :param disable_rollback: A flag to disable cloudformation rollback. 34 | """ 35 | context = SceptreContext( 36 | command_path=path, 37 | command_params=ctx.params, 38 | project_path=ctx.obj.get("project_path"), 39 | user_variables=ctx.obj.get("user_variables"), 40 | options=ctx.obj.get("options"), 41 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 42 | ) 43 | 44 | action = "create" 45 | plan = SceptrePlan(context) 46 | 47 | if change_set_name: 48 | confirmation(action, yes, change_set=change_set_name, command_path=path) 49 | plan.create_change_set(change_set_name) 50 | else: 51 | confirmation(action, yes, command_path=path) 52 | responses = plan.create() 53 | exit(stack_status_exit_code(responses.values())) 54 | -------------------------------------------------------------------------------- /sceptre/cli/delete.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from sceptre.context import SceptreContext 4 | from sceptre.cli.helpers import catch_exceptions 5 | from sceptre.cli.helpers import confirmation 6 | from sceptre.cli.helpers import stack_status_exit_code 7 | from sceptre.plan.plan import SceptrePlan 8 | 9 | from colorama import Fore, Style 10 | 11 | 12 | @click.command(name="delete", short_help="Deletes a stack or a change set.") 13 | @click.argument("path") 14 | @click.argument("change-set-name", required=False) 15 | @click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") 16 | @click.pass_context 17 | @catch_exceptions 18 | def delete_command(ctx, path, change_set_name, yes): 19 | """ 20 | Deletes a stack for a given config PATH. Or if CHANGE_SET_NAME is specified 21 | deletes a change set for stack in PATH. 22 | \f 23 | 24 | :param path: Path to execute command on. 25 | :type path: str 26 | :param change_set_name: The name of the change set to use - optional 27 | :type change_set_name: str 28 | :param yes: Flag to answer yes to all CLI questions. 29 | :type yes: bool 30 | """ 31 | context = SceptreContext( 32 | command_path=path, 33 | command_params=ctx.params, 34 | project_path=ctx.obj.get("project_path"), 35 | user_variables=ctx.obj.get("user_variables"), 36 | options=ctx.obj.get("options"), 37 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 38 | full_scan=True, 39 | ) 40 | 41 | plan = SceptrePlan(context) 42 | plan.resolve(command="delete", reverse=True) 43 | 44 | if change_set_name: 45 | delete_msg = ( 46 | "The Change Set will be delete on the following stacks, if applicable:\n" 47 | ) 48 | else: 49 | delete_msg = "The following stacks, in the following order, will be deleted:\n" 50 | 51 | dependencies = "" 52 | for stack in plan: 53 | dependencies += "{}{}{}\n".format(Fore.YELLOW, stack.name, Style.RESET_ALL) 54 | 55 | print(delete_msg + "{}".format(dependencies)) 56 | 57 | confirmation( 58 | plan.delete.__name__, yes, change_set=change_set_name, command_path=path 59 | ) 60 | if change_set_name: 61 | plan.delete_change_set(change_set_name) 62 | else: 63 | responses = plan.delete() 64 | exit(stack_status_exit_code(responses.values())) 65 | -------------------------------------------------------------------------------- /sceptre/cli/describe.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from sceptre.context import SceptreContext 4 | from sceptre.cli.helpers import catch_exceptions, simplify_change_set_description, write 5 | from sceptre.plan.plan import SceptrePlan 6 | 7 | 8 | @click.group(name="describe") 9 | @click.pass_context 10 | def describe_group(ctx): 11 | """ 12 | Commands for describing attributes of stacks. 13 | """ 14 | pass 15 | 16 | 17 | @describe_group.command(name="change-set") 18 | @click.argument("path") 19 | @click.argument("change-set-name") 20 | @click.option("-v", "--verbose", is_flag=True, help="Display verbose output.") 21 | @click.pass_context 22 | @catch_exceptions 23 | def describe_change_set(ctx, path, change_set_name, verbose): 24 | """ 25 | Describes the change set. 26 | \f 27 | 28 | :param path: Path to execute the command on. 29 | :type path: str 30 | :param change_set_name: Name of the Change Set to use. 31 | :type change_set_name: str 32 | :param verbose: A flag to display verbose output. 33 | :type verbose: bool 34 | """ 35 | context = SceptreContext( 36 | command_path=path, 37 | command_params=ctx.params, 38 | project_path=ctx.obj.get("project_path"), 39 | user_variables=ctx.obj.get("user_variables"), 40 | options=ctx.obj.get("options"), 41 | output_format=ctx.obj.get("output_format"), 42 | no_colour=ctx.obj.get("no_colour"), 43 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 44 | ) 45 | 46 | plan = SceptrePlan(context) 47 | 48 | responses = plan.describe_change_set(change_set_name) 49 | for response in responses.values(): 50 | description = response 51 | if not verbose: 52 | description = simplify_change_set_description(description) 53 | write(description, context.output_format, context.no_colour) 54 | 55 | 56 | @describe_group.command(name="policy") 57 | @click.argument("path") 58 | @click.pass_context 59 | @catch_exceptions 60 | def describe_policy(ctx, path): 61 | """ 62 | Displays the stack policy used. 63 | \f 64 | 65 | :param path: Path to execute the command on. 66 | :type path: str 67 | """ 68 | context = SceptreContext( 69 | command_path=path, 70 | command_params=ctx.params, 71 | project_path=ctx.obj.get("project_path"), 72 | user_variables=ctx.obj.get("user_variables"), 73 | options=ctx.obj.get("options"), 74 | output_format=ctx.obj.get("output_format"), 75 | no_colour=ctx.obj.get("no_colour"), 76 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 77 | ) 78 | 79 | plan = SceptrePlan(context) 80 | responses = plan.get_policy() 81 | for response in responses.values(): 82 | write(response, context.output_format, context.no_colour) 83 | -------------------------------------------------------------------------------- /sceptre/cli/execute.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from typing import Optional 4 | from sceptre.context import SceptreContext 5 | from sceptre.cli.helpers import catch_exceptions, confirmation 6 | from sceptre.plan.plan import SceptrePlan 7 | 8 | 9 | @click.command(name="execute") 10 | @click.argument("path") 11 | @click.argument("change-set-name") 12 | @click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") 13 | @click.option( 14 | "--disable-rollback/--enable-rollback", 15 | default=None, 16 | help="Disable or enable the cloudformation automatic rollback", 17 | ) 18 | @click.pass_context 19 | @catch_exceptions 20 | def execute_command(ctx, path, change_set_name, yes, disable_rollback: Optional[bool]): 21 | """ 22 | Executes a Change Set. 23 | \f 24 | 25 | :param path: Path to execute the command on. 26 | :type path: str 27 | :param change_set_name: Change Set to use. 28 | :type change_set_name: str 29 | :param yes: A flag to answer 'yes' too all CLI questions. 30 | :type yes: bool 31 | """ 32 | context = SceptreContext( 33 | command_path=path, 34 | command_params=ctx.params, 35 | project_path=ctx.obj.get("project_path"), 36 | user_variables=ctx.obj.get("user_variables"), 37 | options=ctx.obj.get("options"), 38 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 39 | ) 40 | 41 | plan = SceptrePlan(context) 42 | confirmation( 43 | plan.execute_change_set.__name__, 44 | yes, 45 | change_set=change_set_name, 46 | command_path=path, 47 | ) 48 | plan.execute_change_set(change_set_name) 49 | -------------------------------------------------------------------------------- /sceptre/cli/policy.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from sceptre.context import SceptreContext 4 | from sceptre.cli.helpers import catch_exceptions 5 | from sceptre.plan.plan import SceptrePlan 6 | 7 | 8 | @click.command(name="set-policy", short_help="Sets Stack policy.") 9 | @click.argument("path") 10 | @click.argument("policy-file", required=False) 11 | @click.option( 12 | "-b", 13 | "--built-in", 14 | type=click.Choice(["deny-all", "allow-all"]), 15 | help="Specify a built in stack policy.", 16 | ) 17 | @click.pass_context 18 | @catch_exceptions 19 | def set_policy_command(ctx, path, policy_file, built_in): 20 | """ 21 | Sets a specific Stack policy for either a file or using a built-in policy. 22 | \f 23 | 24 | :param path: Path to execute the command on. 25 | :type path: str 26 | :param policy_file: path to the AWS Policy file to use. 27 | :type policy_file: str 28 | :param built_in: the name of the built-in policy file to use. 29 | :type built_in: str 30 | """ 31 | context = SceptreContext( 32 | command_path=path, 33 | command_params=ctx.params, 34 | project_path=ctx.obj.get("project_path"), 35 | user_variables=ctx.obj.get("user_variables"), 36 | options=ctx.obj.get("options"), 37 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 38 | ) 39 | plan = SceptrePlan(context) 40 | 41 | if built_in == "deny-all": 42 | plan.lock() 43 | elif built_in == "allow-all": 44 | plan.unlock() 45 | else: 46 | plan.set_policy(policy_file) 47 | -------------------------------------------------------------------------------- /sceptre/cli/status.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from sceptre.context import SceptreContext 4 | from sceptre.cli.helpers import catch_exceptions, write 5 | from sceptre.plan.plan import SceptrePlan 6 | 7 | 8 | @click.command(name="status", short_help="Print status of stack or stack_group.") 9 | @click.argument("path") 10 | @click.pass_context 11 | @catch_exceptions 12 | def status_command(ctx, path): 13 | """ 14 | Prints the stack status or the status of the stacks within a 15 | stack_group for a given config PATH. 16 | \f 17 | 18 | :param path: Path to execute the command on. 19 | :type path: str 20 | """ 21 | context = SceptreContext( 22 | command_path=path, 23 | command_params=ctx.params, 24 | project_path=ctx.obj.get("project_path"), 25 | user_variables=ctx.obj.get("user_variables"), 26 | options=ctx.obj.get("options"), 27 | no_colour=ctx.obj.get("no_colour"), 28 | output_format=ctx.obj.get("output_format"), 29 | ignore_dependencies=ctx.obj.get("ignore_dependencies"), 30 | ) 31 | 32 | plan = SceptrePlan(context) 33 | responses = plan.get_status() 34 | message = "\n".join( 35 | "{}: {}".format(stack.name, status) for stack, status in responses.items() 36 | ) 37 | write(message, no_colour=context.no_colour) 38 | -------------------------------------------------------------------------------- /sceptre/config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | 6 | __author__ = "Cloudreach" 7 | __email__ = "sceptre@cloudreach.com" 8 | 9 | 10 | # Set up logging to ``/dev/null`` like a library is supposed to. 11 | # http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library 12 | class NullHandler(logging.Handler): # pragma: no cover 13 | def emit(self, record): 14 | pass 15 | 16 | 17 | logging.getLogger("sceptre").addHandler(NullHandler()) 18 | -------------------------------------------------------------------------------- /sceptre/config/strategies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | sceptre.config.strategies 5 | 6 | This module contains the implementations of the strategies used to merge config 7 | attributes. 8 | """ 9 | from copy import deepcopy 10 | 11 | 12 | def list_join(a, b): 13 | """ 14 | Takes two lists and joins them. 15 | 16 | :param a: A list. 17 | :type a: list 18 | :param b: A list. 19 | :type b: list 20 | :returns: A joined list from the two parameters. 21 | :rtype: list 22 | """ 23 | if a and not isinstance(a, list): 24 | raise TypeError("{} is not a list".format(a)) 25 | 26 | if b and not isinstance(b, list): 27 | raise TypeError("{} is not a list".format(b)) 28 | 29 | if a is None: 30 | return deepcopy(b) 31 | 32 | if b is not None: 33 | return deepcopy(a + b) 34 | 35 | return deepcopy(a) 36 | 37 | 38 | def dict_merge(a, b): 39 | """ 40 | Takes two dictionaries and merges them. 41 | 42 | :param a: A dictionary. 43 | :type a: dict 44 | :param b: A dictionary. 45 | :type b: dict 46 | :returns: A merged dict. 47 | :rtype: dict 48 | """ 49 | if a and not isinstance(a, dict): 50 | raise TypeError("{} is not a dict".format(a)) 51 | if b and not isinstance(b, dict): 52 | raise TypeError("{} is not a dict".format(b)) 53 | 54 | if a is None: 55 | return deepcopy(b) 56 | 57 | if b is not None: 58 | return deepcopy({**a, **b}) 59 | 60 | return deepcopy(a) 61 | 62 | 63 | def child_wins(a, b): 64 | """ 65 | Always returns the second parameter. 66 | 67 | :param a: An object. 68 | :type a: object 69 | :param b: An object. 70 | :type b: object 71 | :returns: b 72 | """ 73 | return b 74 | 75 | 76 | def child_or_parent(a, b): 77 | """ 78 | Returns the second arg if it is not empty, else the first. 79 | 80 | :param a: An object. 81 | :type a: object 82 | :param b: An object. 83 | :type b: object 84 | :returns: b 85 | """ 86 | return b or a 87 | 88 | 89 | LIST_STRATEGIES = { 90 | "merge": list_join, 91 | "override": child_wins, 92 | } 93 | DICT_STRATEGIES = { 94 | "merge": dict_merge, 95 | "override": child_wins, 96 | } 97 | -------------------------------------------------------------------------------- /sceptre/diffing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/sceptre/diffing/__init__.py -------------------------------------------------------------------------------- /sceptre/hooks/cmd.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from sceptre.hooks import Hook 3 | from sceptre.exceptions import InvalidHookArgumentTypeError 4 | 5 | 6 | class Cmd(Hook): 7 | """ 8 | Cmd implements a Sceptre hook which can run arbitrary commands. 9 | """ 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(Cmd, self).__init__(*args, **kwargs) 13 | 14 | def run(self): 15 | """ 16 | Executes a command through the shell. 17 | 18 | See hooks documentation for details. 19 | 20 | :raises: sceptre.exceptions.InvalidHookArgumentTypeError invalid input 21 | :raises: CalledProcessError failed command 22 | :raises: FileNotFoundError missing shell 23 | :raises: PermissionError non-executable shell 24 | """ 25 | envs = self.stack.connection_manager.create_session_environment_variables() 26 | 27 | if isinstance(self.argument, str) and self.argument != "": 28 | command_to_run = self.argument 29 | shell = None 30 | 31 | elif ( 32 | isinstance(self.argument, dict) 33 | and set(self.argument) == {"run", "shell"} 34 | and isinstance(self.argument["run"], str) 35 | and isinstance(self.argument["shell"], str) 36 | and self.argument["run"] != "" 37 | and self.argument["shell"] != "" 38 | ): 39 | command_to_run = self.argument["run"] 40 | shell = self.argument["shell"] 41 | 42 | else: 43 | raise InvalidHookArgumentTypeError( 44 | "A cmd hook requires either a string argument or an object with " 45 | "`run` and `shell` keys with string values. " 46 | f"You gave `{self.argument!r}`." 47 | ) 48 | 49 | subprocess.check_call(command_to_run, shell=True, env=envs, executable=shell) 50 | -------------------------------------------------------------------------------- /sceptre/logging.py: -------------------------------------------------------------------------------- 1 | from logging import LoggerAdapter, Logger 2 | from typing import MutableMapping, Any, Tuple 3 | 4 | 5 | class StackLoggerAdapter(LoggerAdapter): 6 | def __init__(self, logger: Logger, stack_name: str, extra: dict = None): 7 | """A small wrapper around a Logger that prefixes log messages with the stack name. 8 | 9 | :param logger: The logger to wrap 10 | :param stack_name: The name of the stack to every log message 11 | :param extra: Extra kwargs to add to the log context (if any) 12 | """ 13 | super().__init__(logger, extra or {}) 14 | self.stack_name = stack_name 15 | 16 | def process( 17 | self, msg: str, kwargs: MutableMapping[str, Any] 18 | ) -> Tuple[Any, MutableMapping[str, Any]]: 19 | msg = f"{self.stack_name} - {msg}" 20 | return super().process(msg, kwargs) 21 | -------------------------------------------------------------------------------- /sceptre/plan/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | 6 | __author__ = "Cloudreach" 7 | __email__ = "sceptre@cloudreach.com" 8 | 9 | 10 | # Set up logging to ``/dev/null`` like a library is supposed to. 11 | # http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library 12 | class NullHandler(logging.Handler): # pragma: no cover 13 | def emit(self, record): 14 | pass 15 | 16 | 17 | logging.getLogger("sceptre").addHandler(NullHandler()) 18 | -------------------------------------------------------------------------------- /sceptre/plan/executor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | sceptre.plan.executor 5 | 6 | This module implements a SceptrePlanExecutor, which is responsible for 7 | executing the command specified in a SceptrePlan. 8 | """ 9 | import logging 10 | from concurrent.futures import ThreadPoolExecutor, as_completed 11 | from typing import List, Set 12 | 13 | from sceptre.plan.actions import StackActions 14 | from sceptre.stack import Stack 15 | 16 | 17 | class SceptrePlanExecutor(object): 18 | def __init__(self, command: str, launch_order: List[Set[Stack]]): 19 | """ 20 | Initialises a SceptrePlanExecutor, generates the launch order, threads 21 | and intial Stack Statuses. 22 | 23 | :param command: The command to execute on the Stack. 24 | 25 | :param launch_order: A list containing sets of Stacks that can be executed concurrently. 26 | """ 27 | 28 | self.logger = logging.getLogger(__name__) 29 | self.command = command 30 | self.launch_order = launch_order 31 | # Select the number of threads based upon the max batch size, 32 | # or use 1 if all batches are empty 33 | self.num_threads = len(max(launch_order, key=len)) or 1 34 | 35 | def execute(self, *args): 36 | """ 37 | Execute is responsible executing the sets of Stacks in launch_order 38 | concurrently, in the correct order. 39 | 40 | :param args: Any arguments that should be passed through to the 41 | StackAction being called. 42 | """ 43 | responses = {} 44 | 45 | with ThreadPoolExecutor(max_workers=self.num_threads) as executor: 46 | for batch in self.launch_order: 47 | futures = [ 48 | executor.submit(self._execute, stack, *args) for stack in batch 49 | ] 50 | 51 | for future in as_completed(futures): 52 | stack, status = future.result() 53 | responses[stack] = status 54 | 55 | return responses 56 | 57 | def _execute(self, stack, *args): 58 | actions = StackActions(stack) 59 | result = getattr(actions, self.command)(*args) 60 | return stack, result 61 | -------------------------------------------------------------------------------- /sceptre/resolvers/environment_variable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from sceptre.resolvers import Resolver 6 | 7 | 8 | class EnvironmentVariable(Resolver): 9 | """ 10 | Resolver for shell environment variables. 11 | 12 | :param argument: Name of the environment variable to return. 13 | :type argument: str 14 | """ 15 | 16 | def __init__(self, *args, **kwargs): 17 | super(EnvironmentVariable, self).__init__(*args, **kwargs) 18 | 19 | def resolve(self): 20 | """ 21 | Retrieves the value of a named environment variable. 22 | 23 | :returns: Value of the environment variable. 24 | :rtype: str 25 | """ 26 | value = None 27 | if self.argument: 28 | value = os.environ.get(self.argument) 29 | return value 30 | -------------------------------------------------------------------------------- /sceptre/resolvers/file_contents.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from sceptre.resolvers import Resolver 4 | 5 | 6 | class FileContents(Resolver): 7 | """ 8 | Resolver for the contents of a file. 9 | 10 | :param argument: Absolute path to file. 11 | :type argument: str 12 | """ 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(FileContents, self).__init__(*args, **kwargs) 16 | 17 | def resolve(self): 18 | """ 19 | Retrieves the contents of a file at a given absolute file path. 20 | 21 | :returns: Contents of file. 22 | :rtype: str 23 | """ 24 | try: 25 | with open(self.argument, "r") as file: 26 | return file.read() 27 | except (EnvironmentError, TypeError) as e: 28 | raise e 29 | -------------------------------------------------------------------------------- /sceptre/resolvers/join.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class Join(Resolver): 5 | """This resolver allows you to join multiple strings together to form a single string. This is 6 | great for combining the outputs of multiple resolvers. This resolver works just like 7 | CloudFormation's ``!Join`` intrinsic function. 8 | 9 | The argument for this resolver should be a list with two elements: (1) A string to join the 10 | elements on and (2) a list of items to join. 11 | 12 | Example: 13 | 14 | parameters: 15 | BaseUrl: !join 16 | - ":" 17 | - - !stack_output my/app/stack.yaml::HostName 18 | - !stack_output my/other/stack.yaml::Port 19 | 20 | """ 21 | 22 | def resolve(self): 23 | error_message = ( 24 | "The argument to !join must be a 2-element list, where the first element is the join " 25 | "string and the second is a list of items to join." 26 | ) 27 | if not isinstance(self.argument, list) or len(self.argument) != 2: 28 | self.raise_invalid_argument_error(error_message) 29 | 30 | delimiter, items_list = self.argument 31 | if not isinstance(delimiter, str) or not isinstance(items_list, list): 32 | self.raise_invalid_argument_error(error_message) 33 | 34 | string_items = map(str, items_list) 35 | joined = delimiter.join(string_items) 36 | return joined 37 | -------------------------------------------------------------------------------- /sceptre/resolvers/no_value.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class NoValue(Resolver): 5 | """This resolver resolves to nothing, functioning just like the AWS::NoValue special value. When 6 | assigned to a resolvable Stack property, it will remove the config key/value from the stack or 7 | the container on the stack where it has been assigned, as if this value wasn't assigned at all. 8 | 9 | This is mostly useful for simplifying conditional logic on Stack and StackGroup config files 10 | where, if a certain condition is met, a value is passed, otherwise it's not passed at all. 11 | """ 12 | 13 | def resolve(self) -> None: 14 | return None 15 | -------------------------------------------------------------------------------- /sceptre/resolvers/select.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class Select(Resolver): 5 | """This resolver allows you to select a specific index from a list of items or a specific key 6 | from a dict.. This is great for combining with the ``!split`` resolver to obtain part of a 7 | string. This function works almost the same as CloudFormation's ``!Select`` intrinsic function, 8 | **except (1) you can use this with negative indexes to select with a reverse index** and (2) 9 | you can select keys from a dict. 10 | 11 | The argument for this resolver should be a list with two elements: (1) A numerical index or 12 | string key and (2) a list or dict of items to select out of. If the index is negative, 13 | it will select from the end of the list. For example, "-1" would select the last element and 14 | "-2" would select the second-to-last element. 15 | 16 | Example: 17 | 18 | parameters: 19 | # This selects the last element after you split the connection string on "/" 20 | DatabaseName: !select 21 | - -1 22 | - !split ["/", !stack_output my/database/stack.yaml::ConnectionString] 23 | """ 24 | 25 | def resolve(self): 26 | error_message = ( 27 | "The argument to !select must be a two-element list, where the first element is the " 28 | "index or key to select with and the second element is the list or dict to select from." 29 | ) 30 | if not isinstance(self.argument, list) or len(self.argument) != 2: 31 | self.raise_invalid_argument_error(error_message) 32 | 33 | index, items = self.argument 34 | if not isinstance(items, (dict, list)): 35 | self.raise_invalid_argument_error(error_message) 36 | 37 | try: 38 | return items[index] 39 | except (TypeError, KeyError, IndexError) as e: 40 | self.raise_invalid_argument_error( 41 | f"Could not select with index/key {index}: {e}", e 42 | ) 43 | -------------------------------------------------------------------------------- /sceptre/resolvers/split.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class Split(Resolver): 5 | """This resolver will split a value on a given delimiter string. This is great when combining with the 6 | ``!select`` resolver. This function works the same as CloudFormation's ``!Split`` intrinsic function. 7 | 8 | Note: The return value of this resolver is a *list*, not a string. This will not work to set Stack 9 | configurations that expect strings, but it WILL work to set Stack configurations that expect lists. 10 | 11 | The argument for this resolver should be a list with two elements: (1) The delimiter to split on and 12 | (2) a string to split. 13 | 14 | Example: 15 | notifications: !split 16 | - ";" 17 | - !stack_output my/sns/topics.yaml::SemicolonDelimitedArns 18 | """ 19 | 20 | def resolve(self): 21 | error_message = ( 22 | "The argument to !split must be a two-element list, where the first element is the " 23 | "string to split on and the second element string to split." 24 | ) 25 | if ( 26 | not isinstance(self.argument, list) 27 | or len(self.argument) != 2 28 | or not all(isinstance(a, str) for a in self.argument) 29 | ): 30 | self.raise_invalid_argument_error(error_message) 31 | 32 | split_on, split_string = self.argument 33 | return split_string.split(split_on) 34 | -------------------------------------------------------------------------------- /sceptre/resolvers/stack_attr.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from sceptre.resolvers import Resolver 4 | 5 | 6 | class StackAttr(Resolver): 7 | """Resolves to the value of another field on the Stack Config, including other resolvers. 8 | 9 | The argument for this resolver should be the "key path" from the stack object, which can access 10 | nested keys/indexes using a "." to separate segments. 11 | 12 | For example, given this Stack Config structure... 13 | 14 | sceptre_user_data: 15 | nested_list: 16 | - first 17 | - second 18 | 19 | Using "!stack_attr sceptre_user_data.nested_list.1" on your stack would resolve to "second". 20 | """ 21 | 22 | # These are all the attributes on Stack Configs whose names are changed when they are assigned 23 | # to the Stack instance. 24 | STACK_ATTR_MAP = { 25 | "template": "template_handler_config", 26 | "protect": "protected", 27 | "stack_name": "external_name", 28 | "stack_tags": "tags", 29 | } 30 | 31 | def resolve(self) -> Any: 32 | """Returns the resolved value of the field referenced by the resolver's argument.""" 33 | segments = self.argument.split(".") 34 | 35 | # Remap top-level attributes to match stack config 36 | first_segment = segments[0] 37 | segments[0] = self.STACK_ATTR_MAP.get(first_segment, first_segment) 38 | 39 | if self._key_is_from_stack_group_config(first_segment): 40 | obj = self.stack.stack_group_config 41 | else: 42 | obj = self.stack 43 | 44 | result = self._recursively_resolve_segments(obj, segments) 45 | return result 46 | 47 | def _key_is_from_stack_group_config(self, key: str): 48 | return key in self.stack.stack_group_config and not hasattr(self.stack, key) 49 | 50 | def _recursively_resolve_segments(self, obj: Any, segments: List[str]): 51 | if not segments: 52 | return obj 53 | 54 | attr_name, *rest = segments 55 | if isinstance(obj, dict): 56 | value = obj[attr_name] 57 | elif isinstance(obj, list): 58 | value = obj[int(attr_name)] 59 | else: 60 | value = getattr(obj, attr_name) 61 | 62 | return self._recursively_resolve_segments(value, rest) 63 | -------------------------------------------------------------------------------- /sceptre/resolvers/sub.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class Sub(Resolver): 5 | """This resolver allows you to create a string using Python string format syntax. This is a 6 | great way to combine together a number of resolver outputs into a single string. This functions 7 | very similarly to Cloudformation's ``!Sub`` intrinsic function. 8 | 9 | The argument to this resolver should be a two-element list: (1) Is the format string, using 10 | curly-brace templates to indicate variables, and (2) a dictionary where the keys are the format 11 | string's variable names and the values are the variable values. 12 | 13 | Example: 14 | 15 | parameters: 16 | ConnectionString: !sub 17 | - "postgres://{username}:{password}@{hostname}:{port}/{database}" 18 | - username: {{ var.username }} 19 | password: !ssm /my/ssm/password 20 | hostname: !stack_output my/database/stack.yaml::HostName 21 | port: !stack_output my/database/stack.yaml::Port 22 | database: {{var.database}} 23 | """ 24 | 25 | def resolve(self): 26 | error_message = ( 27 | "The argument to !sub must be a two-element list, where the first element is the " 28 | "a format string and the second element is a dict of values to interpolate into it." 29 | ) 30 | if not isinstance(self.argument, list) or len(self.argument) != 2: 31 | self.raise_invalid_argument_error(error_message) 32 | 33 | template, variables = self.argument 34 | if not isinstance(template, str) or not isinstance(variables, dict): 35 | self.raise_invalid_argument_error(error_message) 36 | 37 | try: 38 | return template.format(**variables) 39 | except KeyError as e: 40 | self.raise_invalid_argument_error( 41 | f"Could not find !sub argument for {e}", e 42 | ) 43 | -------------------------------------------------------------------------------- /sceptre/stack_policies/lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Deny", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /sceptre/stack_policies/unlock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Allow", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /sceptre/stack_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | sceptre.stack_status 5 | 6 | This module implemets structs for simplified Stack status and simplified 7 | ChangeSet status values. 8 | """ 9 | 10 | 11 | class StackStatus(object): 12 | """ 13 | StackStatus stores simplified Stack statuses. 14 | """ 15 | 16 | COMPLETE = "complete" 17 | FAILED = "failed" 18 | IN_PROGRESS = "in progress" 19 | PENDING = "pending" 20 | 21 | 22 | class StackChangeSetStatus(object): 23 | """ 24 | StackChangeSetStatus stores simplified ChangeSet statuses. 25 | """ 26 | 27 | PENDING = "pending" 28 | READY = "ready" 29 | DEFUNCT = "defunct" 30 | NO_CHANGES = "no changes" 31 | -------------------------------------------------------------------------------- /sceptre/stack_status_colourer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | sceptre.stack_status_colourer 5 | 6 | This module implements a StackStatusColourer class, colours any Stack Statuses 7 | found in a given string. 8 | """ 9 | 10 | import re 11 | from colorama import Fore, Style 12 | 13 | 14 | class StackStatusColourer(object): 15 | """ 16 | StackStatusColourer adds colours to stack statuses. 17 | These are documented here: 18 | https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html 19 | """ 20 | 21 | STACK_STATUS_CODES = { 22 | "CREATE_COMPLETE": Fore.GREEN, 23 | "CREATE_FAILED": Fore.RED, 24 | "CREATE_IN_PROGRESS": Fore.YELLOW, 25 | "DELETE_COMPLETE": Fore.GREEN, 26 | "DELETE_FAILED": Fore.RED, 27 | "DELETE_IN_PROGRESS": Fore.YELLOW, 28 | "DELETE_SKIPPED": Fore.CYAN, 29 | "IMPORT_COMPLETE": Fore.GREEN, 30 | "IMPORT_IN_PROGRESS": Fore.YELLOW, 31 | "IMPORT_ROLLBACK_COMPLETE": Fore.GREEN, 32 | "IMPORT_ROLLBACK_FAILED": Fore.RED, 33 | "IMPORT_ROLLBACK_IN_PROGRESS": Fore.YELLOW, 34 | "PENDING": Fore.CYAN, 35 | "REVIEW_IN_PROGRESS": Fore.YELLOW, 36 | "ROLLBACK_COMPLETE": Fore.RED, 37 | "ROLLBACK_FAILED": Fore.RED, 38 | "ROLLBACK_IN_PROGRESS": Fore.YELLOW, 39 | "UPDATE_COMPLETE": Fore.GREEN, 40 | "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS": Fore.YELLOW, 41 | "UPDATE_FAILED": Fore.RED, 42 | "UPDATE_IN_PROGRESS": Fore.YELLOW, 43 | "UPDATE_ROLLBACK_COMPLETE": Fore.GREEN, 44 | "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS": Fore.YELLOW, 45 | "UPDATE_ROLLBACK_FAILED": Fore.RED, 46 | "UPDATE_ROLLBACK_IN_PROGRESS": Fore.YELLOW, 47 | } 48 | 49 | STACK_STATUS_PATTERN = re.compile(r"\b({0})\b".format("|".join(STACK_STATUS_CODES))) 50 | 51 | def colour(self, string): 52 | """ 53 | Colours all Stack Statueses in ``string``. 54 | 55 | The colours applied are defined in 56 | ``sceptre.stack_status_colourer.StackStatusColourer.STACK_STATUS_CODES`` 57 | 58 | :param string: A string to colour. 59 | :type string: str 60 | :returns: The string with all stack status values coloured. 61 | :rtype: str 62 | """ 63 | stack_statuses = re.findall(self.STACK_STATUS_PATTERN, string) 64 | for status in stack_statuses: 65 | string = re.sub( 66 | r"\b{0}\b".format(status), 67 | "{0}{1}{2}".format( 68 | self.STACK_STATUS_CODES[status], status, Style.RESET_ALL 69 | ), 70 | string, 71 | ) 72 | return string 73 | -------------------------------------------------------------------------------- /sceptre/template_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import logging 4 | 5 | import six 6 | from jsonschema import validate, ValidationError 7 | 8 | from sceptre.exceptions import TemplateHandlerArgumentsInvalidError 9 | from sceptre.logging import StackLoggerAdapter 10 | 11 | 12 | @six.add_metaclass(abc.ABCMeta) 13 | class TemplateHandler: 14 | """ 15 | TemplateHandler is an abstract base class that should be inherited 16 | by all Template Handlers. 17 | 18 | :param name: Name of the template 19 | :type name: str 20 | 21 | :param arguments: The arguments of the template handler 22 | :type arguments: dict 23 | 24 | :param sceptre_user_data: Sceptre user data in stack config 25 | :type sceptre_user_data: dict 26 | 27 | :param connection_manager: Connection manager used to call AWS 28 | :type connection_manager: sceptre.connection_manager.ConnectionManager 29 | 30 | :param stack_group_config: The Stack group config to use as defaults. 31 | :type stack_group_config: dict 32 | """ 33 | 34 | __metaclass__ = abc.ABCMeta 35 | 36 | standard_template_extensions = [".json", ".yaml", ".template"] 37 | jinja_template_extensions = [".j2"] 38 | python_template_extensions = [".py"] 39 | supported_template_extensions = ( 40 | standard_template_extensions 41 | + jinja_template_extensions 42 | + python_template_extensions 43 | ) 44 | 45 | def __init__( 46 | self, 47 | name, 48 | arguments=None, 49 | sceptre_user_data=None, 50 | connection_manager=None, 51 | stack_group_config=None, 52 | ): 53 | self.logger = StackLoggerAdapter(logging.getLogger(__name__), name) 54 | self.name = name 55 | self.arguments = arguments 56 | self.sceptre_user_data = sceptre_user_data 57 | self.connection_manager = connection_manager 58 | 59 | if stack_group_config is None: 60 | stack_group_config = {} 61 | self.stack_group_config = stack_group_config 62 | 63 | @abc.abstractmethod 64 | def schema(self): 65 | """ 66 | Returns the schema for the arguments of this Template Resolver. This will 67 | be used to validate that the arguments passed in the stack config are what 68 | the Template Handler expects. 69 | :return: JSON schema that can be validated 70 | :rtype: object 71 | """ 72 | pass 73 | 74 | @abc.abstractmethod 75 | def handle(self): 76 | """ 77 | An abstract method which must be overwritten by all inheriting classes. 78 | This method is called to retrieve the template. 79 | Implementation of this method in subclasses must return a string that 80 | can be interpreted by Sceptre (CloudFormation YAML / JSON, Jinja or Python) 81 | """ 82 | pass # pragma: no cover 83 | 84 | def validate(self): 85 | """ 86 | Validates if the current arguments are correct according to the schema. If this 87 | does not raise an exception, the template handler's arguments are valid. 88 | """ 89 | try: 90 | validate(instance=self.arguments, schema=self.schema()) 91 | except ValidationError as e: 92 | raise TemplateHandlerArgumentsInvalidError(e) 93 | -------------------------------------------------------------------------------- /sceptre/template_handlers/file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sceptre.template_handlers.helper as helper 3 | 4 | from os import path 5 | from pathlib import Path 6 | 7 | from sceptre.exceptions import UnsupportedTemplateFileTypeError 8 | from sceptre.template_handlers import TemplateHandler 9 | from sceptre.helpers import normalise_path 10 | 11 | 12 | class File(TemplateHandler): 13 | """ 14 | Template handler that can load files from disk. Supports JSON, YAML, Jinja2 and Python. 15 | """ 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(File, self).__init__(*args, **kwargs) 19 | 20 | def schema(self): 21 | return { 22 | "type": "object", 23 | "properties": { 24 | "path": {"type": "string"}, 25 | }, 26 | "required": ["path"], 27 | } 28 | 29 | def handle(self): 30 | input_path = Path(self.arguments["path"]) 31 | path = self._resolve_template_path(str(input_path)) 32 | 33 | if input_path.suffix not in self.supported_template_extensions: 34 | raise UnsupportedTemplateFileTypeError( 35 | "Template has file extension %s. Only %s are supported.", 36 | input_path.suffix, 37 | ",".join(self.supported_template_extensions), 38 | ) 39 | 40 | try: 41 | if input_path.suffix in self.standard_template_extensions: 42 | with open(path) as template_file: 43 | return template_file.read() 44 | elif input_path.suffix in self.jinja_template_extensions: 45 | return helper.render_jinja_template( 46 | path, 47 | {"sceptre_user_data": self.sceptre_user_data}, 48 | self.stack_group_config.get("j2_environment", {}), 49 | ) 50 | elif input_path.suffix in self.python_template_extensions: 51 | return helper.call_sceptre_handler(path, self.sceptre_user_data) 52 | except Exception as e: 53 | helper.print_template_traceback(path) 54 | raise e 55 | 56 | def _resolve_template_path(self, template_path): 57 | """ 58 | Return the project_path joined to template_path as 59 | a string. 60 | 61 | Note that os.path.join defers to an absolute path 62 | if the input is absolute. 63 | """ 64 | return path.join( 65 | self.stack_group_config["project_path"], 66 | "templates", 67 | normalise_path(template_path), 68 | ) 69 | -------------------------------------------------------------------------------- /sponsors/cloudreach_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/sponsors/cloudreach_logo.png -------------------------------------------------------------------------------- /sponsors/godaddy_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/sponsors/godaddy_logo.png -------------------------------------------------------------------------------- /sponsors/sage_bionetworks_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/sponsors/sage_bionetworks_logo.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures-vpc/config/account/stack-group/config.yaml: -------------------------------------------------------------------------------- 1 | template_bucket_name: stack_group_template_bucket_name 2 | custom_key: "custom_value" 3 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/config/account/stack-group/region/config.yaml: -------------------------------------------------------------------------------- 1 | region: region_region 2 | dependencies: 3 | - top/level 4 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/config/account/stack-group/region/vpc.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: path/to/template 3 | parameters: 4 | param1: val1 5 | dependencies: 6 | - "child/level" 7 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/config/config.yaml: -------------------------------------------------------------------------------- 1 | profile: account_profile 2 | project_code: account_project_code 3 | region: account_region 4 | required_version: ">1.0" 5 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/config/top/level.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: somethingelse.py 3 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/hooks/custom_hook.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from sceptre.hooks import Hook 4 | 5 | 6 | class CustomHook(Hook): 7 | """ 8 | This is a test task. 9 | 10 | """ 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CustomHook, self).__init__(*args, **kwargs) 14 | 15 | def run(self): 16 | """ 17 | Prints a statement 18 | 19 | """ 20 | print("Custom task output") 21 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/resolvers/custom_resolver.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class CustomResolver(Resolver): 5 | def __init__(self, *args, **kwargs): 6 | super(CustomResolver, self).__init__(*args, **kwargs) 7 | 8 | def resolve(self): 9 | return "value" 10 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/stack_policies/lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Deny", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/stack_policies/unlock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Allow", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/compiled_vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Parameters": { 11 | "CidrBlock": { 12 | "Default": "10.0.0.0/16", 13 | "Type": "String" 14 | } 15 | }, 16 | "Resources": { 17 | "IGWAttachment": { 18 | "Properties": { 19 | "InternetGatewayId": { 20 | "Ref": "InternetGateway" 21 | }, 22 | "VpcId": { 23 | "Ref": "VirtualPrivateCloud" 24 | } 25 | }, 26 | "Type": "AWS::EC2::VPCGatewayAttachment" 27 | }, 28 | "InternetGateway": { 29 | "Type": "AWS::EC2::InternetGateway" 30 | }, 31 | "VirtualPrivateCloud": { 32 | "Properties": { 33 | "CidrBlock": { 34 | "Ref": "CidrBlock" 35 | }, 36 | "EnableDnsHostnames": true, 37 | "EnableDnsSupport": true, 38 | "InstanceTenancy": "default" 39 | }, 40 | "Type": "AWS::EC2::VPC" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/compiled_vpc_sud.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Resources": { 11 | "IGWAttachment": { 12 | "Properties": { 13 | "InternetGatewayId": { 14 | "Ref": "InternetGateway" 15 | }, 16 | "VpcId": { 17 | "Ref": "VirtualPrivateCloud" 18 | } 19 | }, 20 | "Type": "AWS::EC2::VPCGatewayAttachment" 21 | }, 22 | "InternetGateway": { 23 | "Type": "AWS::EC2::InternetGateway" 24 | }, 25 | "VirtualPrivateCloud": { 26 | "Properties": { 27 | "CidrBlock": "10.0.0.0/16", 28 | "EnableDnsHostnames": true, 29 | "EnableDnsSupport": true, 30 | "InstanceTenancy": "default" 31 | }, 32 | "Type": "AWS::EC2::VPC" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/sg.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | {% for sg in sceptre_user_data %} 3 | {{ sg.name }}: 4 | Type: AWS::EC2::SecurityGroup 5 | Properties: 6 | InboundIp: {{ sg.inbound_ip }} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | VPC: 3 | Type: AWS::EC2::VPC 4 | Properties: 5 | CidrBlock: {{ sceptre_user_data.vpc_id }} 6 | Outputs: 7 | VpcId: 8 | Value: 9 | Ref: VPC 10 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Parameters": { 11 | "CidrBlock": { 12 | "Default": "10.0.0.0/16", 13 | "Type": "String" 14 | } 15 | }, 16 | "Resources": { 17 | "IGWAttachment": { 18 | "Properties": { 19 | "InternetGatewayId": { 20 | "Ref": "InternetGateway" 21 | }, 22 | "VpcId": { 23 | "Ref": "VirtualPrivateCloud" 24 | } 25 | }, 26 | "Type": "AWS::EC2::VPCGatewayAttachment" 27 | }, 28 | "InternetGateway": { 29 | "Type": "AWS::EC2::InternetGateway" 30 | }, 31 | "VirtualPrivateCloud": { 32 | "Properties": { 33 | "CidrBlock": { 34 | "Ref": "CidrBlock" 35 | }, 36 | "EnableDnsHostnames": true, 37 | "EnableDnsSupport": true, 38 | "InstanceTenancy": "default" 39 | }, 40 | "Type": "AWS::EC2::VPC" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Parameter, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | 8 | def sceptre_handler(sceptre_user_data): 9 | t = Template() 10 | 11 | cidr_block_param = t.add_parameter( 12 | Parameter( 13 | "CidrBlock", 14 | Type="String", 15 | Default="10.0.0.0/16", 16 | ) 17 | ) 18 | 19 | vpc = t.add_resource( 20 | VPC( 21 | "VirtualPrivateCloud", 22 | CidrBlock=Ref(cidr_block_param), 23 | InstanceTenancy="default", 24 | EnableDnsSupport=True, 25 | EnableDnsHostnames=True, 26 | ) 27 | ) 28 | 29 | igw = t.add_resource( 30 | InternetGateway( 31 | "InternetGateway", 32 | ) 33 | ) 34 | 35 | t.add_resource( 36 | VPCGatewayAttachment( 37 | "IGWAttachment", 38 | VpcId=Ref(vpc), 39 | InternetGatewayId=Ref(igw), 40 | ) 41 | ) 42 | 43 | t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) 44 | return t.to_json() 45 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc.template: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Parameters": { 11 | "CidrBlock": { 12 | "Default": "10.0.0.0/16", 13 | "Type": "String" 14 | } 15 | }, 16 | "Resources": { 17 | "IGWAttachment": { 18 | "Properties": { 19 | "InternetGatewayId": { 20 | "Ref": "InternetGateway" 21 | }, 22 | "VpcId": { 23 | "Ref": "VirtualPrivateCloud" 24 | } 25 | }, 26 | "Type": "AWS::EC2::VPCGatewayAttachment" 27 | }, 28 | "InternetGateway": { 29 | "Type": "AWS::EC2::InternetGateway" 30 | }, 31 | "VirtualPrivateCloud": { 32 | "Properties": { 33 | "CidrBlock": { 34 | "Ref": "CidrBlock" 35 | }, 36 | "EnableDnsHostnames": true, 37 | "EnableDnsSupport": true, 38 | "InstanceTenancy": "default" 39 | }, 40 | "Type": "AWS::EC2::VPC" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | Outputs: 3 | VpcId: 4 | Description: New VPC ID 5 | Value: 6 | Ref: VirtualPrivateCloud 7 | Parameters: 8 | CidrBlock: 9 | Default: 10.0.0.0/16 10 | Type: String 11 | Resources: 12 | IGWAttachment: 13 | Properties: 14 | InternetGatewayId: 15 | Ref: InternetGateway 16 | VpcId: 17 | Ref: VirtualPrivateCloud 18 | Type: AWS::EC2::VPCGatewayAttachment 19 | InternetGateway: 20 | Type: AWS::EC2::InternetGateway 21 | VirtualPrivateCloud: 22 | Properties: 23 | CidrBlock: 24 | Ref: CidrBlock 25 | EnableDnsHostnames: 'true' 26 | EnableDnsSupport: 'true' 27 | InstanceTenancy: default 28 | Type: AWS::EC2::VPC 29 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc.yaml.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | VPC: 3 | Type: AWS::EC2::VPC 4 | Properties: 5 | CidrBlock: {{ sceptre_user_data.vpc_id }} 6 | Outputs: 7 | VpcId: 8 | Value: 9 | Ref: VPC 10 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc_sgt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Parameter, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | 8 | class VpcTemplate(object): 9 | def __init__(self): 10 | self.template = Template() 11 | 12 | self.add_parameters() 13 | 14 | self.add_vpc() 15 | self.add_igw() 16 | 17 | self.add_outputs() 18 | 19 | def add_parameters(self): 20 | t = self.template 21 | 22 | self.cidr_block_param = t.add_parameter( 23 | Parameter( 24 | "CidrBlock", 25 | Type="String", 26 | Default="10.0.0.0/16", 27 | ) 28 | ) 29 | 30 | def add_vpc(self): 31 | t = self.template 32 | 33 | self.vpc = t.add_resource( 34 | VPC( 35 | "VirtualPrivateCloud", 36 | CidrBlock=Ref(self.cidr_block_param), 37 | InstanceTenancy="default", 38 | EnableDnsSupport=True, 39 | EnableDnsHostnames=True, 40 | ) 41 | ) 42 | 43 | def add_igw(self): 44 | t = self.template 45 | 46 | self.igw = t.add_resource( 47 | InternetGateway( 48 | "InternetGateway", 49 | ) 50 | ) 51 | 52 | t.add_resource( 53 | VPCGatewayAttachment( 54 | "IGWAttachment", 55 | VpcId=Ref(self.vpc), 56 | InternetGatewayId=Ref(self.igw), 57 | ) 58 | ) 59 | 60 | def add_outputs(self): 61 | t = self.template 62 | 63 | t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(self.vpc))) 64 | 65 | 66 | def sceptre_handler(sceptre_user_data): 67 | vpc = VpcTemplate() 68 | return vpc.template.to_json() 69 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc_sud.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | 8 | def sceptre_handler(sceptre_user_data): 9 | t = Template() 10 | 11 | vpc = t.add_resource( 12 | VPC( 13 | "VirtualPrivateCloud", 14 | CidrBlock=sceptre_user_data["cidr_block"], 15 | InstanceTenancy="default", 16 | EnableDnsSupport=True, 17 | EnableDnsHostnames=True, 18 | ) 19 | ) 20 | 21 | igw = t.add_resource( 22 | InternetGateway( 23 | "InternetGateway", 24 | ) 25 | ) 26 | 27 | t.add_resource( 28 | VPCGatewayAttachment( 29 | "IGWAttachment", 30 | VpcId=Ref(vpc), 31 | InternetGatewayId=Ref(igw), 32 | ) 33 | ) 34 | 35 | t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) 36 | 37 | return t.to_json() 38 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc_sud_incorrect_function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def sceptre_incorrect_function(): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc_sud_incorrect_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def sceptre_handler(): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/fixtures-vpc/templates/vpc_t.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Parameter, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | t = Template() 8 | 9 | cidr_block_param = t.add_parameter( 10 | Parameter( 11 | "CidrBlock", 12 | Type="String", 13 | Default="10.0.0.0/16", 14 | ) 15 | ) 16 | 17 | vpc = t.add_resource( 18 | VPC( 19 | "VirtualPrivateCloud", 20 | CidrBlock=Ref(cidr_block_param), 21 | InstanceTenancy="default", 22 | EnableDnsSupport=True, 23 | EnableDnsHostnames=True, 24 | ) 25 | ) 26 | 27 | igw = t.add_resource( 28 | InternetGateway( 29 | "InternetGateway", 30 | ) 31 | ) 32 | 33 | igw_attachment = t.add_resource( 34 | VPCGatewayAttachment( 35 | "IGWAttachment", 36 | VpcId=Ref(vpc), 37 | InternetGatewayId=Ref(igw), 38 | ) 39 | ) 40 | 41 | vpc_id_output = t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) 42 | -------------------------------------------------------------------------------- /tests/fixtures/config/account/stack-group/config.yaml: -------------------------------------------------------------------------------- 1 | template_bucket_name: stack_group_template_bucket_name 2 | required_version: ">1.0" 3 | -------------------------------------------------------------------------------- /tests/fixtures/config/account/stack-group/region/config.yaml: -------------------------------------------------------------------------------- 1 | region: region_region 2 | required_version: ">1.0" 3 | dependencies: 4 | - top/level 5 | -------------------------------------------------------------------------------- /tests/fixtures/config/account/stack-group/region/construct_nodes.yaml: -------------------------------------------------------------------------------- 1 | template: 2 | path: path/to/template 3 | parameters: 4 | param1: !environment_variable example 5 | param2: 6 | param3: !environment_variable example 7 | param4: !environment_variable example 8 | param5: 9 | - param6: !environment_variable example 10 | - param7: !environment_variable example 11 | -------------------------------------------------------------------------------- /tests/fixtures/config/account/stack-group/region/security_groups.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | param1: {{ var.variable_key }} 3 | param2: {{ environment_variable.TEST_ENV_VAR }} 4 | param3: {{ stack_group_config.region }} 5 | param4: {{ stack_group_config.project_code }} 6 | param5: {{ stack_group_config.required_version }} 7 | param6: {{ stack_group_config.template_bucket_name }} 8 | -------------------------------------------------------------------------------- /tests/fixtures/config/account/stack-group/region/subnets.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/tests/fixtures/config/account/stack-group/region/subnets.yaml -------------------------------------------------------------------------------- /tests/fixtures/config/account/stack-group/region/vpc.yaml: -------------------------------------------------------------------------------- 1 | template_path: path/to/template 2 | parameters: 3 | param1: val1 4 | dependencies: 5 | - "child/level" 6 | -------------------------------------------------------------------------------- /tests/fixtures/config/config.yaml: -------------------------------------------------------------------------------- 1 | profile: account_profile 2 | project_code: account_project_code 3 | region: account_region 4 | required_version: ">1.0" 5 | -------------------------------------------------------------------------------- /tests/fixtures/config/top/level.yaml: -------------------------------------------------------------------------------- 1 | template_path: vpc.py 2 | -------------------------------------------------------------------------------- /tests/fixtures/hooks/custom_hook.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from sceptre.hooks import Hook 4 | 5 | 6 | class CustomHook(Hook): 7 | """ 8 | This is a test task. 9 | 10 | """ 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(CustomHook, self).__init__(*args, **kwargs) 14 | 15 | def run(self): 16 | """ 17 | Prints a statement 18 | 19 | """ 20 | print("Custom task output") 21 | -------------------------------------------------------------------------------- /tests/fixtures/resolvers/custom_resolver.py: -------------------------------------------------------------------------------- 1 | from sceptre.resolvers import Resolver 2 | 3 | 4 | class CustomResolver(Resolver): 5 | def __init__(self, *args, **kwargs): 6 | super(CustomResolver, self).__init__(*args, **kwargs) 7 | 8 | def resolve(self): 9 | return "value" 10 | -------------------------------------------------------------------------------- /tests/fixtures/stack_policies/lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Deny", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/stack_policies/unlock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Allow", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/templates/chdir.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template 4 | 5 | from os import chdir, getcwd 6 | 7 | 8 | def sceptre_handler(sceptre_user_data): 9 | t = Template() 10 | 11 | curr_dir = getcwd() 12 | chdir("..") 13 | chdir(curr_dir) 14 | 15 | return t.to_json() 16 | -------------------------------------------------------------------------------- /tests/fixtures/templates/compiled_vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Parameters": { 11 | "CidrBlock": { 12 | "Default": "10.0.0.0/16", 13 | "Type": "String" 14 | } 15 | }, 16 | "Resources": { 17 | "IGWAttachment": { 18 | "Properties": { 19 | "InternetGatewayId": { 20 | "Ref": "InternetGateway" 21 | }, 22 | "VpcId": { 23 | "Ref": "VirtualPrivateCloud" 24 | } 25 | }, 26 | "Type": "AWS::EC2::VPCGatewayAttachment" 27 | }, 28 | "InternetGateway": { 29 | "Type": "AWS::EC2::InternetGateway" 30 | }, 31 | "VirtualPrivateCloud": { 32 | "Properties": { 33 | "CidrBlock": { 34 | "Ref": "CidrBlock" 35 | }, 36 | "EnableDnsHostnames": true, 37 | "EnableDnsSupport": true, 38 | "InstanceTenancy": "default" 39 | }, 40 | "Type": "AWS::EC2::VPC" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures/templates/compiled_vpc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | VPC: 4 | Type: AWS::EC2::VPC 5 | Properties: 6 | CidrBlock: 10.0.0.0/16 7 | Outputs: 8 | VpcId: 9 | Value: 10 | Ref: VPC 11 | -------------------------------------------------------------------------------- /tests/fixtures/templates/compiled_vpc_sud.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Resources": { 11 | "IGWAttachment": { 12 | "Properties": { 13 | "InternetGatewayId": { 14 | "Ref": "InternetGateway" 15 | }, 16 | "VpcId": { 17 | "Ref": "VirtualPrivateCloud" 18 | } 19 | }, 20 | "Type": "AWS::EC2::VPCGatewayAttachment" 21 | }, 22 | "InternetGateway": { 23 | "Type": "AWS::EC2::InternetGateway" 24 | }, 25 | "VirtualPrivateCloud": { 26 | "Properties": { 27 | "CidrBlock": "10.0.0.0/16", 28 | "EnableDnsHostnames": true, 29 | "EnableDnsSupport": true, 30 | "InstanceTenancy": "default" 31 | }, 32 | "Type": "AWS::EC2::VPC" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/fixtures/templates/sg.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | {% for sg in sceptre_user_data %} 3 | {{ sg.name }}: 4 | Type: AWS::EC2::SecurityGroup 5 | Properties: 6 | InboundIp: {{ sg.inbound_ip }} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | VPC: 3 | Type: AWS::EC2::VPC 4 | Properties: 5 | CidrBlock: {{ sceptre_user_data.vpc_id }} 6 | Outputs: 7 | VpcId: 8 | Value: 9 | Ref: VPC 10 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Parameter, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | 8 | def sceptre_handler(sceptre_user_data): 9 | t = Template() 10 | 11 | cidr_block_param = t.add_parameter( 12 | Parameter( 13 | "CidrBlock", 14 | Type="String", 15 | Default="10.0.0.0/16", 16 | ) 17 | ) 18 | 19 | vpc = t.add_resource( 20 | VPC( 21 | "VirtualPrivateCloud", 22 | CidrBlock=Ref(cidr_block_param), 23 | InstanceTenancy="default", 24 | EnableDnsSupport=True, 25 | EnableDnsHostnames=True, 26 | ) 27 | ) 28 | 29 | igw = t.add_resource( 30 | InternetGateway( 31 | "InternetGateway", 32 | ) 33 | ) 34 | 35 | t.add_resource( 36 | VPCGatewayAttachment( 37 | "IGWAttachment", 38 | VpcId=Ref(vpc), 39 | InternetGatewayId=Ref(igw), 40 | ) 41 | ) 42 | 43 | t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) 44 | return t.to_json() 45 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc.template: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "VpcId": { 4 | "Description": "New VPC ID", 5 | "Value": { 6 | "Ref": "VirtualPrivateCloud" 7 | } 8 | } 9 | }, 10 | "Parameters": { 11 | "CidrBlock": { 12 | "Default": "10.0.0.0/16", 13 | "Type": "String" 14 | } 15 | }, 16 | "Resources": { 17 | "IGWAttachment": { 18 | "Properties": { 19 | "InternetGatewayId": { 20 | "Ref": "InternetGateway" 21 | }, 22 | "VpcId": { 23 | "Ref": "VirtualPrivateCloud" 24 | } 25 | }, 26 | "Type": "AWS::EC2::VPCGatewayAttachment" 27 | }, 28 | "InternetGateway": { 29 | "Type": "AWS::EC2::InternetGateway" 30 | }, 31 | "VirtualPrivateCloud": { 32 | "Properties": { 33 | "CidrBlock": { 34 | "Ref": "CidrBlock" 35 | }, 36 | "EnableDnsHostnames": true, 37 | "EnableDnsSupport": true, 38 | "InstanceTenancy": "default" 39 | }, 40 | "Type": "AWS::EC2::VPC" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc.without_start_marker.yaml: -------------------------------------------------------------------------------- 1 | Outputs: 2 | VpcId: 3 | Description: New VPC ID 4 | Value: 5 | Ref: VirtualPrivateCloud 6 | Parameters: 7 | CidrBlock: 8 | Default: 10.0.0.0/16 9 | Type: String 10 | Resources: 11 | IGWAttachment: 12 | Properties: 13 | InternetGatewayId: 14 | Ref: InternetGateway 15 | VpcId: 16 | Ref: VirtualPrivateCloud 17 | Type: AWS::EC2::VPCGatewayAttachment 18 | InternetGateway: 19 | Type: AWS::EC2::InternetGateway 20 | VirtualPrivateCloud: 21 | Properties: 22 | CidrBlock: 23 | Ref: CidrBlock 24 | EnableDnsHostnames: true 25 | EnableDnsSupport: true 26 | InstanceTenancy: default 27 | Type: AWS::EC2::VPC 28 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | Outputs: 3 | VpcId: 4 | Description: New VPC ID 5 | Value: 6 | Ref: VirtualPrivateCloud 7 | Parameters: 8 | CidrBlock: 9 | Default: 10.0.0.0/16 10 | Type: String 11 | Resources: 12 | IGWAttachment: 13 | Properties: 14 | InternetGatewayId: 15 | Ref: InternetGateway 16 | VpcId: 17 | Ref: VirtualPrivateCloud 18 | Type: AWS::EC2::VPCGatewayAttachment 19 | InternetGateway: 20 | Type: AWS::EC2::InternetGateway 21 | VirtualPrivateCloud: 22 | Properties: 23 | CidrBlock: 24 | Ref: CidrBlock 25 | EnableDnsHostnames: true 26 | EnableDnsSupport: true 27 | InstanceTenancy: default 28 | Type: AWS::EC2::VPC 29 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc.yaml.j2: -------------------------------------------------------------------------------- 1 | Resources: 2 | VPC: 3 | Type: AWS::EC2::VPC 4 | Properties: 5 | CidrBlock: {{ sceptre_user_data.vpc_id }} 6 | Outputs: 7 | VpcId: 8 | Value: 9 | Ref: VPC 10 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc_sgt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Parameter, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | 8 | class VpcTemplate(object): 9 | def __init__(self): 10 | self.template = Template() 11 | 12 | self.add_parameters() 13 | 14 | self.add_vpc() 15 | self.add_igw() 16 | 17 | self.add_outputs() 18 | 19 | def add_parameters(self): 20 | t = self.template 21 | 22 | self.cidr_block_param = t.add_parameter( 23 | Parameter( 24 | "CidrBlock", 25 | Type="String", 26 | Default="10.0.0.0/16", 27 | ) 28 | ) 29 | 30 | def add_vpc(self): 31 | t = self.template 32 | 33 | self.vpc = t.add_resource( 34 | VPC( 35 | "VirtualPrivateCloud", 36 | CidrBlock=Ref(self.cidr_block_param), 37 | InstanceTenancy="default", 38 | EnableDnsSupport=True, 39 | EnableDnsHostnames=True, 40 | ) 41 | ) 42 | 43 | def add_igw(self): 44 | t = self.template 45 | 46 | self.igw = t.add_resource( 47 | InternetGateway( 48 | "InternetGateway", 49 | ) 50 | ) 51 | 52 | t.add_resource( 53 | VPCGatewayAttachment( 54 | "IGWAttachment", 55 | VpcId=Ref(self.vpc), 56 | InternetGatewayId=Ref(self.igw), 57 | ) 58 | ) 59 | 60 | def add_outputs(self): 61 | t = self.template 62 | 63 | t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(self.vpc))) 64 | 65 | 66 | def sceptre_handler(sceptre_user_data): 67 | vpc = VpcTemplate() 68 | return vpc.template.to_json() 69 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc_sud.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | 8 | def sceptre_handler(sceptre_user_data): 9 | t = Template() 10 | 11 | vpc = t.add_resource( 12 | VPC( 13 | "VirtualPrivateCloud", 14 | CidrBlock=sceptre_user_data["cidr_block"], 15 | InstanceTenancy="default", 16 | EnableDnsSupport=True, 17 | EnableDnsHostnames=True, 18 | ) 19 | ) 20 | 21 | igw = t.add_resource( 22 | InternetGateway( 23 | "InternetGateway", 24 | ) 25 | ) 26 | 27 | t.add_resource( 28 | VPCGatewayAttachment( 29 | "IGWAttachment", 30 | VpcId=Ref(vpc), 31 | InternetGatewayId=Ref(igw), 32 | ) 33 | ) 34 | 35 | t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) 36 | 37 | return t.to_json() 38 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc_sud_incorrect_function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def sceptre_incorrect_function(): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc_sud_incorrect_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def sceptre_handler(): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/fixtures/templates/vpc_t.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from troposphere import Template, Parameter, Ref, Output 4 | 5 | from troposphere.ec2 import VPC, InternetGateway, VPCGatewayAttachment 6 | 7 | t = Template() 8 | 9 | cidr_block_param = t.add_parameter( 10 | Parameter( 11 | "CidrBlock", 12 | Type="String", 13 | Default="10.0.0.0/16", 14 | ) 15 | ) 16 | 17 | vpc = t.add_resource( 18 | VPC( 19 | "VirtualPrivateCloud", 20 | CidrBlock=Ref(cidr_block_param), 21 | InstanceTenancy="default", 22 | EnableDnsSupport=True, 23 | EnableDnsHostnames=True, 24 | ) 25 | ) 26 | 27 | igw = t.add_resource( 28 | InternetGateway( 29 | "InternetGateway", 30 | ) 31 | ) 32 | 33 | igw_attachment = t.add_resource( 34 | VPCGatewayAttachment( 35 | "IGWAttachment", 36 | VpcId=Ref(vpc), 37 | InternetGatewayId=Ref(igw), 38 | ) 39 | ) 40 | 41 | vpc_id_output = t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) 42 | -------------------------------------------------------------------------------- /tests/test_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/tests/test_cli/__init__.py -------------------------------------------------------------------------------- /tests/test_diffing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/tests/test_diffing/__init__.py -------------------------------------------------------------------------------- /tests/test_hooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sceptre/sceptre/69a8a5a648fb91bbda2c0e88881cf94102ccc32f/tests/test_hooks/__init__.py -------------------------------------------------------------------------------- /tests/test_plan.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import MagicMock, patch, sentinel 3 | 4 | from sceptre.context import SceptreContext 5 | from sceptre.stack import Stack 6 | from sceptre.config.reader import ConfigReader 7 | from sceptre.plan.plan import SceptrePlan 8 | 9 | 10 | class TestSceptrePlan(object): 11 | def setup_method(self, test_method): 12 | self.patcher_SceptrePlan = patch("sceptre.plan.plan.SceptrePlan") 13 | self.stack = Stack( 14 | name="dev/app/stack", 15 | project_code=sentinel.project_code, 16 | template_handler_config={"path": "/path/to/thing"}, 17 | region=sentinel.region, 18 | profile=sentinel.profile, 19 | parameters={"key1": "val1"}, 20 | sceptre_user_data=sentinel.sceptre_user_data, 21 | hooks={}, 22 | s3_details=None, 23 | dependencies=sentinel.dependencies, 24 | cloudformation_service_role=sentinel.cloudformation_service_role, 25 | protected=False, 26 | tags={"tag1": "val1"}, 27 | external_name=sentinel.external_name, 28 | notifications=[sentinel.notification], 29 | on_failure=sentinel.on_failure, 30 | stack_timeout=sentinel.stack_timeout, 31 | ) 32 | self.mock_context = MagicMock(spec=SceptreContext) 33 | self.mock_config_reader = MagicMock(spec=ConfigReader) 34 | self.mock_context.project_path = sentinel.project_path 35 | self.mock_context.command_path = sentinel.command_path 36 | self.mock_context.config_file = sentinel.config_file 37 | self.mock_context.full_config_path.return_value = sentinel.full_config_path 38 | self.mock_context.user_variables = {} 39 | self.mock_context.options = {} 40 | self.mock_context.no_colour = True 41 | self.mock_config_reader.context = self.mock_context 42 | 43 | def test_planner_executes_without_params(self): 44 | plan = MagicMock(spec=SceptrePlan) 45 | plan.context = self.mock_context 46 | plan.launch.return_value = sentinel.success 47 | result = plan.launch() 48 | plan.launch.assert_called_once_with() 49 | assert result == sentinel.success 50 | 51 | def test_planner_executes_with_params(self): 52 | plan = MagicMock(spec=SceptrePlan) 53 | plan.context = self.mock_context 54 | plan.launch.return_value = sentinel.success 55 | result = plan.launch("test-attribute") 56 | plan.launch.assert_called_once_with("test-attribute") 57 | assert result == sentinel.success 58 | 59 | def test_command_not_found_error_raised(self): 60 | with pytest.raises(AttributeError): 61 | plan = MagicMock(spec=SceptrePlan) 62 | plan.context = self.mock_context 63 | plan.invalid_command() 64 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_environment_variable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from unittest.mock import patch 4 | 5 | from sceptre.resolvers.environment_variable import EnvironmentVariable 6 | 7 | 8 | class TestEnvironmentVariableResolver(object): 9 | def setup_method(self, test_method): 10 | self.environment_variable_resolver = EnvironmentVariable(argument=None) 11 | 12 | @patch("sceptre.resolvers.environment_variable.os") 13 | def test_resolving_with_set_environment_variable(self, mock_os): 14 | mock_os.environ = {"VARIABLE": "value"} 15 | self.environment_variable_resolver.argument = "VARIABLE" 16 | response = self.environment_variable_resolver.resolve() 17 | assert response == "value" 18 | 19 | def test_resolving_with_unset_environment_variable(self): 20 | self.environment_variable_resolver.argument = "UNSETVARIABLE" 21 | response = self.environment_variable_resolver.resolve() 22 | assert response is None 23 | 24 | def test_resolving_with_environment_variable_name_as_none(self): 25 | self.environment_variable_resolver.argument = None 26 | response = self.environment_variable_resolver.resolve() 27 | assert response is None 28 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_file_contents.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import tempfile 4 | import pytest 5 | 6 | from sceptre.resolvers.file_contents import FileContents 7 | 8 | 9 | class TestFileContentsResolver(object): 10 | def setup_method(self, test_method): 11 | self.file_contents_resolver = FileContents(argument=None) 12 | 13 | def test_resolving_with_existing_file(self): 14 | with tempfile.NamedTemporaryFile(mode="w+") as f: 15 | f.write("file contents") 16 | f.seek(0) 17 | self.file_contents_resolver.argument = f.name 18 | result = self.file_contents_resolver.resolve() 19 | 20 | assert result == "file contents" 21 | 22 | def test_resolving_with_non_existant_file(self): 23 | with pytest.raises(IOError): 24 | self.file_contents_resolver.argument = "/non_existant_file" 25 | self.file_contents_resolver.resolve() 26 | 27 | def test_resolving_with_file_path_non_string_type(self): 28 | with pytest.raises(TypeError): 29 | self.file_contents_resolver.argument = None 30 | self.file_contents_resolver.resolve() 31 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_join.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from sceptre.exceptions import InvalidResolverArgumentError 6 | from sceptre.resolvers import Resolver 7 | from sceptre.resolvers.join import Join 8 | 9 | 10 | class ArgResolver(Resolver): 11 | def resolve(self): 12 | return self.argument 13 | 14 | 15 | class TestJoin: 16 | def test_resolve__joins_resolver_values_into_single_string(self): 17 | argument = [ 18 | "/", 19 | [ 20 | ArgResolver("first"), 21 | "middle", 22 | ArgResolver("last"), 23 | ], 24 | ] 25 | join = Join(argument, Mock()) 26 | resolved = join.resolve() 27 | expected = "first/middle/last" 28 | assert expected == resolved 29 | 30 | def test_resolve__argument_returns_non_string__casts_it_to_string(self): 31 | argument = [ 32 | "/", 33 | [ 34 | ArgResolver(123), 35 | "other", 36 | ], 37 | ] 38 | join = Join(argument, Mock()) 39 | resolved = join.resolve() 40 | expected = "123/other" 41 | assert expected == resolved 42 | 43 | def test_resolve__argument_returns_none__not_included_in_string(self): 44 | argument = [ 45 | "/", 46 | [ 47 | ArgResolver("first"), 48 | ArgResolver(None), 49 | ArgResolver("last"), 50 | ], 51 | ] 52 | join = Join(argument, Mock()) 53 | resolved = join.resolve() 54 | expected = "first/last" 55 | assert expected == resolved 56 | 57 | @pytest.mark.parametrize( 58 | "bad_argument", 59 | [ 60 | pytest.param("just a string", id="just a string"), 61 | pytest.param([123, ["something"]], id="non-string delimiter"), 62 | pytest.param( 63 | ["join", "not list to join"], id="second argument is not list" 64 | ), 65 | pytest.param(["first", ["second"], "third"], id="too many items"), 66 | ], 67 | ) 68 | def test_resolve__invalid_arguments_passed__raises_invalid_resolver_argument_error( 69 | self, bad_argument 70 | ): 71 | join = Join(bad_argument, Mock()) 72 | with pytest.raises(InvalidResolverArgumentError): 73 | join.resolve() 74 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_select.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from sceptre.exceptions import InvalidResolverArgumentError 6 | from sceptre.resolvers import Resolver 7 | from sceptre.resolvers.select import Select 8 | 9 | 10 | class MyListResolver(Resolver): 11 | def resolve(self): 12 | return ["first", "second", "third"] 13 | 14 | 15 | class ItemResolver(Resolver): 16 | def resolve(self): 17 | return self.argument 18 | 19 | 20 | class TestSelect: 21 | def test_resolve__second_arg_is_list_resolver__selects_item_at_list_index(self): 22 | argument = [1, MyListResolver()] 23 | select = Select(argument, Mock()) 24 | resolved = select.resolve() 25 | expected = "second" 26 | assert expected == resolved 27 | 28 | def test_resolve__second_arg_is_list_of_resolvers__selects_item_at_list_index(self): 29 | argument = [ 30 | 1, 31 | [ItemResolver("first"), ItemResolver("second"), ItemResolver("third")], 32 | ] 33 | select = Select(argument, Mock()) 34 | resolved = select.resolve() 35 | expected = "second" 36 | assert expected == resolved 37 | 38 | def test_resolve__negative_index__selects_in_reverse(self): 39 | argument = [-1, MyListResolver()] 40 | select = Select(argument, Mock()) 41 | resolved = select.resolve() 42 | expected = "third" 43 | assert expected == resolved 44 | 45 | def test_resolve__can_select_key_from_dict(self): 46 | argument = ["something", ItemResolver({"something": 123})] 47 | select = Select(argument, Mock()) 48 | resolved = select.resolve() 49 | expected = 123 50 | assert expected == resolved 51 | 52 | @pytest.mark.parametrize( 53 | "bad_argument", 54 | [ 55 | pytest.param("just a string", id="just a string"), 56 | pytest.param([123, "something"], id="second item is not list or dict"), 57 | pytest.param([99, [1, 2]], id="index out of bounds"), 58 | pytest.param(["hello", [1, 2]], id="string index on list"), 59 | pytest.param(["hello", {"something": "else"}], id="key not present"), 60 | pytest.param(["first", ["second"], "third"], id="too many items"), 61 | ], 62 | ) 63 | def test_resolve__invalid_arguments__raises_invalid_resolver_argument_error( 64 | self, bad_argument 65 | ): 66 | select = Select(bad_argument, Mock()) 67 | with pytest.raises(InvalidResolverArgumentError): 68 | select.resolve() 69 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_split.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from sceptre.exceptions import InvalidResolverArgumentError 6 | from sceptre.resolvers import Resolver 7 | from sceptre.resolvers.split import Split 8 | 9 | 10 | class MyResolver(Resolver): 11 | def resolve(self): 12 | return "first,second,third" 13 | 14 | 15 | class TestSplit: 16 | def test_resolve__splits_resolver_value_into_list(self): 17 | argument = [",", MyResolver()] 18 | split = Split(argument, Mock()) 19 | resolved = split.resolve() 20 | expected = ["first", "second", "third"] 21 | assert expected == resolved 22 | 23 | @pytest.mark.parametrize( 24 | "bad_argument", 25 | [ 26 | pytest.param("just a string", id="just a string"), 27 | pytest.param([123, "something"], id="first item is not string"), 28 | pytest.param(["something", 123], id="second item is not string"), 29 | ], 30 | ) 31 | def test_resolve__invalid_arguments__raises_invalid_resolver_argument_error( 32 | self, bad_argument 33 | ): 34 | split = Split(bad_argument, Mock()) 35 | with pytest.raises(InvalidResolverArgumentError): 36 | split.resolve() 37 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_stack_attr.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from sceptre.resolvers.stack_attr import StackAttr 6 | from sceptre.stack import Stack 7 | 8 | 9 | class TestResolver(object): 10 | def setup_method(self, test_method): 11 | self.stack_group_config = {} 12 | self.stack = Mock(spec=Stack, stack_group_config=self.stack_group_config) 13 | self.stack.name = "my/stack.yaml" 14 | 15 | self.resolver = StackAttr(stack=self.stack) 16 | 17 | def test__resolve__returns_attribute_off_stack(self): 18 | self.resolver.argument = "testing_this" 19 | self.stack.testing_this = "hurray!" 20 | result = self.resolver.resolve() 21 | 22 | assert result == "hurray!" 23 | 24 | def test_resolve__nested_attribute__accesses_nested_value(self): 25 | self.stack.testing_this = {"top": [{"thing": "first"}, {"thing": "second"}]} 26 | 27 | self.resolver.argument = "testing_this.top.1.thing" 28 | result = self.resolver.resolve() 29 | 30 | assert result == "second" 31 | 32 | def test_resolve__attribute_not_defined__accesses_it_off_stack_group_config(self): 33 | self.stack.stack_group_config["testing_this"] = { 34 | "top": [{"thing": "first"}, {"thing": "second"}] 35 | } 36 | 37 | self.resolver.argument = "testing_this.top.1.thing" 38 | result = self.resolver.resolve() 39 | 40 | assert result == "second" 41 | 42 | @pytest.mark.parametrize( 43 | "config,attr_name", 44 | [ 45 | ("template", "template_handler_config"), 46 | ("protect", "protected"), 47 | ("stack_name", "external_name"), 48 | ("stack_tags", "tags"), 49 | ], 50 | ) 51 | def test_resolve__accessing_attribute_renamed_on_stack__resolves_correct_value( 52 | self, config, attr_name 53 | ): 54 | setattr(self.stack, attr_name, "value") 55 | self.resolver.argument = config 56 | 57 | result = self.resolver.resolve() 58 | 59 | assert result == "value" 60 | 61 | def test_resolve__attribute_not_defined__raises_attribute_error(self): 62 | self.resolver.argument = "nonexistant" 63 | 64 | with pytest.raises(AttributeError): 65 | self.resolver.resolve() 66 | -------------------------------------------------------------------------------- /tests/test_resolvers/test_sub.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from sceptre.exceptions import InvalidResolverArgumentError 6 | from sceptre.resolvers import Resolver 7 | from sceptre.resolvers.sub import Sub 8 | 9 | 10 | class FirstResolver(Resolver): 11 | def resolve(self): 12 | return "first" 13 | 14 | 15 | class SecondResolver(Resolver): 16 | def resolve(self): 17 | return "second" 18 | 19 | 20 | class TestSub: 21 | def test_resolve__combines_resolvers_into_single_string(self): 22 | argument = [ 23 | "{first} is {first_value}; {second} is {second_value}", 24 | { 25 | "first": FirstResolver(), 26 | "second": SecondResolver(), 27 | "first_value": 123, 28 | "second_value": 456, 29 | }, 30 | ] 31 | sub = Sub(argument, Mock()) 32 | resolved = sub.resolve() 33 | expected = "first is 123; second is 456" 34 | assert expected == resolved 35 | 36 | @pytest.mark.parametrize( 37 | "bad_argument", 38 | [ 39 | pytest.param("just a string", id="just a string"), 40 | pytest.param([123, {"something": "else"}], id="first item is not string"), 41 | pytest.param(["123", "hello"], id="second item is not a dict"), 42 | pytest.param( 43 | ["{this}", {"that": "hi"}], id="format string requires key not in dict" 44 | ), 45 | pytest.param(["first", ["second"], "third"], id="too many items"), 46 | ], 47 | ) 48 | def test_resolve__invalid_arguments__raises_invalid_resolver_argument_error( 49 | self, bad_argument 50 | ): 51 | sub = Sub(bad_argument, Mock()) 52 | with pytest.raises(InvalidResolverArgumentError): 53 | sub.resolve() 54 | -------------------------------------------------------------------------------- /tests/test_stack_status_colourer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from colorama import init, Fore, Style 4 | from sceptre.stack_status_colourer import StackStatusColourer 5 | 6 | 7 | class TestStackStatusColourer(object): 8 | def setup_method(self, test_method): 9 | init() 10 | self.stack_status_colourer = StackStatusColourer() 11 | self.statuses = { 12 | "CREATE_COMPLETE": Fore.GREEN, 13 | "CREATE_FAILED": Fore.RED, 14 | "CREATE_IN_PROGRESS": Fore.YELLOW, 15 | "DELETE_COMPLETE": Fore.GREEN, 16 | "DELETE_FAILED": Fore.RED, 17 | "DELETE_IN_PROGRESS": Fore.YELLOW, 18 | "ROLLBACK_COMPLETE": Fore.RED, 19 | "ROLLBACK_FAILED": Fore.RED, 20 | "ROLLBACK_IN_PROGRESS": Fore.YELLOW, 21 | "UPDATE_COMPLETE": Fore.GREEN, 22 | "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS": Fore.YELLOW, 23 | "UPDATE_FAILED": Fore.RED, 24 | "UPDATE_IN_PROGRESS": Fore.YELLOW, 25 | "UPDATE_ROLLBACK_COMPLETE": Fore.GREEN, 26 | "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS": Fore.YELLOW, 27 | "UPDATE_ROLLBACK_FAILED": Fore.RED, 28 | "UPDATE_ROLLBACK_IN_PROGRESS": Fore.YELLOW, 29 | } 30 | 31 | def test_colour_with_string_with_no_stack_statuses(self): 32 | response = self.stack_status_colourer.colour("string with no statuses") 33 | assert response == "string with no statuses" 34 | 35 | def test_colour_with_string_with_single_stack_status(self): 36 | strings = [ 37 | "string string {0} string".format(status) 38 | for status in sorted(self.statuses.keys()) 39 | ] 40 | 41 | responses = [self.stack_status_colourer.colour(string) for string in strings] 42 | 43 | assert responses == [ 44 | "string string {0}{1}{2} string".format( 45 | self.statuses[status], status, Style.RESET_ALL 46 | ) 47 | for status in sorted(self.statuses.keys()) 48 | ] 49 | 50 | def test_colour_with_string_with_multiple_stack_statuses(self): 51 | response = self.stack_status_colourer.colour( 52 | " ".join(sorted(self.statuses.keys())) 53 | ) 54 | assert response == " ".join( 55 | [ 56 | "{0}{1}{2}".format(self.statuses[status], status, Style.RESET_ALL) 57 | for status in sorted(self.statuses.keys()) 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /tests/test_template_handlers/test_template_handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest import TestCase 3 | 4 | import pytest 5 | 6 | from sceptre.exceptions import TemplateHandlerArgumentsInvalidError 7 | from sceptre.template_handlers import TemplateHandler 8 | 9 | 10 | class MockTemplateHandler(TemplateHandler): 11 | def __init__(self, *args, **kwargs): 12 | super(MockTemplateHandler, self).__init__(*args, **kwargs) 13 | 14 | def schema(self): 15 | return { 16 | "type": "object", 17 | "properties": {"argument": {"type": "string"}}, 18 | "required": ["argument"], 19 | } 20 | 21 | def handle(self): 22 | return "TestTemplateHandler" 23 | 24 | 25 | class TestTemplateHandlers(TestCase): 26 | def test_template_handler_validates_schema(self): 27 | handler = MockTemplateHandler(name="mock", arguments={"argument": "test"}) 28 | handler.validate() 29 | 30 | def test_template_handler_errors_when_arguments_invalid(self): 31 | with pytest.raises(TemplateHandlerArgumentsInvalidError): 32 | handler = MockTemplateHandler( 33 | name="mock", arguments={"non-existent": "test"} 34 | ) 35 | handler.validate() 36 | 37 | def test_logger__logs_have_stack_name_prefix(self): 38 | template_handler = MockTemplateHandler( 39 | name="mock", arguments={"argument": "test"} 40 | ) 41 | with self.assertLogs(template_handler.logger.name, logging.INFO) as handler: 42 | template_handler.logger.info("Bonjour") 43 | 44 | assert handler.records[0].message == f"{template_handler.name} - Bonjour" 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | envlist = py{39,310,311,312} 4 | skip_missing_interpreters = true 5 | 6 | # set for poetry to manages dependencies in tox testenv targets 7 | # (https://python-poetry.org/docs/faq/#usecase-3) 8 | 9 | [testenv] 10 | skip_install = true 11 | allowlist_externals = poetry 12 | commands_pre = 13 | poetry install --all-extras -v 14 | commands = 15 | poetry run pytest {posargs: --cov=sceptre --cov-append --cov-report=term-missing --junitxml=test-reports/junit-{envname}.xml} 16 | 17 | [testenv:report] 18 | skip_install = true 19 | allowlist_externals = poetry 20 | ignore_errors = true 21 | fail_under = 90.0 22 | show_missing = true 23 | commands_pre = 24 | poetry install 25 | commands = 26 | poetry run coverage report 27 | poetry run coverage html 28 | 29 | [testenv:clean] 30 | skip_install = true 31 | allowlist_externals = poetry 32 | commands_pre = 33 | poetry install 34 | commands = 35 | poetry run coverage erase 36 | --------------------------------------------------------------------------------