├── .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 |
--------------------------------------------------------------------------------