├── .gitignore ├── ci ├── validate_cf_spec.yml ├── s3_cloudformation.cf ├── codebuild_capsule.cf └── codebuild_project.cf ├── .prettierrc.json ├── .eslintrc.json ├── policies ├── dev_policy.json ├── admin_policy.json └── iam-admin-group.cf.yaml ├── SECURITY.md ├── config ├── capsule_init_questions.json └── capsule_init_ci_questions.json ├── CHANGELOG.md ├── docs ├── contribute.md ├── templates.md ├── CODE_OF_CONDUCT.md └── advanced_use.md ├── CONTRIBUTING.md ├── ci.sh ├── LICENSE ├── templates ├── child_templates │ ├── template.cfoai.yaml │ ├── template.route53.yaml │ ├── template.certificate.yaml │ ├── template.s3.yaml │ ├── template.lambda.yaml │ └── template.cloudfront.yaml └── template.yaml ├── .github └── workflows │ └── codeql-analysis.yml ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── bin └── capsule.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore temporary files 2 | 3 | # VIM # 4 | *.swp 5 | *.swo 6 | /node_modules 7 | -------------------------------------------------------------------------------- /ci/validate_cf_spec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 0.2 3 | 4 | phases: 5 | build: 6 | commands: 7 | - ./ci.sh 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017 4 | }, 5 | "env": { 6 | "browser": false, 7 | "node": true, 8 | "es2017": true 9 | }, 10 | "extends": ["eslint:recommended", "plugin:prettier/recommended"] 11 | } 12 | -------------------------------------------------------------------------------- /policies/dev_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "OASDevPolicy1", 6 | "Effect": "Allow", 7 | "Action": [], 8 | "Resource": [] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Capsule security patches are supported for the following versions: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.1.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you discover a vulnerability, please add a ticket to the project and add the label `security` 14 | -------------------------------------------------------------------------------- /ci/s3_cloudformation.cf: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "CloudFormation Template Store - S3 bucket for storing the CF templates" 3 | 4 | Parameters: 5 | ProjectName: 6 | Type: String 7 | Description: "Name of the project. TODO: Add validation regex" 8 | Resources: 9 | S3Bucket: 10 | Type: "AWS::S3::Bucket" 11 | Properties: 12 | BucketName: !Sub "cf-${ProjectName}-capsule-ci" 13 | -------------------------------------------------------------------------------- /config/capsule_init_questions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "input", 4 | "name": "ProjectName", 5 | "message": "Enter your project name (Alphanumeric and underscore characters only)" 6 | }, 7 | { 8 | "type": "input", 9 | "name": "Domain", 10 | "message": "Enter your Domain name" 11 | }, 12 | { 13 | "type": "input", 14 | "name": "SubDomain", 15 | "message": "Enter your Sub-domain (ex: www)" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | Templates for: 10 | 11 | * Certificates 12 | * CloudFront 13 | * CFOAI 14 | * Route 53 15 | * S3 16 | 17 | Command line support for: 18 | 19 | * Initialize 20 | * Apply 21 | 22 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # Contributing to Project 2 | 3 | ### Code of Conduct 4 | 5 | Modus has adopted a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect project participants to adhere to. 6 | 7 | ### Submitting a Pull Request 8 | 9 | If you are a first time contributor, you can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 10 | 11 | ### License 12 | 13 | By contributing, you agree that your contributions will belicensed under it's [license](../LICENSE) 14 | -------------------------------------------------------------------------------- /config/capsule_init_ci_questions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "input", 4 | "name": "RepositoryURL", 5 | "message": "Enter your source code repository (https url)" 6 | }, 7 | { 8 | "type": "input", 9 | "name": "WebsiteCode", 10 | "message": "Enter the folder where your build source code resides" 11 | }, 12 | { 13 | "type": "input", 14 | "name": "InstallCommands", 15 | "message": "Enter the install commands" 16 | }, 17 | { 18 | "type": "input", 19 | "name": "BuildCommands", 20 | "message": "Enter any build commands" 21 | }, 22 | { 23 | "type": "input", 24 | "name": "PostBuildCommands", 25 | "message": "Enter any post-build commands" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Project 2 | 3 | ### Code of Conduct 4 | 5 | Modus has adopted a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect project participants to adhere to. 6 | 7 | ### Submitting a Pull Request 8 | 9 | If you are a first time contributor, you can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 10 | 11 | ### Development 12 | 13 | [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) are integrated with a pre-commit hook. We recommend using an [editor integration](https://eslint.org/docs/user-guide/integrations#editors) to be able to format/lint the code automatically before making a commit. 14 | 15 | ### License 16 | 17 | By contributing, you agree that your contributions will belicensed under it's [license](./LICENSE) -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # http://redsymbol.net/articles/unofficial-bash-strict-mode/ 4 | set -euo pipefail 5 | IFS=$'\n\t' 6 | 7 | # Credit to Stack Overflow questioner Jiarro and answerer Dave Dopson 8 | # http://stackoverflow.com/questions/59895/can-a-bash-script-tell-what-directory-its-stored-in 9 | # http://stackoverflow.com/a/246128/424301 10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 11 | 12 | # Returns all variants of CF templates 13 | function search_cf_files () { 14 | find "${DIR}"/* \ 15 | -type f \ 16 | \( \ 17 | -iname '*.cf' \ 18 | -o \ 19 | -iname 'template.*.yaml' \ 20 | -o \ 21 | -iname '*.cf.yaml' \ 22 | -o \ 23 | -iname 'template.*.json' \ 24 | -o \ 25 | -iname '*.cf.json' \ 26 | \) 27 | } 28 | 29 | # Search for all CF files within the repo 30 | CF_TEMPLATES=$(search_cf_files) 31 | 32 | for i in $CF_TEMPLATES ; do 33 | echo "Testing: $i"; 34 | aws cloudformation validate-template \ 35 | --template-body "file://$i" \ 36 | > /dev/null ; 37 | done 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011-present, Modus Create, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/child_templates/template.cfoai.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Website CloudFormation stack - CloudFront origin access identity" 3 | 4 | Parameters: 5 | Domain: 6 | Type: String 7 | Description: "The DNS name of an existing Amazon Route53 hosted zone, e.g. moduscreate.com" 8 | AllowedPattern: "(?!-)[a-z0-9-]{1,63}(?", 27 | "contributors": [ 28 | "Facundo Victor ", 29 | "Andy Dennis ", 30 | "Lucas Still ", 31 | "Sergio Bruder ", 32 | "Mike Schwartz ", 33 | "Drew Falkman ", 34 | "Mike LaFortune ", 35 | "Grgur Grisogono ", 36 | "Ivan Kovic " 37 | ], 38 | "main": "bin/capsule.js", 39 | "bin": { 40 | "capsule": "bin/capsule.js" 41 | }, 42 | "scripts": { 43 | "start": "bin/capsule.js", 44 | "test": "echo \"Error: no test specified\" && exit 1", 45 | "lint": "eslint 'bin/*'" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/ModusCreateOrg/capsule" 50 | }, 51 | "keywords": [ 52 | "capsule", 53 | "js", 54 | "javascript", 55 | "aws", 56 | "cli", 57 | "hosting", 58 | "static", 59 | "cloudfront", 60 | "cdn", 61 | "cloudformation" 62 | ], 63 | "bugs": { 64 | "url": "https://github.com/ModusCreateOrg/capsule/issues" 65 | }, 66 | "dependencies": { 67 | "aws-sdk": "^2.287.0", 68 | "chalk": "^2.4.1", 69 | "cli-spinner": "^0.2.8", 70 | "commander": "^2.17.0", 71 | "inquirer": "^6.2.0", 72 | "pkginfo": "0.4.1" 73 | }, 74 | "devDependencies": { 75 | "eslint": "6.6.0", 76 | "eslint-config-prettier": "6.5.0", 77 | "eslint-plugin-prettier": "3.1.1", 78 | "husky": "3.0.9", 79 | "lint-staged": "9.4.2", 80 | "prettier": "1.18.2" 81 | }, 82 | "husky": { 83 | "hooks": { 84 | "pre-commit": "lint-staged" 85 | } 86 | }, 87 | "lint-staged": { 88 | "*.{js}": [ 89 | "eslint --fix", 90 | "git add" 91 | ], 92 | "*.{json}": [ 93 | "prettier --write", 94 | "git add" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ci/codebuild_capsule.cf: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: "Capsule CloudFormation stack - CodeBuild Project" 4 | 5 | Parameters: 6 | CodeBuildProjectCodeName: 7 | Type: String 8 | Description: "CodeBuild Capsule Project codename" 9 | RepositoryURL: 10 | Type: String 11 | Description: "HTTPS URL for the Git repository" 12 | AllowedPattern: "^https:\\/\\/(.*)\\/(.*)\\/(.)*" 13 | ConstraintDescription: "This should be a valid repository HTTPS URL" 14 | RepositoryType: 15 | Type: String 16 | Description: "CODECOMMIT|CODEPIPELINE|GITHUB|GITHUB_ENTERPRISE|BITBUCKET|S3" 17 | AllowedValues: 18 | - CODECOMMIT 19 | - CODEPIPELINE 20 | - GITHUB 21 | - GITHUB_ENTERPRISE 22 | - BITBUCKET 23 | - S3 24 | Default: GITHUB 25 | EnvironmentImage: 26 | Type: String 27 | Description: "Image to use for running a container where the build will execute" 28 | AllowedPattern: "^(.*)\\/(.*)\\/(.)*:(.)*" 29 | ConstraintDescription: "Image reference (/:)" 30 | Default: aws/codebuild/ubuntu-base:14.04 31 | ComputeType: 32 | Type: String 33 | Description: "Small (3 GB memory, 2 vCPU) | Medium (7 GB memory, 4 vCPU) | large (15 GB memory, 8 vCPU)" 34 | AllowedValues: 35 | - BUILD_GENERAL1_SMALL 36 | - BUILD_GENERAL1_MEDIUM 37 | - BUILD_GENERAL1_LARGE 38 | Default: BUILD_GENERAL1_SMALL 39 | BuildSpecLocation: 40 | Type: String 41 | Description: "Location of the build spec to use (Defaults to the repository root)" 42 | Default: buildspec.yml 43 | Resources: 44 | CodeBuildProject: 45 | Type: AWS::CodeBuild::Project 46 | DependsOn: CodeBuildRole 47 | Properties: 48 | Name: !Sub ${AWS::StackName}-${CodeBuildProjectCodeName} 49 | Artifacts: 50 | Type: no_artifacts 51 | Environment: 52 | Image: !Ref EnvironmentImage 53 | Type: LINUX_CONTAINER 54 | ComputeType: !Ref ComputeType 55 | ServiceRole: !Ref CodeBuildRole 56 | Triggers: 57 | Webhook: yes 58 | Source: 59 | Type: !Ref RepositoryType 60 | Location: !Ref RepositoryURL 61 | Auth: 62 | Type: OAUTH 63 | BuildSpec: !Ref BuildSpecLocation 64 | CodeBuildRole: 65 | Type: AWS::IAM::Role 66 | Properties: 67 | AssumeRolePolicyDocument: 68 | Version: "2012-10-17" 69 | Statement: 70 | - Effect: Allow 71 | Principal: 72 | Service: codebuild.amazonaws.com 73 | Action: sts:AssumeRole 74 | RolePolicies: 75 | Type: "AWS::IAM::Policy" 76 | Properties: 77 | PolicyName: "root" 78 | PolicyDocument: 79 | Version: "2012-10-17" 80 | Statement: 81 | - Effect: Allow 82 | Action: 83 | - "logs:CreateLogGroup" 84 | - "logs:CreateLogStream" 85 | - "logs:PutLogEvents" 86 | - "cloudformation:ValidateTemplate" 87 | Resource: "*" 88 | Roles: 89 | - Ref: "CodeBuildRole" 90 | 91 | Outputs: 92 | CodeBuildURL: 93 | Description: CodeBuild URL 94 | Value: !Sub https://console.aws.amazon.com/codebuild/home?region=${AWS::Region}#/projects/${CodeBuildProject}/view 95 | -------------------------------------------------------------------------------- /templates/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Website CloudFormation stack" 3 | 4 | Parameters: 5 | Domain: 6 | Type: String 7 | Description: "The DNS name of an existing Amazon Route53 hosted zone, e.g. moduscreate.com" 8 | AllowedPattern: "(?!-)[a-z0-9-]{1,63}(? 20 | Lambda function to process _redirects file and populate bucket with redirect files 21 | Code: 22 | ZipFile: !Sub | 23 | var AWS = require('aws-sdk'); 24 | var s3 = new AWS.S3(); 25 | exports.handler = (event, context, callback) => { 26 | const re = /[^\.](\S+)\b/g; 27 | const params = { 28 | Bucket: event.Records[0].s3.bucket.name, 29 | Key: '_redirects' 30 | } 31 | let redirectsList; 32 | let arr = []; 33 | let m ='' 34 | s3.getObject(params).promise() 35 | .then(data => { 36 | redirectsList = data.Body.toString(); 37 | let entries = redirectsList.split("\n"); 38 | entries = entries.filter(Boolean) 39 | for (var i in entries) { 40 | while ((m=re.exec(entries[i])) !== null) { 41 | arr.push(m[1]); 42 | } 43 | let data = '' 44 | data += '' 45 | data += '' 46 | data += ' { 69 | putParams.Key = indexFile 70 | s3.putObject(putParams).promise() 71 | .then(data => { 72 | callback(null, data); 73 | }) 74 | .catch(err => { 75 | console.log('failure:', err); 76 | }); 77 | callback(null, data); 78 | }) 79 | .catch(err => { 80 | console.log('failure:', err); 81 | }); 82 | arr = [] 83 | } 84 | }) 85 | }; 86 | Handler: index.handler 87 | MemorySize: 128 88 | Role: !Sub ${ProcessRedirectsLambdaFunctionRole.Arn} 89 | Runtime: nodejs8.10 90 | Timeout: 25 91 | 92 | ProcessRedirectsLambdaFunctionRole: 93 | Type: AWS::IAM::Role 94 | Properties: 95 | AssumeRolePolicyDocument: 96 | Version: 2012-10-17 97 | Statement: 98 | - Effect: Allow 99 | Principal: 100 | Service: 101 | - lambda.amazonaws.com 102 | Action: 103 | - sts:AssumeRole 104 | 105 | 106 | ProcessRedirectsLambdaRolePolicies: 107 | Type: "AWS::IAM::Policy" 108 | Properties: 109 | PolicyName: "root" 110 | PolicyDocument: 111 | Version: "2012-10-17" 112 | Statement: 113 | - 114 | Effect: Allow 115 | Action: "s3:*" 116 | Resource: 117 | - !Sub "arn:aws:s3:::${ProjectS3Bucket}/*" 118 | - 119 | Effect: Allow 120 | Action: "lambda:*" 121 | Resource: 122 | - "arn:aws:lambda:*:*:function:*" 123 | Roles: 124 | - Ref: "ProcessRedirectsLambdaFunctionRole" 125 | 126 | ProcessRedirectsLambdaFunctionVersion: 127 | Type: AWS::Lambda::Version 128 | Properties: 129 | FunctionName: !Ref ProcessRedirectsLambdaFunction 130 | Description: !Sub "URL rewriting" 131 | 132 | Outputs: 133 | ProcessRedirectsLambdaFunctionOutput: 134 | Value: !Ref "ProcessRedirectsLambdaFunctionVersion" 135 | Description: "The Lambda function to use on the S3 bucket" 136 | 137 | 138 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Open Source Code Of Conduct 2 | 3 | This code of conduct outlines our expectations for participants within the Modus Create community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. 4 | 5 | Our open source community strives to: 6 | 7 | #### Be friendly and patient. 8 | 9 | #### Be welcoming 10 | 11 | We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 12 | 13 | #### Be considerate 14 | 15 | Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. 16 | 17 | #### Be respectful 18 | 19 | Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. 20 | Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. This includes, but is not limited to: 21 | 22 | - Violent threats or language directed against another person. 23 | - Discriminatory jokes and language. 24 | - Posting sexually explicit or violent material. 25 | - Posting (or threatening to post) other people’s personally identifying information (“doxing”). 26 | - Personal insults, especially those using racist or sexist terms. 27 | - Unwelcome sexual attention. 28 | - Advocating for, or encouraging, any of the above behavior. 29 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 30 | 31 | #### When we disagree, try to understand why 32 | 33 | Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. 34 | 35 | #### Remember that we’re different 36 | 37 | The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 38 | 39 | This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. 40 | 41 | ## Diversity Statement 42 | 43 | We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. 44 | 45 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. 46 | 47 | ## Reporting Issues 48 | 49 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via `opensource@moduscreate.com`. All reports will be handled with discretion. In your report please include: 50 | 51 | - Your contact information. 52 | - Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. 53 | - Any additional information that may be helpful. 54 | 55 | After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. 56 | 57 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. 58 | 59 | This Code Of Conduct follows the [template](http://todogroup.org/opencodeofconduct/) established by the [TODO Group](http://todogroup.org/). -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Open Source Code Of Conduct 2 | 3 | This code of conduct outlines our expectations for participants within the Modus Create community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. 4 | 5 | Our open source community strives to: 6 | 7 | #### Be friendly and patient. 8 | 9 | #### Be welcoming 10 | 11 | We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 12 | 13 | #### Be considerate 14 | 15 | Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. 16 | 17 | #### Be respectful 18 | 19 | Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. 20 | Be careful in the words that you choose: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren’t acceptable. This includes, but is not limited to: 21 | 22 | * Violent threats or language directed against another person. 23 | * Discriminatory jokes and language. 24 | * Posting sexually explicit or violent material. 25 | * Posting (or threatening to post) other people’s personally identifying information (“doxing”). 26 | * Personal insults, especially those using racist or sexist terms. 27 | * Unwelcome sexual attention. 28 | * Advocating for, or encouraging, any of the above behavior. 29 | * Repeated harassment of others. In general, if someone asks you to stop, then stop. 30 | 31 | #### When we disagree, try to understand why 32 | 33 | Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. 34 | 35 | #### Remember that we’re different 36 | 37 | The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 38 | 39 | This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. 40 | 41 | ## Diversity Statement 42 | 43 | We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. 44 | 45 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. 46 | 47 | ## Reporting Issues 48 | 49 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via `opensource@moduscreate.com`. All reports will be handled with discretion. In your report please include: 50 | 51 | * Your contact information. 52 | * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. 53 | * Any additional information that may be helpful. 54 | 55 | After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. 56 | 57 | Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. 58 | 59 | This Code Of Conduct follows the [template](http://todogroup.org/opencodeofconduct/) established by the [TODO Group](http://todogroup.org/). 60 | -------------------------------------------------------------------------------- /docs/advanced_use.md: -------------------------------------------------------------------------------- 1 | # Advanced use 2 | 3 | The following document provides a guide to the advanced features 4 | provided by capsule. 5 | 6 | 7 | ## Your website configuration 8 | 9 | Capsule supports a number of command line parameters to allow you to configure your web site. 10 | 11 | In addition to this, you can use a JSON configuration file as generated by the `init` command, containing these values. 12 | 13 | When loading configuration options, the JSON config is loaded first, if specified. Following this any command line 14 | parameters are then loaded. Command line parameters will override any parameters specified in the JSON file. 15 | 16 | An example can be seen here: 17 | 18 | ``` 19 | ./bin/capsule.js create --project-name "exampledotcom" --dom example.com --subdom app --url https://github.com/ExampleCom/exampledotcom --config ~/.aws/config.json --site_config='{"WebsiteCode":"./build"}' --site_config_file=./my_config/capsule.json ci 20 | ``` 21 | 22 | In this example the value of the `--site_config` flag will overwrite the the key value pair specified in the `--site_config_file` JSON file. 23 | Thus if `WebsiteCode` is defined in `capsule.json` it's value will be overridden with `./build`. 24 | 25 | 26 | You can always check what command line parameters are available by running the `capsule -h` command. 27 | 28 | 29 | ## Advanced CLI use 30 | 31 | The capsule cli is a NodeJS cli app with the intention to simplify the generation of the hosting infrastructure and ci infrastructure. For nested stacks, it requires the generation of a base s3 bucket. This can be generated in the following way: 32 | 33 | ```sh 34 | $ ./capsule create --project-name s3 35 | ``` 36 | 37 | Once this bucket is in place, the web infrastructure can then be created using the `web` command line option. 38 | 39 | For example: 40 | 41 | ```sh 42 | $ ./capsule create --project-name web 43 | ``` 44 | 45 | Having setup the infrastructure, you can then add in the CI pipeline as follows: 46 | 47 | 48 | ```sh 49 | $ ./capsule create --project-name ci 50 | ``` 51 | 52 | 53 | Not all the examples above, have assumed a `capsule.js` file is present, and that the ProjectName value has 54 | been overwritten by the `--project-name` parameter. 55 | 56 | In addition to the `create` command, Capsule also supports an `update` and `delete` command. 57 | 58 | For getting the complete list of options and how they are used, just enter `--help`: 59 | 60 | ```sh 61 | $ ./bin/capsule.js --help 62 | 63 | Usage: capsule [options] [command] 64 | 65 | Options: 66 | 67 | -V, --version output the version number 68 | -v, --verbose verbose output 69 | -h, --help output usage information 70 | 71 | Commands: 72 | 73 | init Define the project parameters 74 | deploy Builds out the web hosting infrastructure in one go 75 | remove [options] Removes the whole of the web hosting infrastructure, including files in S3 buckets 76 | create [options] Initializes the s3 bucket required to store nested stack templates takes: s3, ci or web 77 | update [options] Updates the templates into the s3 bucket and runs the nested stack takes: s3, ci or web 78 | delete [options] Deletes the s3 bucket contents takes: s3, ci or web 79 | 80 | ``` 81 | 82 | 83 | ### Advanced CI use 84 | 85 | The `ci` tool can be executed from the command line in order to setup the 86 | CodeBuild process. Located in this repository are two CodeBuild files: 87 | 88 | 1. `codebuild_capsule.cf` - this contains the CodeBuild CF templates for this project 89 | 2. `codebuild_project.cf` - which provides a template for the Capsule user to use for their own project 90 | 91 | In addition to the `ci` tool the CodeBuild cf templates can also be executed from the aws cli. 92 | 93 | From the CLI it can be used like this: 94 | 95 | ```sh 96 | aws cloudformation create-stack \ 97 | --stack-name \ 98 | --template-body file:///ci/.cf \ 99 | --parameters ParameterKey=CodeBuildProjectCodeName,ParameterValue= \ 100 | ParameterKey=RepositoryURL,ParameterValue= \ 101 | ParameterKey=BuildSpecLocation,ParameterValue= 102 | ``` 103 | 104 | Example: 105 | 106 | ```sh 107 | aws cloudformation create-stack \ 108 | --stack-name moduscreate-labs \ 109 | --template-body file:///ci/codebuild_project.cf \ 110 | --parameters ParameterKey=CodeBuildProjectCodeName,ParameterValue=labs \ 111 | ParameterKey=RepositoryURL,ParameterValue=https://github.com/ModusCreateOrg/labs.git 112 | ``` 113 | 114 | #### Supported parameters: 115 | 116 | - *CodeBuildProjectCodeName*: CodeBuild Project codename. 117 | - *RepositoryURL*: HTTPS URL for the Git repository. This should be a valid repository HTTPS URL. 118 | - *RepositoryType*: `CODECOMMIT`|`CODEPIPELINE`|`GITHUB`|`GITHUB_ENTERPRISE`|`BITBUCKET`|`S3`. Default: `GITHUB`. 119 | - *EnvironmentImage*: Image to use for running a container where the build will execute. Needs to respect the format `/:`. Default: `aws/codebuild/ubuntu-base:14.04` 120 | - *ComputeType*: `BUILD_GENERAL1_SMALL` (Small 3 GB memory, 2 vCPU) | `BUILD_GENERAL1_MEDIUM` (Medium 7 GB memory, 4 vCPU) | `BUILD_GENERAL1_LARGE` (large 15 GB memory, 8 vCPU). Default: `BUILD_GENERAL1_SMALL`. 121 | - *BuildSpecLocation*: Path of the file `buildspec.yml` to use (Defaults to `/buildspec.yml` 122 | -------------------------------------------------------------------------------- /ci/codebuild_project.cf: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: "User Website CloudFormation stack - CodeBuild Project" 4 | 5 | Parameters: 6 | CodeBuildProjectCodeName: 7 | Type: String 8 | Description: "User CodeBuild Project codename" 9 | RepositoryURL: 10 | Type: String 11 | Description: "HTTPS URL for the Git repository" 12 | AllowedPattern: "^https:\\/\\/(.*)\\/(.*)\\/(.)*" 13 | ConstraintDescription: "This should be a valid repository HTTPS URL" 14 | RepositoryType: 15 | Type: String 16 | Description: "CODECOMMIT|CODEPIPELINE|GITHUB|GITHUB_ENTERPRISE|BITBUCKET|S3" 17 | AllowedValues: 18 | - CODECOMMIT 19 | - CODEPIPELINE 20 | - GITHUB 21 | - GITHUB_ENTERPRISE 22 | - BITBUCKET 23 | - S3 24 | Default: GITHUB 25 | EnvironmentImage: 26 | Type: String 27 | Description: "Image to use for running a container where the build will execute" 28 | AllowedPattern: "^(.*)\\/(.*)\\/(.)*:(.)*" 29 | ConstraintDescription: "Image reference (/:)" 30 | #Default: aws/codebuild/ubuntu-base:14.04 31 | Default: aws/codebuild/nodejs:10.1.0 32 | ComputeType: 33 | Type: String 34 | Description: "Small (3 GB memory, 2 vCPU) | Medium (7 GB memory, 4 vCPU) | large (15 GB memory, 8 vCPU)" 35 | AllowedValues: 36 | - BUILD_GENERAL1_SMALL 37 | - BUILD_GENERAL1_MEDIUM 38 | - BUILD_GENERAL1_LARGE 39 | Default: BUILD_GENERAL1_SMALL 40 | BuildSpecLocation: 41 | Type: String 42 | Description: "Location of the build spec to use (Defaults to the repository root)" 43 | Default: buildspec.yml 44 | CloudDistId: 45 | Type: String 46 | Description: CloudFront Distribution ID 47 | WebsiteCode: 48 | Type: String 49 | Description: "Location of code to be moved to S3 bucket" 50 | Default: "./build" 51 | ProjectS3Bucket: 52 | Type: String 53 | Description: "Name of S3 bucket hosting the site" 54 | InstallCommands: 55 | Type: String 56 | Description: "List of command to run on install e.g. apt-get." 57 | BuildCommands: 58 | Type: String 59 | Description: "List of command to run on build e.g. npm test." 60 | PostBuildCommands: 61 | Type: String 62 | Description: "List of command to run on post build." 63 | Resources: 64 | CodeBuildProject: 65 | Type: AWS::CodeBuild::Project 66 | DependsOn: CodeBuildRole 67 | Properties: 68 | Name: !Sub ${AWS::StackName}-${CodeBuildProjectCodeName} 69 | Artifacts: 70 | Type: no_artifacts 71 | Environment: 72 | Image: !Ref EnvironmentImage 73 | Type: LINUX_CONTAINER 74 | ComputeType: !Ref ComputeType 75 | EnvironmentVariables: 76 | - 77 | Name: CLOUD_DIST_ID 78 | Value: !Ref CloudDistId 79 | - 80 | Name: WEBSITE_CODE 81 | Value: !Ref WebsiteCode 82 | - 83 | Name: PROJECT_S3_BUCKET 84 | Value: !Ref ProjectS3Bucket 85 | - 86 | Name: INSTALL_COMMANDS 87 | Value: !Ref InstallCommands 88 | - 89 | Name: BUILD_COMMANDS 90 | Value: !Ref BuildCommands 91 | - 92 | Name: POST_BUILD_COMMANDS 93 | Value: !Ref PostBuildCommands 94 | 95 | ServiceRole: !Ref CodeBuildRole 96 | Triggers: 97 | Webhook: yes 98 | Source: 99 | Type: !Ref RepositoryType 100 | Location: !Ref RepositoryURL 101 | Auth: 102 | Type: OAUTH 103 | BuildSpec: | 104 | version: 0.2 105 | phases: 106 | install: 107 | commands: 108 | - $INSTALL_COMMANDS 109 | build: 110 | commands: 111 | - $BUILD_COMMANDS 112 | post_build: 113 | commands: 114 | - aws s3 sync $WEBSITE_CODE s3://$PROJECT_S3_BUCKET/ 115 | - aws cloudfront create-invalidation --distribution-id $CLOUD_DIST_ID --path "/*" 116 | - $POST_BUILD_COMMANDS 117 | CodeBuildRole: 118 | Type: AWS::IAM::Role 119 | Properties: 120 | AssumeRolePolicyDocument: 121 | Version: "2012-10-17" 122 | Statement: 123 | - Effect: Allow 124 | Principal: 125 | Service: codebuild.amazonaws.com 126 | Action: sts:AssumeRole 127 | RolePolicies: 128 | Type: "AWS::IAM::Policy" 129 | Properties: 130 | PolicyName: "root" 131 | PolicyDocument: 132 | Version: "2012-10-17" 133 | Statement: 134 | - 135 | Effect: Allow 136 | Action: 137 | - "logs:CreateLogGroup" 138 | - "logs:CreateLogStream" 139 | - "logs:PutLogEvents" 140 | - "cloudformation:ValidateTemplate" 141 | - "cloudfront:CreateInvalidation" 142 | Resource: "*" 143 | - 144 | Effect: Allow 145 | Action: "s3:*" 146 | Resource: 147 | - !Sub "arn:aws:s3:::${ProjectS3Bucket}" 148 | - !Sub "arn:aws:s3:::${ProjectS3Bucket}/*" 149 | - 150 | Effect: Allow 151 | Action: "ssm:GetParameters" 152 | Resource: 153 | - "arn:aws:ssm:*:*:parameter/*" 154 | 155 | Roles: 156 | - Ref: "CodeBuildRole" 157 | 158 | Outputs: 159 | CodeBuildURL: 160 | Description: CodeBuild URL 161 | Value: !Sub https://console.aws.amazon.com/codebuild/home?region=${AWS::Region}#/projects/${CodeBuildProject}/view 162 | -------------------------------------------------------------------------------- /policies/admin_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "OASAdminRoute53ReadHealthCheck", 6 | "Effect": "Allow", 7 | "Action": [ 8 | "route53:ListTagsForResources", 9 | "route53:ListTagsForResource", 10 | "route53:GetHealthCheck" 11 | ], 12 | "Resource": [ 13 | "arn:aws:route53:::healthcheck/*" 14 | ] 15 | }, 16 | { 17 | "Sid": "OASAdminRoute53ReadHostedZone", 18 | "Effect": "Allow", 19 | "Action": [ 20 | "route53:GetHostedZoneLimit", 21 | "route53:ListTagsForResources", 22 | "route53:ListTagsForResource", 23 | "route53:ListVPCAssociationAuthorizations", 24 | "route53:ListTrafficPolicyInstancesByHostedZone" 25 | ], 26 | "Resource": [ 27 | "arn:aws:route53:::hostedzone/*" 28 | ] 29 | }, 30 | { 31 | "Sid": "OASAdminRoute53ReadTrafficPolicy", 32 | "Effect": "Allow", 33 | "Action": [ 34 | "route53:GetTrafficPolicy", 35 | "route53:ListTrafficPolicyInstancesByPolicy", 36 | "route53:ListTrafficPolicyVersions" 37 | ], 38 | "Resource": [ 39 | "arn:aws:route53:::trafficpolicy/*" 40 | ] 41 | }, 42 | { 43 | "Sid": "OASAdminRoute53ReadTrafficPolicyInstance", 44 | "Effect": "Allow", 45 | "Action": [ 46 | "route53:GetTrafficPolicyInstance" 47 | ], 48 | "Resource": [ 49 | "arn:aws:route53:::trafficpolicyinstance/*" 50 | ] 51 | }, 52 | { 53 | "Sid": "OASAdminRoute53ReadDeletagionSet", 54 | "Effect": "Allow", 55 | "Action": [ 56 | "route53:GetReusableDelegationSetLimit" 57 | ], 58 | "Resource": [ 59 | "arn:aws:route53:::delegationset/*" 60 | ] 61 | }, 62 | { 63 | "Sid": "OASAdminRoute53ReadQueryLoggingConfig", 64 | "Effect": "Allow", 65 | "Action": [ 66 | "route53:GetQueryLoggingConfig" 67 | ], 68 | "Resource": [ 69 | "arn:aws:route53:::queryloggingconfig/*" 70 | ] 71 | }, 72 | { 73 | "Sid": "OASAdminRoute53Read", 74 | "Effect": "Allow", 75 | "Action": [ 76 | "route53:ListTrafficPolicyInstances", 77 | "route53:GetTrafficPolicyInstanceCount", 78 | "route53:TestDNSAnswer", 79 | "route53:GetAccountLimit", 80 | "route53:ListTrafficPolicies" 81 | ], 82 | "Resource": [ 83 | "*" 84 | ] 85 | }, 86 | { 87 | "Sid": "OASAdminS3ReadObjects", 88 | "Effect": "Allow", 89 | "Action": [ 90 | "s3:GetObjectVersionTagging", 91 | "s3:GetObjectVersionTorrent", 92 | "s3:GetObjectAcl", 93 | "s3:GetObjectVersionAcl", 94 | "s3:GetObjectTagging", 95 | "s3:GetObject", 96 | "s3:GetObjectTorrent", 97 | "s3:GetObjectVersionForReplication", 98 | "s3:GetObjectVersion", 99 | "s3:ListMultipartUploadParts" 100 | ], 101 | "Resource": [ 102 | "arn:aws:s3:::*/*" 103 | ] 104 | }, 105 | { 106 | "Sid": "OASAdminS3ReadBuckets", 107 | "Effect": "Allow", 108 | "Action": [ 109 | "s3:ListBucketByTags", 110 | "s3:ListBucketMultipartUploads", 111 | "s3:ListBucketVersions", 112 | "s3:GetLifecycleConfiguration", 113 | "s3:GetBucketTagging", 114 | "s3:GetInventoryConfiguration", 115 | "s3:GetBucketWebsite", 116 | "s3:GetBucketLogging", 117 | "s3:GetAccelerateConfiguration", 118 | "s3:GetBucketVersioning", 119 | "s3:GetBucketAcl", 120 | "s3:GetBucketNotification", 121 | "s3:GetBucketPolicy", 122 | "s3:GetReplicationConfiguration", 123 | "s3:GetEncryptionConfiguration", 124 | "s3:GetBucketRequestPayment", 125 | "s3:GetBucketCORS", 126 | "s3:GetAnalyticsConfiguration", 127 | "s3:GetMetricsConfiguration", 128 | "s3:GetBucketLocation", 129 | "s3:GetIpConfiguration" 130 | ], 131 | "Resource": [ 132 | "arn:aws:s3:::*" 133 | ] 134 | }, 135 | { 136 | "Sid": "OASAdminCloudFormationReadStack", 137 | "Effect": "Allow", 138 | "Action": [ 139 | "cloudformation:DescribeStackResource", 140 | "cloudformation:DescribeStackResources", 141 | "cloudformation:DescribeStackEvents", 142 | "cloudformation:DescribeChangeSet", 143 | "cloudformation:GetStackPolicy", 144 | "cloudformation:GetTemplate" 145 | ], 146 | "Resource": [ 147 | "arn:aws:cloudformation:*:*:stack/*/*" 148 | ] 149 | }, 150 | { 151 | "Sid": "OASAdminCloudFormationReadStackSet", 152 | "Effect": "Allow", 153 | "Action": [ 154 | "cloudformation:DescribeStackSetOperation", 155 | "cloudformation:DescribeStackInstance", 156 | "cloudformation:DescribeStackSet" 157 | ], 158 | "Resource": [ 159 | "arn:aws:cloudformation:*:*:stackset/*:*" 160 | ] 161 | }, 162 | { 163 | "Sid": "OASAdminCloudFormationRead", 164 | "Effect": "Allow", 165 | "Action": [ 166 | "cloudformation:GetTemplateSummary", 167 | "cloudformation:EstimateTemplateCost", 168 | "cloudformation:DescribeAccountLimits" 169 | ], 170 | "Resource": [ 171 | "*" 172 | ] 173 | }, 174 | { 175 | "Sid": "OASAdminCodePipelineReadActionType", 176 | "Effect": "Allow", 177 | "Action": [ 178 | "codepipeline:ListActionTypes" 179 | ], 180 | "Resource": [ 181 | "arn:aws:codepipeline:*:*:actiontype:*/*/*/*" 182 | ] 183 | }, 184 | { 185 | "Sid": "OASAdminCodePipelineReadPipeline", 186 | "Effect": "Allow", 187 | "Action": [ 188 | "codepipeline:GetPipelineState", 189 | "codepipeline:GetPipeline", 190 | "codepipeline:GetPipelineExecution" 191 | ], 192 | "Resource": [ 193 | "arn:aws:codepipeline:*:*:*" 194 | ] 195 | }, 196 | { 197 | "Sid": "OASAdminCodePipelineRead", 198 | "Effect": "Allow", 199 | "Action": [ 200 | "codepipeline:GetThirdPartyJobDetails", 201 | "codepipeline:GetJobDetails" 202 | ], 203 | "Resource": [ 204 | "*" 205 | ] 206 | }, 207 | { 208 | "Sid": "OASAdminAWSCertificateManagerReadCertificate", 209 | "Effect": "Allow", 210 | "Action": [ 211 | "acm:DescribeCertificate", 212 | "acm:GetCertificate" 213 | ], 214 | "Resource": [ 215 | "arn:aws:acm:*:*:certificate/*" 216 | ] 217 | }, 218 | { 219 | "Sid": "OASAdminAWSCertificateManagerRead", 220 | "Effect": "Allow", 221 | "Action": [ 222 | "acm:ListTagsForCertificate" 223 | ], 224 | "Resource": [ 225 | "*" 226 | ] 227 | }, 228 | { 229 | "Sid": "OASAdminCloudFrontRead", 230 | "Effect": "Allow", 231 | "Action": [ 232 | "cloudfront:GetCloudFrontOriginAccessIdentityConfig", 233 | "cloudfront:GetInvalidation", 234 | "cloudfront:GetStreamingDistributionConfig", 235 | "cloudfront:GetDistribution", 236 | "cloudfront:GetStreamingDistribution", 237 | "cloudfront:GetCloudFrontOriginAccessIdentity", 238 | "cloudfront:GetDistributionConfig", 239 | "cloudfront:ListTagsForResource" 240 | ], 241 | "Resource": [ 242 | "*" 243 | ] 244 | } 245 | ] 246 | } 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capsule 2 | 3 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 4 | [![Powered by Modus_Create](https://img.shields.io/badge/powered_by-Modus_Create-blue.svg?longCache=true&style=flat&logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMzIwIDMwMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNOTguODI0IDE0OS40OThjMCAxMi41Ny0yLjM1NiAyNC41ODItNi42MzcgMzUuNjM3LTQ5LjEtMjQuODEtODIuNzc1LTc1LjY5Mi04Mi43NzUtMTM0LjQ2IDAtMTcuNzgyIDMuMDkxLTM0LjgzOCA4Ljc0OS01MC42NzVhMTQ5LjUzNSAxNDkuNTM1IDAgMCAxIDQxLjEyNCAxMS4wNDYgMTA3Ljg3NyAxMDcuODc3IDAgMCAwLTcuNTIgMzkuNjI4YzAgMzYuODQyIDE4LjQyMyA2OS4zNiA0Ni41NDQgODguOTAzLjMyNiAzLjI2NS41MTUgNi41Ny41MTUgOS45MjF6TTY3LjgyIDE1LjAxOGM0OS4xIDI0LjgxMSA4Mi43NjggNzUuNzExIDgyLjc2OCAxMzQuNDggMCA4My4xNjgtNjcuNDIgMTUwLjU4OC0xNTAuNTg4IDE1MC41ODh2LTQyLjM1M2M1OS43NzggMCAxMDguMjM1LTQ4LjQ1OSAxMDguMjM1LTEwOC4yMzUgMC0zNi44NS0xOC40My02OS4zOC00Ni41NjItODguOTI3YTk5Ljk0OSA5OS45NDkgMCAwIDEtLjQ5Ny05Ljg5NyA5OC41MTIgOTguNTEyIDAgMCAxIDYuNjQ0LTM1LjY1NnptMTU1LjI5MiAxODIuNzE4YzE3LjczNyAzNS41NTggNTQuNDUgNTkuOTk3IDk2Ljg4OCA1OS45OTd2NDIuMzUzYy02MS45NTUgMC0xMTUuMTYyLTM3LjQyLTEzOC4yOC05MC44ODZhMTU4LjgxMSAxNTguODExIDAgMCAwIDQxLjM5Mi0xMS40NjR6bS0xMC4yNi02My41ODlhOTguMjMyIDk4LjIzMiAwIDAgMS00My40MjggMTQuODg5QzE2OS42NTQgNzIuMjI0IDIyNy4zOSA4Ljk1IDMwMS44NDUuMDAzYzQuNzAxIDEzLjE1MiA3LjU5MyAyNy4xNiA4LjQ1IDQxLjcxNC01MC4xMzMgNC40Ni05MC40MzMgNDMuMDgtOTcuNDQzIDkyLjQzem01NC4yNzgtNjguMTA1YzEyLjc5NC04LjEyNyAyNy41NjctMTMuNDA3IDQzLjQ1Mi0xNC45MTEtLjI0NyA4Mi45NTctNjcuNTY3IDE1MC4xMzItMTUwLjU4MiAxNTAuMTMyLTIuODQ2IDAtNS42NzMtLjA4OC04LjQ4LS4yNDNhMTU5LjM3OCAxNTkuMzc4IDAgMCAwIDguMTk4LTQyLjExOGMuMDk0IDAgLjE4Ny4wMDguMjgyLjAwOCA1NC41NTcgMCA5OS42NjUtNDAuMzczIDEwNy4xMy05Mi44Njh6IiBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KPC9zdmc+)](https://moduscreate.com) 5 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/ModusCreateOrg/capsule/blob/OAS-15_documentation/LICENSE) 6 | 7 | Automated CLI for static web application hosting on AWS using S3 buckets. 8 | 9 |

10 | @modus/capsule 13 |

14 | 15 | ## Introduction 16 | 17 | This automated script simplifies setting up an AWS site. Add S3 buckets, 18 | register DNS, and create an SSL certificate in minutes with no DevOps knowledge. 19 | 20 | Capsule uses the following AWS technologies: 21 | 22 | 1. Amazon S3 23 | 2. Amazon CloudFormation 24 | 3. Amazon Certificate Manager 25 | 4. Amazon Route53 26 | 5. Amazon CloudFront 27 | 6. Amazon CloudBuild 28 | 29 | ### Is Capsule right for me? 30 | 31 | If you are exploring how to deploy a single page web application (SPA) this may be a good option for you. 32 | 33 | If you need a more comprehensive set of development tools and processses that encompass a wider variety of AWS services, for example, AWS Lambda, you might consider using the [AWS Amplify Console](https://aws.amazon.com/amplify/console/) instead as part of a more comprehensive application development pipeline that uses AWS and DevOps concepts. 34 | 35 | ## Installation 36 | 37 | Install `capsule` as a global CLI from NPM. 38 | 39 | ```sh 40 | npm install -g capsule 41 | ``` 42 | 43 | You can now use `capsule` from your command line. 44 | 45 | ## Quick Start - Configuring Your Environment 46 | 47 | In order to use Capsule you will need the following: 48 | 49 | * An AWS account. You can sign up here: https://aws.amazon.com/ 50 | * A registered domain name. This can be obtained through AWS or via a third party such as GoDaddy. 51 | * For continuous integration, a source code repository such as GitHub where your static website is located 52 | * A static website (HTML, JS, CSS) that does not require server side code like PHP, Python or Java. 53 | 54 | 55 | ### Security Credentials 56 | 57 | In order to use the Capsule command line interface you will need a number of security credentials. 58 | These credentials are used by the script to interact with your AWS account. 59 | 60 | 61 | #### Region 62 | 63 | Currently only a few regions can handle Certificate Manager in combination with CloudFront. You should therefore when 64 | creating the configuration described in the next section, ensure you chose a region that supports these features. 65 | 66 | Amazon provides a fature list at the following site: 67 | 68 | https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/ 69 | 70 | A safe bet is to use `us-east-1` as this is the region that Capsule has been tested in. 71 | 72 | 73 | #### AWS Config - JSON setup 74 | 75 | First we are going to create a single file `config.json` 76 | 77 | We a need directory to store this file, so create a new directory `.aws` under the root of your user 78 | 79 | *Mac/Linux* 80 | 81 | `mkdir ~/.aws` 82 | 83 | *Windows* 84 | 85 | `%UserProfile%\.aws` 86 | 87 | Next create the `config.json` file in the .aws directory, containing these keys: 88 | 89 | ```json 90 | { 91 | "accessKeyId": , 92 | "secretAccessKey": , 93 | "region": "us-east-1" 94 | } 95 | ``` 96 | 97 | After creating these files, log into your AWS account. We now need to create an Access Key. This can be done as follows: 98 | 99 | 1. Open your AWS Console 100 | 2. Click on your username in the top right 101 | 3. Select `My Security Credentials` 102 | 4. Fromt he screen that loads, click on `Users` in the sidebar 103 | 5. Next click on your username in the table 104 | 6. When this loads, click on the `Security Credentials` tab 105 | 7. We can now create an Access Key by clking `Create Access Key` 106 | 8. Finally click `Show User Security Credentials` and copy the ID and value. 107 | 108 | These details can only be displayed once, so if forget or lose them, you will need to generate a new key. 109 | If you wish you can download the CSV file from this screen as a backup. 110 | 111 | Re-open the config file e.g. vim `~/.aws/config.json` 112 | 113 | Now replace the `accessKeyId` value () with the value you copied from the AWS console. 114 | 115 | Next replace the `secretAccessKey` value (YOUR_SECRET_ACCESS_KEY) wuth the key you copied from the console. 116 | 117 | Make sure you wrap the value you paste in with `"` and `"`. 118 | 119 | You can change the region if you wish as well, but please check the region supports all the features required by Capsule. 120 | 121 | Save the file. You are now ready to use Capsule to build out your static site. 122 | 123 | 124 | #### AWS Config - YAML setup 125 | 126 | First we are going to create two files. These are the `config` and `credentials` file. 127 | 128 | Create a new directory `.aws` under the root of your user 129 | 130 | *Mac/Linux* 131 | 132 | `mkdir ~/.aws` 133 | 134 | *Windows* 135 | 136 | `%UserProfile%\.aws` 137 | 138 | Next create a credentials file in the .aws directory, containing these keys: 139 | 140 | ```yaml 141 | [default] 142 | aws_access_key_id= 143 | aws_secret_access_key= 144 | ``` 145 | 146 | After this, create a config file in the .aws directory and include the following: 147 | 148 | ```yaml 149 | [default] 150 | region= 151 | output= 152 | ``` 153 | 154 | After creating these files, log into your AWS account. We now need to create an Access Key. This can be done as follows: 155 | 156 | 1. Open your AWS Console 157 | 2. Click on your username in the top right 158 | 3. Select `My Security Credentials` 159 | 4. Fromt he screen that loads, click on `Users` in the sidebar 160 | 5. Next click on your username in the table 161 | 6. When this loads, click on the `Security Credentials` tab 162 | 7. We can now create an Access Key by clking `Create Access Key` 163 | 8. Finally click `Show User Security Credentials` and copy the ID and value. 164 | 165 | These details can only be displayed once, so if forget or lose them, you will need to generate a new key. 166 | If you wish you can download the CSV file from this screen as a backup. 167 | 168 | Re-open the credentials file e.g. vim `~/.aws/credentials` 169 | 170 | Paste the ID into the key id value: 171 | 172 | ```yaml 173 | aws_access_key_id= 174 | ``` 175 | 176 | Following this, paste the secret value into the secret key value: 177 | 178 | ```yaml 179 | aws_secret_access_key= 180 | ``` 181 | 182 | Our final step is the edit the `config` file and set a region and output type. 183 | 184 | You can chose whichever region makes sense to you, we are going to use us-east-1, and set the output to `json`. 185 | 186 | ```yaml 187 | [default] 188 | region=us-east-1 189 | output=json 190 | ``` 191 | 192 | Save the file. 193 | 194 | Your credentials and configuration are now setup to use Capsule. 195 | 196 | 197 | ## Quick Start - Configure Your Project 198 | 199 | In order to create the `config.json` file containing your project configuration 200 | run the command `capsule.js init` 201 | 202 | Answer the questions presented to you on the screen. 203 | 204 | If you wish to run multiple bash commands inside of the `build` or `post_build` 205 | CodeBuild actions, then you will need to pass these as a single parameter. 206 | 207 | Use the following chart as a guide for bash commands: 208 | 209 | ``` 210 | A ; B # Run A and then B, regardless of success of A 211 | A && B # Run B if and only if A succeeded 212 | A || B # Run B if and only if A failed 213 | ``` 214 | 215 | In the nodejs world for example this could translate to the following: 216 | 217 | ``` 218 | npm build dev ; npm test 219 | ``` 220 | 221 | 222 | ### Project Names 223 | 224 | During the setup of your site you will need to define a project name. This will be used to name the 225 | S3 bucket in AWS. Therefore your project name must confirm to the S3 bucket naming standards. 226 | 227 | You can find these here: 228 | 229 | https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html 230 | 231 | 232 | 233 | ## Quick Start - Deploy Your Project 234 | 235 | Once the `capsule.json` file is generated, you are now ready to deploy your project to 236 | your AWS account. 237 | 238 | To do this, simply type: 239 | 240 | `capsule.js deploy` 241 | 242 | If you wish to learn more about the templates that are implemented by the 243 | deploy command, please refer to the [templates read me](docs/templates.md) file. 244 | 245 | ### Authorizing your certificate 246 | 247 | In order to authorize your certificate you will need to log into the AWS console. 248 | 249 | Depending on whether you are using DNS or Email authorization you will need to follow the relevant steps below. 250 | 251 | 252 | #### Domain configuration 253 | 254 | As part of the process of creating your static site, you will need to point an existing domain or subdomain to your S3 bucket. 255 | When executing the `web` command from the cli the process will halt once it reaches the certificate manager portion. 256 | 257 | At this point you should log into your AWS console and select the ` Certificate Manager` service. On this screen the domain 258 | you passed to the cli should be visible e.g. `example.com`. 259 | 260 | Open up the drop-down arrow for the domain and follow the insturctions provived bu Amazon to validate control of the domain. Note that Amazon will send an email to your account at: 261 | 262 | * webmaster@example.com 263 | * admin@example.com 264 | * postmaster@example.com 265 | * administrator@example.com 266 | * hostmaster@example.com 267 | 268 | Where `example.com` is the domain you passed to the cli tool. 269 | 270 | 271 | ### CloudFront waiting time 272 | 273 | Once the CloudFormation templates are kicked off the CloudFront stack process, you can expect to wait around ~20 mins for this 274 | process to complete. 275 | 276 | You can check on progress under the CloudFront services home page: 277 | 278 | https://console.aws.amazon.com/cloudfront/home?region=us-east-1 279 | 280 | 281 | ### Continuous Integration (CI) 282 | 283 | Capsule allows for Continuous Integration (CI) of your changes from a source code repository. 284 | As present the CI is currently using codebuild only. 285 | 286 | The cloudformation template [codebuild.cf](ci/codebuild.cf) allows you to quickly 287 | setup a basic CI infrastructure for any repository. 288 | 289 | #### Requirements 290 | 291 | The following section lists any specific requirements for source control products and services. 292 | 293 | 294 | ##### GitHub 295 | The AWS codebuild service must be already authenticated with Github using OAuth before creating the stack. 296 | You can read more on OAuth integration steps on GitHubs website here: 297 | 298 | https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ 299 | 300 | Make sure that the user you have setup in GitHub, to be used by CodeBuild has admin permissions on the repository. 301 | 302 | If it does not, you may see errors such as: 303 | 304 | ``` 305 | Repository not found or permission denied. (Service: AWSCodeBuild; Status Code: 400; Error Code: OAuthProviderException; Request ID: ) 306 | ``` 307 | 308 | This is especially important if you wish to manage webhooks e.g. 309 | 310 | ``` 311 | Triggers: 312 | Webhook: yes 313 | ``` 314 | 315 | In this case, the user will need Admin permissions. 316 | 317 | You will need to create a [webhook in GitHub](https://developer.github.com/webhooks/). 318 | 319 | 320 | ## Advanced Options 321 | 322 | Capsule comes with a number of advanced options. These allow: 323 | 324 | 1. A more granular deployment process 325 | 2. Overridding default settings in the capsule.json with command line values 326 | 327 | To read me please check out the documentation [here](docs/advanced_use.md). 328 | 329 | ### Templates 330 | 331 | To learn more about the CloudFormation templates that make up a portion of the capsule project 332 | please refer to the template documentation [here](docs/templates.md). 333 | 334 | ## Contibute to this project 335 | 336 | To learn about our contributor guidelines, please check out the documentation [here](docs/contribute.md) 337 | 338 | ## Modus Create 339 | 340 | [Modus Create](https://moduscreate.com) is a digital product consultancy. We use a distributed team of the best talent in the world to offer a full suite of digital product design-build services; ranging from consumer facing apps, to digital migration, to agile development training, and business transformation. 341 | 342 | [![Modus Create](https://res.cloudinary.com/modus-labs/image/upload/h_80/v1533109874/modus/logo-long-black.png)](https://moduscreate.com) 343 | 344 | This project is part of [Modus Labs](https://labs.moduscreate.com). 345 | 346 | [![Modus Labs](https://res.cloudinary.com/modus-labs/image/upload/h_80/v1531492623/labs/logo-black.png)](https://labs.moduscreate.com) 347 | 348 | ## Licensing 349 | 350 | This project is [MIT licensed](./LICENSE). 351 | -------------------------------------------------------------------------------- /policies/iam-admin-group.cf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: "Admin Role for the capsule CI infrastructure" 4 | Parameters: 5 | ProjectName: 6 | Type: String 7 | Description: "Project Name" 8 | AdminGroupName: 9 | Type: String 10 | Description: "Admin Group Name" 11 | AdminGroupPath: 12 | Type: String 13 | Description: "Admin Group Path" 14 | Default: '/admin/' 15 | Resources: 16 | AdminGroup: 17 | Type: "AWS::IAM::Group" 18 | Properties: 19 | GroupName: !Ref AdminGroupName 20 | Path: !Ref AdminGroupPath 21 | Route53Policy: 22 | Type: "AWS::IAM::ManagedPolicy" 23 | Properties: 24 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-route53-policy" 25 | Groups: 26 | - !Ref AdminGroup 27 | PolicyDocument: 28 | Version: "2012-10-17" 29 | Statement: 30 | - Sid: OASAdminRoute53ReadHealthCheck 31 | Effect: Allow 32 | Action: 33 | - route53:ListTagsForResources 34 | - route53:ListTagsForResource 35 | - route53:GetHealthCheck 36 | - route53:GetHealthCheckLastFailureReason 37 | - route53:GetHealthCheckStatus 38 | Resource: "arn:aws:route53:::healthcheck/*" 39 | - Sid: OASAdminRoute53ReadHostedZone 40 | Effect: Allow 41 | Action: 42 | - route53:GetHostedZone 43 | - route53:GetHostedZoneLimit 44 | - route53:ListTagsForResources 45 | - route53:ListTagsForResource 46 | - route53:ListVPCAssociationAuthorizations 47 | - route53:ListTrafficPolicyInstancesByHostedZone 48 | - route53:ListResourceRecordSets 49 | Resource: "arn:aws:route53:::hostedzone/*" 50 | - Sid: OASAdminRoute53ReadTrafficPolicy 51 | Effect: Allow 52 | Action: 53 | - route53:GetTrafficPolicy 54 | - route53:ListTrafficPolicyInstancesByPolicy 55 | - route53:ListTrafficPolicyVersions 56 | Resource: "arn:aws:route53:::trafficpolicy/*" 57 | - Sid: OASAdminRoute53ReadTrafficPolicyInstance 58 | Effect: Allow 59 | Action: 60 | - route53:GetTrafficPolicyInstance 61 | Resource: "arn:aws:route53:::trafficpolicyinstance/*" 62 | - Sid: OASAdminRoute53ReadDeletagionSet 63 | Effect: Allow 64 | Action: 65 | - route53:GetReusableDelegationSetLimit 66 | - route53:GetReusableDelegationSet 67 | Resource: "arn:aws:route53:::delegationset/*" 68 | - Sid: OASAdminRoute53ReadQueryLoggingConfig 69 | Effect: Allow 70 | Action: 71 | - route53:GetQueryLoggingConfig 72 | Resource: "arn:aws:route53:::queryloggingconfig/*" 73 | - Sid: OASAdminRoute53Read 74 | Effect: Allow 75 | Action: 76 | - route53:ListTrafficPolicyInstances 77 | - route53:GetTrafficPolicyInstanceCount 78 | - route53:TestDNSAnswer 79 | - route53:GetAccountLimit 80 | - route53:ListTrafficPolicies 81 | Resource: "*" 82 | - Sid: OASAdminRoute53WriteHealthCheck 83 | Effect: Allow 84 | Action: 85 | - route53:ChangeTagsForResource 86 | - route53:DeleteHealthCheck 87 | - route53:UpdateHealthCheck 88 | Resource: "arn:aws:route53:::healthcheck/*" 89 | - Sid: OASAdminRoute53WriteHostedZone 90 | Effect: Allow 91 | Action: 92 | - route53:CreateHostedZone 93 | - route53:ChangeResourceRecordSets 94 | - route53:ChangeTagsForResource 95 | - route53:DeleteHostedZone 96 | - route53:AssociateVPCWithHostedZone 97 | - route53:DisassociateVPCFromHostedZone 98 | - route53:UpdateHostedZoneComment 99 | - route53:ListVPCAssociationAuthorizations 100 | Resource: "*" 101 | - Sid: OASAdminRoute53WriteTrafficPolicy 102 | Effect: Allow 103 | Action: 104 | - route53:DeleteTrafficPolicy 105 | - route53:UpdateTrafficPolicyComment 106 | Resource: "arn:aws:route53:::trafficpolicy/*" 107 | - Sid: OASAdminRoute53WriteTrafficPolicyInstance 108 | Effect: Allow 109 | Action: 110 | - route53:DeleteTrafficPolicyInstance 111 | - route53:UpdateTrafficPolicyInstance 112 | Resource: "arn:aws:route53:::trafficpolicyinstance/*" 113 | - Sid: OASAdminRoute53WriteDeletagionSet 114 | Effect: Allow 115 | Action: 116 | - route53:DeleteReusableDelegationSet 117 | Resource: "arn:aws:route53:::delegationset/*" 118 | - Sid: OASAdminRoute53Write 119 | Effect: Allow 120 | Action: 121 | - route53:CreateTrafficPolicyInstance 122 | - route53:CreateTrafficPolicy 123 | - route53:CreateTrafficPolicyVersion 124 | - route53:CreateReusableDelegationSet 125 | - route53:CreateHealthCheck 126 | - route53:CreateQueryLoggingConfig 127 | Resource: "*" 128 | S3Policy: 129 | Type: "AWS::IAM::ManagedPolicy" 130 | Properties: 131 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-s3-policy" 132 | Groups: 133 | - !Ref AdminGroup 134 | PolicyDocument: 135 | Version: "2012-10-17" 136 | Statement: 137 | - Sid: OASAdminS3ReadObjects 138 | Effect: Allow 139 | Action: 140 | - s3:GetObjectVersionTagging 141 | - s3:GetObjectVersionTorrent 142 | - s3:GetObjectAcl 143 | - s3:GetObjectVersionAcl 144 | - s3:GetObjectTagging 145 | - s3:GetObject 146 | - s3:GetObjectTorrent 147 | - s3:GetObjectVersionForReplication 148 | - s3:GetObjectVersion 149 | - s3:ListMultipartUploadParts 150 | Resource: "arn:aws:s3:::*/*" 151 | - Sid: OASAdminS3ReadBuckets 152 | Effect: Allow 153 | Action: 154 | - s3:ListBucketByTags 155 | - s3:ListBucketMultipartUploads 156 | - s3:ListBucketVersions 157 | - s3:GetLifecycleConfiguration 158 | - s3:GetBucketTagging 159 | - s3:GetInventoryConfiguration 160 | - s3:GetBucketWebsite 161 | - s3:GetBucketLogging 162 | - s3:GetAccelerateConfiguration 163 | - s3:GetBucketVersioning 164 | - s3:GetBucketAcl 165 | - s3:GetBucketNotification 166 | - s3:GetBucketPolicy 167 | - s3:GetReplicationConfiguration 168 | - s3:GetEncryptionConfiguration 169 | - s3:GetBucketRequestPayment 170 | - s3:GetBucketCORS 171 | - s3:GetAnalyticsConfiguration 172 | - s3:GetMetricsConfiguration 173 | - s3:GetBucketLocation 174 | - s3:GetIpConfiguration 175 | Resource: "arn:aws:s3:::*" 176 | - Sid: OASAdminS3WriteObjects 177 | Effect: Allow 178 | Action: 179 | - s3:AbortMultipartUpload 180 | - s3:DeleteObject 181 | - s3:DeleteObjectTagging 182 | - s3:DeleteObjectVersion 183 | - s3:DeleteObjectVersionTagging 184 | - s3:PutObject 185 | - s3:PutObjectAcl 186 | - s3:PutObjectTagging 187 | - s3:PutObjectVersionAcl 188 | - s3:PutObjectVersionTagging 189 | - s3:ReplicateDelete 190 | - s3:ReplicateObject 191 | - s3:ReplicateTags 192 | - s3:RestoreObject 193 | Resource: "arn:aws:s3:::*/*" 194 | - Sid: OASAdminS3WriteBuckets 195 | Effect: Allow 196 | Action: 197 | - s3:DeleteBucket 198 | - s3:DeleteBucketPolicy 199 | - s3:DeleteBucketWebsite 200 | - s3:PutAccelerateConfiguration 201 | - s3:PutAnalyticsConfiguration 202 | - s3:PutBucketAcl 203 | - s3:PutBucketCORS 204 | - s3:PutBucketLogging 205 | - s3:PutBucketNotification 206 | - s3:PutBucketPolicy 207 | - s3:PutBucketRequestPayment 208 | - s3:PutBucketTagging 209 | - s3:PutBucketVersioning 210 | - s3:PutBucketWebsite 211 | - s3:PutEncryptionConfiguration 212 | - s3:PutInventoryConfiguration 213 | - s3:PutIpConfiguration 214 | - s3:PutLifecycleConfiguration 215 | - s3:PutMetricsConfiguration 216 | - s3:PutReplicationConfiguration 217 | Resource: "arn:aws:s3:::*" 218 | - Sid: OASAdminS3Write 219 | Effect: Allow 220 | Action: 221 | - s3:CreateBucket 222 | Resource: "*" 223 | CloudFormationPolicy: 224 | Type: "AWS::IAM::ManagedPolicy" 225 | Properties: 226 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-cloudformation-policy" 227 | Groups: 228 | - !Ref AdminGroup 229 | PolicyDocument: 230 | Version: "2012-10-17" 231 | Statement: 232 | - Sid: OASAdminCloudFormationReadStack 233 | Effect: Allow 234 | Action: 235 | - cloudformation:DescribeStackResource 236 | - cloudformation:DescribeStackResources 237 | - cloudformation:DescribeStackEvents 238 | - cloudformation:DescribeChangeSet 239 | - cloudformation:GetStackPolicy 240 | - cloudformation:GetTemplate 241 | - cloudformation:ListStackResources 242 | - cloudformation:ListChangeSets 243 | - cloudformation:GetTemplate 244 | Resource: "arn:aws:cloudformation:*:*:stack/*/*" 245 | - Sid: OASAdminCloudFormationReadStackSet 246 | Effect: Allow 247 | Action: 248 | - cloudformation:DescribeStackSetOperation 249 | - cloudformation:DescribeStackInstance 250 | - cloudformation:DescribeStackSet 251 | - cloudformation:ListStackSets 252 | - cloudformation:ListStackSetOperations 253 | - cloudformation:ListStackSetOperationResults 254 | Resource: "arn:aws:cloudformation:*:*:stackset/*:*" 255 | - Sid: OASAdminCloudFormationRead 256 | Effect: Allow 257 | Action: 258 | - cloudformation:GetTemplateSummary 259 | - cloudformation:EstimateTemplateCost 260 | - cloudformation:DescribeAccountLimits 261 | - cloudformation:ListStacks 262 | - cloudformation:ListImports 263 | - cloudformation:ListExports 264 | Resource: "*" 265 | - Sid: OASAdminCloudFormationWriteStack 266 | Effect: Allow 267 | Action: 268 | - cloudformation:DeleteStack 269 | - cloudformation:UpdateStack 270 | - cloudformation:CancelUpdateStack 271 | - cloudformation:CreateChangeSet 272 | - cloudformation:DeleteChangeSet 273 | - cloudformation:ExecuteChangeSet 274 | - cloudformation:SetStackPolicy 275 | - cloudformation:SignalResource 276 | - cloudformation:ContinueUpdateRollback 277 | - cloudformation:UpdateTerminationProtection 278 | Resource: "arn:aws:cloudformation:*:*:stack/*/*" 279 | - Sid: OASAdminCloudFormationWriteStackSet 280 | Effect: Allow 281 | Action: 282 | - cloudformation:DeleteStackInstances 283 | - cloudformation:UpdateStackInstances 284 | - cloudformation:DeleteStackSet 285 | - cloudformation:UpdateStackSet 286 | - cloudformation:StopStackSetOperation 287 | Resource: "arn:aws:cloudformation:*:*:stackset/*:*" 288 | - Sid: OASAdminCloudFormationWrite 289 | Effect: Allow 290 | Action: 291 | - cloudformation:CreateStack 292 | - cloudformation:CreateStackSet 293 | - cloudformation:CreateStackInstances 294 | - cloudformation:ValidateTemplate 295 | - cloudformation:CreateUploadBucket 296 | Resource: "*" 297 | CodePipelinePolicy: 298 | Type: "AWS::IAM::ManagedPolicy" 299 | Properties: 300 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-codepipeline-policy" 301 | Groups: 302 | - !Ref AdminGroup 303 | PolicyDocument: 304 | Version: "2012-10-17" 305 | Statement: 306 | - Sid: OASAdminCodePipelineReadActionType 307 | Effect: Allow 308 | Action: 309 | - codepipeline:ListActionTypes 310 | Resource: "arn:aws:codepipeline:*:*:actiontype:*/*/*/*" 311 | - Sid: OASAdminCodePipelineReadWebhook 312 | Effect: Allow 313 | Action: 314 | - codepipeline:ListWebhooks 315 | Resource: "arn:aws:codepipeline:*:*:webhook:*/*/*/*" 316 | - Sid: OASAdminCodePipelineReadPipeline 317 | Effect: Allow 318 | Action: 319 | - codepipeline:GetPipelineState 320 | - codepipeline:GetPipeline 321 | - codepipeline:GetPipelineExecution 322 | - codepipeline:ListPipelineExecutions 323 | - codepipeline:ListPipelines 324 | Resource: "arn:aws:codepipeline:*:*:*" 325 | - Sid: OASAdminCodePipelineRead 326 | Effect: Allow 327 | Action: 328 | - codepipeline:GetThirdPartyJobDetails 329 | - codepipeline:GetJobDetails 330 | Resource: "*" 331 | - Sid: OASAdminCodePipelineWriteActionType 332 | Effect: Allow 333 | Action: 334 | - codepipeline:CreateCustomActionType 335 | - codepipeline:DeleteCustomActionType 336 | - codepipeline:PollForJobs 337 | Resource: "arn:aws:codepipeline:*:*:actiontype:*/*/*/*" 338 | - Sid: OASAdminCodePipelineWriteWebhook 339 | Effect: Allow 340 | Action: 341 | - codepipeline:DeleteWebhook 342 | - codepipeline:DeregisterWebhookWithThirdParty 343 | - codepipeline:PutWebhook 344 | - codepipeline:RegisterWebhookWithThirdParty 345 | Resource: "arn:aws:codepipeline:*:*:webhook:*/*/*/*" 346 | - Sid: OASAdminCodePipelineWritePipeline 347 | Effect: Allow 348 | Action: 349 | - codepipeline:DeletePipeline 350 | - codepipeline:PutWebhook 351 | - codepipeline:StartPipelineExecution 352 | - codepipeline:UpdatePipeline 353 | Resource: "arn:aws:codepipeline:*:*:*" 354 | - Sid: OASAdminCodePipelineWrite 355 | Effect: Allow 356 | Action: 357 | - codepipeline:AcknowledgeJob 358 | - codepipeline:AcknowledgeThirdPartyJob 359 | - codepipeline:PollForThirdPartyJobs 360 | - codepipeline:PutApprovalResult 361 | - codepipeline:PutJobFailureResult 362 | - codepipeline:PutJobSuccessResult 363 | - codepipeline:PutThirdPartyJobFailureResult 364 | - codepipeline:PutThirdPartyJobSuccessResult 365 | - codepipeline:CreatePipeline 366 | - codepipeline:PutActionRevision 367 | - codepipeline:DisableStageTransition 368 | - codepipeline:EnableStageTransition 369 | - codepipeline:RetryStageExecution 370 | Resource: "*" 371 | CertManagerPolicy: 372 | Type: "AWS::IAM::ManagedPolicy" 373 | Properties: 374 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-acm-policy" 375 | Groups: 376 | - !Ref AdminGroup 377 | PolicyDocument: 378 | Version: "2012-10-17" 379 | Statement: 380 | - Sid: OASAdminAWSCertificateManagerReadCertificate 381 | Effect: Allow 382 | Action: 383 | - acm:DescribeCertificate 384 | - acm:GetCertificate 385 | Resource: "arn:aws:acm:*:*:certificate/*" 386 | - Sid: OASAdminAWSCertificateManagerRead 387 | Effect: Allow 388 | Action: 389 | - acm:ListTagsForCertificate 390 | - acm:ListCertificates 391 | Resource: "*" 392 | - Sid: OASAdminAWSCertificateManagerWriteCertificate 393 | Effect: Allow 394 | Action: 395 | - acm:AddTagsToCertificate 396 | - acm:DeleteCertificate 397 | - acm:ImportCertificate 398 | - acm:RemoveTagsFromCertificate 399 | - acm:ResendValidationEmail 400 | Resource: "arn:aws:acm:*:*:certificate/*" 401 | - Sid: OASAdminAWSCertificateManagerWrite 402 | Effect: Allow 403 | Action: 404 | - acm:RequestCertificate 405 | Resource: "*" 406 | CloudFrontPolicy: 407 | Type: "AWS::IAM::ManagedPolicy" 408 | Properties: 409 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-cloudfront-policy" 410 | Groups: 411 | - !Ref AdminGroup 412 | PolicyDocument: 413 | Version: "2012-10-17" 414 | Statement: 415 | - Sid: OASAdminCloudFrontRead 416 | Effect: Allow 417 | Action: 418 | - cloudfront:GetCloudFrontOriginAccessIdentityConfig 419 | - cloudfront:GetInvalidation 420 | - cloudfront:GetStreamingDistributionConfig 421 | - cloudfront:GetDistribution 422 | - cloudfront:GetStreamingDistribution 423 | - cloudfront:GetCloudFrontOriginAccessIdentity 424 | - cloudfront:GetDistributionConfig 425 | - cloudfront:ListTagsForResource 426 | - cloudfront:ListCloudFrontOriginAccessIdentities 427 | - cloudfront:ListDistributionsByWebACLId 428 | - cloudfront:ListDistributions 429 | - cloudfront:ListInvalidations 430 | - cloudfront:ListStreamingDistributions 431 | Resource: "*" 432 | - Sid: OASAdminCloudFrontWrite 433 | Effect: Allow 434 | Action: 435 | - cloudfront:CreateCloudFrontOriginAccessIdentity 436 | - cloudfront:CreateDistribution 437 | - cloudfront:CreateInvalidation 438 | - cloudfront:CreateStreamingDistribution 439 | - cloudfront:DeleteCloudFrontOriginAccessIdentity 440 | - cloudfront:DeleteDistribution 441 | - cloudfront:DeleteStreamingDistribution 442 | - cloudfront:UpdateCloudFrontOriginAccessIdentity 443 | - cloudfront:UpdateDistribution 444 | - cloudfront:UpdateStreamingDistribution 445 | - cloudfront:CreateDistributionWithTags 446 | - cloudfront:CreateStreamingDistributionWithTags 447 | - cloudfront:TagResource 448 | - cloudfront:UntagResource 449 | Resource: "*" 450 | CodeBuildPolicy: 451 | Type: "AWS::IAM::ManagedPolicy" 452 | Properties: 453 | ManagedPolicyName: !Sub "cf-${ProjectName}-${AdminGroupName}-codebuild-policy" 454 | Groups: 455 | - !Ref AdminGroup 456 | PolicyDocument: 457 | Version: "2012-10-17" 458 | Statement: 459 | - Sid: OASAdminCodeBuildReadProject 460 | Effect: Allow 461 | Action: 462 | - codebuild:BatchGetBuilds 463 | - codebuild:BatchGetProjects 464 | - codebuild:ListBuildsForProject 465 | Resource: "arn:aws:codebuild:*:*:project/*" 466 | - Sid: OASAdminCodeBuildRead 467 | Effect: Allow 468 | Action: 469 | - codebuild:ListBuilds 470 | - codebuild:ListConnectedOAuthAccounts 471 | - codebuild:ListCuratedEnvironmentImages 472 | - codebuild:ListProjects 473 | - codebuild:ListRepositories 474 | - codebuild:PersistOAuthToken 475 | Resource: "*" 476 | - Sid: OASAdminCodeBuildWriteProject 477 | Effect: Allow 478 | Action: 479 | - codebuild:BatchDeleteBuilds 480 | - codebuild:DeleteProject 481 | - codebuild:StartBuild 482 | - codebuild:StopBuild 483 | - codebuild:UpdateProject 484 | Resource: "arn:aws:codebuild:*:*:project/*" 485 | - Sid: OASAdminCodeBuildWrite 486 | Effect: Allow 487 | Action: 488 | - codebuild:CreateProject 489 | - codebuild:PersistOAuthToken 490 | Resource: "*" 491 | -------------------------------------------------------------------------------- /templates/child_templates/template.cloudfront.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Website CloudFormation stack - CloudFront" 3 | 4 | Parameters: 5 | Domain: 6 | Type: String 7 | Description: "The DNS name of an existing Amazon Route53 hosted zone, e.g. moduscreate.com" 8 | AllowedPattern: "(?!-)[a-z0-9-]{1,63}(?::certificate/" 98 | AllowedPattern: "arn:aws:acm:[a-z0-9-]+:[\\d]+:certificate/[a-z0-9]{8}-(?:[a-z0-9]{4}-){3}[a-z0-9]{12}" 99 | ConstraintDescription: "AcmCertificateArn must be a valid ARN. Allowed pattern: arn:aws:acm:[a-z0-9-]+:[\\d]+:certificate/[a-z0-9]{8}-(?:[a-z0-9]{4}-){3}[a-z0-9]{12}" 100 | CloudFrontOriginAccessIdentityId: 101 | Type: String 102 | Description: "CloudFront origin access identity id" 103 | 400ResponseCode: 104 | Type: Number 105 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 400 errors" 106 | AllowedValues: 107 | - 200 108 | - 400 109 | - 403 110 | - 404 111 | - 405 112 | - 414 113 | - 500 114 | - 501 115 | - 502 116 | - 503 117 | - 504 118 | ConstraintDescription: "400ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 119 | Default: 400 120 | 400ResponsePagePath: 121 | Type: String 122 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 400 errors" 123 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 124 | ConstraintDescription: "400ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 125 | Default: "" 126 | 400ErrorCachingMinTTL: 127 | Type: Number 128 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 400 errors" 129 | MaxValue: 31536000 130 | MinValue: 0 131 | ConstraintDescription: "400ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 132 | Default: 300 133 | 403ResponseCode: 134 | Type: Number 135 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 403 errors" 136 | AllowedValues: 137 | - 200 138 | - 400 139 | - 403 140 | - 404 141 | - 405 142 | - 414 143 | - 500 144 | - 501 145 | - 502 146 | - 503 147 | - 504 148 | ConstraintDescription: "403ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 149 | Default: 403 150 | 403ResponsePagePath: 151 | Type: String 152 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 403 errors" 153 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 154 | ConstraintDescription: "403ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 155 | Default: "" 156 | 403ErrorCachingMinTTL: 157 | Type: Number 158 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 403 errors" 159 | MaxValue: 31536000 160 | MinValue: 0 161 | ConstraintDescription: "403ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 162 | Default: 300 163 | 404ResponseCode: 164 | Type: Number 165 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 404 errors" 166 | AllowedValues: 167 | - 200 168 | - 400 169 | - 403 170 | - 404 171 | - 405 172 | - 414 173 | - 500 174 | - 501 175 | - 502 176 | - 503 177 | - 504 178 | ConstraintDescription: "404ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 179 | Default: 200 180 | 404ResponsePagePath: 181 | Type: String 182 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 404 errors" 183 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 184 | ConstraintDescription: "404ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 185 | Default: "/index.html" 186 | 404ErrorCachingMinTTL: 187 | Type: Number 188 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 404 errors" 189 | MaxValue: 31536000 190 | MinValue: 0 191 | ConstraintDescription: "404ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 192 | Default: 300 193 | 405ResponseCode: 194 | Type: Number 195 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 405 errors" 196 | AllowedValues: 197 | - 200 198 | - 400 199 | - 403 200 | - 404 201 | - 405 202 | - 414 203 | - 500 204 | - 501 205 | - 502 206 | - 503 207 | - 504 208 | ConstraintDescription: "405ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 209 | Default: 405 210 | 405ResponsePagePath: 211 | Type: String 212 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 405 errors" 213 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 214 | ConstraintDescription: "405ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 215 | Default: "" 216 | 405ErrorCachingMinTTL: 217 | Type: Number 218 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 405 errors" 219 | MaxValue: 31536000 220 | MinValue: 0 221 | ConstraintDescription: "405ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 222 | Default: 300 223 | 414ResponseCode: 224 | Type: Number 225 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 414 errors" 226 | AllowedValues: 227 | - 200 228 | - 400 229 | - 403 230 | - 404 231 | - 405 232 | - 414 233 | - 500 234 | - 501 235 | - 502 236 | - 503 237 | - 504 238 | ConstraintDescription: "414ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 239 | Default: 414 240 | 414ResponsePagePath: 241 | Type: String 242 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 414 errors" 243 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 244 | ConstraintDescription: "414ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 245 | Default: "" 246 | 414ErrorCachingMinTTL: 247 | Type: Number 248 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 414 errors" 249 | MaxValue: 31536000 250 | MinValue: 0 251 | ConstraintDescription: "414ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 252 | Default: 300 253 | 500ResponseCode: 254 | Type: Number 255 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 500 errors" 256 | AllowedValues: 257 | - 200 258 | - 400 259 | - 403 260 | - 404 261 | - 405 262 | - 414 263 | - 500 264 | - 501 265 | - 502 266 | - 503 267 | - 504 268 | ConstraintDescription: "500ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 269 | Default: 500 270 | 500ResponsePagePath: 271 | Type: String 272 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 500 errors" 273 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 274 | ConstraintDescription: "500ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 275 | Default: "" 276 | 500ErrorCachingMinTTL: 277 | Type: Number 278 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 500 errors" 279 | MaxValue: 31536000 280 | MinValue: 0 281 | ConstraintDescription: "500ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 282 | Default: 300 283 | 501ResponseCode: 284 | Type: Number 285 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 501 errors" 286 | AllowedValues: 287 | - 200 288 | - 400 289 | - 403 290 | - 404 291 | - 405 292 | - 414 293 | - 500 294 | - 501 295 | - 502 296 | - 503 297 | - 504 298 | ConstraintDescription: "501ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 299 | Default: 501 300 | 501ResponsePagePath: 301 | Type: String 302 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 501 errors" 303 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 304 | ConstraintDescription: "501ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 305 | Default: "" 306 | 501ErrorCachingMinTTL: 307 | Type: Number 308 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 501 errors" 309 | MaxValue: 31536000 310 | MinValue: 0 311 | ConstraintDescription: "501ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 312 | Default: 300 313 | 502ResponseCode: 314 | Type: Number 315 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 502 errors" 316 | AllowedValues: 317 | - 200 318 | - 400 319 | - 403 320 | - 404 321 | - 405 322 | - 414 323 | - 500 324 | - 501 325 | - 502 326 | - 503 327 | - 504 328 | ConstraintDescription: "502ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 329 | Default: 502 330 | 502ResponsePagePath: 331 | Type: String 332 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 502 errors" 333 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 334 | ConstraintDescription: "502ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 335 | Default: "" 336 | 502ErrorCachingMinTTL: 337 | Type: Number 338 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 502 errors" 339 | MaxValue: 31536000 340 | MinValue: 0 341 | ConstraintDescription: "502ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 342 | Default: 300 343 | 503ResponseCode: 344 | Type: Number 345 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 503 errors" 346 | AllowedValues: 347 | - 200 348 | - 400 349 | - 403 350 | - 404 351 | - 405 352 | - 414 353 | - 500 354 | - 501 355 | - 502 356 | - 503 357 | - 504 358 | ConstraintDescription: "503ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 359 | Default: 503 360 | 503ResponsePagePath: 361 | Type: String 362 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 503 errors" 363 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 364 | ConstraintDescription: "503ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 365 | Default: "" 366 | 503ErrorCachingMinTTL: 367 | Type: Number 368 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 503 errors" 369 | MaxValue: 31536000 370 | MinValue: 0 371 | ConstraintDescription: "503ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 372 | Default: 300 373 | 504ResponseCode: 374 | Type: Number 375 | Description: "The HTTP status code that CloudFront returns to a viewer along with the custom error page for HTTP 504 errors" 376 | AllowedValues: 377 | - 200 378 | - 400 379 | - 403 380 | - 404 381 | - 405 382 | - 414 383 | - 500 384 | - 501 385 | - 502 386 | - 503 387 | - 504 388 | ConstraintDescription: "504ResponseCode must be a valid HTTP status code. Allowed values: [200, 400, 403, 404, 405, 414, 500, 501, 502, 503, 504]" 389 | Default: 504 390 | 504ResponsePagePath: 391 | Type: String 392 | Description: "The path to the custom error page that CloudFront returns to a viewer for HTTP 504 errors" 393 | AllowedPattern: "(?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 394 | ConstraintDescription: "504ResponsePagePath must be a valid path. Allowed pattern: (?:^/[a-zA-Z0-9_.*$&/~-]*$|^$)" 395 | Default: "" 396 | 504ErrorCachingMinTTL: 397 | Type: Number 398 | Description: "The minimum amount of time, in seconds, that Amazon CloudFront caches the HTTP status code HTTP 504 errors" 399 | MaxValue: 31536000 400 | MinValue: 0 401 | ConstraintDescription: "504ErrorCachingMinTTL must be a valid number. Allowed range: 0 - 31536000" 402 | Default: 300 403 | 404 | 405 | Conditions: 406 | NotHandling400: !Equals [ !Ref "400ResponsePagePath", "" ] 407 | NotHandling403: !Equals [ !Ref "403ResponsePagePath", "" ] 408 | NotHandling404: !Equals [ !Ref "404ResponsePagePath", "" ] 409 | NotHandling405: !Equals [ !Ref "405ResponsePagePath", "" ] 410 | NotHandling414: !Equals [ !Ref "414ResponsePagePath", "" ] 411 | NotHandling500: !Equals [ !Ref "500ResponsePagePath", "" ] 412 | NotHandling501: !Equals [ !Ref "501ResponsePagePath", "" ] 413 | NotHandling502: !Equals [ !Ref "502ResponsePagePath", "" ] 414 | NotHandling503: !Equals [ !Ref "503ResponsePagePath", "" ] 415 | NotHandling504: !Equals [ !Ref "504ResponsePagePath", "" ] 416 | 417 | Resources: 418 | CloudFrontDistribution: 419 | Type: "AWS::CloudFront::Distribution" 420 | Properties: 421 | DistributionConfig: 422 | Aliases: 423 | - !Sub "${Subdomain}.${Domain}" 424 | CustomErrorResponses: 425 | - !If 426 | - "NotHandling400" 427 | - !Ref "AWS::NoValue" 428 | - 429 | ErrorCode: 400 430 | ErrorCachingMinTTL: !Ref "400ErrorCachingMinTTL" 431 | ResponseCode: !Ref "400ResponseCode" 432 | ResponsePagePath: !Ref "400ResponsePagePath" 433 | - !If 434 | - "NotHandling403" 435 | - !Ref "AWS::NoValue" 436 | - 437 | ErrorCode: 403 438 | ErrorCachingMinTTL: !Ref "403ErrorCachingMinTTL" 439 | ResponseCode: !Ref "403ResponseCode" 440 | ResponsePagePath: !Ref "403ResponsePagePath" 441 | - !If 442 | - "NotHandling404" 443 | - !Ref "AWS::NoValue" 444 | - 445 | ErrorCode: 404 446 | ErrorCachingMinTTL: !Ref "404ErrorCachingMinTTL" 447 | ResponseCode: !Ref "404ResponseCode" 448 | ResponsePagePath: !Ref "404ResponsePagePath" 449 | - !If 450 | - "NotHandling405" 451 | - !Ref "AWS::NoValue" 452 | - 453 | ErrorCode: 405 454 | ErrorCachingMinTTL: !Ref "405ErrorCachingMinTTL" 455 | ResponseCode: !Ref "405ResponseCode" 456 | ResponsePagePath: !Ref "405ResponsePagePath" 457 | - !If 458 | - "NotHandling414" 459 | - !Ref "AWS::NoValue" 460 | - 461 | ErrorCode: 414 462 | ErrorCachingMinTTL: !Ref "414ErrorCachingMinTTL" 463 | ResponseCode: !Ref "414ResponseCode" 464 | ResponsePagePath: !Ref "414ResponsePagePath" 465 | - !If 466 | - "NotHandling500" 467 | - !Ref "AWS::NoValue" 468 | - 469 | ErrorCode: 500 470 | ErrorCachingMinTTL: !Ref "500ErrorCachingMinTTL" 471 | ResponseCode: !Ref "500ResponseCode" 472 | ResponsePagePath: !Ref "500ResponsePagePath" 473 | - !If 474 | - "NotHandling501" 475 | - !Ref "AWS::NoValue" 476 | - 477 | ErrorCode: 501 478 | ErrorCachingMinTTL: !Ref "501ErrorCachingMinTTL" 479 | ResponseCode: !Ref "501ResponseCode" 480 | ResponsePagePath: !Ref "501ResponsePagePath" 481 | - !If 482 | - "NotHandling502" 483 | - !Ref "AWS::NoValue" 484 | - 485 | ErrorCode: 502 486 | ErrorCachingMinTTL: !Ref "502ErrorCachingMinTTL" 487 | ResponseCode: !Ref "502ResponseCode" 488 | ResponsePagePath: !Ref "502ResponsePagePath" 489 | - !If 490 | - "NotHandling503" 491 | - !Ref "AWS::NoValue" 492 | - 493 | ErrorCode: 503 494 | ErrorCachingMinTTL: !Ref "503ErrorCachingMinTTL" 495 | ResponseCode: !Ref "503ResponseCode" 496 | ResponsePagePath: !Ref "503ResponsePagePath" 497 | - !If 498 | - "NotHandling504" 499 | - !Ref "AWS::NoValue" 500 | - 501 | ErrorCode: 504 502 | ErrorCachingMinTTL: !Ref "504ErrorCachingMinTTL" 503 | ResponseCode: !Ref "504ResponseCode" 504 | ResponsePagePath: !Ref "504ResponsePagePath" 505 | DefaultRootObject: !Ref "DefaultRootObject" 506 | Enabled: true 507 | HttpVersion: !Ref "HttpVersion" 508 | IPV6Enabled: !Ref "IPV6Enabled" 509 | PriceClass: !Ref "PriceClass" 510 | ViewerCertificate: 511 | AcmCertificateArn: !Ref "AcmCertificateArn" 512 | MinimumProtocolVersion: !Ref "MinimumProtocolVersion" 513 | SslSupportMethod: !Ref "SslSupportMethod" 514 | Origins: 515 | - 516 | Id: !Sub "${Subdomain}.${Domain}" 517 | DomainName: !Ref "OriginS3EndPoint" 518 | OriginPath: !Ref "OriginPath" 519 | S3OriginConfig: 520 | OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentityId}" 521 | DefaultCacheBehavior: 522 | TargetOriginId: !Sub "${Subdomain}.${Domain}" 523 | Compress: true 524 | MinTTL: !Ref "MinTTL" 525 | MaxTTL: !Ref "MaxTTL" 526 | DefaultTTL: !Ref "DefaultTTL" 527 | ForwardedValues: 528 | QueryString: false 529 | ViewerProtocolPolicy: "redirect-to-https" 530 | 531 | Outputs: 532 | CloudFrontDistributionDomainName: 533 | Value: !GetAtt ["CloudFrontDistribution", "DomainName"] 534 | Description: "CloudFront distribution domain name" 535 | -------------------------------------------------------------------------------- /bin/capsule.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | //############################################################################# 3 | // @licence: MIT 4 | // @description: Automated CLI for Static web application hosting on AWS 5 | //############################################################################# 6 | 7 | const fs = require("fs"); 8 | const commander = require("commander"); 9 | const chalk = require("chalk"); 10 | const aws = require("aws-sdk"); 11 | const path = require("path"); 12 | // This is how this module is supposed to be used, ignored the eslint warning 13 | // eslint-disable-next-line no-unused-vars 14 | const pkginfo = require("pkginfo")(module); 15 | const Spinner = require("cli-spinner").Spinner; 16 | const { prompt } = require("inquirer"); 17 | 18 | let cf; 19 | let s3; 20 | let cfr; 21 | 22 | //############################################################################# 23 | 24 | /* 25 | * Globals 26 | */ 27 | const { 28 | // AWS Access Key 29 | AWS_ACCESS_KEY_ID, 30 | // AWS secret key 31 | AWS_SECRET_ACCESS_KEY, 32 | // AWS profile name 33 | AWS_PROFILE, 34 | // AWS region - unused 35 | } = process.env; 36 | 37 | /* 38 | * References 39 | * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#w2ab2c15c15c17c11 40 | * 41 | */ 42 | const stack_states = [ 43 | "CREATE_COMPLETE", 44 | "CREATE_FAILED", 45 | "DELETE_COMPLETE", 46 | "DELETE_FAILED", 47 | "UPDATE_COMPLETE", 48 | "UPDATE_FAILED", 49 | "ROLLBACK_COMPLETE", 50 | "ROLLBACK_FAILED", 51 | "UPDATE_ROLLBACK_COMPLETE", 52 | "UPDATE_ROLLBACK_FAILED", 53 | "REVIEW_IN_PROGRESS", 54 | ]; 55 | 56 | const error_states = [ 57 | "CREATE_FAILED", 58 | "DELETE_FAILED", 59 | "UPDATE_FAILED", 60 | "ROLLBACK_FAILED", 61 | "UPDATE_ROLLBACK_FAILED", 62 | "UPDATE_ROLLBACK_IN_PROGRESS", 63 | "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", 64 | ]; 65 | 66 | const paths = { 67 | base: path.resolve(__dirname, "../"), 68 | ci_s3: "ci/s3_cloudformation.cf", 69 | ci: "ci/codebuild_capsule.cf", 70 | ci_project: "ci/codebuild_project.cf", 71 | cf_templates: "templates/child_templates/", 72 | web_template: "templates/template.yaml", 73 | base_config: "config/capsule_init_questions.json", 74 | ci_config: "config/capsule_init_ci_questions.json", 75 | aws_url: "https://s3.amazonaws.com/", 76 | output_config: "capsule.json", 77 | }; 78 | 79 | const projectParameters = { 80 | site_config_params: {}, 81 | site_config_file: {}, 82 | site_config: {}, 83 | }; 84 | 85 | let last_time = new Date(new Date() - 1000); 86 | 87 | /** 88 | * Takes a JSON config file, opens its, reads the contents 89 | * passes it, and returns a JS object. 90 | * 91 | * @method getJsonFile 92 | * 93 | * @param {String} path 94 | * 95 | * @return {Object} data 96 | */ 97 | const getJsonFile = path => { 98 | return new Promise((resolve, reject) => { 99 | fs.readFile(path, "utf8", (err, data) => { 100 | if (err) reject(err); 101 | else resolve(JSON.parse(data)); 102 | }); 103 | }); 104 | }; 105 | 106 | /** 107 | * Pass in a config file in JSON format 108 | * process and return an object 109 | * 110 | * @method parseJsonConfig 111 | * 112 | * @param {String} site_config_file 113 | * 114 | * @return {Object} 115 | */ 116 | const parseJsonConfig = async site_config_file => getJsonFile(site_config_file); 117 | 118 | /** 119 | * 120 | * Write config file out by taking the 121 | * JSOn object and stringifying it 122 | * 123 | * @method writeConfigFile 124 | * 125 | * @param {Object} answers 126 | * 127 | * @return {void} 128 | */ 129 | const writeConfigFile = answers => { 130 | fs.writeFile(paths.output_config, JSON.stringify(answers), function(err) { 131 | if (err) { 132 | logIfVerbose(`${err}`); 133 | } 134 | }); 135 | }; 136 | 137 | commander 138 | .version(module.exports.version) 139 | .option("-v, --verbose", "verbose output"); 140 | 141 | //TODO - move code for assigning vars into a separate function 142 | //to allow code re-use. 143 | commander 144 | .command("init") 145 | .description("Define the project parameters") 146 | .action(async function() { 147 | commander.type = "init"; 148 | const generic_questions = await parseJsonConfig( 149 | path.resolve(paths.base, paths.base_config) 150 | ); 151 | const ci_questions = await parseJsonConfig( 152 | path.resolve(paths.base, paths.ci_config) 153 | ); 154 | console.log("Executing project initialization"); 155 | let combined_answers = {}; 156 | await prompt(generic_questions).then(answers => { 157 | combined_answers = answers; 158 | }); 159 | await prompt(ci_questions).then(answers => { 160 | combined_answers.ci = answers; 161 | }); 162 | writeConfigFile(combined_answers); 163 | }); 164 | 165 | commander 166 | .command("deploy") 167 | .description("Builds out the web hosting infrastructure in one go") 168 | .action(function(options) { 169 | console.log(chalk.bgRed.bold("DO NOT CANCEL THIS PROCESS UNTIL COMPLETED")); 170 | console.log("Executing project deployment"); 171 | commander.type = options._name || undefined; 172 | }); 173 | 174 | commander 175 | .command("remove") 176 | .description( 177 | "Removes the whole of the web hosting infrastructure, including files in S3 buckets" 178 | ) 179 | .option( 180 | "-n, --project-name ", 181 | "Push cf templates to the s3 bucket, and creates it if it does not exist" 182 | ) 183 | .option( 184 | "-d, --dom ", 185 | "The name of the static website domain being created from the cf templates" 186 | ) 187 | .option( 188 | "-s, --subdom ", 189 | "The name of the static website subdomain being created from the cf templates" 190 | ) 191 | .option( 192 | "-c, --config ", 193 | "Load the configuration from the specified path" 194 | ) 195 | .option("-p, --aws-profile ", "The AWS profile to use") 196 | .action(function(options) { 197 | console.log("Executing project removal"); 198 | commander.type = options._name || undefined; 199 | commander.projectName = options.projectName || undefined; 200 | commander.config = options.config || undefined; 201 | commander.awsProfile = options.awsProfile || undefined; 202 | commander.dom = options.dom || undefined; 203 | commander.subdom = options.subdom || undefined; 204 | }); 205 | 206 | // The following commands are the mroe granular ones, that allow step by step deployment 207 | // of the web hosting infrastructure 208 | commander 209 | .command("create ") 210 | .description( 211 | "Initializes the s3 bucket required to store nested stack templates takes: s3, ci or web" 212 | ) 213 | .option( 214 | "-n, --project-name ", 215 | "Push cf templates to the s3 bucket, and creates it if it does not exist" 216 | ) 217 | .option( 218 | "-d, --dom ", 219 | "The name of the static website domain being created from the cf templates" 220 | ) 221 | .option( 222 | "-s, --subdom ", 223 | "The name of the static website subdomain being created from the cf templates" 224 | ) 225 | .option( 226 | "-c, --config ", 227 | "Load the configuration from the specified path" 228 | ) 229 | .option("-p, --aws-profile ", "The AWS profile to use") 230 | .option("-u, --url ", "The source control URL to use") 231 | .option( 232 | "-t, --ci_project_path ", 233 | "CodeBuild project relative path" 234 | ) 235 | .option( 236 | "-j, --site_config ", 237 | "A JSON object contianing site configuration, overrides values defined in site config file" 238 | ) 239 | .option( 240 | "-f, --site_config_file ", 241 | "Custom configuration file used in CodeBuild for building the static site" 242 | ) 243 | .action(function(type, options) { 244 | console.log("Executing create for: " + type); 245 | commander.type = options._name || undefined; 246 | commander.projectName = options.projectName || undefined; 247 | commander.config = options.config || undefined; 248 | commander.awsProfile = options.awsProfile || undefined; 249 | commander.dom = options.dom || undefined; 250 | commander.subdom = options.subdom || undefined; 251 | commander.url = options.url || undefined; 252 | commander.site_config = options.site_config || {}; 253 | commander.site_config_file = options.site_config_file || undefined; 254 | commander.ci_project_path = options.ci_project_path || undefined; 255 | }); 256 | 257 | commander 258 | .command("update ") 259 | .description( 260 | "Updates the templates into the s3 bucket and runs the nested stack takes: s3, ci or web" 261 | ) 262 | .option( 263 | "-n, --project-name ", 264 | "Push cf templates to the s3 bucket, and creates it if it does not exist" 265 | ) 266 | .option( 267 | "-d, --dom ", 268 | "The name of the static website domain being created from the cf templates" 269 | ) 270 | .option( 271 | "-s, --subdom ", 272 | "The name of the static website subdomain being created from the cf templates" 273 | ) 274 | .option( 275 | "-c, --config ", 276 | "Load the configuration from the specified path" 277 | ) 278 | .option("-p, --aws-profile ", "The AWS profile to use") 279 | .option("-u, --url ", "The source control URL to use") 280 | .option( 281 | "-t, --ci_project_path ", 282 | "CodeBuild project relative path" 283 | ) 284 | .option( 285 | "-j, --site_config ", 286 | "A JSON object contianing site configuration, overrides values defined in site config file" 287 | ) 288 | .option( 289 | "-f, --site_config_file ", 290 | "Custom configuration file used in CodeBuild for building the static site" 291 | ) 292 | .action(function(type, options) { 293 | console.log("Executing update for: " + type); 294 | commander.type = options._name || undefined; 295 | commander.projectName = options.projectName || undefined; 296 | commander.config = options.config || undefined; 297 | commander.awsProfile = options.awsProfile || undefined; 298 | commander.dom = options.dom || undefined; 299 | commander.subdom = options.subdom || undefined; 300 | commander.url = options.url || undefined; 301 | commander.site_config = options.site_config || {}; 302 | commander.site_config_file = options.site_config_file || undefined; 303 | commander.ci_project_path = options.ci_project_path || undefined; 304 | }); 305 | 306 | commander 307 | .command("delete ") 308 | .description("Deletes the s3 bucket contents takes: s3, ci or web") 309 | .option( 310 | "-n, --project-name ", 311 | "Push cf templates to the s3 bucket, and creates it if it does not exist" 312 | ) 313 | .option( 314 | "-d, --dom ", 315 | "The name of the static website domain being created from the cf templates" 316 | ) 317 | .option( 318 | "-s, --subdom ", 319 | "The name of the static website subdomain being created from the cf templates" 320 | ) 321 | .option( 322 | "-c, --config ", 323 | "Load the configuration from the specified path" 324 | ) 325 | .option("-p, --aws-profile ", "The AWS profile to use") 326 | .action(function(type, options) { 327 | console.log("Executing delete for: " + type); 328 | commander.type = options._name || undefined; 329 | commander.projectName = options.projectName || undefined; 330 | commander.config = options.config || undefined; 331 | commander.awsProfile = options.awsProfile || undefined; 332 | commander.dom = options.dom || undefined; 333 | commander.subdom = options.subdom || undefined; 334 | }); 335 | 336 | commander.parse(process.argv); 337 | 338 | // Helpers #################################################################### 339 | 340 | const logIfVerbose = (str, error) => { 341 | if (commander.verbose) { 342 | if (error) { 343 | console.error(`ERROR: ${str}`); 344 | } else { 345 | console.log(chalk.bgGreen(`INFO: ${str}`)); 346 | } 347 | } 348 | }; 349 | 350 | const printError = str => console.error(chalk.red(`ERROR: ${str}`)); 351 | 352 | const printErrorAndDie = (str, showHelp) => { 353 | printError(str); 354 | if (showHelp) commander.help(); 355 | process.exit(1); 356 | }; 357 | 358 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 359 | 360 | const getRandomToken = () => Math.floor(Math.random() * 89999) + 10000; 361 | 362 | // File Helpers ############################################################### 363 | 364 | /** 365 | * TODO: This may require to get it from github directly to avoid packing it 366 | * 367 | * @method getTemplateBody 368 | * 369 | * @param {String} path 370 | * 371 | * @return {String} data 372 | */ 373 | const getTemplateBody = path => { 374 | return new Promise((resolve, reject) => { 375 | fs.readFile(path, "utf8", (err, data) => { 376 | if (err) reject(err); 377 | else resolve(data); 378 | }); 379 | }); 380 | }; 381 | 382 | /** 383 | * get the S3 file 384 | * then re-use the existing functions to 385 | * build the stack 386 | * 387 | * @method getCiS3Template 388 | * 389 | * @param {String} 390 | * 391 | * @return {String} 392 | */ 393 | const getCiS3Template = () => 394 | getTemplateBody(path.resolve(paths.base, paths.ci_s3)); 395 | 396 | /** 397 | * Get the codebuild file 398 | * then re-use the existing functions to 399 | * build the stack. 400 | * 401 | * By default we use the ci_project path, alternative relative path can be supplied through CLI params. 402 | * 403 | * 404 | * @method getCiTemplate 405 | * 406 | * @param {String} 407 | * 408 | * @return {String} 409 | */ 410 | const getCiTemplate = ciProjectPath => getTemplateBody(ciProjectPath); 411 | 412 | /** 413 | * Get the template.yml file 414 | * then re-use the existing functions to 415 | * build the stack 416 | * 417 | * @method getWebTemplate 418 | * 419 | * @param {String} 420 | * 421 | * @return {String} 422 | */ 423 | const getWebTemplate = async () => 424 | getTemplateBody(path.resolve(paths.base, paths.web_template)); 425 | 426 | // AWS Helpers ################################################################ 427 | 428 | /** 429 | * Environment variables should have higher precedence 430 | * Load the aws libraries with authentication already set 431 | * 432 | * Reference: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html 433 | * Reference: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html 434 | * Reference: https://github.com/aws/aws-sdk-js/pull/1391 435 | * Reference: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-json-file.html 436 | * 437 | * @method loadAWSConfiguration 438 | * 439 | * @param {String} config_path 440 | * @param {Object} aws_profile 441 | * 442 | * @return {void} 443 | * 444 | */ 445 | const loadAWSConfiguration = async (config_path, aws_profile) => { 446 | if ((AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) || AWS_PROFILE) { 447 | process.env.AWS_SDK_LOAD_CONFIG = "1"; 448 | aws.config.credentials = new aws.SharedIniFileCredentials(); 449 | } 450 | if (aws_profile) { 451 | process.env.AWS_SDK_LOAD_CONFIG = "1"; 452 | aws.config.credentials = new aws.SharedIniFileCredentials({ 453 | profile: aws_profile, 454 | }); 455 | } else if (config_path) { 456 | aws.config.loadFromPath(config_path); 457 | } else { 458 | process.env.AWS_SDK_LOAD_CONFIG = "1"; 459 | aws.config.credentials = new aws.SharedIniFileCredentials(); 460 | } 461 | 462 | cf = new aws.CloudFormation(); 463 | s3 = new aws.S3(); 464 | cfr = new aws.CloudFront(); 465 | }; 466 | 467 | // AWS CF Helpers ############################################################# 468 | 469 | /** 470 | * Given the cloud formation stack state, it returns the color red if it is a 471 | * failure state, green if it is a success state, and yellow otherwise. 472 | * 473 | * @method getStackEventColor 474 | * 475 | * @param {String} state 476 | * 477 | * @return {String} color 478 | */ 479 | const getStackEventColor = state => { 480 | switch (true) { 481 | case error_states.includes(state): 482 | return "red"; 483 | case stack_states.includes(state): 484 | return "green"; 485 | default: 486 | return "yellow"; 487 | } 488 | }; 489 | 490 | /** 491 | * Given the cloud formation stack event, it returns a string with a single 492 | * line description for it. 493 | * 494 | * @method getStackEventColor 495 | * 496 | * @param {String} event 497 | * 498 | * @return {String} output_line 499 | */ 500 | const printStackEventOutputLine = e => { 501 | let time = `${e.Timestamp.toLocaleString()}`; 502 | let status = `${chalk[getStackEventColor(e.ResourceStatus)]( 503 | e.ResourceStatus 504 | )}`; 505 | let resource = `${e.ResourceType}`; 506 | let id = `${e.PhysicalResourceId}`; 507 | console.log(`${time} ${status} ${resource} ${id}`); 508 | }; 509 | 510 | /** 511 | * Given an object of key->value, it will return the list of parameters in the 512 | * format expected by AWS. 513 | * Reference: 514 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#createStack-property 515 | * 516 | * @method getFormattedParameters 517 | * 518 | * @param {Object} parameters 519 | * 520 | * @return {Object} formated_parameters 521 | */ 522 | const getFormattedParameters = parameters => { 523 | let formated_parameters = []; 524 | for (let p in parameters) { 525 | formated_parameters.push({ 526 | ParameterKey: p, 527 | ParameterValue: parameters[p], 528 | }); 529 | } 530 | return formated_parameters; 531 | }; 532 | 533 | /** 534 | * Given the name of the stack, a string with the template body to apply, an 535 | * object with the stack parameters, and a token, it starts the CF stack 536 | * creation request identifed by the token. 537 | * Reference: 538 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#createStack-property 539 | * 540 | * @method createCFStack 541 | * 542 | * @param {String} name 543 | * @param {String} template_body 544 | * @param {Object} parameters 545 | * @param {Object} token 546 | * 547 | * @return {Object} 548 | * 549 | */ 550 | const createCFStack = async (name, template_body, parameters, token) => { 551 | return new Promise((resolve, reject) => { 552 | cf.createStack( 553 | { 554 | StackName: name, 555 | ClientRequestToken: token, 556 | Parameters: getFormattedParameters(parameters), 557 | Tags: [ 558 | { Key: "name", Value: name }, 559 | { Key: "provisioner", Value: "capsule" }, 560 | ], 561 | Capabilities: ["CAPABILITY_IAM"], 562 | TemplateBody: template_body, 563 | }, 564 | (err, data) => { 565 | if (err) reject(err); 566 | else resolve(data); 567 | } 568 | ); 569 | }); 570 | }; 571 | 572 | /** 573 | * Given the name of the stack, a string with the template body to apply, an 574 | * object with the stack parameters, and a token, it starts the CF stack 575 | * update request identifed by the token. 576 | * Reference: 577 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#createStack-property 578 | * 579 | * @method updateCFStack 580 | * 581 | * @param {String} name 582 | * @param {String} template_body 583 | * @param {Object} parameters 584 | * @param {Object} token 585 | * 586 | * @return {Object} 587 | * 588 | */ 589 | const updateCFStack = async (name, template_body, parameters, token) => { 590 | return new Promise((resolve, reject) => { 591 | cf.updateStack( 592 | { 593 | StackName: name, 594 | ClientRequestToken: token, 595 | Parameters: getFormattedParameters(parameters), 596 | Tags: [ 597 | { Key: "name", Value: name }, 598 | { Key: "provisioner", Value: "capsule" }, 599 | ], 600 | Capabilities: ["CAPABILITY_IAM"], 601 | TemplateBody: template_body, 602 | }, 603 | (err, data) => { 604 | if (err) reject(err); 605 | else resolve(data); 606 | } 607 | ); 608 | }); 609 | }; 610 | 611 | /** 612 | * References: 613 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#describeStackEvents-property 614 | * 615 | * @method describeStack 616 | * 617 | * @param {String} StackName 618 | * 619 | * @return {Object} data 620 | * 621 | */ 622 | const describeStack = async StackName => { 623 | return new Promise((resolve, reject) => { 624 | cf.describeStacks({ StackName }, (err, data) => { 625 | if (err) reject(err); 626 | else resolve(data); 627 | }); 628 | }); 629 | }; 630 | 631 | /** 632 | * Given the id and name of the stack,and a token, it starts the CF stack 633 | * delete request identifed by the token. 634 | * References: 635 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#deleteStack-property 636 | * 637 | * @method deleteCFStack 638 | * 639 | * @param {String} id 640 | * @param {String} name 641 | * @param {Object} token 642 | * 643 | * @return {Object} data 644 | * 645 | */ 646 | const deleteCFStack = async (id, name, token) => { 647 | return new Promise((resolve, reject) => { 648 | cf.deleteStack( 649 | { 650 | StackName: id, 651 | ClientRequestToken: token, 652 | }, 653 | (err, data) => { 654 | if (err) reject(err); 655 | else resolve(data); 656 | } 657 | ); 658 | }); 659 | }; 660 | 661 | /** 662 | * Given the stack name it returns the stack details if exists, if not it 663 | * returns false. 664 | * 665 | * @method getStackIfExists 666 | * 667 | * @param {String} name 668 | * 669 | * @return {Boolean} 670 | * @return {Object} Stack 671 | */ 672 | const getStackIfExists = async name => { 673 | try { 674 | let { Stacks } = await describeStack(name); 675 | return Stacks[0]; 676 | } catch (e) { 677 | logIfVerbose(e.message, true); 678 | if (e.message.match("(.*)" + name + "(.*)does not exist(.*)")) { 679 | return false; 680 | } 681 | throw e; 682 | } 683 | }; 684 | 685 | /** 686 | * References: 687 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#describeStackEvents-property 688 | * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-listing-event-history.html 689 | * 690 | * @method getNextStackEvent 691 | * 692 | * @param {String} id 693 | * @param {Object} next 694 | * 695 | * @return {Object} data 696 | * 697 | */ 698 | const getNextStackEvent = async (id, next) => { 699 | return new Promise((resolve, reject) => { 700 | cf.describeStackEvents( 701 | { 702 | StackName: id, 703 | NextToken: next, 704 | }, 705 | (err, data) => { 706 | if (err) reject(err); 707 | else resolve(data); 708 | } 709 | ); 710 | }); 711 | }; 712 | 713 | /** 714 | * Given the Stack id, it returns the list of events of the stack and nested 715 | * stacks. 716 | * 717 | * @method getStackEvents 718 | * 719 | * @param {String} id 720 | * 721 | * @return {Array} list 722 | */ 723 | const getStackEvents = async id => { 724 | let response = await getNextStackEvent(id); 725 | let events = response.StackEvents; 726 | 727 | while (typeof response.NextToken !== "undefined") { 728 | response = await getNextStackEvent(id, response.NextToken); 729 | events.concat(response.StackEvents); 730 | } 731 | 732 | let nestedStackIds = events.reduce((list, e) => { 733 | let physical_resource_id = e.PhysicalResourceId; 734 | if ( 735 | e.ResourceType === "AWS::CloudFormation::Stack" && 736 | physical_resource_id != "" && 737 | e.StackId != physical_resource_id && 738 | !list.includes(physical_resource_id) 739 | ) { 740 | list.push(physical_resource_id); 741 | } 742 | return list; 743 | }, []); 744 | 745 | for (id of nestedStackIds) events.concat(await getStackEvents(id)); 746 | return events.sort((e1, e2) => e1.Timestamp - e2.Timestamp); 747 | }; 748 | 749 | /** 750 | * Given the stack id and the token that identifies the stack change request, 751 | * it prints the events filtered by the id and the token, and it ensures the 752 | * events are always new. 753 | * The monitoring will finish when an event with status is one of the final 754 | * status from AWS. 755 | * See: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#w2ab2c15c15c17c11 756 | * 757 | * @method monitorStackProgress 758 | * 759 | * @param {String} id 760 | * @param {Object} token 761 | * 762 | * @return {void} 763 | * 764 | */ 765 | const monitorStackProgress = async (id, token) => { 766 | let in_progress = true; 767 | let events_seen = []; 768 | let spinner = new Spinner(); 769 | spinner.setSpinnerString("|/-\\"); 770 | logIfVerbose(`Start monitoring stack ${id}`); 771 | spinner.start(); 772 | while (in_progress) { 773 | let events; 774 | try { 775 | events = await getStackEvents(id); 776 | } catch (error) { 777 | logIfVerbose(`Can't get stack events: ${error}`); 778 | } 779 | 780 | if (events === undefined) { 781 | logIfVerbose(`No new Events: In progress`); 782 | continue; 783 | } 784 | 785 | for (const event of events) { 786 | if ( 787 | event.Timestamp < last_time || 788 | events_seen.includes(event.EventId) || 789 | (token && event.ClientRequestToken !== token) 790 | ) { 791 | logIfVerbose(`Event ignored: ${event.EventId}`); 792 | } else { 793 | logIfVerbose(`NEW Event: ${event}`); 794 | printStackEventOutputLine(event); 795 | events_seen.push(event.EventId); 796 | } 797 | if ( 798 | event.ResourceType === "AWS::CloudFormation::Stack" && 799 | event.StackId === id && 800 | event.PhysicalResourceId === id && 801 | stack_states.includes(event.ResourceStatus) && 802 | (!token || (token && event.ClientRequestToken === token)) && 803 | event.Timestamp > last_time 804 | ) { 805 | in_progress = false; 806 | } 807 | last_time = event.Timestamp; 808 | } 809 | await delay(1000); 810 | } 811 | spinner.stop(); 812 | process.stdout.write("\n"); 813 | logIfVerbose(`End monitoring stack ${id} with token ${token}`); 814 | }; 815 | 816 | /** 817 | * Given the stack name, the stack template in string format, and its 818 | * parameters. It creates the stack and monitors it by polling for the stack 819 | * events and printing it in stdout. 820 | * When creating the initial S3 bucket to store the CF templates, it uses the 821 | * s3_cloudformation.cf templates from the local file system. 822 | * When building out the stack where the static website will be hosted 823 | * it uses the bucket created from the s3_cloudformation.cf file, and looks 824 | * in this for the template.yml file. 825 | * 826 | * @method createStack 827 | * 828 | * @param {String} name 829 | * @param {String} templateBody 830 | * @param {Object} parameters 831 | * 832 | * @return {void} 833 | */ 834 | const createStack = async (name, templateBody, parameters) => { 835 | let token = `${name}-create-` + getRandomToken(); 836 | let { StackId } = await createCFStack(name, templateBody, parameters, token); 837 | await monitorStackProgress(StackId, token); 838 | }; 839 | 840 | /** 841 | * Given the stack name, the stack template in string format, and its 842 | * parameters. It updates the stack and monitors it by polling for the stack 843 | * events and printing it in stdout. 844 | * 845 | * @method updateStack 846 | * 847 | * @param {String} name 848 | * @param {String} templateBody 849 | * @param {Object} parameters 850 | * 851 | * @return {void} 852 | */ 853 | const updateStack = async (name, templateBody, parameters) => { 854 | let stack = await getStackIfExists(name); 855 | if (stack.StackId) { 856 | let StackId = stack.StackId; 857 | let token = `${name}-update-` + getRandomToken(); 858 | await updateCFStack(name, templateBody, parameters, token); 859 | await monitorStackProgress(StackId, token); 860 | } 861 | }; 862 | 863 | /** 864 | * Given the stack name, it deletes the stack and monitors it by polling for 865 | * the stack events and printing it in stdout. 866 | * 867 | * @method deleteStack 868 | * 869 | * @param {String} name 870 | * 871 | * @return {void} 872 | */ 873 | const deleteStack = async name => { 874 | let { StackId } = await getStackIfExists(name); 875 | if (StackId) { 876 | let token = `${name}-delete-` + getRandomToken(); 877 | await deleteCFStack(StackId, name, token); 878 | await monitorStackProgress(StackId, token); 879 | } 880 | }; 881 | 882 | // AWS S3 Helpers ############################################################# 883 | 884 | /** 885 | * List the files in an S3 bucket. 886 | * 887 | * Reference: 888 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#listObjectsV2-property 889 | * 890 | * @method listS3BucketObjects 891 | * 892 | * @param {String} name 893 | * 894 | * @return {void} 895 | */ 896 | const listS3BucketObjects = async name => { 897 | return new Promise((resolve, reject) => { 898 | s3.listObjectsV2( 899 | { 900 | Bucket: name, 901 | }, 902 | (err, data) => { 903 | if (err) reject(err); 904 | else resolve(data); 905 | } 906 | ); 907 | }); 908 | }; 909 | 910 | /** 911 | * Given an s3 bucket, it removes all its content. This is required by CF in 912 | * order to remove an s3 bucket. 913 | * Reference: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#deleteObjects-property 914 | * 915 | * @method clearS3Bucket 916 | * 917 | * @param {String} name 918 | * 919 | * @return {Object} Objects 920 | * @return {Object} data 921 | */ 922 | const clearS3Bucket = async name => { 923 | try { 924 | let { Contents } = await listS3BucketObjects(name); 925 | if (Contents.length) { 926 | let Objects = Contents.map(obj => { 927 | return { Key: obj.Key }; 928 | }); 929 | return new Promise((resolve, reject) => { 930 | s3.deleteObjects( 931 | { 932 | Bucket: name, 933 | Delete: { Objects }, 934 | }, 935 | (err, data) => { 936 | if (err) reject(err); 937 | else resolve(data); 938 | } 939 | ); 940 | }); 941 | } 942 | } catch (e) { 943 | logIfVerbose(e.message, true); 944 | if (e.message.match("(.*)does not exist(.*)")) return; 945 | throw e; 946 | } 947 | }; 948 | 949 | /** 950 | * Given the name of the project, it creates the s3 bucket used for storing the 951 | * CF templates for nested CF Stacks. 952 | * 953 | * @method createS3Bucket 954 | * 955 | * @param {String} projectName 956 | * 957 | * @return {void} 958 | */ 959 | const createS3Bucket = async projectName => { 960 | await createStack(projectName, await getCiS3Template(), { 961 | ProjectName: projectName, 962 | }); 963 | }; 964 | 965 | /** 966 | * Given the name of the project, it updates the s3 bucket used for storing the 967 | * CF templates for nested CF Stacks. Given that the bucket may require to be 968 | * re-created, it will clean the bucket. 969 | * 970 | * @method updateS3Bucket 971 | * 972 | * @param {String} projectName 973 | * @param {String} bucketName 974 | * 975 | * @return {void} 976 | */ 977 | const updateS3Bucket = async (projectName, bucketName) => { 978 | await clearS3Bucket(bucketName); 979 | await updateStack(projectName, await getCiS3Template(), { 980 | ProjectName: projectName, 981 | }); 982 | }; 983 | 984 | /** 985 | * Given the name of the project, it removes the CF templates stored in the s3 986 | * bucket used for the CI. And finally removes the CI s3 bucket. 987 | * 988 | * @method deleteS3Bucket 989 | * 990 | * @param {String} projectName 991 | * @param {String} bucketName 992 | * 993 | * @return {void} 994 | */ 995 | const deleteS3Bucket = async (projectName, bucketName) => { 996 | await clearS3Bucket(bucketName); 997 | await deleteStack(projectName); 998 | }; 999 | 1000 | /** 1001 | * A method to add files to an S3 bucket 1002 | * 1003 | * @method addFilesToS3Bucket 1004 | * 1005 | * @param {String} projectName 1006 | * @param {String} bucketName 1007 | * 1008 | * @return {void} 1009 | */ 1010 | const addFilesToS3Bucket = async (projectName, bucketName) => { 1011 | const templates_path = path.resolve(paths.base, paths.cf_templates); 1012 | fs.readdir(templates_path, (err, files) => { 1013 | if (!files || files.length === 0) { 1014 | logIfVerbose(`Templates folder is missing`); 1015 | return; 1016 | } 1017 | for (const file of files) { 1018 | const file_path = path.join(templates_path, file); 1019 | if (fs.lstatSync(file_path).isDirectory()) { 1020 | continue; 1021 | } 1022 | fs.readFile(file_path, (error, file_content) => { 1023 | if (error) { 1024 | throw error; 1025 | } 1026 | s3.putObject( 1027 | { 1028 | Bucket: bucketName, 1029 | Key: file, 1030 | Body: file_content, 1031 | }, 1032 | () => { 1033 | logIfVerbose( 1034 | `Successfully uploaded '${file}' to '${bucketName}' for project '${projectName}' !` 1035 | ); 1036 | } 1037 | ); 1038 | }); 1039 | } 1040 | }); 1041 | }; 1042 | 1043 | /** 1044 | * Get the CloudFront distribution ID 1045 | * and return the value 1046 | * 1047 | * @method getCloudFrontDistID 1048 | * 1049 | * @param {String} bucketName 1050 | * 1051 | * @return {String} distId 1052 | * 1053 | */ 1054 | const getCloudFrontDistID = async bucketName => { 1055 | return new Promise((resolve, reject) => { 1056 | cfr.listDistributions({}, function(err, data) { 1057 | if (err) { 1058 | logIfVerbose(`${err} , ${err.stack}`); 1059 | reject(err); 1060 | } else { 1061 | resolve(extractDistId(data, bucketName)); 1062 | } 1063 | }); 1064 | }); 1065 | }; 1066 | 1067 | /** 1068 | * Search through object and get id 1069 | * of CloudFront distribtuion associated 1070 | * with the S3 bucket. 1071 | * 1072 | * @method extractDistId 1073 | * 1074 | * @param {Object} data 1075 | * @param {String} bucketName 1076 | * 1077 | * @return {String} distId 1078 | * 1079 | */ 1080 | const extractDistId = (data, bucketName) => { 1081 | return new Promise((resolve, reject) => { 1082 | for (var i in data.DistributionList.Items) { 1083 | for (var id in data.DistributionList.Items[i].Origins.Items) { 1084 | if ( 1085 | data.DistributionList.Items[i].Origins.Items[id].Id === bucketName 1086 | ) { 1087 | resolve(data.DistributionList.Items[i].Id); 1088 | } 1089 | } 1090 | } 1091 | reject(undefined); 1092 | }); 1093 | }; 1094 | 1095 | /** 1096 | * Given the name of the project where the cf templates are stored, 1097 | * grab the scripts from the s3 bucket with that name and spin 1098 | * up the web infrastructure. 1099 | * 1100 | * TODO: paramters should be passed through as a single object for 1101 | * createStack. 1102 | * 1103 | * @method createWebStack 1104 | * 1105 | * @param {String} s3BucketName 1106 | * @param {String} webProjectName 1107 | * @param {String} subdomain 1108 | * @param {String} domain 1109 | * 1110 | * @return {void} 1111 | */ 1112 | const createWebStack = async ( 1113 | s3BucketName, 1114 | webProjectName, 1115 | subdomain, 1116 | domain 1117 | ) => { 1118 | await createStack(webProjectName, await getWebTemplate(), { 1119 | TemplatesDirectoryUrl: paths.aws_url + s3BucketName, 1120 | Domain: domain, 1121 | Subdomain: subdomain, 1122 | }); 1123 | }; 1124 | 1125 | /** 1126 | * Given the name of the project, it updates the target projects stack 1127 | * and updates it.. 1128 | * 1129 | * TODO: paramters should be passed through as a single object for 1130 | * createStack. 1131 | * 1132 | * @method updateWebStack 1133 | * 1134 | * @param {String} webProjectName 1135 | * 1136 | * @return {void} 1137 | */ 1138 | const updateWebStack = async ( 1139 | s3BucketName, 1140 | webProjectName, 1141 | subdomain, 1142 | domain 1143 | ) => { 1144 | await updateStack(webProjectName, await getWebTemplate(), { 1145 | TemplatesDirectoryUrl: paths.aws_url + s3BucketName, 1146 | Domain: domain, 1147 | Subdomain: subdomain, 1148 | }); 1149 | }; 1150 | 1151 | /** 1152 | * Given the name of the project, it removes the web stack. 1153 | * 1154 | * @method deleteWebStack 1155 | * 1156 | * @param {String} webProjectName 1157 | * 1158 | * @return {void} 1159 | */ 1160 | const deleteWebStack = async webProjectName => { 1161 | await deleteStack(webProjectName); 1162 | }; 1163 | 1164 | /** 1165 | * Given the name of the project, it runs the codebuild 1166 | * template which in turn checks the code out from 1167 | * the repository, install, tests and buulds it 1168 | * Finally the code is pushed to the S3 bucket defined by 1169 | * the subdomain and domain. 1170 | * 1171 | * @method createCiStack 1172 | * 1173 | * @param {String} ciprojectName 1174 | * @param {Object} site_config 1175 | * @param {String} ciProjectPath 1176 | * 1177 | * @return {void} 1178 | */ 1179 | const createCiStack = async (ciprojectName, site_config, ciProjectPath) => { 1180 | await createStack( 1181 | ciprojectName, 1182 | await getCiTemplate(ciProjectPath), 1183 | site_config 1184 | ); 1185 | }; 1186 | 1187 | /** 1188 | * Given the name of the project, it updates the target projects stack 1189 | * CF templates for codebuild. 1190 | * 1191 | * @method updateCiStack 1192 | * 1193 | * @param {String} ciprojectName 1194 | * @param {Object} site_config 1195 | * @param {String} ciProjectPath 1196 | * 1197 | * @return {void} 1198 | */ 1199 | const updateCiStack = async (ciprojectName, site_config, ciProjectPath) => { 1200 | await updateStack( 1201 | ciprojectName, 1202 | await getCiTemplate(ciProjectPath), 1203 | site_config 1204 | ); 1205 | }; 1206 | 1207 | /** 1208 | * Given the name of the project, it removes the CI process. . 1209 | * 1210 | * @method deleteCiStack 1211 | * 1212 | * @param {String} ciprojectName 1213 | * 1214 | * @return {void} 1215 | */ 1216 | const deleteCiStack = async (ciprojectName, bucketName) => { 1217 | await clearS3Bucket(bucketName); 1218 | await deleteStack(ciprojectName); 1219 | }; 1220 | 1221 | /** 1222 | * Handle S3 bucket commands. 1223 | * The S3 bucket contains the CF templates 1224 | * that are used by the web commands to 1225 | * built out the static hosting site. 1226 | * 1227 | * @method s3Cmnds 1228 | * 1229 | * @return {void} 1230 | */ 1231 | const s3Cmds = async type => { 1232 | const projectName = projectParameters.site_config["ProjectName"]; 1233 | const bucketName = projectParameters.site_config["S3BucketName"]; 1234 | 1235 | if (type === "create") { 1236 | await createS3Bucket(projectName); 1237 | logIfVerbose(`Uploading files....`); 1238 | await addFilesToS3Bucket(projectName, bucketName); 1239 | } 1240 | 1241 | if (type === "update") { 1242 | await updateS3Bucket(projectName, bucketName); 1243 | } 1244 | 1245 | if (type === "delete") { 1246 | await deleteS3Bucket(projectName, bucketName); 1247 | } 1248 | }; 1249 | 1250 | /** 1251 | * Handle web commands. 1252 | * These take the CF scripts from the S3 bucket 1253 | * and spin up the web hosting infrastructure 1254 | * for the static site. 1255 | * 1256 | * @method webCmds 1257 | * 1258 | * @param {String} cmd 1259 | * 1260 | * @return {void} 1261 | */ 1262 | const webCmds = async type => { 1263 | const s3BucketName = projectParameters.site_config["S3BucketName"]; 1264 | const webProjectName = projectParameters.site_config["WebProjectName"]; 1265 | const subDomain = projectParameters.site_config["SubDomain"]; 1266 | const domain = projectParameters.site_config["Domain"]; 1267 | 1268 | if (!domain) { 1269 | printErrorAndDie("Website domain name is required!", true); 1270 | } 1271 | 1272 | if (type === "create") { 1273 | await createWebStack(s3BucketName, webProjectName, subDomain, domain); 1274 | } 1275 | 1276 | if (type === "update") { 1277 | await updateWebStack(s3BucketName, webProjectName, subDomain, domain); 1278 | } 1279 | 1280 | if (type === "delete") { 1281 | await deleteWebStack(webProjectName); 1282 | } 1283 | }; 1284 | 1285 | /** 1286 | * Handle continuous integration stack build out 1287 | * This allows you to use CloudBuild to pull code from 1288 | * a repository and dump it into the S3 bucket. 1289 | * 1290 | * @method cliCmds 1291 | * 1292 | * @param {String} cmd 1293 | * 1294 | * @return {void} 1295 | */ 1296 | const ciCmds = async type => { 1297 | const ciprojectName = 1298 | projectParameters.site_config.ci["CodeBuildProjectCodeName"]; 1299 | const webBucketName = projectParameters.site_config["ProjectS3Bucket"]; 1300 | const ciProjectPath = projectParameters.ci_project_path; 1301 | let site_config = projectParameters.site_config.ci; 1302 | site_config["ProjectS3Bucket"] = webBucketName; 1303 | 1304 | if (type === "create" || type === "update") { 1305 | site_config["CloudDistId"] = await getCloudFrontDistID(webBucketName); 1306 | } 1307 | 1308 | if (type === "create") { 1309 | await createCiStack(ciprojectName, site_config, ciProjectPath); 1310 | } 1311 | 1312 | if (type === "update") { 1313 | await updateCiStack(ciprojectName, site_config, ciProjectPath); 1314 | } 1315 | 1316 | if (type === "delete") { 1317 | await deleteCiStack(ciprojectName, webBucketName); 1318 | } 1319 | }; 1320 | 1321 | /** 1322 | * Merges together the values from the site_config.json file 1323 | * (or whatever named file the user specified) with any values 1324 | * passed in from the command line as part of the sc flag. 1325 | * 1326 | * @method mergeConfig 1327 | * 1328 | * 1329 | * @return {Object} 1330 | * 1331 | */ 1332 | const mergeConfig = async () => { 1333 | let config_params = projectParameters.site_config_params; 1334 | let file_params = {}; 1335 | if ( 1336 | projectParameters.site_config_file !== undefined && 1337 | fs.existsSync(projectParameters.site_config_file) 1338 | ) { 1339 | file_params = await parseJsonConfig(projectParameters.site_config_file); 1340 | } 1341 | if (typeof config_params === "string") { 1342 | config_params = JSON.parse(config_params); 1343 | } else if (config_params === undefined) { 1344 | config_params = {}; 1345 | } 1346 | return Object.assign({}, file_params, config_params); 1347 | }; 1348 | 1349 | /** 1350 | * Config global variables used to confiure the project 1351 | * We start by reading in the capsule.json file. 1352 | * Following this, any overrides passed in via the 1353 | * --site_config flag are merged in and take precedent. 1354 | * The final step is to then check if any named flags are 1355 | * also passed in e.g. --project-name. 1356 | * If so these takes precedent over the configuration in the 1357 | * file and the config in the --site_config. 1358 | * 1359 | * Once complete the projectParameters object contains 1360 | * all of the key/value pairs we need in order to 1361 | * populate the CF scripts. 1362 | * 1363 | * @method{processConfiguration} 1364 | * 1365 | * @return {void} 1366 | * 1367 | */ 1368 | const processConfiguration = async () => { 1369 | projectParameters.site_config_params = commander.site_config; //commandline JSON object 1370 | projectParameters.site_config_file = commander.site_config_file 1371 | ? commander.site_config_file 1372 | : "capsule.json"; 1373 | projectParameters.site_config = await mergeConfig(); 1374 | 1375 | if (commander.projectName !== undefined) { 1376 | projectParameters.site_config["ProjectName"] = commander.projectName; 1377 | } 1378 | 1379 | projectParameters.site_config["S3BucketName"] = `cf-${ 1380 | projectParameters.site_config["ProjectName"] 1381 | }-capsule-ci`; 1382 | projectParameters.site_config.ci["CodeBuildProjectCodeName"] = `capsule-${ 1383 | projectParameters.site_config["ProjectName"] 1384 | }-ci`; 1385 | projectParameters.site_config["WebProjectName"] = `capsule-${ 1386 | projectParameters.site_config["ProjectName"] 1387 | }-web`; 1388 | 1389 | if (commander.url !== undefined) { 1390 | projectParameters.site_config["RepositoryURL"] = commander.url; 1391 | } 1392 | 1393 | if (commander.dom !== undefined) { 1394 | projectParameters.site_config["Domain"] = commander.dom; 1395 | } 1396 | 1397 | if (commander.subdom !== undefined) { 1398 | projectParameters.site_config["SubDomain"] = commander.subdom; 1399 | } 1400 | 1401 | projectParameters.site_config["ProjectS3Bucket"] = projectParameters 1402 | .site_config["SubDomain"] 1403 | ? `${projectParameters.site_config["SubDomain"]}.${ 1404 | projectParameters.site_config["Domain"] 1405 | }` 1406 | : projectParameters.site_config["Domain"]; 1407 | projectParameters.ci_project_path = commander.ci_project_path 1408 | ? commander.ci_project_path 1409 | : paths.ci_project; 1410 | }; 1411 | 1412 | // MAIN ####################################################################### 1413 | (async () => { 1414 | global.cwd = process.cwd(); 1415 | const type = commander.type; 1416 | 1417 | await loadAWSConfiguration(commander.config, commander.awsProfile); 1418 | 1419 | if (commander.type !== "init") { 1420 | await processConfiguration(); 1421 | } 1422 | 1423 | if (commander.type === "deploy") { 1424 | const deployType = "create"; 1425 | await s3Cmds(deployType); 1426 | await webCmds(deployType); 1427 | await ciCmds(deployType); 1428 | } 1429 | 1430 | if (commander.type === "remove") { 1431 | const deleteType = "delete"; 1432 | await ciCmds(deleteType); 1433 | await webCmds(deleteType); 1434 | await s3Cmds(deleteType); 1435 | } 1436 | 1437 | if (commander.args.includes("s3")) { 1438 | await s3Cmds(type); 1439 | } 1440 | 1441 | if (commander.args.includes("web")) { 1442 | await webCmds(type); 1443 | } 1444 | 1445 | if (commander.args.includes("ci")) { 1446 | await ciCmds(type); 1447 | /* */ 1448 | } 1449 | })(); 1450 | --------------------------------------------------------------------------------