Skip to content

Commit

Permalink
Add ability to configure AWS during shep new (#92)
Browse files Browse the repository at this point in the history
* add ability to configure AWS during new

* refactor configuration of role

* add policy to shep created roles

* hide IAM task if not being run
  • Loading branch information
chris-olszewski authored and southpolesteve committed Nov 8, 2016
1 parent 5ceb903 commit d1b15e3
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 19 deletions.
20 changes: 19 additions & 1 deletion src/commands/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ const questions = [
name: 'path',
message: 'Project folder name',
default: 'my-api'
},
{
type: 'input',
name: 'region',
message: 'Region for project',
default: 'us-east-1',
config: true
},
{
type: 'input',
name: 'rolename',
message: 'Enter the name of the IAM role which you wish to use, if it is not found it will be created',
default: 'shepRole',
config: true
}
]

Expand All @@ -16,12 +30,16 @@ export const desc = 'Create a new shep project'
export function builder (yargs) {
return yargs
.describe('path', 'Location to create the new shep project')
.boolean('skip-config')
.describe('skip-config', 'Skips configuring shep project')
.describe('region', 'Region for new shep project')
.describe('rolename', 'Name of IAM Role which will be used to execute Lambda functions')
.example('shep new', 'Launch an interactive CLI')
.example('shep new my-api', 'Generates a project at `my-api`')
}

export function handler (opts) {
inquirer.prompt(questions.filter((q) => !opts[q.name]))
inquirer.prompt(questions.filter((q) => !opts[q.name] && (!opts.skipConfig || !q.config)))
.then((inputs) => merge({}, inputs, opts))
.then(_new)
}
56 changes: 43 additions & 13 deletions src/new/index.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,93 @@
import { mkdir, writeFile } from '../util/modules/fs'
import { getRole, createRole, attachPolicy } from '../util/aws/iam'
import * as templates from './templates'
import Promise from 'bluebird'
import exec from '../util/modules/exec'
import listr from '../util/modules/listr'

export default function run (opts) {
const path = opts.path
const rolename = opts.rolename
const region = opts.region

const tasks = listr([
let tasks = [
{
title: `Setup IAM Role`,
task: setupIam
},
{
title: `Create ${path}/`,
task: () => mkdir(path)
},
{
title: 'Create Subdirectories',
task: () => createSubDirs(path)
task: createSubDirs
},
{
title: 'Create Files',
task: () => createFiles(path)
task: createFiles
},
{
title: 'Install Depedencies',
task: () => npmInstall(path)
task: npmInstall
},
{
title: 'Initialize Git',
task: () => initGit(path)
task: initGit
}
], opts.quiet)
]

return tasks.run()
if (!rolename) tasks = tasks.splice(1)

return listr(tasks, opts.quiet)
.run({ path, rolename, region })
}

function createSubDirs (path) {
function setupIam (context) {
const rolename = context.rolename
let newRole = false

return getRole(rolename)
.catch({ code: 'NoSuchEntity' }, () => {
newRole = true
return createRole(rolename)
})
.tap(arn => {
context.arn = arn
})
.then(() => { if (newRole) return attachPolicy(rolename) })
.catch({ code: 'LimitExceeded' }, () => {
return Promise.reject('Current AWS User does not have sufficient permissions to do this')
})
}

function createSubDirs ({ path }) {
return Promise.all([
mkdir(path + '/functions'),
mkdir(path + '/config')
])
}

function createFiles (path) {
function createFiles ({ path, arn, region }) {
const accountId = (/[0-9]{12}(?=:)/.exec(arn) || [ '' ])[0]

return Promise.all([
writeFile(path + '/package.json', templates.pkg(path)),
writeFile(path + '/package.json', templates.pkg({ apiName: path, region, accountId })),
writeFile(path + '/config/development.js', templates.env('development')),
writeFile(path + '/config/beta.js', templates.env('beta')),
writeFile(path + '/config/production.js', templates.env('production')),
writeFile(path + '/.gitignore', templates.gitignore()),
writeFile(path + '/README.md', templates.readme(path)),
writeFile(path + '/lambda.json', templates.lambda()),
writeFile(path + '/lambda.json', templates.lambda(arn)),
writeFile(path + '/api.json', templates.api(path)),
writeFile(path + '/webpack.config.js', templates.webpack())
])
}

function npmInstall (path) {
function npmInstall ({ path }) {
return exec('npm', ['install'], { cwd: path })
}

function initGit (path) {
function initGit ({ path }) {
return exec('git', ['init'], { cwd: path })
}
25 changes: 20 additions & 5 deletions src/new/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,34 @@ node_modules/*
config/*`
}

export function lambda () {
export function lambda (arn = '') {
let obj = {
Handler: 'index.handler',
MemorySize: 128,
Role: '',
Role: arn,
Timeout: 10,
Runtime: 'nodejs4.3'
}

return JSON.stringify(obj, null, 2)
}

export function pkg (apiName) {
export function lambdaRole () {
return `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}`
}

export function pkg ({ apiName, accountId = '', region = '' }) {
let obj = {
name: apiName,
version: '1.0.0',
Expand All @@ -46,8 +61,8 @@ export function pkg (apiName) {
minimatch: '3.0.3'
},
shep: {
region: '',
accountId: '',
region: region,
accountId: accountId,
apiId: ''
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/util/aws/iam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import AWS from './'
import { lambdaRole } from '../../new/templates'

export function createRole (name) {
const iam = new AWS.IAM()
const params = {
RoleName: name,
AssumeRolePolicyDocument: lambdaRole()
}

return iam.createRole(params).promise().get('Role').get('Arn')
}

export function getRole (name) {
const iam = new AWS.IAM()
const params = { RoleName: name }

return iam.getRole(params).promise().get('Role').get('Arn')
}

export function attachPolicy (name) {
const iam = new AWS.IAM()
const params = {
PolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
RoleName: name
}

return iam.attachRolePolicy(params).promise()
}
28 changes: 28 additions & 0 deletions test/new/index-new-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import test from 'ava'
import Promise from 'bluebird'
import { fs } from '../helpers/fs'
import { exec } from '../helpers/exec'
import td from '../helpers/testdouble'

const rolename = 'fooRole'
const region = 'us-east-1'
const path = 'foo-path'
const accountId = '123412341234'
const roleArn = `arn:aws:iam:${accountId}:role/${rolename}`
const templates = td.replace('../../src/new/templates')
const iam = td.replace('../../src/util/aws/iam')
td.when(iam.getRole(rolename)).thenReturn(Promise.reject({ code: 'NoSuchEntity' }))
td.when(iam.createRole(rolename)).thenReturn(Promise.resolve(roleArn))

test.before(() => {
const shep = require('../../src/index')
return shep.new({ region, rolename, path, quiet: true })
})

test('Creates role and writes configured templates', () => {
td.verify(fs.writeFile(), { ignoreExtraArgs: true })
td.verify(exec(), { ignoreExtraArgs: true })
td.verify(templates.pkg({ apiName: path, region, accountId }))
td.verify(templates.lambda(roleArn))
td.verify(iam.attachPolicy(rolename))
})
32 changes: 32 additions & 0 deletions test/new/index-prexisting-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import test from 'ava'
import Promise from 'bluebird'
import { fs } from '../helpers/fs'
import { exec } from '../helpers/exec'
import td from '../helpers/testdouble'

const rolename = 'fooRole'
const region = 'us-east-1'
const path = 'foo-path'
const accountId = '123412341234'
const roleArn = `arn:aws:iam:${accountId}:role/${rolename}`
const templates = td.replace('../../src/new/templates')
const iam = td.replace('../../src/util/aws/iam')
td.when(iam.getRole(rolename)).thenReturn(Promise.resolve(roleArn))

test.before(() => {
const shep = require('../../src/index')
return shep.new({ region, rolename, path, quiet: true })
})

test('If role is found, no role is created', () => {
td.verify(iam.createRole(), { times: 0, ignoreExtraArgs: true })
td.verify(iam.attachPolicy(), { times: 0, ignoreExtraArgs: true })
td.verify(fs.writeFile(), { ignoreExtraArgs: true })
td.verify(exec(), { ignoreExtraArgs: true })
})

test('Wrote configured templates', () => {
td.verify(templates.pkg({ apiName: path, region, accountId }))
td.verify(templates.lambda(roleArn))
})

8 changes: 8 additions & 0 deletions test/new/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import test from 'ava'
import td from '../helpers/testdouble'
import { createdDir, wroteFile } from '../helpers/fs'
import { didExec } from '../helpers/exec'

const path = 'foo-api'
const iam = td.replace('../../src/util/aws/iam')

test.before(() => {
const shep = require('../../src/index')
return shep.new({ path, quiet: true })
})

test('No calls to AWS if no rolename', () => {
td.verify(iam.getRole(), { times: 0, ignoreExtraArgs: true })
td.verify(iam.createRole(), { times: 0, ignoreExtraArgs: true })
})

test(createdDir, `${path}/functions`)
test(createdDir, `${path}/functions`)
test(createdDir, `${path}/config`)
Expand All @@ -22,3 +29,4 @@ test(wroteFile, `${path}/webpack.config.js`)

test(didExec, 'npm', 'install')
test(didExec, 'git', 'init')

0 comments on commit d1b15e3

Please sign in to comment.