├── .gitignore ├── img ├── how-torus-stack-works.png └── jam_stack_architecture.png ├── .torusignore ├── test ├── stackExistsTest.js ├── updateNameserverTest.js ├── deployStackTest.js ├── deployStackTestTwo.js ├── validateTemplateTest.js ├── generateTemplateTest.js └── resourceExistsTest.js ├── lib ├── deleteStack.js ├── stackResourceExists.js ├── stackExists.js ├── resourceExists.js ├── deployTemplate.js ├── generateTemplate.js ├── templateDefaults.js └── deployStack.js ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── index.js ├── package.json ├── LICENSE ├── .travis.yml ├── arjan.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | templates/ -------------------------------------------------------------------------------- /img/how-torus-stack-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torus-tools/stack/HEAD/img/how-torus-stack-works.png -------------------------------------------------------------------------------- /img/jam_stack_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torus-tools/stack/HEAD/img/jam_stack_architecture.png -------------------------------------------------------------------------------- /.torusignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | .git 4 | .github 5 | package.json 6 | package-lock.json 7 | LICENSE 8 | .gitignore 9 | .torusignore -------------------------------------------------------------------------------- /test/stackExistsTest.js: -------------------------------------------------------------------------------- 1 | /* const {HostedZone} = require('../lib/resourceExists') 2 | 3 | HostedZone('azuerotours.com').then(data => console.log(data)).catch(err => console.log(err)) */ -------------------------------------------------------------------------------- /test/updateNameserverTest.js: -------------------------------------------------------------------------------- 1 | /* const dns = require('@torus-tools/domains') 2 | 3 | const ns = [ 4 | 'ns-822.awsdns-38.net', 5 | 'ns-19.awsdns-02.com', 6 | 'ns-1238.awsdns-26.org', 7 | 'ns-2034.awsdns-62.co.uk' 8 | ] 9 | 10 | dns.godaddy.updateNameservers('localizehtml.com', ns) 11 | .then(data=>console.log(data)).catch(err=>console.log(err)) 12 | //updateNameServers.aws('torusframework.com', ns).then(data=>console.log(data)).catch(err=>console.log(err)) */ -------------------------------------------------------------------------------- /lib/deleteStack.js: -------------------------------------------------------------------------------- 1 | //require('dotenv').config(); 2 | var AWS = require('aws-sdk'); 3 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 4 | 5 | module.exports = function deleteStack(stackName) { 6 | return new Promise((resolve, reject) => { 7 | var params = {StackName: stackName}; 8 | cloudformation.deleteStack(params).promise() 9 | .then(() => resolve(stackName + ' cloudFormation Stack is being deleted.')) 10 | .catch((err) => reject(err)) 11 | }) 12 | } -------------------------------------------------------------------------------- /test/deployStackTest.js: -------------------------------------------------------------------------------- 1 | /* const {deployStack} = require('../lib/deployStack') 2 | 3 | const stack = { 4 | bucket: true, 5 | www: true, 6 | dns: true, 7 | cdn: false, 8 | https: false 9 | } 10 | 11 | const config = { 12 | index:"index.html", 13 | error:"error.html", 14 | last_deployment:"", 15 | providers: { 16 | domain: 'godaddy', 17 | bucket: 'aws', 18 | cdn: 'aws', 19 | dns: 'aws', 20 | https: 'aws' 21 | } 22 | } 23 | 24 | deployStack('localizehtml.com', stack, config, true) */ -------------------------------------------------------------------------------- /test/deployStackTestTwo.js: -------------------------------------------------------------------------------- 1 | /* const {deployStack} = require('../lib/deployStack') 2 | 3 | const stack = { 4 | bucket: true, 5 | www: true, 6 | dns: true, 7 | cdn: false, 8 | https: false 9 | } 10 | 11 | const config = { 12 | index:"index.html", 13 | error:"error.html", 14 | last_deployment:"", 15 | providers: { 16 | domain: 'godaddy', 17 | bucket: 'aws', 18 | cdn: 'aws', 19 | dns: 'aws', 20 | https: 'aws' 21 | } 22 | } 23 | 24 | deployStack('localizehtml.com', stack, config, true) */ -------------------------------------------------------------------------------- /lib/stackResourceExists.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | var AWS = require('aws-sdk'); 3 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 4 | 5 | module.exports = function getStackResources(domain){ 6 | return new Promise((resolve, reject)=> { 7 | let stackName = domain.split('.').join('') + 'Stack' 8 | var params = {StackName: stackName}; 9 | cloudformation.describeStackResources(params).promise().then(data=>{ 10 | let resources = {} 11 | for(let obj of data.StackResources) resources[obj.LogicalResourceId]=obj.PhysicalResourceId 12 | resolve(resources) 13 | }).catch(err=> reject(err)) 14 | }) 15 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Feature: Title of the requested feature' 5 | labels: Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/validateTemplateTest.js: -------------------------------------------------------------------------------- 1 | /* require('dotenv').config(); 2 | var AWS = require('aws-sdk'); 3 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 4 | 5 | let templateBody = {"AWSTemplateFormatVersion":"2010-09-09","Resources":{"RootBucket":{"Type":"AWS::S3::Bucket","DeletionPolicy":"Delete","Properties":{"AccessControl":"PublicRead","BucketName":"azuerotours.com","WebsiteConfiguration":{"ErrorDocument":"error.html","IndexDocument":"index.html"}}},"HostedZone":{"Type":"AWS::Route53::HostedZone","DeletionPolicy":"Delete","Properties":{"Name":"azuerotours.com"}}}} 6 | 7 | cloudformation.validateTemplate({TemplateBody: JSON.stringify(templateBody)}).promise() 8 | .then((data)=> console.log(data)) 9 | .catch(err=>console.log(err)) 10 | */ -------------------------------------------------------------------------------- /lib/stackExists.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 3 | 4 | module.exports = function aws(domain) { 5 | return new Promise((resolve, reject) => { 6 | let stackName = domain.split('.').join('') + 'Stack' 7 | cloudformation.describeStacks({StackName: stackName}).promise().then(data => { 8 | if(data.Stacks[0] && data.Stacks[0].StackName === stackName){ 9 | if(data.Stacks[0].StackStatus !== 'REVIEW_IN_PROGRESS') resolve(data.Stacks[0].StackId) 10 | else resolve(null) 11 | } 12 | else resolve(null) 13 | }).catch(err => { 14 | if(err.message === `Stack with id ${stackName} does not exist` || err.message === `Stack with id [${stackName}] does not exist`) resolve(null) 15 | else reject(err) 16 | }) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'Bug: Title of the issue' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - arjan-optimize Version [e.g. 0.6.2] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const generateTemplate = require('./lib/generateTemplate') 2 | const {deployStack, deployParts, deployFull} = require('./lib/deployStack') 3 | const {deployTemplate} = require('./lib/deployTemplate') 4 | const deleteStack = require('./lib/deleteStack') 5 | const stackExists = require('./lib/stackExists') 6 | const stackResourceExists = require('./lib/stackResourceExists') 7 | const resourceExists = require('./lib/resourceExists') 8 | 9 | module.exports.generateTemplate = generateTemplate; 10 | module.exports.deployStack = deployStack; 11 | module.exports.deployParts = deployParts; 12 | module.exports.deployFull = deployFull; 13 | module.exports.deployTemplate = deployTemplate; 14 | module.exports.deleteStack = deleteStack; 15 | module.exports.stackExists = stackExists; 16 | module.exports.stackResourceExists = stackResourceExists; 17 | module.exports.resourceExists = resourceExists; 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@torus-tools/stack", 3 | "version": "0.0.125", 4 | "description": "A promise-based SDK for generating, deploying and managing stacks in AWS with cloudformation.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --timeout 15000" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/gkpty/arjan-deploy.git" 12 | }, 13 | "keywords": [ 14 | "static sites", 15 | "jam stack", 16 | "deploy", 17 | "aws", 18 | "AWS-SDK-JS" 19 | ], 20 | "author": "gabriel kardonski @gkpty", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/torus-tools/stack/issues" 24 | }, 25 | "homepage": "https://github.com/torus-tools/stack#readme", 26 | "dependencies": { 27 | "@torus-tools/content": "0.0.22", 28 | "@torus-tools/domains": "0.0.14", 29 | "aws-sdk": "^2.732.0", 30 | "dotenv": "^8.0.0" 31 | }, 32 | "devDependencies": { 33 | "mocha": "^8.1.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gabriel Kardonski 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 | -------------------------------------------------------------------------------- /test/generateTemplateTest.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | var assert = require('assert'); 3 | var generateTemplate = require('../lib/generateTemplate') 4 | //var ValidateTemplate = require('../lib/ValidateTemplate') 5 | //var AWS = require('aws-sdk-mock'); 6 | var AWS = require('aws-sdk'); 7 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 8 | 9 | const stack = { 10 | bucket: true, 11 | www: true, 12 | dns: true, 13 | cdn: false, 14 | https: false 15 | } 16 | 17 | const config = { 18 | options: { 19 | index:"index.html", 20 | error:"error.html", 21 | }, 22 | last_deployment:"", 23 | providers: { 24 | domain: 'godaddy', 25 | bucket: 'aws', 26 | cdn: 'aws', 27 | dns: 'aws', 28 | https: 'aws' 29 | } 30 | } 31 | 32 | describe('Check the generateTemplate method', function() { 33 | let domain = "www.test.com"; 34 | describe('No params supplied',()=>{ 35 | it('Should produce an error', ()=>{ 36 | generateTemplate().catch(err => assert.strictEqual(err.includes('Error: Please use a valid domain name'), true)) 37 | }) 38 | }); 39 | describe('Validates a generated cloudFromation template for a prod setup', ()=>{ 40 | it('Should call the generateTemplate method and validate the result with the AWS SDK', async function() { 41 | let temp = await generateTemplate(domain, stack, config) 42 | let templateBody = temp.template 43 | //console.log(templateBody) 44 | assert.strictEqual(typeof templateBody, "object") 45 | assert.strictEqual(JSON.stringify(templateBody).length>20? true:false, true) 46 | let validate = await cloudformation.validateTemplate({TemplateBody: JSON.stringify(templateBody)}).promise().catch(err=>console.log(err)) 47 | assert.strictEqual(typeof validate, 'object') 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /test/resourceExistsTest.js: -------------------------------------------------------------------------------- 1 | /* var assert = require('assert'); 2 | var generateTemplate = require('../lib/generateTemplate') 3 | //var ValidateTemplate = require('../lib/ValidateTemplate') 4 | //var AWS = require('aws-sdk-mock'); 5 | var AWS = require('aws-sdk'); 6 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 7 | 8 | let templi = {"AWSTemplateFormatVersion":"2010-09-09","Resources":{"BucketPolicy":{"Type":"AWS::S3::BucketPolicy","Properties":{"Bucket":"testingsite.com","PolicyDocument":{"Version":"2012-10-17","Statement":[{"Sid":"PublicReadGetObject","Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::testingsite.com/*"}]}}},"RootBucket":{"Type":"AWS::S3::Bucket","Properties":{"AccessControl":"PublicRead","BucketName":"testingsite.com","WebsiteConfiguration":{"ErrorDocument":"error.html","IndexDocument":"index.html"}}}}} 9 | 10 | describe('Check the generateTemplate method', function() { 11 | let domain = "test.com"; 12 | let indexDoc = "index.html"; 13 | let errDoc = "error.html" 14 | describe('No params supplied',()=>{ 15 | it('Should produce an error', ()=>{ 16 | generateTemplate().catch(err => assert.equal(err.includes('invalid domain'), true)) 17 | }) 18 | }); 19 | describe('Validates a generated cloudFromation template for a dev setup', ()=>{ 20 | it('Should generate a basic template and validate it with the AWS SDK', async function() { 21 | let temp = await generateTemplate(domain) 22 | let templateBody = temp.template 23 | assert.equal(typeof templateBody, "object") 24 | assert.equal(JSON.stringify(templateBody).length>20? true:false, true) 25 | let validate = await cloudformation.validateTemplate({TemplateBody: JSON.stringify(templateBody)}).promise().catch(err=>console.log(err)) 26 | assert.equal(typeof validate, 'object') 27 | }); 28 | }); 29 | stackExists, 30 | bucketExists, 31 | distributionExists, 32 | certificateExists, 33 | dnsRecordExists, 34 | hostedZoneExists, 35 | newHostedZone 36 | }); */ 37 | 38 | const resourceExists = require('../lib/resourceExists') 39 | 40 | resourceExists.CloudFrontDist('vivesyasociados.com').then((data) => console.log(data)) 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - npm test 9 | env: 10 | global: 11 | - secure: OROKwacygmcaDs2yxAVBMFbcANX0VvpivBEQotXeBpBtTprzqxxG2KvhBEg1IsUoUv+BTGUYDxuzsbqjzFdTCjaHPN5K+8I7YbGvUmi5hbjGd3cRpRiBiG9YTwAMd+5ICLZUQ56jDFf9Va3hvFTo9boaZ/AkB9KQBQ51cFwKaQUubsSmcuDv60oWNfVsL9v0DaYxb5aOCXDxP3oaqNS0QZj1BxlgJ8PwTrDnuQ0m3oD/91q5GC1moLDx7p3IuUD77BsvCbfsjKzAixFv9YB6pcV0bevWZep3qPCXy7RNGNy0ctq/H+2dQUzAXUsT6I2hjAIHjM7D6rxHqGR+DlJNmNheSPp5kmX6ETYPzEHmqaAhnYTbm/Zv4sL86qkNSLGjT52JtbapOy8gJMqIB8XH26IDUyhOd+gav0lKscTtOtE5w7lk7zanwBHN1wVufq8IBa39aNb2aJ+OaocqqmF3H4fbuf92fVmiDEOZoogHFgc6kZftUVZ/Lwral6vTP5TT54CXYOM9N5Fb2d54RdJRbP35XUxfpQ7zewM2T8jMOPSkHyYl+9BAlFf92jgM9tKVCX78P7XfBAJbXuP3/XAJxXaY5HPLGLTveheUlhMUkKyq8EKhixVWIx6Mrms/OJvJiwzr9EJiIiWu+h1kBKcMngbeurlT0g3+IFYdjUQxfDU= 12 | - secure: WEWg+fQIflXyBI7A0iQWW0Z0rW6dIHwvXNN2qLqri/MvbdCvG7tOWrWZY5boaqbutAlTHlPr9pUXiDJVLUvevaOxzDsI/g+nrR/gBCV1aXUNaxJdgD+A9SPTu8kogSaVH7vdihSRvzS6THY28GZm3ADfTsDZN9FkqsAzAlmlBSnnwViQZVi21fGjknhJXReQn+T9mkXCqCyY8WeZL2rQU5QOeLnkTOR8XpXPvWb7dRBGqcKNIt9TsiA1ssSyIKCsRP92IsMPjZYt+HLz7TUVnpnH/PmnRr3rD0Hcxzg3k9VfcnArCEX+fJwQStOSi6mj8+o+SPLfIVkKqM/2yZiofbTKuCQZCBQqfj9lMDf1f1qhz0twfSDc+iKFbl2n13EwjAaHsJUctNABT0xoKwNp6o/kzxe8lPxTj8CApFeQV+0dy+Y/T/F7E0fMziI5nSAYvnqnO32prt8aF6ENyp0gWjYr+cI6KEnq08tMP2FW7NTh1XxbHG2SAs/4J79FAzDPlD16qmZtsMc2uXAQMGAWBvMvf6CCY3rvD6SSpDESeyQu74HlA4ujl7CmrXyo1AmohDgIxempuNILayqafmmW7RnrPhWv7gRM9o3jx8va6VzD7aq1OY9KA9a5UFYjL9+WB9TFZCyjj181yLweZ5D4e8Om834eViW8qJtvEAmieNc= 13 | - secure: pnvjYptFBOwV94x//2ztcKzohZApH7XDwMh2miRrj7KK7WbhVIwUvZ667PpcLugEwtkhhxljwWvdf/R9MLtkBtAP22ZieRjdXrfv6k6j/H46EuRNC8qfH3Zsnwh0Bcy1kXl5GoRWO455We+jlKkGqfO1CO2ooWtgNzEtoWZOhtNFk0rocqXDomsxOVJX9+V3XcCdUKe5+XxgqkBKvodZQYo3KeVJpcxRKNoXD4scGlGkaQ2Ut02Pjt+klxxAZ4bpQsPDDt3/GPkMwabStxhawMTnXDHEMS9aldDHph1lux6baIhD9yKywbBcMPHXnfDVncdIemMaSAEd+PybpL20q7ukXtbZutVMARGXNIB7wM2qtsxzwKrCT2zoNrH9qP+leaRY6us0gALvZlldHxQC3XiVyVW3MHlWKklm1KPDpLSj5nEAf9FByRqD/Y8kf89XNlURrCBTa595gCHTRYjnrvtk8sX8pi+oM/BOjz0xEw1B3Rfuk6NmMAft+WqQIn9nZtXJFq8K8/tHjLMZ+8bPlQstAmYM78SKXNhV2VA5VkrNBukDSOoB+Z2o/Eurs5xq2DkiOge3mO3pngUmAAU6UgNy5pTMBcOZaq2XNI8GC/x5fgKG4Ghp/L7uOdrDPzPT4tCLhl/n6POy79w2sEJnegulfrUTz4fXcNJP6BuFxpg= 14 | -------------------------------------------------------------------------------- /arjan.js: -------------------------------------------------------------------------------- 1 | 2 | const arjan = ` 3 | @@@ @@@@@@ 4 | @@@(***(@@ @@*******%@@@ 5 | @@@***********&@@ @@@@@@@@@@@@@@@@@@@@@ @@/*************&@ 6 | @@@@@@(***,,****%@@ @@@@@&(********@ @******(&@@@@@ @@%****,,****@@@@ 7 | @/**,,,,,****/(**************#@ @/************(/****,,,,***&@ 8 | @#**,,,,,********************@ ,@******************,,,,***@@ 9 | @***,,*********************@, %@********************,***@ 10 | @@************************@( &&**********************@ 11 | @@**********************%@ @%********************@@ 12 | @*********************/@ .@*******************/@ 13 | @*********************@& &%*******************@ 14 | @*********************@& &%*******************@ 15 | @/********************(@ @*******************/@ 16 | @%********#@@@@@%******%@ @%*******#&*********&@ 17 | @******@/ #@@@@@(@*****@/ &&*****&@@@@@@ #@*****@ 18 | @*******@@*@@@@% @*****@ @*****@ .@@@@@ /@%****@ 19 | @**********/&@@@@@/*****@, %&****#@&*,%@@@(*******@ 20 | @@***********************@. (@*********************@@ 21 | @/**********************@% @#********************/@ 22 | @**********************@# @@********************@ 23 | @%********************#@. .@/******************&@ 24 | @/*******************@/ %@*****************/@ 25 | @/*****************@& @@***************/@ 26 | @%***************&@ @#*************&@ 27 | @*************/@/ #@@@@@@@@@# %@************@ 28 | @************@ @@@@@@@@@@@@@ @**********@ 29 | @@**********&% .@@@@@@@@@@@. @(********@@ 30 | @@*********%& &@@@% @/*******@@ 31 | @@********/@ @*******@@ 32 | @/*******@# %&*****%@ 33 | @@******/@ ,@*****@@ 34 | @@*****%@ @(***@@ 35 | @&/**@@@@@@@@@@@@,(%%#/,@@@@@@@@@@@@&%/@ 36 | `; -------------------------------------------------------------------------------- /lib/resourceExists.js: -------------------------------------------------------------------------------- 1 | //load files from the .env file 2 | //require('dotenv').config(); 3 | // Load the AWS SDK for Node.js 4 | var AWS = require('aws-sdk'); 5 | var route53 = new AWS.Route53({apiVersion: '2013-04-01'}); 6 | var cloudfront = new AWS.CloudFront({apiVersion: '2019-03-26'}); 7 | var s3 = new AWS.S3({apiVersion: '2006-03-01'}); 8 | var acm = new AWS.ACM({apiVersion: '2015-12-08'}); 9 | //if the stack exists it should return an object. all objects must contain an ID field that contains the logical id of the resource + other properties 10 | 11 | function CloudFrontDist(domainName) { 12 | return new Promise((resolve, reject) => { 13 | cloudfront.listDistributions({}).promise() 14 | .then(data=>{ 15 | let items = data.DistributionList.Items 16 | let lastElem = items.length-1 17 | for(let i in items){ 18 | if(items[i].Origins.Items[0].DomainName.startsWith(domainName)){ 19 | let exists = items[i].Id 20 | resolve(exists) 21 | } 22 | if(i === lastElem.toString()) resolve(null) 23 | } 24 | }).catch(err=>reject(err)) 25 | }) 26 | } 27 | 28 | function RootBucket(domain) { 29 | return new Promise((resolve, reject) => { 30 | var params = {Bucket: domain} 31 | s3.headBucket(params, function(err, data) { 32 | if (err) { 33 | if(err.code === 'NotFound' || err.code === 'Forbidden') resolve(false) 34 | else reject(err) 35 | } 36 | else resolve({id:domain}) 37 | }); 38 | }) 39 | } 40 | 41 | function HostedZone(domainName) { 42 | return new Promise((resolve, reject) => { 43 | var params = {"DNSName": domainName}; 44 | route53.listHostedZonesByName(params, function(err, data) { 45 | if (err) reject(err.stack); 46 | else { 47 | let exists = null; 48 | if(data.HostedZones[0] && data.HostedZones[0].Name === domainName + '.') exists = data.HostedZones[0].Id.substr(data.HostedZones[0].Id.lastIndexOf('/')+1, data.HostedZones[0].Id.length); 49 | resolve({id:exists}) 50 | } 51 | }) 52 | }) 53 | } 54 | 55 | function AcmCertificate(domainName) { 56 | return new Promise((resolve, reject) => { 57 | var params = {CertificateStatuses: ['ISSUED']}; 58 | acm.listCertificates(params).promise().then(data =>{ 59 | for(c in data.CertificateSummaryList) { 60 | if(data.CertificateSummaryList[c].DomainName === domainName) resolve({id:data.CertificateSummaryList[c].CertificateArn}) 61 | if(c >= data.CertificateSummaryList.length-1) resolve(null) 62 | } 63 | }).catch(err => reject(err)) 64 | }) 65 | } 66 | 67 | module.exports = { 68 | RootBucket, 69 | CloudFrontDist, 70 | HostedZone, 71 | AcmCertificate 72 | } 73 | 74 | // stackExists must be factored out into another file 75 | // all other methods should be renamed according to the cloudformation resource names -------------------------------------------------------------------------------- /lib/deployTemplate.js: -------------------------------------------------------------------------------- 1 | //function that reads a cloudfomration template and deploys it 2 | var AWS = require('aws-sdk'); 3 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 4 | var stackExists = require('./stackExists'); 5 | 6 | function deployTemplate(domain, template, importAction){ 7 | return new Promise((resolve, reject) => { 8 | var stackName = domain.split('.').join('') + 'Stack'; 9 | //console.log('TEMPLATE ', template) 10 | createChangeSet(domain, stackName, template.template, template.existingResources, importAction) 11 | .then((changeSet) => { 12 | //console.log('CHANGESET ', changeSet) 13 | cloudformation.waitFor('changeSetCreateComplete', {ChangeSetName: changeSet.name, StackName: stackName}).promise() 14 | .then(()=> { 15 | cloudformation.executeChangeSet({ChangeSetName: changeSet.name, StackName: stackName}).promise() 16 | .then(data => { 17 | //console.log('EXECUTED CHANGESET ', data) 18 | let waitAction = 'stackCreateComplete' 19 | if(changeSet.action === 'UPDATE') waitAction = 'stackUpdateComplete' 20 | else if(changeSet.action === 'IMPORT') waitAction = 'stackImportComplete'; 21 | cloudformation.waitFor(waitAction, {StackName: stackName}).promise() 22 | .then(() => resolve({stackName:stackName, changeSetName:changeSet.name, action:changeSet.action, template:JSON.parse(changeSet.template)})) 23 | .catch((err) => reject(err)) 24 | }).catch((err) => reject(err)) 25 | }).catch((err) => reject(err)) 26 | }).catch((err) => reject(err)) 27 | }) 28 | } 29 | 30 | function createChangeSet(domain, stackName, template, existingResources, importAction){ 31 | let dateString = new Date().toString() 32 | let changeSetName = stackName + dateString.split("GMT")[0].split(' ').join('').replace(/:/g,'') 33 | //console.log(JSON.stringify(template)) 34 | return new Promise((resolve, reject) => { 35 | //console.log('STACKNAME ', stackName) 36 | stackExists(domain) 37 | .then((data) =>{ 38 | //console.log('STACK EXISTS ', data) 39 | let action = data ? 'UPDATE': 'CREATE'; 40 | let params = { 41 | ChangeSetName: changeSetName, 42 | StackName: stackName, 43 | Capabilities: ['CAPABILITY_NAMED_IAM'], 44 | ChangeSetType: action, 45 | TemplateBody: JSON.stringify(template) 46 | }; 47 | //console.log(existingResources) 48 | if(importAction){ 49 | let importTemplate = { 50 | "AWSTemplateFormatVersion": "2010-09-09", 51 | "Resources": {} 52 | } 53 | for(elem of existingResources) importTemplate.Resources[elem['LogicalResourceId']] = template.Resources[elem['LogicalResourceId']] 54 | params.TemplateBody = JSON.stringify(importTemplate) 55 | params.ChangeSetType = 'IMPORT'; 56 | params["ResourcesToImport"] = existingResources; 57 | } 58 | //console.log('PARAMS ', JSON.stringify(params)) 59 | cloudformation.createChangeSet(params).promise() 60 | .then(()=> resolve({name:changeSetName, action:params.ChangeSetType, template: params.TemplateBody})) 61 | .catch((err)=> reject(err.stack)) 62 | }) 63 | .catch((err)=> reject(err)) 64 | }) 65 | } 66 | 67 | module.exports = { 68 | deployTemplate, 69 | createChangeSet 70 | } 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/generateTemplate.js: -------------------------------------------------------------------------------- 1 | const resourceExists = require('./resourceExists'); 2 | const {initialTemplate, stackResources, importables, templateDefaults} = require('./templateDefaults') 3 | 4 | function domainIsValid(domainName){ 5 | if (/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/.test(domainName)) return true; 6 | else return false; 7 | } 8 | 9 | function addToResources(array, LogicalResourceId, ResourceIdentifier, ResourceType, resourceIdentifierType){ 10 | return new Promise((resolve, reject) => { 11 | let resource = { 12 | 'LogicalResourceId': LogicalResourceId, 13 | 'ResourceIdentifier': {}, 14 | 'ResourceType': ResourceType 15 | } 16 | resource.ResourceIdentifier[resourceIdentifierType] = ResourceIdentifier 17 | array.push(resource) 18 | resolve(resource) 19 | }) 20 | } 21 | 22 | module.exports = function genTemplate(domain, stack, config, template, records, overwrite){ 23 | return new Promise((resolve, reject) => { 24 | if(domainIsValid(domain)){ 25 | let stackSize = 0; 26 | for(let resource in stack) { 27 | if(stack[resource]) stack[resource] = stackResources[resource] 28 | } 29 | let defaults = templateDefaults(domain, stack, config) 30 | //if(!config || !config.options || !config.options.index) config.index = 'index.html' 31 | //if(!config || !config.options || !config.options.error) config.error = 'error.html' 32 | let new_temp = template? JSON.parse(JSON.stringify(template)) : initialTemplate 33 | if(stack.bucket) new_temp.Resources['BucketPolicy'] = defaults['BucketPolicy'] 34 | if(records && stack.dns && config.providers.dns === 'aws') new_temp.Resources['RecordSet'] = defaults['RecordSet'] 35 | if(overwrite) { 36 | for(let key in stack) if(stack[key]){ 37 | //should also check that the provider for the given key is AWS 38 | if(resourceExists[stack[key]]) stackSize+=1 39 | new_temp.Resources[key] = defaults[key] 40 | } 41 | } 42 | else { 43 | for(let key in stack) { 44 | if(stack[key] && config.providers[key] === 'aws' || stack[key] && !config.providers[key]) { 45 | if(resourceExists[stack[key]]) stackSize+=1 46 | if(!new_temp.Resources[stack[key]]) new_temp.Resources[stack[key]] = defaults[stack[key]] 47 | } 48 | } 49 | } 50 | //custom stuff for the CDN depending on the ssl option 51 | if(stack.ssl && config.providers.ssl === 'aws'){ 52 | new_temp.Resources.CloudFrontDist.Properties.DistributionConfig["ViewerCertificate"] = { 53 | "AcmCertificateArn" : { "Ref": "AcmCertificate"}, 54 | "MinimumProtocolVersion" : "TLSv1.2_2018", 55 | "SslSupportMethod" : "sni-only" 56 | } 57 | new_temp.Resources.CloudFrontDist.Properties.DistributionConfig["Aliases"] = [ 58 | domain, 59 | `www.${domain}` 60 | ] 61 | new_temp.Resources.CloudFrontDist["DependsOn"] = ["AcmCertificate"]; 62 | } 63 | //generate the resources to import array 64 | let ResourcesToImport = [] 65 | let comparison = 0; 66 | for(let resource in stack) if(stack[resource] && resourceExists[stack[resource]] && config.providers[resource] === 'aws'){ 67 | comparison +=1; 68 | resourceExists[stack[resource]](domain).then(data => { 69 | if(importables[stack[resource]] && data && data.id) { 70 | addToResources(ResourcesToImport, stack[resource], data.id, new_temp.Resources[stack[resource]].Type, importables[stack[resource]]) 71 | .then((resource)=>{ 72 | if(comparison === stackSize) { 73 | resolve({"template":new_temp, "existingResources": ResourcesToImport}) 74 | } 75 | }) 76 | } 77 | else { 78 | if(comparison === stackSize) { 79 | resolve({"template":new_temp, "existingResources": ResourcesToImport}) 80 | } 81 | } 82 | }) 83 | } 84 | } 85 | else reject('Error: Please use a valid domain name') 86 | }) 87 | } -------------------------------------------------------------------------------- /lib/templateDefaults.js: -------------------------------------------------------------------------------- 1 | const s3HostedZoneID = { 2 | "us-east-1" : "Z3AQBSTGFYJSTF", 3 | "us-west-1" : "Z2F56UZL2M1ACD", 4 | "us-west-2" : "Z3BJ6K6RIION7M", 5 | "eu-west-1" : "Z1BKCTXD74EZPE", 6 | "ap-southeast-1" : "Z3O0J2DXBE1FTB", 7 | "ap-southeast-2" : "Z1WCIGYICN2BYD", 8 | "ap-northeast-1" : "Z2M4EHUR26P7ZW", 9 | "sa-east-1" : "Z31GFT0UA1I2HV", 10 | } 11 | const cloudFrontHostedZoneID = "Z2FDTNDATAQYW2" 12 | 13 | const initialTemplate = { 14 | "AWSTemplateFormatVersion": "2010-09-09", 15 | "Resources": {} 16 | } 17 | 18 | const stackResources = { 19 | bucket: "RootBucket", 20 | www: "WwwBucket", 21 | cdn: "CloudFrontDist", 22 | dns: "HostedZone", 23 | ssl: "AcmCertificate" 24 | } 25 | 26 | const importables = { 27 | "BucketPolicy":null, 28 | "RootBucket":"BucketName", 29 | "WwwBucket":"BucketName", 30 | "HostedZone":"HostedZoneId", 31 | "RecordSet":null, 32 | "AcmCertificate":null, 33 | "CloudFrontDist":"DistributionId" 34 | } 35 | 36 | function templateDefaults(domain, stack, config) { 37 | let DistributionDomain = { "Fn::GetAtt": ["CloudFrontDist", "DomainName"] } 38 | let awsRegion = process.env.AWS_REGION 39 | return { 40 | "RootBucket":{ 41 | "Type": "AWS::S3::Bucket", 42 | "DeletionPolicy": "Delete", 43 | "Properties": { 44 | "AccessControl": "PublicRead", 45 | "BucketName": domain, 46 | "WebsiteConfiguration": { 47 | "ErrorDocument" : config.options.error, 48 | "IndexDocument" : config.options.index 49 | }, 50 | "PublicAccessBlockConfiguration": { 51 | "BlockPublicAcls" : false, 52 | "BlockPublicPolicy" : false, 53 | "IgnorePublicAcls" : false, 54 | "RestrictPublicBuckets" : false 55 | } 56 | } 57 | }, 58 | "WwwBucket":{ 59 | "Type": "AWS::S3::Bucket", 60 | "DeletionPolicy": "Delete", 61 | "Properties": { 62 | "BucketName": `www.${domain}`, 63 | "WebsiteConfiguration": { 64 | "RedirectAllRequestsTo": { 65 | "HostName" : domain, 66 | "Protocol" : stack.https ? "https":"http" 67 | } 68 | } 69 | } 70 | }, 71 | "BucketPolicy": { 72 | "Type": "AWS::S3::BucketPolicy", 73 | "Properties": { 74 | "Bucket": domain, 75 | "PolicyDocument": { 76 | "Version": "2012-10-17", 77 | "Statement": [ 78 | { 79 | "Sid": "PublicReadGetObject", 80 | "Effect": "Allow", 81 | "Principal": "*", 82 | "Action": "s3:GetObject", 83 | "Resource": `arn:aws:s3:::${domain}/*` 84 | } 85 | ] 86 | } 87 | } 88 | }, 89 | "HostedZone":{ 90 | "Type": "AWS::Route53::HostedZone", 91 | "DeletionPolicy": "Delete", 92 | "Properties": { 93 | "Name" : domain 94 | } 95 | }, 96 | "RecordSet":{ 97 | "Type": "AWS::Route53::RecordSetGroup", 98 | "Properties": { 99 | "HostedZoneName": `${domain}.`, 100 | "RecordSets" : [ 101 | { 102 | "AliasTarget" : { 103 | "DNSName" : stack.cdn ? DistributionDomain:`s3-website-${awsRegion}.amazonaws.com`, 104 | "HostedZoneId" : stack.cdn ? cloudFrontHostedZoneID: s3HostedZoneID[awsRegion] 105 | }, 106 | "Name" : domain, 107 | "Type" : "A", 108 | }, 109 | { 110 | "AliasTarget" : { 111 | "DNSName" : stack.cdn ? DistributionDomain:`s3-website-${awsRegion}.amazonaws.com`, 112 | "HostedZoneId" : stack.cdn ? cloudFrontHostedZoneID : s3HostedZoneID[awsRegion] 113 | }, 114 | "Name" : `www.${domain}`, 115 | "Type" : "A", 116 | } 117 | ] 118 | }, 119 | "DependsOn":["HostedZone"] 120 | }, 121 | "AcmCertificate":{ 122 | "Type": "AWS::CertificateManager::Certificate", 123 | "Properties": { 124 | "DomainName" : domain, 125 | "DomainValidationOptions" : [ 126 | { 127 | "DomainName": domain, 128 | "HostedZoneId": { "Ref": "HostedZone"} 129 | } 130 | ], 131 | "SubjectAlternativeNames" : [ `*.${domain}` ], 132 | "ValidationMethod" : "DNS" 133 | } 134 | }, 135 | "CloudFrontDist":{ 136 | "Type": "AWS::CloudFront::Distribution", 137 | "DeletionPolicy": "Delete", 138 | "Properties": { 139 | "DistributionConfig":{ 140 | "DefaultCacheBehavior" : { 141 | "AllowedMethods" : ["GET", "HEAD"], 142 | "CachedMethods" : ["GET", "HEAD"], 143 | "Compress" : true, 144 | "DefaultTTL" : 86400, 145 | "ForwardedValues" : { 146 | "QueryString" : false 147 | }, 148 | "MaxTTL" : 31536000, 149 | "MinTTL" : 0, 150 | "SmoothStreaming" : false, 151 | "TargetOriginId" : `s3-${domain}`, 152 | "ViewerProtocolPolicy" : stack.https ? "redirect-to-https":"allow-all" 153 | }, 154 | "DefaultRootObject" : config.options.index, 155 | "Enabled" : true, 156 | "HttpVersion":"http2", 157 | "Origins" : [ 158 | { 159 | "DomainName" : `${domain}.s3.${awsRegion}.amazonaws.com`, 160 | "Id" : `s3-${domain}`, 161 | "S3OriginConfig" : { 162 | "OriginAccessIdentity" : "" 163 | } 164 | } 165 | ], 166 | "PriceClass" : "PriceClass_All" 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | module.exports = { 174 | initialTemplate, 175 | stackResources, 176 | importables, 177 | templateDefaults 178 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Torus Stack 2 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://gkpty.mit-license.org) 3 | [![Build Status](https://travis-ci.com/torus-tools/stack.svg?branch=master)](https://travis-ci.com/torus-tools/stack) 4 | 5 | A promise-based javascript SDK that generates and deploys JSON cloudformation templates for static websites in AWS. It uses the AWS SDK to create, and execute changesets in a particular sequence that enables automation of the entire process while maintaining a short deployment time. 6 | 7 | You are free to customize the generated cloudformation template or its resources individually in any way you want using the AWS console/CLIs/SDKs and/or the torus stack SDK/CLI command. You can also push the torus/config.json to github and enable other team-members (with permission) to collaborate on the stack. 8 | 9 | ## Features 10 | - Creates a single cloudformation template 11 | - Saves your cloudformation changesets locally 12 | - Automatically imports existing resources for a given domain in AWS 13 | - Adds continous deployment with github using codepipeline 14 | - Completely open source 15 | - Responsive community support 🙂 16 | ## Getting Started 17 | 18 | **Prerequisites** 19 | 20 | - An AWS account 21 | - node and npm 22 | - The latest version of the torus CLI 23 | 24 | **Deploy a static site with a CDN and HTTPS** 25 | 26 | - pop up your terminal, go into your desired project `cd project_name`, and run `torus stack create prod` 27 | 28 | 29 | - **When using Torus Tools you are using your own AWS account from your own machine.** 30 | - **Any charges incurred by your websites will be billed directly from AWS to your AWS account.** 31 | - **Torus Tools does NOT have any access to your AWS account/bill.** 32 | 33 | 34 | # Architecture 35 | 36 | Because the content in a static site doesnt have to be processed on a request basis it can be served completely from a server’s cache, or a cheaper cloud based object storage solution like AWS s3. To place the content closer to the end users, provide faster response times, and a secure url, a CDN (content distribution network) can be added. 37 | 38 | ![The CDN fetches contet from the origin (s3 bucket) and distributes it to several edge locations scattered around the globe.](img/jam_stack_architecture.png) 39 | 40 | # setups 41 | 42 | For an easier development workflow we have defined some setups that include Dev, Test and Prod (production). You can customize these by additionally providing flags. 43 | 44 | **dev → test → prod** 45 | 46 | 47 | 1. **Dev:** public S3 bucket 48 | 2. **Test:** public S3 root bucket, www reroute bucket and a route53 hosted zone. 49 | 3. **Prod:** public S3 root bucket, www reroute bucket, route53 hosted zone, cloudfront distribution, ACM certificate 50 | # How it Works 51 | 52 | The Torus Stack SDK has a series of methods that take care of generating/provisioning cloudformation templates. The deployment process for a complete stack will first deploy a cloudformation template with an s3 bucket, public policy and a hosted zone. Then it will update it with a cloudfront distribution. 53 | 54 | If there are existing buckets/cdn's/hosted zones for the given domain, torus will propmpt you to confirm if you want to import those resources. 55 | 56 | ![](img/how-torus-stack-works.png) 57 | 58 | # Cost breakdown (from AWS) 59 | 60 | This is a breakdown of the costs of hosting a static site in AWS 61 | Let’s say your website uses CloudFront for a month (30 days), and the site has 1,000 visitors each day for that month. Each visitor clicked 1 page that returned a single object (1 request) and they did this once each day for 30 days. Turns out each request is 1MB for the amount of data transferred, so in total for the month that comes to 30,000MB or 29GB (1GB = 1,024MB). Half the requests are from the US and half are from Europe, so your monthly total for data transferred comes to $2.47. Also, each click is an HTTP request, so for the month that equals 30,000 HTTP requests, which comes to a total of $0.02 for the month. Adding the two values together, the total cost for using CloudFront for this example would be $2.49 for the month. 62 | 63 | | **Resource** | **Consumption** | **Cost** | 64 | | -------------- | -------------------- | ---------- | 65 | | Cloudfront | 29GB bandwith | $ 2.47 | 66 | | Cloudfront | 30,000 http requests | $ 0.02 | 67 | | Route53 | 1 Hosted Zone | $ 0.50 | 68 | | s3 | 5GB storage | $ 0.15 | 69 | | **Total Cost** | ------------------ | **$ 3.14** | 70 | 71 | 72 | # Programmatic Usage 73 | const {deployStack} = require('../lib/deployStack') 74 | 75 | deployStack('testingsit.com', {bucket:true}, {index:'index.html', error:'error.html', providers:{bucket:'aws'}}, true) 76 | 77 | 78 | # API 79 | 80 | ## stackExists(domain) 81 | - **description**: determines wether or not a stack exists for a given domain. If it does exist it returns the stack's ID. 82 | - **params**: (domain) 83 | - **domain**: STRING: REQUIRED: the root domain of your site i.e. yoursite.com 84 | - **returns**: promise(resolve, reject) 85 | - **resolve**: (stackID | null) 86 | - **stackID**: STRING: ID or ARN (Amazon resource number) of the existing resource 87 | - **reject**: (error) 88 | 89 | ## resourceExists.resource(domain) 90 | - **description**: determines wether or not a particular resource exists for a given domain. If it does exist it returns the resource's ID/ARN Resources include: 91 | - CloudFrontDist 92 | - RootBucket 93 | - HostedZone 94 | - AcmCertificate 95 | - **params**: (domain) 96 | - **domain**: STRING: REQUIRED: the root domain of your site i.e. yoursite.com 97 | - **returns**: promise(resolve, reject) 98 | - **resolve**: (ResourceID | null) 99 | - **resourceID**: STRING: ID or ARN (Amazon resource number) of the existing resource 100 | - **reject**: (error) 101 | 102 | ## generateTemplate(domain, stack, config, template, overwrite) 103 | - **description**: Generates a cloudformation template for a given domain 104 | - **params**: (domain, stack, config, template, overwrite) 105 | - **domain**: STRING: REQUIRED: the root domain of your site i.e. yoursite.com 106 | - **stack**: OBJECT: REQUIRED: Contains the desired resources for the given stack with boolean values 107 | ``` 108 | const stack = { 109 | bucket: true, 110 | www: true, 111 | dns: true, 112 | cdn: false, 113 | https: false 114 | } 115 | ``` 116 | - **config**: OBJECT: REQUIRED: Stack configuration. Contains the desired providers for each resource as well as the index and error documents. 117 | ``` 118 | const config = { 119 | index:"index.html", 120 | error:"error.html", 121 | last_deployment:"", 122 | providers: { 123 | domain: 'godaddy', 124 | bucket: 'aws', 125 | cdn: 'aws', 126 | dns: 'aws', 127 | https: 'aws' 128 | } 129 | } 130 | ``` 131 | - **template**: OBJECT: An existing JSON cloudformation template that you wicsh to modify 132 | - **overwrite**: BOOLEAN: Set as true if you wish to overwrite the existing template with the generated template. By default, only new resources are added to the existing template. 133 | - **returns**: promise(resolve, reject) 134 | - **resolve**: ({"template":{}, "existingResources":[]}) 135 | - **template**: OBJECT: the generated cloudformation template 136 | - **existingResource**: ARRAY: an array of resources that exist for the given domain that should be imported into the template 137 | - **reject**: (error) 138 | -------------------------------------------------------------------------------- /lib/deployStack.js: -------------------------------------------------------------------------------- 1 | // deployStack must contain an option called force or overwrite that overwrites all existing values from the template with the default values 2 | const generateTemplate = require('./generateTemplate') 3 | const stackExists = require('./stackExists') 4 | const {CloudFrontDist} = require('./resourceExists') 5 | const {stackResources, initialTemplate} = require('./templateDefaults') 6 | const {deployTemplate} = require('./deployTemplate') 7 | const {listFiles, uploadContent} = require('@torus-tools/content') 8 | const domains = require('@torus-tools/domains') 9 | 10 | const AWS = require('aws-sdk'); 11 | var cloudformation = new AWS.CloudFormation({apiVersion: '2010-05-15'}); 12 | 13 | const supported_providers = { 14 | registrar:['aws', 'godaddy'], 15 | bucket: ['aws'], 16 | dns:['aws', 'godaddy'], 17 | cdn: ['aws'], 18 | ssl: ['aws'] 19 | } 20 | 21 | async function deployStack(domain, stack, config, content, overwrite, local, cli){ 22 | //read the providers from the config 23 | //var importsTemplate = {template:JSON.parse(JSON.stringify(initialTemplate)), existingResources:[]} 24 | console.time('Elapsed Time') 25 | console.log('Setting up . . .') 26 | let template = null 27 | let partialStack = { 28 | bucket: false, 29 | www: false, 30 | dns: false 31 | } 32 | let stackId = await stackExists(domain) 33 | let templateString = '' 34 | if(stackId) { 35 | let temp = await cloudformation.getTemplate({StackName: stackId}).promise().catch(err => console.log(err)) 36 | templateString = temp.TemplateBody 37 | template = JSON.parse(templateString) 38 | } 39 | for(let key in partialStack) if(stack[key]) partialStack[key] = true 40 | console.log('finished setting up') 41 | console.log('generating templates . . .') 42 | let partialRecords = stack.cdn? false : true 43 | var partTemplate = local? JSON.parse(fs.readFileSync('./torus/template.json', utf8)): await generateTemplate(domain, partialStack, config, template, partialRecords, overwrite).catch(err => {throw new Error(err)}) 44 | var partialTemplate = JSON.parse(JSON.stringify(partTemplate)) 45 | var fullTemplate = local? partialTemplate : await generateTemplate(domain, stack, config, template, true, overwrite).catch(err => {throw new Error(err)}) 46 | if(partialTemplate && fullTemplate) console.log('finished generating templates') 47 | if(stackId && JSON.stringify(fullTemplate.template) === templateString){ 48 | console.log('no changes detected') 49 | return('no changes detected') 50 | } 51 | else { 52 | //import then update or create 53 | if(fullTemplate.existingResources.length > 0 && !stackId){ 54 | deployTemplate(domain, fullTemplate, true) 55 | .then((data)=> { 56 | console.log('finished importing resources') 57 | deployParts(domain, stack, config, partialTemplate, partialStack, fullTemplate, data.template, content, cli) 58 | .then(data => { 59 | console.timeEnd('Elapsed Time') 60 | return data 61 | }).catch(err=> {throw new Error(err)}) 62 | }).catch(err=> {throw new Error(err)}) 63 | } 64 | //update or create 65 | else{ 66 | deployParts(domain, stack, config, partialTemplate, partialStack, fullTemplate, template, content, cli) 67 | .then(data => { 68 | console.timeEnd('Elapsed Time') 69 | return data 70 | }).catch(err=> {throw new Error(err)}) 71 | } 72 | } 73 | } 74 | 75 | function deployParts(domain, stack, config, partialTemplate, partialStack, fullTemplate, template, content, cli){ 76 | return new Promise((resolve, reject) => { 77 | let size = 0; 78 | for(let key in partialStack) { 79 | size += 1; 80 | //should add an or at the end if the template does exist but the bucket policy isnt public 81 | if(partialStack[key] && !template || partialStack[key] && !template.Resources[stackResources[key]]) { 82 | cli? cli.action.start('Deploying basic resources') : console.log('Deploying basic resources...') 83 | deployTemplate(domain, partialTemplate).then(data => { 84 | if(cli) cli.action.stop() 85 | deployFull(domain, stack, config, fullTemplate, partialTemplate, content, cli) 86 | .then(data=> { 87 | resolve('All done') 88 | }).catch(err => reject(err)) 89 | }).catch(err => reject(err)) 90 | break 91 | } 92 | else if(size >= Object.keys(partialStack).length-1){ 93 | //console.log('NOT DEPLOYINBG PARTIAL ') 94 | deployFull(domain, stack, config, fullTemplate, partialTemplate, content, cli) 95 | .then(data=> { 96 | resolve('All Done') 97 | }).catch(err => reject(err)) 98 | break 99 | } 100 | } 101 | }) 102 | } 103 | 104 | //need to pass a param that contains the exisitng tempplate. if that template exists and contains a resource CDN then the cache for updates resources should be invalidated. 105 | //before deploy full must obtain nameservers for route53 106 | function deployFull(domain, stack, config, fullTemplate, partialTemplate, content, cli){ 107 | return new Promise((resolve, reject) => { 108 | let fullstack = false 109 | let done = false 110 | //Transfer Nameservers 111 | transferNs(domain, stack, config, cli).then(data=>{ 112 | if(content) { 113 | listFiles().then(data => { 114 | uploadContent(domain, data, false, false, null, cli).then(()=>{ 115 | done? resolve('All Done!'): done=true 116 | if(fullstack) cli? cli.action.start('Deploying additional resources'): console.log('Deploying additional resources') 117 | }).catch(err => reject(err)) 118 | }).catch(err => reject(err)) 119 | } 120 | if(JSON.stringify(fullTemplate) !== JSON.stringify(partialTemplate)){ 121 | fullstack = true 122 | deployTemplate(domain, fullTemplate).then(()=>{ 123 | if(cli) cli.action.stop() 124 | CloudFrontDist(domain).then(data => { 125 | cli? cli.action.start('creating records'): console.log('creating records...') 126 | createRecords(domain, stack, config, data.domain, cli) 127 | .then(data=>{ 128 | if(cli) cli.action.stop() 129 | done? resolve('All Done!'): done=true 130 | }) 131 | }) 132 | }) 133 | } 134 | else done? resolve('All Done!'): done=true 135 | }) 136 | }) 137 | } 138 | 139 | function transferNs(domain, stack, config, cli){ 140 | return new Promise((resolve, reject)=>{ 141 | let ns = '' 142 | if(stack.dns && config.providers.dns !== config.providers.registrar){ 143 | domains[config.providers.dns].getNameservers(domain).then(nameservers => { 144 | if(supported_providers.registrar.includes(config.providers.registrar)) { 145 | //update nameservers automatrically for godaddy & AWS 146 | cli? cli.action.start('Updating domain nameservers') : console.log('Updating domain nameservers...') 147 | domains[config.providers.registrar].updateNameservers(domain, nameservers).then(data=>{ 148 | resolve('done') 149 | }).catch(err => reject(err)) 150 | } 151 | else { 152 | //manually update nameservers for unsupported providers 153 | for(let n of nameservers) ns += n+'\n' 154 | console.log('\n\x1b[33mPlease update the nameservers for this domain to the following:\x1b[0m\n'+ns) 155 | cli.prompt('Have you finished updating the nameservers?').then(res=>{ 156 | if(res==='y' || res==='yes' || res==='Y' || res==='YES') resolve('done') 157 | else reject('You must update your nameservers when the DNS is different to the registrar') 158 | }) 159 | } 160 | }).catch(err => reject(err)) 161 | } 162 | else resolve('done') 163 | }) 164 | } 165 | 166 | function createRecords(domain, stack, config, url, cli){ 167 | return new Promise((resolve, reject)=> { 168 | let records = [ 169 | { 170 | data: url, 171 | name: 'www', 172 | ttl: 3600, 173 | type: 'CNAME' 174 | } 175 | ] 176 | if(config.providers['dns'] !== 'aws') { 177 | if(supported_providers.dns.includes(config.providers.dns)){ 178 | cli? cli.action.start('creating DNS records'): console.log('creating DNS records...') 179 | let recordReroute = false 180 | let redirectUrl = stack.https?'https://www.'+domain:'http://www.'+domain 181 | domains[config.providers.dns].upsertRecords(domain, records).then(()=>{ 182 | recordReroute? resolve('All Done'): recordReroute = true 183 | }).catch(err => reject(err)) 184 | domains[config.providers.dns].createRedirect(domain, redirectUrl).then(()=>{ 185 | recordReroute? resolve('All Done'): recordReroute = true 186 | }).catch(err => reject(err)) 187 | } 188 | else { 189 | console.log('Please create a DNS record with the following properties:\n', records[0], '\n', 'Then create a 301 redirect from the root to www.') 190 | resolve('All Done') 191 | } 192 | } 193 | else resolve('All Done') 194 | }) 195 | } 196 | 197 | //FOR S3 198 | /* ResourceRecordSet: { 199 | AliasTarget: { 200 | DNSName: `s3-website-${process.env.AWS_REGION}.amazonaws.com`, 201 | EvaluateTargetHealth: false, 202 | HostedZoneId: 'Z3AQBSTGFYJSTF' // a code depending on your region and resource for more info refer to https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_website_region_endpoints 203 | }, 204 | Name: wname, 205 | Type: "A" 206 | } */ 207 | 208 | //FOR CLOUDFRONT 209 | /* ResourceRecordSet: { 210 | Name: domain, 211 | Type: 'A', 212 | ResourceRecords: [], 213 | AliasTarget: 214 | { 215 | HostedZoneId: obj.hostedZoneId, 216 | DNSName: data.Distribution.DomainName, 217 | EvaluateTargetHealth: false 218 | } 219 | } */ 220 | 221 | 222 | module.exports = { 223 | deployStack, 224 | deployFull, 225 | deployParts 226 | } --------------------------------------------------------------------------------