├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── box.json ├── composer.json ├── doc ├── AwsSdk │ └── index.rst ├── Blueprints │ ├── Images │ │ └── overview.png │ └── index.rst ├── Contributing │ └── index.rst ├── FilePaths │ └── index.rst ├── Functions │ └── index.rst ├── GettingStarted │ ├── Images │ │ ├── kickstart_demo_deploy.png │ │ ├── kickstart_demo_ec2list.png │ │ └── kickstart_demo_stacklist.png │ └── index.rst ├── Images │ ├── StackFormation.svg │ └── stackformation_200px.png ├── Makefile ├── Misc │ └── index.rst ├── ShellCommands │ └── index.rst ├── Stack │ └── index.rst ├── StackPolicies │ └── index.rst ├── StackReferences │ └── index.rst ├── Templates │ └── index.rst ├── UserData │ └── index.rst ├── conf.py ├── index.rst └── make.bat ├── docker ├── README.md ├── _base.sh ├── build.sh ├── golang │ └── Dockerfile ├── php-fpm.conf ├── push.sh └── slim │ └── Dockerfile ├── misc └── convert_fn_join_to_inline_ref.php ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── AwsInspector │ ├── Command │ │ ├── Agent │ │ │ └── AddIdentityCommand.php │ │ ├── Clean │ │ │ └── CleanSnapshotsCommand.php │ │ ├── CloudwatchLogs │ │ │ ├── AddLambdaTriggerCommand.php │ │ │ ├── DeleteLambdaTriggerCommand.php │ │ │ ├── DeleteLogGroupCommand.php │ │ │ ├── SetRetentionCommand.php │ │ │ ├── ShowLogGroupsCommand.php │ │ │ └── TailCommand.php │ │ ├── Ec2 │ │ │ ├── AbstractCommand.php │ │ │ ├── ListCommand.php │ │ │ ├── SshCommand.php │ │ │ └── TerminateCommand.php │ │ └── Profile │ │ │ ├── DisableCommand.php │ │ │ ├── EnableCommand.php │ │ │ └── ListCommand.php │ ├── CommandRegistry.php │ ├── Helper │ │ └── Curl.php │ ├── Model │ │ ├── AbstractResource.php │ │ ├── AutoScaling │ │ │ ├── AutoScalingGroup.php │ │ │ ├── LaunchConfiguration.php │ │ │ └── Repository.php │ │ ├── CloudWatchLogs │ │ │ ├── LogGroup.php │ │ │ ├── LogStream.php │ │ │ └── Repository.php │ │ ├── Collection.php │ │ ├── Ebs │ │ │ ├── Repository.php │ │ │ └── Volume.php │ │ ├── Ec2 │ │ │ ├── CollectionRegistry.php │ │ │ ├── Factory.php │ │ │ ├── Instance.php │ │ │ └── Repository.php │ │ ├── ElastiCache │ │ │ ├── CacheCluster.php │ │ │ └── Repository.php │ │ ├── Elb │ │ │ ├── Elb.php │ │ │ └── Repository.php │ │ ├── Iam │ │ │ ├── Repository.php │ │ │ └── User.php │ │ ├── Rds │ │ │ ├── Database.php │ │ │ └── Repository.php │ │ ├── Route53 │ │ │ └── Repository.php │ │ └── SecurityGroup │ │ │ ├── Repository.php │ │ │ └── SecurityGroup.php │ ├── Registry.php │ ├── SdkFactory.php │ └── Ssh │ │ ├── Agent.php │ │ ├── Command.php │ │ ├── Connection.php │ │ ├── Identity.php │ │ ├── LocalConnection.php │ │ └── PrivateKey.php ├── StackFormation │ ├── Blueprint.php │ ├── BlueprintAction.php │ ├── BlueprintFactory.php │ ├── Command │ │ ├── AbstractCommand.php │ │ ├── Blueprint │ │ │ ├── AbstractBlueprintCommand.php │ │ │ ├── DeployCommand.php │ │ │ ├── DeployPolicyCommand.php │ │ │ ├── Show │ │ │ │ ├── ChangesetCommand.php │ │ │ │ ├── DependenciesCommand.php │ │ │ │ ├── ParametersCommand.php │ │ │ │ ├── StacknameCommand.php │ │ │ │ └── TemplateCommand.php │ │ │ └── ValidateCommand.php │ │ ├── SetupCommand.php │ │ └── Stack │ │ │ ├── AbstractStackCommand.php │ │ │ ├── CompareAllCommand.php │ │ │ ├── DeleteCommand.php │ │ │ ├── DiffCommand.php │ │ │ ├── ListCommand.php │ │ │ ├── ObserveCommand.php │ │ │ ├── Show │ │ │ ├── AbstractShowCommand.php │ │ │ ├── DependantsCommand.php │ │ │ ├── OutputsCommand.php │ │ │ ├── ParametersCommand.php │ │ │ └── ResourcesCommand.php │ │ │ ├── TimelineCommand.php │ │ │ └── TreeCommand.php │ ├── CommandRegistry.php │ ├── Config.php │ ├── ConfigTreeBuilder.php │ ├── DependencyTracker.php │ ├── Diff.php │ ├── Exception │ │ ├── BlueprintNotFoundException.php │ │ ├── BlueprintReferenceNotFoundException.php │ │ ├── CleanCloudFormationException.php │ │ ├── InvalidStackNameException.php │ │ ├── MissingEnvVarException.php │ │ ├── NoBlueprintsFoundException.php │ │ ├── OperationAbortedException.php │ │ ├── OutputNotFoundException.php │ │ ├── ParameterNotFoundException.php │ │ ├── ResourceNotFoundException.php │ │ ├── StackCannotBeUpdatedException.php │ │ ├── StackNoUpdatesToBePerformedException.php │ │ ├── StackNotFoundException.php │ │ ├── TagNotFoundException.php │ │ └── ValueResolverException.php │ ├── Helper │ │ ├── Cache.php │ │ ├── ChangeSetTable.php │ │ ├── Decorator.php │ │ ├── Div.php │ │ ├── Exception.php │ │ ├── Finder.php │ │ ├── Pipeline.php │ │ ├── StackEventsTable.php │ │ ├── StaticCache.php │ │ └── Validator.php │ ├── Observer.php │ ├── Poller.php │ ├── PrefixedTemplate.php │ ├── Preprocessor.php │ ├── Profile │ │ ├── Manager.php │ │ └── YamlCredentialProvider.php │ ├── Stack.php │ ├── StackFactory.php │ ├── Template.php │ ├── TemplateDecodeException.php │ ├── TemplateInvalidException.php │ ├── TemplateMerger.php │ ├── Terminator.php │ └── ValueResolver │ │ ├── Stage │ │ ├── AbstractValueResolverStage.php │ │ ├── Clean.php │ │ ├── ConditionalValue.php │ │ ├── EnvironmentVariable.php │ │ ├── EnvironmentVariableWithFallback.php │ │ ├── Md5.php │ │ ├── ProfileSwitcher.php │ │ ├── StackOutput.php │ │ ├── StackParameter.php │ │ ├── StackResource.php │ │ ├── Tstamp.php │ │ └── Variable.php │ │ └── ValueResolver.php ├── dotenv.php └── stackformation.php └── tests ├── AwsInspector ├── Helper │ └── CurlTest.php ├── Model │ ├── AbstractResourceTest.php │ ├── AutoScaling │ │ ├── AutoScalingGroupTest.php │ │ └── RepositoryTest.php │ ├── CollectionTest.php │ └── Ec2 │ │ └── InstanceTest.php ├── RegistryTest.php └── Ssh │ ├── CommandTest.php │ └── ConnectionTest.php ├── StackFormation ├── BlueprintActionTest.php ├── BlueprintGetParameterTest.php ├── BlueprintTest.php ├── CacheTest.php ├── ConfigTest.php ├── HelperTest.php ├── PollerTest.php ├── PreprocessorTest.php ├── ProfileManagerTest.php ├── StackTest.php ├── ValidateTagsTest.php ├── ValueResolver │ └── Stage │ │ └── ConditionalValueTest.php └── ValueResolverTest.php ├── fixtures ├── Config │ ├── blueprint.1.yml │ ├── blueprint.conditional_value.yml │ ├── blueprint.conditional_vars.yml │ ├── blueprint.duplicate_stackname.yml │ ├── blueprint.duplicateglobalvar.yml │ ├── blueprint.invalid_attribute.yml │ ├── blueprint.reference.yml │ ├── blueprint.select_profile.yml │ ├── blueprint.switch_profile.yml │ ├── blueprint.template_missing.yml │ ├── blueprint.templatefile_missing.yml │ ├── dummy.template │ └── dummy_policy.json ├── Preprocessor │ ├── injectFileContentFnFileContentFromParentDirectory │ │ ├── blueprint │ │ │ ├── expected.template │ │ │ └── input.template │ │ └── userdata.sh │ ├── injectFileContentFnFileContentFromSameDirectory │ │ └── blueprint │ │ │ ├── expected.template │ │ │ ├── input.template │ │ │ └── userdata.sh │ └── injectFileContentFnFileContentFromSubDirectory │ │ └── blueprint │ │ ├── expected.template │ │ ├── input.template │ │ └── userdata │ │ └── userdata.sh ├── ProfileManager │ ├── fixture_basic │ │ └── profiles.yml │ ├── fixture_before_scripts │ │ └── profiles.yml │ ├── fixture_encrpyted │ │ └── profiles.yml.encrypted │ ├── fixture_encrpyted_mix │ │ ├── profiles.personal.yml │ │ └── profiles.yml.encrypted │ └── fixture_merge_profiles │ │ ├── profiles.personal.yml │ │ └── profiles.yml ├── RunBeforeScript │ └── foo.txt ├── Stack │ ├── test-stack1.describeStackResources.json │ └── test-stack1.json ├── foo.pem └── resolve_md5.txt └── phpunit.bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /composer.lock 3 | /vendor/ 4 | *.phar 5 | doc/_build 6 | docker/golang/php-fpm.conf 7 | docker/slim/php-fpm.conf 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | matrix: 8 | allow_failures: 9 | - php: 7.0 10 | 11 | before_script: 12 | - composer install --no-progress --no-interaction 13 | script: 14 | - phpunit --debug --coverage-clover build/logs/clover.xml --log-junit build/logs/junit.xml 15 | after_script: 16 | - vendor/bin/test-reporter 17 | 18 | notifications: 19 | email: 20 | recipients: 21 | - travisci@fabrizio-branca.de 22 | on_success: always 23 | on_failure: always 24 | 25 | addons: 26 | code_climate: 27 | repo_token: 28 | secure: Qhy+BuyLPC7D87NFvKJC47CLUyJmRzIcxQSY1VIZoKWerxD42THMLlwU4DwIbIdHjTMDVV6F+fYvX10XddXksjMSt7iCrlCU8ICF9VmT2QbGFgn910FBydSQCB5yrqgEASsZnAzxnO34Hcea8545gMhCNfFnpCcWjnMjNSKU8JgwYuowb+9aneLibWuMsOBixAS9ziBmxrw34Uozn6nLE2y3i9DVxdl4J3D9qslzc+qp76U5zGx14Jl4aZ0IrR8oQZZnSmiJqSGtk2WJK/R8lOFp/v/+a76Jhu/OJA0Xfx7hGSwvk8luF0K/uuIdHfOO91T0DvK7lBuMEPL1eytN+mIzuplDk58+DMORMTKsriqHLI6l+gblwMg3k/rvI29mwjhvYkdunP5WwckdZYEGMcAa3J475C9vtkjntc+8amGQ3+FeeU9V+mff852raYWs4KYCfvuMF5Gmy7aixpqZAHxn3tXenfv8HIFP67P2qbE5w+ErtbVJMl7HVY2t4pWVS+GMitbBvjqnunbIvAK9fThqlDkAD3PZ5nQR3Ad6LvLlMR2KIEnNdjaA9+XxsLpsqNfG2WbrKFmQBPOfXDFguiB2wEqtpB1YLtSZIk+aGmLHOqOC9f6ZklrtuxijIA0G46I9wJI9tASjRTdXtxzuMeiqJJEKzNRFfjxMIZaWuX0= 29 | 30 | before_deploy: 31 | - curl -LSs https://box-project.github.io/box2/installer.php | php 32 | - env 33 | - php box.phar --no-interaction build 34 | deploy: 35 | provider: releases 36 | api_key: 37 | secure: PRTuXlZ9Y83g22H+nukMCpK2HmchmpNNJIS7aT3ywxXOV8qbtmGITEBKe0RgkFo4odANGy4P762RpmOxWY+e081q/4gVztr+fjTmLjzvRSGvD5dPs8j298uedJ25zdo57Raq5E0WC3FEkar91SIk37S73IvDhtzKFMJAkKPSmZ5vYFMRu0CUIQ2wN/T/gMnMytjQbcgVn1vGai3xilN/YNukuv4iuj1aBIEZXnJNKUkz6gpo3nNqHb8PHoYkv7SfxMwEda7joYNFu4p6eU5mlwHoY1gv1iUNKHLRYvFOQAXthbbp2NxImHehtxOy1UFHiz+onL1nTciBgQbjysQgKpZz0ByEB7OKGu/59x1P2F2g9cxUSpqjx4i4M9bI7ALweE7BnyfZj4vHW57kFOmsGlAwaEsJIdJwZS8PXHDy575x3HGY2HdXVBySBRHBbyjUdlKnhp42W8R9+wA56wBK836DBlFFJgxRBdViuZsu08mcR4VnsPMU8oykhfhI8QNCnSV8A5/Hcz2NGfXJi5UV9MMMXfiPLYfe4K2nABBiWu7P6gxQwjPbz4N3CipzFD0cOakGanWnjDIsshTqA/Tn+pl7BGbqfkXciZe5hUuEbhGGk/Jj8i5IsU7ZipArK1sRCMwSSpSEqKQ9EvTxlgceiNw0LBVWrsBxGOvUN88GV3I= 38 | file: stackformation.phar 39 | on: 40 | repo: AOEpeople/StackFormation 41 | branch: master 42 | tags: true 43 | php: "5.6" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackFormation 2 | 3 | **Lightweight AWS CloudFormation Stack, Template and Parameter Manager and Preprocessor** 4 | 5 | 6 | [![Build Status](https://travis-ci.org/AOEpeople/StackFormation.svg?branch=master)](https://travis-ci.org/AOEpeople/StackFormation) 7 | [![Code Climate](https://codeclimate.com/github/AOEpeople/StackFormation/badges/gpa.svg)](https://codeclimate.com/github/AOEpeople/StackFormation) 8 | [![Test Coverage](https://codeclimate.com/github/AOEpeople/StackFormation/badges/coverage.svg)](https://codeclimate.com/github/AOEpeople/StackFormation/coverage) 9 | [![Documentation](https://readthedocs.org/projects/stackformation/badge/?version=latest)](http://stackformation.readthedocs.io) 10 | 11 | If you want to use StackFormation, please have a look at the [StackFormation documentation](http://stackformation.readthedocs.org) 12 | 13 | ### License 14 | [Open Software License v. 3.0 (OSL-3.0)](https://github.com/AOEpeople/StackFormation/blob/master/LICENSE.md) 15 | 16 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "src/stackformation.php", 3 | "output": "stackformation.phar", 4 | "chmod": "0775", 5 | "compression": "GZ", 6 | "extract": false, 7 | "files": [ "vendor/autoload.php" ], 8 | "directories": [ "src" ], 9 | "finder": [{ "name": "*.php", "in": "vendor" }], 10 | "git-version": "package_version", 11 | "stub": true 12 | } 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aoepeople/stackformation", 3 | "description": "Lightweight AWS CloudFormation Stack Manager", 4 | "license": "OSL-3.0", 5 | "authors": [{ 6 | "name": "Fabrizio Branca", 7 | "email": "firstname.lastname@aoe.com", 8 | "homepage": "https://github.com/fbrnc", 9 | "role": "Author" 10 | }, { 11 | "name": "Lee Saferite", 12 | "email": "lee.saferite@aoe.com", 13 | "homepage": "https://github.com/LeeSaferite", 14 | "role": "Contributor" 15 | }, { 16 | "name": "Daniel Niedergesäß", 17 | "email": "dn@smart-devs.rocks", 18 | "homepage": "https://github.com/smart-devs", 19 | "role": "Contributor" 20 | }], 21 | "require": { 22 | "symfony/console": "^3.0.0", 23 | "symfony/yaml": "^3.0.0", 24 | "symfony/config": "^3.0.0", 25 | "symfony/finder": "^3.1", 26 | "aws/aws-sdk-php": "^3.52", 27 | "tedivm/jshrink": "^1.1", 28 | "vlucas/phpdotenv": "~2.1", 29 | "monolog/monolog": "~1", 30 | "php": ">=5.5.0" 31 | }, 32 | "suggest": { 33 | "aoepeople/vault": "Allows encrypting/decrypting ssh keys and profiles on the fly" 34 | }, 35 | "replace": { 36 | "aoepeople/awsinspector": "*" 37 | }, 38 | "require-dev": { 39 | "squizlabs/php_codesniffer": "^2.4", 40 | "codeclimate/php-test-reporter": "0.3.2", 41 | "phpunit/phpunit": "~4.0" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "StackFormation\\": "src/StackFormation/", 46 | "AwsInspector\\": "src/AwsInspector/", 47 | "StackFormation\\Tests\\": "tests/StackFormation/", 48 | "AwsInspector\\Tests\\": "tests/AwsInspector/" 49 | }, 50 | "files": ["src/dotenv.php"] 51 | }, 52 | "bin": [ "src/stackformation.php" ] 53 | } 54 | -------------------------------------------------------------------------------- /doc/AwsSdk/index.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | AWS Sdk 3 | ********* 4 | 5 | StackFormation uses the AWS SDK for PHP. You should configure your keys in env vars: 6 | 7 | .. code-block:: shell 8 | 9 | $ export AWS_ACCESS_KEY_ID=INSERT_YOUR_ACCESS_KEY 10 | $ export AWS_SECRET_ACCESS_KEY=INSERT_YOUR_PRIVATE_KEY 11 | $ export AWS_DEFAULT_REGION=eu-west-1 -------------------------------------------------------------------------------- /doc/Blueprints/Images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/StackFormation/5332dbbe54653e50d610cbaf75fb865c68aa2f1e/doc/Blueprints/Images/overview.png -------------------------------------------------------------------------------- /doc/Contributing/index.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Contributing 3 | ************ 4 | 5 | Your contributions are always welcome! 6 | Please feel free to fork this repository and submit pull request as many you want! If you have any questions please feel free to contact us. 7 | 8 | Contributors 9 | ============ 10 | 11 | - `Fabrizio Branca `__ 12 | - `Julian Kleinhans `__ 13 | - `Lee Saferite `__ 14 | - `Daniel Niedergesäß `__ -------------------------------------------------------------------------------- /doc/FilePaths/index.rst: -------------------------------------------------------------------------------- 1 | ********** 2 | File paths 3 | ********** 4 | 5 | Relative file paths 6 | =================== 7 | 8 | Please note that all files paths in the ``template`` section of a ``blueprints.yml`` are relative to the current ``blueprints.yml`` file and all files included via ``Fn::FileContent``/ ``Fn:FileContentTrimLines`` or ``Fn:FileContentMinify`` are relative to the CloudFormation template file. 9 | 10 | Example: 11 | 12 | .. code-block:: text 13 | 14 | blueprints/ 15 | stack1/ 16 | userdata/ 17 | provisioning.sh 18 | blueprints.yml 19 | my.template 20 | 21 | blueprints.yml: 22 | 23 | .. code-block:: yaml 24 | 25 | blueprints: 26 | - stackname: test 27 | template: my.template 28 | 29 | my.template 30 | 31 | .. code-block:: json 32 | 33 | { [...] 34 | "Ec2Instance": { 35 | "Type": "AWS::AutoScaling::LaunchConfiguration", 36 | "Properties": { 37 | "UserData": {"Fn::Base64": {"Fn::FileContent": "userdata/provisioning.sh"}} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /doc/Functions/index.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Functions 3 | ********* 4 | 5 | Fn::FileContent 6 | =============== 7 | 8 | Before uploading CloudFormation template to the API there's some pre-processing going on: I've introduced a new function "FileContent" that accepts a path to a file. This file will be read, converted into JSON (using ``Fn::Join``). The path is relative to the path of the current CloudFormation template file. 9 | 10 | Usage Example: 11 | 12 | .. code-block:: json 13 | 14 | [...] 15 | "UserData": {"Fn::Base64": {"Fn::FileContent":"../scripts/setup.sh"}}, 16 | [...] 17 | 18 | Fn::FileContentTrimLines 19 | ======================== 20 | 21 | These function are similar to ``Fn::FileContent`` but additional it trim whitespace. This comes in handy when deploying Lambda function where the content can't be larger than 2048kb if you want to directly embed the source code via CloudFormation (instead of deploying a zip file). 22 | 23 | Fn::FileContentMinify 24 | ======================== 25 | 26 | These function are similar to ``Fn::FileContent`` but additional it minify the code. This comes in handy when deploying Lambda function where the content can't be larger than 2048kb if you want to directly embed the source code via CloudFormation (instead of deploying a zip file). 27 | 28 | Fn::FileContentUnpretty 29 | ======================= 30 | 31 | This function is the same as ``Fn::FileContent`` expect it will return the resulting JSON without formatting it, which will reduce the file size significantly due to the missing whitespace in the JSON structure (not inside the file content!) This is useful if you're seeing the "...at 'templateBody' failed to satisfy constraint: Member must have length less than or equal to 51200" error message. 32 | 33 | Fn::Split 34 | ========= 35 | 36 | Sometimes you have a dynamic number of array items. ``Fn::Split`` allows you to configure them as a single string and transforms them into an array: 37 | 38 | .. code-block:: json 39 | 40 | "Aliases": { "Fn::Split": [",", "www.example.com,cdn.example.com"]} 41 | 42 | results in: 43 | 44 | .. code-block:: json 45 | 46 | "Aliases": ["www.example.com", "cdn.example.com"] 47 | -------------------------------------------------------------------------------- /doc/GettingStarted/Images/kickstart_demo_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/StackFormation/5332dbbe54653e50d610cbaf75fb865c68aa2f1e/doc/GettingStarted/Images/kickstart_demo_deploy.png -------------------------------------------------------------------------------- /doc/GettingStarted/Images/kickstart_demo_ec2list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/StackFormation/5332dbbe54653e50d610cbaf75fb865c68aa2f1e/doc/GettingStarted/Images/kickstart_demo_ec2list.png -------------------------------------------------------------------------------- /doc/GettingStarted/Images/kickstart_demo_stacklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/StackFormation/5332dbbe54653e50d610cbaf75fb865c68aa2f1e/doc/GettingStarted/Images/kickstart_demo_stacklist.png -------------------------------------------------------------------------------- /doc/Images/stackformation_200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/StackFormation/5332dbbe54653e50d610cbaf75fb865c68aa2f1e/doc/Images/stackformation_200px.png -------------------------------------------------------------------------------- /doc/Misc/index.rst: -------------------------------------------------------------------------------- 1 | **** 2 | Misc 3 | **** 4 | 5 | Use the ``jq`` tool to create a simple list of all parameters (almost) ready to paste it in the blueprints.yml 6 | 7 | .. code-block:: shell 8 | 9 | $ cat my.template | jq '.Parameters | keys' | sed 's/",/: \'\'/g' | sed 's/"//g' 10 | -------------------------------------------------------------------------------- /doc/Stack/index.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Stack 3 | ***** 4 | 5 | Stackname filter 6 | ================ 7 | 8 | You can configure a regular expression in the ``STACKFORMATION_NAME_FILTER`` environment variable (e.g. via ``.env.default``) which will filter all your stack lists to the stacks matching this pattern. This is useful if you have a naming convention in place and you don't want to see other team's stacks in your list. 9 | 10 | Example: 11 | 12 | .. code-block:: text 13 | 14 | STACKFORMATION_NAME_FILTER=/^myproject-(a|b)-/ 15 | -------------------------------------------------------------------------------- /doc/StackPolicies/index.rst: -------------------------------------------------------------------------------- 1 | ************** 2 | Stack policies 3 | ************** 4 | 5 | Using stack policies 6 | ==================== 7 | 8 | To prevent stack resources from being unintentionally updated or deleted during a stack update you can use `stack policies `__. Stack policies apply only during stack updates and should be used only as a fail-safe mechanism to prevent accidental updates to certain stack resources. 9 | 10 | It's suggested to create a stack\_policies directory below the corresponding stack directory: 11 | 12 | .. code-block:: yaml 13 | 14 | blueprints/ 15 | stack1/ 16 | stack_policies/ 17 | blueprints.yml 18 | ... 19 | stack2/ 20 | stack_policies/ 21 | blueprints.yml 22 | ... 23 | ... 24 | 25 | You have to tell StackFormation where it could find the stack policy. 26 | 27 | Example: 28 | 29 | .. code-block:: yaml 30 | 31 | blueprints: 32 | - stackname: 'my-stack' 33 | template: 'templates/my-stack.template' 34 | stackPolicy: 'stack_policies/my-stack.json' 35 | -------------------------------------------------------------------------------- /doc/StackReferences/index.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | Stack references 3 | **************** 4 | 5 | Referencing outputs/resources/parameters from other stacks 6 | 7 | TODO 8 | -------------------------------------------------------------------------------- /doc/UserData/index.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | User data 3 | ********* 4 | 5 | Inject user data 6 | ================ 7 | 8 | TODO 9 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ###################################### 2 | StackFormation |version| Documentation 3 | ###################################### 4 | 5 | .. image:: Images/stackformation_200px.png 6 | :align: right 7 | 8 | |Build Status| |Code Climate| |Test Coverage| 9 | 10 | **Lightweight AWS CloudFormation Stack, Template and Parameter Manager and Preprocessor** 11 | 12 | Deploying CloudFormation stacks to AWS can be done using the AWS Console, AWS Cli or any SDK. While this is perfectly ok it can be a challenge to keep track of what template is used for what stack and manage the input parameters. 13 | This is where "StackFormation" comes in. 14 | 15 | StackFormation (note the wordplay: CloudFormation / Stacks) will read `blueprint.yml` files that contains information about stacks, the templates they use and their input parameters. It also allows you to query values for input parameters from other stack's resources or outputs. 16 | In addition to that StackFormation makes it easy to embed scripts into UserData. 17 | 18 | .. image:: Images/StackFormation.svg 19 | 20 | If you have any questions please feel free to contact us: 21 | 22 | - `Fabrizio Branca `__ 23 | - `Julian Kleinhans `__ 24 | 25 | This version of the documentation covering StackFormation |release| has been rendered at: |today| 26 | 27 | .. toctree:: 28 | :maxdepth: 3 29 | :caption: Getting started Documentation 30 | 31 | GettingStarted/index 32 | 33 | .. toctree:: 34 | :maxdepth: 3 35 | :caption: StackFormation Documentation 36 | 37 | Blueprints/index 38 | Templates/index 39 | StackReferences/index 40 | UserData/index 41 | Stack/index 42 | StackPolicies/index 43 | Functions/index 44 | FilePaths/index 45 | ShellCommands/index 46 | AwsSdk/index 47 | Misc/index 48 | 49 | .. toctree:: 50 | :maxdepth: 3 51 | :caption: User Documentation 52 | 53 | Contributing/index 54 | 55 | **License** 56 | 57 | `Open Software License v. 3.0 (OSL-3.0) `__ 58 | 59 | .. |Build Status| image:: https://travis-ci.org/AOEpeople/StackFormation.svg?branch=master 60 | :target: https://travis-ci.org/AOEpeople/StackFormation 61 | .. |Code Climate| image:: https://codeclimate.com/github/AOEpeople/StackFormation/badges/gpa.svg 62 | :target: https://codeclimate.com/github/AOEpeople/StackFormation 63 | .. |Test Coverage| image:: https://codeclimate.com/github/AOEpeople/StackFormation/badges/coverage.svg 64 | :target: https://codeclimate.com/github/AOEpeople/StackFormation/coverage 65 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | 2 | # AWS StackFormation with Docker 3 | **Lightweight AWS CloudFormation Stack, Template and Parameter Manager and Preprocessor** 4 | - **Github:** https://github.com/AOEpeople/StackFormation 5 | - **Documentation:** http://stackformation.readthedocs.io/en/latest/ 6 | - **DockerHub:** https://hub.docker.com/r/kj187/stackformation/ 7 | 8 | 9 | # Available tags 10 | 11 | - `4.3.6`, `latest` (includes PHP 7.0) 12 | - `4.3.6-golang`, `latest-golang` (includes PHP 7.0, Golang 1.6.2 linux/amd64) 13 | 14 | ### Golang version 15 | The Golang version includes the slim version as basis and on top it supports golang. 16 | Golang could be used for AWS Lambda for instance 17 | (see https://github.com/kj187/aws_stackformation_templates/tree/master/blueprints/lambda/golang) 18 | 19 | ## Usage 20 | 21 | ``` 22 | $ docker run --rm -it -v $(pwd):/app -w /app kj187/stackformation:latest 23 | ``` 24 | 25 | Or if you want to use lambda with golang 26 | ``` 27 | $ docker run --rm -it -v $(pwd):/app -w /app kj187/stackformation:latest-golang 28 | ``` 29 | 30 | ## Build a new image version 31 | 32 | ``` 33 | $ git clone git@github.com:AOEpeople/StackFormation.git 34 | $ ./StackFormation/docker/build.sh STACKFORMATION_GITHUB_RELEASE_VERSION 35 | ``` 36 | 37 | STACKFORMATION_GITHUB_RELEASE_VERSION 38 | Github release version which includes an automated generated phar file 39 | 40 | Push to docker hub 41 | 42 | ``` 43 | $ ./StackFormation/docker/push.sh STACKFORMATION_GITHUB_RELEASE_VERSION 44 | ``` 45 | -------------------------------------------------------------------------------- /docker/_base.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function echoerr { 4 | echo "============================================" 1>&2; 5 | echo "ERROR: $@" 1>&2; 6 | echo "============================================" 1>&2; 7 | } 8 | function error_exit { echoerr "$1"; exit 1; } 9 | 10 | if [ -z "$1" ] ; then error_exit "Version number not set!"; fi 11 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 12 | 13 | clean() { 14 | rm -rf $DIR/slim/php-fpm.conf 15 | rm -rf $DIR/golang/php-fpm.conf 16 | } 17 | 18 | cp php-fpm.conf ./slim 19 | cp php-fpm.conf ./golang 20 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . $PWD/_base.sh 4 | 5 | build() { 6 | docker_repo=$1 7 | sf_release=$2 8 | img_tag=$3 9 | img_latest_tag=$4 10 | 11 | echo ">>>> Build $docker_repo:$img_tag and $docker_repo:$img_latest_tag" 12 | echo ">>>>>> docker build -t $docker_repo:$img_tag -t $docker_repo:$img_latest_tag --build-arg STACKFORMATION_VERSION=$sf_release ." 13 | docker build -t $docker_repo:$img_tag -t $docker_repo:$img_latest_tag --build-arg STACKFORMATION_VERSION=$sf_release . || error_exit "Cant build image" 14 | } 15 | 16 | echo '>> Start building process' 17 | 18 | cd $DIR/slim 19 | echo ">>> Building Slim version" 20 | build kj187/stackformation $1 $1 latest 21 | 22 | cd $DIR/golang 23 | echo ">>> Building Golang version" 24 | build kj187/stackformation $1 $1-golang latest-golang 25 | 26 | echo '>> Building finished' 27 | 28 | clean 29 | -------------------------------------------------------------------------------- /docker/golang/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | MAINTAINER Julian Kleinhans 4 | 5 | ARG STACKFORMATION_VERSION 6 | 7 | RUN locale-gen en_US.UTF-8 8 | ENV LANG en_US.UTF-8 9 | ENV LANGUAGE en_US:en 10 | ENV LC_ALL en_US.UTF-8 11 | 12 | ## Common 13 | RUN apt-get update \ 14 | && apt-get install -y sudo wget curl zip unzip git software-properties-common 15 | 16 | 17 | ## AWS cli 18 | RUN apt-get install -y python3 19 | RUN curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" \ 20 | && unzip awscli-bundle.zip \ 21 | && /usr/bin/python3 awscli-bundle/install -i /usr/local/aws -b /usr/bin/aws 22 | 23 | 24 | ## PHP 25 | RUN add-apt-repository -y ppa:ondrej/php \ 26 | && apt-get update \ 27 | && apt-get install -y php7.0-fpm php7.0-cli php7.0-mcrypt php7.0-gd php7.0-mysql \ 28 | php7.0-pgsql php7.0-imap php-memcached php7.0-mbstring php7.0-xml php7.0-curl \ 29 | php7.0-sqlite3 php7.0-xdebug \ 30 | && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 31 | && mkdir /run/php 32 | COPY php-fpm.conf /etc/php/7.0/fpm/php-fpm.conf 33 | 34 | 35 | ## Stackformation 36 | RUN wget -q https://github.com/AOEpeople/StackFormation/releases/download/${STACKFORMATION_VERSION}/stackformation.phar \ 37 | && mv stackformation.phar /usr/bin/stackformation \ 38 | && chmod ugo+x /usr/bin/stackformation 39 | 40 | 41 | ## Golang 42 | RUN apt-get update \ 43 | && apt-get install -y git software-properties-common \ 44 | && add-apt-repository -y ppa:ubuntu-lxc/lxd-stable \ 45 | && apt-get update \ 46 | && apt-get install -y golang 47 | 48 | 49 | ## Cleanup 50 | RUN apt-get remove -y --purge software-properties-common \ 51 | && apt-get clean 52 | RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 53 | 54 | ENTRYPOINT ["stackformation"] 55 | -------------------------------------------------------------------------------- /docker/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . $PWD/_base.sh 4 | 5 | push() { 6 | echo ">>>> Push $1:$2 to dockerhub" 7 | docker push $1:$2 8 | } 9 | 10 | echo '>> Start docker hub push process' 11 | docker login 12 | 13 | cd $DIR/slim 14 | push kj187/stackformation $1 15 | push kj187/stackformation latest 16 | 17 | cd $DIR/golang 18 | push kj187/stackformation $1-golang 19 | push kj187/stackformation latest-golang 20 | 21 | echo '>> Pushing to docker hub finished' 22 | -------------------------------------------------------------------------------- /docker/slim/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | MAINTAINER Julian Kleinhans 4 | 5 | ARG STACKFORMATION_VERSION 6 | 7 | RUN locale-gen en_US.UTF-8 8 | 9 | ENV LANG en_US.UTF-8 10 | ENV LANGUAGE en_US:en 11 | ENV LC_ALL en_US.UTF-8 12 | 13 | RUN apt-get update \ 14 | && apt-get install -y wget curl zip unzip git software-properties-common \ 15 | && add-apt-repository -y ppa:ondrej/php \ 16 | && apt-get update \ 17 | && apt-get install -y php7.0-fpm php7.0-cli php7.0-mcrypt php7.0-gd php7.0-mysql \ 18 | php7.0-pgsql php7.0-imap php-memcached php7.0-mbstring php7.0-xml php7.0-curl \ 19 | php7.0-sqlite3 php7.0-xdebug \ 20 | && php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 21 | && mkdir /run/php \ 22 | && apt-get remove -y --purge software-properties-common \ 23 | && apt-get -y autoremove \ 24 | && apt-get clean 25 | 26 | RUN wget -q https://github.com/AOEpeople/StackFormation/releases/download/${STACKFORMATION_VERSION}/stackformation.phar \ 27 | && mv stackformation.phar /usr/bin/stackformation \ 28 | && chmod ugo+x /usr/bin/stackformation 29 | 30 | RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 31 | 32 | COPY php-fpm.conf /etc/php/7.0/fpm/php-fpm.conf 33 | 34 | ENTRYPOINT ["stackformation"] 35 | -------------------------------------------------------------------------------- /misc/convert_fn_join_to_inline_ref.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 10 | 11 | ./tests 12 | 13 | 14 | 15 | 16 | 17 | src 18 | 19 | src/stackformation.php 20 | src/dotenv.php 21 | src/StackFormation/CommandRegistry.php 22 | src/AwsInspector/CommandRegistry.php 23 | src/StackFormation/Command 24 | src/AwsInspector/Command 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AwsInspector/Command/Agent/AddIdentityCommand.php: -------------------------------------------------------------------------------- 1 | setName('agent:add-identity') 17 | ->setDescription('Add identity') 18 | ->addArgument( 19 | 'key', 20 | InputArgument::REQUIRED, 21 | 'private key' 22 | ); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output) 26 | { 27 | $key = $input->getArgument('key'); 28 | $identity = new \AwsInspector\Ssh\Identity(\AwsInspector\Ssh\PrivateKey::get($key), true); 29 | $identity->loadIdentity(); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/AwsInspector/Command/Clean/CleanSnapshotsCommand.php: -------------------------------------------------------------------------------- 1 | setName('clean:orphaned-snapshots-from-createimage') 18 | ->setDescription('Delete snapshots created via CreateImage where the AMI does not exist anymore'); 19 | } 20 | 21 | protected function execute(InputInterface $input, OutputInterface $output) 22 | { 23 | 24 | $iam = new \AwsInspector\Model\Iam\Repository(); 25 | $accountId = $iam->findCurrentUser()->getAccountId(); 26 | 27 | $output->writeln('Owner: ' . $accountId); 28 | 29 | $ec2Client = SdkFactory::getClient('EC2'); /* @var $ec2Client \Aws\Ec2\Ec2Client */ 30 | 31 | $res = $ec2Client->describeImages([ 32 | 'Owners' => [$accountId] 33 | ]); 34 | 35 | $activeImageIds = array_flip($res->search('Images[].ImageId')); 36 | 37 | $res = $ec2Client->describeSnapshots([ 38 | // 'Filters' => [['Name' => 'owner-id','Values' => [$accountId]]] 39 | 'OwnerIds' => [$accountId] 40 | ]); 41 | 42 | $orphanSnapshots = []; 43 | 44 | foreach ($res->get('Snapshots') as $snapshotData) { 45 | $description = $snapshotData["Description"]; 46 | // Created by CreateImage(i-ee0c7564) for ami-9945d0ea from vol-e4b6ff16 47 | // if (preg_match('/^Created by CreateImage\(i-.*\) for \(ami-.*\) from \(vol-.*\)$/', $description)) { 48 | if (preg_match('/^Created by CreateImage\(i-.*\) for (ami-.+) from vol-.+/', $description, $matches)) { 49 | $amiId = $matches[1]; 50 | if (isset($activeImageIds[$amiId])) { 51 | $output->writeln('Found active AMI: ' . $amiId); 52 | } else { 53 | $output->writeln('AMI not found: ' . $amiId); 54 | $orphanSnapshots[] = $snapshotData['SnapshotId']; 55 | } 56 | } 57 | } 58 | 59 | foreach ($orphanSnapshots as $snapshotId) { 60 | $output->writeln('Deleting ' . $snapshotId); 61 | $result = $ec2Client->deleteSnapshot([ 62 | 'SnapshotId' => $snapshotId 63 | ]); 64 | } 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/AwsInspector/Command/CloudwatchLogs/DeleteLambdaTriggerCommand.php: -------------------------------------------------------------------------------- 1 | setName('cloudwatchlogs:delete-lambda-trigger') 16 | ->setDescription('Deletes a subscription') 17 | ->addArgument( 18 | 'group', 19 | InputArgument::REQUIRED, 20 | 'Log group name pattern' 21 | )->addArgument( 22 | 'lambdaArn', 23 | InputArgument::REQUIRED, 24 | 'The ARN of the Lambda destination to delete permissions' 25 | )->addArgument( 26 | 'filterName', 27 | InputArgument::REQUIRED, 28 | 'A name for the subscription filter' 29 | ); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | // TODO: refactor this to use \AwsInspector\Model\CloudWatchLogs\Repository 35 | 36 | $groupPattern = $input->getArgument('group'); 37 | $lambdaArn = $input->getArgument('lambdaArn'); 38 | $filterName = $input->getArgument('filterName'); 39 | 40 | /* @var $cloudwatchLogsClient \Aws\CloudWatchLogs\CloudWatchLogsClient */ 41 | $cloudwatchLogsClient = \AwsInspector\SdkFactory::getClient('cloudwatchlogs'); 42 | $lambdaClient = \AwsInspector\SdkFactory::getClient('lambda'); 43 | 44 | $nextToken = null; 45 | 46 | do { 47 | $params = ['limit' => 50]; 48 | if ($nextToken) { 49 | $params['nextToken'] = $nextToken; 50 | } 51 | 52 | $result = $cloudwatchLogsClient->describeLogGroups($params); 53 | foreach ($result->get('logGroups') as $logGroup) { 54 | $name = $logGroup['logGroupName']; 55 | if (preg_match('/'.$groupPattern.'/', $name)) { 56 | try { 57 | $subscriptionFilters = $cloudwatchLogsClient->describeSubscriptionFilters(['logGroupName' => $logGroup['logGroupName']]); 58 | if (empty($subscriptionFilters->get('subscriptionFilters'))) { 59 | continue; 60 | } 61 | 62 | $cloudwatchLogsClient->deleteSubscriptionFilter([ 63 | 'filterName' => $filterName, 64 | 'logGroupName' => $logGroup['logGroupName'] 65 | ]); 66 | 67 | $lambdaClient->removePermission([ 68 | 'FunctionName' => $lambdaArn, 69 | 'StatementId' => (string) md5($logGroup['logGroupName']), 70 | ]); 71 | 72 | $output->writeln('Delete lambda trigger for ' . $logGroup['logGroupName']); 73 | } catch (\Aws\CloudWatchLogs\Exception\CloudWatchLogsException $e) { 74 | if ($e->getAwsErrorCode() != 'ResourceNotFoundException') { 75 | throw $e; 76 | } 77 | } 78 | } 79 | } 80 | $nextToken = $result->get("nextToken"); 81 | } while ($nextToken); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/AwsInspector/Command/CloudwatchLogs/DeleteLogGroupCommand.php: -------------------------------------------------------------------------------- 1 | setName('cloudwatchlogs:delete-log-group') 19 | ->setDescription('Delete log group') 20 | ->addArgument( 21 | 'group', 22 | InputArgument::REQUIRED, 23 | 'Log group name pattern' 24 | ); 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $groupPattern = $input->getArgument('group'); 30 | $repository = new Repository(); 31 | foreach ($repository->findLogGroups($groupPattern) as $logGroup) { /* @var $logGroup LogGroup */ 32 | $output->writeln('Deleting ' . $logGroup->getLogGroupName()); 33 | $repository->deleteLogGroup($logGroup->getLogGroupName()); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/AwsInspector/Command/CloudwatchLogs/SetRetentionCommand.php: -------------------------------------------------------------------------------- 1 | setName('cloudwatchlogs:set-retention') 17 | ->setDescription('Set retention') 18 | ->addArgument( 19 | 'days', 20 | InputArgument::REQUIRED, 21 | 'retention in days' 22 | ) 23 | ->addArgument( 24 | 'group', 25 | InputArgument::REQUIRED, 26 | 'Log group name pattern' 27 | ); 28 | } 29 | 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | // TODO: refactor this to use \AwsInspector\Model\CloudWatchLogs\Repository 33 | 34 | $days = $input->getArgument('days'); 35 | $days = intval($days); 36 | if ($days == 0) { 37 | throw new \Exception('Invalid retention period'); 38 | } 39 | $groupPattern = $input->getArgument('group'); 40 | 41 | $cloudwatchLogsClient = \AwsInspector\SdkFactory::getClient('cloudwatchlogs'); /* @var $cloudwatchLogsClient \Aws\CloudWatchLogs\CloudWatchLogsClient */ 42 | 43 | $totalBytes = 0; 44 | $nextToken = null; 45 | do { 46 | $params = ['limit' => 50]; 47 | if ($nextToken) { 48 | $params['nextToken'] = $nextToken; 49 | } 50 | $result = $cloudwatchLogsClient->describeLogGroups($params); 51 | foreach ($result->get('logGroups') as $logGroup) { 52 | $name = $logGroup['logGroupName']; 53 | if (preg_match('/'.$groupPattern.'/', $name)) { 54 | $retention = isset($logGroup['retentionInDays']) ? $logGroup['retentionInDays'] : 'never'; 55 | if ($retention != $days) { 56 | $output->writeln('Updating ' . $logGroup['logGroupName']); 57 | $cloudwatchLogsClient->putRetentionPolicy([ 58 | 'logGroupName' => $name, 59 | 'retentionInDays' => $days 60 | ]); 61 | } else { 62 | $output->writeln('Skipping ' . $logGroup['logGroupName']); 63 | } 64 | } else { 65 | $output->writeln('Does not match pattern: ' . $logGroup['logGroupName']); 66 | } 67 | } 68 | $nextToken = $result->get("nextToken"); 69 | } while ($nextToken); 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/AwsInspector/Command/CloudwatchLogs/ShowLogGroupsCommand.php: -------------------------------------------------------------------------------- 1 | setName('cloudwatchlogs:show-log-groups') 18 | ->setDescription('Show Log Groups') 19 | ->addArgument( 20 | 'group', 21 | InputArgument::OPTIONAL, 22 | 'Log group name pattern' 23 | ); 24 | } 25 | 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | $groupPattern = $input->getArgument('group'); 29 | if (empty($groupPattern)) { 30 | $groupPattern = '.*'; 31 | } 32 | 33 | $cloudwatchLogsClient = \AwsInspector\SdkFactory::getClient('cloudwatchlogs'); /* @var $cloudwatchLogsClient \Aws\CloudWatchLogs\CloudWatchLogsClient */ 34 | 35 | 36 | $table = new Table($output); 37 | $table->setHeaders(['Name', 'Retention [days]', 'Size [MB]']); 38 | 39 | $totalBytes = 0; 40 | $nextToken = null; 41 | do { 42 | $params = ['limit' => 50]; 43 | if ($nextToken) { 44 | $params['nextToken'] = $nextToken; 45 | } 46 | $result = $cloudwatchLogsClient->describeLogGroups($params); 47 | foreach ($result->get('logGroups') as $logGroup) { 48 | $name = $logGroup['logGroupName']; 49 | if (preg_match('/'.$groupPattern.'/', $name)) { 50 | $table->addRow([ 51 | $logGroup['logGroupName'], 52 | isset($logGroup['retentionInDays']) ? $logGroup['retentionInDays'] : 'Never', 53 | round($logGroup['storedBytes'] / (1024*1024)) 54 | ]); 55 | $totalBytes += $logGroup['storedBytes']; 56 | } 57 | } 58 | $nextToken = $result->get("nextToken"); 59 | } while ($nextToken); 60 | 61 | 62 | $table->render(); 63 | 64 | $output->writeln('Total size: ' . $this->formatBytes($totalBytes)); 65 | } 66 | 67 | protected function formatBytes($bytes, $precision = 2) { 68 | $units = array('B', 'KB', 'MB', 'GB', 'TB'); 69 | $bytes = max($bytes, 0); 70 | $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 71 | $pow = min($pow, count($units) - 1); 72 | $bytes /= (1 << (10 * $pow)); 73 | return round($bytes, $precision) . ' ' . $units[$pow]; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/AwsInspector/Command/Ec2/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | setName('ec2:list') 19 | ->setDescription('List all instances') 20 | ->addOption( 21 | 'tag', 22 | 't', 23 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 24 | 'tag (Example: "Environment:Deploy")' 25 | ) 26 | ->addOption( 27 | 'column', 28 | 'c', 29 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 30 | 'Extra column (tag)' 31 | ) 32 | ->addOption( 33 | 'sort', 34 | 's', 35 | InputOption::VALUE_OPTIONAL, 36 | 'Sort by column' 37 | ); 38 | } 39 | 40 | protected function execute(InputInterface $input, OutputInterface $output) 41 | { 42 | $tags = $input->getOption('tag'); 43 | $tags = $this->convertTags($tags); 44 | 45 | $mapping = [ 46 | 'InstanceId' => 'InstanceId', 47 | 'ImageId' => 'ImageId', 48 | 'State' => 'State.Name', 49 | 'SubnetId' => 'SubnetId', 50 | 'AZ' => 'Placement.AvailabilityZone', 51 | 'PublicIp' => 'PublicIpAddress', 52 | 'PrivateIp' => 'PrivateIpAddress', 53 | 'KeyName' => 'KeyName' 54 | ]; 55 | 56 | // dynamically add current tags 57 | foreach (array_keys($tags) as $tagName) { 58 | $mapping[$tagName] = 'Tags[?Key==`'.$tagName.'`].Value | [0]'; 59 | } 60 | 61 | foreach ($input->getOption('column') as $tagName) { 62 | $mapping[$tagName] = 'Tags[?Key==`'.$tagName.'`].Value | [0]'; 63 | } 64 | 65 | $repository = new Repository(); 66 | $instanceCollection = $repository->findEc2InstancesByTags($tags); 67 | 68 | $rows = []; 69 | foreach ($instanceCollection as $instance) { /* @var $instance Instance */ 70 | $rows[] = $instance->extractData($mapping); 71 | } 72 | 73 | $this->sortColumn = $input->getOption('sort'); 74 | if ($this->sortColumn !== null) { 75 | usort($rows, [$this, 'sortByColumn']); 76 | } 77 | 78 | if (count($rows)) { 79 | $table = new \Symfony\Component\Console\Helper\Table($output); 80 | $table 81 | ->setHeaders(array_keys(end($rows))) 82 | ->setRows($rows); 83 | $table->render(); 84 | } else { 85 | $output->writeln('No matching instances found.'); 86 | } 87 | } 88 | 89 | private function sortByColumn($a, $b) 90 | { 91 | if (!isset($a[$this->sortColumn])) { 92 | return 0; 93 | } 94 | return strcmp($a[$this->sortColumn], $b[$this->sortColumn]); 95 | } 96 | } -------------------------------------------------------------------------------- /src/AwsInspector/Command/Profile/DisableCommand.php: -------------------------------------------------------------------------------- 1 | setName('profile:disable') 16 | ->setDescription('Disable current profile'); 17 | } 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output) 20 | { 21 | if (!is_file('.env')) { 22 | $output->writeln('No .env file found'); 23 | return; 24 | } 25 | if (!unlink('.env')) { 26 | throw new \Exception('Error deleting .env file'); 27 | } 28 | $output->writeln('Deleted file .env'); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/AwsInspector/Command/Profile/EnableCommand.php: -------------------------------------------------------------------------------- 1 | setName('profile:enable') 19 | ->setDescription('Enable profile') 20 | ->addArgument( 21 | 'profile', 22 | InputArgument::REQUIRED, 23 | 'Profile' 24 | ); 25 | } 26 | 27 | protected function interact(InputInterface $input, OutputInterface $output) 28 | { 29 | $profile = $input->getArgument('profile'); 30 | if (empty($profile)) { 31 | 32 | $profileManager = new Manager(); 33 | 34 | $helper = $this->getHelper('question'); 35 | $question = new ChoiceQuestion( 36 | 'Please select the profile you want to use', 37 | $profileManager->listAllProfiles() 38 | ); 39 | 40 | $question->setErrorMessage('Profile %s is invalid.'); 41 | 42 | $profile = $helper->ask($input, $output, $question); 43 | $output->writeln('Selected Profile: '.$profile); 44 | 45 | $input->setArgument('profile', $profile); 46 | } 47 | } 48 | 49 | protected function execute(InputInterface $input, OutputInterface $output) 50 | { 51 | $profileManager = new Manager(); 52 | $file = $profileManager->writeProfileToDotEnv($input->getArgument('profile')); 53 | $output->writeln('File written: ' . $file); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/AwsInspector/Command/Profile/ListCommand.php: -------------------------------------------------------------------------------- 1 | setName('profile:list') 17 | ->setDescription('List all AWS profiles found in configuration'); 18 | } 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output) 21 | { 22 | $profileManager = new Manager(); 23 | 24 | $rows=[]; 25 | foreach($profileManager->listAllProfiles() as $profileName) { 26 | $rows[] = [$profileName]; 27 | } 28 | 29 | $table = new \Symfony\Component\Console\Helper\Table($output); 30 | $table 31 | ->setHeaders(array('Profile Name')) 32 | ->setRows($rows) 33 | ; 34 | $table->render(); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/AwsInspector/CommandRegistry.php: -------------------------------------------------------------------------------- 1 | apiData = $apiData; 24 | $this->profileManager = is_null($profileManager) ? new \StackFormation\Profile\Manager() : $profileManager; 25 | } 26 | 27 | /** 28 | * @return array 29 | */ 30 | public function getApiData() 31 | { 32 | return $this->apiData; 33 | } 34 | 35 | /** 36 | * @param array $mapping 37 | * @return array 38 | */ 39 | public function extractData(array $mapping) 40 | { 41 | $result = []; 42 | foreach ($mapping as $fieldName => $expression) { 43 | $result[$fieldName] = \JmesPath\Env::search($expression, $this->apiData); 44 | } 45 | return $result; 46 | } 47 | 48 | /** 49 | * @param string $method 50 | * @param $args 51 | * @return mixed|null 52 | * @throws \Exception 53 | */ 54 | public function __call($method, $args) 55 | { 56 | if (substr($method, 0, 3) != 'get') { 57 | $class = get_class($this); 58 | throw new \Exception("Invalid method '$method' (class: $class)"); 59 | } 60 | 61 | $field = substr($method, 3); 62 | if (isset($this->apiData[$field])) { 63 | return $this->apiData[$field]; 64 | } 65 | 66 | return null; 67 | } 68 | 69 | /** 70 | * @return array 71 | * @throws \Exception 72 | */ 73 | public function getAssocTags() 74 | { 75 | //if (!isset($this->apiData['Tags']) && !method_exists($this, 'getTags')) { 76 | // throw new \Exception('Tags are not supported'); 77 | //} 78 | return $this->convertToAssocArray($this->getTags() ?: []); 79 | } 80 | 81 | /** 82 | * @param string $key 83 | * @return sring|null 84 | */ 85 | public function getTag($key) 86 | { 87 | $tags = $this->getAssocTags(); 88 | return isset($tags[$key]) ? $tags[$key] : null; 89 | } 90 | 91 | /** 92 | * @param array $filter 93 | * @return bool 94 | * @throws \Exception 95 | */ 96 | public function matchesTags(array $filter) 97 | { 98 | $tags = $this->getAssocTags(); 99 | $patched = array_replace_recursive($tags, $filter); 100 | return $tags == $patched; 101 | } 102 | 103 | /** 104 | * @param array $tags 105 | * @return array 106 | */ 107 | protected function convertToAssocArray(array $tags) 108 | { 109 | $assocTags = []; 110 | foreach ($tags as $data) { 111 | $assocTags[$data['Key']] = $data['Value']; 112 | } 113 | return $assocTags; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/AutoScaling/LaunchConfiguration.php: -------------------------------------------------------------------------------- 1 | profileManager = is_null($profileManager) ? new \StackFormation\Profile\Manager() : $profileManager; 19 | $this->asgClient = $this->profileManager->getClient('AutoScaling', $profile); 20 | } 21 | 22 | /** 23 | * @return \AwsInspector\Model\Collection 24 | */ 25 | public function findAutoScalingGroups() 26 | { 27 | $result = $this->asgClient->describeAutoScalingGroups(); 28 | 29 | $rows = $result->search('AutoScalingGroups[]'); 30 | 31 | $collection = new \AwsInspector\Model\Collection(); 32 | foreach ($rows as $row) { 33 | $collection->attach(new AutoScalingGroup($row)); 34 | } 35 | 36 | return $collection; 37 | } 38 | 39 | /** 40 | * @param array $tags 41 | * @return \AwsInspector\Model\Collection 42 | */ 43 | public function findAutoScalingGroupsByTags(array $tags = array()) 44 | { 45 | $collection = new \AwsInspector\Model\Collection(); 46 | foreach ($this->findAutoScalingGroups() as $autoScalingGroup) { /* @var $autoScalingGroup AutoScalingGroup */ 47 | if ($autoScalingGroup->matchesTags($tags)) { 48 | $collection->attach($autoScalingGroup); 49 | } 50 | } 51 | return $collection; 52 | } 53 | 54 | /** 55 | * @param string $regex 56 | * @return \AwsInspector\Model\Collection 57 | */ 58 | public function findByAutoScalingGroupName($regex) 59 | { 60 | $collection = new \AwsInspector\Model\Collection(); 61 | foreach ($this->findAutoScalingGroups() as $autoScalingGroup) { /* @var $autoScalingGroup AutoScalingGroup */ 62 | if (preg_match($regex, $autoScalingGroup->getAutoScalingGroupName())) { 63 | $collection->attach($autoScalingGroup); 64 | } 65 | } 66 | 67 | return $collection; 68 | } 69 | 70 | /** 71 | * @return \AwsInspector\Model\Collection 72 | */ 73 | public function findLaunchConfigurations() 74 | { 75 | $result = $this->asgClient->describeLaunchConfigurations(); 76 | 77 | $rows = $result->search('LaunchConfigurations[]'); 78 | 79 | $collection = new \AwsInspector\Model\Collection(); 80 | foreach ($rows as $row) { 81 | $collection->attach(new LaunchConfiguration($row)); 82 | } 83 | 84 | return $collection; 85 | } 86 | 87 | /** 88 | * @return array 89 | */ 90 | public function findLaunchConfigurationsGroupedByImageId() 91 | { 92 | $imageIds = []; 93 | foreach ($this->findLaunchConfigurations() as $launchConfiguration) { /* @var $launchConfiguration LaunchConfiguration */ 94 | $imageId = $launchConfiguration->getImageId(); 95 | if (!isset($imageIds[$imageId])) { 96 | $imageIds[$imageId] = []; 97 | } 98 | $imageIds[$imageId][] = $launchConfiguration; 99 | } 100 | 101 | return $imageIds; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/CloudWatchLogs/LogGroup.php: -------------------------------------------------------------------------------- 1 | $value) { 20 | $normalizedApiData[ucfirst($key)] = $value; 21 | } 22 | parent::__construct($normalizedApiData, $profileManager); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/CloudWatchLogs/LogStream.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | $normalizedApiData[ucfirst($key)] = $value; 24 | } 25 | parent::__construct($normalizedApiData, $profileManager); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/CloudWatchLogs/Repository.php: -------------------------------------------------------------------------------- 1 | profileManager = $profileManager ?: new \StackFormation\Profile\Manager(); 21 | $this->cloudWatchLogsClient = $this->profileManager->getClient('CloudWatchLogs', $profile); 22 | } 23 | 24 | /** 25 | * @param string $logGroupNameFilter 26 | * @return \AwsInspector\Model\Collection 27 | */ 28 | public function findLogGroups($logGroupNameFilter=null) 29 | { 30 | $collection = new \AwsInspector\Model\Collection(); 31 | $nextToken = null; 32 | do { 33 | $params = ['limit' => 50]; 34 | if ($nextToken) { 35 | $params['nextToken'] = $nextToken; 36 | } 37 | $result = $this->cloudWatchLogsClient->describeLogGroups($params); 38 | foreach ($result->get('logGroups') as $row) { 39 | if (!$logGroupNameFilter || Finder::matchWildcard($logGroupNameFilter, $row['logGroupName'])) { 40 | $collection->attach(new LogGroup($row)); 41 | } 42 | } 43 | $nextToken = $result->get("nextToken"); 44 | } while ($nextToken); 45 | return $collection; 46 | } 47 | 48 | /** 49 | * @param $logGroupName 50 | * @param string $logStreamNameFilter 51 | * @return \AwsInspector\Model\Collection 52 | */ 53 | public function findLogStreams($logGroupName, $logStreamNameFilter=null) 54 | { 55 | if (empty($logGroupName)) { 56 | throw new \InvalidArgumentException('LogGroupName cannot be empty'); 57 | } 58 | $result = $this->cloudWatchLogsClient->describeLogStreams([ 59 | 'logGroupName' => $logGroupName, 60 | 'orderBy' => 'LastEventTime', 61 | 'descending' => true 62 | ]); 63 | $rows = $result->search('logStreams[]'); 64 | $collection = new \AwsInspector\Model\Collection(); 65 | foreach ($rows as $row) { 66 | if (!$logStreamNameFilter || Finder::matchWildcard($logStreamNameFilter, $row['logStreamName'])) { 67 | $collection->attach(new LogStream($row)); 68 | } 69 | } 70 | return $collection; 71 | } 72 | 73 | public function findLogEvents($logGroupName, $logStreamName, &$nextForwardToken) 74 | { 75 | $params = [ 76 | 'limit' => 50, 77 | 'logGroupName' => $logGroupName, 78 | 'logStreamName' => $logStreamName 79 | ]; 80 | if ($nextForwardToken) { 81 | $params['nextToken'] = $nextForwardToken; 82 | } 83 | $res = $this->cloudWatchLogsClient->getLogEvents($params); 84 | $nextForwardToken = $res->get('nextForwardToken'); 85 | return $res->search('events[].message'); 86 | } 87 | 88 | public function deleteLogGroup($logGroupName) 89 | { 90 | $this->cloudWatchLogsClient->deleteLogGroup(['logGroupName' => $logGroupName]); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/Collection.php: -------------------------------------------------------------------------------- 1 | rewind(); 12 | return $this->current(); 13 | } 14 | 15 | public function flatten($method) { 16 | $flattenedObjects = []; 17 | foreach ($this as $item) { 18 | $flattenedObjects[] = $item->$method(); 19 | } 20 | return $flattenedObjects; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/Ebs/Repository.php: -------------------------------------------------------------------------------- 1 | findEbsVolumes([[ 13 | 'Name' => 'attachment.instance-id', 14 | 'Values' => [$instanceId] 15 | ]]); 16 | } 17 | 18 | /** 19 | * @param array $filters 20 | * @return \AwsInspector\Model\Collection 21 | * @throws \Exception 22 | */ 23 | public function findEbsVolumes(array $filters=[]) { 24 | $ec2Client = \AwsInspector\SdkFactory::getClient('ec2'); /* @var $ec2Client \Aws\Ec2\Ec2Client */ 25 | $result = $ec2Client->describeVolumes(['Filters' => $filters]); 26 | $rows = $result->search('Volumes[]'); 27 | 28 | $collection = new \AwsInspector\Model\Collection(); 29 | foreach ($rows as $row) { 30 | $collection->attach(new Volume($row)); 31 | } 32 | return $collection; 33 | } 34 | 35 | /** 36 | * @param array $tags 37 | * @return \AwsInspector\Model\Collection 38 | */ 39 | public function findEbsVolumesByTags(array $tags=array()) { 40 | foreach ($tags as $tagName => $tagValue) { 41 | $filters[] = ['Name' => 'tag:'.$tagName, "Values" => [$tagValue]]; 42 | } 43 | return $this->findEbsVolumes($filters); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/Ebs/Volume.php: -------------------------------------------------------------------------------- 1 | findEc2InstancesByTags($tags); 18 | } 19 | return self::$registry[$hash]; 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/Ec2/Factory.php: -------------------------------------------------------------------------------- 1 | resourceName)) { 40 | 41 | // TODO: this should be changed! 42 | $region = getenv('HURRICANE_TEST_REGION'); 43 | if (empty($region)) { 44 | throw new \Exception('Region missing'); 45 | } 46 | 47 | // get account id from current user 48 | $iam = new \AwsInspector\Model\Iam\Repository(); 49 | $accountId = $iam->findCurrentUser()->getAccountId(); 50 | 51 | $parts = []; 52 | $parts['prefix'] = 'arn:aws:elasticache'; 53 | $parts['region'] = $region; 54 | $parts['AccountId'] = $accountId; 55 | $parts['resourcetype'] = 'cluster'; 56 | $parts['name'] = $this->getCacheClusterId(); 57 | $this->resourceName = implode(':', $parts); 58 | } 59 | return $this->resourceName; 60 | } 61 | 62 | public function getTags() 63 | { 64 | if (is_null($this->tags)) { 65 | $elastiCacheClient = \AwsInspector\SdkFactory::getClient('ElastiCache'); 66 | /* @var $elastiCacheClient \Aws\ElastiCache\ElastiCacheClient */ 67 | $result = $elastiCacheClient->listTagsForResource(['ResourceName' => $this->getResourceName()]); 68 | $this->tags = $result->get('TagList'); 69 | } 70 | return $this->tags; 71 | } 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/ElastiCache/Repository.php: -------------------------------------------------------------------------------- 1 | elastiCacheClient = \AwsInspector\SdkFactory::getClient('ElastiCache'); 18 | } 19 | 20 | public function findCacheClusters() 21 | { 22 | $result = $this->elastiCacheClient->describeCacheClusters(['ShowCacheNodeInfo' => true]); 23 | $rows = $result->search('CacheClusters[]'); 24 | 25 | $collection = new \AwsInspector\Model\Collection(); 26 | foreach ($rows as $row) { 27 | $collection->attach(new CacheCluster($row)); 28 | } 29 | return $collection; 30 | } 31 | 32 | /** 33 | * @param array $tags 34 | * @return \AwsInspector\Model\Collection 35 | */ 36 | public function findCacheClustersByTags(array $tags = array()) 37 | { 38 | $cacheClusters = $this->findCacheClusters(); 39 | $matchingCacheClusters = new Collection(); 40 | foreach ($cacheClusters as $cacheCluster) { /* @var $cacheCluster CacheCluster */ 41 | if ($cacheCluster->matchesTags($tags)) { 42 | $matchingCacheClusters->attach($cacheCluster); 43 | } 44 | } 45 | return $matchingCacheClusters; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/Elb/Elb.php: -------------------------------------------------------------------------------- 1 | tags)) { 31 | $elbClient = \AwsInspector\SdkFactory::getClient('ElasticLoadBalancing'); 32 | /* @var $elbClient \Aws\ElasticLoadBalancing\ElasticLoadBalancingClient */ 33 | $result = $elbClient->describeTags(['LoadBalancerNames' => [$this->getLoadBalancerName()]]); 34 | $this->tags = $result->search('TagDescriptions[0].Tags'); 35 | } 36 | return $this->tags; 37 | } 38 | 39 | public function getInstanceStates() 40 | { 41 | $elbClient = \AwsInspector\SdkFactory::getClient('ElasticLoadBalancing'); 42 | /* @var $elbClient \Aws\ElasticLoadBalancing\ElasticLoadBalancingClient */ 43 | $res = $elbClient->describeInstanceHealth(['LoadBalancerName' => $this->getLoadBalancerName()]); 44 | $instances = []; 45 | foreach ($res->search('InstanceStates[]') as $instanceState) { 46 | $instances[$instanceState['InstanceId']] = $instanceState; 47 | } 48 | return $instances; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/Elb/Repository.php: -------------------------------------------------------------------------------- 1 | elbClient = \AwsInspector\SdkFactory::getClient('ElasticLoadBalancing'); 18 | } 19 | 20 | public function findElbByName($name) 21 | { 22 | $result = $this->elbClient->describeLoadBalancers([ 'LoadBalancerNames' => [$name] ]); 23 | return new Elb($result->search('LoadBalancerDescriptions[0]')); 24 | } 25 | 26 | public function findElbs() 27 | { 28 | $result = $this->elbClient->describeLoadBalancers(); 29 | $rows = $result->search('LoadBalancerDescriptions[]'); 30 | 31 | $collection = new \AwsInspector\Model\Collection(); 32 | foreach ($rows as $row) { 33 | $collection->attach(new Elb($row)); 34 | } 35 | return $collection; 36 | } 37 | 38 | /** 39 | * @param array $tags 40 | * @return \AwsInspector\Model\Collection 41 | */ 42 | public function findElbsByTags(array $tags = array()) 43 | { 44 | $elbs = $this->findElbs(); 45 | $matchingElbs = new Collection(); 46 | foreach ($elbs as $elb) { 47 | /* @var $elb Elb */ 48 | if ($elb->matchesTags($tags)) { 49 | $matchingElbs->attach($elb); 50 | } 51 | } 52 | return $matchingElbs; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/Iam/Repository.php: -------------------------------------------------------------------------------- 1 | iamClient = \AwsInspector\SdkFactory::getClient('Iam'); 18 | } 19 | 20 | /** 21 | * @return User 22 | */ 23 | public function findCurrentUser() 24 | { 25 | $result = $this->iamClient->getUser(); 26 | return new User($result->get('User')); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/Iam/User.php: -------------------------------------------------------------------------------- 1 | getArn()); 19 | return $parts[4]; 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/Rds/Database.php: -------------------------------------------------------------------------------- 1 | resourceName)) { 51 | 52 | // get account id from current user 53 | $iam = new \AwsInspector\Model\Iam\Repository(); 54 | $accountId = $iam->findCurrentUser()->getAccountId(); 55 | 56 | $parts = []; 57 | $parts['prefix'] = 'arn:aws:rds'; 58 | $parts['region'] = substr($this->getAvailabilityZone(), 0, -1); 59 | $parts['AccountId'] = $accountId; 60 | $parts['resourcetype'] = 'db'; 61 | $parts['name'] = $this->getDBInstanceIdentifier(); 62 | $this->resourceName = implode(':', $parts); 63 | } 64 | return $this->resourceName; 65 | } 66 | 67 | public function getTags() 68 | { 69 | if (is_null($this->tags)) { 70 | $rdsClient = \AwsInspector\SdkFactory::getClient('Rds'); 71 | /* @var $rdsClient \Aws\Rds\RdsClient */ 72 | $result = $rdsClient->listTagsForResource(['ResourceName' => $this->getResourceName()]); 73 | $this->tags = $result->get('TagList'); 74 | } 75 | return $this->tags; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/AwsInspector/Model/Rds/Repository.php: -------------------------------------------------------------------------------- 1 | rdsClient = \AwsInspector\SdkFactory::getClient('Rds'); 18 | } 19 | 20 | public function findDatabases() 21 | { 22 | $result = $this->rdsClient->describeDBInstances(); 23 | $rows = $result->search('DBInstances[]'); 24 | 25 | $collection = new \AwsInspector\Model\Collection(); 26 | foreach ($rows as $row) { 27 | $collection->attach(new Database($row)); 28 | } 29 | return $collection; 30 | } 31 | 32 | /** 33 | * @param array $tags 34 | * @return \AwsInspector\Model\Collection 35 | */ 36 | public function findDatabasesByTags(array $tags = array()) 37 | { 38 | $databases = $this->findDatabases(); 39 | $matchingDatabases = new Collection(); 40 | foreach ($databases as $database) { 41 | /* @var $database Database */ 42 | try { 43 | if ($database->matchesTags($tags)) { 44 | $matchingDatabases->attach($database); 45 | } 46 | } catch (\Aws\Rds\Exception\RdsException $e) { 47 | if ($e->getAwsErrorCode() != 'AccessDenied') { 48 | throw $e; 49 | } 50 | } 51 | } 52 | return $matchingDatabases; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/Route53/Repository.php: -------------------------------------------------------------------------------- 1 | listResourceRecordSets([ 22 | 'HostedZoneId' => $hostedZoneId, 23 | 'StartRecordName' => $nextRecordName 24 | ]); 25 | foreach ($res->search('ResourceRecordSets') as $recordSet) { 26 | $name = $recordSet['Name']; 27 | $type = $recordSet['Type']; 28 | unset($recordSet['Name']); 29 | unset($recordSet['Type']); 30 | $this->recordSets[$name][$type] = $recordSet; 31 | } 32 | $nextRecordName = $res->get('NextRecordName'); 33 | } while ($res->get('IsTruncated')); 34 | } 35 | 36 | public function getAllRecordSets() 37 | { 38 | return $this->recordSets; 39 | } 40 | 41 | public function findByRecordSet($regex) 42 | { 43 | foreach ($this->recordSets as $name => $data) { 44 | if (preg_match($regex, $name)) { 45 | return $data; 46 | } 47 | } 48 | return []; 49 | } 50 | 51 | public function findByRecordSetNameAndType($recordSetName, $type) 52 | { 53 | $recordSetsForName = $this->findByRecordSet($recordSetName); 54 | if (count($recordSetName) == 0) { 55 | return []; 56 | } elseif (!isset($recordSetsForName[$type])) { 57 | return []; 58 | } else { 59 | return $recordSetsForName[$type]; 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/SecurityGroup/Repository.php: -------------------------------------------------------------------------------- 1 | findSecurityGroups([['Name' => 'group-id', 'Values' => [$groupId]]])->getFirst(); 13 | return $group; 14 | } 15 | 16 | /** 17 | * @param array $filters 18 | * @return \AwsInspector\Model\Collection 19 | * @throws \Exception 20 | */ 21 | public function findSecurityGroups(array $filters=[]) { 22 | $ec2Client = \AwsInspector\SdkFactory::getClient('ec2'); /* @var $ec2Client \Aws\Ec2\Ec2Client */ 23 | $result = $ec2Client->describeSecurityGroups(['Filters' => $filters]); 24 | $rows = $result->search('SecurityGroups[]'); 25 | 26 | $collection = new \AwsInspector\Model\Collection(); 27 | foreach ($rows as $row) { 28 | $securityGroup = new SecurityGroup($row); 29 | if ($securityGroup !== false) { 30 | $collection->attach($securityGroup); 31 | } 32 | } 33 | return $collection; 34 | } 35 | 36 | /** 37 | * @param array $tags 38 | * @return \AwsInspector\Model\Collection 39 | */ 40 | public function findSecurityGroupsByTags(array $tags=array()) { 41 | foreach ($tags as $tagName => $tagValue) { 42 | $filters[] = ['Name' => 'tag-key', "Values" => [$tagName]]; 43 | $filters[] = ['Name' => 'tag-value', "Values" => [$tagValue]]; 44 | } 45 | return $this->findSecurityGroups($filters); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/AwsInspector/Model/SecurityGroup/SecurityGroup.php: -------------------------------------------------------------------------------- 1 | getIpPermissions() as $permission) { 28 | if ($permission['IpProtocol'] != $protocol || $permission['FromPort'] != $port) { 29 | continue; 30 | } 31 | if ($origin instanceof SecurityGroup) { 32 | foreach ($permission['UserIdGroupPairs'] as $idGroupPair) { 33 | if ($idGroupPair['GroupId'] == $origin->getGroupId()) { 34 | return true; 35 | } 36 | } 37 | } else { 38 | $isRange = (strpos($origin, '/') !== false); 39 | foreach ($permission['IpRanges'] as $ipRange) { 40 | if ($isRange) { 41 | if ($origin == $ipRange['CidrIp']) { 42 | return true; 43 | } 44 | } else { 45 | if ($this->ipMatchesCidr($origin, $ipRange['CidrIp'])) { 46 | return true; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | return false; 53 | } 54 | 55 | protected function ipMatchesCidr($ip, $range) 56 | { 57 | list ($subnet, $bits) = explode('/', $range); 58 | $ip = ip2long($ip); 59 | $subnet = ip2long($subnet); 60 | $mask = -1 << (32 - $bits); 61 | $subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned 62 | return ($ip & $mask) == $subnet; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/AwsInspector/Registry.php: -------------------------------------------------------------------------------- 1 | getClient($client, $profile, $args); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/AwsInspector/Ssh/Agent.php: -------------------------------------------------------------------------------- 1 | /dev/null 2>&1', $output); 16 | exec('ssh-add ' . $privateKeyFile, $output); 17 | var_dump($output); 18 | } 19 | 20 | /** 21 | * Check if the current private key is already loaded 22 | * 23 | * @param $privateKeyFile 24 | * @return bool 25 | */ 26 | public static function identityLoaded($privateKeyFile) { 27 | $returnVar = null; 28 | exec('ssh-add -l | grep '. $privateKeyFile, $output, $returnVar); 29 | return ($returnVar == 0); 30 | } 31 | 32 | /** 33 | * Since deleting an entity requires the PUBLIC key instead of the private key (that's used for adding) 34 | * we're going to extract the public key first and write it into a temp file 35 | * 36 | * @param $privateKeyFile 37 | * @throws \Exception 38 | */ 39 | public static function deleteIdentity($privateKeyFile) { 40 | if (!is_file($privateKeyFile)) { 41 | throw new \Exception('Private key file not found.'); 42 | } 43 | exec('ssh-add -L | grep '. $privateKeyFile, $output); 44 | $tmpFile = tempnam(sys_get_temp_dir(), 'publickey_'); 45 | file_put_contents($tmpFile, end($output)); 46 | exec('ssh-add -d ' . $tmpFile . ' >/dev/null 2>&1', $output); 47 | unlink($tmpFile); 48 | } 49 | 50 | public static function deleteAllIdentities() { 51 | exec('ssh-add -D', $output); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/AwsInspector/Ssh/Command.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 21 | if (!is_string($command)) { 22 | throw new \InvalidArgumentException("Command must be a string."); 23 | } 24 | $this->command = $command; 25 | $this->asUser = $asUser; 26 | } 27 | 28 | protected function getCommandString() 29 | { 30 | $command = $this->command; 31 | if ($this->asUser) { 32 | $command = sprintf('sudo -u %s bash -c %s', 33 | escapeshellarg($this->asUser), 34 | escapeshellarg($command) 35 | ); 36 | } 37 | return $command; 38 | } 39 | 40 | public function __toString() 41 | { 42 | if (empty($this->connection->__toString())) { 43 | return $this->getCommandString(); 44 | } 45 | return sprintf( 46 | "%s %s", 47 | $this->connection, 48 | escapeshellarg($this->getCommandString()) 49 | ); 50 | } 51 | 52 | public function exec() 53 | { 54 | // file_put_contents('/tmp/exec.log', $this->__toString() . "\n\n", FILE_APPEND); 55 | $returnVar = null; 56 | exec($this->__toString(), $output, $returnVar); 57 | return [ 58 | 'output' => $output, 59 | 'returnVar' => $returnVar 60 | ]; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/AwsInspector/Ssh/Identity.php: -------------------------------------------------------------------------------- 1 | privateKey = $privateKey; 15 | $this->keepIdentity = $keepIdentity; 16 | } 17 | 18 | public function getPrivateKeyFile() { 19 | return $this->privateKey->getPrivateKeyFile(); 20 | } 21 | 22 | public function loadIdentity() 23 | { 24 | if (!Agent::identityLoaded($this->getPrivateKeyFile())) { 25 | Agent::addIdentity($this->getPrivateKeyFile()); 26 | } 27 | return $this; 28 | } 29 | 30 | public function removeIdentity() 31 | { 32 | if (!empty($this->getPrivateKeyFile()) && Agent::identityLoaded($this->getPrivateKeyFile())) { 33 | // echo "Removing identity {$this->unlockedPrivateKeyFile}\n"; 34 | Agent::deleteIdentity($this->getPrivateKeyFile()); 35 | } 36 | return $this; 37 | } 38 | 39 | public function __destruct() 40 | { 41 | if (!$this->keepIdentity) { 42 | $this->removeIdentity(); 43 | // Remove control paths for ssh multiplexing. See \AwsInspector\Ssh->__toString() 44 | // TODO: deleting these files isn't enough. The mux needs to be closed. 45 | exec('rm ~/mux* 2> /dev/null'); 46 | } 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/AwsInspector/Ssh/LocalConnection.php: -------------------------------------------------------------------------------- 1 | privateKeyFile = $privateKeyFile; 27 | } else { 28 | $encryptedPrivateKeyFile = $privateKeyFile . '.encrypted'; 29 | $this->privateKeyFile = $privateKeyFile . '.unlocked'; 30 | if (is_file($encryptedPrivateKeyFile)) { 31 | if (class_exists('\Vault\Vault')) { 32 | $vault = new Vault(); 33 | $vault->decryptFile($encryptedPrivateKeyFile, $this->privateKeyFile); 34 | chmod($this->privateKeyFile, 0600); 35 | $this->unlocked = true; 36 | } else { 37 | throw new \Exception('Please install aoepeople/vault'); 38 | } 39 | } else { 40 | throw new FileNotFoundException('Could not find private key file ' . $privateKeyFile); 41 | } 42 | } 43 | $this->privateKeyFile = realpath($this->privateKeyFile); 44 | } 45 | 46 | public function getPrivateKeyFile() { 47 | return $this->privateKeyFile; 48 | } 49 | 50 | public function __destruct() 51 | { 52 | if ($this->unlocked) { 53 | unlink($this->privateKeyFile); 54 | $this->unlocked = null; 55 | } 56 | } 57 | 58 | 59 | 60 | } -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/AbstractBlueprintCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('blueprint', InputArgument::REQUIRED, 'Blueprint'); 16 | } 17 | 18 | protected function interact(InputInterface $input, OutputInterface $output) 19 | { 20 | $this->interactAskForBlueprint($input, $output); 21 | } 22 | 23 | abstract protected function executeWithBlueprint(\StackFormation\Blueprint $blueprint, InputInterface $input, OutputInterface $output); 24 | 25 | protected final function execute(InputInterface $input, OutputInterface $output) 26 | { 27 | $blueprintName = $input->getArgument('blueprint'); 28 | $blueprint = $this->blueprintFactory->getBlueprint($blueprintName); 29 | return $this->executeWithBlueprint($blueprint, $input, $output); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/DeployPolicyCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:deploy-policy') 30 | ->setDescription('Deploy stack policy'); 31 | } 32 | 33 | protected function executeWithBlueprint(Blueprint $blueprint, InputInterface $input, OutputInterface $output) 34 | { 35 | $blueprintAction = new BlueprintAction($blueprint, $this->profileManager, $output); 36 | $blueprintAction->updateStackPolicy(); 37 | $output->writeln('Updated stack policy.'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/Show/ChangesetCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:show:changeset') 15 | ->setDescription('Preview changeset'); 16 | } 17 | 18 | protected function executeWithBlueprint(\StackFormation\Blueprint $blueprint, InputInterface $input, OutputInterface $output) 19 | { 20 | $blueprintAction = new \StackFormation\BlueprintAction($blueprint, $this->profileManager, $output); 21 | $changeSetResult = $blueprintAction->getChangeSet(); 22 | $table = new \StackFormation\Helper\ChangeSetTable($output); 23 | $table->render($changeSetResult); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/Show/DependenciesCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:show:dependencies') 17 | ->setDescription('Show (incoming) dependencies to stacks and environment variables'); 18 | } 19 | 20 | protected function executeWithBlueprint(Blueprint $blueprint, InputInterface $input, OutputInterface $output) 21 | { 22 | // trigger resolving all placeholders 23 | $this->dependencyTracker->reset(); 24 | $blueprint->getPreprocessedTemplate(); 25 | 26 | $output->writeln("Blueprint '{$blueprint->getName()} depends on following stack's resources/parameters/outputs:"); 27 | 28 | $table = new Table($output); 29 | $table->setHeaders(['Origin ('.$blueprint->getName().')', 'Source Stack', 'Field']) 30 | ->setRows($this->dependencyTracker->getStackDependenciesAsFlatList()) 31 | ->render(); 32 | 33 | $output->writeln("Blueprint '{$blueprint->getName()} depends on following environment variables:"); 34 | $table = new Table($output); 35 | $table->setHeaders(['Var', 'Current Value', 'Type', 'Origin (within "'.$blueprint->getName().'")']) 36 | ->setRows($this->dependencyTracker->getEnvDependenciesAsFlatList()) 37 | ->render(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/Show/ParametersCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:show:parameters') 19 | ->setDescription('Preview parameters and tags') 20 | ->addOption( 21 | 'unresolved', 22 | null, 23 | InputOption::VALUE_NONE, 24 | 'Do not resolve placeholders' 25 | ); 26 | } 27 | 28 | protected function executeWithBlueprint(Blueprint $blueprint, InputInterface $input, OutputInterface $output) 29 | { 30 | $unresolved = $input->getOption('unresolved'); 31 | 32 | $output->writeln("Blueprint '{$blueprint->getName()}':"); 33 | 34 | $parameters = $blueprint->getParameters(!$unresolved); 35 | 36 | $output->writeln('== PARAMETERS =='); 37 | $table = new Table($output); 38 | $table 39 | ->setHeaders(['Key', 'Value']) 40 | ->setRows($parameters); 41 | $table->render(); 42 | 43 | $output->writeln('== TAGS =='); 44 | $table = new Table($output); 45 | $table 46 | ->setHeaders(['Key', 'Value']) 47 | ->setRows($blueprint->getTags(!$unresolved)); 48 | $table->render(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/Show/StacknameCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:show:stackname') 16 | ->setDescription('Return stack name for given blueprint name (resolving placeholders)'); 17 | } 18 | 19 | protected function executeWithBlueprint(Blueprint $blueprint, InputInterface $input, OutputInterface $output) 20 | { 21 | $output->writeln($blueprint->getStackName()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/Show/TemplateCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:show:template') 17 | ->setDescription('Preview preprocessed template'); 18 | } 19 | 20 | protected function executeWithBlueprint(Blueprint $blueprint, InputInterface $input, OutputInterface $output) 21 | { 22 | $output->writeln($blueprint->getPreprocessedTemplate()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Blueprint/ValidateCommand.php: -------------------------------------------------------------------------------- 1 | setName('blueprint:validate') 19 | ->setDescription('Validate a blueprint\'s template'); 20 | } 21 | 22 | protected function executeWithBlueprint(Blueprint $blueprint, InputInterface $input, OutputInterface $output) 23 | { 24 | $blueprintAction = new BlueprintAction($blueprint, $this->profileManager, $output); 25 | $blueprintAction->validateTemplate(); // will throw an exception if there's a problem 26 | 27 | $formatter = new FormatterHelper(); 28 | $formattedBlock = $formatter->formatBlock(['No validation errors found.'], 'info', true); 29 | 30 | $output->writeln("\n\n$formattedBlock\n\n"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/StackFormation/Command/SetupCommand.php: -------------------------------------------------------------------------------- 1 | setName('setup') 20 | ->setDescription('Setup stackformation'); 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output) 24 | { 25 | $fs = new Filesystem(); 26 | 27 | if ($fs->exists(self::ENV_FILE)) { 28 | $output->writeln(self::ENV_FILE . ' already exist!'); 29 | return; 30 | } 31 | 32 | $helper = $this->getHelper('question'); 33 | $data = []; 34 | 35 | $awsAccessKeyIdQuestion= new Question('Please enter your AWS_ACCESS_KEY_ID: '); 36 | $data['AWS_ACCESS_KEY_ID'] = $helper->ask($input, $output, $awsAccessKeyIdQuestion); 37 | 38 | $awsSecretAccessKeyIdQuestion= new Question('Please enter your AWS_SECRET_ACCESS_KEY: '); 39 | $data['AWS_SECRET_ACCESS_KEY'] = $helper->ask($input, $output, $awsSecretAccessKeyIdQuestion); 40 | 41 | $regionQuestion = new Question('Please enter the name of your region [eu-west-1]: ', 'eu-west-1'); 42 | $data['AWS_DEFAULT_REGION'] = $helper->ask($input, $output, $regionQuestion); 43 | 44 | try { 45 | $fs->dumpFile(self::ENV_FILE, $this->parseContent($data)); 46 | $output->writeln(''); 47 | $output->writeln(self::ENV_FILE . ' file was successfully created.'); 48 | $output->writeln('You should also add it to your .gitignore with:'); 49 | $output->writeln('echo .env.default >> .gitignore'); 50 | } catch (IOExceptionInterface $e) { 51 | echo 'An error occurred while creating ' . self::ENV_FILE . ' file at ' . $e->getPath(); 52 | } 53 | } 54 | 55 | /** 56 | * @param array $$data 57 | * @return string 58 | */ 59 | protected function parseContent(array $data) 60 | { 61 | $content = ''; 62 | foreach ($data as $key => $value) { 63 | $content .= $key . '=' . $value . PHP_EOL; 64 | } 65 | 66 | return $content; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/AbstractStackCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('stack', InputArgument::REQUIRED, 'Stack'); 17 | $this->afterConfigure(); 18 | } 19 | 20 | protected function afterConfigure() 21 | { 22 | // overwrite this in your inheriting class (e.g. for adding optional arguments) 23 | } 24 | 25 | protected function interact(InputInterface $input, OutputInterface $output) 26 | { 27 | $this->interactAskForStack($input, $output); 28 | } 29 | 30 | abstract protected function executeWithStack(\StackFormation\Stack $stack, InputInterface $input, OutputInterface $output); 31 | 32 | protected final function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | $stackName = $input->getArgument('stack'); 35 | Validator::validateStackname($stackName); 36 | $stack = $this->getStackFactory()->getStack($stackName); 37 | return $this->executeWithStack($stack, $input, $output); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | setName('stack:delete') 20 | ->setDescription('Delete Stack') 21 | ->addArgument( 22 | 'stack', 23 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 24 | 'Stack' 25 | ) 26 | ->addOption( 27 | 'except', 28 | null, 29 | InputOption::VALUE_OPTIONAL, 30 | 'Stack that should NOT be deleted' 31 | ) 32 | ->addOption( 33 | 'force', 34 | null, 35 | InputOption::VALUE_NONE, 36 | 'Skip asking for confirmation' 37 | ); 38 | } 39 | 40 | protected function interact(InputInterface $input, OutputInterface $output) 41 | { 42 | $this->interactAskForStack($input, $output); 43 | 44 | if (!$input->getOption('force')) { 45 | $stacks = $this->getResolvedStacks($input); 46 | $stacks = "\n - " . implode("\n - ", $stacks) . "\n"; 47 | $helper = $this->getHelper('question'); 48 | $question = new ConfirmationQuestion("Are you sure you want to delete following stacks? $stacks [y/N] ", false); 49 | if (!$helper->ask($input, $output, $question)) { 50 | throw new \Exception('Operation aborted'); 51 | } 52 | $input->setOption('force', true); 53 | } 54 | } 55 | 56 | protected function getResolvedStacks(InputInterface $input) 57 | { 58 | $stacks = Finder::find( 59 | (array)$input->getArgument('stack'), 60 | $this->getStacks() 61 | ); 62 | 63 | $except = $input->getOption('except'); 64 | if (!empty($except)) { 65 | if (($key = array_search($except, $stacks)) !== false) { 66 | unset($stacks[$key]); 67 | } 68 | } 69 | 70 | return $stacks; 71 | } 72 | 73 | protected function execute(InputInterface $input, OutputInterface $output) 74 | { 75 | if (!$input->getOption('force')) { 76 | throw new \Exception('Operation aborted (use --force)'); 77 | } 78 | 79 | $stacks = $this->getResolvedStacks($input); 80 | 81 | if (count($stacks) == 0) { 82 | $output->writeln("No stacks deleted."); 83 | } 84 | 85 | foreach ($stacks as $stackName) { 86 | $this->getStackFactory()->getStack($stackName)->delete(); 87 | $output->writeln("Triggered deletion of stack '$stackName'."); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/DiffCommand.php: -------------------------------------------------------------------------------- 1 | setName('stack:diff') 20 | ->setDescription('Compare a stack\'s template and input parameters with its blueprint'); 21 | } 22 | 23 | protected function executeWithStack(Stack $stack, InputInterface $input, OutputInterface $output) 24 | { 25 | $blueprint = $this->blueprintFactory->getBlueprintByStack($stack); 26 | 27 | $diff = new Diff($output); 28 | $diff->setStack($stack)->setBlueprint($blueprint); 29 | 30 | $formatter = new FormatterHelper(); 31 | $output->writeln("\n" . $formatter->formatBlock(['Parameters:'], 'error', true) . "\n"); 32 | $diff->diffParameters(); 33 | 34 | $output->writeln("\n" . $formatter->formatBlock(['Template:'], 'error', true) . "\n"); 35 | $diff->diffTemplates(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/ListCommand.php: -------------------------------------------------------------------------------- 1 | setName('stack:list') 19 | ->setDescription('List all stacks') 20 | ->addOption( 21 | 'nameFilter', 22 | null, 23 | InputOption::VALUE_OPTIONAL, 24 | 'Name Filter (regex). Example --nameFilter \'/^foo/\'' 25 | ) 26 | ->addOption( 27 | 'statusFilter', 28 | null, 29 | InputOption::VALUE_OPTIONAL, 30 | 'Name Filter (regex). Example --statusFilter \'/IN_PROGRESS/\'' 31 | ); 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | $nameFilter = $input->getOption('nameFilter'); 37 | $statusFilter = $input->getOption('statusFilter'); 38 | 39 | $stacks = $this->getStackFactory()->getStacksFromApi(false, $nameFilter, $statusFilter); 40 | 41 | $rows = []; 42 | foreach ($stacks as $stackName => $stack) { /* @var $stack Stack */ 43 | $rows[] = [$stackName, Decorator::decorateStatus($stack->getStatus())]; 44 | } 45 | 46 | $table = new Table($output); 47 | $table 48 | ->setHeaders(['Name', 'Status']) 49 | ->setRows($rows); 50 | $table->render(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/ObserveCommand.php: -------------------------------------------------------------------------------- 1 | setName('stack:observe') 19 | ->setDescription('Observe stack progress') 20 | ->addOption( 21 | 'deleteOnTerminate', 22 | null, 23 | InputOption::VALUE_NONE, 24 | 'Delete current stack if StackFormation received SIGTERM (e.g. Jenkins job abort) or SIGINT (e.g. CTRL+C)' 25 | ); 26 | } 27 | 28 | protected function executeWithStack(Stack $stack, InputInterface $input, OutputInterface $output) 29 | { 30 | $observer = new Observer($stack, $this->getStackFactory(), $output); 31 | if ($input->getOption('deleteOnTerminate')) { 32 | $observer->deleteOnSignal(); 33 | } 34 | return $observer->observeStackActivity(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/Show/AbstractShowCommand.php: -------------------------------------------------------------------------------- 1 | property, ['resources', 'outputs', 'parameters'])) { 20 | throw new \Exception('Invalid property'); 21 | } 22 | 23 | $this 24 | ->setName('stack:show:'.$this->property) 25 | ->setDescription('Show a live stack\'s '.$this->property); 26 | } 27 | 28 | protected function afterConfigure() 29 | { 30 | $this->addArgument('key', InputArgument::OPTIONAL, 'key'); 31 | } 32 | 33 | protected function executeWithStack(Stack $stack, InputInterface $input, OutputInterface $output) 34 | { 35 | $methodName = 'get'.ucfirst($this->property); 36 | 37 | $key = $input->getArgument('key'); 38 | if ($key) { 39 | $methodName = substr($methodName, 0, -1); 40 | $output->writeln($stack->$methodName($key)); 41 | return; 42 | } 43 | 44 | $data = $stack->$methodName(); 45 | 46 | $rows = []; 47 | foreach ($data as $k => $v) { 48 | $v = strlen($v) > 100 ? substr($v, 0, 100) . "..." : $v; 49 | $rows[] = [$k, $v]; 50 | } 51 | 52 | $table = new Table($output); 53 | $table->setHeaders(['Key', 'Value']) 54 | ->setRows($rows) 55 | ->render(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/Show/DependantsCommand.php: -------------------------------------------------------------------------------- 1 | setName('stack:show:dependants') 20 | ->setDescription('Show (outgoing) dependencies to blueprints'); 21 | } 22 | 23 | protected function executeWithStack(Stack $stack, InputInterface $input, OutputInterface $output) 24 | { 25 | $this->dependencyTracker->reset(); 26 | foreach ($this->blueprintFactory->getAllBlueprints() as $blueprint) { 27 | $blueprint->getPreprocessedTemplate(); 28 | } 29 | 30 | $dependants = $this->dependencyTracker->findDependantsForStack($stack->getName()); 31 | 32 | $rows = []; 33 | foreach ($dependants as $dependant) { 34 | $rows[] = [ 35 | $dependant['targetType'] . ':' . $dependant['targetResource'], 36 | $dependant['type'] . ':' . $dependant['blueprint'] . ':' . $dependant['key'], 37 | ]; 38 | } 39 | 40 | $output->writeln("Following blueprints depend on stack '{$stack->getName()}':"); 41 | 42 | $table = new Table($output); 43 | $table->setHeaders(['Origin (Stack: '.$stack->getName() . ')', 'Blueprint']) 44 | ->setRows($rows) 45 | ->render(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/StackFormation/Command/Stack/Show/OutputsCommand.php: -------------------------------------------------------------------------------- 1 | setName('stack:tree') 19 | ->setDescription('List all stacks as tree') 20 | ->addOption( 21 | 'nameFilter', 22 | null, 23 | InputOption::VALUE_OPTIONAL, 24 | 'Name Filter (regex). Example --nameFilter \'/^foo/\'' 25 | ) 26 | ->addOption( 27 | 'statusFilter', 28 | null, 29 | InputOption::VALUE_OPTIONAL, 30 | 'Name Filter (regex). Example --statusFilter \'/IN_PROGRESS/\'' 31 | ); 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | $nameFilter = $input->getOption('nameFilter'); 37 | $statusFilter = $input->getOption('statusFilter'); 38 | 39 | $stacks = $this->getStackFactory()->getStacksFromApi(false, $nameFilter, $statusFilter); 40 | $dataTree = $this->prepareTree($stacks); 41 | $dataTree = $this->flatternTree($dataTree); 42 | $this->renderNode($dataTree); 43 | } 44 | 45 | /** 46 | * prepare stack list 47 | * 48 | * @param StackFormation\Stack[] 49 | * @return array 50 | */ 51 | protected function prepareTree(array $arr) { 52 | $tree = []; 53 | foreach ($arr as $a) { 54 | $name = $a->getName(); 55 | $cur = &$tree; 56 | foreach (explode("-", $name) as $e) { 57 | if (empty($cur[$e])) $cur[$e] = []; 58 | $cur = &$cur[$e]; 59 | } 60 | } 61 | return $tree; 62 | } 63 | 64 | /** 65 | * flattern tree to gain a better overview 66 | * 67 | * @param array $treeIn 68 | * @return array 69 | */ 70 | protected function flatternTree(array $treeIn) 71 | { 72 | $treeOut = []; 73 | foreach ($treeIn as $name => $children) { 74 | if (count($children) === 0) { 75 | $treeOut[$name] = []; 76 | } elseif (count($children) === 1) { 77 | $name = sprintf('%s-%s', $name, key($children)); 78 | if (count($children[key($children)]) > 0) { 79 | $treeOut[$name] = $this->flatternTree($children[key($children)]); 80 | }else{ 81 | $treeOut[$name] = []; 82 | } 83 | } else { 84 | $treeOut[$name] = $this->flatternTree($children); 85 | } 86 | 87 | } 88 | return $treeOut; 89 | } 90 | 91 | /** 92 | * render tree node 93 | * 94 | * @param string $tree 95 | * @param int $depth 96 | * @param int $cap 97 | */ 98 | protected function renderNode($tree, $depth = 0, $cap = 0) { 99 | 100 | $n = count($tree); 101 | foreach ($tree as $k => $next) { 102 | for ($pre = "", $i = $depth - 1; $i >= 0; $i--){ 103 | $pre.= $cap >> $i & 1 ? "│ " : " "; 104 | } 105 | echo $pre, --$n > 0 ? '├──' : '└──', $k, PHP_EOL; 106 | if (false === empty($next)){ 107 | $this->renderNode($next, $depth + 1, ($cap << 1) | ($n > 0)); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/StackFormation/CommandRegistry.php: -------------------------------------------------------------------------------- 1 | blueprintName = $blueprintname; 13 | $message = "Blueprint '$blueprintname' not found."; 14 | parent::__construct($message, $code, $previous); 15 | } 16 | 17 | public function getBlueprintName() 18 | { 19 | return $this->blueprintName; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/StackFormation/Exception/BlueprintReferenceNotFoundException.php: -------------------------------------------------------------------------------- 1 | stackName = $stackName; 13 | $message = "Invalid stack name '$stackName'"; 14 | parent::__construct($message, 0, $previous=null); 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getStackName() 21 | { 22 | return $this->stackName; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/StackFormation/Exception/MissingEnvVarException.php: -------------------------------------------------------------------------------- 1 | envVar = $varName; 13 | $message = "Environment variable '$varName' not found"; 14 | parent::__construct($message, $code, $previous); 15 | } 16 | 17 | public function getEnvVar() 18 | { 19 | return $this->envVar; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/StackFormation/Exception/NoBlueprintsFoundException.php: -------------------------------------------------------------------------------- 1 | stackName = $stackName; 14 | $this->state = $state; 15 | parent::__construct("Stack '$stackName' not found (state: $state)", 0, $previous); 16 | } 17 | 18 | public function getStackName() { 19 | return $this->stackName; 20 | } 21 | 22 | public function getState() { 23 | return $this->state; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/StackFormation/Exception/StackNoUpdatesToBePerformedException.php: -------------------------------------------------------------------------------- 1 | stackName = $stackName; 14 | parent::__construct("No updates to be performened on stack $stackName", 0, $previous); 15 | } 16 | 17 | public function getStackName() { 18 | return $this->stackName; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/StackFormation/Exception/StackNotFoundException.php: -------------------------------------------------------------------------------- 1 | sourceBlueprint = $sourceBlueprint; 17 | $this->sourceType = $sourceType; 18 | $this->sourceKey = $sourceKey; 19 | 20 | if (!is_scalar($value)) { 21 | $value = var_export($value, true); 22 | } 23 | 24 | $message = "Error resolving value '$value'" . $this->getExceptionMessageAppendix(); 25 | parent::__construct($message, 0, $previous); 26 | } 27 | 28 | /** 29 | * Craft exception message appendix 30 | * 31 | * @return string 32 | */ 33 | protected function getExceptionMessageAppendix() 34 | { 35 | $tmp = []; 36 | if ($this->sourceBlueprint) { $tmp[] = 'Blueprint: ' . $this->sourceBlueprint->getName(); } 37 | if ($this->sourceType) { $tmp[] = 'Type:' . $this->sourceType; } 38 | if ($this->sourceKey) { $tmp[] = 'Key:' . $this->sourceKey; } 39 | if (count($tmp)) { 40 | return ' (' . implode(', ', $tmp) . ')'; 41 | } 42 | return ''; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/Cache.php: -------------------------------------------------------------------------------- 1 | get('message', function() { return 'Hello World'; }); 12 | * 13 | * Will return the message from cache if present and will generate it 14 | * by executing the callback and store it in the cache before returning the value. 15 | * 16 | * @author Fabrizio Branca 17 | */ 18 | class Cache 19 | { 20 | 21 | protected $cache = []; 22 | 23 | /** 24 | * Get (and generate) 25 | * 26 | * @param $key 27 | * @param callable|null $callback 28 | * @param bool $fresh 29 | * @return mixed 30 | * @throws \Exception 31 | */ 32 | public function get($key, callable $callback = null, $fresh = false) 33 | { 34 | if ($fresh || !$this->has($key)) { 35 | if (!is_null($callback)) { 36 | $this->set($key, $callback()); 37 | } else { 38 | throw new \Exception(sprintf("Cache key '%s' not found.", $key)); 39 | } 40 | } /* else { 41 | echo "HIT!"; 42 | } */ 43 | return $this->cache[$key]; 44 | } 45 | 46 | /** 47 | * Has 48 | * 49 | * @param $key 50 | * @return bool 51 | */ 52 | public function has($key) 53 | { 54 | return isset($this->cache[$key]); 55 | } 56 | 57 | /** 58 | * Set 59 | * 60 | * @param $key 61 | * @param $value 62 | */ 63 | public function set($key, $value) 64 | { 65 | $this->cache[$key] = $value; 66 | } 67 | 68 | /** 69 | * Delete 70 | * 71 | * @param $key 72 | */ 73 | public function delete($key) 74 | { 75 | unset($this->cache[$key]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/ChangeSetTable.php: -------------------------------------------------------------------------------- 1 | setHeaders(['Action', 'LogicalResourceId', 'PhysicalResourceId', 'ResourceType', 'Replacement']); 11 | $this->setRows($this->getRows($changeSetResult)); 12 | parent::render(); 13 | } 14 | 15 | protected function getRows(\Aws\Result $changeSetResult) { 16 | $rows = []; 17 | foreach ($changeSetResult->search('Changes[]') as $change) { 18 | $resourceChange = $change['ResourceChange']; 19 | $rows[] = [ 20 | // $change['Type'], // would this ever show anything other than 'Resource'? 21 | $this->decorateChangesetAction($resourceChange['Action']), 22 | $resourceChange['LogicalResourceId'], 23 | isset($resourceChange['PhysicalResourceId']) ? $resourceChange['PhysicalResourceId'] : '', 24 | $resourceChange['ResourceType'], 25 | isset($resourceChange['Replacement']) ? Decorator::decorateChangesetReplacement($resourceChange['Replacement']) : '', 26 | ]; 27 | } 28 | return $rows; 29 | } 30 | 31 | protected function decorateChangesetAction($changeSetAction) { 32 | switch ($changeSetAction) { 33 | case 'Modify': return "$changeSetAction"; 34 | case 'Add': return "$changeSetAction"; 35 | case 'Remove': return "$changeSetAction"; 36 | } 37 | return $changeSetAction; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/StackFormation/Helper/Decorator.php: -------------------------------------------------------------------------------- 1 | UPDATE_ROLLBACK_COMPLETE"; 14 | } 15 | if (strpos($status, 'IN_PROGRESS') !== false) { 16 | return "$status"; 17 | } 18 | if (strpos($status, 'COMPLETE') !== false) { 19 | return "$status"; 20 | } 21 | if (strpos($status, 'FAILED') !== false) { 22 | return "$status"; 23 | } 24 | return $status; 25 | } 26 | 27 | public static function decorateChangesetReplacement($changeSetReplacement) 28 | { 29 | if ($changeSetReplacement == 'Conditional') { 30 | return "$changeSetReplacement"; 31 | } 32 | if ($changeSetReplacement == 'False') { 33 | return "$changeSetReplacement"; 34 | } 35 | if ($changeSetReplacement == 'True') { 36 | return "$changeSetReplacement"; 37 | } 38 | return $changeSetReplacement; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/Div.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | $tmp[] = "$key$keyValueSeparator$value"; 22 | } 23 | return implode($itemSeparator, $tmp); 24 | } 25 | 26 | /** 27 | * @param string $program 28 | * @return bool 29 | * @see n98-magerun/src/N98/Util/OperatingSystem.php 30 | */ 31 | public static function isProgramInstalled($program) 32 | { 33 | $out = null; 34 | $return = null; 35 | @exec('which ' . $program, $out, $return); 36 | return $return === 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/Exception.php: -------------------------------------------------------------------------------- 1 | getResponse()->getBody(); 11 | $xml = simplexml_load_string($message); 12 | if ($xml !== false && $xml->Error->Message) { 13 | return $xml->Error->Message; 14 | } 15 | return $exception->getMessage(); 16 | } 17 | 18 | public static function refineException(\Aws\CloudFormation\Exception\CloudFormationException $exception) 19 | { 20 | $message = self::extractMessage($exception); 21 | $matches = []; 22 | if (preg_match('/^Stack \[(.+)\] does not exist$/', $message, $matches)) { 23 | return new \StackFormation\Exception\StackNotFoundException($matches[1], $exception); 24 | } 25 | if (preg_match('/.+stack\/(.+)\/.+is in ([A-Z_]+) state and can not be updated./', $message, $matches)) { 26 | return new \StackFormation\Exception\StackCannotBeUpdatedException($matches[1], $matches[2], $exception); 27 | } 28 | if (strpos($message, 'No updates are to be performed.') !== false) { 29 | return new \StackFormation\Exception\StackNoUpdatesToBePerformedException('TBD'); 30 | } 31 | return new \StackFormation\Exception\CleanCloudFormationException($message, 0, $exception); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/Finder.php: -------------------------------------------------------------------------------- 1 | describeLogGroups($params); 55 | foreach ($resGroups->search('logGroups[].logGroupName') as $logGroupName) { 56 | $streamsNextToken = null; 57 | do { 58 | $streamsParams = [ 59 | 'logGroupName' => $logGroupName, 60 | 'orderBy' => 'LastEventTime' 61 | ]; 62 | if ($streamsNextToken) { 63 | $streamsParams['nextToken'] = $streamsNextToken; 64 | } 65 | $resStreams = $cloudWatchLogClient->describeLogStreams($streamsParams); 66 | foreach ($resStreams->search('logStreams[].logStreamName') as $logStreamName) { 67 | if ($stream == $logStreamName) { 68 | return $logGroupName; 69 | } 70 | } 71 | $streamsNextToken = $resStreams->get("nextToken"); 72 | } while ($streamsNextToken); 73 | } 74 | $groupsNextToken = $resGroups->get("nextToken"); 75 | } while ($groupsNextToken); 76 | return null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/Pipeline.php: -------------------------------------------------------------------------------- 1 | stages[] = $stage; 24 | return $this; 25 | } 26 | 27 | /** 28 | * Process the payload. 29 | * 30 | * @param $payload 31 | * @return mixed 32 | */ 33 | public function process($payload) 34 | { 35 | foreach ($this->stages as $stage) { 36 | $payload = call_user_func($stage, $payload); 37 | } 38 | return $payload; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/StaticCache.php: -------------------------------------------------------------------------------- 1 | get($key, $callback, $fresh); 34 | } 35 | 36 | /** 37 | * Has 38 | * 39 | * @param $key 40 | * @return bool 41 | */ 42 | public function has($key) 43 | { 44 | return self::getCache()->has($key); 45 | } 46 | 47 | /** 48 | * Set 49 | * 50 | * @param $key 51 | * @param $value 52 | */ 53 | public function set($key, $value) 54 | { 55 | return self::getCache()->set($key, $value); 56 | } 57 | 58 | /** 59 | * Delete 60 | * 61 | * @param $key 62 | */ 63 | public function delete($key) 64 | { 65 | return self::getCache()->delete($key); 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/StackFormation/Helper/Validator.php: -------------------------------------------------------------------------------- 1 | 10) { 27 | throw new \Exception('No more than 10 tags are allowed'); 28 | } 29 | foreach ($tags as $tag) { 30 | // key 31 | if (!isset($tag['Key'])) { 32 | throw new \Exception('Tag key is missing'); 33 | } 34 | $key = $tag['Key']; 35 | if (strlen($key) > 127) { 36 | throw new \Exception('Keys cannot be longer than 127 characters'); 37 | } 38 | if (!preg_match('/^[a-zA-Z0-9_\-+=:\/@\.]{1,127}$/', $key)) { 39 | throw new \Exception("Invalid characters in key '$key'"); 40 | } 41 | if (strpos($key, 'aws:') === 0) { 42 | throw new \Exception('The aws: prefix cannot be used for keys'); 43 | } 44 | 45 | // value 46 | if (!isset($tag['Value'])) { 47 | throw new \Exception('Tag value is missing'); 48 | } 49 | $value = $tag['Value']; 50 | if (strlen($value) > 255) { 51 | throw new \Exception('Values cannot be longer than 255 characters'); 52 | } 53 | if (!preg_match('/^[a-zA-Z0-9_\-+=:\/@\.]{1,255}$/', $value)) { 54 | throw new \Exception("Invalid characters in value '$value' (key: $key)"); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/StackFormation/Poller.php: -------------------------------------------------------------------------------- 1 | filepath = $filePath; 18 | $this->cache = new \StackFormation\Helper\Cache(); 19 | $this->preProcessor = $preprocessor ? $preprocessor : new Preprocessor(); 20 | } 21 | 22 | public function getFilePath() 23 | { 24 | return $this->filepath; 25 | } 26 | 27 | public function getFileContent() 28 | { 29 | return $this->cache->get( 30 | __METHOD__, 31 | function () { 32 | return file_get_contents($this->filepath); 33 | } 34 | ); 35 | } 36 | 37 | public function getProcessedTemplate() 38 | { 39 | return $this->cache->get( 40 | __METHOD__, 41 | function () { 42 | return $this->preProcessor->processJson($this->getFileContent(), $this->getBasePath()); 43 | } 44 | ); 45 | } 46 | 47 | public function getDecodedJson() 48 | { 49 | if (!$this->cache->has(__METHOD__)) { 50 | $templateBody = $this->getProcessedTemplate(); 51 | $array = json_decode($templateBody, true); 52 | if (!is_array($array)) { 53 | throw new TemplateDecodeException($this->getFilePath(), sprintf("Error decoding file '%s'", $this->getFilePath())); 54 | } 55 | if ($array['AWSTemplateFormatVersion'] != '2010-09-09') { 56 | throw new TemplateInvalidException($this->getFilePath(), 'Invalid AWSTemplateFormatVersion'); 57 | } 58 | 59 | $this->cache->set(__METHOD__, $array); 60 | } 61 | 62 | return $this->cache->get(__METHOD__); 63 | } 64 | 65 | public function getDescription() 66 | { 67 | $data = $this->getDecodedJson(); 68 | 69 | return isset($data['Description']) ? $data['Description'] : ''; 70 | } 71 | 72 | public function getBasePath() 73 | { 74 | return dirname($this->getFilePath()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/StackFormation/TemplateDecodeException.php: -------------------------------------------------------------------------------- 1 | templateFile = $templateFile; 13 | } 14 | 15 | public function getTemplateFile() 16 | { 17 | return $this->templateFile; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/StackFormation/TemplateInvalidException.php: -------------------------------------------------------------------------------- 1 | templateFile = $templateFile; 13 | } 14 | 15 | public function getTemplateFile() 16 | { 17 | return $this->templateFile; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/StackFormation/Terminator.php: -------------------------------------------------------------------------------- 1 | currentStack = $stack; 13 | $this->output = $output; 14 | } 15 | 16 | public function setupSignalHandler() 17 | { 18 | $this->output->writeln('Handling signals SIGTERM and SIGINT for stack ' . $this->currentStack->getName()); 19 | declare(ticks = 1); 20 | pcntl_signal(SIGTERM, array($this, 'signalHandler')); // Jenkins: aborting a job 21 | pcntl_signal(SIGINT, array($this, 'signalHandler')); // CTRL+C on command line 22 | } 23 | 24 | public function signalHandler($signo) 25 | { 26 | switch ($signo) { 27 | case SIGINT: $this->output->writeln("Caught SIGINT"); break; 28 | case SIGTERM: $this->output->writeln("Caught SIGTERM"); break; 29 | default: $this->output->writeln("Caught $signo"); break; 30 | } 31 | $this->output->writeln("Deleting stack {$this->currentStack->getName()}"); 32 | $this->currentStack->delete(); 33 | exit; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/AbstractValueResolverStage.php: -------------------------------------------------------------------------------- 1 | valueResolver = $valueResolver; 20 | $this->sourceBlueprint = $sourceBlueprint; 21 | $this->sourceType = $sourceType; 22 | $this->sourceKey = $sourceKey; 23 | } 24 | 25 | public function __invoke($string) 26 | { 27 | try { 28 | return $this->invoke($string); 29 | } catch (\Exception $e) { 30 | throw new \StackFormation\Exception\ValueResolverException($string, $this->sourceBlueprint, $this->sourceType, $this->sourceKey, $e); 31 | } 32 | } 33 | 34 | abstract function invoke($string); 35 | 36 | /** 37 | * Convenience method 38 | * 39 | * @return \StackFormation\StackFactory 40 | */ 41 | protected function getStackFactory() 42 | { 43 | return $this->valueResolver->getStackFactory($this->sourceBlueprint); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/Clean.php: -------------------------------------------------------------------------------- 1 | $value) { 16 | if ($this->isTrue($condition, $this->sourceBlueprint)) { 17 | return $value; 18 | } 19 | } 20 | return ''; 21 | } 22 | 23 | /** 24 | * Evaluate is key 'is true' 25 | * 26 | * @param $condition 27 | * @param Blueprint|null $sourceBlueprint 28 | * @return bool 29 | * @throws \Exception 30 | */ 31 | public function isTrue($condition, Blueprint $sourceBlueprint=null) 32 | { 33 | // resolve placeholders 34 | $condition = $this->valueResolver->resolvePlaceholders($condition, $sourceBlueprint, 'conditional_value', $condition); 35 | 36 | if ($condition == 'default') { 37 | return true; 38 | } 39 | if (strpos($condition, '==') !== false) { 40 | list($left, $right) = explode('==', $condition, 2); 41 | $left = trim($left); 42 | $right = trim($right); 43 | return ($left == $right); 44 | } elseif (strpos($condition, '!=') !== false) { 45 | list($left, $right) = explode('!=', $condition, 2); 46 | $left = trim($left); 47 | $right = trim($right); 48 | return ($left != $right); 49 | } elseif (strpos($condition, '~=') !== false) { 50 | list($subject, $pattern) = explode('~=', $condition, 2); 51 | $subject = trim($subject); 52 | $pattern = trim($pattern); 53 | return preg_match($pattern, $subject); 54 | } 55 | throw new \Exception('Invalid condition: ' . $condition); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/EnvironmentVariable.php: -------------------------------------------------------------------------------- 1 | valueResolver->getDependencyTracker()->trackEnvUsage($matches[1], false, $value, $this->sourceBlueprint, $this->sourceType, $this->sourceKey); 20 | return getenv($matches[1]); 21 | }, 22 | $string 23 | ); 24 | return $string; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/EnvironmentVariableWithFallback.php: -------------------------------------------------------------------------------- 1 | valueResolver->getDependencyTracker()->trackEnvUsage($matches[1], true, $value, $this->sourceBlueprint, $this->sourceType, $this->sourceKey); 16 | return $value; 17 | }, 18 | $string 19 | ); 20 | return $string; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/Md5.php: -------------------------------------------------------------------------------- 1 | sourceBlueprint) { 18 | chdir($this->sourceBlueprint->getBasePath()); 19 | } 20 | if (!is_file($file)) { 21 | throw new FileNotFoundException("File '$file' not found."); 22 | } 23 | $md5 = md5_file($file); 24 | chdir($cwd); 25 | return $md5; 26 | }, 27 | $string 28 | ); 29 | return $string; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/ProfileSwitcher.php: -------------------------------------------------------------------------------- 1 | valueResolver->getDependencyTracker(), 21 | $this->valueResolver->getProfileManager(), 22 | $this->valueResolver->getConfig(), 23 | $matches[1] 24 | ); 25 | $value = $subValueResolver->resolvePlaceholders($matches[2], $this->sourceBlueprint, $this->sourceType, $this->sourceKey); 26 | 27 | // restoring AWS_UNSET_PROFILE value if it was set before 28 | if ($unsetProfileBackup) { 29 | putenv('AWS_UNSET_PROFILE=1'); 30 | } 31 | return $value; 32 | }, 33 | $string 34 | ); 35 | return $string; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/StackOutput.php: -------------------------------------------------------------------------------- 1 | valueResolver->getDependencyTracker()->trackStackDependency('output', $matches[1], $matches[2], $this->sourceBlueprint, $this->sourceType, $this->sourceKey); 16 | return $this->getStackFactory()->getStackOutput($matches[1], $matches[2]); 17 | }, 18 | $string 19 | ); 20 | return $string; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/StackParameter.php: -------------------------------------------------------------------------------- 1 | valueResolver->getDependencyTracker()->trackStackDependency('parameter', $matches[1], $matches[2], $this->sourceBlueprint, $this->sourceType, $this->sourceKey); 16 | return $this->getStackFactory()->getStackParameter($matches[1], $matches[2]); 17 | }, 18 | $string 19 | ); 20 | return $string; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/StackResource.php: -------------------------------------------------------------------------------- 1 | valueResolver->getDependencyTracker()->trackStackDependency('resource', $matches[1], $matches[2], $this->sourceBlueprint, $this->sourceType, $this->sourceKey); 16 | return $this->getStackFactory()->getStackResource($matches[1], $matches[2]); 17 | }, 18 | $string 19 | ); 20 | return $string; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/StackFormation/ValueResolver/Stage/Tstamp.php: -------------------------------------------------------------------------------- 1 | valueResolver->getConfig()->getGlobalVars(); 15 | if ($this->sourceBlueprint) { 16 | $vars = array_merge($vars, $this->sourceBlueprint->getVars()); 17 | } 18 | if (!isset($vars[$matches[1]])) { 19 | throw new \Exception("Variable '{$matches[1]}' not found"); 20 | } 21 | $value = $vars[$matches[1]]; 22 | if (is_array($value)) { 23 | $value = $this->valueResolver->resolvePlaceholders($value, $this->sourceBlueprint); 24 | } 25 | if ($value == $matches[0]) { 26 | throw new \Exception('Direct circular reference detected'); 27 | } 28 | return $value; 29 | }, 30 | $string 31 | ); 32 | return $string; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/dotenv.php: -------------------------------------------------------------------------------- 1 | overload(); 9 | } 10 | if (is_readable(CWD . DIRECTORY_SEPARATOR . '.env')) { 11 | $dotenv = new Dotenv\Dotenv(CWD); 12 | $dotenv->overload(); 13 | } 14 | } 15 | 16 | register_shutdown_function(function() { 17 | \AwsInspector\Ssh\Connection::closeMuxConnections(); 18 | }); 19 | -------------------------------------------------------------------------------- /src/stackformation.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add($command); 17 | } 18 | foreach (\AwsInspector\CommandRegistry::getCommands() as $command) { 19 | $app->add($command); 20 | } 21 | 22 | $app->run(); 23 | -------------------------------------------------------------------------------- /tests/AwsInspector/Model/CollectionTest.php: -------------------------------------------------------------------------------- 1 | title = 'StackFormation'; 16 | $collection->attach($a); 17 | 18 | $b = new \stdClass(); 19 | $b->title = 'WhateverElse'; 20 | $collection->attach($b); 21 | 22 | $collection->next(); 23 | 24 | $this->assertSame($a, $collection->getFirst()); 25 | $this->assertSame('StackFormation', $collection->getFirst()->title); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/AwsInspector/Model/Ec2/InstanceTest.php: -------------------------------------------------------------------------------- 1 | [], 17 | 'PrivateIpAddress' => '4.5.7.6' 18 | ]); 19 | $connection = $instance->getSshConnection(); 20 | $this->assertEquals('4.5.7.6', $connection->getHost()); 21 | $this->assertEquals('ec2-user', $connection->getUsername()); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function user() 28 | { 29 | $instance = new Instance([ 30 | 'Tags' => [['Key' => 'inspector', 'Value' => 'User:Foo']], 31 | 'PrivateIpAddress' => '4.5.7.6', 32 | ]); 33 | $this->assertEquals('User:Foo', $instance->getTag('inspector')); 34 | $connection = $instance->getSshConnection(); 35 | $this->assertEquals('4.5.7.6', $connection->getHost()); 36 | $this->assertEquals('Foo', $connection->getUsername()); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function sshConnectionTestViaBastion() 43 | { 44 | $instance = $this->getMock('AwsInspector\Model\Ec2\Instance', ['getJumpHost'], [[ 45 | 'Tags' => [ 46 | ['Key' => 'inspector', 'Value' => 'User:Foo,Type:Bastion,Environment:dpl'] 47 | ], 48 | 'PrivateIpAddress' => '4.5.7.6', 49 | ]]); 50 | $instance->method('getJumpHost')->willReturn(new Instance([ 51 | 'Tags' => [['Key' => 'inspector', 'Value' => 'User:Bar']], 52 | 'PrivateIpAddress' => '1.2.3.4', 53 | ])); 54 | 55 | /* @var $instance Instance */ 56 | $this->assertEquals('User:Foo,Type:Bastion,Environment:dpl', $instance->getTag('inspector')); 57 | 58 | $connection = $instance->getSshConnection(); 59 | $this->assertEquals('4.5.7.6', $connection->getHost()); 60 | $this->assertEquals('Foo', $connection->getUsername()); 61 | 62 | $proxyCommand = "ssh -o ConnectTimeout=5 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null Bar@1.2.3.4 'nc %h %p'"; 63 | $command = 'ssh -o ProxyCommand="'.$proxyCommand.'" -o ConnectTimeout=5 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null Foo@4.5.7.6'; 64 | 65 | $this->assertEquals($command, $connection->__toString()); 66 | } 67 | 68 | public function exec() 69 | { 70 | $instance = new Instance([ 71 | 'Tags' => [], 72 | 'PrivateIpAddress' => '1.2.3.4', 73 | ]); 74 | 75 | $instance->exec(); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tests/AwsInspector/RegistryTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expectedValue, $value); 16 | } 17 | 18 | /** 19 | * @test 20 | */ 21 | public function getReturnsExpectedObject() 22 | { 23 | $object = new \stdClass(); 24 | $object->value = 'My String'; 25 | \AwsInspector\Registry::set('key_phpunit_getReturnsExpectedObject', $object); 26 | $value = \AwsInspector\Registry::get('key_phpunit_getReturnsExpectedObject'); 27 | $this->assertTrue($value instanceof \stdClass); 28 | $this->assertSame($object->value, $value->value); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function getReturnsFalseIfExpectedKeyIsNotAvailable() 35 | { 36 | $value = \AwsInspector\Registry::get('key_phpunit_getReturnsFalseIfExpectedKeyIsNotAvailable'); 37 | $this->assertFalse($value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/AwsInspector/Ssh/CommandTest.php: -------------------------------------------------------------------------------- 1 | getMock('\AwsInspector\Ssh\Connection', [], [], '', false); 17 | $connectionMock->method('__toString')->willReturn('connection'); 18 | $command = new Command($connectionMock, 'command'); 19 | $this->assertEquals("connection 'command'", $command->__toString()); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function commandWithArguments() 26 | { 27 | $connectionMock = $this->getMock('\AwsInspector\Ssh\Connection', [], [], '', false); 28 | $connectionMock->method('__toString')->willReturn('connection'); 29 | $command = new Command($connectionMock, 'echo '.escapeshellarg('Hello World')); 30 | $this->assertEquals("connection 'echo '\''Hello World'\'''", $command->__toString()); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function runLocalCommand() 37 | { 38 | $testfile = tempnam(sys_get_temp_dir(), __FUNCTION__); 39 | $command = new Command(new LocalConnection(), 'echo -n '.escapeshellarg('Hello World') . ' > ' . $testfile); 40 | $this->assertEquals("echo -n 'Hello World' > $testfile", $command->__toString()); 41 | $command->exec(); 42 | $this->assertEquals('Hello World', file_get_contents($testfile)); 43 | unlink($testfile); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function asUser() 50 | { 51 | $connectionMock = $this->getMock('\AwsInspector\Ssh\Connection', [], [], '', false); 52 | $connectionMock->method('__toString')->willReturn('connection'); 53 | $command = new Command($connectionMock, 'whoami', 'www-data'); 54 | $this->assertEquals( 55 | "connection 'sudo -u '\''www-data'\'' bash -c '\''whoami'\'''", 56 | $command->__toString() 57 | ); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function runLocalCommandasUser() 64 | { 65 | try { 66 | $testfile = tempnam(sys_get_temp_dir(), __FUNCTION__); 67 | $command = new Command(new LocalConnection(), 'whoami > ' . $testfile, 'root'); 68 | $command->exec(); 69 | $this->assertEquals('root', trim(file_get_contents($testfile))); 70 | unlink($testfile); 71 | } catch (\Exception $e) { 72 | $this->markTestSkipped('It is ok if this test fails'); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/AwsInspector/Ssh/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 19 | 'ssh -o ConnectTimeout=5 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null TestUsername@1.2.3.4', 20 | $connection->__toString() 21 | ); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function withPrivateKey() 28 | { 29 | $connection = new Connection('TestUsername', '1.2.3.4', PrivateKey::get(FIXTURE_ROOT . 'foo.pem')); 30 | $this->assertEquals( 31 | 'ssh -i '.FIXTURE_ROOT.'foo.pem -o ConnectTimeout=5 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null TestUsername@1.2.3.4', 32 | $connection->__toString() 33 | ); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function withJumpHost() 40 | { 41 | $jumpHost = new Instance([ 42 | 'Tags' => [], 43 | 'PrivateIpAddress' => '4.5.7.6' 44 | ]); 45 | $connection = new Connection('TestUsername', '1.2.3.4', null, $jumpHost); 46 | $this->assertEquals( 47 | 'ssh -o ProxyCommand="ssh -o ConnectTimeout=5 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ec2-user@4.5.7.6 \'nc %h %p\'" -o ConnectTimeout=5 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null TestUsername@1.2.3.4', 48 | $connection->__toString() 49 | ); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /tests/StackFormation/CacheTest.php: -------------------------------------------------------------------------------- 1 | cache = new \StackFormation\Helper\Cache(); 13 | } 14 | 15 | /** 16 | * @throws Exception 17 | * @test 18 | */ 19 | public function storeInCache() 20 | { 21 | $result = $this->cache->has('test'); 22 | $this->assertFalse($result); 23 | 24 | $this->cache->set('test', '42'); 25 | $result = $this->cache->has('test'); 26 | $this->assertTrue($result); 27 | 28 | $result = $this->cache->get('test'); 29 | $this->assertEquals('42', $result); 30 | 31 | $this->cache->delete('test'); 32 | $result = $this->cache->has('test'); 33 | 34 | $this->assertFalse($result); 35 | } 36 | 37 | /** 38 | * @throws Exception 39 | * @test 40 | */ 41 | public function storeInCacheWithCallback() 42 | { 43 | $result = $this->cache->has('test'); 44 | $this->assertFalse($result); 45 | 46 | $result = $this->cache->get('test', function() { 47 | return '42'; 48 | }); 49 | $this->assertEquals('42', $result); 50 | 51 | $result = $this->cache->get('test'); 52 | $this->assertEquals('42', $result); 53 | 54 | $this->cache->delete('test'); 55 | $result = $this->cache->has('test'); 56 | 57 | $this->assertFalse($result); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function keyNotFound() 64 | { 65 | $this->setExpectedException('Exception', "Cache key 'doesnotexist' not found."); 66 | $this->cache->get('doesnotexist'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/StackFormation/ConfigTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('Exception', "Stackname 'a' does not specify a template."); 13 | $config = new \StackFormation\Config([FIXTURE_ROOT . '/Config/blueprint.template_missing.yml']); 14 | } 15 | 16 | /** 17 | * @test 18 | */ 19 | public function duplicateStackName() 20 | { 21 | $this->setExpectedException('Exception', "Stackname 'a' was declared more than once."); 22 | $config = new \StackFormation\Config([FIXTURE_ROOT . '/Config/blueprint.duplicate_stackname.yml']); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function globalVariable() 29 | { 30 | $config = new \StackFormation\Config([FIXTURE_ROOT . '/Config/blueprint.1.yml']); 31 | $globalVars = $config->getGlobalVars(); 32 | $this->assertArrayHasKey('GlobalFoo', $globalVars); 33 | $this->assertEquals('GlobalBar', $globalVars['GlobalFoo']); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function getBlueprints() 40 | { 41 | $config = new \StackFormation\Config([FIXTURE_ROOT . '/Config/blueprint.1.yml']); 42 | $this->assertTrue($config->blueprintExists('fixture1')); 43 | $this->assertTrue($config->blueprintExists('fixture2')); 44 | $names = $config->getBlueprintNames(); 45 | $this->assertTrue(in_array('fixture1', $names)); 46 | $this->assertTrue(in_array('fixture2', $names)); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function invalidAttribute() 53 | { 54 | $this->setExpectedException( 55 | '\Symfony\Component\Config\Definition\Exception\InvalidConfigurationException', 56 | 'Unrecognized option "parameter" under "root.blueprints.invalid_attribute"' 57 | ); 58 | $config = new \StackFormation\Config([FIXTURE_ROOT . '/Config/blueprint.invalid_attribute.yml']); 59 | } 60 | 61 | //public function testDuplicateGlobalVar() 62 | //{ 63 | // $this->markTestSkipped('This is not so trivial :)'); 64 | // $this->setExpectedException('Exception'); 65 | // $config = new \StackFormation\Config([FIXTURE_ROOT . '/Config/blueprint.duplicateglobalvar.yml']); 66 | // $vars = $config->getGlobalVars(); 67 | //} 68 | } 69 | -------------------------------------------------------------------------------- /tests/StackFormation/HelperTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('Exception'); 42 | Validator::validateStackname($stackName); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function invalidStackNameProvider() 49 | { 50 | return [ 51 | [new \stdClass()], 52 | [ []], 53 | [ null ], 54 | [ 42 ], 55 | ['ecom_t_stack'], 56 | [''], 57 | ['stackname with whitespace'], 58 | ['123ecom'], 59 | [str_repeat('a', 129)], 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/StackFormation/PollerTest.php: -------------------------------------------------------------------------------- 1 | counters[__METHOD__])) { $this->counters[__METHOD__] = 0; } 19 | return $this->counters[__METHOD__]++ > 5; 20 | }; 21 | \StackFormation\Poller::poll($function, 0); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function pollExceed() 28 | { 29 | $this->setExpectedException('Exception', 'Max polls exceeded.'); 30 | $function = function() { 31 | if (!isset($this->counters[__METHOD__])) { $this->counters[__METHOD__] = 0; } 32 | return $this->counters[__METHOD__]++ > 51; 33 | }; 34 | \StackFormation\Poller::poll($function, 0); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function returnValueFromCallback() 41 | { 42 | $function = function() { 43 | if (!isset($this->counters[__METHOD__])) { $this->counters[__METHOD__] = 0; } 44 | return ($this->counters[__METHOD__]++ > 5) ? "HELLO WORLD" : false; 45 | }; 46 | $result = \StackFormation\Poller::poll($function, 0); 47 | $this->assertEquals("HELLO WORLD", $result); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/StackFormation/PreprocessorTest.php: -------------------------------------------------------------------------------- 1 | preprocessor = new \StackFormation\Preprocessor(); 16 | } 17 | 18 | /** 19 | * @param string $fixtureDirectory 20 | * @throws \Exception 21 | * @test 22 | * @dataProvider processFileDataProvider 23 | */ 24 | public function processFile($fixtureDirectory) 25 | { 26 | $prefix = FIXTURE_ROOT . 'Preprocessor/'; 27 | $prefix .= $fixtureDirectory . '/'; 28 | $templatePath = $prefix . 'blueprint/input.template'; 29 | $fileContent = file_get_contents($templatePath); 30 | $this->assertEquals( 31 | $this->preprocessor->processJson($fileContent, dirname($templatePath)), 32 | file_get_contents($prefix. 'blueprint/expected.template') 33 | ); 34 | } 35 | 36 | public function processFileDataProvider() 37 | { 38 | $prefix = FIXTURE_ROOT . 'Preprocessor/'; 39 | $directories = glob($prefix.'*', GLOB_ONLYDIR); 40 | array_walk($directories, function(&$directory) use ($prefix) { 41 | $directory = [ str_replace($prefix, '', $directory)]; 42 | }); 43 | return $directories; 44 | } 45 | 46 | /** 47 | * @param string $inputJson 48 | * @param string $expectedJson 49 | * @throws \Exception 50 | * @test 51 | * @dataProvider processJsonDataProvider 52 | */ 53 | public function processJson($inputJson, $expectedJson) 54 | { 55 | $this->assertEquals( 56 | $this->preprocessor->processJson($inputJson, sys_get_temp_dir()), 57 | $expectedJson 58 | ); 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function processJsonDataProvider() 65 | { 66 | return [ 67 | // strip comments 68 | ['Hello World /* Comment */', 'Hello World '], 69 | ['Hello World /* Comment */ Hello World', 'Hello World Hello World'], 70 | ['/* Comment */ Hello World', ' Hello World'], 71 | // support single quotes 72 | ["Hello World /* 'Comment' */ Hello World", 'Hello World Hello World'], 73 | // ignore double quotes 74 | ['Hello World /* "Comment" */', 'Hello World /* "Comment" */'], 75 | ['Hello World /* "Comment */', 'Hello World /* "Comment */'], 76 | // multi-line 77 | ["Hello World /* Multiline\nComment */ Hello World", 'Hello World Hello World'], 78 | // parseRefInDoubleQuotedStrings 79 | ['"Key": "Name", "Value": "magento-{Ref:Environment}-{Ref:Build}-instance"', '"Key": "Name", "Value": {"Fn::Join": ["", ["magento-", {"Ref":"Environment"}, "-", {"Ref":"Build"}, "-instance"]]}'], 80 | // expandPort 81 | ['{"IpProtocol": "tcp", "Port": "80", "CidrIp": "1.2.3.4/32"},', '{"IpProtocol": "tcp", "FromPort": "80", "ToPort": "80", "CidrIp": "1.2.3.4/32"},'], 82 | // replace ref 83 | ["WAIT_CONDITION_HANDLE='{Ref:WaitConditionHandle}'", "WAIT_CONDITION_HANDLE='\", {\"Ref\": \"WaitConditionHandle\"}, \"'"], 84 | ["REGION='{Ref:AWS::Region}'", "REGION='\", {\"Ref\": \"AWS::Region\"}, \"'"], 85 | ['"Aliases": { "Fn::Split": [",", "a,b,c"] }', '"Aliases": ["a", "b", "c"]'], 86 | ['"Aliases": { "Fn::Split": ["+", "a,b,c"] }', '"Aliases": ["a,b,c"]'], 87 | ['"Aliases": { "Fn::Split": ["+", "a+b+c"] }', '"Aliases": ["a", "b", "c"]'], 88 | ]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/StackFormation/StackTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $cfnClientMock; 16 | 17 | public function setUp() 18 | { 19 | $describeStackResourcesResult = new \Aws\Result(json_decode(file_get_contents(FIXTURE_ROOT.'Stack/test-stack1.describeStackResources.json'), true)); 20 | 21 | $this->cfnClientMock = $this->getMock('\Aws\CloudFormation\CloudFormationClient', ['describeStackResources'], [], '', false); 22 | $this->cfnClientMock 23 | ->method('describeStackResources') 24 | ->with(['StackName' => 'test-stack1']) 25 | ->willReturn($describeStackResourcesResult); 26 | 27 | $data = file_get_contents(FIXTURE_ROOT.'Stack/test-stack1.json'); 28 | $data = json_decode($data, true); 29 | $this->stack = new \StackFormation\Stack($data, $this->cfnClientMock); 30 | } 31 | 32 | public function testName() 33 | { 34 | $this->assertEquals('test-stack1', $this->stack->getName()); 35 | } 36 | 37 | public function testDescription() 38 | { 39 | $this->assertEquals('Test Description', $this->stack->getDescription()); 40 | } 41 | 42 | public function testStatus() 43 | { 44 | $this->assertEquals('UPDATE_COMPLETE', $this->stack->getStatus()); 45 | } 46 | 47 | public function testParameter() 48 | { 49 | $this->assertEquals('Bar', $this->stack->getParameter('Foo')); 50 | } 51 | 52 | public function testParameterThatDoesntExist() 53 | { 54 | $this->setExpectedException('Exception', "Parameter 'DoesNotExist' not found in stack 'test-stack1'"); 55 | $this->stack->getParameter('DoesNotExist'); 56 | } 57 | 58 | public function testTag() 59 | { 60 | $this->assertEquals('BarTag', $this->stack->getTag('FooTag')); 61 | } 62 | 63 | public function testTagThatDoesntExist() 64 | { 65 | $this->setExpectedException('Exception', "Tag 'DoesNotExist' not found in stack 'test-stack1'"); 66 | $this->stack->getTag('DoesNotExist'); 67 | } 68 | 69 | public function testOutput() 70 | { 71 | $this->assertEquals('OutputBar', $this->stack->getOutput('OutputFoo')); 72 | } 73 | 74 | public function testOutputThatDoesntExist() 75 | { 76 | $this->setExpectedException('Exception', "Output 'DoesNotExist' not found in stack 'test-stack1'"); 77 | $this->stack->getOutput('DoesNotExist'); 78 | } 79 | 80 | public function testResource() 81 | { 82 | $this->assertEquals('sg-de4494b8', $this->stack->getResource('InstanceSg')); 83 | } 84 | 85 | public function testResourceThatDoesntExist() 86 | { 87 | $this->setExpectedException('Exception', "Resource 'DoesNotExist' not found in stack 'test-stack1'"); 88 | $this->stack->getResource('DoesNotExist'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/StackFormation/ValidateTagsTest.php: -------------------------------------------------------------------------------- 1 | 'Name', 'Value' => 'Bar']], 27 | [['Key' => str_repeat('A', 127), 'Value' => 'Bar']], 28 | [['Key' => 'Name', 'Value' => str_repeat('A', 255)]], 29 | [['Key' => 'Name', 'Value' => base64_encode('&*#,!')]], 30 | ]; 31 | } 32 | 33 | /** 34 | * @param array $tag 35 | * @throws \Exception 36 | * @test 37 | * @dataProvider invalidTagsProvider 38 | */ 39 | public function invalidTag(array $tag) 40 | { 41 | $this->setExpectedException('Exception'); 42 | Validator::validateTags([$tag]); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function invalidTagsProvider() 49 | { 50 | return [ 51 | [['Key' => 'aws:Name', 'Value' => 'Bar']], 52 | [['Name' => 'Foo', 'Value' => 'Bar']], 53 | [['Key' => 'Na,me', 'Value' => 'Bar']], 54 | [['Key' => 'Name', 'Value' => 'Ba,r']], 55 | [['Key' => str_repeat('A', 128), 'Value' => 'Bar']], 56 | [['Key' => 'Name', 'Value' => str_repeat('A', 256)]], 57 | ]; 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function valueIsMissing() 64 | { 65 | $this->setExpectedException('Exception', 'Tag value is missing'); 66 | Validator::validateTags([['Key' => 'foo']]); 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function keyIsMissing() 73 | { 74 | $this->setExpectedException('Exception', 'Tag key is missing'); 75 | Validator::validateTags([['Value' => 'foo']]); 76 | } 77 | 78 | /** 79 | * @test 80 | */ 81 | public function moreThanTenTags() 82 | { 83 | $this->setExpectedException('Exception', 'No more than 10 tags are allowed'); 84 | $tags = []; 85 | for ($i=0; $i<11; $i++) { 86 | $tags[] = ['Key' => "Key$i", 'Value' => "Value$i"]; 87 | } 88 | Validator::validateTags($tags); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.1.yml: -------------------------------------------------------------------------------- 1 | vars: 2 | GlobalFoo: GlobalBar 3 | 4 | blueprints: 5 | 6 | - stackname: 'fixture1' 7 | template: 'dummy.template' 8 | vars: 9 | BlueprintFoo: 'BlueprintBar' 10 | parameters: 11 | ParamFoo: ParamBar 12 | tags: 13 | TagFoo: TagBar 14 | 15 | - stackname: 'fixture2' 16 | template: 'dummy.template' 17 | 18 | - stackname: 'fixture3' 19 | template: 'dummy.template' 20 | tags: 21 | TagFoo: '{env:Foo}' 22 | 23 | - stackname: 'fixture4' 24 | template: 'dummy.template' 25 | Capabilities: 'FOO' 26 | 27 | - stackname: 'fixture5' 28 | template: 'dummy.template' 29 | Capabilities: 'FOO,BAR' 30 | 31 | - stackname: 'fixture6' 32 | template: 'dummy.template' 33 | before: 34 | - 'echo -n "HELLO WORLD" > {env:TESTFILE}' 35 | 36 | - stackname: 'fixture7' 37 | template: 'dummy.template' 38 | stackPolicy: 'dummy_policy.json' 39 | 40 | - stackname: 'fixture8' 41 | template: 'dummy.template' 42 | profile: 'before_scripts_profile' 43 | before: 44 | - 'echo -n "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" > {env:TESTFILE}' -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.conditional_value.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'conditional_value' 4 | template: 'dummy.template' 5 | parameters: 6 | CondValue: 7 | '{env:Foo}==Val1': a 8 | '{env:Foo}==Val2': b 9 | 'default': c 10 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.conditional_vars.yml: -------------------------------------------------------------------------------- 1 | vars: 2 | 3 | FooVar: 4 | '{env:Foo}==Val1': a 5 | '{env:Foo}==Val2': b 6 | 'default': c 7 | 8 | blueprints: 9 | 10 | - stackname: 'fixture_var_conditional_global' 11 | template: 'dummy.template' 12 | parameters: 13 | Parameter1: '{var:FooVar}' 14 | 15 | - stackname: 'fixture_var_conditional_local' 16 | template: 'dummy.template' 17 | vars: 18 | LocalFooVar: 19 | '{env:Foo}==Val1': a 20 | '{env:Foo}==Val2': b 21 | 'default': c 22 | parameters: 23 | Parameter1: '{var:LocalFooVar}' -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.duplicate_stackname.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'a' 4 | template: 'dummy.template' 5 | - stackname: 'b' 6 | template: 'dummy.template' 7 | - stackname: 'a' 8 | template: 'dummy.template' 9 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.duplicateglobalvar.yml: -------------------------------------------------------------------------------- 1 | vars: 2 | GlobalFoo: GlobalBar 3 | GlobalFoo: GlobalBar2 4 | 5 | blueprints: -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.invalid_attribute.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'invalid_attribute' 4 | template: 'dummy.template' 5 | # this is 'parameters' not 'parameter' 6 | parameter: 7 | Foo: Bar 8 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.reference.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'reference-fixture-{env:FOO1}' 4 | template: 'dummy.template' 5 | vars: 6 | MyVar: '{env:FOO2}' 7 | parameters: 8 | MyParam: '{env:FOO3}' 9 | MyParam2: 'prefix{var:MyVar}' 10 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.select_profile.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'fixture_selectprofile' 4 | template: 'dummy.template' 5 | profile: 'myprofile' 6 | 7 | - stackname: 'fixture_selectprofile_conditional' 8 | template: 'dummy.template' 9 | profile: 10 | '{env:Foo}==Val1': a 11 | '{env:Foo}==Val2': b 12 | 'default': c 13 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.switch_profile.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'switch_profile' 4 | template: 'dummy.template' 5 | profile: 'myprofile1' 6 | parameters: 7 | Foo1: 'Bar1' 8 | Foo2: '[profile:myprofile2:{output:remoteStack:fooField}]' 9 | Foo3: '{output:localStack:fooField}' 10 | 11 | - stackname: 'switch_profile_complex' 12 | template: 'dummy.template' 13 | profile: 'myprofile1' 14 | parameters: 15 | VarnishAmi: '[profile:t.deploy-ecom:{output:ecom-{env:ACCOUNT}-all-ami-types-{env:BASE_TYPE_VERSION}-stack:VarnishAmi}]' -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.template_missing.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'a' 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/Config/blueprint.templatefile_missing.yml: -------------------------------------------------------------------------------- 1 | blueprints: 2 | 3 | - stackname: 'a' 4 | template: 'doesnotexist.template' 5 | 6 | -------------------------------------------------------------------------------- /tests/fixtures/Config/dummy.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "", 4 | "Resources": { 5 | "MyResource": { 6 | "Type": "AWS::CloudFormation::WaitConditionHandle" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/fixtures/Config/dummy_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Statement" : [ 3 | { 4 | "Effect" : "Allow", 5 | "Action" : "Update:Delete", 6 | "Principal": "*", 7 | "Resource" : "*" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromParentDirectory/blueprint/expected.template: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "LaunchConfiguration": { 4 | "Properties": { 5 | "UserData": {"Fn::Base64": {"Fn::Join": ["", [ 6 | "#!\/usr\/bin\/env bash\n", 7 | "echo \"Hello World\"\n" 8 | ]]}} 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromParentDirectory/blueprint/input.template: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "LaunchConfiguration": { 4 | "Properties": { 5 | "UserData": {"Fn::Base64": {"Fn::FileContent": "../userdata.sh"}} 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromParentDirectory/userdata.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hello World" -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromSameDirectory/blueprint/expected.template: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "LaunchConfiguration": { 4 | "Properties": { 5 | "UserData": {"Fn::Base64": {"Fn::Join": ["", [ 6 | "#!\/usr\/bin\/env bash\n", 7 | "echo \"Hello World\"\n" 8 | ]]}} 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromSameDirectory/blueprint/input.template: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "LaunchConfiguration": { 4 | "Properties": { 5 | "UserData": {"Fn::Base64": {"Fn::FileContent": "userdata.sh"}} 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromSameDirectory/blueprint/userdata.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hello World" -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromSubDirectory/blueprint/expected.template: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "LaunchConfiguration": { 4 | "Properties": { 5 | "UserData": {"Fn::Base64": {"Fn::Join": ["", [ 6 | "#!\/usr\/bin\/env bash\n", 7 | "echo \"Hello World\"\n" 8 | ]]}} 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromSubDirectory/blueprint/input.template: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "LaunchConfiguration": { 4 | "Properties": { 5 | "UserData": {"Fn::Base64": {"Fn::FileContent": "userdata/userdata.sh"}} 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/fixtures/Preprocessor/injectFileContentFnFileContentFromSubDirectory/blueprint/userdata/userdata.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hello World" -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_basic/profiles.yml: -------------------------------------------------------------------------------- 1 | profiles: 2 | test1: 3 | region: 'us-east-1' 4 | access_key: 'TESTACCESSKEY1' 5 | secret_key: 'TESTSECRETKEY1' 6 | test2: 7 | region: 'us-east-1' 8 | access_key: 'TESTACCESSKEY2' 9 | secret_key: 'TESTSECRETKEY2' 10 | test3: 11 | region: 'us-east-1' 12 | access_key: 'TESTACCESSKEY3' 13 | secret_key: 'TESTSECRETKEY3' 14 | assume_role: 'arn:aws:iam::0123456789012:role/p-magento-iam-DeployRole' 15 | -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_before_scripts/profiles.yml: -------------------------------------------------------------------------------- 1 | profiles: 2 | before_scripts_profile: 3 | region: 'us-east-1' 4 | access_key: 'TESTACCESSKEY1' 5 | secret_key: 'TESTSECRETKEY1' -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_encrpyted/profiles.yml.encrypted: -------------------------------------------------------------------------------- 1 | MHxuL0t6ZEVJV3NHdlhhZEFhVTBMM3lDZmQ2QWJuaEtqZHBUVEVJTksxQjZzL2J2TEhFMXJ2SnZyUytmTkh0ZXJoRjVEQzI2TmtQUmZDcURkQXhrbk9oaXBIVzdoTnZEcm13TmFBUHlLUTZDL1dlWlZKWlMxVHgrSmpZUmlIT2JVckZsR2xzNnl4b3F0R2VCQlAxY0dwSUxlVmJ3QUczSDVxek5ZNlBCSHRTbWJTaXBBcjJXYldoNHNOSHpEWkk2WXQyRWhYdWJ5aTBFekhMM0RsR3daUzRZd3h2MmJhQXZHbTNob1g2Mk5xaGMvTEV4RjlZUURIU3BEMUJrbWlJcHhDS3oydEV0YXE0dmc2VjlFVkMrQjBYMDd2elVYa0RZZFVuMmpkb0Z1Yzg3My8xbFZkTDFjSHp0OXFvTE1VNFJ3NTR5QVRtOW01TVBnOVh4Uk16OWFKSnVHQzJGSjhnK3hDeWFTQUFSMkEzMGpaZzE1aEhwYzN5NnNWbnJ0MWJoZTZwRjJFV203dzJnMkZva0hUMDZBR1BWTkQxZEVLanhnaTRHQnNxa2pHWDBlNm5HUFZFb3ptc0VqTHAzMlhsWFNFb2tSMmlRSjN3bkZQa0QvbzVYVGRwOFFLTFdxdlNtcEc2Rm4wRDFUSlVFekVYNU53ZDdpWEs0V3RRT3dad3Y0a3Q0cERSTEVLN3U5ZjJtR3BpMkI3V01GQVdvazlGWFU9fDl3TmJPdWkwY01HQm50TmRGZ0Y1NVE9PQ== -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_encrpyted_mix/profiles.personal.yml: -------------------------------------------------------------------------------- 1 | profiles: 2 | test1-personal: 3 | region: 'us-east-1' 4 | access_key: 'TESTPERSONALACCESSKEY1' 5 | secret_key: 'TESTPERSONALSECRETKEY1' 6 | test2-personal: 7 | region: 'us-east-1' 8 | access_key: 'TESTPERSONALACCESSKEY2' 9 | secret_key: 'TESTPERSONALSECRETKEY2' -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_encrpyted_mix/profiles.yml.encrypted: -------------------------------------------------------------------------------- 1 | MHxuL0t6ZEVJV3NHdlhhZEFhVTBMM3lDZmQ2QWJuaEtqZHBUVEVJTksxQjZzL2J2TEhFMXJ2SnZyUytmTkh0ZXJoRjVEQzI2TmtQUmZDcURkQXhrbk9oaXBIVzdoTnZEcm13TmFBUHlLUTZDL1dlWlZKWlMxVHgrSmpZUmlIT2JVckZsR2xzNnl4b3F0R2VCQlAxY0dwSUxlVmJ3QUczSDVxek5ZNlBCSHRTbWJTaXBBcjJXYldoNHNOSHpEWkk2WXQyRWhYdWJ5aTBFekhMM0RsR3daUzRZd3h2MmJhQXZHbTNob1g2Mk5xaGMvTEV4RjlZUURIU3BEMUJrbWlJcHhDS3oydEV0YXE0dmc2VjlFVkMrQjBYMDd2elVYa0RZZFVuMmpkb0Z1Yzg3My8xbFZkTDFjSHp0OXFvTE1VNFJ3NTR5QVRtOW01TVBnOVh4Uk16OWFKSnVHQzJGSjhnK3hDeWFTQUFSMkEzMGpaZzE1aEhwYzN5NnNWbnJ0MWJoZTZwRjJFV203dzJnMkZva0hUMDZBR1BWTkQxZEVLanhnaTRHQnNxa2pHWDBlNm5HUFZFb3ptc0VqTHAzMlhsWFNFb2tSMmlRSjN3bkZQa0QvbzVYVGRwOFFLTFdxdlNtcEc2Rm4wRDFUSlVFekVYNU53ZDdpWEs0V3RRT3dad3Y0a3Q0cERSTEVLN3U5ZjJtR3BpMkI3V01GQVdvazlGWFU9fDl3TmJPdWkwY01HQm50TmRGZ0Y1NVE9PQ== -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_merge_profiles/profiles.personal.yml: -------------------------------------------------------------------------------- 1 | profiles: 2 | test1-personal: 3 | region: 'us-east-1' 4 | access_key: 'TESTPERSONALACCESSKEY1' 5 | secret_key: 'TESTPERSONALSECRETKEY1' 6 | test2-personal: 7 | region: 'us-east-1' 8 | access_key: 'TESTPERSONALACCESSKEY2' 9 | secret_key: 'TESTPERSONALSECRETKEY2' -------------------------------------------------------------------------------- /tests/fixtures/ProfileManager/fixture_merge_profiles/profiles.yml: -------------------------------------------------------------------------------- 1 | profiles: 2 | test1: 3 | region: 'us-east-1' 4 | access_key: 'TESTACCESSKEY1' 5 | secret_key: 'TESTSECRETKEY1' 6 | test2: 7 | region: 'us-east-1' 8 | access_key: 'TESTACCESSKEY2' 9 | secret_key: 'TESTSECRETKEY2' 10 | test3: 11 | region: 'us-east-1' 12 | access_key: 'TESTACCESSKEY3' 13 | secret_key: 'TESTSECRETKEY3' 14 | assume_role: 'arn:aws:iam::0123456789012:role/p-magento-iam-DeployRole' 15 | -------------------------------------------------------------------------------- /tests/fixtures/RunBeforeScript/foo.txt: -------------------------------------------------------------------------------- 1 | HELLO WORLD FROM FILE -------------------------------------------------------------------------------- /tests/fixtures/Stack/test-stack1.describeStackResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "StackResources": [ 3 | { 4 | "StackName": "test-stack1", 5 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 6 | "LogicalResourceId": "AutoScalingGroup", 7 | "PhysicalResourceId": "test-stack1-AutoScalingGroup-ELHRZ3CQ5KTD", 8 | "ResourceType": "AWS::AutoScaling::AutoScalingGroup", 9 | "Timestamp": "2016-06-19T06:26:50+00:00", 10 | "ResourceStatus": "UPDATE_COMPLETE" 11 | }, { 12 | "StackName": "test-stack1", 13 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 14 | "LogicalResourceId": "Dns", 15 | "PhysicalResourceId": "jenkins.example.net", 16 | "ResourceType": "AWS::Route53::RecordSet", 17 | "Timestamp": "2016-06-09T04:25:13+00:00", 18 | "ResourceStatus": "CREATE_COMPLETE" 19 | }, { 20 | "StackName": "test-stack1", 21 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 22 | "LogicalResourceId": "InstanceSg", 23 | "PhysicalResourceId": "sg-de4494b8", 24 | "ResourceType": "AWS::EC2::SecurityGroup", 25 | "Timestamp": "2016-06-11T04:43:44+00:00", 26 | "ResourceStatus": "UPDATE_COMPLETE" 27 | }, { 28 | "StackName": "test-stack1", 29 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 30 | "LogicalResourceId": "LaunchConfiguration", 31 | "PhysicalResourceId": "test-stack1-LaunchConfiguration-22VBM2XN6V32", 32 | "ResourceType": "AWS::AutoScaling::LaunchConfiguration", 33 | "Timestamp": "2016-06-19T06:23:45+00:00", 34 | "ResourceStatus": "UPDATE_COMPLETE" 35 | }, { 36 | "StackName": "test-stack1", 37 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 38 | "LogicalResourceId": "LoadBalancer", 39 | "PhysicalResourceId": "demo-jenk-LoadBala-11B79X246A9EJ", 40 | "ResourceType": "AWS::ElasticLoadBalancing::LoadBalancer", 41 | "Timestamp": "2016-06-11T04:43:29+00:00", 42 | "ResourceStatus": "UPDATE_COMPLETE" 43 | }, { 44 | "StackName": "test-stack1", 45 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 46 | "LogicalResourceId": "LoadBalancerSg", 47 | "PhysicalResourceId": "sg-e3449485", 48 | "ResourceType": "AWS::EC2::SecurityGroup", 49 | "Timestamp": "2016-06-11T04:43:25+00:00", 50 | "ResourceStatus": "UPDATE_COMPLETE" 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /tests/fixtures/Stack/test-stack1.json: -------------------------------------------------------------------------------- 1 | { 2 | "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack\/test-stack1\/037063f1-2dfa-11e6-a4dd-50a686be73f2", 3 | "StackName": "test-stack1", 4 | "Description": "Test Description", 5 | "Parameters": [ 6 | {"ParameterKey": "Foo", "ParameterValue": "Bar"}, 7 | {"ParameterKey": "Foo2","ParameterValue": "Bar2"} 8 | ], 9 | "CreationTime": "2016-06-09T04:24:10+00:00", 10 | "LastUpdatedTime": "2016-06-19T06:23:36+00:00", 11 | "StackStatus": "UPDATE_COMPLETE", 12 | "DisableRollback": true, 13 | "NotificationARNs": [], 14 | "Outputs": [ 15 | {"OutputKey": "OutputFoo", "OutputValue": "OutputBar"}, 16 | {"OutputKey": "OutputFoo2","OutputValue": "OutputBar2"} 17 | ], 18 | "Tags": [ 19 | {"Key": "FooTag", "Value": "BarTag"}, 20 | {"Key": "stackformation:blueprint", "Value": "TmFtZT1kZW1vLWplbmtpbnM="} 21 | ] 22 | } -------------------------------------------------------------------------------- /tests/fixtures/foo.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/StackFormation/5332dbbe54653e50d610cbaf75fb865c68aa2f1e/tests/fixtures/foo.pem -------------------------------------------------------------------------------- /tests/fixtures/resolve_md5.txt: -------------------------------------------------------------------------------- 1 | ASDS SFsd df dg zg aerg g ag sdfgfdgstesrgrg -------------------------------------------------------------------------------- /tests/phpunit.bootstrap.php: -------------------------------------------------------------------------------- 1 | setUseIncludePath(true); 11 | define('FIXTURE_ROOT', __DIR__ . '/fixtures/'); --------------------------------------------------------------------------------