From c6c38b5f54ec2035f6ba92a4c607198f26539a06 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Tue, 16 May 2017 14:38:59 +0200 Subject: [PATCH] Add GooglePackage plugin --- index.js | 2 + index.test.js | 2 + package.json | 1 + package/googlePackage.js | 61 ++++ package/googlePackage.test.js | 121 +++++++ package/lib/cleanupServerlessDir.js | 19 + package/lib/cleanupServerlessDir.test.js | 77 ++++ package/lib/compileFunctions.js | 120 +++++++ package/lib/compileFunctions.test.js | 332 ++++++++++++++++++ package/lib/generateArtifactDirectoryName.js | 16 + .../lib/generateArtifactDirectoryName.test.js | 33 ++ package/lib/mergeServiceResources.js | 26 ++ package/lib/mergeServiceResources.test.js | 101 ++++++ package/lib/prepareDeployment.js | 47 +++ package/lib/prepareDeployment.test.js | 65 ++++ package/lib/writeFilesToDisk.js | 29 ++ package/lib/writeFilesToDisk.test.js | 73 ++++ .../templates/core-configuration-template.yml | 3 + 18 files changed, 1128 insertions(+) create mode 100644 package/googlePackage.js create mode 100644 package/googlePackage.test.js create mode 100644 package/lib/cleanupServerlessDir.js create mode 100644 package/lib/cleanupServerlessDir.test.js create mode 100644 package/lib/compileFunctions.js create mode 100644 package/lib/compileFunctions.test.js create mode 100644 package/lib/generateArtifactDirectoryName.js create mode 100644 package/lib/generateArtifactDirectoryName.test.js create mode 100644 package/lib/mergeServiceResources.js create mode 100644 package/lib/mergeServiceResources.test.js create mode 100644 package/lib/prepareDeployment.js create mode 100644 package/lib/prepareDeployment.test.js create mode 100644 package/lib/writeFilesToDisk.js create mode 100644 package/lib/writeFilesToDisk.test.js create mode 100644 package/templates/core-configuration-template.yml diff --git a/index.js b/index.js index 550c2d5..aa0c9a6 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ whole provider implementation. */ const GoogleProvider = require('./provider/googleProvider'); +const GooglePackage = require('./package/googlePackage'); const GoogleDeploy = require('./deploy/googleDeploy'); const GoogleRemove = require('./remove/googleRemove'); const GoogleInvoke = require('./invoke/googleInvoke'); @@ -19,6 +20,7 @@ class GoogleIndex { this.options = options; this.serverless.pluginManager.addPlugin(GoogleProvider); + this.serverless.pluginManager.addPlugin(GooglePackage); this.serverless.pluginManager.addPlugin(GoogleDeploy); this.serverless.pluginManager.addPlugin(GoogleRemove); this.serverless.pluginManager.addPlugin(GoogleInvoke); diff --git a/index.test.js b/index.test.js index 9cc304d..e707b80 100644 --- a/index.test.js +++ b/index.test.js @@ -2,6 +2,7 @@ const GoogleIndex = require('./index'); const GoogleProvider = require('./provider/googleProvider'); +const GooglePackage = require('./package/googlePackage'); const GoogleDeploy = require('./deploy/googleDeploy'); const GoogleRemove = require('./remove/googleRemove'); const GoogleInvoke = require('./invoke/googleInvoke'); @@ -36,6 +37,7 @@ describe('GoogleIndex', () => { const addedPlugins = serverless.plugins; expect(addedPlugins).toContain(GoogleProvider); + expect(addedPlugins).toContain(GooglePackage); expect(addedPlugins).toContain(GoogleDeploy); expect(addedPlugins).toContain(GoogleRemove); expect(addedPlugins).toContain(GoogleInvoke); diff --git a/package.json b/package.json index da53440..4a1973a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "async": "^2.1.4", "bluebird": "^3.4.7", "chalk": "^1.1.3", + "fs-extra": "^3.0.1", "googleapis": "^16.1.0", "lodash": "^4.17.4" }, diff --git a/package/googlePackage.js b/package/googlePackage.js new file mode 100644 index 0000000..d1cc375 --- /dev/null +++ b/package/googlePackage.js @@ -0,0 +1,61 @@ +'use strict'; + +const BbPromise = require('bluebird'); + +const cleanupServerlessDir = require('./lib/cleanupServerlessDir'); +const validate = require('../shared/validate'); +const utils = require('../shared/utils'); +const setDeploymentBucketName = require('../shared/setDeploymentBucketName'); +const prepareDeployment = require('./lib/prepareDeployment'); +const saveCreateTemplateFile = require('./lib/writeFilesToDisk'); +const mergeServiceResources = require('./lib/mergeServiceResources'); +const generateArtifactDirectoryName = require('./lib/generateArtifactDirectoryName'); +const compileFunctions = require('./lib/compileFunctions'); +const saveUpdateTemplateFile = require('./lib/writeFilesToDisk'); + +class GooglePackage { + constructor(serverless, options) { + this.serverless = serverless; + this.options = options; + this.provider = this.serverless.getProvider('google'); + + Object.assign( + this, + cleanupServerlessDir, + validate, + utils, + setDeploymentBucketName, + prepareDeployment, + saveCreateTemplateFile, + generateArtifactDirectoryName, + compileFunctions, + mergeServiceResources, + saveUpdateTemplateFile); + + this.hooks = { + 'package:cleanup': () => BbPromise.bind(this) + .then(this.cleanupServerlessDir), + + 'before:package:initialize': () => BbPromise.bind(this) + .then(this.validate) + .then(this.setDefaults), + + 'package:initialize': () => BbPromise.bind(this) + .then(this.setDeploymentBucketName) + .then(this.prepareDeployment) + .then(this.saveCreateTemplateFile), + + 'before:package:compileFunctions': () => BbPromise.bind(this) + .then(this.generateArtifactDirectoryName), + + 'package:compileFunctions': () => BbPromise.bind(this) + .then(this.compileFunctions), + + 'package:finalize': () => BbPromise.bind(this) + .then(this.mergeServiceResources) + .then(this.saveUpdateTemplateFile), + }; + } +} + +module.exports = GooglePackage; diff --git a/package/googlePackage.test.js b/package/googlePackage.test.js new file mode 100644 index 0000000..084def7 --- /dev/null +++ b/package/googlePackage.test.js @@ -0,0 +1,121 @@ +'use strict'; + +const sinon = require('sinon'); +const BbPromise = require('bluebird'); + +const GoogleProvider = require('../provider/googleProvider'); +const GooglePackage = require('./googlePackage'); +const Serverless = require('../test/serverless'); + +describe('GooglePackage', () => { + let serverless; + let options; + let googlePackage; + + beforeEach(() => { + serverless = new Serverless(); + options = { + stage: 'my-stage', + region: 'my-region', + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + googlePackage = new GooglePackage(serverless, options); + }); + + describe('#constructor()', () => { + it('should set the serverless instance', () => { + expect(googlePackage.serverless).toEqual(serverless); + }); + + it('should set options if provided', () => { + expect(googlePackage.options).toEqual(options); + }); + + it('should make the provider accessible', () => { + expect(googlePackage.provider).toBeInstanceOf(GoogleProvider); + }); + + describe('hooks', () => { + let cleanupServerlessDirStub; + let validateStub; + let setDefaultsStub; + let setDeploymentBucketNameStub; + let prepareDeploymentStub; + let saveCreateTemplateFileStub; + let generateArtifactDirectoryNameStub; + let compileFunctionsStub; + let mergeServiceResourcesStub; + let saveUpdateTemplateFileStub; + + beforeEach(() => { + cleanupServerlessDirStub = sinon.stub(googlePackage, 'cleanupServerlessDir') + .returns(BbPromise.resolve()); + validateStub = sinon.stub(googlePackage, 'validate') + .returns(BbPromise.resolve()); + setDefaultsStub = sinon.stub(googlePackage, 'setDefaults') + .returns(BbPromise.resolve()); + setDeploymentBucketNameStub = sinon.stub(googlePackage, 'setDeploymentBucketName') + .returns(BbPromise.resolve()); + prepareDeploymentStub = sinon.stub(googlePackage, 'prepareDeployment') + .returns(BbPromise.resolve()); + saveCreateTemplateFileStub = sinon.stub(googlePackage, 'saveCreateTemplateFile') + .returns(BbPromise.resolve()); + generateArtifactDirectoryNameStub = sinon.stub(googlePackage, 'generateArtifactDirectoryName') + .returns(BbPromise.resolve()); + compileFunctionsStub = sinon.stub(googlePackage, 'compileFunctions') + .returns(BbPromise.resolve()); + mergeServiceResourcesStub = sinon.stub(googlePackage, 'mergeServiceResources') + .returns(BbPromise.resolve()); + saveUpdateTemplateFileStub = sinon.stub(googlePackage, 'saveUpdateTemplateFile') + .returns(BbPromise.resolve()); + }); + + afterEach(() => { + googlePackage.cleanupServerlessDir.restore(); + googlePackage.validate.restore(); + googlePackage.setDefaults.restore(); + googlePackage.setDeploymentBucketName.restore(); + googlePackage.prepareDeployment.restore(); + googlePackage.saveCreateTemplateFile.restore(); + googlePackage.generateArtifactDirectoryName.restore(); + googlePackage.compileFunctions.restore(); + googlePackage.mergeServiceResources.restore(); + googlePackage.saveUpdateTemplateFile.restore(); + }); + + it('should run "package:cleanup" promise chain', () => googlePackage + .hooks['package:cleanup']().then(() => { + expect(cleanupServerlessDirStub.calledOnce).toEqual(true); + })); + + it('should run "before:package:initialize" promise chain', () => googlePackage + .hooks['before:package:initialize']().then(() => { + expect(validateStub.calledOnce).toEqual(true); + expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); + })); + + it('should run "package:initialize" promise chain', () => googlePackage + .hooks['package:initialize']().then(() => { + expect(setDeploymentBucketNameStub.calledOnce).toEqual(true); + expect(prepareDeploymentStub.calledAfter(setDeploymentBucketNameStub)).toEqual(true); + expect(saveCreateTemplateFileStub.calledAfter(prepareDeploymentStub)).toEqual(true); + })); + + it('should run "before:package:compileFunctions" promise chain', () => googlePackage + .hooks['before:package:compileFunctions']().then(() => { + expect(generateArtifactDirectoryNameStub.calledOnce).toEqual(true); + })); + + it('should run "package:compileFunctions" promise chain', () => googlePackage + .hooks['package:compileFunctions']().then(() => { + expect(compileFunctionsStub.calledOnce).toEqual(true); + })); + + it('should run "package:finalize" promise chain', () => googlePackage + .hooks['package:finalize']().then(() => { + expect(mergeServiceResourcesStub.calledOnce).toEqual(true); + expect(saveUpdateTemplateFileStub.calledAfter(mergeServiceResourcesStub)).toEqual(true); + })); + }); + }); +}); diff --git a/package/lib/cleanupServerlessDir.js b/package/lib/cleanupServerlessDir.js new file mode 100644 index 0000000..8ea6976 --- /dev/null +++ b/package/lib/cleanupServerlessDir.js @@ -0,0 +1,19 @@ +'use strict'; + +const BbPromise = require('bluebird'); +const path = require('path'); +const fse = require('fs-extra'); + +module.exports = { + cleanupServerlessDir() { + if (this.serverless.config.servicePath) { + const serverlessDirPath = path.join(this.serverless.config.servicePath, '.serverless'); + + if (fse.pathExistsSync(serverlessDirPath)) { + fse.removeSync(serverlessDirPath); + } + } + + return BbPromise.resolve(); + }, +}; diff --git a/package/lib/cleanupServerlessDir.test.js b/package/lib/cleanupServerlessDir.test.js new file mode 100644 index 0000000..e7bed6a --- /dev/null +++ b/package/lib/cleanupServerlessDir.test.js @@ -0,0 +1,77 @@ +'use strict'; + +const path = require('path'); + +const sinon = require('sinon'); +const fse = require('fs-extra'); + +const GoogleProvider = require('../../provider/googleProvider'); +const GooglePackage = require('../googlePackage'); +const Serverless = require('../../test/serverless'); + +describe('CleanupServerlessDir', () => { + let serverless; + let googlePackage; + let pathExistsSyncStub; + let removeSyncStub; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.config = { + servicePath: false, + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googlePackage = new GooglePackage(serverless, options); + pathExistsSyncStub = sinon.stub(fse, 'pathExistsSync'); + removeSyncStub = sinon.stub(fse, 'removeSync').returns(); + }); + + afterEach(() => { + fse.pathExistsSync.restore(); + fse.removeSync.restore(); + }); + + describe('#cleanupServerlessDir()', () => { + it('should resolve if no servicePath is given', () => { + googlePackage.serverless.config.servicePath = false; + + pathExistsSyncStub.returns(); + + return googlePackage.cleanupServerlessDir().then(() => { + expect(pathExistsSyncStub.calledOnce).toEqual(false); + expect(removeSyncStub.calledOnce).toEqual(false); + }); + }); + + it('should remove the .serverless directory if it exists', () => { + const serviceName = googlePackage.serverless.service.service; + googlePackage.serverless.config.servicePath = serviceName; + const serverlessDirPath = path.join(serviceName, '.serverless'); + + pathExistsSyncStub.returns(true); + + return googlePackage.cleanupServerlessDir().then(() => { + expect(pathExistsSyncStub.calledWithExactly(serverlessDirPath)).toEqual(true); + expect(removeSyncStub.calledWithExactly(serverlessDirPath)).toEqual(true); + }); + }); + + it('should not remove the .serverless directory if does not exist', () => { + const serviceName = googlePackage.serverless.service.service; + googlePackage.serverless.config.servicePath = serviceName; + const serverlessDirPath = path.join(serviceName, '.serverless'); + + pathExistsSyncStub.returns(false); + + return googlePackage.cleanupServerlessDir().then(() => { + expect(pathExistsSyncStub.calledWithExactly(serverlessDirPath)).toEqual(true); + expect(removeSyncStub.calledWithExactly(serverlessDirPath)).toEqual(false); + }); + }); + }); +}); diff --git a/package/lib/compileFunctions.js b/package/lib/compileFunctions.js new file mode 100644 index 0000000..54333ad --- /dev/null +++ b/package/lib/compileFunctions.js @@ -0,0 +1,120 @@ +'use strict'; + +/* eslint no-use-before-define: 0 */ + +const path = require('path'); + +const _ = require('lodash'); +const BbPromise = require('bluebird'); + +module.exports = { + compileFunctions() { + const artifactFilePath = this.serverless.service.package.artifact; + const fileName = artifactFilePath.split(path.sep).pop(); + + this.serverless.service.package + .artifactFilePath = `${this.serverless.service.package.artifactDirectoryName}/${fileName}`; + + this.serverless.service.getAllFunctions().forEach((functionName) => { + const funcObject = this.serverless.service.getFunction(functionName); + + this.serverless.cli + .log(`Compiling function "${functionName}"...`); + + validateHandlerProperty(funcObject, functionName); + validateEventsProperty(funcObject, functionName); + + const funcTemplate = getFunctionTemplate( + funcObject, + this.options.region, + `gs://${ + this.serverless.service.provider.deploymentBucketName + }/${this.serverless.service.package.artifactFilePath}`); + + funcTemplate.properties.availableMemoryMb = _.get(funcObject, 'memorySize') + || _.get(this, 'serverless.service.provider.memorySize') + || 256; + funcTemplate.properties.timeout = _.get(funcObject, 'timeout') + || _.get(this, 'serverless.service.provider.timeout') + || '60s'; + + const eventType = Object.keys(funcObject.events[0])[0]; + + if (eventType === 'http') { + const url = funcObject.events[0].http; + + funcTemplate.properties.httpsTrigger = {}; + funcTemplate.properties.httpsTrigger.url = url; + } + if (eventType === 'event') { + const type = funcObject.events[0].event.eventType; + const path = funcObject.events[0].event.path; //eslint-disable-line + const resource = funcObject.events[0].event.resource; + + funcTemplate.properties.eventTrigger = {}; + funcTemplate.properties.eventTrigger.eventType = type; + if (path) funcTemplate.properties.eventTrigger.path = path; + funcTemplate.properties.eventTrigger.resource = resource; + } + + this.serverless.service.provider.compiledConfigurationTemplate.resources.push(funcTemplate); + }); + + return BbPromise.resolve(); + }, +}; + +const validateHandlerProperty = (funcObject, functionName) => { + if (!funcObject.handler) { + const errorMessage = [ + `Missing "handler" property for function "${functionName}".`, + ' Your function needs a "handler".', + ' Please check the docs for more info.', + ].join(''); + throw new Error(errorMessage); + } +}; + +const validateEventsProperty = (funcObject, functionName) => { + if (!funcObject.events || funcObject.events.length === 0) { + const errorMessage = [ + `Missing "events" property for function "${functionName}".`, + ' Your function needs at least one "event".', + ' Please check the docs for more info.', + ].join(''); + throw new Error(errorMessage); + } + + if (funcObject.events.length > 1) { + const errorMessage = [ + `The function "${functionName}" has more than one event.`, + ' Only one event per function is supported.', + ' Please check the docs for more info.', + ].join(''); + throw new Error(errorMessage); + } + + const supportedEvents = ['http', 'event']; + const eventType = Object.keys(funcObject.events[0])[0]; + if (supportedEvents.indexOf(eventType) === -1) { + const errorMessage = [ + `Event type "${eventType}" of function "${functionName}" not supported.`, + ` supported event types are: ${supportedEvents.join(', ')}`, + ].join(''); + throw new Error(errorMessage); + } +}; + +const getFunctionTemplate = (funcObject, region, sourceArchiveUrl) => { //eslint-disable-line + return { + type: 'cloudfunctions.v1beta2.function', + name: funcObject.name, + properties: { + location: region, + availableMemoryMb: 256, + timeout: '60s', + function: funcObject.handler, + sourceArchiveUrl, + }, + }; +}; diff --git a/package/lib/compileFunctions.test.js b/package/lib/compileFunctions.test.js new file mode 100644 index 0000000..a000c92 --- /dev/null +++ b/package/lib/compileFunctions.test.js @@ -0,0 +1,332 @@ +'use strict'; + +const sinon = require('sinon'); + +const GoogleProvider = require('../../provider/googleProvider'); +const GooglePackage = require('../googlePackage'); +const Serverless = require('../../test/serverless'); + +describe('CompileFunctions', () => { + let serverless; + let googlePackage; + let consoleLogStub; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.service.package = { + artifact: 'artifact.zip', + artifactDirectoryName: 'some-path', + }; + serverless.service.provider = { + compiledConfigurationTemplate: { + resources: [], + }, + deploymentBucketName: 'sls-my-service-dev-12345678', + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googlePackage = new GooglePackage(serverless, options); + consoleLogStub = sinon.stub(googlePackage.serverless.cli, 'log').returns(); + }); + + afterEach(() => { + googlePackage.serverless.cli.log.restore(); + }); + + describe('#compileFunctions()', () => { + it('should throw an error if the function has no handler property', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: null, + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should throw an error if the function has no events property', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: null, + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should throw an error if the function has 0 events', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [], + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should throw an error if the function has more than 1 event', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { http: 'event1' }, + { http: 'event2' }, + ], + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should throw an error if the functions event is not supported', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { invalidEvent: 'event1' }, + ], + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should set the memory size based on the functions configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + memorySize: 1024, + events: [ + { http: 'foo' }, + ], + }, + }; + + const compiledResources = [{ + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func1', + properties: { + location: 'us-central1', + function: 'func1', + availableMemoryMb: 1024, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + }, + }]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect(googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources) + .toEqual(compiledResources); + }); + }); + + it('should set the memory size based on the provider configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { http: 'foo' }, + ], + }, + }; + googlePackage.serverless.service.provider.memorySize = 1024; + + const compiledResources = [{ + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func1', + properties: { + location: 'us-central1', + function: 'func1', + availableMemoryMb: 1024, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + }, + }]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect(googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources) + .toEqual(compiledResources); + }); + }); + + it('should set the timout based on the functions configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + timeout: '120s', + events: [ + { http: 'foo' }, + ], + }, + }; + + const compiledResources = [{ + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func1', + properties: { + location: 'us-central1', + function: 'func1', + availableMemoryMb: 256, + timeout: '120s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + }, + }]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect(googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources) + .toEqual(compiledResources); + }); + }); + + it('should set the timeout based on the provider configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { http: 'foo' }, + ], + }, + }; + googlePackage.serverless.service.provider.timeout = '120s'; + + const compiledResources = [{ + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func1', + properties: { + location: 'us-central1', + function: 'func1', + availableMemoryMb: 256, + timeout: '120s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + }, + }]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect(googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources) + .toEqual(compiledResources); + }); + }); + + it('should compile "http" events properly', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { http: 'foo' }, + ], + }, + }; + + const compiledResources = [{ + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func1', + properties: { + location: 'us-central1', + function: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + }, + }]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect(googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources) + .toEqual(compiledResources); + }); + }); + + it('should compile "event" events properly', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { + event: { + eventType: 'foo', + path: 'some-path', + resource: 'some-resource', + }, + }, + ], + }, + func2: { + handler: 'func2', + events: [ + { + event: { + eventType: 'foo', + resource: 'some-resource', + }, + }, + ], + }, + }; + + const compiledResources = [ + { + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func1', + properties: { + location: 'us-central1', + function: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + eventTrigger: { + eventType: 'foo', + path: 'some-path', + resource: 'some-resource', + }, + }, + }, + { + type: 'cloudfunctions.v1beta2.function', + name: 'my-service-dev-func2', + properties: { + location: 'us-central1', + function: 'func2', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + eventTrigger: { + eventType: 'foo', + resource: 'some-resource', + }, + }, + }, + ]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.called).toEqual(true); + expect(googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources) + .toEqual(compiledResources); + }); + }); + }); +}); diff --git a/package/lib/generateArtifactDirectoryName.js b/package/lib/generateArtifactDirectoryName.js new file mode 100644 index 0000000..f99075c --- /dev/null +++ b/package/lib/generateArtifactDirectoryName.js @@ -0,0 +1,16 @@ +'use strict'; + +const BbPromise = require('bluebird'); + +module.exports = { + generateArtifactDirectoryName() { + const date = new Date(); + const serviceWithStage = `${this.serverless.service.service}/${this.options.stage}`; + const dateString = `${date.getTime().toString()}-${date.toISOString()}`; + + this.serverless.service.package + .artifactDirectoryName = `serverless/${serviceWithStage}/${dateString}`; + + return BbPromise.resolve(); + }, +}; diff --git a/package/lib/generateArtifactDirectoryName.test.js b/package/lib/generateArtifactDirectoryName.test.js new file mode 100644 index 0000000..17c2217 --- /dev/null +++ b/package/lib/generateArtifactDirectoryName.test.js @@ -0,0 +1,33 @@ +'use strict'; + +const GoogleProvider = require('../../provider/googleProvider'); +const GooglePackage = require('../googlePackage'); +const Serverless = require('../../test/serverless'); + +describe('GenerateArtifactDirectoryName', () => { + let serverless; + let googlePackage; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.service.package = { + artifactDirectoryName: null, + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googlePackage = new GooglePackage(serverless, options); + }); + + it('should create a valid artifact directory name', () => { + const expectedRegex = new RegExp('serverless/my-service/dev/.*'); + + return googlePackage.generateArtifactDirectoryName().then(() => { + expect(serverless.service.package.artifactDirectoryName) + .toMatch(expectedRegex); + }); + }); +}); diff --git a/package/lib/mergeServiceResources.js b/package/lib/mergeServiceResources.js new file mode 100644 index 0000000..7b73940 --- /dev/null +++ b/package/lib/mergeServiceResources.js @@ -0,0 +1,26 @@ +'use strict'; + +/* eslint no-use-before-define: 0 */ + +const _ = require('lodash'); +const BbPromise = require('bluebird'); + +module.exports = { + mergeServiceResources() { + const resources = this.serverless.service.resources; + + if ((typeof resources === 'undefined') || _.isEmpty(resources)) return BbPromise.resolve(); + + _.mergeWith( + this.serverless.service.provider.compiledConfigurationTemplate, + resources, + mergeCustomizer); + + return BbPromise.resolve(); + }, +}; + +const mergeCustomizer = (objValue, srcValue) => { + if (_.isArray(objValue)) return objValue.concat(srcValue); + return objValue; +}; diff --git a/package/lib/mergeServiceResources.test.js b/package/lib/mergeServiceResources.test.js new file mode 100644 index 0000000..057ca92 --- /dev/null +++ b/package/lib/mergeServiceResources.test.js @@ -0,0 +1,101 @@ +'use stict'; + +const GoogleProvider = require('../../provider/googleProvider'); +const GooglePackage = require('../googlePackage'); +const Serverless = require('../../test/serverless'); + +describe('MergeServiceResources', () => { + let serverless; + let googlePackage; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.service.provider = { + compiledConfigurationTemplate: {}, + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googlePackage = new GooglePackage(serverless, options); + }); + + it('should resolve if service resources are not defined', () => googlePackage + .mergeServiceResources().then(() => { + expect(serverless.service.provider + .compiledConfigurationTemplate).toEqual({}); + })); + + it('should resolve if service resources is empty', () => { + serverless.service.resources = {}; + + return googlePackage.mergeServiceResources().then(() => { + expect(serverless.service.provider + .compiledConfigurationTemplate).toEqual({}); + }); + }); + + it('should merge all the resources if provided', () => { + serverless.service.provider.compiledConfigurationTemplate = { + resources: [ + { + name: 'resource1', + type: 'type1', + properties: { + property1: 'value1', + }, + }, + ], + }; + + serverless.service.resources = { + resources: [ + { + name: 'resource2', + type: 'type2', + properties: { + property1: 'value1', + }, + }, + ], + imports: [ + { + path: 'path/to/template.jinja', + name: 'my-template', + }, + ], + }; + + const expectedResult = { + resources: [ + { + name: 'resource1', + type: 'type1', + properties: { + property1: 'value1', + }, + }, + { + name: 'resource2', + type: 'type2', + properties: { + property1: 'value1', + }, + }, + ], + imports: [ + { + path: 'path/to/template.jinja', + name: 'my-template', + }, + ], + }; + + return googlePackage.mergeServiceResources().then(() => { + expect(serverless.service.provider.compiledConfigurationTemplate) + .toEqual(expectedResult); + }); + }); +}); diff --git a/package/lib/prepareDeployment.js b/package/lib/prepareDeployment.js new file mode 100644 index 0000000..6ab1259 --- /dev/null +++ b/package/lib/prepareDeployment.js @@ -0,0 +1,47 @@ +'use strict'; + +/* eslint no-use-before-define: 0 */ + +const path = require('path'); + +const _ = require('lodash'); +const BbPromise = require('bluebird'); + +module.exports = { + prepareDeployment() { + let deploymentTemplate = this.serverless.service.provider.compiledConfigurationTemplate; + + deploymentTemplate = this.serverless.utils.readFileSync( + path.join( + __dirname, + '..', + 'templates', + 'core-configuration-template.yml')); + + const bucket = deploymentTemplate.resources.find(findDeploymentBucket); + + const name = this.serverless.service.provider.deploymentBucketName; + const updatedBucket = updateBucketName(bucket, name); + + const bucketIndex = deploymentTemplate.resources.findIndex(findDeploymentBucket); + + deploymentTemplate.resources[bucketIndex] = updatedBucket; + + this.serverless.service.provider.compiledConfigurationTemplate = deploymentTemplate; + + return BbPromise.resolve(); + }, +}; + +const updateBucketName = (bucket, name) => { + const newBucket = _.cloneDeep(bucket); + newBucket.name = name; + return newBucket; +}; + +const findDeploymentBucket = (resource) => { + const type = 'storage.v1.bucket'; + const name = 'will-be-replaced-by-serverless'; + + return resource.type === type && resource.name === name; +}; diff --git a/package/lib/prepareDeployment.test.js b/package/lib/prepareDeployment.test.js new file mode 100644 index 0000000..b42fb07 --- /dev/null +++ b/package/lib/prepareDeployment.test.js @@ -0,0 +1,65 @@ +'use strict'; + +const sinon = require('sinon'); + +const GoogleProvider = require('../../provider/googleProvider'); +const GooglePackage = require('../googlePackage'); +const Serverless = require('../../test/serverless'); + +describe('PrepareDeployment', () => { + let coreResources; + let serverless; + let googlePackage; + + beforeEach(() => { + coreResources = { + resources: [ + { + type: 'storage.v1.bucket', + name: 'will-be-replaced-by-serverless', + }, + ], + }; + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.service.provider = { + compiledConfigurationTemplate: coreResources, + deploymentBucketName: 'sls-my-service-dev-12345678', + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googlePackage = new GooglePackage(serverless, options); + }); + + describe('#prepareDeployment()', () => { + let readFileSyncStub; + + beforeEach(() => { + readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns(coreResources); + }); + + afterEach(() => { + serverless.utils.readFileSync.restore(); + }); + + it('should load the core configuration template into the serverless instance', () => { + const expectedCompiledConfiguration = { + resources: [ + { + type: 'storage.v1.bucket', + name: 'sls-my-service-dev-12345678', + }, + ], + }; + + return googlePackage.prepareDeployment().then(() => { + expect(readFileSyncStub.calledOnce).toEqual(true); + expect(serverless.service.provider + .compiledConfigurationTemplate).toEqual(expectedCompiledConfiguration); + }); + }); + }); +}); diff --git a/package/lib/writeFilesToDisk.js b/package/lib/writeFilesToDisk.js new file mode 100644 index 0000000..fc4da9f --- /dev/null +++ b/package/lib/writeFilesToDisk.js @@ -0,0 +1,29 @@ +'use strict'; + +/* eslint no-use-before-define: 0 */ + +const path = require('path'); + +const BbPromise = require('bluebird'); + +module.exports = { + saveCreateTemplateFile() { + const filePath = path.join(this.serverless.config.servicePath, + '.serverless', 'configuration-template-create.yml'); + + this.serverless.utils.writeFileSync(filePath, + this.serverless.service.provider.compiledConfigurationTemplate); + + return BbPromise.resolve(); + }, + + saveUpdateTemplateFile() { + const filePath = path.join(this.serverless.config.servicePath, + '.serverless', 'configuration-template-update.yml'); + + this.serverless.utils.writeFileSync(filePath, + this.serverless.service.provider.compiledConfigurationTemplate); + + return BbPromise.resolve(); + }, +}; diff --git a/package/lib/writeFilesToDisk.test.js b/package/lib/writeFilesToDisk.test.js new file mode 100644 index 0000000..6c54c87 --- /dev/null +++ b/package/lib/writeFilesToDisk.test.js @@ -0,0 +1,73 @@ +'use strict'; + +const path = require('path'); + +const sinon = require('sinon'); + +const GoogleProvider = require('../../provider/googleProvider'); +const GooglePackage = require('../googlePackage'); +const Serverless = require('../../test/serverless'); + +describe('WriteFilesToDisk', () => { + let serverless; + let googlePackage; + let writeFileSyncStub; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.service.provider = { + compiledConfigurationTemplate: { + foo: 'bar', + }, + }; + serverless.config = { + servicePath: 'foo/my-service', + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googlePackage = new GooglePackage(serverless, options); + writeFileSyncStub = sinon.stub(googlePackage.serverless.utils, 'writeFileSync'); + }); + + afterEach(() => { + googlePackage.serverless.utils.writeFileSync.restore(); + }); + + describe('#saveCreateTemplateFile()', () => { + it('should write the template file into the services .serverless directory', () => { + const createFilePath = path.join( + googlePackage.serverless.config.servicePath, + '.serverless', + 'configuration-template-create.yml', + ); + + return googlePackage.saveCreateTemplateFile().then(() => { + expect(writeFileSyncStub.calledWithExactly( + createFilePath, + googlePackage.serverless.service.provider.compiledConfigurationTemplate, + )).toEqual(true); + }); + }); + }); + + describe('#saveUpdateTemplateFile()', () => { + it('should write the template file into the services .serverless directory', () => { + const updateFilePath = path.join( + googlePackage.serverless.config.servicePath, + '.serverless', + 'configuration-template-update.yml', + ); + + return googlePackage.saveUpdateTemplateFile().then(() => { + expect(writeFileSyncStub.calledWithExactly( + updateFilePath, + googlePackage.serverless.service.provider.compiledConfigurationTemplate, + )).toEqual(true); + }); + }); + }); +}); diff --git a/package/templates/core-configuration-template.yml b/package/templates/core-configuration-template.yml new file mode 100644 index 0000000..12f779b --- /dev/null +++ b/package/templates/core-configuration-template.yml @@ -0,0 +1,3 @@ +resources: +- type: storage.v1.bucket + name: will-be-replaced-by-serverless