├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── AUTHORS.rst ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.rst ├── RELEASE.md ├── codecov.yml ├── conf └── README.rst ├── docs ├── .gitignore ├── Makefile ├── api │ ├── modules.rst │ ├── stacker.actions.rst │ ├── stacker.blueprints.rst │ ├── stacker.blueprints.variables.rst │ ├── stacker.commands.rst │ ├── stacker.commands.stacker.rst │ ├── stacker.config.rst │ ├── stacker.config.translators.rst │ ├── stacker.hooks.rst │ ├── stacker.logger.rst │ ├── stacker.lookups.handlers.rst │ ├── stacker.lookups.rst │ ├── stacker.providers.aws.rst │ ├── stacker.providers.rst │ └── stacker.rst ├── blueprints.rst ├── commands.rst ├── conf.py ├── config.rst ├── environments.rst ├── index.rst ├── lookups.rst ├── organizations_using_stacker.rst ├── templates.rst ├── terminology.rst └── translators.rst ├── examples └── cross-account │ ├── .aws │ └── config │ ├── README.md │ ├── stacker.yaml │ └── templates │ ├── stacker-bucket.yaml │ └── stacker-role.yaml ├── requirements.in ├── scripts ├── compare_env ├── docker-stacker ├── stacker └── stacker.cmd ├── setup.cfg ├── setup.py ├── stacker ├── __init__.py ├── actions │ ├── __init__.py │ ├── base.py │ ├── build.py │ ├── destroy.py │ ├── diff.py │ ├── graph.py │ └── info.py ├── awscli_yamlhelper.py ├── blueprints │ ├── __init__.py │ ├── base.py │ ├── raw.py │ ├── testutil.py │ └── variables │ │ ├── __init__.py │ │ └── types.py ├── commands │ ├── __init__.py │ └── stacker │ │ ├── __init__.py │ │ ├── base.py │ │ ├── build.py │ │ ├── destroy.py │ │ ├── diff.py │ │ ├── graph.py │ │ └── info.py ├── config │ ├── __init__.py │ └── translators │ │ ├── __init__.py │ │ └── kms.py ├── context.py ├── dag │ └── __init__.py ├── environment.py ├── exceptions.py ├── hooks │ ├── __init__.py │ ├── aws_lambda.py │ ├── command.py │ ├── ecs.py │ ├── iam.py │ ├── keypair.py │ ├── route53.py │ └── utils.py ├── logger │ └── __init__.py ├── lookups │ ├── __init__.py │ ├── handlers │ │ ├── __init__.py │ │ ├── ami.py │ │ ├── default.py │ │ ├── dynamodb.py │ │ ├── envvar.py │ │ ├── file.py │ │ ├── hook_data.py │ │ ├── kms.py │ │ ├── output.py │ │ ├── rxref.py │ │ ├── split.py │ │ ├── ssmstore.py │ │ └── xref.py │ └── registry.py ├── plan.py ├── providers │ ├── __init__.py │ ├── aws │ │ ├── __init__.py │ │ └── default.py │ └── base.py ├── session_cache.py ├── stack.py ├── status.py ├── target.py ├── tests │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── test_base.py │ │ ├── test_build.py │ │ ├── test_destroy.py │ │ └── test_diff.py │ ├── blueprints │ │ ├── __init__.py │ │ ├── test_base.py │ │ ├── test_raw.py │ │ └── test_testutil.py │ ├── conftest.py │ ├── factories.py │ ├── fixtures │ │ ├── __init__.py │ │ ├── basic.env │ │ ├── cfn_template.json │ │ ├── cfn_template.json.j2 │ │ ├── cfn_template.yaml │ │ ├── keypair │ │ │ ├── fingerprint │ │ │ ├── id_rsa │ │ │ └── id_rsa.pub │ │ ├── mock_blueprints.py │ │ ├── mock_hooks.py │ │ ├── mock_lookups.py │ │ ├── not-basic.env │ │ ├── parameter_resolution │ │ │ └── template.yml │ │ ├── vpc-bastion-db-web-pre-1.0.yaml │ │ ├── vpc-bastion-db-web.yaml │ │ └── vpc-custom-log-format-info.yaml │ ├── hooks │ │ ├── __init__.py │ │ ├── test_aws_lambda.py │ │ ├── test_command.py │ │ ├── test_ecs.py │ │ ├── test_iam.py │ │ └── test_keypair.py │ ├── lookups │ │ ├── __init__.py │ │ ├── handlers │ │ │ ├── __init__.py │ │ │ ├── test_ami.py │ │ │ ├── test_default.py │ │ │ ├── test_dynamodb.py │ │ │ ├── test_envvar.py │ │ │ ├── test_file.py │ │ │ ├── test_hook_data.py │ │ │ ├── test_output.py │ │ │ ├── test_rxref.py │ │ │ ├── test_split.py │ │ │ ├── test_ssmstore.py │ │ │ └── test_xref.py │ │ └── test_registry.py │ ├── providers │ │ ├── __init__.py │ │ └── aws │ │ │ ├── __init__.py │ │ │ └── test_default.py │ ├── test_config.py │ ├── test_context.py │ ├── test_dag.py │ ├── test_environment.py │ ├── test_lookups.py │ ├── test_parse_user_data.py │ ├── test_plan.py │ ├── test_stack.py │ ├── test_stacker.py │ ├── test_util.py │ └── test_variables.py ├── tokenize_userdata.py ├── ui.py ├── util.py └── variables.py ├── test-requirements.in └── tests ├── Makefile ├── README.md ├── cleanup_functional_test_buckets.sh ├── fixtures ├── blueprints │ └── test_repo.json └── stack_policies │ ├── default.json │ └── none.json ├── run_test_suite.sh ├── stacker.yaml.sh ├── test_helper.bash └── test_suite ├── 01_stacker_build_no_config.bats ├── 02_stacker_build_empty_config.bats ├── 03_stacker_build-config_with_no_stacks.bats ├── 04_stacker_build-config_with_no_namespace.bats ├── 05_stacker_build-missing_environment_key.bats ├── 06_stacker_build-duplicate_stacks.bats ├── 07_stacker_graph-json_format.bats ├── 08_stacker_graph-dot_format.bats ├── 09_stacker_build-missing_variable.bats ├── 10_stacker_build-simple_build.bats ├── 11_stacker_info-simple_info.bats ├── 12_stacker_build-simple_build_with_output_lookups.bats ├── 13_stacker_build-simple_build_with_environment.bats ├── 14_stacker_build-interactive_with_skipped_update.bats ├── 15_stacker_build-no_namespace.bats ├── 16_stacker_build-overriden_environment_key_with_-e.bats ├── 17_stacker_build-dump.bats ├── 18_stacker_diff-simple_diff_with_output_lookups.bats ├── 19_stacker_build-replacements-only_test_with_additional_resource_no_keyerror.bats ├── 20_stacker_build-locked_stacks.bats ├── 21_stacker_build-default_mode_without_&_with_protected_stack.bats ├── 22_stacker_build-recreate_failed_stack_non-interactive_mode.bats ├── 23_stacker_build-recreate_failed_stack_interactive_mode.bats ├── 24_stacker_build-handle_rollbacks_during_updates.bats ├── 25_stacker_build-handle_rollbacks_in_dependent_stacks.bats ├── 26_stacker_build-raw_template.bats ├── 27_stacker_diff-raw_template.bats ├── 28_stacker_build-raw_template_parameter_resolution.bats ├── 29_stacker_build-no_parallelism.bats ├── 30_stacker_build-tailing.bats ├── 31_stacker_build-override_stack_name.bats ├── 32_stacker_build-multi_region.bats └── 33_stacker_build-profiles.bats /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | workflows: 4 | version: 2 5 | test-all: 6 | jobs: 7 | - lint 8 | - unit-test-37: 9 | requires: 10 | - lint 11 | - functional-test-37: 12 | requires: 13 | - unit-test-37 14 | - unit-test-38: 15 | requires: 16 | - lint 17 | - functional-test-38: 18 | requires: 19 | - unit-test-38 20 | - functional-test-37 21 | - unit-test-39: 22 | requires: 23 | - lint 24 | - functional-test-39: 25 | requires: 26 | - unit-test-39 27 | - functional-test-38 28 | - unit-test-310: 29 | requires: 30 | - lint 31 | - functional-test-310: 32 | requires: 33 | - unit-test-310 34 | - functional-test-39 35 | - cleanup-functional-buckets: 36 | requires: 37 | - functional-test-37 38 | - functional-test-38 39 | - functional-test-39 40 | - functional-test-310 41 | 42 | jobs: 43 | lint: 44 | docker: 45 | - image: circleci/python:3.7 46 | steps: 47 | - checkout 48 | - run: sudo pip install flake8 codecov pep8-naming 49 | - run: sudo python setup.py install 50 | - run: flake8 --version 51 | - run: sudo make lint 52 | 53 | unit-test-37: 54 | docker: 55 | - image: circleci/python:3.7 56 | steps: &unit_test_steps 57 | - checkout 58 | - run: sudo python setup.py install 59 | - run: sudo make test-unit 60 | 61 | unit-test-38: 62 | docker: 63 | - image: circleci/python:3.8 64 | steps: *unit_test_steps 65 | 66 | unit-test-39: 67 | docker: 68 | - image: circleci/python:3.9 69 | steps: *unit_test_steps 70 | 71 | unit-test-310: 72 | docker: 73 | - image: circleci/python:3.10 74 | steps: *unit_test_steps 75 | 76 | functional-test-37: 77 | docker: 78 | - image: circleci/python:3.7 79 | steps: &functional_test_steps 80 | - checkout 81 | - run: 82 | command: | 83 | git clone https://github.com/bats-core/bats-core.git 84 | cd bats-core 85 | git checkout v1.0.2 86 | sudo ./install.sh /usr/local 87 | bats --version 88 | - run: sudo python setup.py install 89 | - run: 90 | command: | 91 | export TERM=xterm 92 | export AWS_DEFAULT_REGION=us-east-1 93 | export STACKER_NAMESPACE=cloudtools-functional-tests-$CIRCLE_BUILD_NUM 94 | export STACKER_ROLE=arn:aws:iam::459170252436:role/cloudtools-functional-tests-sta-FunctionalTestRole-1M9HFJ9VQVMFX 95 | sudo -E make test-functional 96 | 97 | functional-test-38: 98 | docker: 99 | - image: circleci/python:3.8 100 | steps: *functional_test_steps 101 | 102 | functional-test-39: 103 | docker: 104 | - image: circleci/python:3.9 105 | steps: *functional_test_steps 106 | 107 | functional-test-310: 108 | docker: 109 | - image: circleci/python:3.10 110 | steps: *functional_test_steps 111 | 112 | cleanup-functional-buckets: 113 | docker: 114 | - image: circleci/python:3.7 115 | steps: 116 | - checkout 117 | - run: 118 | command: | 119 | tests/cleanup_functional_test_buckets.sh 120 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store* 32 | ehthumbs.db 33 | Icon? 34 | Thumbs.db 35 | 36 | # Vagrant 37 | .vagrant 38 | Vagrantfile 39 | 40 | # Editor crap 41 | *.sw* 42 | *~ 43 | .idea 44 | *.iml 45 | 46 | # Byte-compiled python 47 | *.pyc 48 | 49 | # Package directory 50 | build/ 51 | 52 | # Build object file directory 53 | objdir/ 54 | dist/ 55 | *.egg-info 56 | .eggs/ 57 | *.egg 58 | 59 | # Coverage artifacts 60 | .coverage 61 | htmlcov 62 | 63 | # Ignore development conf/env files 64 | dev.yaml 65 | dev.env 66 | tests/fixtures/blueprints/*-result 67 | FakeKey.pem 68 | vm_setup.sh 69 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Stacker was designed and developed by the OpsEng team at `Remind, Inc.`_ 5 | 6 | Current Maintainers 7 | ------------------- 8 | 9 | - `Michael Barrett`_ 10 | - `Eric Holmes`_ 11 | - `Ignacio Nin`_ 12 | - `Russell Ballestrini`_ 13 | 14 | Alumni 15 | ------ 16 | 17 | - `Michael Hahn`_ 18 | - `Tom Taubkin`_ 19 | 20 | Thanks 21 | ------ 22 | 23 | Stacker wouldn't be where it is today without the open source community that 24 | has formed around it. Thank you to everyone who has contributed, and special 25 | thanks to the following folks who have contributed great features and bug 26 | requests, as well as given guidance in stacker's development: 27 | 28 | - `Adam McElwee`_ 29 | - `Daniel Miranda`_ 30 | - `Troy Ready`_ 31 | - `Garison Draper`_ 32 | - `Mariusz`_ 33 | - `Tolga Tarhan`_ 34 | 35 | .. _`Remind, Inc.`: https://www.remind.com/ 36 | 37 | .. _`Michael Barrett`: https://github.com/phobologic 38 | .. _`Eric Holmes`: https://github.com/ejholmes 39 | .. _`Ignacio Nin`: https://github.com/Lowercases 40 | .. _`Russell Ballestrini`: https://github.com/russellballestrini 41 | 42 | .. _`Michael Hahn`: https://github.com/mhahn 43 | .. _`Tom Taubkin`: https://github.com/ttaub 44 | 45 | .. _`Adam McElwee`: https://github.com/acmcelwee 46 | .. _`Daniel Miranda`: https://github.com/danielkza 47 | .. _`Troy Ready`: https://github.com/troyready 48 | .. _`Garison Draper`: https://github.com/GarisonLotus 49 | .. _`Mariusz`: https://github.com/discobean 50 | .. _`Tolga Tarhan`: https://github.com/ttarhan 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cloudtools-maintainers@groups.google.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! 4 | 5 | You can contribute in many ways: 6 | 7 | ## Types of Contributions 8 | 9 | ### Report Bugs 10 | 11 | Report bugs at https://github.com/cloudtools/stacker/issues. 12 | 13 | If you are reporting a bug, please include: 14 | 15 | * Your operating system name and version. 16 | * Any details about your local setup that might be helpful in troubleshooting. 17 | * Detailed steps to reproduce the bug. 18 | 19 | ### Fix Bugs 20 | 21 | Look through the GitHub issues for bugs. Anything tagged with "bug" 22 | is open to whoever wants to implement it. 23 | 24 | ### Implement Features 25 | 26 | Look through the GitHub issues for features. Anything tagged with "feature" 27 | is open to whoever wants to implement it. 28 | 29 | ### Write Documentation 30 | 31 | stacker could always use more documentation, whether as part of the 32 | official stacker docs, in docstrings, or even on the web in blog posts, 33 | articles, and such. 34 | 35 | Note: We use Google style docstrings (http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example\_google.html) 36 | 37 | ### Submit Feedback 38 | 39 | The best way to send feedback is to file an issue at https://github.com/cloudtools/stacker/issues. 40 | 41 | If you are proposing a feature: 42 | 43 | * Explain in detail how it would work. 44 | * Keep the scope as narrow as possible, to make it easier to implement. 45 | * Remember that this is a volunteer-driven project, and that contributions 46 | are welcome :) 47 | 48 | 49 | ## Get Started! 50 | 51 | Ready to contribute? Here's how to set up `stacker` for local development. 52 | 53 | 1. Fork the `stacker` repo on GitHub. 54 | 2. Clone your fork locally: 55 | 56 | ```console 57 | $ git clone git@github.com:your_name_here/stacker.git 58 | ``` 59 | 60 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development: 61 | 62 | ```console 63 | $ mkvirtualenv stacker 64 | $ cd stacker/ 65 | $ python setup.py develop 66 | ``` 67 | 68 | 4. Create a branch for local development: 69 | 70 | ```console 71 | $ git checkout -b name-of-your-bugfix-or-feature 72 | ``` 73 | 74 | Now you can make your changes locally. 75 | 76 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox: 77 | 78 | ```console 79 | $ make test 80 | ``` 81 | 82 | To get flake8 just pip install it into your virtualenv. 83 | 84 | 6. Commit your changes and push your branch to GitHub: 85 | 86 | ```console 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | ``` 91 | 92 | 7. Submit a pull request through the GitHub website. 93 | 94 | For information about the functional testing suite, see [tests/README.md](./tests). 95 | 96 | ## Pull Request Guidelines 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. (See `Write Documentation` above for guidelines) 102 | 3. The pull request should work for Python 2.7 and for PyPy. Check 103 | https://circleci.com/gh/cloudtools/stacker and make sure that the tests pass for all supported Python versions. 104 | 4. Please update the `Upcoming/Master` section of the [CHANGELOG](./CHANGELOG.md) with a small bullet point about the change. 105 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.10 2 | MAINTAINER Mike Barrett 3 | 4 | COPY scripts/docker-stacker /bin/docker-stacker 5 | RUN mkdir -p /stacks && pip install --upgrade pip setuptools 6 | WORKDIR /stacks 7 | COPY . /tmp/stacker 8 | RUN pip install --upgrade pip 9 | RUN pip install --upgrade setuptools 10 | RUN cd /tmp/stacker && python setup.py install && rm -rf /tmp/stacker 11 | 12 | ENTRYPOINT ["docker-stacker"] 13 | CMD ["-h"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Remind101, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build lint test-unit test-functional test 2 | 3 | build: 4 | docker build -t remind101/stacker . 5 | 6 | lint: 7 | flake8 --ignore E402,W503,W504,W605,N818 --exclude stacker/tests/ stacker 8 | flake8 --ignore E402,N802,W605,N818 stacker/tests # ignore setUp naming 9 | 10 | test-unit: clean 11 | python setup.py test 12 | 13 | test-unit3: clean 14 | python3 setup.py test 15 | 16 | clean: 17 | rm -rf .egg stacker.egg-info 18 | 19 | test-functional: 20 | cd tests && bats test_suite 21 | 22 | # General testing target for most development. 23 | test: lint test-unit test-unit3 24 | 25 | apidocs: 26 | sphinx-apidoc --force -o docs/api stacker 27 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Steps to release a new version 2 | 3 | ## Preparing for the release 4 | 5 | - Check out a branch named for the version: `git checkout -b release-1.1.1` 6 | - Change version in setup.py and stacker/\_\_init\_\_.py 7 | - Update CHANGELOG.md with changes made since last release (see below for helpful 8 | command) 9 | - add changed files: `git add setup.py stacker/\_\_init\_\_.py CHANGELOG.md` 10 | - Commit changes: `git commit -m "Release 1.1.1"` 11 | - Create a signed tag: `git tag --sign -m "Release 1.1.1" 1.1.1` 12 | - Push branch up to git: `git push -u origin release-1.1.1` 13 | - Open a PR for the release, ensure that tests pass 14 | 15 | ## Releasing 16 | 17 | - Push tag: `git push --tags` 18 | - Merge PR into master, checkout master locally: `git checkout master; git pull` 19 | - Create PyPI release: `python setup.py sdist upload --sign` 20 | - Update github release page: https://github.com/cloudtools/stacker/releases 21 | - use the contents of the latest CHANGELOG entry for the body. 22 | 23 | # Helper to create CHANGELOG entries 24 | git log --reverse --pretty=format:"%s" | tail -100 | sed 's/^/- /' 25 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /conf/README.rst: -------------------------------------------------------------------------------- 1 | Please check out the stacker_blueprints_ repo for example configs and 2 | blueprints. 3 | 4 | .. _stacker_blueprints: https://github.com/cloudtools/stacker_blueprints 5 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | stacker 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | stacker 8 | -------------------------------------------------------------------------------- /docs/api/stacker.actions.rst: -------------------------------------------------------------------------------- 1 | stacker\.actions package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.actions\.base module 8 | ----------------------------- 9 | 10 | .. automodule:: stacker.actions.base 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | stacker\.actions\.build module 16 | ------------------------------ 17 | 18 | .. automodule:: stacker.actions.build 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | stacker\.actions\.destroy module 24 | -------------------------------- 25 | 26 | .. automodule:: stacker.actions.destroy 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | stacker\.actions\.diff module 32 | ----------------------------- 33 | 34 | .. automodule:: stacker.actions.diff 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | stacker\.actions\.info module 40 | ----------------------------- 41 | 42 | .. automodule:: stacker.actions.info 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: stacker.actions 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /docs/api/stacker.blueprints.rst: -------------------------------------------------------------------------------- 1 | stacker\.blueprints package 2 | =========================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stacker.blueprints.variables 10 | 11 | Submodules 12 | ---------- 13 | 14 | stacker\.blueprints\.base module 15 | -------------------------------- 16 | 17 | .. automodule:: stacker.blueprints.base 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | stacker\.blueprints\.testutil module 23 | ------------------------------------ 24 | 25 | .. automodule:: stacker.blueprints.testutil 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: stacker.blueprints 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/api/stacker.blueprints.variables.rst: -------------------------------------------------------------------------------- 1 | stacker\.blueprints\.variables package 2 | ====================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.blueprints\.variables\.types module 8 | -------------------------------------------- 9 | 10 | .. automodule:: stacker.blueprints.variables.types 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: stacker.blueprints.variables 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/api/stacker.commands.rst: -------------------------------------------------------------------------------- 1 | stacker\.commands package 2 | ========================= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stacker.commands.stacker 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: stacker.commands 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/api/stacker.commands.stacker.rst: -------------------------------------------------------------------------------- 1 | stacker\.commands\.stacker package 2 | ================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.commands\.stacker\.base module 8 | --------------------------------------- 9 | 10 | .. automodule:: stacker.commands.stacker.base 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | stacker\.commands\.stacker\.build module 16 | ---------------------------------------- 17 | 18 | .. automodule:: stacker.commands.stacker.build 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | stacker\.commands\.stacker\.destroy module 24 | ------------------------------------------ 25 | 26 | .. automodule:: stacker.commands.stacker.destroy 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | stacker\.commands\.stacker\.diff module 32 | --------------------------------------- 33 | 34 | .. automodule:: stacker.commands.stacker.diff 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | stacker\.commands\.stacker\.info module 40 | --------------------------------------- 41 | 42 | .. automodule:: stacker.commands.stacker.info 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | 48 | Module contents 49 | --------------- 50 | 51 | .. automodule:: stacker.commands.stacker 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | -------------------------------------------------------------------------------- /docs/api/stacker.config.rst: -------------------------------------------------------------------------------- 1 | stacker\.config package 2 | ======================= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stacker.config.translators 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: stacker.config 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/api/stacker.config.translators.rst: -------------------------------------------------------------------------------- 1 | stacker\.config\.translators package 2 | ==================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.config\.translators\.kms module 8 | ---------------------------------------- 9 | 10 | .. automodule:: stacker.config.translators.kms 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: stacker.config.translators 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/api/stacker.hooks.rst: -------------------------------------------------------------------------------- 1 | stacker\.hooks package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.hooks\.aws\_lambda module 8 | ---------------------------------- 9 | 10 | .. automodule:: stacker.hooks.aws_lambda 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | stacker\.hooks\.ecs module 16 | -------------------------- 17 | 18 | .. automodule:: stacker.hooks.ecs 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | stacker\.hooks\.iam module 24 | -------------------------- 25 | 26 | .. automodule:: stacker.hooks.iam 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | stacker\.hooks\.keypair module 32 | ------------------------------ 33 | 34 | .. automodule:: stacker.hooks.keypair 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | stacker\.hooks\.route53 module 40 | ------------------------------ 41 | 42 | .. automodule:: stacker.hooks.route53 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | stacker\.hooks\.utils module 48 | ---------------------------- 49 | 50 | .. automodule:: stacker.hooks.utils 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: stacker.hooks 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /docs/api/stacker.logger.rst: -------------------------------------------------------------------------------- 1 | stacker\.logger package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.logger\.formatter module 8 | --------------------------------- 9 | 10 | .. automodule:: stacker.logger.formatter 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | stacker\.logger\.handler module 16 | ------------------------------- 17 | 18 | .. automodule:: stacker.logger.handler 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: stacker.logger 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/api/stacker.lookups.handlers.rst: -------------------------------------------------------------------------------- 1 | stacker\.lookups\.handlers package 2 | ================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.lookups\.handlers\.ami module 8 | -------------------------------------- 9 | 10 | .. automodule:: stacker.lookups.handlers.ami 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | stacker\.lookups\.handlers\.default module 16 | ------------------------------------------ 17 | 18 | .. automodule:: stacker.lookups.handlers.default 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | stacker\.lookups\.handlers\.dynamodb module 24 | ------------------------------------------- 25 | 26 | .. automodule:: stacker.lookups.handlers.dynamodb 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | stacker\.lookups\.handlers\.envvar module 32 | ----------------------------------------- 33 | 34 | .. automodule:: stacker.lookups.handlers.envvar 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | stacker\.lookups\.handlers\.file module 40 | --------------------------------------- 41 | 42 | .. automodule:: stacker.lookups.handlers.file 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | stacker\.lookups\.handlers\.hook\_data module 48 | --------------------------------------------- 49 | 50 | .. automodule:: stacker.lookups.handlers.hook_data 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | stacker\.lookups\.handlers\.kms module 56 | -------------------------------------- 57 | 58 | .. automodule:: stacker.lookups.handlers.kms 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | stacker\.lookups\.handlers\.output module 64 | ----------------------------------------- 65 | 66 | .. automodule:: stacker.lookups.handlers.output 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | stacker\.lookups\.handlers\.rxref module 72 | ---------------------------------------- 73 | 74 | .. automodule:: stacker.lookups.handlers.rxref 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | stacker\.lookups\.handlers\.split module 80 | ---------------------------------------- 81 | 82 | .. automodule:: stacker.lookups.handlers.split 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | stacker\.lookups\.handlers\.ssmstore module 88 | ------------------------------------------- 89 | 90 | .. automodule:: stacker.lookups.handlers.ssmstore 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | stacker\.lookups\.handlers\.xref module 96 | --------------------------------------- 97 | 98 | .. automodule:: stacker.lookups.handlers.xref 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | 104 | Module contents 105 | --------------- 106 | 107 | .. automodule:: stacker.lookups.handlers 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | -------------------------------------------------------------------------------- /docs/api/stacker.lookups.rst: -------------------------------------------------------------------------------- 1 | stacker\.lookups package 2 | ======================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stacker.lookups.handlers 10 | 11 | Submodules 12 | ---------- 13 | 14 | stacker\.lookups\.registry module 15 | --------------------------------- 16 | 17 | .. automodule:: stacker.lookups.registry 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: stacker.lookups 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/api/stacker.providers.aws.rst: -------------------------------------------------------------------------------- 1 | stacker\.providers\.aws package 2 | =============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stacker\.providers\.aws\.default module 8 | --------------------------------------- 9 | 10 | .. automodule:: stacker.providers.aws.default 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: stacker.providers.aws 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/api/stacker.providers.rst: -------------------------------------------------------------------------------- 1 | stacker\.providers package 2 | ========================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stacker.providers.aws 10 | 11 | Submodules 12 | ---------- 13 | 14 | stacker\.providers\.base module 15 | ------------------------------- 16 | 17 | .. automodule:: stacker.providers.base 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: stacker.providers 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /docs/api/stacker.rst: -------------------------------------------------------------------------------- 1 | stacker package 2 | =============== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | stacker.actions 10 | stacker.blueprints 11 | stacker.commands 12 | stacker.config 13 | stacker.hooks 14 | stacker.logger 15 | stacker.lookups 16 | stacker.providers 17 | stacker.tests 18 | 19 | Submodules 20 | ---------- 21 | 22 | stacker\.context module 23 | ----------------------- 24 | 25 | .. automodule:: stacker.context 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | stacker\.environment module 31 | --------------------------- 32 | 33 | .. automodule:: stacker.environment 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | stacker\.exceptions module 39 | -------------------------- 40 | 41 | .. automodule:: stacker.exceptions 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | stacker\.plan module 47 | -------------------- 48 | 49 | .. automodule:: stacker.plan 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | stacker\.session\_cache module 55 | ------------------------------ 56 | 57 | .. automodule:: stacker.session_cache 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | stacker\.stack module 63 | --------------------- 64 | 65 | .. automodule:: stacker.stack 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | stacker\.status module 71 | ---------------------- 72 | 73 | .. automodule:: stacker.status 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | stacker\.tokenize\_userdata module 79 | ---------------------------------- 80 | 81 | .. automodule:: stacker.tokenize_userdata 82 | :members: 83 | :undoc-members: 84 | :show-inheritance: 85 | 86 | stacker\.util module 87 | -------------------- 88 | 89 | .. automodule:: stacker.util 90 | :members: 91 | :undoc-members: 92 | :show-inheritance: 93 | 94 | stacker\.variables module 95 | ------------------------- 96 | 97 | .. automodule:: stacker.variables 98 | :members: 99 | :undoc-members: 100 | :show-inheritance: 101 | 102 | 103 | Module contents 104 | --------------- 105 | 106 | .. automodule:: stacker 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. stacker documentation master file, created by 2 | sphinx-quickstart on Fri Aug 14 09:59:29 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to stacker's documentation! 7 | =================================== 8 | 9 | stacker is a tool and library used to create & update multiple CloudFormation 10 | stacks. It was originally written at Remind_ and 11 | released to the open source community. 12 | 13 | stacker Blueprints are written in troposphere_, though the purpose of 14 | most templates is to keep them as generic as possible and then use 15 | configuration to modify them. 16 | 17 | At Remind we use stacker to manage all of our Cloudformation stacks - 18 | both in development, staging and production without any major issues. 19 | 20 | 21 | Main Features 22 | ------------- 23 | 24 | - Easily `Create/Update `_/`Destroy `_ 25 | many stacks in parallel (though with an understanding of cross-stack 26 | dependencies) 27 | - Makes it easy to manage large environments in a single config, while still 28 | allowing you to break each part of the environment up into its own 29 | completely separate stack. 30 | - Manages dependencies between stacks, only launching one after all the stacks 31 | it depends on are finished. 32 | - Only updates stacks that have changed and that have not been explicitly 33 | locked or disabled. 34 | - Easily pass Outputs from one stack in as Variables on another (which also 35 | automatically provides an implicit dependency) 36 | - Use `Environments `_ to manage slightly different 37 | configuration in different environments. 38 | - Use `Lookups `_ to allow dynamic fetching or altering of 39 | data used in Variables. 40 | - A diff command for diffing your config against what is running in a live 41 | CloudFormation environment. 42 | - A small library of pre-shared Blueprints can be found at the 43 | stacker_blueprints_ repo, making things like setting up a VPC easy. 44 | 45 | 46 | Contents: 47 | 48 | .. toctree:: 49 | :maxdepth: 2 50 | 51 | organizations_using_stacker 52 | terminology 53 | config 54 | environments 55 | translators 56 | lookups 57 | commands 58 | blueprints 59 | templates 60 | API Docs 61 | 62 | 63 | 64 | Indices and tables 65 | ================== 66 | 67 | * :ref:`genindex` 68 | * :ref:`modindex` 69 | * :ref:`search` 70 | 71 | .. _Remind: http://www.remind.com/ 72 | .. _troposphere: https://github.com/cloudtools/troposphere 73 | .. _stacker_blueprints: https://github.com/cloudtools/stacker_blueprints 74 | -------------------------------------------------------------------------------- /docs/organizations_using_stacker.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Organizations using stacker 3 | =========================== 4 | 5 | Below is a list of organizations that currently use stacker in some sense. If 6 | you are using stacker, please submit a PR and add your company below! 7 | 8 | Remind_ 9 | 10 | Remind helps educators send quick, simple messages to students and parents on 11 | any device. We believe that when communication improves, relationships get 12 | stronger. Education gets better. 13 | 14 | Remind is the original author of stacker, and has been using it to manage the 15 | infrastructure in multiple environments (including production) since early 16 | 2015. 17 | 18 | 19 | .. _Remind: https://www.remind.com/ 20 | 21 | `Onica`_ 22 | 23 | Onica is a global technology consulting company at the forefront of 24 | cloud computing. Through collaboration with Amazon Web Services, 25 | we help customers embrace a broad spectrum of innovative solutions. 26 | From migration strategy to operational excellence, cloud native 27 | development, and immersive transformation. Onica is a full spectrum 28 | AWS integrator. 29 | 30 | .. _`Onica`: https://www.onica.com 31 | 32 | AltoStack_ 33 | 34 | AltoStack is a technology and services consultancy specialising in Cloud 35 | Consultancy, DevOps, Continuous Delivery and Configuration Management. 36 | 37 | From strategy and operations to culture and technology, AltoStack helps 38 | businesses identify and address opportunities for growth and profitability. 39 | 40 | We are an Amazon Web Services - (AWS) APN Consulting Partner. 41 | 42 | .. _AltoStack: https://altostack.io/ 43 | 44 | Cobli_ 45 | 46 | Cobli develops cutting-edge solutions for fleet management efficiency and 47 | intelligence in South America. We bring advanced tracking, analysis and 48 | predictions to fleets of any size by connecting vehicles to an easy to use 49 | platform through smart devices. 50 | 51 | Cobli manages most of its AWS infrastructure using stacker, and we encourage 52 | our developers to contribute to free-software whenever possible. 53 | 54 | .. _Cobli: https://cobli.co/ 55 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Templates 3 | ========== 4 | 5 | CloudFormation templates can be provided via python Blueprints_ or JSON/YAML. 6 | JSON/YAML templates are specified for stacks via the ``template_path`` config 7 | option (see `Stacks `_). 8 | 9 | Jinja2 Templating 10 | ================= 11 | 12 | Templates with a ``.j2`` extension will be parsed using `Jinja2 13 | `_. The stacker ``context`` and ``mappings`` objects 14 | and stack ``variables`` objects are available for use in the template: 15 | 16 | .. code-block:: yaml 17 | 18 | Description: TestTemplate 19 | Resources: 20 | Bucket: 21 | Type: AWS::S3::Bucket 22 | Properties: 23 | BucketName: {{ context.environment.foo }}-{{ variables.myparamname }} 24 | -------------------------------------------------------------------------------- /docs/terminology.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Terminology 3 | =========== 4 | 5 | blueprint 6 | ========= 7 | 8 | .. _blueprints: 9 | 10 | A python class that is responsible for creating a CloudFormation template. 11 | Usually this is built using troposphere_. 12 | 13 | config 14 | ====== 15 | 16 | A YAML config file that defines the `stack definitions`_ for all of the 17 | stacks you want stacker to manage. 18 | 19 | environment 20 | =========== 21 | 22 | A set of variables that can be used inside the config, allowing you to 23 | slightly adjust configs based on which environment you are launching. 24 | 25 | namespace 26 | ========= 27 | 28 | A way to uniquely identify a stack. Used to determine the naming of many 29 | things, such as the S3 bucket where compiled templates are stored, as well 30 | as the prefix for stack names. 31 | 32 | stack definition 33 | ================ 34 | 35 | .. _stack definitions: 36 | 37 | Defines the stack_ you want to build, usually there are multiple of these in 38 | the config_. It also defines the variables_ to be used when building the 39 | stack_. 40 | 41 | stack 42 | ===== 43 | 44 | .. _stacks: 45 | 46 | The resulting stack of resources that is created by CloudFormation when it 47 | executes a template. Each stack managed by stacker is defined by a 48 | `stack definition`_ in the config_. 49 | 50 | output 51 | ====== 52 | 53 | A CloudFormation Template concept. Stacks can output values, allowing easy 54 | access to those values. Often used to export the unique ID's of resources that 55 | templates create. Stacker makes it simple to pull outputs from one stack and 56 | then use them as a variable_ in another stack. 57 | 58 | variable 59 | ======== 60 | 61 | .. _variables: 62 | 63 | Dynamic variables that are passed into stacks when they are being built. 64 | Variables are defined within the config_. 65 | 66 | lookup 67 | ====== 68 | 69 | A method for expanding values in the config_ at build time. By default 70 | lookups are used to reference Output values from other stacks_ within the 71 | same namespace_. 72 | 73 | provider 74 | ======== 75 | 76 | Provider that supports provisioning rendered blueprints_. By default, an 77 | AWS provider is used. 78 | 79 | context 80 | ======= 81 | 82 | Context is responsible for translating the values passed in via the 83 | command line and specified in the config_ to stacks_. 84 | 85 | .. _troposphere: https://github.com/cloudtools/troposphere 86 | .. _CloudFormation Parameters: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html 87 | -------------------------------------------------------------------------------- /docs/translators.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Translators 3 | =========== 4 | 5 | .. note:: 6 | Translators have been deprecated in favor of `Lookups `_ 7 | and will be removed in a future release. 8 | 9 | Stacker provides the ability to dynamically replace values in the config via a 10 | concept called translators. A translator is meant to take a value and convert 11 | it by calling out to another service or system. This is initially meant to 12 | deal with encrypting fields in your config. 13 | 14 | Translators are custom YAML constructors. As an example, if you have a 15 | database and it has a parameter called ``DBPassword`` that you don't want to 16 | store in clear text in your config (maybe because you want to check it into 17 | your version control system to share with the team), you could instead 18 | encrypt the value using ``kms``. For example:: 19 | 20 | # We use the aws cli to get the encrypted value for the string 21 | # "PASSWORD" using the master key called 'myStackerKey' in us-east-1 22 | $ aws --region us-east-1 kms encrypt --key-id alias/myStackerKey \ 23 | --plaintext "PASSWORD" --output text --query CiphertextBlob 24 | 25 | CiD6bC8t2Y<...encrypted blob...> 26 | 27 | # In stacker we would reference the encrypted value like: 28 | DBPassword: !kms us-east-1@CiD6bC8t2Y<...encrypted blob...> 29 | 30 | # The above would resolve to 31 | DBPassword: PASSWORD 32 | 33 | This requires that the person using stacker has access to the master key used 34 | to encrypt the value. 35 | 36 | It is also possible to store the encrypted blob in a file (useful if the 37 | value is large) using the `file://` prefix, ie:: 38 | 39 | DockerConfig: !kms file://dockercfg 40 | 41 | .. note:: 42 | Translators resolve the path specified with `file://` relative to 43 | the location of the config file, not where the stacker command is run. 44 | -------------------------------------------------------------------------------- /examples/cross-account/.aws/config: -------------------------------------------------------------------------------- 1 | # The master account is like the root of our AWS account tree. It's the 2 | # entrypoint for all other profiles to sts.AssumeRole from. 3 | [profile master] 4 | region = us-east-1 5 | role_arn = arn:aws:iam:::role/Stacker 6 | role_session_name = stacker 7 | credential_source = Environment 8 | 9 | [profile prod] 10 | region = us-east-1 11 | role_arn = arn:aws:iam:::role/Stacker 12 | role_session_name = stacker 13 | source_profile = master 14 | 15 | [profile stage] 16 | region = us-east-1 17 | role_arn = arn:aws:iam:::role/Stacker 18 | role_session_name = stacker 19 | source_profile = master 20 | -------------------------------------------------------------------------------- /examples/cross-account/stacker.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: '' 3 | 4 | # We'll set this to an empty string until we've provisioned the 5 | # "stacker-bucket" stack below. 6 | stacker_bucket: '' 7 | 8 | stacks: 9 | # This stack will provision an S3 bucket for stacker to use to upload 10 | # templates. This will also configure the bucket with a bucket policy 11 | # allowing CloudFormation in other accounts to fetch templates from it. 12 | - name: stacker-bucket 13 | # We're going to "target" this stack in our "master" account. 14 | profile: master 15 | template_path: templates/stacker-bucket.yaml 16 | variables: 17 | # Change these to the correct AWS account IDs, must be comma seperated list 18 | Roles: arn:aws:iam:::role/Stacker, arn:aws:iam:::role/Stacker 19 | -------------------------------------------------------------------------------- /examples/cross-account/templates/stacker-bucket.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: A bucket for stacker to store CloudFormation templates 4 | Parameters: 5 | Roles: 6 | Type: CommaDelimitedList 7 | Description: A list of IAM roles that will be given read access on the bucket. 8 | 9 | Resources: 10 | StackerBucket: 11 | Type: AWS::S3::Bucket 12 | Properties: 13 | BucketEncryption: 14 | ServerSideEncryptionConfiguration: 15 | - ServerSideEncryptionByDefault: 16 | SSEAlgorithm: AES256 17 | 18 | BucketPolicy: 19 | Type: AWS::S3::BucketPolicy 20 | Properties: 21 | Bucket: 22 | Ref: StackerBucket 23 | PolicyDocument: 24 | Statement: 25 | - Action: 26 | - s3:GetObject 27 | Effect: Allow 28 | Principal: 29 | AWS: 30 | Ref: Roles 31 | Resource: 32 | - Fn::Sub: arn:aws:s3:::${StackerBucket}/* 33 | 34 | Outputs: 35 | BucketId: 36 | Value: 37 | Ref: StackerBucket 38 | -------------------------------------------------------------------------------- /examples/cross-account/templates/stacker-role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: A role that stacker can assume 4 | Parameters: 5 | MasterAccountId: 6 | Type: String 7 | Description: The 12-digit ID for the master account 8 | MinLength: 12 9 | MaxLength: 12 10 | AllowedPattern: "[0-9]+" 11 | ConstraintDescription: Must contain a 12 digit account ID 12 | RoleName: 13 | Type: String 14 | Description: The name of the stacker role. 15 | Default: Stacker 16 | 17 | 18 | Conditions: 19 | # Check if we're creating this role in the master account. 20 | InMasterAccount: 21 | Fn::Equals: 22 | - { Ref: "AWS::AccountId" } 23 | - { Ref: "MasterAccountId" } 24 | 25 | Resources: 26 | StackerRole: 27 | Type: AWS::IAM::Role 28 | Properties: 29 | RoleName: 30 | Ref: RoleName 31 | AssumeRolePolicyDocument: 32 | Version: "2012-10-17" 33 | Statement: 34 | Fn::If: 35 | - InMasterAccount 36 | - Effect: Allow 37 | Principal: 38 | AWS: 39 | Fn::Sub: "arn:aws:iam::${MasterAccountId}:root" 40 | Action: sts:AssumeRole 41 | Condition: 42 | 'Null': 43 | aws:MultiFactorAuthAge: false 44 | - Effect: Allow 45 | Principal: 46 | AWS: 47 | Fn::Sub: "arn:aws:iam::${MasterAccountId}:role/${RoleName}" 48 | Action: sts:AssumeRole 49 | Condition: 50 | 'Null': 51 | aws:MultiFactorAuthAge: false 52 | 53 | # Generally, Stacker will need fairly wide open permissions, since it will be 54 | # managing all resources in an account. 55 | StackerPolicies: 56 | Type: AWS::IAM::Policy 57 | Properties: 58 | PolicyName: Stacker 59 | PolicyDocument: 60 | Version: "2012-10-17" 61 | Statement: 62 | - Effect: Allow 63 | Action: ["*"] 64 | Resource: "*" 65 | Roles: 66 | - Ref: StackerRole 67 | 68 | Outputs: 69 | StackerRole: 70 | Value: 71 | Fn::GetAtt: 72 | - StackerRole 73 | - Arn 74 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | troposphere>=3.0.0 2 | botocore>=1.12.111 3 | boto3>=1.9.111,<2.0 4 | PyYAML>=3.13b1 5 | awacs>=0.6.0 6 | gitpython>=3.0 7 | jinja2>=2.7 8 | schematics>=2.1.0 9 | formic2 10 | python-dateutil>=2.0,<3.0 11 | MarkupSafe>=2 12 | more-itertools 13 | rsa>=4.7 14 | python-jose 15 | future 16 | -------------------------------------------------------------------------------- /scripts/compare_env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ A script to compare environment files. """ 3 | 4 | import argparse 5 | import os.path 6 | 7 | from stacker.environment import parse_environment 8 | 9 | 10 | def parse_args(): 11 | parser = argparse.ArgumentParser(description=__doc__) 12 | parser.add_argument( 13 | "-i", "--ignore-changed", action="store_true", 14 | help="Only print added & deleted keys, not changed keys.") 15 | parser.add_argument( 16 | "-s", "--show-changes", action="store_true", 17 | help="Print content changes.") 18 | parser.add_argument( 19 | "first_env", type=str, 20 | help="The first environment file to compare.") 21 | parser.add_argument( 22 | "second_env", type=str, 23 | help="The second environment file to compare.") 24 | 25 | return parser.parse_args() 26 | 27 | 28 | def parse_env_file(path): 29 | expanded_path = os.path.expanduser(path) 30 | with open(expanded_path) as fd: 31 | return parse_environment(fd.read()) 32 | 33 | 34 | def main(): 35 | args = parse_args() 36 | 37 | first_env = parse_env_file(args.first_env) 38 | second_env = parse_env_file(args.second_env) 39 | 40 | first_env_keys = set(first_env.keys()) 41 | second_env_keys = set(second_env.keys()) 42 | 43 | common_keys = first_env_keys & second_env_keys 44 | removed_keys = first_env_keys - second_env_keys 45 | added_keys = second_env_keys - first_env_keys 46 | 47 | changed_keys = set() 48 | 49 | for k in common_keys: 50 | if first_env[k] != second_env[k]: 51 | changed_keys.add(k) 52 | 53 | print "-- Added keys:" 54 | print " %s" % ", ".join(added_keys) 55 | print 56 | print "-- Removed keys:" 57 | print " %s" % ", ".join(removed_keys) 58 | print 59 | print "-- Changed keys:" 60 | if not args.show_changes: 61 | print " %s" % ", ".join(changed_keys) 62 | if args.show_changes: 63 | for k in changed_keys: 64 | print " %s:" % (k) 65 | print " < %s" % (first_env[k]) 66 | print " > %s" % (second_env[k]) 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /scripts/docker-stacker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is meant to be used from within the Docker image for stacker. It 4 | # simply installs the stacks at /stacks and then runs stacker. 5 | 6 | set -e 7 | 8 | cd /stacks 9 | python setup.py install 10 | 11 | exec stacker $@ 12 | -------------------------------------------------------------------------------- /scripts/stacker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from stacker.logger import setup_logging 4 | from stacker.commands import Stacker 5 | 6 | if __name__ == "__main__": 7 | stacker = Stacker(setup_logging=setup_logging) 8 | args = stacker.parse_args() 9 | stacker.configure(args) 10 | args.run(args) 11 | -------------------------------------------------------------------------------- /scripts/stacker.cmd: -------------------------------------------------------------------------------- 1 | @echo OFF 2 | REM=""" 3 | setlocal 4 | set PythonExe="" 5 | set PythonExeFlags= 6 | 7 | for %%i in (cmd bat exe) do ( 8 | for %%j in (python.%%i) do ( 9 | call :SetPythonExe "%%~$PATH:j" 10 | ) 11 | ) 12 | for /f "tokens=2 delims==" %%i in ('assoc .py') do ( 13 | for /f "tokens=2 delims==" %%j in ('ftype %%i') do ( 14 | for /f "tokens=1" %%k in ("%%j") do ( 15 | call :SetPythonExe %%k 16 | ) 17 | ) 18 | ) 19 | %PythonExe% -x %PythonExeFlags% "%~f0" %* 20 | exit /B %ERRORLEVEL% 21 | goto :EOF 22 | 23 | :SetPythonExe 24 | if not ["%~1"]==[""] ( 25 | if [%PythonExe%]==[""] ( 26 | set PythonExe="%~1" 27 | ) 28 | ) 29 | goto :EOF 30 | """ 31 | 32 | # =================================================== 33 | # Python script starts here 34 | # Above helper adapted from https://github.com/aws/aws-cli/blob/1.11.121/bin/aws.cmd 35 | # =================================================== 36 | 37 | #!/usr/bin/env python 38 | 39 | from stacker.logger import setup_logging 40 | from stacker.commands import Stacker 41 | 42 | if __name__ == "__main__": 43 | stacker = Stacker(setup_logging=setup_logging) 44 | args = stacker.parse_args() 45 | stacker.configure(args) 46 | args.run(args) 47 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [aliases] 5 | test = pytest 6 | 7 | [tool:pytest] 8 | testpaths = stacker/tests 9 | cov = stacker 10 | filterwarnings = 11 | ignore::DeprecationWarning 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | VERSION = "1.7.2" 5 | 6 | src_dir = os.path.dirname(__file__) 7 | 8 | def get_install_requirements(path): 9 | content = open(os.path.join(os.path.dirname(__file__), path)).read() 10 | return [req for req in content.split("\n") if req != "" and not req.startswith("#")] 11 | 12 | install_requires = get_install_requirements("requirements.in") 13 | 14 | setup_requires = ['pytest-runner'] 15 | 16 | tests_require = get_install_requirements("test-requirements.in") 17 | 18 | scripts = [ 19 | "scripts/compare_env", 20 | "scripts/docker-stacker", 21 | "scripts/stacker.cmd", 22 | "scripts/stacker", 23 | ] 24 | 25 | 26 | def read(filename): 27 | full_path = os.path.join(src_dir, filename) 28 | with open(full_path) as fd: 29 | return fd.read() 30 | 31 | 32 | if __name__ == "__main__": 33 | setup( 34 | name="stacker", 35 | version=VERSION, 36 | author="Michael Barrett", 37 | author_email="loki77@gmail.com", 38 | license="New BSD license", 39 | url="https://github.com/cloudtools/stacker", 40 | description="AWS CloudFormation Stack manager", 41 | long_description=read("README.rst"), 42 | packages=find_packages(), 43 | scripts=scripts, 44 | install_requires=install_requires, 45 | tests_require=tests_require, 46 | setup_requires=setup_requires, 47 | extras_require=dict(testing=tests_require), 48 | classifiers=[ 49 | "Development Status :: 5 - Production/Stable", 50 | "Environment :: Console", 51 | "License :: OSI Approved :: BSD License", 52 | "Programming Language :: Python :: 3.7", 53 | "Programming Language :: Python :: 3.8", 54 | "Programming Language :: Python :: 3.9", 55 | "Programming Language :: Python :: 3.10", 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /stacker/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "1.7.2" 3 | -------------------------------------------------------------------------------- /stacker/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/actions/__init__.py -------------------------------------------------------------------------------- /stacker/actions/graph.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import json 4 | 5 | from .base import BaseAction, plan 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def each_step(graph): 12 | """Returns an iterator that yields each step and it's direct 13 | dependencies. 14 | """ 15 | 16 | steps = graph.topological_sort() 17 | steps.reverse() 18 | 19 | for step in steps: 20 | deps = graph.downstream(step.name) 21 | yield (step, deps) 22 | 23 | 24 | def dot_format(out, graph, name="digraph"): 25 | """Outputs the graph using the graphviz "dot" format.""" 26 | 27 | out.write("digraph %s {\n" % name) 28 | for step, deps in each_step(graph): 29 | for dep in deps: 30 | out.write(" \"%s\" -> \"%s\";\n" % (step, dep)) 31 | 32 | out.write("}\n") 33 | 34 | 35 | def json_format(out, graph): 36 | """Outputs the graph in a machine readable JSON format.""" 37 | steps = {} 38 | for step, deps in each_step(graph): 39 | steps[step.name] = {} 40 | steps[step.name]["deps"] = [dep.name for dep in deps] 41 | 42 | json.dump({"steps": steps}, out, indent=4) 43 | out.write("\n") 44 | 45 | 46 | FORMATTERS = { 47 | "dot": dot_format, 48 | "json": json_format, 49 | } 50 | 51 | 52 | class Action(BaseAction): 53 | 54 | def _generate_plan(self): 55 | return plan( 56 | description="Print graph", 57 | stack_action=None, 58 | context=self.context) 59 | 60 | def run(self, format=None, reduce=False, *args, **kwargs): 61 | """Generates the underlying graph and prints it. 62 | 63 | """ 64 | plan = self._generate_plan() 65 | if reduce: 66 | # This will performa a transitive reduction on the underlying 67 | # graph, producing less edges. Mostly useful for the "dot" format, 68 | # when converting to PNG, so it creates a prettier/cleaner 69 | # dependency graph. 70 | plan.graph.transitive_reduction() 71 | 72 | fn = FORMATTERS[format] 73 | fn(sys.stdout, plan.graph) 74 | sys.stdout.flush() 75 | -------------------------------------------------------------------------------- /stacker/actions/info.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .base import BaseAction 4 | from .. import exceptions 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class Action(BaseAction): 10 | """Get information on CloudFormation stacks. 11 | 12 | Displays the outputs for the set of CloudFormation stacks. 13 | 14 | """ 15 | 16 | def run(self, *args, **kwargs): 17 | logger.info('Outputs for stacks: %s', self.context.get_fqn()) 18 | if not self.context.get_stacks(): 19 | logger.warn('WARNING: No stacks detected (error in config?)') 20 | for stack in self.context.get_stacks(): 21 | provider = self.build_provider(stack) 22 | 23 | try: 24 | provider_stack = provider.get_stack(stack.fqn) 25 | except exceptions.StackDoesNotExist: 26 | logger.info('Stack "%s" does not exist.' % (stack.fqn,)) 27 | continue 28 | 29 | logger.info('%s:', stack.fqn) 30 | if 'Outputs' in provider_stack: 31 | for output in provider_stack['Outputs']: 32 | logger.info( 33 | '\t%s: %s', 34 | output['OutputKey'], 35 | output['OutputValue'] 36 | ) 37 | -------------------------------------------------------------------------------- /stacker/awscli_yamlhelper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | import json 14 | import yaml 15 | from yaml.resolver import ScalarNode, SequenceNode 16 | 17 | from botocore.compat import six 18 | 19 | 20 | def intrinsics_multi_constructor(loader, tag_prefix, node): 21 | """ 22 | YAML constructor to parse CloudFormation intrinsics. 23 | This will return a dictionary with key being the instrinsic name 24 | """ 25 | 26 | # Get the actual tag name excluding the first exclamation 27 | tag = node.tag[1:] 28 | 29 | # Some intrinsic functions doesn't support prefix "Fn::" 30 | prefix = "Fn::" 31 | if tag in ["Ref", "Condition"]: 32 | prefix = "" 33 | 34 | cfntag = prefix + tag 35 | 36 | if tag == "GetAtt" and isinstance(node.value, six.string_types): 37 | # ShortHand notation for !GetAtt accepts Resource.Attribute format 38 | # while the standard notation is to use an array 39 | # [Resource, Attribute]. Convert shorthand to standard format 40 | value = node.value.split(".", 1) 41 | 42 | elif isinstance(node, ScalarNode): 43 | # Value of this node is scalar 44 | value = loader.construct_scalar(node) 45 | 46 | elif isinstance(node, SequenceNode): 47 | # Value of this node is an array (Ex: [1,2]) 48 | value = loader.construct_sequence(node) 49 | 50 | else: 51 | # Value of this node is an mapping (ex: {foo: bar}) 52 | value = loader.construct_mapping(node) 53 | 54 | return {cfntag: value} 55 | 56 | 57 | def yaml_dump(dict_to_dump): 58 | """ 59 | Dumps the dictionary as a YAML document 60 | :param dict_to_dump: 61 | :return: 62 | """ 63 | return yaml.safe_dump(dict_to_dump, default_flow_style=False) 64 | 65 | 66 | def yaml_parse(yamlstr): 67 | """Parse a yaml string""" 68 | try: 69 | # PyYAML doesn't support json as well as it should, so if the input 70 | # is actually just json it is better to parse it with the standard 71 | # json parser. 72 | return json.loads(yamlstr) 73 | except ValueError: 74 | yaml.SafeLoader.add_multi_constructor( 75 | "!", intrinsics_multi_constructor) 76 | return yaml.safe_load(yamlstr) 77 | -------------------------------------------------------------------------------- /stacker/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/blueprints/__init__.py -------------------------------------------------------------------------------- /stacker/blueprints/variables/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/blueprints/variables/__init__.py -------------------------------------------------------------------------------- /stacker/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .stacker import Stacker # NOQA 2 | -------------------------------------------------------------------------------- /stacker/commands/stacker/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .build import Build 4 | from .destroy import Destroy 5 | from .info import Info 6 | from .diff import Diff 7 | from .graph import Graph 8 | from .base import BaseCommand 9 | from ...config import render_parse_load as load_config 10 | from ...context import Context 11 | from ...providers.aws import default 12 | from ... import __version__ 13 | from ... import session_cache 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class Stacker(BaseCommand): 19 | 20 | name = "stacker" 21 | subcommands = (Build, Destroy, Info, Diff, Graph) 22 | 23 | def configure(self, options, **kwargs): 24 | 25 | session_cache.default_profile = options.profile 26 | 27 | self.config = load_config( 28 | options.config.read(), 29 | environment=options.environment, 30 | validate=True, 31 | ) 32 | 33 | options.provider_builder = default.ProviderBuilder( 34 | region=options.region, 35 | interactive=options.interactive, 36 | replacements_only=options.replacements_only, 37 | recreate_failed=options.recreate_failed, 38 | service_role=self.config.service_role, 39 | ) 40 | 41 | options.context = Context( 42 | environment=options.environment, 43 | config=self.config, 44 | # Allow subcommands to provide any specific kwargs to the Context 45 | # that it wants. 46 | **options.get_context_kwargs(options) 47 | ) 48 | 49 | super(Stacker, self).configure(options, **kwargs) 50 | if options.interactive: 51 | logger.info("Using interactive AWS provider mode.") 52 | else: 53 | logger.info("Using default AWS provider mode") 54 | 55 | def add_arguments(self, parser): 56 | parser.add_argument("--version", action="version", 57 | version="%%(prog)s %s" % (__version__,)) 58 | -------------------------------------------------------------------------------- /stacker/commands/stacker/build.py: -------------------------------------------------------------------------------- 1 | """Launches or updates CloudFormation stacks based on the given config. 2 | 3 | Stacker is smart enough to figure out if anything (the template or parameters) 4 | have changed for a given stack. If nothing has changed, stacker will correctly 5 | skip executing anything against the stack. 6 | 7 | """ 8 | 9 | from .base import BaseCommand, cancel 10 | from ...actions import build 11 | 12 | 13 | class Build(BaseCommand): 14 | 15 | name = "build" 16 | description = __doc__ 17 | 18 | def add_arguments(self, parser): 19 | super(Build, self).add_arguments(parser) 20 | parser.add_argument("-o", "--outline", action="store_true", 21 | help="Print an outline of what steps will be " 22 | "taken to build the stacks") 23 | parser.add_argument("--force", action="append", default=[], 24 | metavar="STACKNAME", type=str, 25 | help="If a stackname is provided to --force, it " 26 | "will be updated, even if it is locked in " 27 | "the config.") 28 | parser.add_argument("--targets", "--stacks", action="append", 29 | metavar="STACKNAME", type=str, 30 | help="Only work on the stacks given, and their " 31 | "dependencies. Can be specified more than " 32 | "once. If not specified then stacker will " 33 | "work on all stacks in the config file.") 34 | parser.add_argument("-j", "--max-parallel", action="store", type=int, 35 | default=0, 36 | help="The maximum number of stacks to execute in " 37 | "parallel. If not provided, the value will " 38 | "be constrained based on the underlying " 39 | "graph.") 40 | parser.add_argument("-t", "--tail", action="store_true", 41 | help="Tail the CloudFormation logs while working " 42 | "with stacks") 43 | parser.add_argument("-d", "--dump", action="store", type=str, 44 | help="Dump the rendered Cloudformation templates " 45 | "to a directory") 46 | 47 | def run(self, options, **kwargs): 48 | super(Build, self).run(options, **kwargs) 49 | action = build.Action(options.context, 50 | provider_builder=options.provider_builder, 51 | cancel=cancel()) 52 | action.execute(concurrency=options.max_parallel, 53 | outline=options.outline, 54 | tail=options.tail, 55 | dump=options.dump) 56 | 57 | def get_context_kwargs(self, options, **kwargs): 58 | return {"stack_names": options.targets, "force_stacks": options.force} 59 | -------------------------------------------------------------------------------- /stacker/commands/stacker/destroy.py: -------------------------------------------------------------------------------- 1 | """Destroys CloudFormation stacks based on the given config. 2 | 3 | Stacker will determine the order in which stacks should be destroyed based on 4 | any manual requirements they specify or output values they rely on from other 5 | stacks. 6 | 7 | """ 8 | from .base import BaseCommand, cancel 9 | from ...actions import destroy 10 | 11 | 12 | class Destroy(BaseCommand): 13 | 14 | name = "destroy" 15 | description = __doc__ 16 | 17 | def add_arguments(self, parser): 18 | super(Destroy, self).add_arguments(parser) 19 | parser.add_argument("-f", "--force", action="store_true", 20 | help="Whether or not you want to go through " 21 | " with destroying the stacks") 22 | parser.add_argument("--targets", "--stacks", action="append", 23 | metavar="STACKNAME", type=str, 24 | help="Only work on the stacks given. Can be " 25 | "specified more than once. If not specified " 26 | "then stacker will work on all stacks in the " 27 | "config file.") 28 | parser.add_argument("-j", "--max-parallel", action="store", type=int, 29 | default=0, 30 | help="The maximum number of stacks to execute in " 31 | "parallel. If not provided, the value will " 32 | "be constrained based on the underlying " 33 | "graph.") 34 | parser.add_argument("-t", "--tail", action="store_true", 35 | help="Tail the CloudFormation logs while working " 36 | "with stacks") 37 | 38 | def run(self, options, **kwargs): 39 | super(Destroy, self).run(options, **kwargs) 40 | action = destroy.Action(options.context, 41 | provider_builder=options.provider_builder, 42 | cancel=cancel()) 43 | action.execute(concurrency=options.max_parallel, 44 | force=options.force, 45 | tail=options.tail) 46 | 47 | def get_context_kwargs(self, options, **kwargs): 48 | return {"stack_names": options.targets} 49 | -------------------------------------------------------------------------------- /stacker/commands/stacker/diff.py: -------------------------------------------------------------------------------- 1 | """ Diffs the config against the currently running CloudFormation stacks 2 | 3 | Sometimes small changes can have big impacts. Run "stacker diff" before 4 | "stacker build" to detect bad things(tm) from happening in advance! 5 | """ 6 | 7 | from .base import BaseCommand 8 | from ...actions import diff 9 | 10 | 11 | class Diff(BaseCommand): 12 | name = "diff" 13 | description = __doc__ 14 | 15 | def add_arguments(self, parser): 16 | super(Diff, self).add_arguments(parser) 17 | parser.add_argument("--force", action="append", default=[], 18 | metavar="STACKNAME", type=str, 19 | help="If a stackname is provided to --force, it " 20 | "will be diffed, even if it is locked in " 21 | "the config.") 22 | parser.add_argument("--stacks", action="append", 23 | metavar="STACKNAME", type=str, 24 | help="Only work on the stacks given. Can be " 25 | "specified more than once. If not specified " 26 | "then stacker will work on all stacks in the " 27 | "config file.") 28 | 29 | def run(self, options, **kwargs): 30 | super(Diff, self).run(options, **kwargs) 31 | action = diff.Action(options.context, 32 | provider_builder=options.provider_builder) 33 | action.execute() 34 | 35 | def get_context_kwargs(self, options, **kwargs): 36 | return {"stack_names": options.stacks, "force_stacks": options.force} 37 | -------------------------------------------------------------------------------- /stacker/commands/stacker/graph.py: -------------------------------------------------------------------------------- 1 | """Prints the the relationships between steps as a graph. 2 | 3 | """ 4 | 5 | from .base import BaseCommand 6 | from ...actions import graph 7 | 8 | 9 | class Graph(BaseCommand): 10 | 11 | name = "graph" 12 | description = __doc__ 13 | 14 | def add_arguments(self, parser): 15 | super(Graph, self).add_arguments(parser) 16 | parser.add_argument("-f", "--format", default="dot", 17 | choices=graph.FORMATTERS, 18 | help="The format to print the graph in.") 19 | parser.add_argument("--reduce", action="store_true", 20 | help="When provided, this will create a " 21 | "graph with less edges, by performing " 22 | "a transitive reduction on the underlying " 23 | "graph. While this will produce a less " 24 | "noisy graph, it is slower.") 25 | 26 | def run(self, options, **kwargs): 27 | super(Graph, self).run(options, **kwargs) 28 | action = graph.Action(options.context, 29 | provider_builder=options.provider_builder) 30 | action.execute( 31 | format=options.format, 32 | reduce=options.reduce) 33 | -------------------------------------------------------------------------------- /stacker/commands/stacker/info.py: -------------------------------------------------------------------------------- 1 | """Gets information on the CloudFormation stacks based on the given config.""" 2 | 3 | from .base import BaseCommand 4 | from ...actions import info 5 | 6 | 7 | class Info(BaseCommand): 8 | 9 | name = "info" 10 | description = __doc__ 11 | 12 | def add_arguments(self, parser): 13 | super(Info, self).add_arguments(parser) 14 | parser.add_argument("--stacks", action="append", 15 | metavar="STACKNAME", type=str, 16 | help="Only work on the stacks given. Can be " 17 | "specified more than once. If not specified " 18 | "then stacker will work on all stacks in the " 19 | "config file.") 20 | 21 | def run(self, options, **kwargs): 22 | super(Info, self).run(options, **kwargs) 23 | action = info.Action(options.context, 24 | provider_builder=options.provider_builder) 25 | 26 | action.execute() 27 | 28 | def get_context_kwargs(self, options, **kwargs): 29 | return {"stack_names": options.stacks} 30 | -------------------------------------------------------------------------------- /stacker/config/translators/__init__.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | from .kms import kms_simple_constructor 4 | 5 | yaml.add_constructor('!kms', kms_simple_constructor) 6 | -------------------------------------------------------------------------------- /stacker/config/translators/kms.py: -------------------------------------------------------------------------------- 1 | # NOTE: The translator is going to be deprecated in favor of the lookup 2 | from ...lookups.handlers.kms import KmsLookup 3 | 4 | 5 | def kms_simple_constructor(loader, node): 6 | value = loader.construct_scalar(node) 7 | return KmsLookup.handler(value) 8 | -------------------------------------------------------------------------------- /stacker/environment.py: -------------------------------------------------------------------------------- 1 | 2 | import yaml 3 | 4 | 5 | class DictWithSourceType(dict): 6 | """An environment dict which keeps track of its source. 7 | 8 | Environment files may be loaded from simple key/value files, or from 9 | structured YAML files, and we need to render them using a different 10 | strategy based on their source. This class adds a source_type property 11 | to a dict which keeps track of whether the source for the dict is 12 | yaml or simple. 13 | """ 14 | def __init__(self, source_type, *args): 15 | dict.__init__(self, args) 16 | if source_type not in ['yaml', 'simple']: 17 | raise ValueError('source_type must be yaml or simple') 18 | self.source_type = source_type 19 | 20 | 21 | def parse_environment(raw_environment): 22 | environment = DictWithSourceType('simple') 23 | for line in raw_environment.split('\n'): 24 | line = line.strip() 25 | if not line: 26 | continue 27 | 28 | if line.startswith('#'): 29 | continue 30 | 31 | try: 32 | key, value = line.split(':', 1) 33 | except ValueError: 34 | raise ValueError('Environment must be in key: value format') 35 | 36 | environment[key] = value.strip() 37 | return environment 38 | 39 | 40 | def parse_yaml_environment(raw_environment): 41 | environment = DictWithSourceType('yaml') 42 | parsed_env = yaml.safe_load(raw_environment) 43 | 44 | if type(parsed_env) != dict: 45 | raise ValueError('Environment must be valid YAML') 46 | environment.update(parsed_env) 47 | return environment 48 | -------------------------------------------------------------------------------- /stacker/hooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/hooks/__init__.py -------------------------------------------------------------------------------- /stacker/hooks/ecs.py: -------------------------------------------------------------------------------- 1 | # A lot of this code exists to deal w/ the broken ECS connect_to_region 2 | # function, and will be removed once this pull request is accepted: 3 | # https://github.com/boto/boto/pull/3143 4 | from past.builtins import basestring 5 | import logging 6 | 7 | from stacker.session_cache import get_session 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def create_clusters(provider, context, **kwargs): 13 | """Creates ECS clusters. 14 | 15 | Expects a "clusters" argument, which should contain a list of cluster 16 | names to create. 17 | 18 | Args: 19 | provider (:class:`stacker.providers.base.BaseProvider`): provider 20 | instance 21 | context (:class:`stacker.context.Context`): context instance 22 | 23 | Returns: boolean for whether or not the hook succeeded. 24 | 25 | """ 26 | conn = get_session(provider.region).client('ecs') 27 | 28 | try: 29 | clusters = kwargs["clusters"] 30 | except KeyError: 31 | logger.error("setup_clusters hook missing \"clusters\" argument") 32 | return False 33 | 34 | if isinstance(clusters, basestring): 35 | clusters = [clusters] 36 | 37 | cluster_info = {} 38 | for cluster in clusters: 39 | logger.debug("Creating ECS cluster: %s", cluster) 40 | r = conn.create_cluster(clusterName=cluster) 41 | cluster_info[r["cluster"]["clusterName"]] = r 42 | return {"clusters": cluster_info} 43 | -------------------------------------------------------------------------------- /stacker/hooks/route53.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from stacker.session_cache import get_session 4 | 5 | from stacker.util import create_route53_zone 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def create_domain(provider, context, **kwargs): 11 | """Create a domain within route53. 12 | 13 | Args: 14 | provider (:class:`stacker.providers.base.BaseProvider`): provider 15 | instance 16 | context (:class:`stacker.context.Context`): context instance 17 | 18 | Returns: boolean for whether or not the hook succeeded. 19 | 20 | """ 21 | session = get_session(provider.region) 22 | client = session.client("route53") 23 | domain = kwargs.get("domain") 24 | if not domain: 25 | logger.error("domain argument or BaseDomain variable not provided.") 26 | return False 27 | zone_id = create_route53_zone(client, domain) 28 | return {"domain": domain, "zone_id": zone_id} 29 | -------------------------------------------------------------------------------- /stacker/hooks/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import collections.abc 4 | import logging 5 | 6 | from stacker.util import load_object_from_string 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def full_path(path): 12 | return os.path.abspath(os.path.expanduser(path)) 13 | 14 | 15 | def handle_hooks(stage, hooks, provider, context): 16 | """ Used to handle pre/post_build hooks. 17 | 18 | These are pieces of code that we want to run before/after the builder 19 | builds the stacks. 20 | 21 | Args: 22 | stage (string): The current stage (pre_run, post_run, etc). 23 | hooks (list): A list of :class:`stacker.config.Hook` containing the 24 | hooks to execute. 25 | provider (:class:`stacker.provider.base.BaseProvider`): The provider 26 | the current stack is using. 27 | context (:class:`stacker.context.Context`): The current stacker 28 | context. 29 | """ 30 | if not hooks: 31 | logger.debug("No %s hooks defined.", stage) 32 | return 33 | 34 | hook_paths = [] 35 | for i, h in enumerate(hooks): 36 | try: 37 | hook_paths.append(h.path) 38 | except KeyError: 39 | raise ValueError("%s hook #%d missing path." % (stage, i)) 40 | 41 | logger.info("Executing %s hooks: %s", stage, ", ".join(hook_paths)) 42 | for hook in hooks: 43 | data_key = hook.data_key 44 | required = hook.required 45 | kwargs = hook.args or {} 46 | enabled = hook.enabled 47 | if not enabled: 48 | logger.debug("hook with method %s is disabled, skipping", 49 | hook.path) 50 | continue 51 | try: 52 | method = load_object_from_string(hook.path) 53 | except (AttributeError, ImportError): 54 | logger.exception("Unable to load method at %s:", hook.path) 55 | if required: 56 | raise 57 | continue 58 | try: 59 | result = method(context=context, provider=provider, **kwargs) 60 | except Exception: 61 | logger.exception("Method %s threw an exception:", hook.path) 62 | if required: 63 | raise 64 | continue 65 | if not result: 66 | if required: 67 | logger.error("Required hook %s failed. Return value: %s", 68 | hook.path, result) 69 | sys.exit(1) 70 | logger.warning("Non-required hook %s failed. Return value: %s", 71 | hook.path, result) 72 | else: 73 | if isinstance(result, collections.abc.Mapping): 74 | if data_key: 75 | logger.debug("Adding result for hook %s to context in " 76 | "data_key %s.", hook.path, data_key) 77 | context.set_hook_data(data_key, result) 78 | else: 79 | logger.debug("Hook %s returned result data, but no data " 80 | "key set, so ignoring.", hook.path) 81 | -------------------------------------------------------------------------------- /stacker/logger/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | DEBUG_FORMAT = ("[%(asctime)s] %(levelname)s %(threadName)s " 5 | "%(name)s:%(lineno)d(%(funcName)s): %(message)s") 6 | INFO_FORMAT = ("[%(asctime)s] %(message)s") 7 | COLOR_FORMAT = ("[%(asctime)s] \033[%(color)sm%(message)s\033[39m") 8 | 9 | ISO_8601 = "%Y-%m-%dT%H:%M:%S" 10 | 11 | 12 | class ColorFormatter(logging.Formatter): 13 | """ Handles colorizing formatted log messages if color provided. """ 14 | def format(self, record): 15 | if 'color' not in record.__dict__: 16 | record.__dict__['color'] = 37 17 | msg = super(ColorFormatter, self).format(record) 18 | return msg 19 | 20 | 21 | def setup_logging(verbosity, formats=None): 22 | """ 23 | Configure a proper logger based on verbosity and optional log formats. 24 | 25 | Args: 26 | verbosity (int): 0, 1, 2 27 | formats (dict): Optional, looks for `info`, `color`, and `debug` keys 28 | which may override the associated default log formats. 29 | """ 30 | if formats is None: 31 | formats = {} 32 | 33 | log_level = logging.INFO 34 | 35 | log_format = formats.get("info", INFO_FORMAT) 36 | 37 | if sys.stdout.isatty(): 38 | log_format = formats.get("color", COLOR_FORMAT) 39 | 40 | if verbosity > 0: 41 | log_level = logging.DEBUG 42 | log_format = formats.get("debug", DEBUG_FORMAT) 43 | 44 | if verbosity < 2: 45 | logging.getLogger("botocore").setLevel(logging.CRITICAL) 46 | 47 | hdlr = logging.StreamHandler() 48 | hdlr.setFormatter(ColorFormatter(log_format, ISO_8601)) 49 | logging.root.addHandler(hdlr) 50 | logging.root.setLevel(log_level) 51 | -------------------------------------------------------------------------------- /stacker/lookups/__init__.py: -------------------------------------------------------------------------------- 1 | from past.builtins import basestring 2 | from collections import namedtuple 3 | import re 4 | 5 | # export resolve_lookups at this level 6 | from .registry import resolve_lookups # NOQA 7 | from .registry import register_lookup_handler # NOQA 8 | 9 | # TODO: we can remove the optionality of of the type in a later release, it 10 | # is only included to allow for an error to be thrown while people are 11 | # converting their configuration files to 1.0 12 | 13 | LOOKUP_REGEX = re.compile(""" 14 | \$\{ # opening brace for the lookup 15 | ((?P[._\-a-zA-Z0-9]*(?=\s)) # type of lookup, must be followed by a 16 | # space 17 | ?\s* # any number of spaces separating the 18 | # type from the input 19 | (?P[@\+\/,\.\?_\-a-zA-Z0-9\:\s=\[\]\*]+) # the input value to the lookup 20 | )\} # closing brace of the lookup 21 | """, re.VERBOSE) 22 | 23 | Lookup = namedtuple("Lookup", ("type", "input", "raw")) 24 | 25 | 26 | def extract_lookups_from_string(value): 27 | """Extract any lookups within a string. 28 | 29 | Args: 30 | value (str): string value we're extracting lookups from 31 | 32 | Returns: 33 | list: list of :class:`stacker.lookups.Lookup` if any 34 | 35 | """ 36 | lookups = set() 37 | for match in LOOKUP_REGEX.finditer(value): 38 | groupdict = match.groupdict() 39 | raw = match.groups()[0] 40 | lookup_type = groupdict["type"] 41 | lookup_input = groupdict["input"] 42 | lookups.add(Lookup(lookup_type, lookup_input, raw)) 43 | return lookups 44 | 45 | 46 | def extract_lookups(value): 47 | """Recursively extracts any stack lookups within the data structure. 48 | 49 | Args: 50 | value (one of str, list, dict): a structure that contains lookups to 51 | output values 52 | 53 | Returns: 54 | list: list of lookups if any 55 | 56 | """ 57 | lookups = set() 58 | if isinstance(value, basestring): 59 | lookups = lookups.union(extract_lookups_from_string(value)) 60 | elif isinstance(value, list): 61 | for v in value: 62 | lookups = lookups.union(extract_lookups(v)) 63 | elif isinstance(value, dict): 64 | for v in value.values(): 65 | lookups = lookups.union(extract_lookups(v)) 66 | return lookups 67 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class LookupHandler(object): 4 | @classmethod 5 | def handle(cls, value, context, provider): 6 | """ 7 | Perform the actual lookup 8 | 9 | :param value: Parameter(s) given to this lookup 10 | :type value: str 11 | :param context: 12 | :param provider: 13 | :return: Looked-up value 14 | :rtype: str 15 | """ 16 | raise NotImplementedError() 17 | 18 | @classmethod 19 | def dependencies(cls, lookup_data): 20 | """ 21 | Calculate any dependencies required to perform this lookup. 22 | 23 | Note that lookup_data may not be (completely) resolved at this time. 24 | 25 | :param lookup_data: Parameter(s) given to this lookup 26 | :type lookup_data VariableValue 27 | :return: Set of stack names (str) this lookup depends on 28 | :rtype: set 29 | """ 30 | del lookup_data # unused in this implementation 31 | return set() 32 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/ami.py: -------------------------------------------------------------------------------- 1 | from stacker.session_cache import get_session 2 | import re 3 | import operator 4 | 5 | from . import LookupHandler 6 | from ...util import read_value_from_path 7 | 8 | TYPE_NAME = "ami" 9 | 10 | 11 | class ImageNotFound(Exception): 12 | def __init__(self, search_string): 13 | self.search_string = search_string 14 | message = ("Unable to find ec2 image with search string: {}").format( 15 | search_string 16 | ) 17 | super(ImageNotFound, self).__init__(message) 18 | 19 | 20 | class AmiLookup(LookupHandler): 21 | @classmethod 22 | def handle(cls, value, provider, **kwargs): 23 | """Fetch the most recent AMI Id using a filter 24 | 25 | For example: 26 | 27 | ${ami [@]owners:self,account,amazon name_regex:serverX-[0-9]+ architecture:x64,i386} 28 | 29 | The above fetches the most recent AMI where owner is self 30 | account or amazon and the ami name matches the regex described, 31 | the architecture will be either x64 or i386 32 | 33 | You can also optionally specify the region in which to perform the 34 | AMI lookup. 35 | 36 | Valid arguments: 37 | 38 | owners (comma delimited) REQUIRED ONCE: 39 | aws_account_id | amazon | self 40 | 41 | name_regex (a regex) REQUIRED ONCE: 42 | e.g. my-ubuntu-server-[0-9]+ 43 | 44 | executable_users (comma delimited) OPTIONAL ONCE: 45 | aws_account_id | amazon | self 46 | 47 | Any other arguments specified are sent as filters to the aws api 48 | For example, "architecture:x86_64" will add a filter 49 | """ # noqa 50 | value = read_value_from_path(value) 51 | 52 | if "@" in value: 53 | region, value = value.split("@", 1) 54 | else: 55 | region = provider.region 56 | 57 | ec2 = get_session(region).client('ec2') 58 | 59 | values = {} 60 | describe_args = {} 61 | 62 | # now find any other arguments that can be filters 63 | matches = re.findall('([0-9a-zA-z_-]+:[^\s$]+)', value) 64 | for match in matches: 65 | k, v = match.split(':', 1) 66 | values[k] = v 67 | 68 | if not values.get('owners'): 69 | raise Exception("'owners' value required when using ami") 70 | owners = values.pop('owners').split(',') 71 | describe_args["Owners"] = owners 72 | 73 | if not values.get('name_regex'): 74 | raise Exception("'name_regex' value required when using ami") 75 | name_regex = values.pop('name_regex') 76 | 77 | executable_users = None 78 | if values.get('executable_users'): 79 | executable_users = values.pop('executable_users').split(',') 80 | describe_args["ExecutableUsers"] = executable_users 81 | 82 | filters = [] 83 | for k, v in values.items(): 84 | filters.append({"Name": k, "Values": v.split(',')}) 85 | describe_args["Filters"] = filters 86 | 87 | result = ec2.describe_images(**describe_args) 88 | 89 | images = sorted(result['Images'], 90 | key=operator.itemgetter('CreationDate'), 91 | reverse=True) 92 | for image in images: 93 | if re.match("^%s$" % name_regex, image.get('Name', '')): 94 | return image['ImageId'] 95 | 96 | raise ImageNotFound(value) 97 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/default.py: -------------------------------------------------------------------------------- 1 | 2 | from . import LookupHandler 3 | 4 | 5 | TYPE_NAME = "default" 6 | 7 | 8 | class DefaultLookup(LookupHandler): 9 | @classmethod 10 | def handle(cls, value, **kwargs): 11 | """Use a value from the environment or fall back to a default if the 12 | environment doesn't contain the variable. 13 | 14 | Format of value: 15 | 16 | :: 17 | 18 | For example: 19 | 20 | Groups: ${default app_security_groups::sg-12345,sg-67890} 21 | 22 | If `app_security_groups` is defined in the environment, its defined 23 | value will be returned. Otherwise, `sg-12345,sg-67890` will be the 24 | returned value. 25 | 26 | This allows defaults to be set at the config file level. 27 | """ 28 | 29 | try: 30 | env_var_name, default_val = value.split("::", 1) 31 | except ValueError: 32 | raise ValueError("Invalid value for default: %s. Must be in " 33 | ":: format." % value) 34 | 35 | if env_var_name in kwargs['context'].environment: 36 | return kwargs['context'].environment[env_var_name] 37 | else: 38 | return default_val 39 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/envvar.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import LookupHandler 4 | from ...util import read_value_from_path 5 | 6 | TYPE_NAME = "envvar" 7 | 8 | 9 | class EnvvarLookup(LookupHandler): 10 | @classmethod 11 | def handle(cls, value, **kwargs): 12 | """Retrieve an environment variable. 13 | 14 | For example: 15 | 16 | # In stacker we would reference the environment variable like this: 17 | conf_key: ${envvar ENV_VAR_NAME} 18 | 19 | You can optionally store the value in a file, ie: 20 | 21 | $ cat envvar_value.txt 22 | ENV_VAR_NAME 23 | 24 | and reference it within stacker (NOTE: the path should be relative 25 | to the stacker config file): 26 | 27 | conf_key: ${envvar file://envvar_value.txt} 28 | 29 | # Both of the above would resolve to 30 | conf_key: ENV_VALUE 31 | """ 32 | value = read_value_from_path(value) 33 | 34 | try: 35 | return os.environ[value] 36 | except KeyError: 37 | raise ValueError('EnvVar "{}" does not exist'.format(value)) 38 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/hook_data.py: -------------------------------------------------------------------------------- 1 | 2 | from . import LookupHandler 3 | 4 | 5 | TYPE_NAME = "hook_data" 6 | 7 | 8 | class HookDataLookup(LookupHandler): 9 | @classmethod 10 | def handle(cls, value, context, **kwargs): 11 | """Returns the value of a key for a given hook in hook_data. 12 | 13 | Format of value: 14 | 15 | :: 16 | """ 17 | try: 18 | hook_name, key = value.split("::") 19 | except ValueError: 20 | raise ValueError("Invalid value for hook_data: %s. Must be in " 21 | ":: format." % value) 22 | 23 | return context.hook_data[hook_name][key] 24 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/kms.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import sys 3 | from stacker.session_cache import get_session 4 | 5 | from . import LookupHandler 6 | from ...util import read_value_from_path 7 | 8 | TYPE_NAME = "kms" 9 | 10 | 11 | class KmsLookup(LookupHandler): 12 | @classmethod 13 | def handle(cls, value, **kwargs): 14 | """Decrypt the specified value with a master key in KMS. 15 | 16 | kmssimple field types should be in the following format: 17 | 18 | [@] 19 | 20 | Note: The region is optional, and defaults to the environment's 21 | `AWS_DEFAULT_REGION` if not specified. 22 | 23 | For example: 24 | 25 | # We use the aws cli to get the encrypted value for the string 26 | # "PASSWORD" using the master key called "myStackerKey" in 27 | # us-east-1 28 | $ aws --region us-east-1 kms encrypt --key-id alias/myStackerKey \ 29 | --plaintext "PASSWORD" --output text --query CiphertextBlob 30 | 31 | CiD6bC8t2Y<...encrypted blob...> 32 | 33 | # In stacker we would reference the encrypted value like: 34 | conf_key: ${kms us-east-1@CiD6bC8t2Y<...encrypted blob...>} 35 | 36 | You can optionally store the encrypted value in a file, ie: 37 | 38 | kms_value.txt 39 | us-east-1@CiD6bC8t2Y<...encrypted blob...> 40 | 41 | and reference it within stacker (NOTE: the path should be relative 42 | to the stacker config file): 43 | 44 | conf_key: ${kms file://kms_value.txt} 45 | 46 | # Both of the above would resolve to 47 | conf_key: PASSWORD 48 | 49 | """ 50 | value = read_value_from_path(value) 51 | 52 | region = None 53 | if "@" in value: 54 | region, value = value.split("@", 1) 55 | 56 | kms = get_session(region).client('kms') 57 | 58 | # encode str value as an utf-8 bytestring for use with codecs.decode. 59 | value = value.encode('utf-8') 60 | 61 | # get raw but still encrypted value from base64 version. 62 | decoded = codecs.decode(value, 'base64') 63 | 64 | # check python version in your system 65 | python3_or_later = sys.version_info[0] >= 3 66 | 67 | # decrypt and return the plain text raw value. 68 | if python3_or_later: 69 | return kms.decrypt(CiphertextBlob=decoded)["Plaintext"]\ 70 | .decode('utf-8') 71 | else: 72 | return kms.decrypt(CiphertextBlob=decoded)["Plaintext"] 73 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/output.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | from collections import namedtuple 4 | 5 | from . import LookupHandler 6 | 7 | TYPE_NAME = "output" 8 | 9 | Output = namedtuple("Output", ("stack_name", "output_name")) 10 | 11 | 12 | class OutputLookup(LookupHandler): 13 | @classmethod 14 | def handle(cls, value, context=None, **kwargs): 15 | """Fetch an output from the designated stack. 16 | 17 | Args: 18 | value (str): string with the following format: 19 | ::, ie. some-stack::SomeOutput 20 | context (:class:`stacker.context.Context`): stacker context 21 | 22 | Returns: 23 | str: output from the specified stack 24 | 25 | """ 26 | 27 | if context is None: 28 | raise ValueError('Context is required') 29 | 30 | d = deconstruct(value) 31 | stack = context.get_stack(d.stack_name) 32 | return stack.outputs[d.output_name] 33 | 34 | @classmethod 35 | def dependencies(cls, lookup_data): 36 | # try to get the stack name 37 | stack_name = '' 38 | for data_item in lookup_data: 39 | if not data_item.resolved(): 40 | # We encountered an unresolved substitution. 41 | # StackName is calculated dynamically based on context: 42 | # e.g. ${output ${default var::source}::name} 43 | # Stop here 44 | return set() 45 | stack_name = stack_name + data_item.value() 46 | match = re.search(r'::', stack_name) 47 | if match: 48 | stack_name = stack_name[0:match.start()] 49 | return {stack_name} 50 | # else: try to append the next item 51 | 52 | # We added all lookup_data, and still couldn't find a `::`... 53 | # Probably an error... 54 | return set() 55 | 56 | 57 | def deconstruct(value): 58 | 59 | try: 60 | stack_name, output_name = value.split("::") 61 | except ValueError: 62 | raise ValueError("output handler requires syntax " 63 | "of ::. Got: %s" % value) 64 | 65 | return Output(stack_name, output_name) 66 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/rxref.py: -------------------------------------------------------------------------------- 1 | """Handler for fetching outputs from fully qualified stacks. 2 | 3 | The `output` handler supports fetching outputs from stacks created within a 4 | sigle config file. Sometimes it's useful to fetch outputs from stacks created 5 | outside of the current config file. `rxref` supports this by not using the 6 | :class:`stacker.context.Context` to expand the fqn of the stack. 7 | 8 | Example: 9 | 10 | conf_value: ${rxref 11 | some-relative-fully-qualified-stack-name::SomeOutputName} 12 | 13 | """ 14 | from . import LookupHandler 15 | from .output import deconstruct 16 | 17 | TYPE_NAME = "rxref" 18 | 19 | 20 | class RxrefLookup(LookupHandler): 21 | @classmethod 22 | def handle(cls, value, provider=None, context=None, **kwargs): 23 | """Fetch an output from the designated stack. 24 | 25 | Args: 26 | value (str): string with the following format: 27 | ::, ie. some-stack::SomeOutput 28 | provider (:class:`stacker.provider.base.BaseProvider`): subclass of 29 | the base provider 30 | context (:class:`stacker.context.Context`): stacker context 31 | 32 | Returns: 33 | str: output from the specified stack 34 | """ 35 | 36 | if provider is None: 37 | raise ValueError('Provider is required') 38 | if context is None: 39 | raise ValueError('Context is required') 40 | 41 | d = deconstruct(value) 42 | stack_fqn = context.get_fqn(d.stack_name) 43 | output = provider.get_output(stack_fqn, d.output_name) 44 | return output 45 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/split.py: -------------------------------------------------------------------------------- 1 | from . import LookupHandler 2 | TYPE_NAME = "split" 3 | 4 | 5 | class SplitLookup(LookupHandler): 6 | @classmethod 7 | def handle(cls, value, **kwargs): 8 | """Split the supplied string on the given delimiter, providing a list. 9 | 10 | Format of value: 11 | 12 | :: 13 | 14 | For example: 15 | 16 | Subnets: ${split ,::subnet-1,subnet-2,subnet-3} 17 | 18 | Would result in the variable `Subnets` getting a list consisting of: 19 | 20 | ["subnet-1", "subnet-2", "subnet-3"] 21 | 22 | This is particularly useful when getting an output from another stack 23 | that contains a list. For example, the standard vpc blueprint outputs 24 | the list of Subnets it creates as a pair of Outputs (PublicSubnets, 25 | PrivateSubnets) that are comma separated, so you could use this in your 26 | config: 27 | 28 | Subnets: ${split ,::${output vpc::PrivateSubnets}} 29 | """ 30 | 31 | try: 32 | delimiter, text = value.split("::", 1) 33 | except ValueError: 34 | raise ValueError("Invalid value for split: %s. Must be in " 35 | ":: format." % value) 36 | 37 | return text.split(delimiter) 38 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/ssmstore.py: -------------------------------------------------------------------------------- 1 | 2 | from stacker.session_cache import get_session 3 | 4 | from . import LookupHandler 5 | from ...util import read_value_from_path 6 | 7 | TYPE_NAME = "ssmstore" 8 | 9 | 10 | class SsmstoreLookup(LookupHandler): 11 | @classmethod 12 | def handle(cls, value, **kwargs): 13 | """Retrieve (and decrypt if applicable) a parameter from 14 | AWS SSM Parameter Store. 15 | 16 | ssmstore field types should be in the following format: 17 | 18 | [@]ssmkey 19 | 20 | Note: The region is optional, and defaults to us-east-1 if not given. 21 | 22 | For example: 23 | 24 | # In stacker we would reference the encrypted value like: 25 | conf_key: ${ssmstore us-east-1@ssmkey} 26 | 27 | You can optionally store the value in a file, ie: 28 | 29 | ssmstore_value.txt 30 | us-east-1@ssmkey 31 | 32 | and reference it within stacker (NOTE: the path should be relative 33 | to the stacker config file): 34 | 35 | conf_key: ${ssmstore file://ssmstore_value.txt} 36 | 37 | # Both of the above would resolve to 38 | conf_key: PASSWORD 39 | 40 | """ 41 | value = read_value_from_path(value) 42 | 43 | region = "us-east-1" 44 | if "@" in value: 45 | region, value = value.split("@", 1) 46 | 47 | client = get_session(region).client("ssm") 48 | response = client.get_parameters( 49 | Names=[ 50 | value, 51 | ], 52 | WithDecryption=True 53 | ) 54 | if 'Parameters' in response: 55 | return str(response['Parameters'][0]['Value']) 56 | 57 | raise ValueError('SSMKey "{}" does not exist in region {}'.format( 58 | value, region)) 59 | -------------------------------------------------------------------------------- /stacker/lookups/handlers/xref.py: -------------------------------------------------------------------------------- 1 | """Handler for fetching outputs from fully qualified stacks. 2 | 3 | The `output` handler supports fetching outputs from stacks created within a 4 | sigle config file. Sometimes it's useful to fetch outputs from stacks created 5 | outside of the current config file. `xref` supports this by not using the 6 | :class:`stacker.context.Context` to expand the fqn of the stack. 7 | 8 | Example: 9 | 10 | conf_value: ${xref some-fully-qualified-stack-name::SomeOutputName} 11 | 12 | """ 13 | from . import LookupHandler 14 | from .output import deconstruct 15 | 16 | TYPE_NAME = "xref" 17 | 18 | 19 | class XrefLookup(LookupHandler): 20 | @classmethod 21 | def handle(cls, value, provider=None, **kwargs): 22 | """Fetch an output from the designated stack. 23 | 24 | Args: 25 | value (str): string with the following format: 26 | ::, ie. some-stack::SomeOutput 27 | provider (:class:`stacker.provider.base.BaseProvider`): subclass of 28 | the base provider 29 | 30 | Returns: 31 | str: output from the specified stack 32 | """ 33 | 34 | if provider is None: 35 | raise ValueError('Provider is required') 36 | 37 | d = deconstruct(value) 38 | stack_fqn = d.stack_name 39 | output = provider.get_output(stack_fqn, d.output_name) 40 | return output 41 | -------------------------------------------------------------------------------- /stacker/lookups/registry.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import warnings 4 | 5 | from past.builtins import basestring 6 | 7 | from ..exceptions import UnknownLookupType, FailedVariableLookup 8 | from ..util import load_object_from_string 9 | 10 | from .handlers import output 11 | from .handlers import kms 12 | from .handlers import xref 13 | from .handlers import ssmstore 14 | from .handlers import dynamodb 15 | from .handlers import envvar 16 | from .handlers import rxref 17 | from .handlers import ami 18 | from .handlers import file as file_handler 19 | from .handlers import split 20 | from .handlers import default 21 | from .handlers import hook_data 22 | 23 | LOOKUP_HANDLERS = {} 24 | 25 | 26 | def register_lookup_handler(lookup_type, handler_or_path): 27 | """Register a lookup handler. 28 | 29 | Args: 30 | lookup_type (str): Name to register the handler under 31 | handler_or_path (OneOf[func, str]): a function or a path to a handler 32 | 33 | """ 34 | handler = handler_or_path 35 | if isinstance(handler_or_path, basestring): 36 | handler = load_object_from_string(handler_or_path) 37 | LOOKUP_HANDLERS[lookup_type] = handler 38 | if type(handler) != type: 39 | # Hander is a not a new-style handler 40 | logger = logging.getLogger(__name__) 41 | logger.warning("Registering lookup `%s`: Please upgrade to use the " 42 | "new style of Lookups." % lookup_type) 43 | warnings.warn( 44 | # For some reason, this does not show up... 45 | # Leaving it in anyway 46 | "Lookup `%s`: Please upgrade to use the new style of Lookups" 47 | "." % lookup_type, 48 | DeprecationWarning, 49 | stacklevel=2, 50 | ) 51 | 52 | 53 | def unregister_lookup_handler(lookup_type): 54 | """Unregister the specified lookup type. 55 | 56 | This is useful when testing various lookup types if you want to unregister 57 | the lookup type after the test runs. 58 | 59 | Args: 60 | lookup_type (str): Name of the lookup type to unregister 61 | 62 | """ 63 | LOOKUP_HANDLERS.pop(lookup_type, None) 64 | 65 | 66 | def resolve_lookups(variable, context, provider): 67 | """Resolve a set of lookups. 68 | 69 | Args: 70 | variable (:class:`stacker.variables.Variable`): The variable resolving 71 | it's lookups. 72 | context (:class:`stacker.context.Context`): stacker context 73 | provider (:class:`stacker.provider.base.BaseProvider`): subclass of the 74 | base provider 75 | 76 | Returns: 77 | dict: dict of Lookup -> resolved value 78 | 79 | """ 80 | resolved_lookups = {} 81 | for lookup in variable.lookups: 82 | try: 83 | handler = LOOKUP_HANDLERS[lookup.type] 84 | except KeyError: 85 | raise UnknownLookupType(lookup) 86 | try: 87 | resolved_lookups[lookup] = handler( 88 | value=lookup.input, 89 | context=context, 90 | provider=provider, 91 | ) 92 | except Exception as e: 93 | raise FailedVariableLookup(variable.name, lookup, e) 94 | return resolved_lookups 95 | 96 | 97 | register_lookup_handler(output.TYPE_NAME, output.OutputLookup) 98 | register_lookup_handler(kms.TYPE_NAME, kms.KmsLookup) 99 | register_lookup_handler(ssmstore.TYPE_NAME, ssmstore.SsmstoreLookup) 100 | register_lookup_handler(envvar.TYPE_NAME, envvar.EnvvarLookup) 101 | register_lookup_handler(xref.TYPE_NAME, xref.XrefLookup) 102 | register_lookup_handler(rxref.TYPE_NAME, rxref.RxrefLookup) 103 | register_lookup_handler(ami.TYPE_NAME, ami.AmiLookup) 104 | register_lookup_handler(file_handler.TYPE_NAME, file_handler.FileLookup) 105 | register_lookup_handler(split.TYPE_NAME, split.SplitLookup) 106 | register_lookup_handler(default.TYPE_NAME, default.DefaultLookup) 107 | register_lookup_handler(hook_data.TYPE_NAME, hook_data.HookDataLookup) 108 | register_lookup_handler(dynamodb.TYPE_NAME, dynamodb.DynamodbLookup) 109 | -------------------------------------------------------------------------------- /stacker/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/providers/__init__.py -------------------------------------------------------------------------------- /stacker/providers/aws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/providers/aws/__init__.py -------------------------------------------------------------------------------- /stacker/providers/base.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def not_implemented(method): 4 | raise NotImplementedError("Provider does not support '%s' " 5 | "method." % method) 6 | 7 | 8 | class BaseProviderBuilder(object): 9 | def build(self, region=None): 10 | not_implemented("build") 11 | 12 | 13 | class BaseProvider(object): 14 | def get_stack(self, stack_name, *args, **kwargs): 15 | # pylint: disable=unused-argument 16 | not_implemented("get_stack") 17 | 18 | def create_stack(self, *args, **kwargs): 19 | # pylint: disable=unused-argument 20 | not_implemented("create_stack") 21 | 22 | def update_stack(self, *args, **kwargs): 23 | # pylint: disable=unused-argument 24 | not_implemented("update_stack") 25 | 26 | def destroy_stack(self, *args, **kwargs): 27 | # pylint: disable=unused-argument 28 | not_implemented("destroy_stack") 29 | 30 | def get_stack_status(self, stack_name, *args, **kwargs): 31 | # pylint: disable=unused-argument 32 | not_implemented("get_stack_status") 33 | 34 | def get_outputs(self, stack_name, *args, **kwargs): 35 | # pylint: disable=unused-argument 36 | not_implemented("get_outputs") 37 | 38 | def get_output(self, stack_name, output): 39 | # pylint: disable=unused-argument 40 | return self.get_outputs(stack_name)[output] 41 | 42 | 43 | class Template(object): 44 | """A value object that represents a CloudFormation stack template, which 45 | could be optionally uploaded to s3. 46 | 47 | Presence of the url attribute indicates that the template was uploaded to 48 | S3, and the uploaded template should be used for CreateStack/UpdateStack 49 | calls. 50 | """ 51 | def __init__(self, url=None, body=None): 52 | self.url = url 53 | self.body = body 54 | -------------------------------------------------------------------------------- /stacker/session_cache.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import logging 3 | from .ui import ui 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | # A global credential cache that can be shared among boto3 sessions. This is 10 | # inherently threadsafe thanks to the GIL: 11 | # https://docs.python.org/3/glossary.html#term-global-interpreter-lock 12 | credential_cache = {} 13 | 14 | default_profile = None 15 | 16 | 17 | def get_session(region, profile=None): 18 | """Creates a boto3 session with a cache 19 | 20 | Args: 21 | region (str): The region for the session 22 | profile (str): The profile for the session 23 | 24 | Returns: 25 | :class:`boto3.session.Session`: A boto3 session with 26 | credential caching 27 | """ 28 | if profile is None: 29 | logger.debug("No AWS profile explicitly provided. " 30 | "Falling back to default.") 31 | profile = default_profile 32 | 33 | logger.debug("Building session using profile \"%s\" in region \"%s\"" 34 | % (profile, region)) 35 | 36 | session = boto3.Session(region_name=region, profile_name=profile) 37 | c = session._session.get_component('credential_provider') 38 | provider = c.get_provider('assume-role') 39 | provider.cache = credential_cache 40 | provider._prompter = ui.getpass 41 | return session 42 | -------------------------------------------------------------------------------- /stacker/status.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | 4 | class Status(object): 5 | def __init__(self, name, code, reason=None): 6 | self.name = name 7 | self.code = code 8 | self.reason = reason or getattr(self, "reason", None) 9 | 10 | def _comparison(self, operator, other): 11 | if hasattr(other, "code"): 12 | return operator(self.code, other.code) 13 | return NotImplemented 14 | 15 | def __eq__(self, other): 16 | return self._comparison(operator.eq, other) 17 | 18 | def __ne__(self, other): 19 | return self._comparison(operator.ne, other) 20 | 21 | def __lt__(self, other): 22 | return self._comparison(operator.lt, other) 23 | 24 | def __gt__(self, other): 25 | return self._comparison(operator.gt, other) 26 | 27 | def __le__(self, other): 28 | return self._comparison(operator.le, other) 29 | 30 | def __ge__(self, other): 31 | return self._comparison(operator.ge, other) 32 | 33 | 34 | class PendingStatus(Status): 35 | def __init__(self, reason=None): 36 | super(PendingStatus, self).__init__("pending", 0, reason) 37 | 38 | 39 | class SubmittedStatus(Status): 40 | def __init__(self, reason=None): 41 | super(SubmittedStatus, self).__init__("submitted", 1, reason) 42 | 43 | 44 | class CompleteStatus(Status): 45 | def __init__(self, reason=None): 46 | super(CompleteStatus, self).__init__("complete", 2, reason) 47 | 48 | 49 | class SkippedStatus(Status): 50 | def __init__(self, reason=None): 51 | super(SkippedStatus, self).__init__("skipped", 3, reason) 52 | 53 | 54 | class FailedStatus(Status): 55 | def __init__(self, reason=None): 56 | super(FailedStatus, self).__init__("failed", 4, reason) 57 | 58 | 59 | class NotSubmittedStatus(SkippedStatus): 60 | reason = "disabled" 61 | 62 | 63 | class NotUpdatedStatus(SkippedStatus): 64 | reason = "locked" 65 | 66 | 67 | class DidNotChangeStatus(SkippedStatus): 68 | reason = "nochange" 69 | 70 | 71 | class StackDoesNotExist(SkippedStatus): 72 | reason = "does not exist in cloudformation" 73 | 74 | 75 | PENDING = PendingStatus() 76 | WAITING = PendingStatus(reason="waiting") 77 | SUBMITTED = SubmittedStatus() 78 | COMPLETE = CompleteStatus() 79 | SKIPPED = SkippedStatus() 80 | FAILED = FailedStatus() 81 | INTERRUPTED = FailedStatus(reason="interrupted") 82 | -------------------------------------------------------------------------------- /stacker/target.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Target(object): 4 | """A "target" is just a node in the stacker graph that does nothing, except 5 | specify dependencies. These can be useful as a means of logically grouping 6 | a set of stacks together that can be targeted with the `--targets` flag. 7 | """ 8 | 9 | def __init__(self, definition): 10 | self.name = definition.name 11 | self.requires = definition.requires or [] 12 | self.required_by = definition.required_by or [] 13 | self.logging = False 14 | -------------------------------------------------------------------------------- /stacker/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/__init__.py -------------------------------------------------------------------------------- /stacker/tests/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/actions/__init__.py -------------------------------------------------------------------------------- /stacker/tests/actions/test_diff.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from operator import attrgetter 4 | from stacker.actions.diff import ( 5 | diff_dictionaries, 6 | diff_parameters, 7 | DictValue 8 | ) 9 | 10 | 11 | class TestDictValueFormat(unittest.TestCase): 12 | def test_status(self): 13 | added = DictValue("k0", None, "value_0") 14 | self.assertEqual(added.status(), DictValue.ADDED) 15 | removed = DictValue("k1", "value_1", None) 16 | self.assertEqual(removed.status(), DictValue.REMOVED) 17 | modified = DictValue("k2", "value_1", "value_2") 18 | self.assertEqual(modified.status(), DictValue.MODIFIED) 19 | unmodified = DictValue("k3", "value_1", "value_1") 20 | self.assertEqual(unmodified.status(), DictValue.UNMODIFIED) 21 | 22 | def test_format(self): 23 | added = DictValue("k0", None, "value_0") 24 | self.assertEqual(added.changes(), 25 | ['+%s = %s' % (added.key, added.new_value)]) 26 | removed = DictValue("k1", "value_1", None) 27 | self.assertEqual(removed.changes(), 28 | ['-%s = %s' % (removed.key, removed.old_value)]) 29 | modified = DictValue("k2", "value_1", "value_2") 30 | self.assertEqual(modified.changes(), [ 31 | '-%s = %s' % (modified.key, modified.old_value), 32 | '+%s = %s' % (modified.key, modified.new_value) 33 | ]) 34 | unmodified = DictValue("k3", "value_1", "value_1") 35 | self.assertEqual(unmodified.changes(), [' %s = %s' % ( 36 | unmodified.key, unmodified.old_value)]) 37 | self.assertEqual(unmodified.changes(), [' %s = %s' % ( 38 | unmodified.key, unmodified.new_value)]) 39 | 40 | 41 | class TestDiffDictionary(unittest.TestCase): 42 | def test_diff_dictionaries(self): 43 | old_dict = { 44 | "a": "Apple", 45 | "b": "Banana", 46 | "c": "Corn", 47 | } 48 | new_dict = { 49 | "a": "Apple", 50 | "b": "Bob", 51 | "d": "Doug", 52 | } 53 | 54 | [count, changes] = diff_dictionaries(old_dict, new_dict) 55 | self.assertEqual(count, 3) 56 | expected_output = [ 57 | DictValue("a", "Apple", "Apple"), 58 | DictValue("b", "Banana", "Bob"), 59 | DictValue("c", "Corn", None), 60 | DictValue("d", None, "Doug"), 61 | ] 62 | expected_output.sort(key=attrgetter("key")) 63 | 64 | # compare all the outputs to the expected change 65 | for expected_change in expected_output: 66 | change = changes.pop(0) 67 | self.assertEqual(change, expected_change) 68 | 69 | # No extra output 70 | self.assertEqual(len(changes), 0) 71 | 72 | 73 | class TestDiffParameters(unittest.TestCase): 74 | def test_diff_parameters_no_changes(self): 75 | old_params = { 76 | "a": "Apple" 77 | } 78 | new_params = { 79 | "a": "Apple" 80 | } 81 | 82 | param_diffs = diff_parameters(old_params, new_params) 83 | self.assertEquals(param_diffs, []) 84 | -------------------------------------------------------------------------------- /stacker/tests/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/blueprints/__init__.py -------------------------------------------------------------------------------- /stacker/tests/blueprints/test_testutil.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from troposphere import ecr 4 | 5 | from ...blueprints.testutil import BlueprintTestCase 6 | from ...blueprints.base import Blueprint 7 | from ...context import Context 8 | from ...variables import Variable 9 | 10 | 11 | class Repositories(Blueprint): 12 | """ Simple blueprint to test our test cases. """ 13 | VARIABLES = { 14 | "Repositories": { 15 | "type": list, 16 | "description": "A list of repository names to create." 17 | } 18 | } 19 | 20 | def create_template(self): 21 | t = self.template 22 | variables = self.get_variables() 23 | 24 | for repo in variables["Repositories"]: 25 | t.add_resource( 26 | ecr.Repository( 27 | "%sRepository" % repo, 28 | RepositoryName=repo, 29 | ) 30 | ) 31 | 32 | 33 | class TestRepositories(BlueprintTestCase): 34 | def test_create_template_passes(self): 35 | ctx = Context({'namespace': 'test'}) 36 | blueprint = Repositories('test_repo', ctx) 37 | blueprint.resolve_variables([ 38 | Variable('Repositories', ["repo1", "repo2"]) 39 | ]) 40 | blueprint.create_template() 41 | self.assertRenderedBlueprint(blueprint) 42 | 43 | def test_create_template_fails(self): 44 | ctx = Context({'namespace': 'test'}) 45 | blueprint = Repositories('test_repo', ctx) 46 | blueprint.resolve_variables([ 47 | Variable('Repositories', ["repo1", "repo2", "repo3"]) 48 | ]) 49 | blueprint.create_template() 50 | with self.assertRaises(AssertionError): 51 | self.assertRenderedBlueprint(blueprint) 52 | 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /stacker/tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import os 4 | 5 | import pytest 6 | import py.path 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | @pytest.fixture(scope='session', autouse=True) 12 | def aws_credentials(): 13 | # Handle change in https://github.com/spulec/moto/issues/1924 14 | # Ensure AWS SDK finds some (bogus) credentials in the environment and 15 | # doesn't try to use other providers. 16 | overrides = { 17 | 'AWS_ACCESS_KEY_ID': 'testing', 18 | 'AWS_SECRET_ACCESS_KEY': 'testing', 19 | 'AWS_DEFAULT_REGION': 'us-east-1' 20 | } 21 | saved_env = {} 22 | for key, value in overrides.items(): 23 | logger.info('Overriding env var: {}={}'.format(key, value)) 24 | saved_env[key] = os.environ.get(key, None) 25 | os.environ[key] = value 26 | 27 | yield 28 | 29 | for key, value in saved_env.items(): 30 | logger.info('Restoring saved env var: {}={}'.format(key, value)) 31 | if value is None: 32 | del os.environ[key] 33 | else: 34 | os.environ[key] = value 35 | 36 | saved_env.clear() 37 | 38 | 39 | @pytest.fixture(scope="package") 40 | def stacker_fixture_dir(): 41 | path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 42 | 'fixtures') 43 | return py.path.local(path) 44 | -------------------------------------------------------------------------------- /stacker/tests/factories.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | 3 | from stacker.context import Context 4 | from stacker.config import Config, Stack 5 | from stacker.lookups import Lookup 6 | 7 | 8 | class MockThreadingEvent(object): 9 | def wait(self, timeout=None): 10 | return False 11 | 12 | 13 | class MockProviderBuilder(object): 14 | def __init__(self, provider, region=None): 15 | self.provider = provider 16 | self.region = region 17 | 18 | def build(self, region=None, profile=None): 19 | return self.provider 20 | 21 | 22 | def mock_provider(**kwargs): 23 | return MagicMock(**kwargs) 24 | 25 | 26 | def mock_context(namespace="default", extra_config_args=None, **kwargs): 27 | config_args = {"namespace": namespace} 28 | if extra_config_args: 29 | config_args.update(extra_config_args) 30 | config = Config(config_args) 31 | if kwargs.get("environment"): 32 | return Context( 33 | config=config, 34 | **kwargs) 35 | return Context( 36 | config=config, 37 | environment={}, 38 | **kwargs) 39 | 40 | 41 | def generate_definition(base_name, stack_id, **overrides): 42 | definition = { 43 | "name": "%s.%d" % (base_name, stack_id), 44 | "class_path": "stacker.tests.fixtures.mock_blueprints.%s" % ( 45 | base_name.upper()), 46 | "requires": [] 47 | } 48 | definition.update(overrides) 49 | return Stack(definition) 50 | 51 | 52 | def mock_lookup(lookup_input, lookup_type, raw=None): 53 | if raw is None: 54 | raw = "%s %s" % (lookup_type, lookup_input) 55 | return Lookup(type=lookup_type, input=lookup_input, raw=raw) 56 | 57 | 58 | class SessionStub(object): 59 | 60 | """Stubber class for boto3 sessions made with session_cache.get_session() 61 | 62 | This is a helper class that should be used when trying to stub out 63 | get_session() calls using the boto3.stubber. 64 | 65 | Example Usage: 66 | 67 | @mock.patch('stacker.lookups.handlers.myfile.get_session', 68 | return_value=sessionStub(client)) 69 | def myfile_test(self, client_stub): 70 | ... 71 | 72 | Attributes: 73 | client_stub (:class:`boto3.session.Session`:): boto3 session stub 74 | 75 | """ 76 | 77 | def __init__(self, client_stub): 78 | self.client_stub = client_stub 79 | 80 | def client(self, region): 81 | """Returns the stubbed client object 82 | 83 | Args: 84 | region (str): So boto3 won't complain 85 | 86 | Returns: 87 | :class:`boto3.session.Session`: The stubbed boto3 session 88 | """ 89 | return self.client_stub 90 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /stacker/tests/fixtures/basic.env: -------------------------------------------------------------------------------- 1 | namespace: test.stacker 2 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/cfn_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "TestTemplate", 4 | "Parameters": { 5 | "Param1": { 6 | "Type": "String" 7 | }, 8 | "Param2": { 9 | "Default": "default", 10 | "Type": "CommaDelimitedList" 11 | } 12 | }, 13 | "Resources": { 14 | "Dummy": { 15 | "Type": "AWS::SNS::Topic", 16 | "Properties": { 17 | "DisplayName": {"Ref" : "Param1"} 18 | } 19 | } 20 | }, 21 | "Outputs": { 22 | "DummyId": { 23 | "Value": "dummy-1234" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/cfn_template.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "TestTemplate", 4 | "Parameters": { 5 | "Param1": { 6 | "Type": "String" 7 | }, 8 | "Param2": { 9 | "Default": "default", 10 | "Type": "CommaDelimitedList" 11 | } 12 | }, 13 | "Resources": { 14 | "Dummy": { 15 | "Type": "AWS::CloudFormation::WaitConditionHandle" 16 | } 17 | }, 18 | "Outputs": { 19 | "DummyId": { 20 | "Value": "dummy-{{ context.environment.foo }}-{{ variables.Param1 }}-{{ variables.bar }}-1234" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/cfn_template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: TestTemplate 3 | Parameters: 4 | Param1: 5 | Type: String 6 | Param2: 7 | Default: default 8 | Type: CommaDelimitedList 9 | Resources: 10 | Bucket: 11 | Type: AWS::S3::Bucket 12 | Properties: 13 | BucketName: 14 | !Join 15 | - "-" 16 | - - !Ref "AWS::StackName" 17 | - !Ref "AWS::Region" 18 | Dummy: 19 | Type: AWS::CloudFormation::WaitConditionHandle 20 | Outputs: 21 | DummyId: 22 | Value: dummy-1234 23 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/keypair/fingerprint: -------------------------------------------------------------------------------- 1 | d7:50:1f:78:55:5f:22:c1:f6:88:c6:5d:82:4f:94:4f 2 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/keypair/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEA7rF34ExOHgT+dDYJUswkhBpyC+vnK+ptx+nGQDTkPj9aP1uAXbXA 4 | C97KK+Ihou0jniYKPJMHsjEK4a7eh2ihoK6JkYs9+y0MeGCAHAYuGXdNt5jv1e0XNgoYdf 5 | JloC0pgOp4Po9+4qeuOds8bb9IxwM/aSaJWygaSc22ZTzeOWQk5PXJNH0lR0ZelUUkj0HK 6 | aouuV6UX/t+czTghgnNZgDjk5sOfUNmugN7fJi+6/dWjOaukDkJttfZXLRTPDux0SZw4Jo 7 | RqZ40cBNS8ipLVk24BWeEjVlNl6rrFDtO4yrkscz7plwXlPiRLcdCdbamcCZaRrdkftKje 8 | 5ypz5dvocQAAA9DJ0TBmydEwZgAAAAdzc2gtcnNhAAABAQDusXfgTE4eBP50NglSzCSEGn 9 | IL6+cr6m3H6cZANOQ+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37 10 | LQx4YIAcBi4Zd023mO/V7Rc2Chh18mWgLSmA6ng+j37ip6452zxtv0jHAz9pJolbKBpJzb 11 | ZlPN45ZCTk9ck0fSVHRl6VRSSPQcpqi65XpRf+35zNOCGCc1mAOOTmw59Q2a6A3t8mL7r9 12 | 1aM5q6QOQm219lctFM8O7HRJnDgmhGpnjRwE1LyKktWTbgFZ4SNWU2XqusUO07jKuSxzPu 13 | mXBeU+JEtx0J1tqZwJlpGt2R+0qN7nKnPl2+hxAAAAAwEAAQAAAQAwMUSy1LUw+nElpYNc 14 | ZDs7MNu17HtQMpTXuCt+6y7qIoBmKmNQiFGuE91d3tpLuvVmCOgoMsdrAtvflR741/dKKf 15 | M8n5B0FjReWZ2ECvtjyOK4HvjNiIEXOBKYPcim/ndSwARnHTHRMWnL5KfewLBA/jbfVBiH 16 | fyFPpWkeJ5v2mg3EDCkTCj7mBZwXYkX8uZ1IN6CZJ9kWNaPO3kloTlamgs6pd/5+OmMGWc 17 | /vhfJQppaJjW58y7D7zCpncHg3Yf0HZsgWRTGJO93TxuyzDlAXITVGwqcz7InTVQZS1XTx 18 | 3FNmIpb0lDtVrKGxwvR/7gP6DpxMlKkzoCg3j1o8tHvBAAAAgQDuZCVAAqQFrY4ZH2TluP 19 | SFulXuTiT4mgQivAwI6ysMxjpX1IGBTgDvHXJ0xyW4LN7pCvg8hRAhsPlaNBX24nNfOGmn 20 | QMYp/qAZG5JP2vEJmDUKmEJ77Twwmk+k0zXfyZyfo7rgpF4c5W2EFnV7xiMtBTKbAj4HMn 21 | qGPYDPGpySTwAAAIEA+w72mMctM2yd9Sxyg5b7ZlhuNyKW1oHcEvLoEpTtru0f8gh7C3HT 22 | C0SiuTOth2xoHUWnbo4Yv5FV3gSoQ/rd1sWbkpEZMwbaPGsTA8bkCn2eItsjfrQx+6oY1U 23 | HgZDrkjbByB3KQiq+VioKsrUmgfT/UgBq2tSnHqcYB56Eqj0sAAACBAPNkMvCstNJGS4FN 24 | nSCGXghoYqKHivZN/IjWP33t/cr72lGp1yCY5S6FCn+JdNrojKYk2VXOSF5xc3fZllbr7W 25 | hmhXRr/csQkymXMDkJHnsdhpMeoEZm7wBjUx+hE1+QbNF63kZMe9sjm5y/YRu7W7H6ngme 26 | kb5FW97sspLYX8WzAAAAF2RhbmllbGt6YUBkYW5pZWwtcGMubGFuAQID 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/keypair/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== 2 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/mock_hooks.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def mock_hook(provider, context, **kwargs): 4 | return {"result": kwargs["value"]} 5 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/mock_lookups.py: -------------------------------------------------------------------------------- 1 | TYPE_NAME = "mock" 2 | 3 | 4 | def handler(value, **kwargs): 5 | return "mock" 6 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/not-basic.env: -------------------------------------------------------------------------------- 1 | namespace: test.stacker 2 | environment: test 3 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/parameter_resolution/template.yml: -------------------------------------------------------------------------------- 1 | # used in functional test suites, to fix https://github.com/cloudtools/stacker/pull/615 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | 4 | Parameters: 5 | NormalParam: 6 | Type: String 7 | SecretParam: 8 | Type: String 9 | Default: default-secret 10 | NoEcho: true 11 | 12 | Outputs: 13 | NormalParam: 14 | Value: !Ref "NormalParam" 15 | SecretParam: 16 | Value: !Ref "SecretParam" 17 | 18 | 19 | Resources: 20 | WaitConditionHandle: 21 | Type: "AWS::CloudFormation::WaitConditionHandle" 22 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/vpc-bastion-db-web-pre-1.0.yaml: -------------------------------------------------------------------------------- 1 | # Hooks require a path. 2 | # If the build should stop when a hook fails, set required to true. 3 | # pre_build happens before the build 4 | # post_build happens after the build 5 | pre_build: 6 | - path: stacker.hooks.route53.create_domain 7 | required: true 8 | enabled: true 9 | # Additional args can be passed as a dict of key/value pairs 10 | # args: 11 | # BaseDomain: foo 12 | # post_build: 13 | 14 | mappings: 15 | AmiMap: 16 | us-east-1: 17 | NAT: ami-ad227cc4 18 | ubuntu1404: &ubuntu1404 ami-74e27e1c # Setting an anchor 19 | bastion: *ubuntu1404 # Using the anchor above 20 | us-west-2: 21 | NAT: ami-290f4119 22 | ubuntu1404west2: &ubuntu1404west2 ami-5189a661 23 | bastion: *ubuntu1404west2 24 | 25 | vpc_parameters: &vpc_parameters 26 | VpcId: vpc::VpcId # parametrs with ::'s in them refer to :: 27 | DefaultSG: vpc::DefaultSG 28 | PublicSubnets: vpc::PublicSubnets 29 | PrivateSubnets: vpc::PrivateSubnets 30 | AvailabilityZones: vpc::AvailabilityZones 31 | 32 | stacks: 33 | - name: vpc 34 | class_path: stacker.tests.fixtures.mock_blueprints.VPC 35 | variables: 36 | InstanceType: m3.medium 37 | SshKeyName: default 38 | ImageName: NAT 39 | # Only build 2 AZs, can be overridden with -p on the command line 40 | # Note: If you want more than 4 AZs you should add more subnets below 41 | # Also you need at least 2 AZs in order to use the DB because 42 | # of the fact that the DB blueprint uses MultiAZ 43 | AZCount: 2 44 | # Enough subnets for 4 AZs 45 | PublicSubnets: 10.128.0.0/24,10.128.1.0/24,10.128.2.0/24,10.128.3.0/24 46 | PrivateSubnets: 10.128.8.0/22,10.128.12.0/22,10.128.16.0/22,10.128.20.0/22 47 | # Uncomment if you want an internal hosted zone for the VPC 48 | # If provided, it will be added to the dns search path of the DHCP 49 | # Options 50 | #InternalDomain: internal 51 | - name: bastion 52 | class_path: stacker.tests.fixtures.mock_blueprints.Bastion 53 | ## !! This should break, parameters not allowed in 1.0 54 | parameters: 55 | # Extends the parameters dict with the contents of the vpc_parameters 56 | # anchor. Basically we're including all VPC Outputs in the parameters 57 | # of the bastion stack. Note: Stacker figures out, automatically, which 58 | # parameters the stack actually needs and only submits those to each 59 | # stack. For example, most stacks are in the PrivateSubnets, but not 60 | # the PublicSubnets, but stacker deals with it for you. 61 | << : *vpc_parameters 62 | InstanceType: m3.medium 63 | OfficeNetwork: 203.0.113.0/24 64 | MinSize: 2 65 | MaxSize: 2 66 | SshKeyName: default 67 | ImageName: bastion 68 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/vpc-bastion-db-web.yaml: -------------------------------------------------------------------------------- 1 | # Hooks require a path. 2 | # If the build should stop when a hook fails, set required to true. 3 | # pre_build happens before the build 4 | # post_build happens after the build 5 | pre_build: 6 | - path: stacker.hooks.route53.create_domain 7 | required: true 8 | enabled: true 9 | # Additional args can be passed as a dict of key/value pairs 10 | # args: 11 | # BaseDomain: foo 12 | # post_build: 13 | 14 | mappings: 15 | AmiMap: 16 | us-east-1: 17 | NAT: ami-ad227cc4 18 | ubuntu1404: &ubuntu1404 ami-74e27e1c # Setting an anchor 19 | bastion: *ubuntu1404 # Using the anchor above 20 | us-west-2: 21 | NAT: ami-290f4119 22 | ubuntu1404west2: &ubuntu1404west2 ami-5189a661 23 | bastion: *ubuntu1404west2 24 | 25 | vpc_parameters: &vpc_parameters 26 | VpcId: vpc::VpcId # parametrs with ::'s in them refer to :: 27 | DefaultSG: vpc::DefaultSG 28 | PublicSubnets: vpc::PublicSubnets 29 | PrivateSubnets: vpc::PrivateSubnets 30 | AvailabilityZones: vpc::AvailabilityZones 31 | 32 | stacks: 33 | - name: vpc 34 | class_path: stacker.tests.fixtures.mock_blueprints.VPC 35 | variables: 36 | InstanceType: m3.medium 37 | SshKeyName: default 38 | ImageName: NAT 39 | # Only build 2 AZs, can be overridden with -p on the command line 40 | # Note: If you want more than 4 AZs you should add more subnets below 41 | # Also you need at least 2 AZs in order to use the DB because 42 | # of the fact that the DB blueprint uses MultiAZ 43 | AZCount: 2 44 | # Enough subnets for 4 AZs 45 | PublicSubnets: 10.128.0.0/24,10.128.1.0/24,10.128.2.0/24,10.128.3.0/24 46 | PrivateSubnets: 10.128.8.0/22,10.128.12.0/22,10.128.16.0/22,10.128.20.0/22 47 | # Uncomment if you want an internal hosted zone for the VPC 48 | # If provided, it will be added to the dns search path of the DHCP 49 | # Options 50 | #InternalDomain: internal 51 | - name: bastion 52 | class_path: stacker.tests.fixtures.mock_blueprints.Bastion 53 | variables: 54 | # Extends the parameters dict with the contents of the vpc_parameters 55 | # anchor. Basically we're including all VPC Outputs in the parameters 56 | # of the bastion stack. Note: Stacker figures out, automatically, which 57 | # parameters the stack actually needs and only submits those to each 58 | # stack. For example, most stacks are in the PrivateSubnets, but not 59 | # the PublicSubnets, but stacker deals with it for you. 60 | << : *vpc_parameters 61 | InstanceType: m3.medium 62 | OfficeNetwork: 203.0.113.0/24 63 | MinSize: 2 64 | MaxSize: 2 65 | SshKeyName: default 66 | ImageName: bastion 67 | -------------------------------------------------------------------------------- /stacker/tests/fixtures/vpc-custom-log-format-info.yaml: -------------------------------------------------------------------------------- 1 | log_formats: 2 | info: "[%(asctime)s] ${environment} custom log format - %(message)s" 3 | 4 | stacks: 5 | - name: vpc 6 | class_path: stacker.tests.fixtures.mock_blueprints.VPC 7 | variables: 8 | InstanceType: m3.medium 9 | SshKeyName: default 10 | ImageName: NAT 11 | # Only build 2 AZs, can be overridden with -p on the command line 12 | # Note: If you want more than 4 AZs you should add more subnets below 13 | # Also you need at least 2 AZs in order to use the DB because 14 | # of the fact that the DB blueprint uses MultiAZ 15 | AZCount: 2 16 | # Enough subnets for 4 AZs 17 | PublicSubnets: 10.128.0.0/24,10.128.1.0/24,10.128.2.0/24,10.128.3.0/24 18 | PrivateSubnets: 10.128.8.0/22,10.128.12.0/22,10.128.16.0/22,10.128.20.0/22 19 | -------------------------------------------------------------------------------- /stacker/tests/hooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/hooks/__init__.py -------------------------------------------------------------------------------- /stacker/tests/hooks/test_ecs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import boto3 4 | from moto import mock_ecs 5 | from testfixtures import LogCapture 6 | 7 | from stacker.hooks.ecs import create_clusters 8 | from ..factories import ( 9 | mock_context, 10 | mock_provider, 11 | ) 12 | 13 | REGION = "us-east-1" 14 | 15 | 16 | class TestECSHooks(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.provider = mock_provider(region=REGION) 20 | self.context = mock_context(namespace="fake") 21 | 22 | def test_create_single_cluster(self): 23 | with mock_ecs(): 24 | cluster = "test-cluster" 25 | logger = "stacker.hooks.ecs" 26 | client = boto3.client("ecs", region_name=REGION) 27 | response = client.list_clusters() 28 | 29 | self.assertEqual(len(response["clusterArns"]), 0) 30 | with LogCapture(logger) as logs: 31 | self.assertTrue( 32 | create_clusters( 33 | provider=self.provider, 34 | context=self.context, 35 | clusters=cluster, 36 | ) 37 | ) 38 | 39 | logs.check( 40 | ( 41 | logger, 42 | "DEBUG", 43 | "Creating ECS cluster: %s" % cluster 44 | ) 45 | ) 46 | 47 | response = client.list_clusters() 48 | self.assertEqual(len(response["clusterArns"]), 1) 49 | 50 | def test_create_multiple_clusters(self): 51 | with mock_ecs(): 52 | clusters = ("test-cluster0", "test-cluster1") 53 | logger = "stacker.hooks.ecs" 54 | client = boto3.client("ecs", region_name=REGION) 55 | response = client.list_clusters() 56 | 57 | self.assertEqual(len(response["clusterArns"]), 0) 58 | for cluster in clusters: 59 | with LogCapture(logger) as logs: 60 | self.assertTrue( 61 | create_clusters( 62 | provider=self.provider, 63 | context=self.context, 64 | clusters=cluster, 65 | ) 66 | ) 67 | 68 | logs.check( 69 | ( 70 | logger, 71 | "DEBUG", 72 | "Creating ECS cluster: %s" % cluster 73 | ) 74 | ) 75 | 76 | response = client.list_clusters() 77 | self.assertEqual(len(response["clusterArns"]), 2) 78 | 79 | def test_fail_create_cluster(self): 80 | with mock_ecs(): 81 | logger = "stacker.hooks.ecs" 82 | client = boto3.client("ecs", region_name=REGION) 83 | response = client.list_clusters() 84 | 85 | self.assertEqual(len(response["clusterArns"]), 0) 86 | with LogCapture(logger) as logs: 87 | create_clusters( 88 | provider=self.provider, 89 | context=self.context 90 | ) 91 | 92 | logs.check( 93 | ( 94 | logger, 95 | "ERROR", 96 | "setup_clusters hook missing \"clusters\" argument" 97 | ) 98 | ) 99 | 100 | response = client.list_clusters() 101 | self.assertEqual(len(response["clusterArns"]), 0) 102 | -------------------------------------------------------------------------------- /stacker/tests/hooks/test_iam.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import boto3 4 | from botocore.exceptions import ClientError 5 | 6 | from moto import mock_iam 7 | 8 | from stacker.hooks.iam import ( 9 | create_ecs_service_role, 10 | _get_cert_arn_from_response, 11 | ) 12 | 13 | from awacs.helpers.trust import get_ecs_assumerole_policy 14 | 15 | from ..factories import ( 16 | mock_context, 17 | mock_provider, 18 | ) 19 | 20 | 21 | REGION = "us-east-1" 22 | 23 | # No test for stacker.hooks.iam.ensure_server_cert_exists until 24 | # updated version of moto is imported 25 | # (https://github.com/spulec/moto/pull/679) merged 26 | 27 | 28 | class TestIAMHooks(unittest.TestCase): 29 | 30 | def setUp(self): 31 | self.context = mock_context(namespace="fake") 32 | self.provider = mock_provider(region=REGION) 33 | 34 | def test_get_cert_arn_from_response(self): 35 | arn = "fake-arn" 36 | # Creation response 37 | response = { 38 | "ServerCertificateMetadata": { 39 | "Arn": arn 40 | } 41 | } 42 | 43 | self.assertEqual(_get_cert_arn_from_response(response), arn) 44 | 45 | # Existing cert response 46 | response = {"ServerCertificate": response} 47 | self.assertEqual(_get_cert_arn_from_response(response), arn) 48 | 49 | def test_create_service_role(self): 50 | role_name = "ecsServiceRole" 51 | policy_name = "AmazonEC2ContainerServiceRolePolicy" 52 | with mock_iam(): 53 | client = boto3.client("iam", region_name=REGION) 54 | 55 | with self.assertRaises(ClientError): 56 | client.get_role(RoleName=role_name) 57 | 58 | self.assertTrue( 59 | create_ecs_service_role( 60 | context=self.context, 61 | provider=self.provider, 62 | ) 63 | ) 64 | 65 | role = client.get_role(RoleName=role_name) 66 | 67 | self.assertIn("Role", role) 68 | self.assertEqual(role_name, role["Role"]["RoleName"]) 69 | client.get_role_policy( 70 | RoleName=role_name, 71 | PolicyName=policy_name 72 | ) 73 | 74 | def test_create_service_role_already_exists(self): 75 | role_name = "ecsServiceRole" 76 | policy_name = "AmazonEC2ContainerServiceRolePolicy" 77 | with mock_iam(): 78 | client = boto3.client("iam", region_name=REGION) 79 | client.create_role( 80 | RoleName=role_name, 81 | AssumeRolePolicyDocument=get_ecs_assumerole_policy().to_json() 82 | ) 83 | 84 | self.assertTrue( 85 | create_ecs_service_role( 86 | context=self.context, 87 | provider=self.provider, 88 | ) 89 | ) 90 | 91 | role = client.get_role(RoleName=role_name) 92 | 93 | self.assertIn("Role", role) 94 | self.assertEqual(role_name, role["Role"]["RoleName"]) 95 | client.get_role_policy( 96 | RoleName=role_name, 97 | PolicyName=policy_name 98 | ) 99 | -------------------------------------------------------------------------------- /stacker/tests/lookups/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/lookups/__init__.py -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/lookups/handlers/__init__.py -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_default.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | import unittest 3 | 4 | from stacker.context import Context 5 | from stacker.lookups.handlers.default import DefaultLookup 6 | 7 | 8 | class TestDefaultLookup(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.provider = MagicMock() 12 | self.context = Context( 13 | environment={ 14 | 'namespace': 'test', 15 | 'env_var': 'val_in_env'} 16 | ) 17 | 18 | def test_env_var_present(self): 19 | lookup_val = "env_var::fallback" 20 | value = DefaultLookup.handle(lookup_val, 21 | provider=self.provider, 22 | context=self.context) 23 | assert value == 'val_in_env' 24 | 25 | def test_env_var_missing(self): 26 | lookup_val = "bad_env_var::fallback" 27 | value = DefaultLookup.handle(lookup_val, 28 | provider=self.provider, 29 | context=self.context) 30 | assert value == 'fallback' 31 | 32 | def test_invalid_value(self): 33 | value = "env_var:fallback" 34 | with self.assertRaises(ValueError): 35 | DefaultLookup.handle(value) 36 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_envvar.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from stacker.lookups.handlers.envvar import EnvvarLookup 3 | import os 4 | 5 | 6 | class TestEnvVarHandler(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.testkey = 'STACKER_ENVVAR_TESTCASE' 10 | self.invalidtestkey = 'STACKER_INVALID_ENVVAR_TESTCASE' 11 | self.testval = 'TestVal' 12 | os.environ[self.testkey] = self.testval 13 | 14 | def test_valid_envvar(self): 15 | value = EnvvarLookup.handle(self.testkey) 16 | self.assertEqual(value, self.testval) 17 | 18 | def test_invalid_envvar(self): 19 | with self.assertRaises(ValueError): 20 | EnvvarLookup.handle(self.invalidtestkey) 21 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_hook_data.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | from stacker.context import Context 5 | from stacker.lookups.handlers.hook_data import HookDataLookup 6 | 7 | 8 | class TestHookDataLookup(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.ctx = Context({"namespace": "test-ns"}) 12 | self.ctx.set_hook_data("fake_hook", {"result": "good"}) 13 | 14 | def test_valid_hook_data(self): 15 | value = HookDataLookup.handle("fake_hook::result", context=self.ctx) 16 | self.assertEqual(value, "good") 17 | 18 | def test_invalid_hook_data(self): 19 | with self.assertRaises(KeyError): 20 | HookDataLookup.handle("fake_hook::bad_key", context=self.ctx) 21 | 22 | def test_bad_value_hook_data(self): 23 | with self.assertRaises(ValueError): 24 | HookDataLookup.handle("fake_hook", context=self.ctx) 25 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_output.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | import unittest 3 | 4 | from stacker.stack import Stack 5 | from ...factories import generate_definition 6 | from stacker.lookups.handlers.output import OutputLookup 7 | 8 | 9 | class TestOutputHandler(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.context = MagicMock() 13 | 14 | def test_output_handler(self): 15 | stack = Stack( 16 | definition=generate_definition("vpc", 1), 17 | context=self.context) 18 | stack.set_outputs({ 19 | "SomeOutput": "Test Output"}) 20 | self.context.get_stack.return_value = stack 21 | value = OutputLookup.handle("stack-name::SomeOutput", 22 | context=self.context) 23 | self.assertEqual(value, "Test Output") 24 | self.assertEqual(self.context.get_stack.call_count, 1) 25 | args = self.context.get_stack.call_args 26 | self.assertEqual(args[0][0], "stack-name") 27 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_rxref.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | import unittest 3 | 4 | from stacker.lookups.handlers.rxref import RxrefLookup 5 | from ....context import Context 6 | from ....config import Config 7 | 8 | 9 | class TestRxrefHandler(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.provider = MagicMock() 13 | self.context = Context( 14 | config=Config({"namespace": "ns"}) 15 | ) 16 | 17 | def test_rxref_handler(self): 18 | self.provider.get_output.return_value = "Test Output" 19 | 20 | value = RxrefLookup.handle("fully-qualified-stack-name::SomeOutput", 21 | provider=self.provider, 22 | context=self.context) 23 | self.assertEqual(value, "Test Output") 24 | 25 | args = self.provider.get_output.call_args 26 | self.assertEqual(args[0][0], "ns-fully-qualified-stack-name") 27 | self.assertEqual(args[0][1], "SomeOutput") 28 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_split.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from stacker.lookups.handlers.split import SplitLookup 4 | 5 | 6 | class TestSplitLookup(unittest.TestCase): 7 | def test_single_character_split(self): 8 | value = ",::a,b,c" 9 | expected = ["a", "b", "c"] 10 | assert SplitLookup.handle(value) == expected 11 | 12 | def test_multi_character_split(self): 13 | value = ",,::a,,b,c" 14 | expected = ["a", "b,c"] 15 | assert SplitLookup.handle(value) == expected 16 | 17 | def test_invalid_value_split(self): 18 | value = ",:a,b,c" 19 | with self.assertRaises(ValueError): 20 | SplitLookup.handle(value) 21 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_ssmstore.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import mock 3 | from botocore.stub import Stubber 4 | from stacker.lookups.handlers.ssmstore import SsmstoreLookup 5 | import boto3 6 | from stacker.tests.factories import SessionStub 7 | 8 | 9 | class TestSSMStoreHandler(unittest.TestCase): 10 | client = boto3.client('ssm', region_name='us-east-1') 11 | 12 | def setUp(self): 13 | self.stubber = Stubber(self.client) 14 | self.get_parameters_response = { 15 | 'Parameters': [ 16 | { 17 | 'Name': 'ssmkey', 18 | 'Type': 'String', 19 | 'Value': 'ssmvalue' 20 | } 21 | ], 22 | 'InvalidParameters': [ 23 | 'invalidssmparam' 24 | ] 25 | } 26 | self.invalid_get_parameters_response = { 27 | 'InvalidParameters': [ 28 | 'ssmkey' 29 | ] 30 | } 31 | self.expected_params = { 32 | 'Names': ['ssmkey'], 33 | 'WithDecryption': True 34 | } 35 | self.ssmkey = "ssmkey" 36 | self.ssmvalue = "ssmvalue" 37 | 38 | @mock.patch('stacker.lookups.handlers.ssmstore.get_session', 39 | return_value=SessionStub(client)) 40 | def test_ssmstore_handler(self, mock_client): 41 | self.stubber.add_response('get_parameters', 42 | self.get_parameters_response, 43 | self.expected_params) 44 | with self.stubber: 45 | value = SsmstoreLookup.handle(self.ssmkey) 46 | self.assertEqual(value, self.ssmvalue) 47 | self.assertIsInstance(value, str) 48 | 49 | @mock.patch('stacker.lookups.handlers.ssmstore.get_session', 50 | return_value=SessionStub(client)) 51 | def test_ssmstore_invalid_value_handler(self, mock_client): 52 | self.stubber.add_response('get_parameters', 53 | self.invalid_get_parameters_response, 54 | self.expected_params) 55 | with self.stubber: 56 | try: 57 | SsmstoreLookup.handle(self.ssmkey) 58 | except ValueError: 59 | assert True 60 | 61 | @mock.patch('stacker.lookups.handlers.ssmstore.get_session', 62 | return_value=SessionStub(client)) 63 | def test_ssmstore_handler_with_region(self, mock_client): 64 | self.stubber.add_response('get_parameters', 65 | self.get_parameters_response, 66 | self.expected_params) 67 | region = "us-east-1" 68 | temp_value = "%s@%s" % (region, self.ssmkey) 69 | with self.stubber: 70 | value = SsmstoreLookup.handle(temp_value) 71 | self.assertEqual(value, self.ssmvalue) 72 | -------------------------------------------------------------------------------- /stacker/tests/lookups/handlers/test_xref.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | import unittest 3 | 4 | from stacker.lookups.handlers.xref import XrefLookup 5 | 6 | 7 | class TestXrefHandler(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.provider = MagicMock() 11 | self.context = MagicMock() 12 | 13 | def test_xref_handler(self): 14 | self.provider.get_output.return_value = "Test Output" 15 | value = XrefLookup.handle("fully-qualified-stack-name::SomeOutput", 16 | provider=self.provider, 17 | context=self.context) 18 | self.assertEqual(value, "Test Output") 19 | self.assertEqual(self.context.get_fqn.call_count, 0) 20 | args = self.provider.get_output.call_args 21 | self.assertEqual(args[0][0], "fully-qualified-stack-name") 22 | self.assertEqual(args[0][1], "SomeOutput") 23 | -------------------------------------------------------------------------------- /stacker/tests/lookups/test_registry.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mock import MagicMock 4 | 5 | from stacker.exceptions import ( 6 | UnknownLookupType, 7 | FailedVariableLookup, 8 | ) 9 | 10 | from stacker.lookups.registry import LOOKUP_HANDLERS 11 | 12 | from stacker.variables import Variable, VariableValueLookup 13 | 14 | from ..factories import ( 15 | mock_context, 16 | mock_provider, 17 | ) 18 | 19 | 20 | class TestRegistry(unittest.TestCase): 21 | def setUp(self): 22 | self.ctx = mock_context() 23 | self.provider = mock_provider() 24 | 25 | def test_autoloaded_lookup_handlers(self): 26 | handlers = [ 27 | "output", "xref", "kms", "ssmstore", "envvar", "rxref", "ami", 28 | "file", "split", "default", "hook_data", "dynamodb", 29 | ] 30 | for handler in handlers: 31 | try: 32 | LOOKUP_HANDLERS[handler] 33 | except KeyError: 34 | self.assertTrue( 35 | False, 36 | "Lookup handler: '{}' was not registered".format(handler), 37 | ) 38 | 39 | def test_resolve_lookups_string_unknown_lookup(self): 40 | with self.assertRaises(UnknownLookupType): 41 | Variable("MyVar", "${bad_lookup foo}") 42 | 43 | def test_resolve_lookups_list_unknown_lookup(self): 44 | with self.assertRaises(UnknownLookupType): 45 | Variable( 46 | "MyVar", [ 47 | "${bad_lookup foo}", "random string", 48 | ] 49 | ) 50 | 51 | def resolve_lookups_with_output_handler_raise_valueerror(self, variable): 52 | """Mock output handler to throw ValueError, then run resolve_lookups 53 | on the given variable. 54 | """ 55 | mock_handler = MagicMock(side_effect=ValueError("Error")) 56 | 57 | # find the only lookup in the variable 58 | for value in variable._value: 59 | if isinstance(value, VariableValueLookup): 60 | value.handler = mock_handler 61 | 62 | with self.assertRaises(FailedVariableLookup) as cm: 63 | variable.resolve(self.ctx, self.provider) 64 | 65 | self.assertIsInstance(cm.exception.error, ValueError) 66 | 67 | def test_resolve_lookups_string_failed_variable_lookup(self): 68 | variable = Variable("MyVar", "${output foo::bar}") 69 | self.resolve_lookups_with_output_handler_raise_valueerror(variable) 70 | 71 | def test_resolve_lookups_list_failed_variable_lookup(self): 72 | variable = Variable( 73 | "MyVar", [ 74 | "random string", "${output foo::bar}", "random string", 75 | ] 76 | ) 77 | self.resolve_lookups_with_output_handler_raise_valueerror(variable) 78 | -------------------------------------------------------------------------------- /stacker/tests/providers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/providers/__init__.py -------------------------------------------------------------------------------- /stacker/tests/providers/aws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudtools/stacker/b357f83596e0f2044a147553ac4fbc16fe3ef97c/stacker/tests/providers/aws/__init__.py -------------------------------------------------------------------------------- /stacker/tests/test_environment.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from stacker.environment import ( 4 | DictWithSourceType, 5 | parse_environment 6 | ) 7 | 8 | test_env = """key1: value1 9 | # some: comment 10 | 11 | # here: about 12 | 13 | # key2 14 | key2: value2 15 | 16 | # another comment here 17 | key3: some:complex::value 18 | 19 | 20 | # one more here as well 21 | key4: :otherValue: 22 | key5: @value 23 | """ 24 | 25 | test_error_env = """key1: valu1 26 | error 27 | """ 28 | 29 | 30 | class TestEnvironment(unittest.TestCase): 31 | 32 | def test_simple_key_value_parsing(self): 33 | parsed_env = parse_environment(test_env) 34 | self.assertTrue(isinstance(parsed_env, DictWithSourceType)) 35 | self.assertEqual(parsed_env["key1"], "value1") 36 | self.assertEqual(parsed_env["key2"], "value2") 37 | self.assertEqual(parsed_env["key3"], "some:complex::value") 38 | self.assertEqual(parsed_env["key4"], ":otherValue:") 39 | self.assertEqual(parsed_env["key5"], "@value") 40 | self.assertEqual(len(parsed_env), 5) 41 | 42 | def test_simple_key_value_parsing_exception(self): 43 | with self.assertRaises(ValueError): 44 | parse_environment(test_error_env) 45 | 46 | def test_blank_value(self): 47 | e = """key1:""" 48 | parsed = parse_environment(e) 49 | self.assertEqual(parsed["key1"], "") 50 | -------------------------------------------------------------------------------- /stacker/tests/test_parse_user_data.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import yaml 4 | 5 | from ..tokenize_userdata import cf_tokenize 6 | 7 | 8 | class TestCfTokenize(unittest.TestCase): 9 | def test_tokenize(self): 10 | user_data = [ 11 | "field0", 12 | "Ref(\"SshKey\")", 13 | "field1", 14 | "Fn::GetAtt(\"Blah\", \"Woot\")" 15 | ] 16 | ud = yaml.dump(user_data) 17 | parts = cf_tokenize(ud) 18 | self.assertIsInstance(parts[1], dict) 19 | self.assertIsInstance(parts[3], dict) 20 | self.assertEqual(parts[1]["Ref"], "SshKey") 21 | self.assertEqual(parts[3]["Fn::GetAtt"], ["Blah", "Woot"]) 22 | self.assertEqual(len(parts), 5) 23 | -------------------------------------------------------------------------------- /stacker/tests/test_stack.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | import unittest 3 | 4 | from stacker.lookups import register_lookup_handler 5 | from stacker.context import Context 6 | from stacker.config import Config 7 | from stacker.stack import Stack 8 | from .factories import generate_definition 9 | 10 | 11 | class TestStack(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self.sd = {"name": "test"} 15 | self.config = Config({"namespace": "namespace"}) 16 | self.context = Context(config=self.config) 17 | self.stack = Stack( 18 | definition=generate_definition("vpc", 1), 19 | context=self.context, 20 | ) 21 | register_lookup_handler("noop", lambda **kwargs: "test") 22 | 23 | def test_stack_requires(self): 24 | definition = generate_definition( 25 | base_name="vpc", 26 | stack_id=1, 27 | variables={ 28 | "Var1": "${noop fakeStack3::FakeOutput}", 29 | "Var2": ( 30 | "some.template.value:${output fakeStack2::FakeOutput}:" 31 | "${output fakeStack::FakeOutput}" 32 | ), 33 | "Var3": "${output fakeStack::FakeOutput}," 34 | "${output fakeStack2::FakeOutput}", 35 | }, 36 | requires=["fakeStack"], 37 | ) 38 | stack = Stack(definition=definition, context=self.context) 39 | self.assertEqual(len(stack.requires), 2) 40 | self.assertIn( 41 | "fakeStack", 42 | stack.requires, 43 | ) 44 | self.assertIn( 45 | "fakeStack2", 46 | stack.requires, 47 | ) 48 | 49 | def test_stack_requires_circular_ref(self): 50 | definition = generate_definition( 51 | base_name="vpc", 52 | stack_id=1, 53 | variables={ 54 | "Var1": "${output vpc.1::FakeOutput}", 55 | }, 56 | ) 57 | stack = Stack(definition=definition, context=self.context) 58 | with self.assertRaises(ValueError): 59 | stack.requires 60 | 61 | def test_stack_cfn_parameters(self): 62 | definition = generate_definition( 63 | base_name="vpc", 64 | stack_id=1, 65 | variables={ 66 | "Param1": "${output fakeStack::FakeOutput}", 67 | }, 68 | ) 69 | stack = Stack(definition=definition, context=self.context) 70 | stack._blueprint = MagicMock() 71 | stack._blueprint.get_parameter_values.return_value = { 72 | "Param2": "Some Resolved Value", 73 | } 74 | self.assertEqual(len(stack.parameter_values), 1) 75 | param = stack.parameter_values["Param2"] 76 | self.assertEqual(param, "Some Resolved Value") 77 | 78 | def test_stack_tags_default(self): 79 | self.config.tags = {"environment": "prod"} 80 | definition = generate_definition( 81 | base_name="vpc", 82 | stack_id=1 83 | ) 84 | stack = Stack(definition=definition, context=self.context) 85 | self.assertEquals(stack.tags, {"environment": "prod"}) 86 | 87 | def test_stack_tags_override(self): 88 | self.config.tags = {"environment": "prod"} 89 | definition = generate_definition( 90 | base_name="vpc", 91 | stack_id=1, 92 | tags={"environment": "stage"} 93 | ) 94 | stack = Stack(definition=definition, context=self.context) 95 | self.assertEquals(stack.tags, {"environment": "stage"}) 96 | 97 | def test_stack_tags_extra(self): 98 | self.config.tags = {"environment": "prod"} 99 | definition = generate_definition( 100 | base_name="vpc", 101 | stack_id=1, 102 | tags={"app": "graph"} 103 | ) 104 | stack = Stack(definition=definition, context=self.context) 105 | self.assertEquals(stack.tags, {"environment": "prod", "app": "graph"}) 106 | 107 | 108 | if __name__ == '__main__': 109 | unittest.main() 110 | -------------------------------------------------------------------------------- /stacker/tokenize_userdata.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from troposphere import Ref, GetAtt 4 | 5 | 6 | HELPERS = { 7 | "Ref": Ref, 8 | "Fn::GetAtt": GetAtt 9 | } 10 | 11 | split_string = "(" + "|".join([r"%s\([^)]+\)" % h for h in HELPERS]) + ")" 12 | replace_string = \ 13 | r"(?P%s)\((?P['\"]?[^)]+['\"]?)+\)" % '|'.join(HELPERS) 14 | 15 | split_re = re.compile(split_string) 16 | replace_re = re.compile(replace_string) 17 | 18 | 19 | def cf_tokenize(s): 20 | """ Parses UserData for Cloudformation helper functions. 21 | 22 | http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html 23 | http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html 24 | http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-cloudformation.html#scenario-userdata-base64 25 | 26 | It breaks apart the given string at each recognized function (see HELPERS) 27 | and instantiates the helper function objects in place of those. 28 | 29 | Returns a list of parts as a result. Useful when used with Join() and 30 | Base64() CloudFormation functions to produce user data. 31 | 32 | ie: Base64(Join('', cf_tokenize(userdata_string))) 33 | """ 34 | t = [] 35 | parts = split_re.split(s) 36 | for part in parts: 37 | cf_func = replace_re.search(part) 38 | if cf_func: 39 | args = [a.strip("'\" ") for a in cf_func.group("args").split(",")] 40 | t.append(HELPERS[cf_func.group("helper")](*args).data) 41 | else: 42 | t.append(part) 43 | return t 44 | -------------------------------------------------------------------------------- /stacker/ui.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import logging 3 | from getpass import getpass 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def get_raw_input(message): 10 | """ Just a wrapper for raw_input for testing purposes. """ 11 | return input(message) 12 | 13 | 14 | class UI(object): 15 | """ This class is used internally by stacker to perform I/O with the 16 | terminal in a multithreaded environment. It ensures that two threads don't 17 | write over each other while asking a user for input (e.g. in interactive 18 | mode). 19 | """ 20 | 21 | def __init__(self): 22 | self._lock = threading.RLock() 23 | 24 | def lock(self, *args, **kwargs): 25 | """Obtains an exclusive lock on the UI for the currently executing 26 | thread.""" 27 | return self._lock.acquire() 28 | 29 | def unlock(self, *args, **kwargs): 30 | return self._lock.release() 31 | 32 | def info(self, *args, **kwargs): 33 | """Logs the line of the current thread owns the underlying lock, or 34 | blocks.""" 35 | self.lock() 36 | try: 37 | return logger.info(*args, **kwargs) 38 | finally: 39 | self.unlock() 40 | 41 | def ask(self, message): 42 | """This wraps the built-in raw_input function to ensure that only 1 43 | thread is asking for input from the user at a give time. Any process 44 | that tries to log output to the terminal will block while the user is 45 | being prompted.""" 46 | self.lock() 47 | try: 48 | return get_raw_input(message) 49 | finally: 50 | self.unlock() 51 | 52 | def getpass(self, *args): 53 | """Wraps getpass to lock the UI.""" 54 | try: 55 | self.lock() 56 | return getpass(*args) 57 | finally: 58 | self.unlock() 59 | 60 | 61 | # Global UI object for other modules to use. 62 | ui = UI() 63 | -------------------------------------------------------------------------------- /test-requirements.in: -------------------------------------------------------------------------------- 1 | pytest~=6.0 2 | pytest-cov~=2.6 3 | mock~=2.0 4 | moto[awslambda,ec2]~=3.0.0 5 | testfixtures~=6.18.3 6 | flake8 7 | pep8-naming -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | permissions: 2 | ./stacker.yaml.sh | stacker build - 3 | 4 | test: permissions 5 | $(eval AWS_ACCESS_KEY_ID := $(shell ./stacker.yaml.sh | stacker info - 2>&1 | awk '/AccessKeyId/ {print $$3}')) 6 | $(eval AWS_SECRET_ACCESS_KEY := $(shell ./stacker.yaml.sh | stacker info - 2>&1 | awk '/SecretAccessKey/ {print $$3}')) 7 | $(eval STACKER_ROLE := $(shell ./stacker.yaml.sh | stacker info - 2>&1 | awk '/FunctionalTestRole/ {print $$3}')) 8 | @STACKER_ROLE=$(STACKER_ROLE) AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) ./run_test_suite.sh ${TESTS} 9 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the functional testing suite for stacker. It exercises all of stacker against a real AWS account. Make sure you have the AWS credentials loaded into your environment when you run these steps. 2 | 3 | ## Setup 4 | 5 | 1. First, ensure that you're inside a virtualenv: 6 | 7 | ```console 8 | $ source venv/bin/activate 9 | ``` 10 | 11 | 2. Set a stacker namespace & the AWS region for the test suite to use: 12 | 13 | ```console 14 | $ export STACKER_NAMESPACE=my-stacker-test-namespace 15 | $ export AWS_DEFAULT_REGION=us-east-1 16 | ``` 17 | 18 | 3. Ensure that bats is installed: 19 | 20 | ```console 21 | # On MacOS if brew is installed 22 | $ brew install bats-core 23 | ``` 24 | 25 | 4. Setup functional test environment & run tests: 26 | 27 | ```console 28 | # To run all the tests 29 | $ make -C tests test 30 | # To run specific tests (ie: tests 1, 2 and 3) 31 | $ TESTS="1 2 3" make -C tests test 32 | ``` 33 | -------------------------------------------------------------------------------- /tests/cleanup_functional_test_buckets.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$AWS_ACCESS_KEY_ID" ] 4 | then 5 | echo "AWS_ACCESS_KEY_ID not set, skipping bucket cleanup." 6 | exit 0 7 | fi 8 | 9 | sudo pip install awscli 10 | 11 | ALL_BUT_LAST_6_BUCKETS=$(aws s3 ls | grep stacker-cloudtools-functional-tests- | sort -r | tail -n +7 | awk '{print $3}') 12 | 13 | for bucket in ${ALL_BUT_LAST_6_BUCKETS} 14 | do 15 | echo "## Deleting bucket: 's3://$bucket'" 16 | aws --region us-east-1 s3 rm --recursive s3://$bucket/ 17 | aws --region us-east-1 s3 rb s3://$bucket 18 | done 19 | -------------------------------------------------------------------------------- /tests/fixtures/blueprints/test_repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "repo1Repository": { 4 | "Properties": { 5 | "RepositoryName": "repo1" 6 | }, 7 | "Type": "AWS::ECR::Repository" 8 | }, 9 | "repo2Repository": { 10 | "Properties": { 11 | "RepositoryName": "repo2" 12 | }, 13 | "Type": "AWS::ECR::Repository" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/fixtures/stack_policies/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Allow", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/stack_policies/none.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Deny", 5 | "Action" : "Update:*", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/run_test_suite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TEST_ARGS=$* 4 | 5 | if [ -z "$TEST_ARGS" ] 6 | then 7 | _TESTS="test_suite" 8 | else 9 | for T in ${TEST_ARGS} 10 | do 11 | _TESTS="${_TESTS} test_suite/$(printf %02d ${T})_*" 12 | done 13 | fi 14 | 15 | echo "bats ${_TESTS}" 16 | 17 | bats ${_TESTS} 18 | -------------------------------------------------------------------------------- /tests/stacker.yaml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat - <&2 echo "To run these tests, you must set a STACKER_NAMESPACE environment variable" 9 | exit 1 10 | fi 11 | 12 | if [ -z "$STACKER_ROLE" ]; then 13 | >&2 echo "To run these tests, you must set a STACKER_ROLE environment variable" 14 | exit 1 15 | fi 16 | 17 | # Setup a base .aws/config that can be use to test stack configurations that 18 | # require stacker to assume a role. 19 | export AWS_CONFIG_DIR=$(mktemp -d) 20 | export AWS_CONFIG_FILE="$AWS_CONFIG_DIR/config" 21 | 22 | cat < "$AWS_CONFIG_FILE" 23 | [default] 24 | region = us-east-1 25 | 26 | [profile stacker] 27 | region = us-east-1 28 | role_arn = ${STACKER_ROLE} 29 | credential_source = Environment 30 | EOF 31 | 32 | # Simple wrapper around the builtin bash `test` command. 33 | assert() { 34 | builtin test "$@" 35 | } 36 | 37 | # Checks that the given line is in $output. 38 | assert_has_line() { 39 | echo "$output" | grep "$@" 1>/dev/null 40 | } 41 | 42 | # This helper wraps "stacker" with bats' "run" and also outputs debug 43 | # information. If you need to execute the stacker binary _without_ calling 44 | # "run", you can use "command stacker". 45 | stacker() { 46 | # Sleep between runs of stacker to try and avoid rate limiting issues. 47 | sleep 2 48 | echo "$ stacker $@" 49 | run command stacker "$@" 50 | echo "$output" 51 | echo 52 | } 53 | 54 | # A helper to tag a test as requiring access to AWS. If no credentials are set, 55 | # then the tests will be skipped. 56 | needs_aws() { 57 | if [ -z "$AWS_ACCESS_KEY_ID" ]; then 58 | skip "aws credentials not set" 59 | fi 60 | } 61 | -------------------------------------------------------------------------------- /tests/test_suite/01_stacker_build_no_config.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../test_helper 4 | 5 | @test "stacker build - no config" { 6 | stacker build 7 | assert ! "$status" -eq 0 8 | assert_has_line -E "too few arguments|the following arguments are required: config" 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_suite/02_stacker_build_empty_config.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | # 3 | load ../test_helper 4 | 5 | @test "stacker build - empty config" { 6 | stacker build <(echo "") 7 | assert ! "$status" -eq 0 8 | assert_has_line 'stacker.exceptions.InvalidConfig:' 9 | } 10 | -------------------------------------------------------------------------------- /tests/test_suite/03_stacker_build-config_with_no_stacks.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../test_helper 4 | 5 | @test "stacker build - config with no stacks" { 6 | needs_aws 7 | 8 | stacker build - < "vpc";' 35 | assert_has_line '"bastion2" -> "vpc";' 36 | assert_has_line '"app1" -> "bastion1";' 37 | assert_has_line '"app2" -> "bastion2";' 38 | assert $(echo "$output" | grep -A 2 vpc | tail -n 2 | grep -c vpc) = '0' 39 | } 40 | -------------------------------------------------------------------------------- /tests/test_suite/09_stacker_build-missing_variable.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load ../test_helper 4 | 5 | @test "stacker build - missing variable" { 6 | needs_aws 7 | 8 | stacker build - <