Merge branch 'master' into jackdanger/allow-specific-apigateway-logs-role

This commit is contained in:
Philipp Muens 2019-10-23 13:03:47 +02:00
commit 25bb4e2da1
30 changed files with 782 additions and 407 deletions

View File

@ -1,3 +1,40 @@
# 1.55.0 (2019-10-23)
- [Allow empty arrays in overrides](https://github.com/serverless/serverless/pull/6813)
- [Make question mark available as variables fallback](https://github.com/serverless/serverless/pull/6808)
- [Improve plugins resolution and initialization flow](https://github.com/serverless/serverless/pull/6814)
- [Azure Python template](https://github.com/serverless/serverless/pull/6822)
- [Chore - stop using deprecated 'new Buffer()' method.](https://github.com/serverless/serverless/pull/6829)
- [AWS - adding naming function for S3 compiled template file name.](https://github.com/serverless/serverless/pull/6828)
- [Span docs! and full `serverless_sdk` docs](https://github.com/serverless/serverless/pull/6809)
- [Fix perms with several CloudWatch log subscriptions](https://github.com/serverless/serverless/pull/6827)
- [Fixing an Azure docs broken link](https://github.com/serverless/serverless/pull/6838)
- [Adding note to Azure nodejs template](https://github.com/serverless/serverless/pull/6839)
- [Updated Azure Functions documentation](https://github.com/serverless/serverless/pull/6840)
- [Support for NotAction and NotResource in IAM role statements](https://github.com/serverless/serverless/pull/6842)
- [added frontmatter to sdk docs](https://github.com/serverless/serverless/pull/6845)
- [Setup <tab> completion via CLI command and interactive CLI step](https://github.com/serverless/serverless/pull/6835)
- [Upgrade gradle version](https://github.com/serverless/serverless/pull/6855)
- [Update Google provider documentation for functions](https://github.com/serverless/serverless/pull/6854)
- [SNS integration tests](https://github.com/serverless/serverless/pull/6846)
- [SQS integration tests](https://github.com/serverless/serverless/pull/6847)
- [Streams integration tests](https://github.com/serverless/serverless/pull/6848)
- [Improvements on SQS docs as suggested on #6516](https://github.com/serverless/serverless/pull/6853)
- [Schedule integration tests](https://github.com/serverless/serverless/pull/6851)
- [Update event documentation](https://github.com/serverless/serverless/pull/6857)
- [Upgrade groovy/gradle/plugin versions and dependencies (aws-groovy-gradle)](https://github.com/serverless/serverless/pull/6862)
- [Upgrade gradle/plugins version and dependencies (aws-clojure-gradle)](https://github.com/serverless/serverless/pull/6861)
- [IoT integration tests](https://github.com/serverless/serverless/pull/6837)
- [Update https-proxy-agent dependency](https://github.com/serverless/serverless/pull/6866)
- [Allow to use Ref in stream arn property](https://github.com/serverless/serverless/pull/6856)
- [Add Tests for resolveFilePathsFromPatterns()](https://github.com/serverless/serverless/pull/6825)
- [Integration tests improvements and fixes](https://github.com/serverless/serverless/pull/6867)
- [Honor cfnRole in custom resources](https://github.com/serverless/serverless/pull/6871)
## Meta
- [Comparison since last release](https://github.com/serverless/serverless/compare/v1.54.0...v1.55.0)
# 1.54.0 (2019-10-09)
- [Fixing typos in variable names](https://github.com/serverless/serverless/pull/6746)

View File

@ -47,6 +47,10 @@ functions:
type: kinesis
arn:
Fn::ImportValue: MyExportedKinesisStreamArnId
- stream:
type: dynamodb
arn:
Ref: MyDynamoDbTableStreamArn
- stream:
type: kinesis
arn:

View File

@ -1,14 +1,11 @@
'use strict';
/* eslint-disable no-unused-expressions */
const chai = require('chai');
const sinon = require('sinon');
const CLI = require('../../lib/classes/CLI');
const os = require('os');
const fse = require('fs-extra');
const exec = require('child_process').exec;
const path = require('path');
const spawn = require('child-process-ext/spawn');
const resolveAwsEnv = require('@serverless/test/resolve-env');
const stripAnsi = require('strip-ansi');
const Serverless = require('../../lib/Serverless');
const { getTmpDirPath } = require('../../tests/utils/fs');
@ -624,8 +621,10 @@ describe('CLI', () => {
});
describe('Integration tests', function() {
this.timeout(0);
this.timeout(1000 * 60 * 10);
const that = this;
const serverlessExec = require('../../tests/serverless-binary');
const env = resolveAwsEnv();
before(() => {
const tmpDir = getTmpDirPath();
@ -634,62 +633,27 @@ describe('CLI', () => {
fse.mkdirsSync(tmpDir);
process.chdir(tmpDir);
serverless = new Serverless();
return serverless.init().then(() => {
// Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows.
const execPrefix = os.platform() === 'win32' ? 'node ' : '';
that.serverlessExec =
execPrefix + path.join(serverless.config.serverlessPath, '..', 'bin', 'serverless');
});
});
after(() => {
process.chdir(that.cwd);
});
it('should print general --help to stdout', done => {
exec(`${this.serverlessExec} --help`, (err, stdout, stderr) => {
if (err) {
process.stdout.write(stdout);
process.stderr.write(stderr);
done(err);
return;
}
expect(stdout).to.contain('contextual help');
done();
});
});
it('should print command --help to stdout', done => {
exec(`${this.serverlessExec} deploy --help`, (err, stdout, stderr) => {
if (err) {
process.stdout.write(stdout);
process.stderr.write(stderr);
done(err);
return;
}
it('should print general --help to stdout', () =>
spawn(serverlessExec, ['--help'], { env }).then(({ stdoutBuffer }) =>
expect(String(stdoutBuffer)).to.contain('contextual help')
));
it('should print command --help to stdout', () =>
spawn(serverlessExec, ['deploy', '--help'], { env }).then(({ stdoutBuffer }) => {
const stdout = String(stdoutBuffer);
expect(stdout).to.contain('deploy');
expect(stdout).to.contain('--stage');
done();
});
});
}));
it('should print help --verbose to stdout', done => {
exec(`${this.serverlessExec} help --verbose`, (err, stdout, stderr) => {
if (err) {
process.stdout.write(stdout);
process.stderr.write(stderr);
done(err);
return;
}
expect(stdout).to.contain('Commands by plugin');
done();
});
});
it('should print help --verbose to stdout', () =>
spawn(serverlessExec, ['help', '--verbose'], { env }).then(({ stdoutBuffer }) =>
expect(String(stdoutBuffer)).to.contain('Commands by plugin')
));
});
});

View File

@ -5,6 +5,8 @@
const chai = require('chai');
const overrideEnv = require('process-utils/override-env');
const cjsResolve = require('ncjsm/resolve/sync');
const spawn = require('child-process-ext/spawn');
const resolveAwsEnv = require('@serverless/test/resolve-env');
const Serverless = require('../../lib/Serverless');
const CLI = require('../../lib/classes/CLI');
const Create = require('../../lib/plugins/create/create');
@ -14,12 +16,10 @@ const path = require('path');
const fs = require('fs');
const fse = require('fs-extra');
const mockRequire = require('mock-require');
const os = require('os');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const BbPromise = require('bluebird');
const getCacheFilePath = require('../utils/getCacheFilePath');
const { execSync } = require('child_process');
const { installPlugin } = require('../../tests/utils/plugins');
const { getTmpDirPath } = require('../../tests/utils/fs');
@ -29,6 +29,7 @@ chai.use(require('sinon-chai'));
const expect = chai.expect;
describe('PluginManager', () => {
const env = resolveAwsEnv();
let pluginManager;
let serverless;
@ -2005,35 +2006,25 @@ describe('PluginManager', () => {
});
describe('Plugin / CLI integration', function() {
this.timeout(0);
this.timeout(1000 * 60 * 10);
const cwd = process.cwd();
let serverlessInstance;
let serviceDir;
let serverlessExec;
const serverlessExec = require('../../tests/serverless-binary');
beforeEach(() => {
// eslint-disable-line prefer-arrow-callback
serverlessInstance = new Serverless();
return serverlessInstance.init().then(() => {
// Cannot rely on shebang in severless.js to invoke script using NodeJS on Windows.
const execPrefix = os.platform() === 'win32' ? 'node ' : '';
serverlessExec =
execPrefix +
path.join(serverlessInstance.config.serverlessPath, '..', 'bin', 'serverless');
const tmpDir = getTmpDirPath();
serviceDir = path.join(tmpDir, 'service');
fse.mkdirsSync(serviceDir);
process.chdir(serviceDir);
try {
execSync(`${serverlessExec} create --template aws-nodejs`);
} catch (error) {
// Expose process output in case of crash
process.stdout.write(error.stdout);
process.stderr.write(error.stderr);
throw error;
}
return spawn(serverlessExec, ['create', '--template', 'aws-nodejs'], {
env,
cwd: serviceDir,
});
});
});
@ -2057,22 +2048,14 @@ describe('PluginManager', () => {
'plugins:\n - local-plugin\n - parent-plugin'
);
let output;
try {
output = execSync(serverlessExec);
} catch (error) {
process.stdout.write(error.stdout);
process.stdout.write(error.stderr);
throw error;
}
const stringifiedOutput = Buffer.from(output, 'base64').toString();
expect(stringifiedOutput).to.contain('SynchronousPluginMock');
expect(stringifiedOutput).to.contain('PromisePluginMock');
return spawn(serverlessExec, [], { env, cwd: serviceDir }).then(({ stdoutBuffer }) => {
const stringifiedOutput = String(stdoutBuffer);
expect(stringifiedOutput).to.contain('SynchronousPluginMock');
expect(stringifiedOutput).to.contain('PromisePluginMock');
});
});
afterEach(() => {
// eslint-disable-line prefer-arrow-callback
process.chdir(cwd);
try {
fse.removeSync(serviceDir);
} catch (e) {

View File

@ -86,78 +86,83 @@ function addCustomResourceToService(awsProvider, resourceName, iamRoleStatements
const s3FileName = outputFilePath.split(path.sep).pop();
const S3Key = `${s3Folder}/${s3FileName}`;
let customResourceRole = Resources[customResourcesRoleLogicalId];
if (!customResourceRole) {
customResourceRole = {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: ['lambda.amazonaws.com'],
},
Action: ['sts:AssumeRole'],
},
],
},
Policies: [
{
PolicyName: {
'Fn::Join': [
'-',
[
awsProvider.getStage(),
awsProvider.serverless.service.service,
'custom-resources-lambda',
],
],
},
PolicyDocument: {
Version: '2012-10-17',
Statement: [],
},
},
],
},
};
const cfnRoleArn = serverless.service.provider.cfnRole;
if (shouldWriteLogs) {
const logGroupsPrefix = awsProvider.naming.getLogGroupName(funcPrefix);
customResourceRole.Properties.Policies[0].PolicyDocument.Statement.push(
{
Effect: 'Allow',
Action: ['logs:CreateLogStream'],
Resource: [
if (!cfnRoleArn) {
let customResourceRole = Resources[customResourcesRoleLogicalId];
if (!customResourceRole) {
customResourceRole = {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: ['lambda.amazonaws.com'],
},
Action: ['sts:AssumeRole'],
},
],
},
Policies: [
{
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${logGroupsPrefix}*:*`,
PolicyName: {
'Fn::Join': [
'-',
[
awsProvider.getStage(),
awsProvider.serverless.service.service,
'custom-resources-lambda',
],
],
},
PolicyDocument: {
Version: '2012-10-17',
Statement: [],
},
},
],
},
{
Effect: 'Allow',
Action: ['logs:PutLogEvents'],
Resource: [
{
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${logGroupsPrefix}*:*:*`,
},
],
}
);
};
Resources[customResourcesRoleLogicalId] = customResourceRole;
if (shouldWriteLogs) {
const logGroupsPrefix = awsProvider.naming.getLogGroupName(funcPrefix);
customResourceRole.Properties.Policies[0].PolicyDocument.Statement.push(
{
Effect: 'Allow',
Action: ['logs:CreateLogStream'],
Resource: [
{
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${logGroupsPrefix}*:*`,
},
],
},
{
Effect: 'Allow',
Action: ['logs:PutLogEvents'],
Resource: [
{
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' +
`:log-group:${logGroupsPrefix}*:*:*`,
},
],
}
);
}
}
const { Statement } = customResourceRole.Properties.Policies[0].PolicyDocument;
iamRoleStatements.forEach(newStmt => {
if (!Statement.find(existingStmt => existingStmt.Resource === newStmt.Resource)) {
Statement.push(newStmt);
}
});
}
const { Statement } = customResourceRole.Properties.Policies[0].PolicyDocument;
iamRoleStatements.forEach(newStmt => {
if (!Statement.find(existingStmt => existingStmt.Resource === newStmt.Resource)) {
Statement.push(newStmt);
}
});
const customResourceFunction = {
Type: 'AWS::Lambda::Function',
@ -169,19 +174,21 @@ function addCustomResourceToService(awsProvider, resourceName, iamRoleStatements
FunctionName: absoluteFunctionName,
Handler,
MemorySize: 1024,
Role: {
'Fn::GetAtt': [customResourcesRoleLogicalId, 'Arn'],
},
Runtime: 'nodejs10.x',
Timeout: 180,
},
DependsOn: [customResourcesRoleLogicalId],
DependsOn: [],
};
Resources[customResourceFunctionLogicalId] = customResourceFunction;
Object.assign(Resources, {
[customResourceFunctionLogicalId]: customResourceFunction,
[customResourcesRoleLogicalId]: customResourceRole,
});
if (cfnRoleArn) {
customResourceFunction.Properties.Role = cfnRoleArn;
} else {
customResourceFunction.Properties.Role = {
'Fn::GetAtt': [customResourcesRoleLogicalId, 'Arn'],
};
customResourceFunction.DependsOn.push(customResourcesRoleLogicalId);
}
if (shouldWriteLogs) {
const customResourceLogGroupLogicalId = awsProvider.naming.getLogGroupLogicalId(

View File

@ -221,6 +221,119 @@ describe('#addCustomResourceToService()', () => {
});
});
it('Should not setup new IAM role, when cfnRole is provided', () => {
const cfnRoleArn = (serverless.service.provider.cfnRole =
'arn:aws:iam::999999999999:role/some-role');
return expect(
BbPromise.all([
// add the custom S3 resource
addCustomResourceToService(provider, 's3', [
...iamRoleStatements,
{
Effect: 'Allow',
Resource: 'arn:aws:s3:::some-bucket',
Action: ['s3:PutBucketNotification', 's3:GetBucketNotification'],
},
]),
// add the custom Cognito User Pool resource
addCustomResourceToService(provider, 'cognitoUserPool', [
...iamRoleStatements,
{
Effect: 'Allow',
Resource: '*',
Action: [
'cognito-idp:ListUserPools',
'cognito-idp:DescribeUserPool',
'cognito-idp:UpdateUserPool',
],
},
]),
// add the custom Event Bridge resource
addCustomResourceToService(provider, 'eventBridge', [
...iamRoleStatements,
{
Effect: 'Allow',
Resource: 'arn:aws:events:*:*:rule/some-rule',
Action: [
'events:PutRule',
'events:RemoveTargets',
'events:PutTargets',
'events:DeleteRule',
],
},
{
Action: ['events:CreateEventBus', 'events:DeleteEventBus'],
Effect: 'Allow',
Resource: 'arn:aws:events:*:*:event-bus/some-event-bus',
},
]),
])
).to.be.fulfilled.then(() => {
const { Resources } = serverless.service.provider.compiledCloudFormationTemplate;
const customResourcesZipFilePath = path.join(
tmpDirPath,
'.serverless',
'custom-resources.zip'
);
expect(execAsyncStub).to.have.callCount(3);
expect(fs.existsSync(customResourcesZipFilePath)).to.equal(true);
// S3 Lambda Function
expect(Resources.CustomDashresourceDashexistingDashs3LambdaFunction).to.deep.equal({
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
S3Key: 'artifact-dir-name/custom-resources.zip',
},
FunctionName: `${serviceName}-dev-custom-resource-existing-s3`,
Handler: 's3/handler.handler',
MemorySize: 1024,
Role: cfnRoleArn,
Runtime: 'nodejs10.x',
Timeout: 180,
},
DependsOn: [],
});
// Cognito User Pool Lambda Function
expect(Resources.CustomDashresourceDashexistingDashcupLambdaFunction).to.deep.equal({
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
S3Key: 'artifact-dir-name/custom-resources.zip',
},
FunctionName: `${serviceName}-dev-custom-resource-existing-cup`,
Handler: 'cognitoUserPool/handler.handler',
MemorySize: 1024,
Role: cfnRoleArn,
Runtime: 'nodejs10.x',
Timeout: 180,
},
DependsOn: [],
});
// Event Bridge Lambda Function
expect(Resources.CustomDashresourceDasheventDashbridgeLambdaFunction).to.deep.equal({
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
S3Key: 'artifact-dir-name/custom-resources.zip',
},
FunctionName: `${serviceName}-dev-custom-resource-event-bridge`,
Handler: 'eventBridge/handler.handler',
MemorySize: 1024,
Role: cfnRoleArn,
Runtime: 'nodejs10.x',
Timeout: 180,
},
DependsOn: [],
});
// Iam Role
expect(Resources.IamRoleCustomResourcesLambdaExecution).to.be.undefined;
});
});
it('should setup CloudWatch Logs when logs.frameworkLambda is true', () => {
serverless.service.provider.logs = { frameworkLambda: true };
return BbPromise.all([

View File

@ -72,13 +72,15 @@ class AwsCompileStreamEvents {
!(
_.has(event.stream.arn, 'Fn::ImportValue') ||
_.has(event.stream.arn, 'Fn::GetAtt') ||
(_.has(event.stream.arn, 'Ref') &&
_.has(this.serverless.service.resources.Parameters, event.stream.arn.Ref)) ||
_.has(event.stream.arn, 'Fn::Join')
)
) {
const errorMessage = [
`Bad dynamic ARN property on stream event in function "${functionName}"`,
' If you use a dynamic "arn" (such as with Fn::GetAtt, Fn::Join',
' or Fn::ImportValue) there must only be one key (either Fn::GetAtt, Fn::Join',
' If you use a dynamic "arn" (such as with Fn::GetAtt, Fn::Join, Ref',
' or Fn::ImportValue) there must only be one key (either Fn::GetAtt, Fn::Join, Ref',
' or Fn::ImportValue) in the arn object. Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
@ -108,6 +110,8 @@ class AwsCompileStreamEvents {
return EventSourceArn['Fn::GetAtt'][0];
} else if (EventSourceArn['Fn::ImportValue']) {
return EventSourceArn['Fn::ImportValue'];
} else if (EventSourceArn.Ref) {
return EventSourceArn.Ref;
} else if (EventSourceArn['Fn::Join']) {
// [0] is the used delimiter, [1] is the array with values
const name = EventSourceArn['Fn::Join'][1].slice(-1).pop();
@ -147,9 +151,9 @@ class AwsCompileStreamEvents {
) {
dependsOn = `"${funcRole['Fn::GetAtt'][0]}"`;
} else if (
// otherwise, check if we have an import
// otherwise, check if we have an import or parameters ref
typeof funcRole === 'object' &&
'Fn::ImportValue' in funcRole
('Fn::ImportValue' in funcRole || 'Ref' in funcRole)
) {
dependsOn = '[]';
} else if (typeof funcRole === 'string') {

View File

@ -234,6 +234,35 @@ describe('AwsCompileStreamEvents', () => {
).to.equal(null);
});
it('should not throw error if IAM role is referenced from cloudformation parameters', () => {
awsCompileStreamEvents.serverless.service.functions = {
first: {
role: { Ref: 'MyStreamRoleArn' },
events: [
{
// doesn't matter if DynamoDB or Kinesis stream
stream: 'arn:aws:dynamodb:region:account:table/foo/stream/1',
},
],
},
};
// pretend that the default IamRoleLambdaExecution is not in place
awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = null;
expect(() => {
awsCompileStreamEvents.compileStreamEvents();
}).to.not.throw(Error);
expect(
awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
.FirstEventSourceMappingDynamodbFoo.DependsOn.length
).to.equal(0);
expect(
awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
.IamRoleLambdaExecution
).to.equal(null);
});
it('should not throw error if IAM role is imported', () => {
awsCompileStreamEvents.serverless.service.functions = {
first: {
@ -444,6 +473,14 @@ describe('AwsCompileStreamEvents', () => {
});
it('should allow specifying DynamoDB and Kinesis streams as CFN reference types', () => {
awsCompileStreamEvents.serverless.service.resources.Parameters = {
SomeDdbTableStreamArn: {
Type: 'String',
},
ForeignKinesisStreamArn: {
Type: 'String',
},
};
awsCompileStreamEvents.serverless.service.functions = {
first: {
events: [
@ -481,6 +518,18 @@ describe('AwsCompileStreamEvents', () => {
type: 'kinesis',
},
},
{
stream: {
arn: { Ref: 'SomeDdbTableStreamArn' },
type: 'dynamodb',
},
},
{
stream: {
arn: { Ref: 'ForeignKinesisStreamArn' },
type: 'kinesis',
},
},
],
},
};
@ -507,6 +556,9 @@ describe('AwsCompileStreamEvents', () => {
{
'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'],
},
{
Ref: 'SomeDdbTableStreamArn',
},
],
});
@ -537,6 +589,60 @@ describe('AwsCompileStreamEvents', () => {
],
],
});
expect(
awsCompileStreamEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[1]
).to.deep.equal({
Effect: 'Allow',
Action: [
'kinesis:GetRecords',
'kinesis:GetShardIterator',
'kinesis:DescribeStream',
'kinesis:ListStreams',
],
Resource: [
{
'Fn::ImportValue': 'ForeignKinesis',
},
{
'Fn::Join': [
':',
[
'arn',
'aws',
'kinesis',
{
Ref: 'AWS::Region',
},
{
Ref: 'AWS::AccountId',
},
'stream/MyStream',
],
],
},
{
Ref: 'ForeignKinesisStreamArn',
},
],
});
});
it('fails if Ref/dynamic stream ARN is used without defining it to the CF parameters', () => {
awsCompileStreamEvents.serverless.service.functions = {
first: {
events: [
{
stream: {
arn: { Ref: 'SomeDdbTableStreamArn' },
},
},
],
},
};
expect(() => awsCompileStreamEvents.compileStreamEvents()).to.throw(Error);
});
it('fails if Fn::GetAtt/dynamic stream ARN is used without a type', () => {

View File

@ -3,11 +3,13 @@
const _ = require('lodash');
const BbPromise = require('bluebird');
const path = require('path');
const fse = require('fs-extra');
const chai = require('chai');
const sinon = require('sinon');
const Package = require('../package');
const Serverless = require('../../../Serverless');
const serverlessConfigFileUtils = require('../../../../lib/utils/getServerlessConfigFile');
const { createTmpDir } = require('../../../../tests/utils/fs');
// Configure chai
chai.use(require('chai-as-promised'));
@ -604,4 +606,55 @@ describe('#packageService()', () => {
);
});
});
describe('#resolveFilePathsFromPatterns()', () => {
// NOTE: the path.join in `beforeEach` will take care of OS
// independent file paths
const handlerFile = 'src/function/handler.js';
const utilsFile = 'src/utils/utils.js';
let servicePath;
beforeEach(() => {
servicePath = createTmpDir();
fse.ensureFileSync(path.join(servicePath, handlerFile));
fse.ensureFileSync(path.join(servicePath, utilsFile));
});
it('should exclude all and include function/handler.js', () => {
const params = {
exclude: ['**'],
include: [handlerFile],
};
serverless.config.servicePath = servicePath;
return expect(packagePlugin.resolveFilePathsFromPatterns(params)).to.be.fulfilled.then(
actual => expect(actual).to.deep.equal([handlerFile])
);
});
it('should include file specified with `!` in exclude params', () => {
const params = {
exclude: ['**', `!${utilsFile}`],
include: [handlerFile],
};
serverless.config.servicePath = servicePath;
return expect(packagePlugin.resolveFilePathsFromPatterns(params)).to.be.fulfilled.then(
actual => expect(actual).to.deep.equal([handlerFile, utilsFile])
);
});
it('should exclude file specified with `!` in include params', () => {
const params = {
exclude: [],
include: [`!${utilsFile}`],
};
const expected = [handlerFile];
serverless.config.servicePath = servicePath;
return expect(packagePlugin.resolveFilePathsFromPatterns(params)).to.be.fulfilled.then(
actual => expect(actual).to.deep.equal(expected)
);
});
});
});

View File

@ -2,4 +2,4 @@
const configUtils = require('./config');
module.exports = configUtils.get('trackingDisabled');
module.exports = Boolean(process.env.SLS_TRACKING_DISABLED || configUtils.get('trackingDisabled'));

View File

@ -2,8 +2,8 @@
const { expect } = require('chai');
const isTabtabCompletionSuported = require('../isTrackingDisabled');
const isSuported = require('./isSupported');
describe('isTabtabCompletionSuported', () => {
it('Should resolve boolean', () => expect(typeof isTabtabCompletionSuported).to.equal('boolean'));
it('Should resolve boolean', () => expect(typeof isSuported).to.equal('boolean'));
});

View File

@ -1,6 +1,6 @@
{
"name": "serverless",
"version": "1.54.0",
"version": "1.55.0",
"engines": {
"node": ">=6.0"
},
@ -73,11 +73,13 @@
"root": true
},
"eslintIgnore": [
"lib/plugins/create/templates/**"
"lib/plugins/create/templates/**",
"lib/plugins/aws/customResources/node_modules/**"
],
"mocha": {
"reporter": "./tests/mocha-reporter",
"require": [
"@serverless/test/setup/log",
"@serverless/test/setup/async-leaks-detector",
"@serverless/test/setup/async-leaks-detector/bluebird-patch",
"@serverless/test/setup/mock-homedir",
@ -87,34 +89,35 @@
},
"devDependencies": {
"@serverless/eslint-config": "^1.2.0",
"@serverless/test": "^2.1.0",
"@serverless/test": "^2.4.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"child-process-ext": "^2.1.0",
"cli-progress-footer": "^1.1.1",
"coveralls": "^3.0.6",
"coveralls": "^3.0.7",
"eslint": "^6.5.1",
"eslint-plugin-import": "^2.18.2",
"git-list-updated": "^1.2.1",
"mocha": "^6.2.1",
"log": "^6.0.0",
"mocha": "^6.2.2",
"mocha-lcov-reporter": "^1.3.0",
"mock-require": "^3.0.3",
"nyc": "^14.1.1",
"prettier": "^1.18.2",
"process-utils": "^2.5.0",
"process-utils": "^3.0.1",
"proxyquire": "^2.1.3",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"strip-ansi": "^5.2.0",
"ws": "^7.1.2"
"ws": "^7.2.0"
},
"dependencies": {
"@serverless/cli": "^1.2.3",
"@serverless/cli": "^1.4.0",
"@serverless/enterprise-plugin": "^3.1.2",
"archiver": "^1.3.0",
"async": "^1.5.2",
"aws-sdk": "^2.545.0",
"bluebird": "^3.7.0",
"aws-sdk": "^2.554.0",
"bluebird": "^3.7.1",
"cachedir": "^2.2.0",
"chalk": "^2.4.2",
"ci-info": "^1.6.0",

View File

@ -6,4 +6,18 @@ module.exports = {
// console.info allowed to report on long going tasks or valuable debug information
'no-console': ['error', { allow: ['info'] }],
},
overrides: [
{
files: ['utils/**.js'],
parserOptions: {
ecmaVersion: 2015,
},
},
{
files: ['utils/aws-cleanup.js', 'utils/integration.js'],
parserOptions: {
ecmaVersion: 2017,
},
},
],
};

View File

@ -3,17 +3,16 @@
const path = require('path');
const AWS = require('aws-sdk');
const _ = require('lodash');
const fetch = require('node-fetch');
const { expect } = require('chai');
const { getTmpDirPath, readYamlFile, writeYamlFile } = require('../../utils/fs');
const { region, confirmCloudWatchLogs } = require('../../utils/misc');
const {
region,
confirmCloudWatchLogs,
createTestService,
deployService,
removeService,
} = require('../../utils/misc');
fetch,
} = require('../../utils/integration');
const { createRestApi, deleteRestApi, getResources } = require('../../utils/api-gateway');
const CF = new AWS.CloudFormation({ region });
@ -30,11 +29,11 @@ describe('AWS - API Gateway Integration Test', function() {
let apiKey;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
serverlessFilePath = path.join(tmpDirPath, 'serverless.yml');
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
serverlessConfigHook:
// Ensure unique API key for each test (to avoid collision among concurrent CI runs)
@ -46,36 +45,12 @@ describe('AWS - API Gateway Integration Test', function() {
serviceName = serverlessConfig.service;
stackName = `${serviceName}-${stage}`;
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
// create an external REST API
const externalRestApiName = `${stage}-${serviceName}-ext-api`;
return createRestApi(externalRestApiName)
.then(restApiMeta => {
restApiId = restApiMeta.id;
return getResources(restApiId);
})
.then(resources => {
restApiRootResourceId = resources[0].id;
console.info(
'Created external rest API ' +
`(id: ${restApiId}, root resource id: ${restApiRootResourceId})`
);
});
await deployService(tmpDirPath);
});
after(() => {
// NOTE: deleting the references to the old, external REST API
const serverless = readYamlFile(serverlessFilePath);
delete serverless.provider.apiGateway.restApiId;
delete serverless.provider.apiGateway.restApiRootResourceId;
writeYamlFile(serverlessFilePath, serverless);
// NOTE: deploying once again to get the stack into the original state
console.info('Redeploying service...');
deployService(tmpDirPath);
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
console.info('Deleting external rest API...');
return deleteRestApi(restApiId);
await removeService(tmpDirPath);
});
beforeEach(() => {
@ -224,7 +199,7 @@ describe('AWS - API Gateway Integration Test', function() {
});
describe('Using stage specific configuration', () => {
before(() => {
before(async () => {
const serverless = readYamlFile(serverlessFilePath);
// enable Logs, Tags and Tracing
_.merge(serverless.provider, {
@ -240,7 +215,7 @@ describe('AWS - API Gateway Integration Test', function() {
},
});
writeYamlFile(serverlessFilePath, serverless);
deployService(tmpDirPath);
await deployService(tmpDirPath);
});
it('should update the stage without service interruptions', () => {
@ -261,7 +236,22 @@ describe('AWS - API Gateway Integration Test', function() {
// NOTE: this test should be at the very end because we're using an external REST API here
describe('when using an existing REST API with stage specific configuration', () => {
before(() => {
before(async () => {
// create an external REST API
const externalRestApiName = `${stage}-${serviceName}-ext-api`;
await createRestApi(externalRestApiName)
.then(restApiMeta => {
restApiId = restApiMeta.id;
return getResources(restApiId);
})
.then(resources => {
restApiRootResourceId = resources[0].id;
console.info(
'Created external rest API ' +
`(id: ${restApiId}, root resource id: ${restApiRootResourceId})`
);
});
const serverless = readYamlFile(serverlessFilePath);
// enable Logs, Tags and Tracing
_.merge(serverless.provider, {
@ -281,7 +271,21 @@ describe('AWS - API Gateway Integration Test', function() {
},
});
writeYamlFile(serverlessFilePath, serverless);
deployService(tmpDirPath);
console.info('Redeploying service (with external Rest API ID)...');
await deployService(tmpDirPath);
});
after(async () => {
// NOTE: deleting the references to the old, external REST API
const serverless = readYamlFile(serverlessFilePath);
delete serverless.provider.apiGateway.restApiId;
delete serverless.provider.apiGateway.restApiRootResourceId;
writeYamlFile(serverlessFilePath, serverless);
// NOTE: deploying once again to get the stack into the original state
console.info('Redeploying service (without external Rest API ID)...');
await deployService(tmpDirPath);
console.info('Deleting external rest API...');
return deleteRestApi(restApiId);
});
it('should update the stage without service interruptions', () => {

View File

@ -20,7 +20,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - Cognito User Pool Integration Test', function() {
@ -34,10 +34,10 @@ describe('AWS - Cognito User Pool Integration Test', function() {
let poolExistingSimpleSetupConfig;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -67,13 +67,13 @@ describe('AWS - Cognito User Pool Integration Test', function() {
createUserPool(poolExistingMultiSetup),
]).then(() => {
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
});
after(() => {
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
await removeService(tmpDirPath);
console.info('Deleting Cognito User Pools');
return BbPromise.all([
deleteUserPool(poolExistingSimpleSetup),

View File

@ -5,12 +5,13 @@ const { expect } = require('chai');
const { getTmpDirPath, readYamlFile, writeYamlFile } = require('../../utils/fs');
const { createEventBus, putEvents, deleteEventBus } = require('../../utils/eventBridge');
const {
createTestService,
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - Event Bridge Integration Test', function() {
@ -31,10 +32,10 @@ describe('AWS - Event Bridge Integration Test', function() {
},
];
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -61,13 +62,13 @@ describe('AWS - Event Bridge Integration Test', function() {
writeYamlFile(serverlessFilePath, config);
// deploy the service
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
});
after(() => {
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
await removeService(tmpDirPath);
console.info(`Deleting Event Bus "${arnEventBusName}"...`);
return deleteEventBus(arnEventBusName);
});

View File

@ -10,7 +10,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - IoT Integration Test', function() {
@ -21,10 +21,10 @@ describe('AWS - IoT Integration Test', function() {
let tmpDirPath;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -43,7 +43,7 @@ describe('AWS - IoT Integration Test', function() {
after(() => {
// Topics are ephemeral and IoT endpoint is part of the account
console.info('Removing service...');
removeService(tmpDirPath);
return removeService(tmpDirPath);
});
describe('Basic Setup', () => {

View File

@ -11,7 +11,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - S3 Integration Test', function() {
@ -25,10 +25,10 @@ describe('AWS - S3 Integration Test', function() {
let bucketExistingComplexSetup;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -57,13 +57,13 @@ describe('AWS - S3 Integration Test', function() {
createBucket(bucketExistingComplexSetup),
]).then(() => {
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
});
after(() => {
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
await removeService(tmpDirPath);
console.info('Deleting S3 buckets');
return BbPromise.all([
deleteBucket(bucketExistingSimpleSetup),

View File

@ -9,7 +9,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - Schedule Integration Test', function() {
@ -19,22 +19,22 @@ describe('AWS - Schedule Integration Test', function() {
let tmpDirPath;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
});
serviceName = serverlessConfig.service;
stackName = `${serviceName}-${stage}`;
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
after(() => {
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
return removeService(tmpDirPath);
});
describe('Minimal Setup', () => {

View File

@ -11,7 +11,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - SNS Integration Test', function() {
@ -24,10 +24,10 @@ describe('AWS - SNS Integration Test', function() {
let existingTopicName;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -53,13 +53,13 @@ describe('AWS - SNS Integration Test', function() {
console.info(`Creating SNS topic "${existingTopicName}"...`);
return createSnsTopic(existingTopicName).then(() => {
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
});
after(() => {
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
await removeService(tmpDirPath);
console.info('Deleting SNS topics');
return removeSnsTopic(existingTopicName);
});

View File

@ -10,7 +10,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - SQS Integration Test', function() {
@ -21,10 +21,10 @@ describe('AWS - SQS Integration Test', function() {
let queueName;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -41,13 +41,13 @@ describe('AWS - SQS Integration Test', function() {
console.info(`Creating SQS queue "${queueName}"...`);
return createSqsQueue(queueName).then(() => {
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
});
after(() => {
after(async () => {
console.info('Removing service...');
removeService(tmpDirPath);
await removeService(tmpDirPath);
console.info('Deleting SQS queue');
return deleteSqsQueue(queueName);
});

View File

@ -15,7 +15,7 @@ const {
deployService,
removeService,
waitForFunctionLogs,
} = require('../../utils/misc');
} = require('../../utils/integration');
const { getMarkers } = require('../shared/utils');
describe('AWS - Stream Integration Test', function() {
@ -28,10 +28,10 @@ describe('AWS - Stream Integration Test', function() {
const historicStreamMessage = 'Hello from the Kinesis horizon!';
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
filesToAdd: [path.join(__dirname, '..', 'shared')],
serverlessConfigHook:
@ -56,13 +56,13 @@ describe('AWS - Stream Integration Test', function() {
console.info(
`Deploying "${stackName}" service with DynamoDB table resource "${tableName}"...`
);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
});
after(() => {
after(async () => {
console.info(`Removing service (and DynamoDB table resource "${tableName}")...`);
removeService(tmpDirPath);
await removeService(tmpDirPath);
console.info('Deleting Kinesis stream');
return deleteKinesisStream(streamName);
});

View File

@ -7,14 +7,8 @@ const _ = require('lodash');
const { expect } = require('chai');
const { getTmpDirPath, readYamlFile, writeYamlFile } = require('../../utils/fs');
const {
region,
confirmCloudWatchLogs,
createTestService,
deployService,
removeService,
wait,
} = require('../../utils/misc');
const { region, confirmCloudWatchLogs, wait } = require('../../utils/misc');
const { createTestService, deployService, removeService } = require('../../utils/integration');
const {
createApi,
deleteApi,
@ -33,22 +27,22 @@ describe('AWS - API Gateway Websocket Integration Test', function() {
let serverlessFilePath;
const stage = 'dev';
before(() => {
before(async () => {
tmpDirPath = getTmpDirPath();
console.info(`Temporary path: ${tmpDirPath}`);
serverlessFilePath = path.join(tmpDirPath, 'serverless.yml');
const serverlessConfig = createTestService(tmpDirPath, {
const serverlessConfig = await createTestService(tmpDirPath, {
templateDir: path.join(__dirname, 'service'),
});
serviceName = serverlessConfig.service;
stackName = `${serviceName}-${stage}`;
console.info(`Deploying "${stackName}" service...`);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
after(() => {
console.info('Removing service...');
removeService(tmpDirPath);
return removeService(tmpDirPath);
});
describe('Minimal Setup', () => {
@ -108,7 +102,7 @@ describe('AWS - API Gateway Websocket Integration Test', function() {
},
});
writeYamlFile(serverlessFilePath, serverless);
deployService(tmpDirPath);
return deployService(tmpDirPath);
});
after(async () => {
@ -121,7 +115,7 @@ describe('AWS - API Gateway Websocket Integration Test', function() {
await deleteStage(websocketApiId, 'dev');
// NOTE: deploying once again to get the stack into the original state
console.info('Redeploying service...');
deployService(tmpDirPath);
await deployService(tmpDirPath);
console.info('Deleting external websocket API...');
await deleteApi(websocketApiId);
});

View File

@ -3,15 +3,15 @@
const path = require('path');
const fs = require('fs');
const fse = require('fs-extra');
const BbPromise = require('bluebird');
const AWS = require('aws-sdk');
const stripAnsi = require('strip-ansi');
const { expect } = require('chai');
const { execSync } = require('../utils/child-process');
const spawn = require('child-process-ext/spawn');
const resolveAwsEnv = require('@serverless/test/resolve-aws-env');
const { getTmpDirPath } = require('../utils/fs');
const { region, getServiceName } = require('../utils/misc');
const serverlessExec = path.join(__dirname, '..', '..', 'bin', 'serverless');
const serverlessExec = require('../serverless-binary');
const CF = new AWS.CloudFormation({ region });
@ -19,36 +19,58 @@ describe('Service Lifecyle Integration Test', function() {
this.timeout(1000 * 60 * 10); // Involves time-taking deploys
const templateName = 'aws-nodejs';
const tmpDir = getTmpDirPath();
const env = resolveAwsEnv();
const spawnOptions = {
cwd: tmpDir,
env,
// As in invoke we optionally read stdin, we need to ensure it's closed
// See https://github.com/sindresorhus/get-stdin/issues/13#issuecomment-279234249
shouldCloseStdin: true,
};
let serviceName;
let StackName;
before(() => {
serviceName = getServiceName();
StackName = `${serviceName}-dev`;
console.info(`Temporary path: ${tmpDir}`);
fse.mkdirsSync(tmpDir);
});
it('should create service in tmp directory', () => {
execSync(`${serverlessExec} create --template ${templateName} --name ${serviceName}`, {
cwd: tmpDir,
});
after(async () => {
try {
await CF.describeStacks({ StackName }).promise();
} catch (error) {
if (error.message.indexOf('does not exist') > -1) return;
throw error;
}
await spawn(serverlessExec, ['remove'], { cwd: tmpDir, env });
});
it('should create service in tmp directory', async () => {
await spawn(
serverlessExec,
['create', '--template', templateName, '--name', serviceName],
spawnOptions
);
expect(fs.existsSync(path.join(tmpDir, 'serverless.yml'))).to.be.equal(true);
expect(fs.existsSync(path.join(tmpDir, 'handler.js'))).to.be.equal(true);
});
it('should deploy service to aws', () => {
execSync(`${serverlessExec} deploy`, { cwd: tmpDir });
it('should deploy service to aws', async () => {
await spawn(serverlessExec, ['deploy'], { cwd: tmpDir, env });
return CF.describeStacks({ StackName })
.promise()
.then(d => expect(d.Stacks[0].StackStatus).to.be.equal('UPDATE_COMPLETE'));
const d = await CF.describeStacks({ StackName }).promise();
expect(d.Stacks[0].StackStatus).to.be.equal('UPDATE_COMPLETE');
});
it('should invoke function from aws', () => {
const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`, {
cwd: tmpDir,
});
const result = JSON.parse(Buffer.from(invoked, 'base64').toString());
it('should invoke function from aws', async () => {
const { stdoutBuffer: invoked } = await spawn(
serverlessExec,
['invoke', '--function', 'hello', '--noGreeting', 'true'],
spawnOptions
);
const result = JSON.parse(invoked);
// parse it once again because the body is stringified to be LAMBDA-PROXY ready
const message = JSON.parse(result.body).message;
expect(message).to.be.equal('Go Serverless v1.0! Your function executed successfully!');
@ -64,20 +86,26 @@ describe('Service Lifecyle Integration Test', function() {
`;
fs.writeFileSync(path.join(tmpDir, 'handler.js'), newHandler);
execSync(`${serverlessExec} deploy`, { cwd: tmpDir });
return spawn(serverlessExec, ['deploy'], spawnOptions);
});
it('should invoke updated function from aws', () => {
const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`, {
cwd: tmpDir,
});
const result = JSON.parse(Buffer.from(invoked, 'base64').toString());
it('should invoke updated function from aws', async () => {
const { stdoutBuffer: invoked } = await spawn(
serverlessExec,
['invoke', '--function', 'hello', '--noGreeting', 'true'],
spawnOptions
);
const result = JSON.parse(invoked);
expect(result.message).to.be.equal('Service Update Succeeded');
});
it('should list existing deployments and roll back to first deployment', () => {
it('should list existing deployments and roll back to first deployment', async () => {
let timestamp;
const listDeploys = execSync(`${serverlessExec} deploy list`, { cwd: tmpDir });
const { stdoutBuffer: listDeploys } = await spawn(
serverlessExec,
['deploy', 'list'],
spawnOptions
);
const output = stripAnsi(listDeploys.toString());
const match = output.match(new RegExp('Datetime: (.+)'));
if (match) {
@ -86,26 +114,31 @@ describe('Service Lifecyle Integration Test', function() {
// eslint-disable-next-line no-unused-expressions
expect(timestamp).to.not.undefined;
execSync(`${serverlessExec} rollback -t ${timestamp}`, { cwd: tmpDir });
await spawn(serverlessExec, ['rollback', '-t', timestamp], { cwd: tmpDir, env });
const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`, {
cwd: tmpDir,
});
const result = JSON.parse(Buffer.from(invoked, 'base64').toString());
const { stdoutBuffer: invoked } = await spawn(
serverlessExec,
['invoke', '--function', 'hello', '--noGreeting', 'true'],
spawnOptions
);
const result = JSON.parse(invoked);
// parse it once again because the body is stringified to be LAMBDA-PROXY ready
const message = JSON.parse(result.body).message;
expect(message).to.be.equal('Go Serverless v1.0! Your function executed successfully!');
});
it('should remove service from aws', () => {
execSync(`${serverlessExec} remove`, { cwd: tmpDir });
it('should remove service from aws', async () => {
await spawn(serverlessExec, ['remove'], { cwd: tmpDir, env });
return CF.describeStacks({ StackName })
.promise()
.then(d => expect(d.Stacks[0].StackStatus).to.be.equal('DELETE_COMPLETE'))
.catch(error => {
if (error.message.indexOf('does not exist') > -1) return BbPromise.resolve();
throw new Error(error);
});
const d = await (async () => {
try {
return await CF.describeStacks({ StackName }).promise();
} catch (error) {
if (error.message.indexOf('does not exist') > -1) return null;
throw error;
}
})();
if (!d) return;
expect(d.Stacks[0].StackStatus).to.be.equal('DELETE_COMPLETE');
});
});

View File

@ -5,7 +5,7 @@ const path = require('path');
const { expect } = require('chai');
const fse = require('fs-extra');
const { execSync } = require('../utils/child-process');
const { serverlessExec } = require('../utils/misc');
const serverlessExec = require('../serverless-binary');
const { getTmpDirPath } = require('../utils/fs');
const fixturePaths = {

View File

@ -4,7 +4,7 @@ const path = require('path');
const { expect } = require('chai');
const fse = require('fs-extra');
const { execSync } = require('../utils/child-process');
const { serverlessExec } = require('../utils/misc');
const serverlessExec = require('../serverless-binary');
const { getTmpDirPath, listZipFiles } = require('../utils/fs');
const fixturePaths = {

View File

@ -0,0 +1,8 @@
'use strict';
const path = require('path');
module.exports = (() => {
if (process.env.SERVERLESS_BINARY_PATH) return path.resolve(process.env.SERVERLESS_BINARY_PATH);
return path.join(__dirname, '../bin/serverless.js');
})();

View File

@ -1,6 +1,7 @@
'use strict';
const AWS = require('aws-sdk');
const log = require('log').get('aws');
const { region, persistentRequest } = require('../misc');
function createUserPool(name, config = {}) {
@ -30,6 +31,7 @@ function deleteUserPool(name) {
}
function findUserPoolByName(name) {
log.debug('find cognito user pool by name %s', name);
const cognito = new AWS.CognitoIdentityServiceProvider({ region });
const params = {
@ -42,6 +44,7 @@ function findUserPoolByName(name) {
.listUserPools(params)
.promise()
.then(result => {
log.debug('cognito.listUserPools %j', result);
const matches = result.UserPools.filter(pool => pool.Name === name);
if (matches.length) {
return matches.shift();
@ -57,7 +60,13 @@ function findUserPoolByName(name) {
function describeUserPool(userPoolId) {
const cognito = new AWS.CognitoIdentityServiceProvider({ region });
return cognito.describeUserPool({ UserPoolId: userPoolId }).promise();
return cognito
.describeUserPool({ UserPoolId: userPoolId })
.promise()
.then(result => {
log.debug('cognito.describeUserPool %s %j', userPoolId, result);
return result;
});
}
function createUser(userPoolId, username, password) {

132
tests/utils/integration.js Normal file
View File

@ -0,0 +1,132 @@
// Integration tests related utils
'use strict';
const path = require('path');
const fse = require('fs-extra');
const spawn = require('child-process-ext/spawn');
const nodeFetch = require('node-fetch');
const logFetch = require('log').get('fetch');
const resolveAwsEnv = require('@serverless/test/resolve-aws-env');
const { getServiceName, wait } = require('./misc');
const { readYamlFile, writeYamlFile } = require('./fs');
const serverlessExec = require('../serverless-binary');
const env = resolveAwsEnv();
async function createTestService(
tmpDir,
options = {
// Either templateName or templateDir have to be provided
templateName: null, // Generic template to use (e.g. 'aws-nodejs')
templateDir: null, // Path to custom pre-prepared service template
filesToAdd: [], // Array of additional files to add to the service directory
serverlessConfigHook: null, // Eventual hook that allows to customize serverless config
}
) {
const serviceName = getServiceName();
fse.mkdirsSync(tmpDir);
if (options.templateName) {
// create a new Serverless service
await spawn(serverlessExec, ['create', '--template', options.templateName], {
cwd: tmpDir,
env,
});
} else if (options.templateDir) {
fse.copySync(options.templateDir, tmpDir, { clobber: true, preserveTimestamps: true });
} else {
throw new Error("Either 'templateName' or 'templateDir' options have to be provided");
}
if (options.filesToAdd && options.filesToAdd.length) {
options.filesToAdd.forEach(filePath => {
fse.copySync(filePath, tmpDir, { preserveTimestamps: true });
});
}
const serverlessFilePath = path.join(tmpDir, 'serverless.yml');
const serverlessConfig = readYamlFile(serverlessFilePath);
// Ensure unique service name
serverlessConfig.service = serviceName;
if (options.serverlessConfigHook) options.serverlessConfigHook(serverlessConfig);
writeYamlFile(serverlessFilePath, serverlessConfig);
process.env.TOPIC_1 = `${serviceName}-1`;
process.env.TOPIC_2 = `${serviceName}-1`;
process.env.BUCKET_1 = `${serviceName}-1`;
process.env.BUCKET_2 = `${serviceName}-2`;
process.env.COGNITO_USER_POOL_1 = `${serviceName}-1`;
process.env.COGNITO_USER_POOL_2 = `${serviceName}-2`;
return serverlessConfig;
}
async function deployService(cwd) {
return spawn(serverlessExec, ['deploy'], { cwd, env });
}
async function removeService(cwd) {
return spawn(serverlessExec, ['remove'], { cwd, env });
}
async function getFunctionLogs(cwd, functionName) {
let logs;
try {
({ stdoutBuffer: logs } = await spawn(
serverlessExec,
['logs', '--function', functionName, '--noGreeting', 'true'],
{
cwd,
env,
}
));
} catch (_) {
// Attempting to read logs before first invocation will will result in a "No existing streams for the function" error
return null;
}
return String(logs);
}
async function waitForFunctionLogs(cwd, functionName, startMarker, endMarker) {
await wait(2000);
const logs = await getFunctionLogs(cwd, functionName);
if (logs && logs.includes(startMarker) && logs.includes(endMarker)) return logs;
return waitForFunctionLogs(cwd, functionName, startMarker, endMarker);
}
let lastRequestId = 0;
async function fetch(url, options) {
const requestId = ++lastRequestId;
logFetch.debug('[%d] %s %o', requestId, url, options);
let response;
try {
response = await nodeFetch(url, options);
} catch (error) {
logFetch.error('[%d] request error: %o', requestId, error);
throw error;
}
/* eslint-disable no-underscore-dangle */
logFetch.debug('[%d] %d %j', requestId, response.status, response.headers._headers);
const responseDecodeResult = response._decode();
response._decode = () => responseDecodeResult;
/* eslint-enable */
responseDecodeResult.then(
buffer => logFetch.debug('[%d] %s', requestId, String(buffer)),
error => logFetch.error('[%d] response resolution error: %o', requestId, error)
);
return response;
}
module.exports = {
createTestService,
deployService,
env,
fetch,
removeService,
waitForFunctionLogs,
};

View File

@ -1,11 +1,7 @@
'use strict';
const path = require('path');
const fse = require('fs-extra');
const BbPromise = require('bluebird');
const CloudWatchLogsSdk = require('aws-sdk/clients/cloudwatchlogs');
const { execSync } = require('../child-process');
const { readYamlFile, writeYamlFile } = require('../fs');
const logger = console;
@ -14,8 +10,6 @@ const cloudWatchLogsSdk = new CloudWatchLogsSdk({ region });
const testServiceIdentifier = 'integ-test';
const serverlessExec = path.resolve(__dirname, '..', '..', '..', 'bin', 'serverless');
const serviceNameRegex = new RegExp(`${testServiceIdentifier}-d+`);
function getServiceName() {
@ -23,14 +17,6 @@ function getServiceName() {
return `${testServiceIdentifier}-${hrtime[1]}`;
}
function deployService(cwd) {
execSync(`${serverlessExec} deploy`, { cwd });
}
function removeService(cwd) {
execSync(`${serverlessExec} remove`, { cwd });
}
function replaceEnv(values) {
const originals = {};
for (const key of Object.keys(values)) {
@ -48,80 +34,6 @@ function replaceEnv(values) {
return originals;
}
function createTestService(
tmpDir,
options = {
// Either templateName or templateDir have to be provided
templateName: null, // Generic template to use (e.g. 'aws-nodejs')
templateDir: null, // Path to custom pre-prepared service template
filesToAdd: [], // Array of additional files to add to the service directory
serverlessConfigHook: null, // Eventual hook that allows to customize serverless config
}
) {
const serviceName = getServiceName();
fse.mkdirsSync(tmpDir);
if (options.templateName) {
// create a new Serverless service
execSync(`${serverlessExec} create --template ${options.templateName}`, { cwd: tmpDir });
} else if (options.templateDir) {
fse.copySync(options.templateDir, tmpDir, { clobber: true, preserveTimestamps: true });
} else {
throw new Error("Either 'templateName' or 'templateDir' options have to be provided");
}
if (options.filesToAdd && options.filesToAdd.length) {
options.filesToAdd.forEach(filePath => {
fse.copySync(filePath, tmpDir, { preserveTimestamps: true });
});
}
const serverlessFilePath = path.join(tmpDir, 'serverless.yml');
const serverlessConfig = readYamlFile(serverlessFilePath);
// Ensure unique service name
serverlessConfig.service = serviceName;
if (options.serverlessConfigHook) options.serverlessConfigHook(serverlessConfig);
writeYamlFile(serverlessFilePath, serverlessConfig);
process.env.TOPIC_1 = `${serviceName}-1`;
process.env.TOPIC_2 = `${serviceName}-1`;
process.env.BUCKET_1 = `${serviceName}-1`;
process.env.BUCKET_2 = `${serviceName}-2`;
process.env.COGNITO_USER_POOL_1 = `${serviceName}-1`;
process.env.COGNITO_USER_POOL_2 = `${serviceName}-2`;
return serverlessConfig;
}
function getFunctionLogs(cwd, functionName) {
try {
const logs = execSync(`${serverlessExec} logs --function ${functionName} --noGreeting true`, {
cwd,
});
const logsString = Buffer.from(logs, 'base64').toString();
process.stdout.write(logsString);
return logsString;
} catch (_) {
// Attempting to read logs before first invocation will will result in a "No existing streams for the function" error
return null;
}
}
function waitForFunctionLogs(cwd, functionName, startMarker, endMarker) {
let logs;
return new BbPromise(resolve => {
const interval = setInterval(() => {
logs = getFunctionLogs(cwd, functionName);
if (logs && logs.includes(startMarker) && logs.includes(endMarker)) {
clearInterval(interval);
return resolve(logs);
}
return null;
}, 2000);
});
}
/**
* Cloudwatch logs when turned on, are usually take some time for being effective
* This function allows to confirm that new setting (turned on cloudwatch logs)
@ -175,15 +87,9 @@ module.exports = {
region,
confirmCloudWatchLogs,
testServiceIdentifier,
serverlessExec,
serviceNameRegex,
getServiceName,
deployService,
removeService,
replaceEnv,
createTestService,
getFunctionLogs,
waitForFunctionLogs,
persistentRequest,
wait,
};