Skip to content

Commit 3e755a2

Browse files
committed
add support for passing an AWS.Credentials objects via options.amazon.credentials
1 parent 559fcba commit 3e755a2

File tree

4 files changed

+267
-21
lines changed

4 files changed

+267
-21
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ gulp.task('deploy', function() {
2424
timestamp: true, // optional: If set to false, the zip will not have a timestamp
2525
waitForDeploy: true, // optional: if set to false the task will end as soon as it starts deploying
2626
amazon: {
27-
accessKeyId: "< your access key (fyi, the 'short' one) >", // optional
28-
secretAccessKey: "< your secret access key (fyi, the 'long' one) >", // optional
27+
credentials: {
28+
accessKeyId: "< your access key (fyi, the 'short' one) >", // optional
29+
secretAccessKey: "< your secret access key (fyi, the 'long' one) >", // optional
30+
}
2931
signatureVersion: "v4", // optional
3032
region: 'eu-west-1',
3133
bucket: 'elasticbeanstalk-apps',
@@ -38,7 +40,11 @@ gulp.task('deploy', function() {
3840

3941
The code above would work as follows
4042
* Take the files sepcified by `gulp.src` and zip them on a file named `{ version }-{ timestamp }.zip` (i.e: `1.0.0-2016.04.08_13.26.32.zip`)
41-
* If amazon credentials (`accessKeyId`, `secretAccessKey`) are provided in the `amazon` object, set them on the `AWS.config.credentials`. If not provided, the default values from AWS CLI configuration will be used.
43+
* There are multiple ways to provide AWS credentials:
44+
1. as an [AWS.Credentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html) object or an object of any inheriting class
45+
2. as an object holding parameters to AWS.Credentials or any of the following inheriting classes: [AWS.CognitoIdentityCredentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityCredentials.html), [AWS.SharedIniFileCredentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SharedIniFileCredentials.html), [AWS.SAMLCredentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SAMLCredentials.html), [AWS.TemporaryCredentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/TemporaryCredentials.html), which gulp-elasticbeanstalk-deploy will then try to autodetect.
46+
3. as a string either holding the path to [AWS.FileSystemCredentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/FileSystemCredentials.html) or the prefix for [AWS.EnvironmentCredentials](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EnvironmentCredentials.html).
47+
2. If no credentials are provided, the default values from AWS CLI configuration will be used.
4248
* Try to upload the zipped file to the bucket specified by `amazon.bucket`. If it fails because the bucket doesn't exist, try to create the bucket and then try to upload the zipped file again
4349
* Uploads the ziped files to the bucket on the path `{{ name }}/{{ filename }}` (i.e: `my-application/1.0.0-2016.04.08_13.26.32.zip`)
4450
* Creates a new version on the Application specified by `applicationName` with VersionLabel `{ version }-{ timestamp }` (i.e: `1.0.0-2016.04.08_13.26.32`)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"left-pad": "^1.1.1",
4444
"lodash": "^4.8.2",
4545
"plexer": "^1.0.1",
46-
"through2": "^2.0.1"
46+
"through2": "^2.0.1",
47+
"uuid": "^3.1.0"
4748
},
4849
"devDependencies": {
4950
"babel-cli": "^6.14.0",

src/plugin.js

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFileSync } from 'fs'
1+
import { readFileSync, existsSync } from 'fs'
22
import { join } from 'path'
33
import { omit, isEqual } from 'lodash'
44
import { log as gulpLog, colors, PluginError } from 'gulp-util'
@@ -9,6 +9,43 @@ import AWS from 'aws-sdk'
99
import pad from 'left-pad'
1010
import { S3File, Bean } from './aws'
1111

12+
const credentialProviders = [
13+
{
14+
Ctor: AWS.Credentials,
15+
fields: [
16+
[ 'accessKeyId', 'secretAccessKey' ]
17+
]
18+
},
19+
{
20+
Ctor: AWS.SAMLCredentials,
21+
fields: [
22+
[ 'RoleArn', 'PrincipalArn', 'SAMLAssertion' ]
23+
]
24+
},
25+
{
26+
Ctor: AWS.CognitoIdentityCredentials,
27+
fields: [
28+
[ 'IdentityPoolId' ],
29+
[ 'IdentityId' ]
30+
]
31+
},
32+
{
33+
// we can only detect these if a custom profile is specified
34+
// but that is fine because shared ini file credentials using the default profile
35+
// are used by the AWS SDK when no credentials are specified
36+
Ctor: AWS.SharedIniFileCredentials,
37+
fields: [
38+
[ 'profile' ]
39+
]
40+
},
41+
{
42+
Ctor: AWS.TemporaryCredentials,
43+
fields: [
44+
[ 'SerialNumber', 'TokenCode' ],
45+
[ 'RoleArn' ]
46+
]
47+
}
48+
]
1249
const IS_TEST = process.env['NODE_ENV'] === 'test'
1350
const log = IS_TEST ? () => {} : gulpLog
1451

@@ -129,7 +166,7 @@ export async function deploy(opts, file, s3file, bean) {
129166
if (e.code !== 'NoSuchBucket')
130167
throw e
131168

132-
await s3file.create()
169+
await s3file.create(opts.region)
133170
await s3file.upload(file)
134171
}
135172

@@ -191,17 +228,53 @@ export function buildOptions(opts) {
191228
if (!options.amazon)
192229
throw new PluginError(PLUGIN_NAME, 'No amazon config provided')
193230

194-
// if keys are provided, create new credentials, otherwise defaults will be used
195-
if (options.amazon.accessKeyId && options.amazon.secretAccessKey) {
231+
AWS.config.update(Object.assign({
232+
signatureVersion: 'v4'
233+
}, options.amazon.config || {}))
234+
235+
if (options.amazon.credentials !== undefined) {
236+
const creds = options.amazon.credentials
237+
const credsType = typeof(creds)
238+
239+
if (credsType === 'string') {
240+
// if the credentials are of type string, assume the user is specifying
241+
// an environment variable name prefix
242+
AWS.config.credentials = existsSync(creds) ? new AWS.FileSystemCredentials(creds)
243+
: new AWS.EnvironmentCredentials(creds)
244+
} else if (credsType !== 'object') {
245+
// otherwise the credentials must be an object
246+
throw new PluginError(PLUGIN_NAME, `Amazon credentials must be an object, got a '${typeof(creds)}'.`)
247+
} else if (creds.constructor.name === 'Credentials' ||
248+
typeof(creds.constructor.__super__) === 'function' &&
249+
creds.constructor.__super__.name === 'Credentials') {
250+
// support pre-build objects of or inheriting the AWS.Credentials class
251+
AWS.config.credentials = creds
252+
} else {
253+
// otherwise try to find a matching provider for the supplied credentials object
254+
const provider = credentialProviders.find(prov =>
255+
prov.fields.find(fields =>
256+
fields.every(field => creds[field] !== undefined)
257+
)
258+
)
259+
if (provider === undefined)
260+
throw new PluginError(PLUGIN_NAME, `Could not find a matching AWS credentials provider for the supplied credentials object.`)
261+
262+
try {
263+
AWS.config.credentials = new provider.Ctor(creds)
264+
} catch(err) {
265+
throw new PluginError(PLUGIN_NAME, `An error occured while trying to construct AWS.${provider.Ctor.name} from supplied credentials object: ${err}`)
266+
}
267+
}
268+
} else if (options.amazon.accessKeyId && options.amazon.secretAccessKey) {
269+
// legacy support for the access key id and secret access key
270+
// passed in directly via the options.amazon object
271+
log('options.amazon.accessKeyId and options.amazon.secretAccessKey are deprecated and will be removed in a future version. Use options.amazon.credentials instead.')
196272
AWS.config.credentials = new AWS.Credentials({
197273
accessKeyId: opts.amazon.accessKeyId,
198274
secretAccessKey: opts.amazon.secretAccessKey
199275
})
200276
}
201277

202-
// Set v4 by default
203-
AWS.config.signatureVersion = options.amazon.signatureVersion || 'v4'
204-
205278
return options
206279
}
207280

test/test.js

Lines changed: 176 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
/* eslint require-jsdoc: "off", new-cap: "off", no-invalid-this: "off" */
2-
import { readFileSync } from 'fs'
2+
import { readFileSync, writeFileSync, unlinkSync } from 'fs'
33
import should from 'should'
44
import { spy, stub } from 'sinon'
55
import AWS from 'aws-sdk'
66
import { File } from 'gulp-util'
77
import { S3File, Bean } from '../src/aws'
88
import * as plugin from '../src/plugin'
99
import gulpEbDeploy from '../src'
10+
import os from 'os'
11+
import uuidv4 from 'uuid/v4'
12+
import path from 'path'
1013

1114
describe('Gulp plugin', () => {
1215
let file
@@ -539,7 +542,27 @@ describe('Gulp plugin', () => {
539542
}
540543
AWS.config.credentials = null
541544
})
542-
it('updates AWS.config.credentials with the provided values', () => {
545+
546+
it('sets AWS.config with signatureVersion v4 by default', () => {
547+
spy(AWS, 'Credentials')
548+
buildOptions({
549+
amazon: {}
550+
})
551+
AWS.config.signatureVersion.should.be.equal('v4')
552+
})
553+
554+
it('allows to set a signatureVersion for AWS.config', () => {
555+
buildOptions({
556+
amazon: {
557+
config: {
558+
signatureVersion: 'v2'
559+
}
560+
}
561+
})
562+
AWS.config.signatureVersion.should.be.equal('v2')
563+
})
564+
565+
it('updates AWS.config.credentials with legacy values', () => {
543566
spy(AWS, 'Credentials')
544567
buildOptions({
545568
amazon: {
@@ -548,26 +571,127 @@ describe('Gulp plugin', () => {
548571
}
549572
})
550573
AWS.Credentials.calledOnce.should.be.true()
574+
AWS.config.credentials.should.be.instanceOf(AWS.Credentials)
551575
AWS.config.credentials.accessKeyId.should.be.equal('__accessKeyId')
552576
AWS.config.credentials.secretAccessKey.should.be.equal('__secretAccessKey')
553577
})
554578

555-
it('sets AWS.config with signatureVersion v4 by default', () => {
556-
spy(AWS, 'Credentials')
579+
it('updates AWS.config.credentials with access key id and secret access key.', () => {
557580
buildOptions({
558-
amazon: {}
581+
amazon: {
582+
credentials: {
583+
accessKeyId: '__accessKeyId',
584+
secretAccessKey: '__secretAccessKey'
585+
}
586+
}
559587
})
560-
AWS.config.signatureVersion.should.be.equal('v4')
588+
AWS.config.credentials.should.be.instanceOf(AWS.Credentials)
589+
AWS.config.credentials.accessKeyId.should.be.equal('__accessKeyId')
590+
AWS.config.credentials.secretAccessKey.should.be.equal('__secretAccessKey')
561591
})
562592

563-
it('allows to set a signatureVersion for AWS.config', () => {
564-
spy(AWS, 'Credentials')
593+
it('updates AWS.config.credentials with SAML credentials.', () => {
565594
buildOptions({
566595
amazon: {
567-
signatureVersion: 'v2'
596+
credentials: {
597+
RoleArn: '__roleArn',
598+
PrincipalArn: '__principalArn',
599+
SAMLAssertion: '__samlAssertion'
600+
}
568601
}
569602
})
570-
AWS.config.signatureVersion.should.be.equal('v2')
603+
AWS.config.credentials.should.be.instanceOf(AWS.SAMLCredentials)
604+
AWS.config.credentials.params.RoleArn.should.be.equal('__roleArn')
605+
AWS.config.credentials.params.PrincipalArn.should.be.equal('__principalArn')
606+
AWS.config.credentials.params.SAMLAssertion.should.be.equal('__samlAssertion')
607+
})
608+
609+
it('updates AWS.config.credentials with MFA temporary credentials.', () => {
610+
AWS.config.credentials = new AWS.Credentials()
611+
buildOptions({
612+
amazon: {
613+
credentials: {
614+
SerialNumber: '__serialNumber',
615+
TokenCode: '__tokenCode'
616+
}
617+
}
618+
})
619+
AWS.config.credentials.should.be.instanceOf(AWS.TemporaryCredentials)
620+
AWS.config.credentials.params.SerialNumber.should.be.equal('__serialNumber')
621+
AWS.config.credentials.params.TokenCode.should.be.equal('__tokenCode')
622+
})
623+
624+
it('updates AWS.config.credentials with IAM role temporary credentials.', () => {
625+
AWS.config.credentials = new AWS.Credentials()
626+
buildOptions({
627+
amazon: {
628+
credentials: {
629+
RoleArn: '__roleArn'
630+
}
631+
}
632+
})
633+
AWS.config.credentials.should.be.instanceOf(AWS.TemporaryCredentials)
634+
AWS.config.credentials.params.RoleArn.should.be.equal('__roleArn')
635+
})
636+
637+
it('updates AWS.config.credentials with Cognito identity ID credentials.', () => {
638+
buildOptions({
639+
amazon: {
640+
credentials: {
641+
IdentityId: '__indentityId'
642+
}
643+
}
644+
})
645+
AWS.config.credentials.should.be.instanceOf(AWS.CognitoIdentityCredentials)
646+
AWS.config.credentials.params.IdentityId.should.be.equal('__indentityId')
647+
})
648+
649+
it('updates AWS.config.credentials with Cognito identity pool ID credentials.', () => {
650+
buildOptions({
651+
amazon: {
652+
credentials: {
653+
IdentityPoolId: '__indentityPoolId'
654+
}
655+
}
656+
})
657+
AWS.config.credentials.should.be.instanceOf(AWS.CognitoIdentityCredentials)
658+
AWS.config.credentials.params.IdentityPoolId.should.be.equal('__indentityPoolId')
659+
})
660+
661+
it('updates AWS.config.credentials with an environment credential prefix.', () => {
662+
process.env.__envPrefix_ACCESS_KEY_ID = '__accessKeyId'
663+
process.env.__envPrefix_SECRET_ACCESS_KEY = '__secretAccessKey'
664+
665+
buildOptions({
666+
amazon: {
667+
credentials: '__envPrefix'
668+
}
669+
})
670+
AWS.config.credentials.should.be.instanceOf(AWS.EnvironmentCredentials)
671+
AWS.config.credentials.accessKeyId.should.be.equal('__accessKeyId')
672+
AWS.config.credentials.secretAccessKey.should.be.equal('__secretAccessKey')
673+
674+
process.env.__envPrefix_ACCESS_KEY_ID = ''
675+
process.env.__envPrefix_SECRET_ACCESS_KEY = ''
676+
})
677+
678+
it('updates AWS.config.credentials with credentials loaded from a credential file', () => {
679+
const fileName = path.join(os.tmpdir(), `credentials-${uuidv4()}.json`)
680+
writeFileSync(fileName, JSON.stringify({
681+
accessKeyId: '__accessKeyId',
682+
secretAccessKey: '__secretAccessKey'
683+
}))
684+
685+
buildOptions({
686+
amazon: {
687+
credentials: fileName
688+
}
689+
})
690+
unlinkSync(fileName)
691+
692+
AWS.config.credentials.should.be.instanceOf(AWS.FileSystemCredentials)
693+
AWS.config.credentials.accessKeyId.should.be.equal('__accessKeyId')
694+
AWS.config.credentials.secretAccessKey.should.be.equal('__secretAccessKey')
571695
})
572696

573697
it('does not update AWS.config.credentials if no access parameters were specified', () => {
@@ -578,6 +702,48 @@ describe('Gulp plugin', () => {
578702
AWS.Credentials.called.should.be.false()
579703
should(AWS.config.credentials).be.null()
580704
})
705+
706+
it('updates AWS.config.credentials with a Credentials object', () => {
707+
spy(AWS, 'Credentials')
708+
const credentials = new AWS.Credentials()
709+
buildOptions({
710+
amazon: {
711+
credentials: credentials
712+
}
713+
})
714+
AWS.Credentials.calledOnce.should.be.true()
715+
AWS.config.credentials.should.be.equal(credentials)
716+
})
717+
718+
it('throws an error when provided credentials are not a string or object', () => {
719+
(() => buildOptions({
720+
amazon: {
721+
credentials: 0
722+
}
723+
})).should.throw()
724+
})
725+
726+
it('throws an error when no matching credential provider is found', () => {
727+
(() => buildOptions({
728+
amazon: {
729+
credentials: {
730+
unknown: '__unknown'
731+
}
732+
}
733+
})).should.throw()
734+
})
735+
736+
it('rethrows an error thrown in the an AWS credentials constructor', () => {
737+
// temporary credentials missing master credentials
738+
(() => buildOptions({
739+
amazon: {
740+
credentials: {
741+
SerialNumber: '__serialNumber',
742+
TokenCode: '__tokenCode'
743+
}
744+
}
745+
})).should.throw()
746+
})
581747
})
582748

583749
describe('gulpEbDeploy', () => {

0 commit comments

Comments
 (0)