├── prettier.config.js ├── .gitignore ├── package ├── templates │ └── core-configuration-template.yml ├── lib │ ├── generateArtifactDirectoryName.js │ ├── cleanupServerlessDir.js │ ├── mergeServiceResources.js │ ├── writeFilesToDisk.js │ ├── generateArtifactDirectoryName.test.js │ ├── prepareDeployment.js │ ├── writeFilesToDisk.test.js │ ├── prepareDeployment.test.js │ ├── mergeServiceResources.test.js │ ├── cleanupServerlessDir.test.js │ └── compileFunctions.js ├── googlePackage.js └── googlePackage.test.js ├── test ├── googleCommand.js └── serverless.js ├── remove ├── lib │ ├── removeDeployment.js │ ├── emptyDeploymentBucket.js │ ├── removeDeployment.test.js │ └── emptyDeploymentBucket.test.js ├── googleRemove.js └── googleRemove.test.js ├── invoke ├── googleInvoke.js ├── lib │ ├── invokeFunction.js │ └── invokeFunction.test.js └── googleInvoke.test.js ├── commitlint.config.js ├── deploy ├── lib │ ├── uploadArtifacts.js │ ├── updateDeployment.js │ ├── cleanupDeploymentBucket.js │ ├── createDeployment.js │ ├── uploadArtifacts.test.js │ ├── updateDeployment.test.js │ ├── createDeployment.test.js │ └── cleanupDeploymentBucket.test.js ├── googleDeploy.js └── googleDeploy.test.js ├── shared ├── utils.js ├── setDeploymentBucketName.js ├── utils.test.js ├── monitorDeployment.js ├── validate.js ├── setDeploymentBucketName.test.js ├── monitorDeployment.test.js └── validate.test.js ├── info ├── googleInfo.js ├── googleInfo.test.js └── lib │ └── displayServiceInfo.js ├── invokeLocal ├── lib │ ├── httpReqRes.js │ ├── getDataAndContext.js │ ├── testMocks │ │ └── index.js │ ├── getDataAndContext.test.js │ ├── nodeJs.js │ └── nodeJs.test.js ├── googleInvokeLocal.js └── googleInvokeLocal.test.js ├── logs ├── googleLogs.js ├── lib │ ├── retrieveLogs.js │ └── retrieveLogs.test.js └── googleLogs.test.js ├── LICENSE ├── index.js ├── README.md ├── index.test.js ├── .github └── workflows │ ├── publish.yml │ ├── validate.yml │ └── integrate.yml ├── MIGRATION_GUIDE.md ├── package.json ├── RELEASE_PROCESS.md ├── provider └── googleProvider.test.js └── CHANGELOG.md /prettier.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('@serverless/eslint-config/prettier.config'); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.eslintcache 2 | /.nyc_output 3 | /coverage 4 | /node_modules 5 | npm-debug.log 6 | /package-lock.json 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /package/templates/core-configuration-template.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | - type: storage.v1.bucket 3 | name: will-be-replaced-by-serverless 4 | -------------------------------------------------------------------------------- /test/googleCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // mock to test functionality in a command unrelated matter 4 | // this mean that not e.g. googleDeploy but the more abstract googleCommand can be used 5 | class GoogleCommand { 6 | constructor(serverless, options, testSubject) { 7 | this.options = options; 8 | this.serverless = serverless; 9 | this.provider = this.serverless.getProvider('google'); 10 | 11 | Object.assign(this, testSubject); 12 | } 13 | } 14 | 15 | module.exports = GoogleCommand; 16 | -------------------------------------------------------------------------------- /package/lib/generateArtifactDirectoryName.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | module.exports = { 6 | generateArtifactDirectoryName() { 7 | const date = new Date(); 8 | const serviceWithStage = `${this.serverless.service.service}/${this.options.stage}`; 9 | const dateString = `${date.getTime().toString()}-${date.toISOString()}`; 10 | 11 | this.serverless.service.package.artifactDirectoryName = `serverless/${serviceWithStage}/${dateString}`; 12 | 13 | return BbPromise.resolve(); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /package/lib/cleanupServerlessDir.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const path = require('path'); 5 | const fse = require('fs-extra'); 6 | 7 | module.exports = { 8 | cleanupServerlessDir() { 9 | if (this.serverless.config.servicePath) { 10 | const serverlessDirPath = path.join(this.serverless.config.servicePath, '.serverless'); 11 | 12 | if (fse.pathExistsSync(serverlessDirPath)) { 13 | fse.removeSync(serverlessDirPath); 14 | } 15 | } 16 | 17 | return BbPromise.resolve(); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /remove/lib/removeDeployment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | removeDeployment() { 5 | this.serverless.cli.log('Removing deployment...'); 6 | 7 | const deploymentName = `sls-${this.serverless.service.service}-${this.options.stage}`; 8 | 9 | const params = { 10 | project: this.serverless.service.provider.project, 11 | deployment: deploymentName, 12 | }; 13 | 14 | return this.provider 15 | .request('deploymentmanager', 'deployments', 'delete', params) 16 | .then(() => this.monitorDeployment(deploymentName, 'remove', 5000)); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package/lib/mergeServiceResources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const _ = require('lodash'); 6 | const BbPromise = require('bluebird'); 7 | 8 | module.exports = { 9 | mergeServiceResources() { 10 | const resources = this.serverless.service.resources; 11 | 12 | if (typeof resources === 'undefined' || _.isEmpty(resources)) return BbPromise.resolve(); 13 | 14 | _.mergeWith( 15 | this.serverless.service.provider.compiledConfigurationTemplate, 16 | resources, 17 | mergeCustomizer 18 | ); 19 | 20 | return BbPromise.resolve(); 21 | }, 22 | }; 23 | 24 | const mergeCustomizer = (objValue, srcValue) => { 25 | if (_.isArray(objValue)) return objValue.concat(srcValue); 26 | return objValue; 27 | }; 28 | -------------------------------------------------------------------------------- /invoke/googleInvoke.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | const validate = require('../shared/validate'); 6 | const setDefaults = require('../shared/utils'); 7 | const invokeFunction = require('./lib/invokeFunction'); 8 | 9 | class GoogleInvoke { 10 | constructor(serverless, options) { 11 | this.serverless = serverless; 12 | this.options = options; 13 | this.provider = this.serverless.getProvider('google'); 14 | 15 | Object.assign(this, validate, setDefaults, invokeFunction); 16 | 17 | this.hooks = { 18 | 'before:invoke:invoke': () => BbPromise.bind(this).then(this.validate).then(this.setDefaults), 19 | 20 | 'invoke:invoke': () => BbPromise.bind(this).then(this.invokeFunction), 21 | }; 22 | } 23 | } 24 | 25 | module.exports = GoogleInvoke; 26 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | rules: { 5 | 'body-leading-blank': [2, 'always'], 6 | 'body-max-line-length': [2, 'always', 72], 7 | 'footer-leading-blank': [2, 'always'], 8 | 'footer-max-line-length': [2, 'always', 72], 9 | 'header-max-length': [2, 'always', 72], 10 | 'scope-case': [2, 'always', 'start-case'], 11 | 'scope-enum': [2, 'always', ['']], 12 | 'subject-case': [2, 'always', 'sentence-case'], 13 | 'subject-empty': [2, 'never'], 14 | 'subject-full-stop': [2, 'never', '.'], 15 | 'type-case': [2, 'always', 'lower-case'], 16 | 'type-empty': [2, 'never'], 17 | 'type-enum': [ 18 | 2, 19 | 'always', 20 | ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'style', 'test'], 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /deploy/lib/uploadArtifacts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | module.exports = { 6 | uploadArtifacts() { 7 | this.serverless.cli.log('Uploading artifacts...'); 8 | 9 | const params = { 10 | bucket: this.serverless.service.provider.deploymentBucketName, 11 | resource: { 12 | name: this.serverless.service.package.artifactFilePath, 13 | contentType: 'application/octet-stream', 14 | }, 15 | media: { 16 | mimeType: 'application/octet-stream', 17 | body: fs.createReadStream(this.serverless.service.package.artifact), 18 | }, 19 | }; 20 | 21 | return this.provider.request('storage', 'objects', 'insert', params).then(() => { 22 | this.serverless.cli.log('Artifacts successfully uploaded...'); 23 | }); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /shared/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const BbPromise = require('bluebird'); 5 | 6 | module.exports = { 7 | setDefaults() { 8 | this.options.stage = 9 | _.get(this, 'options.stage') || _.get(this, 'serverless.service.provider.stage') || 'dev'; 10 | this.options.runtime = _.get(this, 'options.runtime') || 'nodejs10'; 11 | 12 | // serverless framework is hard-coding us-east-1 region from aws 13 | // this is temporary fix for multiple regions 14 | let region = _.get(this, 'options.region') || _.get(this, 'serverless.service.provider.region'); 15 | 16 | if (region === 'us-east-1') { 17 | region = 'us-central1'; 18 | } 19 | 20 | this.options.region = region; 21 | this.serverless.service.provider.region = region; 22 | 23 | return BbPromise.resolve(); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /info/googleInfo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | const validate = require('../shared/validate'); 6 | const setDefaults = require('../shared/utils'); 7 | const displayServiceInfo = require('./lib/displayServiceInfo'); 8 | 9 | class GoogleInfo { 10 | constructor(serverless, options) { 11 | this.serverless = serverless; 12 | this.options = options; 13 | this.provider = this.serverless.getProvider('google'); 14 | 15 | Object.assign(this, validate, setDefaults, displayServiceInfo); 16 | 17 | this.hooks = { 18 | 'before:info:info': () => BbPromise.bind(this).then(this.validate).then(this.setDefaults), 19 | 20 | 'deploy:deploy': () => BbPromise.bind(this).then(this.displayServiceInfo), 21 | 22 | 'info:info': () => BbPromise.bind(this).then(this.displayServiceInfo), 23 | }; 24 | } 25 | } 26 | 27 | module.exports = GoogleInfo; 28 | -------------------------------------------------------------------------------- /package/lib/writeFilesToDisk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const path = require('path'); 6 | 7 | const BbPromise = require('bluebird'); 8 | 9 | module.exports = { 10 | saveCreateTemplateFile() { 11 | const filePath = path.join( 12 | this.serverless.config.servicePath, 13 | '.serverless', 14 | 'configuration-template-create.yml' 15 | ); 16 | 17 | this.serverless.utils.writeFileSync( 18 | filePath, 19 | this.serverless.service.provider.compiledConfigurationTemplate 20 | ); 21 | 22 | return BbPromise.resolve(); 23 | }, 24 | 25 | saveUpdateTemplateFile() { 26 | const filePath = path.join( 27 | this.serverless.config.servicePath, 28 | '.serverless', 29 | 'configuration-template-update.yml' 30 | ); 31 | 32 | this.serverless.utils.writeFileSync( 33 | filePath, 34 | this.serverless.service.provider.compiledConfigurationTemplate 35 | ); 36 | 37 | return BbPromise.resolve(); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /invokeLocal/lib/httpReqRes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const http = require('http'); 5 | const net = require('net'); 6 | 7 | // The getReqRes method create an express request and an express response 8 | // as they are created in an express server before being passed to the middlewares 9 | // Google use express 4.17.1 to run http cloud function 10 | // https://cloud.google.com/functions/docs/writing/http#http_frameworks 11 | const app = express(); 12 | 13 | module.exports = { 14 | getReqRes() { 15 | const req = new http.IncomingMessage(new net.Socket()); 16 | const expressRequest = Object.assign(req, { app }); 17 | Object.setPrototypeOf(expressRequest, express.request); 18 | 19 | const res = new http.ServerResponse(req); 20 | const expressResponse = Object.assign(res, { app, req: expressRequest }); 21 | Object.setPrototypeOf(expressResponse, express.response); 22 | 23 | expressRequest.res = expressResponse; 24 | 25 | return { 26 | expressRequest, 27 | expressResponse, 28 | }; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /logs/googleLogs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | const validate = require('../shared/validate'); 6 | const setDefaults = require('../shared/utils'); 7 | const retrieveLogs = require('./lib/retrieveLogs'); 8 | 9 | class GoogleLogs { 10 | constructor(serverless, options) { 11 | this.serverless = serverless; 12 | this.options = options; 13 | this.provider = this.serverless.getProvider('google'); 14 | 15 | this.commands = { 16 | logs: { 17 | lifecycleEvents: ['logs'], 18 | options: { 19 | count: { 20 | usage: 'Amount of requested logs', 21 | shortcut: 'c', 22 | type: 'string', 23 | }, 24 | }, 25 | }, 26 | }; 27 | 28 | Object.assign(this, validate, setDefaults, retrieveLogs); 29 | 30 | this.hooks = { 31 | 'before:logs:logs': () => BbPromise.bind(this).then(this.validate).then(this.setDefaults), 32 | 33 | 'logs:logs': () => BbPromise.bind(this).then(this.retrieveLogs), 34 | }; 35 | } 36 | } 37 | 38 | module.exports = GoogleLogs; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Serverless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package/lib/generateArtifactDirectoryName.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GoogleProvider = require('../../provider/googleProvider'); 4 | const GooglePackage = require('../googlePackage'); 5 | const Serverless = require('../../test/serverless'); 6 | 7 | describe('GenerateArtifactDirectoryName', () => { 8 | let serverless; 9 | let googlePackage; 10 | 11 | beforeEach(() => { 12 | serverless = new Serverless(); 13 | serverless.service.service = 'my-service'; 14 | serverless.service.package = { 15 | artifactDirectoryName: null, 16 | }; 17 | serverless.setProvider('google', new GoogleProvider(serverless)); 18 | const options = { 19 | stage: 'dev', 20 | region: 'us-central1', 21 | }; 22 | googlePackage = new GooglePackage(serverless, options); 23 | }); 24 | 25 | it('should create a valid artifact directory name', () => { 26 | const expectedRegex = new RegExp('serverless/my-service/dev/.*'); 27 | 28 | return googlePackage.generateArtifactDirectoryName().then(() => { 29 | expect(serverless.service.package.artifactDirectoryName).toMatch(expectedRegex); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /remove/lib/emptyDeploymentBucket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | module.exports = { 6 | emptyDeploymentBucket() { 7 | return BbPromise.bind(this).then(this.getObjectsToRemove).then(this.removeObjects); 8 | }, 9 | 10 | getObjectsToRemove() { 11 | const params = { 12 | bucket: this.serverless.service.provider.deploymentBucketName, 13 | }; 14 | 15 | return this.provider.request('storage', 'objects', 'list', params).then((response) => { 16 | if (!response.items || !response.items.length) return BbPromise.resolve([]); 17 | 18 | return BbPromise.resolve(response.items); 19 | }); 20 | }, 21 | 22 | removeObjects(objectsToRemove) { 23 | if (!objectsToRemove.length) return BbPromise.resolve(); 24 | 25 | this.serverless.cli.log('Removing artifacts in deployment bucket...'); 26 | 27 | const removePromises = objectsToRemove.map((object) => { 28 | const params = { 29 | bucket: object.bucket, 30 | object: object.name, 31 | }; 32 | return this.provider.request('storage', 'objects', 'delete', params); 33 | }); 34 | 35 | return BbPromise.all(removePromises); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /remove/googleRemove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | const validate = require('../shared/validate'); 6 | const setDefaults = require('../shared/utils'); 7 | const setDeploymentBucketName = require('../shared/setDeploymentBucketName'); 8 | const emptyDeploymentBucket = require('./lib/emptyDeploymentBucket'); 9 | const removeDeployment = require('./lib/removeDeployment'); 10 | const monitorDeployment = require('../shared/monitorDeployment'); 11 | 12 | class GoogleRemove { 13 | constructor(serverless, options) { 14 | this.serverless = serverless; 15 | this.options = options; 16 | this.provider = this.serverless.getProvider('google'); 17 | 18 | Object.assign( 19 | this, 20 | validate, 21 | setDefaults, 22 | setDeploymentBucketName, 23 | emptyDeploymentBucket, 24 | removeDeployment, 25 | monitorDeployment 26 | ); 27 | 28 | this.hooks = { 29 | 'before:remove:remove': () => 30 | BbPromise.bind(this) 31 | .then(this.validate) 32 | .then(this.setDefaults) 33 | .then(this.setDeploymentBucketName), 34 | 35 | 'remove:remove': () => 36 | BbPromise.bind(this).then(this.emptyDeploymentBucket).then(this.removeDeployment), 37 | }; 38 | } 39 | } 40 | 41 | module.exports = GoogleRemove; 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | NOTE: this plugin is used to add all the differnet provider related plugins at once. 5 | This way only one plugin needs to be added to the service in order to get access to the 6 | whole provider implementation. 7 | */ 8 | 9 | const GoogleProvider = require('./provider/googleProvider'); 10 | const GooglePackage = require('./package/googlePackage'); 11 | const GoogleDeploy = require('./deploy/googleDeploy'); 12 | const GoogleRemove = require('./remove/googleRemove'); 13 | const GoogleInvoke = require('./invoke/googleInvoke'); 14 | const GoogleInvokeLocal = require('./invokeLocal/googleInvokeLocal'); 15 | const GoogleLogs = require('./logs/googleLogs'); 16 | const GoogleInfo = require('./info/googleInfo'); 17 | 18 | class GoogleIndex { 19 | constructor(serverless, options) { 20 | this.serverless = serverless; 21 | this.options = options; 22 | 23 | this.serverless.pluginManager.addPlugin(GoogleProvider); 24 | this.serverless.pluginManager.addPlugin(GooglePackage); 25 | this.serverless.pluginManager.addPlugin(GoogleDeploy); 26 | this.serverless.pluginManager.addPlugin(GoogleRemove); 27 | this.serverless.pluginManager.addPlugin(GoogleInvoke); 28 | this.serverless.pluginManager.addPlugin(GoogleInvokeLocal); 29 | this.serverless.pluginManager.addPlugin(GoogleLogs); 30 | this.serverless.pluginManager.addPlugin(GoogleInfo); 31 | } 32 | } 33 | 34 | module.exports = GoogleIndex; 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Google Cloud Functions Plugin 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/serverless/serverless-google-cloudfunctions/badge.svg?branch=master)](https://coveralls.io/github/serverless/serverless-google-cloudfunctions?branch=master) 4 | 5 | This plugin enables support for [Google Cloud Functions](https://cloud.google.com/functions/) within the [Serverless Framework](https://github.com/serverless/serverless). 6 | 7 | ## This project is looking for maintainers! 8 | 9 | If you would like to be a maintainer of this project, please reach out to one of the active [Serverless organization](https://github.com/serverless) members to express your interest. 10 | 11 | We'd love to collaborate closely with amazing developers as we drive the development of this open technology into the future. 12 | 13 | Welcome, and thanks in advance for your help! 14 | 15 | ## Documentation 16 | 17 | The documentation can be found [here](https://serverless.com/framework/docs/providers/google). 18 | 19 | --- 20 | 21 | ## Easier development with Docker 22 | 23 | You can spin up a Docker container which mounts this code with the following command: 24 | 25 | ```bash 26 | docker-compose run node bash 27 | ``` 28 | 29 | ## Migration 30 | 31 | > Google Cloud Functions v1beta2 API version will be shut down on April 15, 2020 32 | 33 | You can follow this [document](./MIGRATION_GUIDE.md) to upgrade your serverless-google-cloudfunctions v2 to v3 34 | -------------------------------------------------------------------------------- /shared/setDeploymentBucketName.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = { 7 | setDeploymentBucketName() { 8 | // set a default name for the deployment bucket 9 | const service = this.serverless.service.service; 10 | const stage = this.options.stage; 11 | const timestamp = +new Date(); 12 | const name = `sls-${service}-${stage}-${timestamp}`; 13 | 14 | this.serverless.service.provider.deploymentBucketName = name; 15 | 16 | // check if there's already a deployment and update if available 17 | const params = { 18 | project: this.serverless.service.provider.project, 19 | deployment: `sls-${this.serverless.service.service}-${this.options.stage}`, 20 | }; 21 | 22 | return this.provider 23 | .request('deploymentmanager', 'resources', 'list', params) 24 | .then((response) => { 25 | if (!_.isEmpty(response) && response.resources) { 26 | const regex = new RegExp(`sls-${service}-${stage}-.+`); 27 | 28 | const deploymentBucket = response.resources.find( 29 | (resource) => resource.type === 'storage.v1.bucket' && resource.name.match(regex) 30 | ); 31 | 32 | this.serverless.service.provider.deploymentBucketName = deploymentBucket.name; 33 | } 34 | }) 35 | .catch(() => BbPromise.resolve()); // if it cannot be found (e.g. on initial deployment) 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /test/serverless.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // mock of the serverless instance 4 | class Serverless { 5 | constructor() { 6 | this.providers = {}; 7 | 8 | this.service = {}; 9 | this.service.getAllFunctions = function () { 10 | //eslint-disable-line 11 | return Object.keys(this.functions); 12 | }; 13 | this.service.getFunction = function (functionName) { 14 | //eslint-disable-line 15 | // NOTE assign the function name only when it is not specified 16 | if (!this.functions[functionName].name) { 17 | // NOTE the stage is always 'dev'! 18 | this.functions[functionName].name = `${this.service}-dev-${functionName}`; 19 | } 20 | return this.functions[functionName]; 21 | }; 22 | this.service.provider = {}; 23 | this.utils = { 24 | writeFileSync() {}, 25 | readFileSync() {}, 26 | }; 27 | 28 | this.cli = { 29 | log() {}, 30 | consoleLog() {}, 31 | printDot() {}, 32 | }; 33 | 34 | this.plugins = []; 35 | this.pluginManager = { 36 | addPlugin: (plugin) => this.plugins.push(plugin), 37 | }; 38 | 39 | this.configSchemaHandler = { 40 | defineProvider: jest.fn(), 41 | defineFunctionEvent: jest.fn(), 42 | }; 43 | 44 | this.processedInput = {}; 45 | } 46 | 47 | setProvider(name, provider) { 48 | this.providers[name] = provider; 49 | } 50 | 51 | getProvider(name) { 52 | return this.providers[name]; 53 | } 54 | } 55 | 56 | module.exports = Serverless; 57 | -------------------------------------------------------------------------------- /invokeLocal/googleInvokeLocal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const validate = require('../shared/validate'); 4 | const setDefaults = require('../shared/utils'); 5 | const getDataAndContext = require('./lib/getDataAndContext'); 6 | const nodeJs = require('./lib/nodeJs'); 7 | 8 | class GoogleInvokeLocal { 9 | constructor(serverless, options) { 10 | this.serverless = serverless; 11 | this.options = options; 12 | 13 | this.provider = this.serverless.getProvider('google'); 14 | 15 | Object.assign(this, validate, setDefaults, getDataAndContext, nodeJs); 16 | 17 | this.hooks = { 18 | 'initialize': () => { 19 | this.options = this.serverless.processedInput.options; 20 | }, 21 | 'before:invoke:local:invoke': async () => { 22 | await this.validate(); 23 | await this.setDefaults(); 24 | await this.getDataAndContext(); 25 | }, 26 | 'invoke:local:invoke': async () => this.invokeLocal(), 27 | }; 28 | } 29 | 30 | async invokeLocal() { 31 | const functionObj = this.serverless.service.getFunction(this.options.function); 32 | this.validateEventsProperty(functionObj, this.options.function); 33 | 34 | const runtime = this.provider.getRuntime(functionObj); 35 | if (!runtime.startsWith('nodejs')) { 36 | throw new Error(`Local invocation with runtime ${runtime} is not supported`); 37 | } 38 | return this.invokeLocalNodeJs(functionObj, this.options.data, this.options.context); 39 | } 40 | } 41 | 42 | module.exports = GoogleInvokeLocal; 43 | -------------------------------------------------------------------------------- /deploy/googleDeploy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | const validate = require('../shared/validate'); 6 | const utils = require('../shared/utils'); 7 | const createDeployment = require('./lib/createDeployment'); 8 | const setDeploymentBucketName = require('../shared/setDeploymentBucketName'); 9 | const monitorDeployment = require('../shared/monitorDeployment'); 10 | const uploadArtifacts = require('./lib/uploadArtifacts'); 11 | const updateDeployment = require('./lib/updateDeployment'); 12 | const cleanupDeploymentBucket = require('./lib/cleanupDeploymentBucket'); 13 | 14 | class GoogleDeploy { 15 | constructor(serverless, options) { 16 | this.serverless = serverless; 17 | this.options = options; 18 | this.provider = this.serverless.getProvider('google'); 19 | 20 | Object.assign( 21 | this, 22 | validate, 23 | utils, 24 | createDeployment, 25 | setDeploymentBucketName, 26 | monitorDeployment, 27 | uploadArtifacts, 28 | updateDeployment, 29 | cleanupDeploymentBucket 30 | ); 31 | 32 | this.hooks = { 33 | 'before:deploy:deploy': () => BbPromise.bind(this).then(this.validate).then(this.setDefaults), 34 | 35 | 'deploy:deploy': () => 36 | BbPromise.bind(this) 37 | .then(this.createDeployment) 38 | .then(this.setDeploymentBucketName) 39 | .then(this.uploadArtifacts) 40 | .then(this.updateDeployment), 41 | 42 | 'after:deploy:deploy': () => BbPromise.bind(this).then(this.cleanupDeploymentBucket), 43 | }; 44 | } 45 | } 46 | 47 | module.exports = GoogleDeploy; 48 | -------------------------------------------------------------------------------- /package/lib/prepareDeployment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const path = require('path'); 6 | 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | 10 | module.exports = { 11 | prepareDeployment() { 12 | let deploymentTemplate = this.serverless.service.provider.compiledConfigurationTemplate; 13 | 14 | deploymentTemplate = this.serverless.utils.readFileSync( 15 | path.join(__dirname, '..', 'templates', 'core-configuration-template.yml') 16 | ); 17 | 18 | const bucket = deploymentTemplate.resources.find(findDeploymentBucket); 19 | 20 | const name = this.serverless.service.provider.deploymentBucketName; 21 | const location = this.serverless.service.provider.region; 22 | const updatedBucket = updateBucket(bucket, name, location); 23 | 24 | const bucketIndex = deploymentTemplate.resources.findIndex(findDeploymentBucket); 25 | 26 | deploymentTemplate.resources[bucketIndex] = updatedBucket; 27 | 28 | this.serverless.service.provider.compiledConfigurationTemplate = deploymentTemplate; 29 | 30 | return BbPromise.resolve(); 31 | }, 32 | }; 33 | 34 | const updateBucket = (bucket, name, location) => { 35 | const newBucket = _.cloneDeep(bucket); 36 | newBucket.name = name; 37 | if (location) { 38 | if (!newBucket.properties) newBucket.properties = {}; 39 | newBucket.properties.location = location; 40 | } 41 | return newBucket; 42 | }; 43 | 44 | const findDeploymentBucket = (resource) => { 45 | const type = 'storage.v1.bucket'; 46 | const name = 'will-be-replaced-by-serverless'; 47 | 48 | return resource.type === type && resource.name === name; 49 | }; 50 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GoogleIndex = require('./index'); 4 | const GoogleProvider = require('./provider/googleProvider'); 5 | const GooglePackage = require('./package/googlePackage'); 6 | const GoogleDeploy = require('./deploy/googleDeploy'); 7 | const GoogleRemove = require('./remove/googleRemove'); 8 | const GoogleInvoke = require('./invoke/googleInvoke'); 9 | const GoogleLogs = require('./logs/googleLogs'); 10 | const GoogleInfo = require('./info/googleInfo'); 11 | const Serverless = require('./test/serverless'); 12 | 13 | describe('GoogleIndex', () => { 14 | let serverless; 15 | let options; 16 | let googleIndex; 17 | 18 | beforeEach(() => { 19 | serverless = new Serverless(); 20 | options = { 21 | stage: 'my-stage', 22 | region: 'my-region', 23 | }; 24 | googleIndex = new GoogleIndex(serverless, options); 25 | }); 26 | 27 | describe('#constructor()', () => { 28 | it('should set the serverless instance', () => { 29 | expect(googleIndex.serverless).toEqual(serverless); 30 | }); 31 | 32 | it('should set options if provided', () => { 33 | expect(googleIndex.options).toEqual(options); 34 | }); 35 | 36 | it('should add all the plugins to the Serverless PluginManager', () => { 37 | const addedPlugins = serverless.plugins; 38 | 39 | expect(addedPlugins).toContain(GoogleProvider); 40 | expect(addedPlugins).toContain(GooglePackage); 41 | expect(addedPlugins).toContain(GoogleDeploy); 42 | expect(addedPlugins).toContain(GoogleRemove); 43 | expect(addedPlugins).toContain(GoogleInvoke); 44 | expect(addedPlugins).toContain(GoogleLogs); 45 | expect(addedPlugins).toContain(GoogleInfo); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /deploy/lib/updateDeployment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const BbPromise = require('bluebird'); 7 | 8 | module.exports = { 9 | updateDeployment() { 10 | return BbPromise.bind(this).then(this.getDeployment).then(this.update); 11 | }, 12 | 13 | getDeployment() { 14 | const params = { 15 | project: this.serverless.service.provider.project, 16 | }; 17 | 18 | return this.provider 19 | .request('deploymentmanager', 'deployments', 'list', params) 20 | .then((response) => { 21 | const deployment = response.deployments.find((dep) => { 22 | const name = `sls-${this.serverless.service.service}-${this.options.stage}`; 23 | return dep.name === name; 24 | }); 25 | 26 | return deployment; 27 | }); 28 | }, 29 | 30 | update(deployment) { 31 | this.serverless.cli.log('Updating deployment...'); 32 | 33 | const filePath = path.join( 34 | this.serverless.config.servicePath, 35 | '.serverless', 36 | 'configuration-template-update.yml' 37 | ); 38 | 39 | const deploymentName = `sls-${this.serverless.service.service}-${this.options.stage}`; 40 | 41 | const params = { 42 | project: this.serverless.service.provider.project, 43 | deployment: deploymentName, 44 | resource: { 45 | name: deploymentName, 46 | fingerprint: deployment.fingerprint, 47 | target: { 48 | config: { 49 | content: fs.readFileSync(filePath).toString(), 50 | }, 51 | }, 52 | }, 53 | }; 54 | 55 | return this.provider 56 | .request('deploymentmanager', 'deployments', 'update', params) 57 | .then(() => this.monitorDeployment(deploymentName, 'update', 5000)); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /invoke/lib/invokeFunction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const BbPromise = require('bluebird'); 6 | const chalk = require('chalk'); 7 | 8 | module.exports = { 9 | invokeFunction() { 10 | return BbPromise.bind(this).then(this.invoke).then(this.printResult); 11 | }, 12 | 13 | invoke() { 14 | const project = this.serverless.service.provider.project; 15 | const region = this.options.region; 16 | let func = this.options.function; 17 | const data = this.options.data || ''; 18 | 19 | func = getGoogleCloudFunctionName(this.serverless.service.functions, func); 20 | 21 | const params = { 22 | name: `projects/${project}/locations/${region}/functions/${func}`, 23 | resource: { 24 | data, 25 | }, 26 | }; 27 | 28 | return this.provider.request( 29 | 'cloudfunctions', 30 | 'projects', 31 | 'locations', 32 | 'functions', 33 | 'call', 34 | params 35 | ); 36 | }, 37 | 38 | printResult(result) { 39 | let res = result; 40 | 41 | if (!result || !result.result) { 42 | res = { 43 | executionId: 'error', 44 | result: 'An error occurred while executing your function...', 45 | }; 46 | } 47 | 48 | const log = `${chalk.grey(res.executionId)} ${res.result}`; 49 | 50 | this.serverless.cli.log(log); 51 | 52 | return BbPromise.resolve(); 53 | }, 54 | }; 55 | 56 | // retrieve the functions name (Google uses our handler property as the function name) 57 | const getGoogleCloudFunctionName = (serviceFunctions, func) => { 58 | if (!serviceFunctions[func]) { 59 | const errorMessage = [ 60 | `Function "${func}" not found. `, 61 | 'Please check your "serverless.yml" file for the correct function name.', 62 | ].join(''); 63 | throw new Error(errorMessage); 64 | } 65 | 66 | return serviceFunctions[func].name; 67 | }; 68 | -------------------------------------------------------------------------------- /deploy/lib/cleanupDeploymentBucket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = { 7 | cleanupDeploymentBucket() { 8 | return BbPromise.bind(this).then(this.getObjectsToRemove).then(this.removeObjects); 9 | }, 10 | 11 | getObjectsToRemove() { 12 | const params = { 13 | bucket: this.serverless.service.provider.deploymentBucketName, 14 | }; 15 | 16 | return this.provider.request('storage', 'objects', 'list', params).then((response) => { 17 | if (!response.items.length) return BbPromise.resolve([]); 18 | 19 | const files = response.items; 20 | 21 | // 4 old ones + the one which will be uploaded after the cleanup = 5 22 | const objectsToKeepCount = 4; 23 | 24 | const orderedObjects = _.orderBy( 25 | files, 26 | (file) => { 27 | const timestamp = file.name.match(/(serverless)\/(.+)\/(.+)\/(\d+)-(.+)\/(.+\.zip)/)[4]; 28 | return timestamp; 29 | }, 30 | ['asc'] 31 | ); 32 | 33 | const objectsToKeep = _.takeRight(orderedObjects, objectsToKeepCount); 34 | const objectsToRemove = _.pullAllWith(files, objectsToKeep, _.isEqual); 35 | 36 | if (objectsToRemove.length) { 37 | return BbPromise.resolve(objectsToRemove); 38 | } 39 | 40 | return BbPromise.resolve([]); 41 | }); 42 | }, 43 | 44 | removeObjects(objectsToRemove) { 45 | if (!objectsToRemove.length) return BbPromise.resolve(); 46 | 47 | this.serverless.cli.log('Removing old artifacts...'); 48 | 49 | const removePromises = objectsToRemove.map((object) => { 50 | const params = { 51 | bucket: object.bucket, 52 | object: object.name, 53 | }; 54 | return this.provider.request('storage', 'objects', 'delete', params); 55 | }); 56 | 57 | return BbPromise.all(removePromises); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /deploy/lib/createDeployment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const BbPromise = require('bluebird'); 7 | 8 | module.exports = { 9 | createDeployment() { 10 | return BbPromise.bind(this).then(this.checkForExistingDeployment).then(this.createIfNotExists); 11 | }, 12 | 13 | checkForExistingDeployment() { 14 | const params = { 15 | project: this.serverless.service.provider.project, 16 | }; 17 | 18 | return this.provider 19 | .request('deploymentmanager', 'deployments', 'list', params) 20 | .then((response) => { 21 | let foundDeployment; 22 | 23 | if (response && response.deployments) { 24 | foundDeployment = response.deployments.find((deployment) => { 25 | const name = `sls-${this.serverless.service.service}-${this.options.stage}`; 26 | return deployment.name === name; 27 | }); 28 | } 29 | 30 | return foundDeployment; 31 | }); 32 | }, 33 | 34 | createIfNotExists(foundDeployment) { 35 | if (foundDeployment) return BbPromise.resolve(); 36 | 37 | this.serverless.cli.log('Creating deployment...'); 38 | 39 | const filePath = path.join( 40 | this.serverless.config.servicePath, 41 | '.serverless', 42 | 'configuration-template-create.yml' 43 | ); 44 | 45 | const deploymentName = `sls-${this.serverless.service.service}-${this.options.stage}`; 46 | 47 | const params = { 48 | project: this.serverless.service.provider.project, 49 | resource: { 50 | name: deploymentName, 51 | target: { 52 | config: { 53 | content: fs.readFileSync(filePath).toString(), 54 | }, 55 | }, 56 | }, 57 | }; 58 | 59 | return this.provider 60 | .request('deploymentmanager', 'deployments', 'insert', params) 61 | .then(() => this.monitorDeployment(deploymentName, 'create', 5000)); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /invokeLocal/lib/getDataAndContext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const stdin = require('get-stdin'); 6 | 7 | module.exports = { 8 | async loadFileInOption(filePath, optionKey) { 9 | const absolutePath = path.isAbsolute(filePath) 10 | ? filePath 11 | : path.join(this.serverless.serviceDir, filePath); 12 | 13 | if (!fs.existsSync(absolutePath)) { 14 | throw new Error(`The file you provided does not exist: ${absolutePath}`); 15 | } 16 | if (absolutePath.endsWith('.js')) { 17 | // to support js - export as an input data 18 | this.options[optionKey] = require(absolutePath); 19 | return; 20 | } 21 | this.options[optionKey] = await this.serverless.utils.readFile(absolutePath); 22 | }, 23 | 24 | async getDataAndContext() { 25 | // unless asked to preserve raw input, attempt to parse any provided objects 26 | if (!this.options.raw) { 27 | if (this.options.data) { 28 | try { 29 | this.options.data = JSON.parse(this.options.data); 30 | } catch (exception) { 31 | // do nothing if it's a simple string or object already 32 | } 33 | } 34 | if (this.options.context) { 35 | try { 36 | this.options.context = JSON.parse(this.options.context); 37 | } catch (exception) { 38 | // do nothing if it's a simple string or object already 39 | } 40 | } 41 | } 42 | 43 | if (!this.options.data) { 44 | if (this.options.path) { 45 | await this.loadFileInOption(this.options.path, 'data'); 46 | } else { 47 | try { 48 | this.options.data = await stdin(); 49 | } catch (e) { 50 | // continue if no stdin was provided 51 | } 52 | } 53 | } 54 | 55 | if (!this.options.context && this.options.contextPath) { 56 | await this.loadFileInOption(this.options.contextPath, 'context'); 57 | } 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Version tags only 2 | 3 | name: Publish 4 | 5 | on: 6 | push: 7 | tags: 8 | - v[0-9]+.[0-9]+.[0-9]+ 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | env: 15 | # Ensure release notes are published by our `serverless-ci` bot 16 | # (If instead we'd use unconditionally provided secrets.GITHUB_TOKEN then 17 | # "github-actions" user will be listed as release publisher) 18 | GITHUB_TOKEN: ${{ secrets.USER_GITHUB_TOKEN }} 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Retrieve dependencies from cache 24 | uses: actions/cache@v2 25 | with: 26 | path: | 27 | ~/.npm 28 | node_modules 29 | key: npm-v14-${{ runner.os }}-refs/heads/master-${{ hashFiles('package.json') }} 30 | 31 | - name: Install Node.js and npm 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: 14.x 35 | registry-url: https://registry.npmjs.org 36 | 37 | - name: Publish new version 38 | # Note: Setting NODE_AUTH_TOKEN as job|workspace wide env var won't work 39 | # as it appears actions/setup-node sets own value 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | run: npm publish 43 | 44 | # Normally we have a guarantee that deps are already there, still it may not be the case when: 45 | # - `master` build for same commit failed (and we still pushed tag manually) 46 | # - We've pushed tag manually before `master` build finalized 47 | - name: Install dependencies 48 | if: steps.cacheNpm.outputs.cache-hit != 'true' 49 | run: | 50 | npm update --no-save 51 | npm update --save-dev --no-save 52 | - name: Publish release notes 53 | run: | 54 | TEMP_ARRAY=($(echo $GITHUB_REF | tr "/" "\n")) 55 | TAG=${TEMP_ARRAY[@]: -1} 56 | npx github-release-from-cc-changelog $TAG 57 | -------------------------------------------------------------------------------- /invokeLocal/lib/testMocks/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * /!\ this file contains fake handlers used in the tests /!\ 3 | */ 4 | 5 | 'use strict'; 6 | const wait = () => new Promise((resolve) => setTimeout(resolve, 10)); 7 | 8 | module.exports = { 9 | eventSyncHandler: (event, context, callback) => { 10 | // eslint-disable-next-line no-console 11 | console.log('EVENT_SYNC_HANDLER'); 12 | callback(null, { result: event.name }); 13 | }, 14 | eventSyncHandlerWithError: (event, context, callback) => { 15 | // eslint-disable-next-line no-console 16 | console.log('EVENT_SYNC_HANDLER'); 17 | callback('SYNC_ERROR'); 18 | }, 19 | eventAsyncHandler: async (event, context) => { 20 | // eslint-disable-next-line no-console 21 | console.log('EVENT_ASYNC_HANDLER'); 22 | await wait(); 23 | return { result: context.name }; 24 | }, 25 | eventAsyncHandlerWithError: async () => { 26 | // eslint-disable-next-line no-console 27 | console.log('EVENT_ASYNC_HANDLER'); 28 | await wait(); 29 | throw new Error('ASYNC_ERROR'); 30 | }, 31 | eventEnvHandler: async () => { 32 | // eslint-disable-next-line no-console 33 | console.log(process.env.MY_VAR); 34 | }, 35 | httpSyncHandler: (req, res) => { 36 | // eslint-disable-next-line no-console 37 | console.log('HTTP_SYNC_HANDLER'); 38 | res.setHeader('x-test', 'headerValue'); 39 | res.send({ responseMessage: req.body.message }); 40 | }, 41 | httpSyncHandlerWithError: () => { 42 | // eslint-disable-next-line no-console 43 | console.log('HTTP_SYNC_HANDLER'); 44 | throw new Error('SYNC_ERROR'); 45 | }, 46 | httpAsyncHandler: async (req, res) => { 47 | // eslint-disable-next-line no-console 48 | console.log('HTTP_ASYNC_HANDLER'); 49 | await wait(); 50 | res.status(404).send(); 51 | }, 52 | httpAsyncHandlerWithError: async () => { 53 | // eslint-disable-next-line no-console 54 | console.log('HTTP_ASYNC_HANDLER'); 55 | await wait(); 56 | throw new Error('ASYNC_ERROR'); 57 | }, 58 | httpEnvHandler: (req, res) => { 59 | // eslint-disable-next-line no-console 60 | console.log(process.env.MY_VAR); 61 | res.send('HTTP_SYNC_BODY'); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /logs/lib/retrieveLogs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const BbPromise = require('bluebird'); 6 | const chalk = require('chalk'); 7 | 8 | module.exports = { 9 | retrieveLogs() { 10 | return BbPromise.bind(this).then(this.getLogs).then(this.printLogs); 11 | }, 12 | 13 | getLogs() { 14 | const project = this.serverless.service.provider.project; 15 | let func = this.options.function; 16 | const pageSize = parseInt(this.options.count, 10) || 10; 17 | 18 | func = getGoogleCloudFunctionName(this.serverless.service.functions, func); 19 | 20 | return this.provider.request('logging', 'entries', 'list', { 21 | filter: `resource.labels.function_name="${func}" AND NOT textPayload=""`, 22 | orderBy: 'timestamp desc', 23 | resourceNames: [`projects/${project}`], 24 | pageSize, 25 | }); 26 | }, 27 | 28 | printLogs(logs) { 29 | if (!logs.entries || !logs.entries.length) { 30 | logs = { 31 | entries: [ 32 | { 33 | timestamp: new Date().toISOString().slice(0, 10), 34 | textPayload: 'There is no log data to show...', 35 | }, 36 | ], 37 | }; 38 | } 39 | 40 | let output = logs.entries.reduce((p, c) => { 41 | let message = ''; 42 | message += `${chalk.grey(`${c.timestamp}:`)}\t`; 43 | message += c.labels && c.labels.execution_id ? `[${c.labels.execution_id}]\t` : ''; 44 | message += `${c.textPayload}\n`; 45 | return p + message; 46 | }, ''); 47 | 48 | output = `Displaying the ${logs.entries.length} most recent log(s):\n\n${output}`; // prettify output 49 | output = output.slice(0, output.length - 1); // remove "\n---\n\n" for the last log entry 50 | 51 | this.serverless.cli.log(output); 52 | 53 | return BbPromise.resolve(); 54 | }, 55 | }; 56 | 57 | const getGoogleCloudFunctionName = (serviceFunctions, func) => { 58 | if (!serviceFunctions[func]) { 59 | const errorMessage = [ 60 | `Function "${func}" not found. `, 61 | 'Please check your "serverless.yml" file for the correct function name.', 62 | ].join(''); 63 | throw new Error(errorMessage); 64 | } 65 | return serviceFunctions[func].name; 66 | }; 67 | -------------------------------------------------------------------------------- /remove/lib/removeDeployment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../../provider/googleProvider'); 7 | const GoogleRemove = require('../googleRemove'); 8 | const Serverless = require('../../test/serverless'); 9 | 10 | describe('RemoveDeployment', () => { 11 | let serverless; 12 | let googleRemove; 13 | let requestStub; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | serverless.service.service = 'my-service'; 18 | serverless.service.provider = { 19 | project: 'my-project', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | const options = { 23 | stage: 'dev', 24 | region: 'us-central1', 25 | }; 26 | googleRemove = new GoogleRemove(serverless, options); 27 | requestStub = sinon.stub(googleRemove.provider, 'request'); 28 | }); 29 | 30 | afterEach(() => { 31 | googleRemove.provider.request.restore(); 32 | }); 33 | 34 | describe('#removeDeployment()', () => { 35 | let consoleLogStub; 36 | let monitorDeploymentStub; 37 | 38 | beforeEach(() => { 39 | consoleLogStub = sinon.stub(googleRemove.serverless.cli, 'log').returns(); 40 | monitorDeploymentStub = sinon 41 | .stub(googleRemove, 'monitorDeployment') 42 | .returns(BbPromise.resolve()); 43 | }); 44 | 45 | afterEach(() => { 46 | googleRemove.serverless.cli.log.restore(); 47 | googleRemove.monitorDeployment.restore(); 48 | }); 49 | 50 | it('should remove and hand over to monitor the deployment', () => { 51 | const params = { 52 | project: 'my-project', 53 | deployment: 'sls-my-service-dev', 54 | }; 55 | requestStub.returns(BbPromise.resolve()); 56 | 57 | return googleRemove.removeDeployment().then(() => { 58 | expect(consoleLogStub.calledOnce).toEqual(true); 59 | expect( 60 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'delete', params) 61 | ).toEqual(true); 62 | expect( 63 | monitorDeploymentStub.calledWithExactly('sls-my-service-dev', 'remove', 5000) 64 | ).toEqual(true); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /shared/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const setDefaults = require('./utils'); 4 | const GoogleProvider = require('../provider/googleProvider'); 5 | const Serverless = require('../test/serverless'); 6 | const GoogleCommand = require('../test/googleCommand'); 7 | 8 | describe('Utils', () => { 9 | let serverless; 10 | let googleCommand; 11 | 12 | beforeEach(() => { 13 | serverless = new Serverless(); 14 | serverless.setProvider('google', new GoogleProvider(serverless)); 15 | googleCommand = new GoogleCommand(serverless, {}, setDefaults); 16 | // mocking the standard value passed in from Serverless here 17 | googleCommand.serverless.service.provider = { 18 | region: 'us-east-1', 19 | }; 20 | }); 21 | 22 | describe('#setDefaults()', () => { 23 | it('should set default values for options if not provided', () => 24 | googleCommand.setDefaults().then(() => { 25 | expect(googleCommand.options.stage).toEqual('dev'); 26 | expect(googleCommand.options.region).toEqual('us-central1'); 27 | expect(googleCommand.options.runtime).toEqual('nodejs10'); 28 | })); 29 | 30 | it('should set the options when they are provided', () => { 31 | googleCommand.options.stage = 'my-stage'; 32 | googleCommand.options.region = 'my-region'; 33 | googleCommand.options.runtime = 'nodejs6'; 34 | 35 | return googleCommand.setDefaults().then(() => { 36 | expect(googleCommand.options.stage).toEqual('my-stage'); 37 | expect(googleCommand.options.region).toEqual('my-region'); 38 | expect(googleCommand.options.runtime).toEqual('nodejs6'); 39 | }); 40 | }); 41 | 42 | it('should set the provider values for stage and region if provided', () => { 43 | googleCommand.serverless.service.provider = { 44 | region: 'my-region', 45 | stage: 'my-stage', 46 | }; 47 | 48 | return googleCommand.setDefaults().then(() => { 49 | expect(googleCommand.options.region).toEqual('my-region'); 50 | expect(googleCommand.options.stage).toEqual('my-stage'); 51 | }); 52 | }); 53 | 54 | it('shoud default to the us-central1 region when no region is provided', () => 55 | googleCommand.setDefaults().then(() => { 56 | expect(googleCommand.options.region).toEqual('us-central1'); 57 | })); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /invoke/googleInvoke.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../provider/googleProvider'); 7 | const GoogleInvoke = require('./googleInvoke'); 8 | const Serverless = require('../test/serverless'); 9 | 10 | describe('GoogleInvoke', () => { 11 | let serverless; 12 | let options; 13 | let googleInvoke; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | options = { 18 | stage: 'my-stage', 19 | region: 'my-region', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | googleInvoke = new GoogleInvoke(serverless, options); 23 | }); 24 | 25 | describe('#constructor()', () => { 26 | it('should set the serverless instance', () => { 27 | expect(googleInvoke.serverless).toEqual(serverless); 28 | }); 29 | 30 | it('should set options if provided', () => { 31 | expect(googleInvoke.options).toEqual(options); 32 | }); 33 | 34 | it('should make the provider accessible', () => { 35 | expect(googleInvoke.provider).toBeInstanceOf(GoogleProvider); 36 | }); 37 | 38 | describe('hooks', () => { 39 | let validateStub; 40 | let setDefaultsStub; 41 | let invokeFunctionStub; 42 | 43 | beforeEach(() => { 44 | validateStub = sinon.stub(googleInvoke, 'validate').returns(BbPromise.resolve()); 45 | setDefaultsStub = sinon.stub(googleInvoke, 'setDefaults').returns(BbPromise.resolve()); 46 | invokeFunctionStub = sinon 47 | .stub(googleInvoke, 'invokeFunction') 48 | .returns(BbPromise.resolve()); 49 | }); 50 | 51 | afterEach(() => { 52 | googleInvoke.validate.restore(); 53 | googleInvoke.setDefaults.restore(); 54 | googleInvoke.invokeFunction.restore(); 55 | }); 56 | 57 | it('should run "before:invoke:invoke" promise chain', () => 58 | googleInvoke.hooks['before:invoke:invoke']().then(() => { 59 | expect(validateStub.calledOnce).toEqual(true); 60 | expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); 61 | })); 62 | 63 | it('should run "invoke:invoke" promise chain', () => 64 | googleInvoke.hooks['invoke:invoke']().then(() => { 65 | expect(invokeFunctionStub.calledOnce).toEqual(true); 66 | })); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /package/lib/writeFilesToDisk.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const sinon = require('sinon'); 6 | 7 | const GoogleProvider = require('../../provider/googleProvider'); 8 | const GooglePackage = require('../googlePackage'); 9 | const Serverless = require('../../test/serverless'); 10 | 11 | describe('WriteFilesToDisk', () => { 12 | let serverless; 13 | let googlePackage; 14 | let writeFileSyncStub; 15 | 16 | beforeEach(() => { 17 | serverless = new Serverless(); 18 | serverless.service.service = 'my-service'; 19 | serverless.service.provider = { 20 | compiledConfigurationTemplate: { 21 | foo: 'bar', 22 | }, 23 | }; 24 | serverless.config = { 25 | servicePath: 'foo/my-service', 26 | }; 27 | serverless.setProvider('google', new GoogleProvider(serverless)); 28 | const options = { 29 | stage: 'dev', 30 | region: 'us-central1', 31 | }; 32 | googlePackage = new GooglePackage(serverless, options); 33 | writeFileSyncStub = sinon.stub(googlePackage.serverless.utils, 'writeFileSync'); 34 | }); 35 | 36 | afterEach(() => { 37 | googlePackage.serverless.utils.writeFileSync.restore(); 38 | }); 39 | 40 | describe('#saveCreateTemplateFile()', () => { 41 | it('should write the template file into the services .serverless directory', () => { 42 | const createFilePath = path.join( 43 | googlePackage.serverless.config.servicePath, 44 | '.serverless', 45 | 'configuration-template-create.yml' 46 | ); 47 | 48 | return googlePackage.saveCreateTemplateFile().then(() => { 49 | expect( 50 | writeFileSyncStub.calledWithExactly( 51 | createFilePath, 52 | googlePackage.serverless.service.provider.compiledConfigurationTemplate 53 | ) 54 | ).toEqual(true); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('#saveUpdateTemplateFile()', () => { 60 | it('should write the template file into the services .serverless directory', () => { 61 | const updateFilePath = path.join( 62 | googlePackage.serverless.config.servicePath, 63 | '.serverless', 64 | 'configuration-template-update.yml' 65 | ); 66 | 67 | return googlePackage.saveUpdateTemplateFile().then(() => { 68 | expect( 69 | writeFileSyncStub.calledWithExactly( 70 | updateFilePath, 71 | googlePackage.serverless.service.provider.compiledConfigurationTemplate 72 | ) 73 | ).toEqual(true); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /info/googleInfo.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../provider/googleProvider'); 7 | const GoogleInfo = require('./googleInfo'); 8 | const Serverless = require('../test/serverless'); 9 | 10 | describe('GoogleInfo', () => { 11 | let serverless; 12 | let options; 13 | let googleInfo; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | options = { 18 | stage: 'my-stage', 19 | region: 'my-region', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | googleInfo = new GoogleInfo(serverless, options); 23 | }); 24 | 25 | describe('#constructor()', () => { 26 | it('should set the serverless instance', () => { 27 | expect(googleInfo.serverless).toEqual(serverless); 28 | }); 29 | 30 | it('should set options if provided', () => { 31 | expect(googleInfo.options).toEqual(options); 32 | }); 33 | 34 | it('should make the provider accessible', () => { 35 | expect(googleInfo.provider).toBeInstanceOf(GoogleProvider); 36 | }); 37 | 38 | describe('hooks', () => { 39 | let validateStub; 40 | let setDefaultsStub; 41 | let displayServiceInfoStub; 42 | 43 | beforeEach(() => { 44 | validateStub = sinon.stub(googleInfo, 'validate').returns(BbPromise.resolve()); 45 | setDefaultsStub = sinon.stub(googleInfo, 'setDefaults').returns(BbPromise.resolve()); 46 | displayServiceInfoStub = sinon 47 | .stub(googleInfo, 'displayServiceInfo') 48 | .returns(BbPromise.resolve()); 49 | }); 50 | 51 | afterEach(() => { 52 | googleInfo.validate.restore(); 53 | googleInfo.setDefaults.restore(); 54 | googleInfo.displayServiceInfo.restore(); 55 | }); 56 | 57 | it('should run "before:info:info" promise chain', () => 58 | googleInfo.hooks['before:info:info']().then(() => { 59 | expect(validateStub.calledOnce).toEqual(true); 60 | expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); 61 | })); 62 | 63 | it('should run "deploy:deploy" promise chain', () => 64 | googleInfo.hooks['deploy:deploy']().then(() => { 65 | expect(displayServiceInfoStub.calledOnce).toEqual(true); 66 | })); 67 | 68 | it('should run "info:info" promise chain', () => 69 | googleInfo.hooks['info:info']().then(() => { 70 | expect(displayServiceInfoStub.calledOnce).toEqual(true); 71 | })); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /shared/monitorDeployment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const BbPromise = require('bluebird'); 6 | const async = require('async'); 7 | 8 | module.exports = { 9 | monitorDeployment(deploymentName, action, frequency) { 10 | const validStatuses = ['DONE']; 11 | 12 | let deploymentStatus = null; 13 | 14 | this.serverless.cli.log(`Checking deployment ${action} progress...`); 15 | 16 | return new BbPromise((resolve, reject) => { 17 | async.whilst( 18 | () => validStatuses.indexOf(deploymentStatus) === -1, 19 | 20 | (callback) => { 21 | setTimeout(() => { 22 | const params = { 23 | project: this.serverless.service.provider.project, 24 | }; 25 | return this.provider 26 | .request('deploymentmanager', 'deployments', 'list', params) 27 | .then((response) => { 28 | // if actions is "remove" and no deployments are left set to "DONE" 29 | if (!response.deployments && action === 'remove') { 30 | deploymentStatus = 'DONE'; 31 | callback(); 32 | } 33 | 34 | const deployment = response.deployments.find((dep) => dep.name === deploymentName); 35 | 36 | // if actions is "remove" and deployment disappeared then set to "DONE" 37 | if (!deployment && action === 'remove') { 38 | deploymentStatus = 'DONE'; 39 | callback(); 40 | } 41 | 42 | throwErrorIfDeploymentFails(deployment); 43 | 44 | deploymentStatus = deployment.operation.status; 45 | 46 | this.serverless.cli.printDot(); 47 | return callback(); 48 | }) 49 | .catch((error) => { 50 | reject(error); 51 | }); 52 | }, frequency); 53 | }, 54 | 55 | () => { 56 | // empty console.log for a prettier output 57 | this.serverless.cli.consoleLog(''); 58 | this.serverless.cli.log('Done...'); 59 | resolve(deploymentStatus); 60 | } 61 | ); 62 | }); 63 | }, 64 | }; 65 | 66 | const throwErrorIfDeploymentFails = (deployment) => { 67 | if (deployment.operation.error && deployment.operation.error.errors.length) { 68 | const errorCode = deployment.operation.error.errors[0].code; 69 | const parsedDetails = deployment.operation.error.errors[0].message; 70 | const errorMessage = [`Deployment failed: ${errorCode}\n\n`, ` ${parsedDetails}`].join(''); 71 | throw new Error(errorMessage); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /MIGRATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Migration Reference from v2 to v3 2 | 3 | ## Background 4 | 5 | > Google Cloud Functions v1beta2 API version will be shut down on April 15, 2020 6 | 7 | When you are using serverless to deploy google cloud functions with serverless google plugin v3 (Support google api v1), 8 | It may face a error like 9 | 10 | ``` 11 | Deployment failed: TYPE_MISMATCH 12 | Resource types cannot be changed, previous (cloudfunctions.v1beta2.function) -> updated (gcp-types/cloudfunctions-v1:projects.locations.functions) 13 | ``` 14 | 15 | Which means the former ones deployed used serverless-google-cloudfunctions v2, thus it failed to deploy. 16 | First please be careful that 17 | 18 | ## Solutions 19 | 20 | If you choose to upgrade to v1 function , and make sure the _package.json_ using the latest plugin in nodejs project 21 | 22 | ```json 23 | "devDependencies": { 24 | "serverless-google-cloudfunctions": "*" 25 | }, 26 | ``` 27 | 28 | There are two options to upgrade the exising cloud functions that deploy via _serverless-google-cloudfunctions v2_ (google api v1beta). 29 | 30 | ### Option 1 31 | 32 | The first is from the devops point of view ,you don't need to change the code at all. 33 | 34 | you need to open the [deployment manager](https://cloud.google.com/deployment-manager) in GCP. 35 | 36 | - _Delete all the functions_ 37 | 38 | You have to delete all the functions and related bucket first ,and then delete the all the related resources from deployment manager 39 | 40 | - _Delete all the related buckets with cloud functions_ 41 | 42 | By default, each time you you use `serverless deploy` , it would create a bucket for you to store the zip package for the function. pls delete this bucket first. 43 | 44 | - _Delete all the function resources in deployment manager_ 45 | 46 | - _Redeploy the functions_ 47 | 48 | ### Option 2 49 | 50 | The second is from the developers' point of view , which means you need to make some changes to the `serverless.yml`. 51 | 52 | - Change the service name or change the function name to make sure this function is different from the older one. 53 | 54 | - Redeploy the functions. 55 | 56 | - Once it's done,you may consider delete the old ones. 57 | 58 | ### Notices 59 | 60 | Both the methods have some pros and cons, see the details from here: 61 | 62 | 1. If your functions are called by bucket or pubsub, the modification of the function name may not impact your business. 63 | 64 | 2. If your functions are called by http, the function name would be changed, which means the http url that need to be called may also be changed 65 | 66 | 3. If you use cloud function to store some important data, pls export these data first and then import them to a new bucket. 67 | -------------------------------------------------------------------------------- /invokeLocal/lib/getDataAndContext.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | 5 | const GoogleProvider = require('../../provider/googleProvider'); 6 | const GoogleInvokeLocal = require('../googleInvokeLocal'); 7 | const Serverless = require('../../test/serverless'); 8 | 9 | jest.mock('get-stdin'); 10 | 11 | describe('getDataAndContext', () => { 12 | let serverless; 13 | let googleInvokeLocal; 14 | let loadFileInOptionStub; 15 | 16 | beforeEach(() => { 17 | serverless = new Serverless(); 18 | serverless.setProvider('google', new GoogleProvider(serverless)); 19 | googleInvokeLocal = new GoogleInvokeLocal(serverless, {}); 20 | loadFileInOptionStub = sinon.stub(googleInvokeLocal, 'loadFileInOption').resolves(); 21 | }); 22 | 23 | afterEach(() => { 24 | googleInvokeLocal.loadFileInOption.restore(); 25 | }); 26 | 27 | describe.each` 28 | key | pathKey 29 | ${'data'} | ${'path'} 30 | ${'context'} | ${'contextPath'} 31 | `('$key', ({ key, pathKey }) => { 32 | it('should keep the raw value if the value exist and there is the raw option', async () => { 33 | const rawValue = Symbol('rawValue'); 34 | googleInvokeLocal.options[key] = rawValue; 35 | googleInvokeLocal.options.raw = true; 36 | await googleInvokeLocal.getDataAndContext(); 37 | expect(googleInvokeLocal.options[key]).toEqual(rawValue); 38 | }); 39 | 40 | it('should keep the raw value if the value exist and is not a valid JSON', async () => { 41 | const rawValue = 'rawValue'; 42 | googleInvokeLocal.options[key] = rawValue; 43 | await googleInvokeLocal.getDataAndContext(); 44 | expect(googleInvokeLocal.options[key]).toEqual(rawValue); 45 | }); 46 | 47 | it('should parse the raw value if the value exist and is a stringified JSON', async () => { 48 | googleInvokeLocal.options[key] = '{"attribute":"value"}'; 49 | await googleInvokeLocal.getDataAndContext(); 50 | expect(googleInvokeLocal.options[key]).toEqual({ attribute: 'value' }); 51 | }); 52 | 53 | it('should load the file from the provided path if it exists', async () => { 54 | const path = 'path'; 55 | googleInvokeLocal.options[pathKey] = path; 56 | await googleInvokeLocal.getDataAndContext(); 57 | expect(loadFileInOptionStub.calledOnceWith(path, key)).toEqual(true); 58 | }); 59 | 60 | it('should not load the file from the provided path if the key already exists', async () => { 61 | const rawValue = Symbol('rawValue'); 62 | googleInvokeLocal.options[key] = rawValue; 63 | googleInvokeLocal.options[pathKey] = 'path'; 64 | 65 | await googleInvokeLocal.getDataAndContext(); 66 | 67 | expect(loadFileInOptionStub.notCalled).toEqual(true); 68 | expect(googleInvokeLocal.options[key]).toEqual(rawValue); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /package/lib/prepareDeployment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | 5 | const GoogleProvider = require('../../provider/googleProvider'); 6 | const GooglePackage = require('../googlePackage'); 7 | const Serverless = require('../../test/serverless'); 8 | 9 | describe('PrepareDeployment', () => { 10 | let coreResources; 11 | let serverless; 12 | let googlePackage; 13 | 14 | beforeEach(() => { 15 | coreResources = { 16 | resources: [ 17 | { 18 | type: 'storage.v1.bucket', 19 | name: 'will-be-replaced-by-serverless', 20 | }, 21 | ], 22 | }; 23 | serverless = new Serverless(); 24 | serverless.service.service = 'my-service'; 25 | serverless.service.provider = { 26 | compiledConfigurationTemplate: coreResources, 27 | deploymentBucketName: 'sls-my-service-dev-12345678', 28 | }; 29 | serverless.setProvider('google', new GoogleProvider(serverless)); 30 | const options = { 31 | stage: 'dev', 32 | region: 'us-central1', 33 | }; 34 | googlePackage = new GooglePackage(serverless, options); 35 | }); 36 | 37 | describe('#prepareDeployment()', () => { 38 | let readFileSyncStub; 39 | 40 | beforeEach(() => { 41 | readFileSyncStub = sinon.stub(serverless.utils, 'readFileSync').returns(coreResources); 42 | }); 43 | 44 | afterEach(() => { 45 | serverless.utils.readFileSync.restore(); 46 | }); 47 | 48 | it('should load the core configuration template into the serverless instance', () => { 49 | const expectedCompiledConfiguration = { 50 | resources: [ 51 | { 52 | type: 'storage.v1.bucket', 53 | name: 'sls-my-service-dev-12345678', 54 | }, 55 | ], 56 | }; 57 | 58 | return googlePackage.prepareDeployment().then(() => { 59 | expect(readFileSyncStub.calledOnce).toEqual(true); 60 | expect(serverless.service.provider.compiledConfigurationTemplate).toEqual( 61 | expectedCompiledConfiguration 62 | ); 63 | }); 64 | }); 65 | 66 | it('should use the configured location', () => { 67 | serverless.service.provider.region = 'europe-west1'; 68 | 69 | const expectedCompiledConfiguration = { 70 | resources: [ 71 | { 72 | type: 'storage.v1.bucket', 73 | name: 'sls-my-service-dev-12345678', 74 | properties: { 75 | location: 'europe-west1', 76 | }, 77 | }, 78 | ], 79 | }; 80 | 81 | return googlePackage.prepareDeployment().then(() => { 82 | expect(readFileSyncStub.calledOnce).toEqual(true); 83 | expect(serverless.service.provider.compiledConfigurationTemplate).toEqual( 84 | expectedCompiledConfiguration 85 | ); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /package/lib/mergeServiceResources.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GoogleProvider = require('../../provider/googleProvider'); 4 | const GooglePackage = require('../googlePackage'); 5 | const Serverless = require('../../test/serverless'); 6 | 7 | describe('MergeServiceResources', () => { 8 | let serverless; 9 | let googlePackage; 10 | 11 | beforeEach(() => { 12 | serverless = new Serverless(); 13 | serverless.service.service = 'my-service'; 14 | serverless.service.provider = { 15 | compiledConfigurationTemplate: {}, 16 | }; 17 | serverless.setProvider('google', new GoogleProvider(serverless)); 18 | const options = { 19 | stage: 'dev', 20 | region: 'us-central1', 21 | }; 22 | googlePackage = new GooglePackage(serverless, options); 23 | }); 24 | 25 | it('should resolve if service resources are not defined', () => 26 | googlePackage.mergeServiceResources().then(() => { 27 | expect(serverless.service.provider.compiledConfigurationTemplate).toEqual({}); 28 | })); 29 | 30 | it('should resolve if service resources is empty', () => { 31 | serverless.service.resources = {}; 32 | 33 | return googlePackage.mergeServiceResources().then(() => { 34 | expect(serverless.service.provider.compiledConfigurationTemplate).toEqual({}); 35 | }); 36 | }); 37 | 38 | it('should merge all the resources if provided', () => { 39 | serverless.service.provider.compiledConfigurationTemplate = { 40 | resources: [ 41 | { 42 | name: 'resource1', 43 | type: 'type1', 44 | properties: { 45 | property1: 'value1', 46 | }, 47 | }, 48 | ], 49 | }; 50 | 51 | serverless.service.resources = { 52 | resources: [ 53 | { 54 | name: 'resource2', 55 | type: 'type2', 56 | properties: { 57 | property1: 'value1', 58 | }, 59 | }, 60 | ], 61 | imports: [ 62 | { 63 | path: 'path/to/template.jinja', 64 | name: 'my-template', 65 | }, 66 | ], 67 | }; 68 | 69 | const expectedResult = { 70 | resources: [ 71 | { 72 | name: 'resource1', 73 | type: 'type1', 74 | properties: { 75 | property1: 'value1', 76 | }, 77 | }, 78 | { 79 | name: 'resource2', 80 | type: 'type2', 81 | properties: { 82 | property1: 'value1', 83 | }, 84 | }, 85 | ], 86 | imports: [ 87 | { 88 | path: 'path/to/template.jinja', 89 | name: 'my-template', 90 | }, 91 | ], 92 | }; 93 | 94 | return googlePackage.mergeServiceResources().then(() => { 95 | expect(serverless.service.provider.compiledConfigurationTemplate).toEqual(expectedResult); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /package/lib/cleanupServerlessDir.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const sinon = require('sinon'); 6 | const fse = require('fs-extra'); 7 | 8 | const GoogleProvider = require('../../provider/googleProvider'); 9 | const GooglePackage = require('../googlePackage'); 10 | const Serverless = require('../../test/serverless'); 11 | 12 | describe('CleanupServerlessDir', () => { 13 | let serverless; 14 | let googlePackage; 15 | let pathExistsSyncStub; 16 | let removeSyncStub; 17 | 18 | beforeEach(() => { 19 | serverless = new Serverless(); 20 | serverless.service.service = 'my-service'; 21 | serverless.config = { 22 | servicePath: false, 23 | }; 24 | serverless.setProvider('google', new GoogleProvider(serverless)); 25 | const options = { 26 | stage: 'dev', 27 | region: 'us-central1', 28 | }; 29 | googlePackage = new GooglePackage(serverless, options); 30 | pathExistsSyncStub = sinon.stub(fse, 'pathExistsSync'); 31 | removeSyncStub = sinon.stub(fse, 'removeSync').returns(); 32 | }); 33 | 34 | afterEach(() => { 35 | fse.pathExistsSync.restore(); 36 | fse.removeSync.restore(); 37 | }); 38 | 39 | describe('#cleanupServerlessDir()', () => { 40 | it('should resolve if no servicePath is given', () => { 41 | googlePackage.serverless.config.servicePath = false; 42 | 43 | pathExistsSyncStub.returns(); 44 | 45 | return googlePackage.cleanupServerlessDir().then(() => { 46 | expect(pathExistsSyncStub.calledOnce).toEqual(false); 47 | expect(removeSyncStub.calledOnce).toEqual(false); 48 | }); 49 | }); 50 | 51 | it('should remove the .serverless directory if it exists', () => { 52 | const serviceName = googlePackage.serverless.service.service; 53 | googlePackage.serverless.config.servicePath = serviceName; 54 | const serverlessDirPath = path.join(serviceName, '.serverless'); 55 | 56 | pathExistsSyncStub.returns(true); 57 | 58 | return googlePackage.cleanupServerlessDir().then(() => { 59 | expect(pathExistsSyncStub.calledWithExactly(serverlessDirPath)).toEqual(true); 60 | expect(removeSyncStub.calledWithExactly(serverlessDirPath)).toEqual(true); 61 | }); 62 | }); 63 | 64 | it('should not remove the .serverless directory if does not exist', () => { 65 | const serviceName = googlePackage.serverless.service.service; 66 | googlePackage.serverless.config.servicePath = serviceName; 67 | const serverlessDirPath = path.join(serviceName, '.serverless'); 68 | 69 | pathExistsSyncStub.returns(false); 70 | 71 | return googlePackage.cleanupServerlessDir().then(() => { 72 | expect(pathExistsSyncStub.calledWithExactly(serverlessDirPath)).toEqual(true); 73 | expect(removeSyncStub.calledWithExactly(serverlessDirPath)).toEqual(false); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-google-cloudfunctions", 3 | "version": "4.6.0", 4 | "description": "Provider plugin for the Serverless Framework v1.x which adds support for Google Cloud Functions.", 5 | "author": "serverless.com", 6 | "repository": "serverless/serverless-google-cloudfunctions", 7 | "homepage": "https://github.com/serverless/serverless-google-cloudfunctions", 8 | "keywords": [ 9 | "serverless", 10 | "serverless framework", 11 | "serverless applications", 12 | "serverless modules", 13 | "google cloud functions", 14 | "iot", 15 | "internet of things", 16 | "serverless.com" 17 | ], 18 | "eslintConfig": { 19 | "extends": "@serverless/eslint-config/node", 20 | "root": true 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged" 25 | } 26 | }, 27 | "lint-staged": { 28 | "*.js": [ 29 | "eslint" 30 | ], 31 | "*.{css,html,js,json,md,yaml,yml}": [ 32 | "prettier -c" 33 | ] 34 | }, 35 | "standard-version": { 36 | "skip": { 37 | "commit": true, 38 | "tag": true 39 | } 40 | }, 41 | "dependencies": { 42 | "async": "^2.6.4", 43 | "bluebird": "^3.7.2", 44 | "chalk": "^3.0.0", 45 | "express": "4.18.1", 46 | "fs-extra": "^8.1.0", 47 | "get-stdin": "^8.0.0", 48 | "googleapis": "^50.0.0", 49 | "lodash": "^4.17.21" 50 | }, 51 | "devDependencies": { 52 | "@commitlint/cli": "^9.1.2", 53 | "@serverless/eslint-config": "^2.2.0", 54 | "coveralls": "^3.1.1", 55 | "eslint": "^7.32.0", 56 | "eslint-plugin-import": "^2.26.0", 57 | "git-list-updated": "^1.2.1", 58 | "github-release-from-cc-changelog": "^2.3.0", 59 | "husky": "^4.3.8", 60 | "jest": "^25.5.4", 61 | "lint-staged": "^10.5.4", 62 | "prettier": "^2.6.2", 63 | "sinon": "^8.1.1", 64 | "standard-version": "^9.3.2" 65 | }, 66 | "peerDependencies": { 67 | "serverless": "2 || 3" 68 | }, 69 | "scripts": { 70 | "commitlint": "commitlint -f HEAD@{15}", 71 | "commitlint:pull-request": "commitlint -f HEAD~1", 72 | "coverage": "jest --coverage", 73 | "lint": "eslint . --cache", 74 | "lint:updated": "pipe-git-updated --ext=js -- eslint --cache", 75 | "prepare-release": "standard-version && prettier --write CHANGELOG.md", 76 | "prettier-check": "prettier -c --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"", 77 | "prettier-check:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier -c", 78 | "prettify": "prettier --write --ignore-path .gitignore \"**/*.{css,html,js,json,md,yaml,yml}\"", 79 | "prettify:updated": "pipe-git-updated --ext=css --ext=html --ext=js --ext=json --ext=md --ext=yaml --ext=yml -- prettier --write", 80 | "test": "jest" 81 | }, 82 | "engines": { 83 | "node": ">=10.0" 84 | }, 85 | "license": "MIT" 86 | } 87 | -------------------------------------------------------------------------------- /package/googlePackage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | 5 | const cleanupServerlessDir = require('./lib/cleanupServerlessDir'); 6 | const validate = require('../shared/validate'); 7 | const utils = require('../shared/utils'); 8 | const setDeploymentBucketName = require('../shared/setDeploymentBucketName'); 9 | const prepareDeployment = require('./lib/prepareDeployment'); 10 | const saveCreateTemplateFile = require('./lib/writeFilesToDisk'); 11 | const mergeServiceResources = require('./lib/mergeServiceResources'); 12 | const generateArtifactDirectoryName = require('./lib/generateArtifactDirectoryName'); 13 | const compileFunctions = require('./lib/compileFunctions'); 14 | const saveUpdateTemplateFile = require('./lib/writeFilesToDisk'); 15 | 16 | class GooglePackage { 17 | constructor(serverless, options) { 18 | this.serverless = serverless; 19 | this.options = options; 20 | this.provider = this.serverless.getProvider('google'); 21 | this.serverless.configSchemaHandler.defineFunctionEvent('google', 'http', { type: 'string' }); 22 | this.serverless.configSchemaHandler.defineFunctionEvent('google', 'event', { 23 | type: 'object', 24 | properties: { 25 | eventType: { 26 | type: 'string', 27 | }, 28 | path: { 29 | type: 'string', 30 | }, 31 | resource: { 32 | type: 'string', 33 | }, 34 | failurePolicy: { 35 | type: 'object', 36 | properties: { 37 | retry: { 38 | type: 'object', 39 | }, 40 | }, 41 | additionalProperties: false, 42 | }, 43 | }, 44 | required: ['eventType', 'resource'], 45 | additionalProperties: false, 46 | }); 47 | 48 | Object.assign( 49 | this, 50 | cleanupServerlessDir, 51 | validate, 52 | utils, 53 | setDeploymentBucketName, 54 | prepareDeployment, 55 | saveCreateTemplateFile, 56 | generateArtifactDirectoryName, 57 | compileFunctions, 58 | mergeServiceResources, 59 | saveUpdateTemplateFile 60 | ); 61 | 62 | this.hooks = { 63 | 'package:cleanup': () => BbPromise.bind(this).then(this.cleanupServerlessDir), 64 | 65 | 'before:package:initialize': () => 66 | BbPromise.bind(this).then(this.validate).then(this.setDefaults), 67 | 68 | 'package:initialize': () => 69 | BbPromise.bind(this) 70 | .then(this.setDeploymentBucketName) 71 | .then(this.prepareDeployment) 72 | .then(this.saveCreateTemplateFile), 73 | 74 | 'before:package:compileFunctions': () => 75 | BbPromise.bind(this).then(this.generateArtifactDirectoryName), 76 | 77 | 'package:compileFunctions': () => BbPromise.bind(this).then(this.compileFunctions), 78 | 79 | 'package:finalize': () => 80 | BbPromise.bind(this).then(this.mergeServiceResources).then(this.saveUpdateTemplateFile), 81 | }; 82 | } 83 | } 84 | 85 | module.exports = GooglePackage; 86 | -------------------------------------------------------------------------------- /shared/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BbPromise = require('bluebird'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = { 7 | validate() { 8 | return BbPromise.bind(this) 9 | .then(this.validateServicePath) 10 | .then(this.validateServiceName) 11 | .then(this.validateHandlers); 12 | }, 13 | 14 | validateServicePath() { 15 | if (!this.serverless.config.servicePath) { 16 | throw new Error('This command can only be run inside a service directory'); 17 | } 18 | 19 | return BbPromise.resolve(); 20 | }, 21 | 22 | validateServiceName() { 23 | const name = this.serverless.service.service; 24 | 25 | // should not contain 'goog' 26 | if (name.match(/goog/)) { 27 | throw new Error('Your service should not contain the string "goog"'); 28 | } 29 | 30 | if (name.match(/_+/)) { 31 | throw new Error('Your service name should not include underscores'); 32 | } 33 | 34 | return BbPromise.resolve(); 35 | }, 36 | 37 | validateHandlers() { 38 | const functions = this.serverless.service.functions; 39 | 40 | _.forEach(functions, (funcVal, funcKey) => { 41 | if (_.includes(funcVal.handler, '/') || _.includes(funcVal.handler, '.')) { 42 | const errorMessage = [ 43 | `The "handler" property for the function "${funcKey}" is invalid.`, 44 | ' Handlers should be plain strings referencing only the exported function name', 45 | ' without characters such as "." or "/" (so e.g. "http" instead of "index.http").', 46 | ' Do you want to nest your functions code in a subdirectory?', 47 | ' Google solves this by utilizing the "main" config in the projects package.json file.', 48 | ' Please check the docs for more info.', 49 | ].join(''); 50 | throw new Error(errorMessage); 51 | } 52 | }); 53 | }, 54 | 55 | validateEventsProperty(funcObject, functionName, supportedEvents = ['http', 'event']) { 56 | if (!funcObject.events || funcObject.events.length === 0) { 57 | const errorMessage = [ 58 | `Missing "events" property for function "${functionName}".`, 59 | ' Your function needs at least one "event".', 60 | ' Please check the docs for more info.', 61 | ].join(''); 62 | throw new Error(errorMessage); 63 | } 64 | 65 | if (funcObject.events.length > 1) { 66 | const errorMessage = [ 67 | `The function "${functionName}" has more than one event.`, 68 | ' Only one event per function is supported.', 69 | ' Please check the docs for more info.', 70 | ].join(''); 71 | throw new Error(errorMessage); 72 | } 73 | 74 | const eventType = Object.keys(funcObject.events[0])[0]; 75 | if (supportedEvents.indexOf(eventType) === -1) { 76 | const errorMessage = [ 77 | `Event type "${eventType}" of function "${functionName}" not supported.`, 78 | ` supported event types are: ${supportedEvents.join(', ')}`, 79 | ].join(''); 80 | throw new Error(errorMessage); 81 | } 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /remove/googleRemove.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../provider/googleProvider'); 7 | const GoogleRemove = require('./googleRemove'); 8 | const Serverless = require('../test/serverless'); 9 | 10 | describe('GoogleRemove', () => { 11 | let serverless; 12 | let options; 13 | let googleRemove; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | options = { 18 | stage: 'my-stage', 19 | region: 'my-region', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | googleRemove = new GoogleRemove(serverless, options); 23 | }); 24 | 25 | describe('#constructor()', () => { 26 | it('should set the serverless instance', () => { 27 | expect(googleRemove.serverless).toEqual(serverless); 28 | }); 29 | 30 | it('should set options if provided', () => { 31 | expect(googleRemove.options).toEqual(options); 32 | }); 33 | 34 | it('should make the provider accessible', () => { 35 | expect(googleRemove.provider).toBeInstanceOf(GoogleProvider); 36 | }); 37 | 38 | describe('hooks', () => { 39 | let validateStub; 40 | let setDefaultsStub; 41 | let setDeploymentBucketNameStub; 42 | let emptyDeploymentBucketStub; 43 | let removeDeploymentStub; 44 | 45 | beforeEach(() => { 46 | validateStub = sinon.stub(googleRemove, 'validate').returns(BbPromise.resolve()); 47 | setDefaultsStub = sinon.stub(googleRemove, 'setDefaults').returns(BbPromise.resolve()); 48 | setDeploymentBucketNameStub = sinon 49 | .stub(googleRemove, 'setDeploymentBucketName') 50 | .returns(BbPromise.resolve()); 51 | emptyDeploymentBucketStub = sinon 52 | .stub(googleRemove, 'emptyDeploymentBucket') 53 | .returns(BbPromise.resolve()); 54 | removeDeploymentStub = sinon 55 | .stub(googleRemove, 'removeDeployment') 56 | .returns(BbPromise.resolve()); 57 | }); 58 | 59 | afterEach(() => { 60 | googleRemove.validate.restore(); 61 | googleRemove.setDefaults.restore(); 62 | googleRemove.setDeploymentBucketName.restore(); 63 | googleRemove.emptyDeploymentBucket.restore(); 64 | googleRemove.removeDeployment.restore(); 65 | }); 66 | 67 | it('should run "before:remove:remove" promise chain', () => 68 | googleRemove.hooks['before:remove:remove']().then(() => { 69 | expect(validateStub.calledOnce).toEqual(true); 70 | expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); 71 | expect(setDeploymentBucketNameStub.calledAfter(setDefaultsStub)).toEqual(true); 72 | })); 73 | 74 | it('should run "remove:remove" promise chain', () => 75 | googleRemove.hooks['remove:remove']().then(() => { 76 | expect(emptyDeploymentBucketStub.calledOnce).toEqual(true); 77 | expect(removeDeploymentStub.calledAfter(emptyDeploymentBucketStub)).toEqual(true); 78 | })); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /logs/googleLogs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../provider/googleProvider'); 7 | const GoogleLogs = require('./googleLogs'); 8 | const Serverless = require('../test/serverless'); 9 | 10 | describe('GoogleLogs', () => { 11 | let serverless; 12 | let options; 13 | let googleLogs; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | options = { 18 | stage: 'my-stage', 19 | region: 'my-region', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | googleLogs = new GoogleLogs(serverless, options); 23 | }); 24 | 25 | describe('#constructor()', () => { 26 | it('should set the serverless instance', () => { 27 | expect(googleLogs.serverless).toEqual(serverless); 28 | }); 29 | 30 | it('should set options if provided', () => { 31 | expect(googleLogs.options).toEqual(options); 32 | }); 33 | 34 | it('should make the provider accessible', () => { 35 | expect(googleLogs.provider).toBeInstanceOf(GoogleProvider); 36 | }); 37 | 38 | it('should have the command "logs"', () => { 39 | expect(googleLogs.commands.logs).not.toEqual(undefined); 40 | }); 41 | 42 | it('should have the lifecycle event "logs" for the "logs" command', () => { 43 | expect(googleLogs.commands.logs.lifecycleEvents).toEqual(['logs']); 44 | }); 45 | 46 | it('should have the option "count" with the "c" shortcut', () => { 47 | expect(googleLogs.commands.logs.options.count).not.toEqual(undefined); 48 | expect(googleLogs.commands.logs.options.count.shortcut).toEqual('c'); 49 | }); 50 | 51 | it('should have the option "count"', () => { 52 | expect(googleLogs.commands.logs.options.count).not.toEqual(undefined); 53 | }); 54 | 55 | it('should have the option "count" with type "string"', () => { 56 | expect(googleLogs.commands.logs.options.count.type).toEqual('string'); 57 | }); 58 | 59 | describe('hooks', () => { 60 | let validateStub; 61 | let setDefaultsStub; 62 | let retrieveLogsStub; 63 | 64 | beforeEach(() => { 65 | validateStub = sinon.stub(googleLogs, 'validate').returns(BbPromise.resolve()); 66 | setDefaultsStub = sinon.stub(googleLogs, 'setDefaults').returns(BbPromise.resolve()); 67 | retrieveLogsStub = sinon.stub(googleLogs, 'retrieveLogs').returns(BbPromise.resolve()); 68 | }); 69 | 70 | afterEach(() => { 71 | googleLogs.validate.restore(); 72 | googleLogs.setDefaults.restore(); 73 | googleLogs.retrieveLogs.restore(); 74 | }); 75 | 76 | it('should run "before:logs:logs" promise chain', () => 77 | googleLogs.hooks['before:logs:logs']().then(() => { 78 | expect(validateStub.calledOnce).toEqual(true); 79 | expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); 80 | })); 81 | 82 | it('should run "logs:logs" promise chain', () => 83 | googleLogs.hooks['logs:logs']().then(() => { 84 | expect(retrieveLogsStub.calledOnce).toEqual(true); 85 | })); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | ## Semi-automation 4 | 5 | This project relies on [semantic commit messages](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary) which allows to streamline the release process (versioning and changelog generation is automated) 6 | 7 | See proposed [Commit Message Guidelines](https://docs.google.com/document/d/1hKUs3qt_aVp_PBI1UqvfaIqKma3jAJimEoGCRGGbOqs/edit#) 8 | 9 | In PR's as coming from forks (community contributions) while its welcome, we do not require to follow semantic commit messages. Yet, such PR is expected to be squash merged by project member with single semantic commit message. 10 | 11 | PR's comming from branches have commit messages validated with [commmitlint](https://commitlint.js.org/#/) 12 | 13 | ## Release flow 14 | 15 | Releases are triggered manually by preparing a release PR's as follows 16 | 17 | 1. Create a `release` branch (should derive from current `master` state) 18 | 2. Bump version ranges of _all_ dependencies to latest supported versions (e.g. if latest version of a dependency is `2.3.5` and range in a `package.json` is `^2.2.4` then it should be updated to `^2.3.5`) 19 | _Note: Unfortunately there seems no reliable utility to automate that (there's a [request at `npm-check-updates`](https://github.com/tjunnone/npm-check-updates/issues/581)) 20 | If you handle installation of dependencies through [npm-cross-link](https://github.com/medikoo/npm-cross-link#npm-cross-link) then [`--bump-deps`](https://github.com/medikoo/npm-cross-link#general-options) option will bump version ranges as expected_ 21 | 3. Commit eventual dependency version updates with following commit message: 22 | `chore: Bump dependencies` 23 | 4. Run `npm run prepare-release` command. 24 | _It'll automatically bump version in `package.json` to expected one (by inspecting changes since previous release) and will generate new changelog entry._ 25 | 5. If needed improve generated changelog entry in `CHANGELOG.md` 26 | 6. Commit `package.json` and `CHANGELOG.md` changes with following commit message: 27 | `chore: Release` 28 | **Note: For automation purpose it is important that it's the last commit in the PR** 29 | 7. Push branch upstream and create a PR. 30 | _Release PR's are automatically detected in CI by fact of `version` in `package.json` file being changed in last commit. In context of that build, existence of new version changelog entry (in `CHANGELOG.md`) is validated._ 31 | 8. After PR is accepted by CI and one of the reviewers, merge it via _"Rebase and merge"_ option 32 | 33 | Further actions are automated in CI context: 34 | 35 | 8. `master` CI build detects that release PR was merged (by fact that it covers change of `version` field in `package.json` file). Having that (after successufl tests pass) version tag is created and pushed to the repository. 36 | 9. _tag_ CI build, publishes new version to npm, also it retrieves release notes from CHANGELOG.md and publishes them to GitHub. 37 | 38 | ### Updating release notes for already published versions 39 | 40 | Improvements to release notes can be done at anytime to any already published version: 41 | 42 | 1. Update `CHANGELOG.md` with desired changes (ensure they'd also end in `master`) 43 | 2. Push updated release notes to GitHub by running: 44 | `npx github-release-from-cc-changelog ` 45 | -------------------------------------------------------------------------------- /shared/setDeploymentBucketName.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const setDeploymentBucketName = require('./setDeploymentBucketName'); 7 | const GoogleProvider = require('../provider/googleProvider'); 8 | const Serverless = require('../test/serverless'); 9 | const GoogleCommand = require('../test/googleCommand'); 10 | 11 | describe('SetDeploymentBucketName', () => { 12 | let serverless; 13 | let googleCommand; 14 | let requestStub; 15 | 16 | beforeEach(() => { 17 | serverless = new Serverless(); 18 | serverless.service = { 19 | service: 'my-service', 20 | }; 21 | serverless.service.provider = { 22 | project: 'my-project', 23 | }; 24 | const options = { 25 | stage: 'dev', 26 | region: 'us-central1', 27 | }; 28 | serverless.setProvider('google', new GoogleProvider(serverless)); 29 | googleCommand = new GoogleCommand(serverless, options, setDeploymentBucketName); 30 | requestStub = sinon.stub(googleCommand.provider, 'request'); 31 | }); 32 | 33 | afterEach(() => { 34 | googleCommand.provider.request.restore(); 35 | }); 36 | 37 | describe('#setDeploymentBucketName()', () => { 38 | it('should set the name if the deployment request errors out', () => { 39 | requestStub.returns(BbPromise.reject()); 40 | 41 | return googleCommand.setDeploymentBucketName().then(() => { 42 | expect(serverless.service.provider.deploymentBucketName).toMatch(/sls-my-service-dev-.+/); 43 | expect( 44 | requestStub.calledWithExactly('deploymentmanager', 'resources', 'list', { 45 | project: 'my-project', 46 | deployment: 'sls-my-service-dev', 47 | }) 48 | ).toEqual(true); 49 | }); 50 | }); 51 | 52 | it('should set the name if a deployment is not present', () => { 53 | const response = {}; 54 | requestStub.returns(BbPromise.resolve(response)); 55 | 56 | return googleCommand.setDeploymentBucketName().then(() => { 57 | expect(serverless.service.provider.deploymentBucketName).toMatch(/sls-my-service-dev-.+/); 58 | expect( 59 | requestStub.calledWithExactly('deploymentmanager', 'resources', 'list', { 60 | project: 'my-project', 61 | deployment: 'sls-my-service-dev', 62 | }) 63 | ).toEqual(true); 64 | }); 65 | }); 66 | 67 | it('should set the bucket name with the one of the deployment if exists', () => { 68 | const response = { 69 | resources: [ 70 | { type: 'some-other-type', name: 'some-other-resource' }, 71 | { type: 'storage.v1.bucket', name: 'some-bucket' }, 72 | { type: 'storage.v1.bucket', name: 'sls-my-service-dev-12345678' }, 73 | ], 74 | }; 75 | requestStub.returns(BbPromise.resolve(response)); 76 | 77 | return googleCommand.setDeploymentBucketName().then(() => { 78 | expect(serverless.service.provider.deploymentBucketName).toEqual( 79 | 'sls-my-service-dev-12345678' 80 | ); 81 | expect( 82 | requestStub.calledWithExactly('deploymentmanager', 'resources', 'list', { 83 | project: 'my-project', 84 | deployment: 'sls-my-service-dev', 85 | }) 86 | ).toEqual(true); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /info/lib/displayServiceInfo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const chalk = require('chalk'); 6 | const BbPromise = require('bluebird'); 7 | const _ = require('lodash'); 8 | 9 | module.exports = { 10 | displayServiceInfo() { 11 | return BbPromise.bind(this).then(this.getResources).then(this.gatherData).then(this.printInfo); 12 | }, 13 | 14 | getResources() { 15 | const project = this.serverless.service.provider.project; 16 | 17 | return this.provider.request('deploymentmanager', 'resources', 'list', { 18 | project, 19 | deployment: `sls-${this.serverless.service.service}-${this.options.stage}`, 20 | }); 21 | }, 22 | 23 | gatherData(resources) { 24 | const data = {}; 25 | 26 | // general data 27 | data.service = this.serverless.service.service; 28 | data.project = this.serverless.service.provider.project; 29 | data.stage = this.options.stage; 30 | data.region = this.options.region; 31 | 32 | data.resources = { 33 | functions: [], 34 | }; 35 | 36 | _.forEach(resources.resources, (resource) => { 37 | if (resource.type === 'gcp-types/cloudfunctions-v1:projects.locations.functions') { 38 | const serviceFuncName = getFunctionNameInService( 39 | resource.name, 40 | this.serverless.service.service, 41 | this.options.stage 42 | ); 43 | const serviceFunc = this.serverless.service.getFunction(serviceFuncName); 44 | const eventType = Object.keys(serviceFunc.events[0])[0]; 45 | const funcEventConfig = serviceFunc.events[0][eventType]; 46 | 47 | let funcResource = funcEventConfig.resource || null; 48 | 49 | if (eventType === 'http') { 50 | const region = this.options.region; 51 | const project = this.serverless.service.provider.project; 52 | const baseUrl = `https://${region}-${project}.cloudfunctions.net`; 53 | const path = serviceFunc.name; // NOTE this might change 54 | funcResource = `${baseUrl}/${path}`; 55 | } 56 | 57 | const func = { 58 | name: serviceFuncName, 59 | resource: funcResource, // how the function can be triggered (e.g. url, pubSub, etc.) 60 | }; 61 | data.resources.functions.push(func); 62 | } 63 | }); 64 | 65 | return BbPromise.resolve(data); 66 | }, 67 | 68 | printInfo(data) { 69 | let message = ''; 70 | 71 | // get all the service related information 72 | message += `${chalk.yellow.underline('Service Information')}\n`; 73 | message += `${chalk.yellow('service:')} ${data.service}\n`; 74 | message += `${chalk.yellow('project:')} ${data.project}\n`; 75 | message += `${chalk.yellow('stage:')} ${data.stage}\n`; 76 | message += `${chalk.yellow('region:')} ${data.region}\n`; 77 | 78 | message += '\n'; 79 | 80 | // get all the functions 81 | message += `${chalk.yellow.underline('Deployed functions')}\n`; 82 | if (data.resources.functions.length) { 83 | data.resources.functions.forEach((func) => { 84 | message += `${chalk.yellow(func.name)}\n`; 85 | message += ` ${func.resource}\n`; 86 | }); 87 | } else { 88 | message += 'There are no functions deployed yet\n'; 89 | } 90 | 91 | this.serverless.cli.consoleLog(message); 92 | 93 | return BbPromise.resolve(); 94 | }, 95 | }; 96 | 97 | const getFunctionNameInService = (funcName, service, stage) => { 98 | let funcNameInService = funcName; 99 | funcNameInService = funcNameInService.replace(`${service}-`, ''); 100 | funcNameInService = funcNameInService.replace(`${stage}-`, ''); 101 | return funcNameInService; 102 | }; 103 | -------------------------------------------------------------------------------- /deploy/lib/uploadArtifacts.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const sinon = require('sinon'); 6 | const BbPromise = require('bluebird'); 7 | 8 | const GoogleProvider = require('../../provider/googleProvider'); 9 | const GoogleDeploy = require('../googleDeploy'); 10 | const Serverless = require('../../test/serverless'); 11 | 12 | describe('UploadArtifacts', () => { 13 | let serverless; 14 | let googleDeploy; 15 | let consoleLogStub; 16 | let requestStub; 17 | let createReadStreamStub; 18 | 19 | beforeEach(() => { 20 | serverless = new Serverless(); 21 | serverless.service = { 22 | service: 'my-service', 23 | provider: { 24 | deploymentBucketName: 'sls-my-service-dev-12345678', 25 | }, 26 | package: { 27 | artifactFilePath: '/some-file-path', 28 | artifact: 'artifact.zip', 29 | }, 30 | }; 31 | serverless.setProvider('google', new GoogleProvider(serverless)); 32 | const options = { 33 | stage: 'dev', 34 | region: 'us-central1', 35 | }; 36 | googleDeploy = new GoogleDeploy(serverless, options); 37 | consoleLogStub = sinon.stub(googleDeploy.serverless.cli, 'log').returns(); 38 | requestStub = sinon.stub(googleDeploy.provider, 'request').returns(BbPromise.resolve()); 39 | createReadStreamStub = sinon.stub(fs, 'createReadStream').returns(); 40 | }); 41 | 42 | afterEach(() => { 43 | googleDeploy.serverless.cli.log.restore(); 44 | googleDeploy.provider.request.restore(); 45 | fs.createReadStream.restore(); 46 | }); 47 | 48 | describe('#uploadArtifacts()', () => { 49 | it('should upload corresponding objects to deployment bucket', () => 50 | googleDeploy.uploadArtifacts().then(() => { 51 | expect( 52 | requestStub.calledWithExactly('storage', 'objects', 'insert', { 53 | bucket: 'sls-my-service-dev-12345678', 54 | resource: { 55 | name: '/some-file-path', 56 | contentType: 'application/octet-stream', 57 | }, 58 | media: { 59 | mimeType: 'application/octet-stream', 60 | body: fs.createReadStream('artifact.zip'), 61 | }, 62 | }) 63 | ).toEqual(true); 64 | })); 65 | 66 | it('should log info messages', () => 67 | googleDeploy.uploadArtifacts().then(() => { 68 | expect(consoleLogStub.called).toEqual(true); 69 | expect( 70 | requestStub.calledWithExactly('storage', 'objects', 'insert', { 71 | bucket: 'sls-my-service-dev-12345678', 72 | resource: { 73 | name: '/some-file-path', 74 | contentType: 'application/octet-stream', 75 | }, 76 | media: { 77 | mimeType: 'application/octet-stream', 78 | body: fs.createReadStream('artifact.zip'), 79 | }, 80 | }) 81 | ).toEqual(true); 82 | })); 83 | 84 | it('should read artifact file as read stream', () => 85 | googleDeploy.uploadArtifacts().then(() => { 86 | expect(createReadStreamStub.calledOnce).toEqual(true); 87 | expect( 88 | requestStub.calledWithExactly('storage', 'objects', 'insert', { 89 | bucket: 'sls-my-service-dev-12345678', 90 | resource: { 91 | name: '/some-file-path', 92 | contentType: 'application/octet-stream', 93 | }, 94 | media: { 95 | mimeType: 'application/octet-stream', 96 | body: fs.createReadStream('artifact.zip'), 97 | }, 98 | }) 99 | ).toEqual(true); 100 | })); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /deploy/googleDeploy.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../provider/googleProvider'); 7 | const GoogleDeploy = require('./googleDeploy'); 8 | const Serverless = require('../test/serverless'); 9 | 10 | describe('GoogleDeploy', () => { 11 | let serverless; 12 | let options; 13 | let googleDeploy; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | options = { 18 | stage: 'my-stage', 19 | region: 'my-region', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | googleDeploy = new GoogleDeploy(serverless, options); 23 | }); 24 | 25 | describe('#constructor()', () => { 26 | it('should set the serverless instance', () => { 27 | expect(googleDeploy.serverless).toEqual(serverless); 28 | }); 29 | 30 | it('should set options if provided', () => { 31 | expect(googleDeploy.options).toEqual(options); 32 | }); 33 | 34 | it('should make the provider accessible', () => { 35 | expect(googleDeploy.provider).toBeInstanceOf(GoogleProvider); 36 | }); 37 | 38 | describe('hooks', () => { 39 | let validateStub; 40 | let setDefaultsStub; 41 | let createDeploymentStub; 42 | let setDeploymentBucketNameStub; 43 | let uploadArtifactsStub; 44 | let updateDeploymentStub; 45 | let cleanupDeploymentBucketStub; 46 | 47 | beforeEach(() => { 48 | validateStub = sinon.stub(googleDeploy, 'validate').returns(BbPromise.resolve()); 49 | setDefaultsStub = sinon.stub(googleDeploy, 'setDefaults').returns(BbPromise.resolve()); 50 | createDeploymentStub = sinon 51 | .stub(googleDeploy, 'createDeployment') 52 | .returns(BbPromise.resolve()); 53 | setDeploymentBucketNameStub = sinon 54 | .stub(googleDeploy, 'setDeploymentBucketName') 55 | .returns(BbPromise.resolve()); 56 | uploadArtifactsStub = sinon 57 | .stub(googleDeploy, 'uploadArtifacts') 58 | .returns(BbPromise.resolve()); 59 | updateDeploymentStub = sinon 60 | .stub(googleDeploy, 'updateDeployment') 61 | .returns(BbPromise.resolve()); 62 | cleanupDeploymentBucketStub = sinon 63 | .stub(googleDeploy, 'cleanupDeploymentBucket') 64 | .returns(BbPromise.resolve()); 65 | }); 66 | 67 | afterEach(() => { 68 | googleDeploy.validate.restore(); 69 | googleDeploy.setDefaults.restore(); 70 | googleDeploy.createDeployment.restore(); 71 | googleDeploy.setDeploymentBucketName.restore(); 72 | googleDeploy.uploadArtifacts.restore(); 73 | googleDeploy.updateDeployment.restore(); 74 | googleDeploy.cleanupDeploymentBucket.restore(); 75 | }); 76 | 77 | it('should run "before:deploy:deploy" promise chain', () => 78 | googleDeploy.hooks['before:deploy:deploy']().then(() => { 79 | expect(validateStub.calledOnce).toEqual(true); 80 | expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); 81 | })); 82 | 83 | it('should run "deploy:deploy" promise chain', () => 84 | googleDeploy.hooks['deploy:deploy']().then(() => { 85 | expect(createDeploymentStub.calledOnce).toEqual(true); 86 | expect(setDeploymentBucketNameStub.calledAfter(createDeploymentStub)).toEqual(true); 87 | expect(uploadArtifactsStub.calledAfter(createDeploymentStub)).toEqual(true); 88 | expect(updateDeploymentStub.calledAfter(uploadArtifactsStub)).toEqual(true); 89 | })); 90 | 91 | it('should run "after:deploy:deploy" promise chain', () => 92 | googleDeploy.hooks['after:deploy:deploy']().then(() => { 93 | expect(cleanupDeploymentBucketStub.calledOnce).toEqual(true); 94 | })); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /invoke/lib/invokeFunction.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | const chalk = require('chalk'); 6 | 7 | const GoogleProvider = require('../../provider/googleProvider'); 8 | const GoogleInvoke = require('../googleInvoke'); 9 | const Serverless = require('../../test/serverless'); 10 | 11 | describe('InvokeFunction', () => { 12 | let serverless; 13 | let googleInvoke; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | serverless.service = { 18 | service: 'my-service', 19 | }; 20 | serverless.service.provider = { 21 | project: 'my-project', 22 | }; 23 | serverless.service.functions = { 24 | func1: { 25 | handler: 'foo', 26 | name: 'full-function-name', 27 | }, 28 | }; 29 | serverless.setProvider('google', new GoogleProvider(serverless)); 30 | const options = { 31 | stage: 'dev', 32 | region: 'us-central1', 33 | }; 34 | googleInvoke = new GoogleInvoke(serverless, options); 35 | }); 36 | 37 | describe('#invokeFunction()', () => { 38 | let invokeStub; 39 | let printResultStub; 40 | 41 | beforeEach(() => { 42 | invokeStub = sinon.stub(googleInvoke, 'invoke').returns(BbPromise.resolve()); 43 | printResultStub = sinon.stub(googleInvoke, 'printResult').returns(BbPromise.resolve()); 44 | }); 45 | 46 | afterEach(() => { 47 | googleInvoke.invoke.restore(); 48 | googleInvoke.printResult.restore(); 49 | }); 50 | 51 | it('should run promise chain', () => 52 | googleInvoke.invokeFunction().then(() => { 53 | expect(invokeStub.calledOnce).toEqual(true); 54 | expect(printResultStub.calledAfter(invokeStub)); 55 | })); 56 | }); 57 | 58 | describe('#invoke()', () => { 59 | let requestStub; 60 | 61 | beforeEach(() => { 62 | requestStub = sinon.stub(googleInvoke.provider, 'request').returns(BbPromise.resolve()); 63 | }); 64 | 65 | afterEach(() => { 66 | googleInvoke.provider.request.restore(); 67 | }); 68 | 69 | it('should invoke the provided function without data option', () => { 70 | googleInvoke.options.function = 'func1'; 71 | 72 | return googleInvoke.invoke().then(() => { 73 | expect( 74 | requestStub.calledWithExactly( 75 | 'cloudfunctions', 76 | 'projects', 77 | 'locations', 78 | 'functions', 79 | 'call', 80 | { 81 | name: 'projects/my-project/locations/us-central1/functions/full-function-name', 82 | resource: { 83 | data: '', 84 | }, 85 | } 86 | ) 87 | ).toEqual(true); 88 | }); 89 | }); 90 | 91 | it('should invoke the provided function with the data option', () => { 92 | googleInvoke.options.function = 'func1'; 93 | googleInvoke.options.data = '{ "some": "json" }'; 94 | 95 | return googleInvoke.invoke().then(() => { 96 | expect( 97 | requestStub.calledWithExactly( 98 | 'cloudfunctions', 99 | 'projects', 100 | 'locations', 101 | 'functions', 102 | 'call', 103 | { 104 | name: 'projects/my-project/locations/us-central1/functions/full-function-name', 105 | resource: { 106 | data: googleInvoke.options.data, 107 | }, 108 | } 109 | ) 110 | ).toEqual(true); 111 | }); 112 | }); 113 | 114 | it('should throw an error if the function could not be found in the service', () => { 115 | googleInvoke.options.function = 'missingFunc'; 116 | 117 | expect(() => googleInvoke.invoke()).toThrow(Error); 118 | }); 119 | }); 120 | 121 | describe('#printResult()', () => { 122 | let consoleLogStub; 123 | 124 | beforeEach(() => { 125 | consoleLogStub = sinon.stub(googleInvoke.serverless.cli, 'log').returns(); 126 | }); 127 | 128 | afterEach(() => { 129 | googleInvoke.serverless.cli.log.restore(); 130 | }); 131 | 132 | it('should print the received execution result on the console', () => { 133 | const result = { 134 | executionId: 'wasdqwerty', 135 | result: 'Foo bar', 136 | }; 137 | 138 | const expectedOutput = `${chalk.grey('wasdqwerty')} Foo bar`; 139 | 140 | return googleInvoke.printResult(result).then(() => { 141 | expect(consoleLogStub.calledWithExactly(expectedOutput)).toEqual(true); 142 | }); 143 | }); 144 | 145 | it('should print an error message to the console when no result was received', () => { 146 | const result = {}; 147 | 148 | const expectedOutput = `${chalk.grey( 149 | 'error' 150 | )} An error occurred while executing your function...`; 151 | 152 | return googleInvoke.printResult(result).then(() => { 153 | expect(consoleLogStub.calledWithExactly(expectedOutput)).toEqual(true); 154 | }); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /invokeLocal/lib/nodeJs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const path = require('path'); 5 | const _ = require('lodash'); 6 | const { getReqRes } = require('./httpReqRes'); 7 | 8 | const tryToRequirePaths = (paths) => { 9 | let loaded; 10 | paths.forEach((pathToLoad) => { 11 | if (loaded) { 12 | return; 13 | } 14 | try { 15 | loaded = require(pathToLoad); 16 | } catch (e) { 17 | // pass 18 | } 19 | }); 20 | return loaded; 21 | }; 22 | 23 | const jsonContentType = 'application/json'; 24 | 25 | module.exports = { 26 | async invokeLocalNodeJs(functionObj, event, customContext) { 27 | // index.js and function.js are the two files supported by default by a cloud-function 28 | // TODO add the file pointed by the main key of the package.json 29 | const paths = ['index.js', 'function.js'].map((fileName) => 30 | path.join(this.serverless.serviceDir, fileName) 31 | ); 32 | 33 | const handlerContainer = tryToRequirePaths(paths); 34 | if (!handlerContainer) { 35 | throw new Error(`Failed to require one of the files ${paths.join(', ')}`); 36 | } 37 | 38 | const cloudFunction = handlerContainer[functionObj.handler]; 39 | if (!cloudFunction) { 40 | throw new Error(`Failed to load function "${functionObj.handler}" from the loaded file`); 41 | } 42 | 43 | this.addEnvironmentVariablesToProcessEnv(functionObj); 44 | 45 | const eventType = Object.keys(functionObj.events[0])[0]; 46 | 47 | switch (eventType) { 48 | case 'event': 49 | return this.handleEvent(cloudFunction, event, customContext); 50 | case 'http': 51 | return this.handleHttp(cloudFunction, event, customContext); 52 | default: 53 | throw new Error(`${eventType} is not supported`); 54 | } 55 | }, 56 | handleError(err, resolve) { 57 | let errorResult; 58 | if (err instanceof Error) { 59 | errorResult = { 60 | errorMessage: err.message, 61 | errorType: err.constructor.name, 62 | stackTrace: err.stack && err.stack.split('\n'), 63 | }; 64 | } else { 65 | errorResult = { 66 | errorMessage: err, 67 | }; 68 | } 69 | 70 | this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4))); 71 | resolve(); 72 | process.exitCode = 1; 73 | }, 74 | handleEvent(cloudFunction, event, customContext) { 75 | let hasResponded = false; 76 | 77 | function handleResult(result) { 78 | if (result instanceof Error) { 79 | this.handleError.call(this, result); 80 | return; 81 | } 82 | this.serverless.cli.consoleLog(JSON.stringify(result, null, 4)); 83 | } 84 | 85 | return new Promise((resolve) => { 86 | const callback = (err, result) => { 87 | if (!hasResponded) { 88 | hasResponded = true; 89 | if (err) { 90 | this.handleError(err, resolve); 91 | } else if (result) { 92 | handleResult.call(this, result); 93 | } 94 | resolve(); 95 | } 96 | }; 97 | 98 | let context = {}; 99 | 100 | if (customContext) { 101 | context = customContext; 102 | } 103 | try { 104 | const maybeThennable = cloudFunction(event, context, callback); 105 | if (maybeThennable) { 106 | Promise.resolve(maybeThennable).then(callback.bind(this, null), callback.bind(this)); 107 | } 108 | } catch (error) { 109 | this.handleError(error, resolve); 110 | } 111 | }); 112 | }, 113 | handleHttp(cloudFunction, event) { 114 | const { expressRequest, expressResponse: response } = getReqRes(); 115 | const request = Object.assign(expressRequest, event); 116 | 117 | return new Promise((resolve) => { 118 | const endCallback = (data) => { 119 | if (data && Buffer.isBuffer(data)) { 120 | data = data.toString(); 121 | } 122 | const headers = response.getHeaders(); 123 | const bodyIsJson = 124 | headers['content-type'] && headers['content-type'].includes(jsonContentType); 125 | if (data && bodyIsJson) { 126 | data = JSON.parse(data); 127 | } 128 | this.serverless.cli.consoleLog( 129 | JSON.stringify( 130 | { 131 | status: response.statusCode, 132 | headers, 133 | body: data, 134 | }, 135 | null, 136 | 4 137 | ) 138 | ); 139 | resolve(); 140 | }; 141 | 142 | Object.assign(response, { end: endCallback }); // Override of the end method which is always called to send the response of the http request 143 | 144 | try { 145 | const maybeThennable = cloudFunction(request, response); 146 | if (maybeThennable) { 147 | Promise.resolve(maybeThennable).catch((error) => this.handleError(error, resolve)); 148 | } 149 | } catch (error) { 150 | this.handleError(error, resolve); 151 | } 152 | }); 153 | }, 154 | 155 | addEnvironmentVariablesToProcessEnv(functionObj) { 156 | const environmentVariables = this.provider.getConfiguredEnvironment(functionObj); 157 | _.merge(process.env, environmentVariables); 158 | }, 159 | }; 160 | -------------------------------------------------------------------------------- /deploy/lib/updateDeployment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const sinon = require('sinon'); 7 | const BbPromise = require('bluebird'); 8 | 9 | const GoogleProvider = require('../../provider/googleProvider'); 10 | const GoogleDeploy = require('../googleDeploy'); 11 | const Serverless = require('../../test/serverless'); 12 | 13 | describe('UpdateDeployment', () => { 14 | let serverless; 15 | let googleDeploy; 16 | let requestStub; 17 | let configurationTemplateUpdateFilePath; 18 | 19 | beforeEach(() => { 20 | serverless = new Serverless(); 21 | serverless.service.service = 'my-service'; 22 | serverless.service.provider = { 23 | project: 'my-project', 24 | }; 25 | serverless.config = { 26 | servicePath: 'tmp', 27 | }; 28 | serverless.setProvider('google', new GoogleProvider(serverless)); 29 | const options = { 30 | stage: 'dev', 31 | region: 'us-central1', 32 | }; 33 | googleDeploy = new GoogleDeploy(serverless, options); 34 | requestStub = sinon.stub(googleDeploy.provider, 'request'); 35 | configurationTemplateUpdateFilePath = path.join( 36 | serverless.config.servicePath, 37 | '.serverless', 38 | 'configuration-template-update.yml' 39 | ); 40 | }); 41 | 42 | afterEach(() => { 43 | googleDeploy.provider.request.restore(); 44 | }); 45 | 46 | describe('#updateDeployment()', () => { 47 | let getDeploymentStub; 48 | let updateStub; 49 | 50 | beforeEach(() => { 51 | getDeploymentStub = sinon.stub(googleDeploy, 'getDeployment').returns(BbPromise.resolve()); 52 | updateStub = sinon.stub(googleDeploy, 'update').returns(BbPromise.resolve()); 53 | }); 54 | 55 | afterEach(() => { 56 | googleDeploy.getDeployment.restore(); 57 | googleDeploy.update.restore(); 58 | }); 59 | 60 | it('should run promise chain', () => 61 | googleDeploy.updateDeployment().then(() => { 62 | expect(getDeploymentStub.calledOnce).toEqual(true); 63 | expect(updateStub.calledAfter(getDeploymentStub)); 64 | })); 65 | }); 66 | 67 | describe('#getDeployment()', () => { 68 | it('should return undefined if no deployments are found', () => { 69 | const response = { 70 | deployments: [{ name: 'some-other-deployment' }], 71 | }; 72 | requestStub.returns(BbPromise.resolve(response)); 73 | 74 | return googleDeploy.getDeployment().then((foundDeployment) => { 75 | expect(foundDeployment).toEqual(undefined); 76 | expect( 77 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 78 | project: 'my-project', 79 | }) 80 | ).toEqual(true); 81 | }); 82 | }); 83 | 84 | it('should return the deployment if found', () => { 85 | const response = { 86 | deployments: [{ name: 'sls-my-service-dev' }, { name: 'some-other-deployment' }], 87 | }; 88 | requestStub.returns(BbPromise.resolve(response)); 89 | 90 | return googleDeploy.getDeployment().then((foundDeployment) => { 91 | expect(foundDeployment).toEqual(response.deployments[0]); 92 | expect( 93 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 94 | project: 'my-project', 95 | }) 96 | ).toEqual(true); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('#update()', () => { 102 | let consoleLogStub; 103 | let readFileSyncStub; 104 | let monitorDeploymentStub; 105 | 106 | beforeEach(() => { 107 | consoleLogStub = sinon.stub(googleDeploy.serverless.cli, 'log').returns(); 108 | readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('some content'); 109 | monitorDeploymentStub = sinon 110 | .stub(googleDeploy, 'monitorDeployment') 111 | .returns(BbPromise.resolve()); 112 | }); 113 | 114 | afterEach(() => { 115 | googleDeploy.serverless.cli.log.restore(); 116 | fs.readFileSync.restore(); 117 | googleDeploy.monitorDeployment.restore(); 118 | }); 119 | 120 | it('should update and hand over to monitor the deployment if it exists', () => { 121 | const deployment = { 122 | name: 'sls-my-service-dev', 123 | fingerprint: '12345678', 124 | }; 125 | const params = { 126 | project: 'my-project', 127 | deployment: 'sls-my-service-dev', 128 | resource: { 129 | name: 'sls-my-service-dev', 130 | fingerprint: deployment.fingerprint, 131 | target: { 132 | config: { 133 | content: fs.readFileSync(configurationTemplateUpdateFilePath).toString(), 134 | }, 135 | }, 136 | }, 137 | }; 138 | requestStub.returns(BbPromise.resolve()); 139 | 140 | return googleDeploy.update(deployment).then(() => { 141 | expect(consoleLogStub.calledOnce).toEqual(true); 142 | expect(readFileSyncStub.called).toEqual(true); 143 | expect( 144 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'update', params) 145 | ).toEqual(true); 146 | expect( 147 | monitorDeploymentStub.calledWithExactly('sls-my-service-dev', 'update', 5000) 148 | ).toEqual(true); 149 | }); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /remove/lib/emptyDeploymentBucket.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../../provider/googleProvider'); 7 | const GoogleRemove = require('../googleRemove'); 8 | const Serverless = require('../../test/serverless'); 9 | 10 | describe('EmptyDeploymentBucket', () => { 11 | let serverless; 12 | let googleRemove; 13 | let key; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | serverless.service = { 18 | service: 'my-service', 19 | provider: { 20 | deploymentBucketName: 'sls-my-service-dev-12345678', 21 | }, 22 | }; 23 | serverless.setProvider('google', new GoogleProvider(serverless)); 24 | const options = { 25 | stage: 'dev', 26 | region: 'us-central1', 27 | }; 28 | googleRemove = new GoogleRemove(serverless, options); 29 | key = `serverless/${serverless.service.service}/${options.stage}`; 30 | }); 31 | 32 | describe('#emptyDeploymentBucket()', () => { 33 | let getObjectsToRemoveStub; 34 | let removeObjectsStub; 35 | 36 | beforeEach(() => { 37 | getObjectsToRemoveStub = sinon 38 | .stub(googleRemove, 'getObjectsToRemove') 39 | .returns(BbPromise.resolve()); 40 | removeObjectsStub = sinon.stub(googleRemove, 'removeObjects').returns(BbPromise.resolve()); 41 | }); 42 | 43 | afterEach(() => { 44 | googleRemove.getObjectsToRemove.restore(); 45 | googleRemove.removeObjects.restore(); 46 | }); 47 | 48 | it('should run promise chain', () => 49 | googleRemove.emptyDeploymentBucket().then(() => { 50 | expect(getObjectsToRemoveStub.calledOnce).toEqual(true); 51 | expect(removeObjectsStub.calledAfter(getObjectsToRemoveStub)); 52 | })); 53 | }); 54 | 55 | describe('#getObjectsToRemove()', () => { 56 | let requestStub; 57 | 58 | beforeEach(() => { 59 | requestStub = sinon.stub(googleRemove.provider, 'request'); 60 | }); 61 | 62 | afterEach(() => { 63 | googleRemove.provider.request.restore(); 64 | }); 65 | 66 | it('should resolve if there are no objects in the deployment bucket', () => { 67 | const response = { 68 | items: [], 69 | }; 70 | requestStub.returns(BbPromise.resolve(response)); 71 | 72 | return googleRemove.getObjectsToRemove().then((objects) => { 73 | expect(objects.length).toEqual(0); 74 | expect(objects).toEqual([]); 75 | expect( 76 | requestStub.calledWithExactly('storage', 'objects', 'list', { 77 | bucket: 'sls-my-service-dev-12345678', 78 | }) 79 | ).toEqual(true); 80 | }); 81 | }); 82 | 83 | it('should return all the objects in the deployment bucket', () => { 84 | const response = { 85 | items: [ 86 | { 87 | bucket: 'sls-my-service-dev-12345678', 88 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 89 | }, 90 | { 91 | bucket: 'sls-my-service-dev-12345678', 92 | name: `${key}/141264711231-2016-08-18T15:43:00/artifact.zip`, 93 | }, 94 | ], 95 | }; 96 | requestStub.returns(BbPromise.resolve(response)); 97 | 98 | return googleRemove.getObjectsToRemove().then((objects) => { 99 | expect(objects.length).toEqual(2); 100 | expect(objects).toContainEqual({ 101 | bucket: 'sls-my-service-dev-12345678', 102 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 103 | }); 104 | expect(objects).toContainEqual({ 105 | bucket: 'sls-my-service-dev-12345678', 106 | name: `${key}/141264711231-2016-08-18T15:43:00/artifact.zip`, 107 | }); 108 | expect( 109 | requestStub.calledWithExactly('storage', 'objects', 'list', { 110 | bucket: 'sls-my-service-dev-12345678', 111 | }) 112 | ).toEqual(true); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('#removeObjects()', () => { 118 | let requestStub; 119 | let consoleLogStub; 120 | 121 | beforeEach(() => { 122 | requestStub = sinon.stub(googleRemove.provider, 'request'); 123 | consoleLogStub = sinon.stub(googleRemove.serverless.cli, 'log').returns(); 124 | }); 125 | 126 | afterEach(() => { 127 | googleRemove.provider.request.restore(); 128 | googleRemove.serverless.cli.log.restore(); 129 | }); 130 | 131 | it('should resolve if no objects should be removed', () => { 132 | const objectsToRemove = []; 133 | 134 | return googleRemove.removeObjects(objectsToRemove).then(() => { 135 | expect(requestStub.calledOnce).toEqual(false); 136 | expect(consoleLogStub.calledOnce).toEqual(false); 137 | }); 138 | }); 139 | 140 | it('should remove all given objects', () => { 141 | const objectsToRemove = [ 142 | { 143 | bucket: 'sls-my-service-dev-12345678', 144 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 145 | }, 146 | { 147 | bucket: 'sls-my-service-dev-12345678', 148 | name: `${key}/141264711231-2016-08-18T15:43:00/artifact.zip`, 149 | }, 150 | ]; 151 | 152 | requestStub.returns(BbPromise.resolve('removePromise')); 153 | 154 | return googleRemove.removeObjects(objectsToRemove).then((removePromises) => { 155 | expect(requestStub.called).toEqual(true); 156 | expect(consoleLogStub.calledOnce).toEqual(true); 157 | expect(removePromises).toEqual(['removePromise', 'removePromise']); 158 | }); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /shared/monitorDeployment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const monitorDeployment = require('./monitorDeployment'); 7 | const GoogleProvider = require('../provider/googleProvider'); 8 | const Serverless = require('../test/serverless'); 9 | const GoogleCommand = require('../test/googleCommand'); 10 | 11 | describe('MonitorDeployment', () => { 12 | let serverless; 13 | let googleCommand; 14 | let consoleLogStub; 15 | let requestStub; 16 | 17 | beforeEach(() => { 18 | serverless = new Serverless(); 19 | serverless.service = { 20 | service: 'my-service', 21 | }; 22 | serverless.service.provider = { 23 | project: 'my-project', 24 | }; 25 | const options = { 26 | stage: 'dev', 27 | region: 'us-central1', 28 | }; 29 | serverless.setProvider('google', new GoogleProvider(serverless)); 30 | googleCommand = new GoogleCommand(serverless, options, monitorDeployment); 31 | consoleLogStub = sinon.stub(googleCommand.serverless.cli, 'log').returns(); 32 | requestStub = sinon.stub(googleCommand.provider, 'request'); 33 | }); 34 | 35 | afterEach(() => { 36 | googleCommand.serverless.cli.log.restore(); 37 | googleCommand.provider.request.restore(); 38 | }); 39 | 40 | describe('#monitorDeployment()', () => { 41 | describe('when monitoring creations or updates', () => { 42 | it('should keep monitoring until "DONE" status is reached', () => { 43 | const deploymentName = 'sls-my-service-dev'; 44 | const response1 = { 45 | deployments: [ 46 | { 47 | name: deploymentName, 48 | operation: { 49 | status: 'RUNNING', 50 | }, 51 | }, 52 | ], 53 | }; 54 | const response2 = { 55 | deployments: [ 56 | { 57 | name: deploymentName, 58 | operation: { 59 | status: 'DONE', 60 | }, 61 | }, 62 | ], 63 | }; 64 | 65 | requestStub.onCall(0).returns(BbPromise.resolve(response1)); 66 | requestStub.onCall(1).returns(BbPromise.resolve(response2)); 67 | 68 | return googleCommand 69 | .monitorDeployment(deploymentName, 'create', 10) 70 | .then((deploymentStatus) => { 71 | expect(consoleLogStub.called).toEqual(true); 72 | expect( 73 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 74 | project: 'my-project', 75 | }) 76 | ).toEqual(true); 77 | expect(deploymentStatus).toEqual('DONE'); 78 | }); 79 | }); 80 | 81 | it('should throw an error if deployment errors out', () => { 82 | const deploymentName = 'sls-my-service-dev'; 83 | const response = { 84 | deployments: [ 85 | { 86 | name: deploymentName, 87 | operation: { 88 | error: { 89 | errors: [ 90 | { 91 | code: 'ERROR', 92 | message: '{ "ResourceErrorMessage": { "details": [ "Error detail"] } }', 93 | }, 94 | ], 95 | }, 96 | }, 97 | }, 98 | ], 99 | }; 100 | 101 | requestStub.returns(BbPromise.resolve(response)); 102 | 103 | return googleCommand.monitorDeployment(deploymentName, 'update', 10).catch((error) => { 104 | expect(consoleLogStub.called).toEqual(true); 105 | expect( 106 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 107 | project: 'my-project', 108 | }) 109 | ).toEqual(true); 110 | expect(error.toString()).toMatch(/Error detail/); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('when monitoring removals', () => { 116 | it('should stop if there are no deployments and the action is "remove"', () => { 117 | const deploymentName = 'sls-my-service-dev'; 118 | const response = {}; 119 | 120 | requestStub.returns(BbPromise.resolve(response)); 121 | 122 | return googleCommand 123 | .monitorDeployment(deploymentName, 'remove', 10) 124 | .then((deploymentStatus) => { 125 | expect(consoleLogStub.called).toEqual(true); 126 | expect( 127 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 128 | project: 'my-project', 129 | }) 130 | ).toEqual(true); 131 | expect(deploymentStatus).toEqual('DONE'); 132 | }); 133 | }); 134 | 135 | it('should stop if the deployment is unavailable and the action is "remove"', () => { 136 | const deploymentName = 'sls-my-service-dev'; 137 | const response = { 138 | deployments: [{ name: 'a-different-deployment' }], 139 | }; 140 | 141 | requestStub.returns(BbPromise.resolve(response)); 142 | 143 | return googleCommand 144 | .monitorDeployment(deploymentName, 'remove', 10) 145 | .then((deploymentStatus) => { 146 | expect(consoleLogStub.called).toEqual(true); 147 | expect( 148 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 149 | project: 'my-project', 150 | }) 151 | ).toEqual(true); 152 | expect(deploymentStatus).toEqual('DONE'); 153 | }); 154 | }); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | # PR's only 2 | 3 | name: Validate 4 | 5 | on: 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | SLS_IGNORE_WARNING: '*' 11 | FORCE_COLOR: 1 12 | 13 | jobs: 14 | linuxNode14: 15 | name: '[Linux] Node.js 14: Lint, Formatting, Eventual Commitlint, Eventual Changelog, Unit tests' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # For commitlint purpose ensure to have complete list of PR commits 22 | # It's loose and imperfect assumption that PR has no more than 30 commits 23 | fetch-depth: 30 24 | 25 | - name: Retrieve last master commit (for `git diff` purposes) 26 | run: | 27 | git checkout -b pr 28 | git fetch --prune --depth=30 origin +refs/heads/master:refs/remotes/origin/master 29 | git checkout master 30 | git checkout pr 31 | 32 | - name: Retrieve dependencies from cache 33 | id: cacheNpm 34 | uses: actions/cache@v2 35 | with: 36 | path: | 37 | ~/.npm 38 | node_modules 39 | key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 40 | restore-keys: | 41 | npm-v14-${{ runner.os }}-${{ github.ref }}- 42 | npm-v14-${{ runner.os }}-refs/heads/master- 43 | 44 | - name: Install Node.js and npm 45 | uses: actions/setup-node@v1 46 | with: 47 | node-version: 14.x 48 | 49 | - name: Install dependencies 50 | if: steps.cacheNpm.outputs.cache-hit != 'true' 51 | run: | 52 | npm update --no-save 53 | npm update --save-dev --no-save 54 | - name: Validate formatting 55 | run: npm run prettier-check:updated 56 | - name: Validate lint rules 57 | run: npm run lint:updated 58 | - name: Validate commit messages 59 | if: github.event.pull_request.base.repo.id == github.event.pull_request.head.repo.id 60 | run: npx commitlint -f master 61 | - name: Validate changelog (if new version) 62 | run: | 63 | NEW_VERSION=`git diff -U0 master package.json | grep '"version": "' | tail -n 1 | grep -oE "[0-9]+\.[0-9]+\.[0-9]+"` || : 64 | if [ -n "$NEW_VERSION" ]; 65 | then 66 | npx dump-release-notes-from-cc-changelog $NEW_VERSION 67 | fi 68 | - name: Unit tests 69 | run: npm test 70 | 71 | linuxNode16: 72 | name: '[Linux] Node.js 16: Unit tests' 73 | runs-on: ubuntu-latest 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@v2 77 | 78 | - name: Retrieve dependencies from cache 79 | id: cacheNpm 80 | uses: actions/cache@v2 81 | with: 82 | path: | 83 | ~/.npm 84 | node_modules 85 | key: npm-v16-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 86 | restore-keys: | 87 | npm-v16-${{ runner.os }}-${{ github.ref }}- 88 | npm-v16-${{ runner.os }}-refs/heads/master- 89 | 90 | - name: Install Node.js and npm 91 | uses: actions/setup-node@v1 92 | with: 93 | node-version: 16.x 94 | 95 | - name: Install dependencies 96 | if: steps.cacheNpm.outputs.cache-hit != 'true' 97 | run: | 98 | npm update --no-save 99 | npm update --save-dev --no-save 100 | - name: Unit tests 101 | run: npm test 102 | 103 | linuxNode12: 104 | name: '[Linux] Node.js 12: Unit tests' 105 | runs-on: ubuntu-latest 106 | steps: 107 | - name: Checkout repository 108 | uses: actions/checkout@v2 109 | 110 | - name: Retrieve dependencies from cache 111 | id: cacheNpm 112 | uses: actions/cache@v2 113 | with: 114 | path: | 115 | ~/.npm 116 | node_modules 117 | key: npm-v12-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 118 | restore-keys: | 119 | npm-v12-${{ runner.os }}-${{ github.ref }}- 120 | npm-v12-${{ runner.os }}-refs/heads/master- 121 | 122 | - name: Install Node.js and npm 123 | uses: actions/setup-node@v1 124 | with: 125 | node-version: 12.x 126 | 127 | - name: Install dependencies 128 | if: steps.cacheNpm.outputs.cache-hit != 'true' 129 | run: | 130 | npm update --no-save 131 | npm update --save-dev --no-save 132 | - name: Unit tests 133 | run: npm test 134 | 135 | linuxNode10: 136 | name: '[Linux] Node.js v10: Unit tests' 137 | runs-on: ubuntu-latest 138 | steps: 139 | - name: Checkout repository 140 | uses: actions/checkout@v2 141 | 142 | - name: Retrieve dependencies from cache 143 | id: cacheNpm 144 | uses: actions/cache@v2 145 | with: 146 | path: | 147 | ~/.npm 148 | node_modules 149 | key: npm-v10-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 150 | restore-keys: | 151 | npm-v10-${{ runner.os }}-${{ github.ref }}- 152 | npm-v10-${{ runner.os }}-refs/heads/master- 153 | 154 | - name: Install Node.js and npm 155 | uses: actions/setup-node@v1 156 | with: 157 | node-version: 10.x 158 | 159 | - name: Install dependencies 160 | if: steps.cacheNpm.outputs.cache-hit != 'true' 161 | run: | 162 | npm update --no-save 163 | npm update --save-dev --no-save 164 | - name: Unit tests 165 | run: npm test 166 | -------------------------------------------------------------------------------- /logs/lib/retrieveLogs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | const chalk = require('chalk'); 6 | 7 | const GoogleProvider = require('../../provider/googleProvider'); 8 | const GoogleLogs = require('../googleLogs'); 9 | const Serverless = require('../../test/serverless'); 10 | 11 | describe('RetrieveLogs', () => { 12 | let serverless; 13 | let googleLogs; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | serverless.service = { 18 | service: 'my-service', 19 | }; 20 | serverless.service.provider = { 21 | project: 'my-project', 22 | }; 23 | serverless.service.functions = { 24 | func1: { 25 | handler: 'foo', 26 | name: 'full-function-name', 27 | }, 28 | }; 29 | serverless.setProvider('google', new GoogleProvider(serverless)); 30 | const options = { 31 | stage: 'dev', 32 | region: 'us-central1', 33 | }; 34 | googleLogs = new GoogleLogs(serverless, options); 35 | }); 36 | 37 | describe('#retrieveLogs()', () => { 38 | let getLogsStub; 39 | let printLogsStub; 40 | 41 | beforeEach(() => { 42 | getLogsStub = sinon.stub(googleLogs, 'getLogs').returns(BbPromise.resolve()); 43 | printLogsStub = sinon.stub(googleLogs, 'printLogs').returns(BbPromise.resolve()); 44 | }); 45 | 46 | afterEach(() => { 47 | googleLogs.getLogs.restore(); 48 | googleLogs.printLogs.restore(); 49 | }); 50 | 51 | it('should run promise chain', () => 52 | googleLogs.retrieveLogs().then(() => { 53 | expect(getLogsStub.calledOnce).toEqual(true); 54 | expect(printLogsStub.calledAfter(getLogsStub)); 55 | })); 56 | }); 57 | 58 | describe('#getLogs()', () => { 59 | let requestStub; 60 | 61 | beforeEach(() => { 62 | requestStub = sinon.stub(googleLogs.provider, 'request').returns(BbPromise.resolve()); 63 | }); 64 | 65 | afterEach(() => { 66 | googleLogs.provider.request.restore(); 67 | }); 68 | 69 | it('should return a default amount of logs for the function if the "count" option is not given', () => { 70 | googleLogs.options.function = 'func1'; 71 | 72 | return googleLogs.getLogs().then(() => { 73 | expect( 74 | requestStub.calledWithExactly('logging', 'entries', 'list', { 75 | filter: 'resource.labels.function_name="full-function-name" AND NOT textPayload=""', 76 | orderBy: 'timestamp desc', 77 | resourceNames: ['projects/my-project'], 78 | pageSize: 10, 79 | }) 80 | ).toEqual(true); 81 | }); 82 | }); 83 | 84 | it('should return logs of the function if the "count" option is given', () => { 85 | googleLogs.options.function = 'func1'; 86 | googleLogs.options.count = 100; 87 | 88 | return googleLogs.getLogs().then(() => { 89 | expect( 90 | requestStub.calledWithExactly('logging', 'entries', 'list', { 91 | filter: 'resource.labels.function_name="full-function-name" AND NOT textPayload=""', 92 | orderBy: 'timestamp desc', 93 | resourceNames: ['projects/my-project'], 94 | pageSize: googleLogs.options.count, 95 | }) 96 | ).toEqual(true); 97 | }); 98 | }); 99 | 100 | it('should parse the "count" option as an integer', () => { 101 | googleLogs.options.function = 'func1'; 102 | googleLogs.options.count = '100'; 103 | 104 | return googleLogs.getLogs().then(() => { 105 | expect( 106 | requestStub.calledWithExactly('logging', 'entries', 'list', { 107 | filter: 'resource.labels.function_name="full-function-name" AND NOT textPayload=""', 108 | orderBy: 'timestamp desc', 109 | resourceNames: ['projects/my-project'], 110 | pageSize: parseInt(googleLogs.options.count, 10), 111 | }) 112 | ).toEqual(true); 113 | }); 114 | }); 115 | 116 | it('should throw an error if the function could not be found in the service', () => { 117 | googleLogs.options.function = 'missingFunc'; 118 | 119 | expect(() => googleLogs.getLogs()).toThrow(Error); 120 | }); 121 | }); 122 | 123 | describe('#printLogs()', () => { 124 | let consoleLogStub; 125 | 126 | beforeEach(() => { 127 | consoleLogStub = sinon.stub(googleLogs.serverless.cli, 'log').returns(); 128 | }); 129 | 130 | afterEach(() => { 131 | googleLogs.serverless.cli.log.restore(); 132 | }); 133 | 134 | it('should print the received execution result log on the console', () => { 135 | const logs = { 136 | entries: [ 137 | { 138 | timestamp: '1970-01-01 00:00', 139 | textPayload: 'Entry 1', 140 | labels: { execution_id: 'foo' }, 141 | }, 142 | { 143 | timestamp: '1970-01-01 00:01', 144 | textPayload: 'Entry 2', 145 | labels: { execution_id: 'bar' }, 146 | }, 147 | ], 148 | }; 149 | 150 | const logEntry1 = `${chalk.grey('1970-01-01 00:00:')}\t[foo]\tEntry 1`; 151 | const logEntry2 = `${chalk.grey('1970-01-01 00:01:')}\t[bar]\tEntry 2`; 152 | const expectedOutput = `Displaying the 2 most recent log(s):\n\n${logEntry1}\n${logEntry2}`; 153 | 154 | return googleLogs.printLogs(logs).then(() => { 155 | expect(consoleLogStub.calledWithExactly(expectedOutput)).toEqual(true); 156 | }); 157 | }); 158 | 159 | it('should print a default message to the console when no logs were received', () => { 160 | const date = `${new Date().toISOString().slice(0, 10)}:`; 161 | const logEntry = `${chalk.grey(date)}\tThere is no log data to show...`; 162 | const expectedOutput = `Displaying the 1 most recent log(s):\n\n${logEntry}`; 163 | 164 | return googleLogs.printLogs({}).then(() => { 165 | expect(consoleLogStub.calledWithExactly(expectedOutput)).toEqual(true); 166 | }); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /package/googlePackage.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../provider/googleProvider'); 7 | const GooglePackage = require('./googlePackage'); 8 | const Serverless = require('../test/serverless'); 9 | 10 | describe('GooglePackage', () => { 11 | let serverless; 12 | let options; 13 | let googlePackage; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | options = { 18 | stage: 'my-stage', 19 | region: 'my-region', 20 | }; 21 | serverless.setProvider('google', new GoogleProvider(serverless)); 22 | googlePackage = new GooglePackage(serverless, options); 23 | }); 24 | 25 | describe('#constructor()', () => { 26 | it('should set the serverless instance', () => { 27 | expect(googlePackage.serverless).toEqual(serverless); 28 | }); 29 | 30 | it('should set options if provided', () => { 31 | expect(googlePackage.options).toEqual(options); 32 | }); 33 | 34 | it('should make the provider accessible', () => { 35 | expect(googlePackage.provider).toBeInstanceOf(GoogleProvider); 36 | }); 37 | 38 | it('should define the schema of the http triggered function', () => { 39 | expect(serverless.configSchemaHandler.defineFunctionEvent).toHaveBeenCalledWith( 40 | 'google', 41 | 'http', 42 | expect.any(Object) 43 | ); 44 | }); 45 | 46 | it('should define the schema of the event triggered function', () => { 47 | expect(serverless.configSchemaHandler.defineFunctionEvent).toHaveBeenCalledWith( 48 | 'google', 49 | 'event', 50 | expect.any(Object) 51 | ); 52 | }); 53 | 54 | describe('hooks', () => { 55 | let cleanupServerlessDirStub; 56 | let validateStub; 57 | let setDefaultsStub; 58 | let setDeploymentBucketNameStub; 59 | let prepareDeploymentStub; 60 | let saveCreateTemplateFileStub; 61 | let generateArtifactDirectoryNameStub; 62 | let compileFunctionsStub; 63 | let mergeServiceResourcesStub; 64 | let saveUpdateTemplateFileStub; 65 | 66 | beforeEach(() => { 67 | cleanupServerlessDirStub = sinon 68 | .stub(googlePackage, 'cleanupServerlessDir') 69 | .returns(BbPromise.resolve()); 70 | validateStub = sinon.stub(googlePackage, 'validate').returns(BbPromise.resolve()); 71 | setDefaultsStub = sinon.stub(googlePackage, 'setDefaults').returns(BbPromise.resolve()); 72 | setDeploymentBucketNameStub = sinon 73 | .stub(googlePackage, 'setDeploymentBucketName') 74 | .returns(BbPromise.resolve()); 75 | prepareDeploymentStub = sinon 76 | .stub(googlePackage, 'prepareDeployment') 77 | .returns(BbPromise.resolve()); 78 | saveCreateTemplateFileStub = sinon 79 | .stub(googlePackage, 'saveCreateTemplateFile') 80 | .returns(BbPromise.resolve()); 81 | generateArtifactDirectoryNameStub = sinon 82 | .stub(googlePackage, 'generateArtifactDirectoryName') 83 | .returns(BbPromise.resolve()); 84 | compileFunctionsStub = sinon 85 | .stub(googlePackage, 'compileFunctions') 86 | .returns(BbPromise.resolve()); 87 | mergeServiceResourcesStub = sinon 88 | .stub(googlePackage, 'mergeServiceResources') 89 | .returns(BbPromise.resolve()); 90 | saveUpdateTemplateFileStub = sinon 91 | .stub(googlePackage, 'saveUpdateTemplateFile') 92 | .returns(BbPromise.resolve()); 93 | }); 94 | 95 | afterEach(() => { 96 | googlePackage.cleanupServerlessDir.restore(); 97 | googlePackage.validate.restore(); 98 | googlePackage.setDefaults.restore(); 99 | googlePackage.setDeploymentBucketName.restore(); 100 | googlePackage.prepareDeployment.restore(); 101 | googlePackage.saveCreateTemplateFile.restore(); 102 | googlePackage.generateArtifactDirectoryName.restore(); 103 | googlePackage.compileFunctions.restore(); 104 | googlePackage.mergeServiceResources.restore(); 105 | googlePackage.saveUpdateTemplateFile.restore(); 106 | }); 107 | 108 | it('should run "package:cleanup" promise chain', () => 109 | googlePackage.hooks['package:cleanup']().then(() => { 110 | expect(cleanupServerlessDirStub.calledOnce).toEqual(true); 111 | })); 112 | 113 | it('should run "before:package:initialize" promise chain', () => 114 | googlePackage.hooks['before:package:initialize']().then(() => { 115 | expect(validateStub.calledOnce).toEqual(true); 116 | expect(setDefaultsStub.calledAfter(validateStub)).toEqual(true); 117 | })); 118 | 119 | it('should run "package:initialize" promise chain', () => 120 | googlePackage.hooks['package:initialize']().then(() => { 121 | expect(setDeploymentBucketNameStub.calledOnce).toEqual(true); 122 | expect(prepareDeploymentStub.calledAfter(setDeploymentBucketNameStub)).toEqual(true); 123 | expect(saveCreateTemplateFileStub.calledAfter(prepareDeploymentStub)).toEqual(true); 124 | })); 125 | 126 | it('should run "before:package:compileFunctions" promise chain', () => 127 | googlePackage.hooks['before:package:compileFunctions']().then(() => { 128 | expect(generateArtifactDirectoryNameStub.calledOnce).toEqual(true); 129 | })); 130 | 131 | it('should run "package:compileFunctions" promise chain', () => 132 | googlePackage.hooks['package:compileFunctions']().then(() => { 133 | expect(compileFunctionsStub.calledOnce).toEqual(true); 134 | })); 135 | 136 | it('should run "package:finalize" promise chain', () => 137 | googlePackage.hooks['package:finalize']().then(() => { 138 | expect(mergeServiceResourcesStub.calledOnce).toEqual(true); 139 | expect(saveUpdateTemplateFileStub.calledAfter(mergeServiceResourcesStub)).toEqual(true); 140 | })); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /deploy/lib/createDeployment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const sinon = require('sinon'); 7 | const BbPromise = require('bluebird'); 8 | 9 | const GoogleProvider = require('../../provider/googleProvider'); 10 | const GoogleDeploy = require('../googleDeploy'); 11 | const Serverless = require('../../test/serverless'); 12 | 13 | describe('CreateDeployment', () => { 14 | let serverless; 15 | let googleDeploy; 16 | let requestStub; 17 | let configurationTemplateCreateFilePath; 18 | 19 | beforeEach(() => { 20 | serverless = new Serverless(); 21 | serverless.service.service = 'my-service'; 22 | serverless.service.provider = { 23 | project: 'my-project', 24 | }; 25 | serverless.config = { 26 | servicePath: 'tmp', 27 | }; 28 | serverless.setProvider('google', new GoogleProvider(serverless)); 29 | const options = { 30 | stage: 'dev', 31 | region: 'us-central1', 32 | }; 33 | googleDeploy = new GoogleDeploy(serverless, options); 34 | requestStub = sinon.stub(googleDeploy.provider, 'request'); 35 | configurationTemplateCreateFilePath = path.join( 36 | serverless.config.servicePath, 37 | '.serverless', 38 | 'configuration-template-create.yml' 39 | ); 40 | }); 41 | 42 | afterEach(() => { 43 | googleDeploy.provider.request.restore(); 44 | }); 45 | 46 | describe('#createDeployment()', () => { 47 | let checkForExistingDeploymentStub; 48 | let createIfNotExistsStub; 49 | 50 | beforeEach(() => { 51 | checkForExistingDeploymentStub = sinon 52 | .stub(googleDeploy, 'checkForExistingDeployment') 53 | .returns(BbPromise.resolve()); 54 | createIfNotExistsStub = sinon 55 | .stub(googleDeploy, 'createIfNotExists') 56 | .returns(BbPromise.resolve()); 57 | }); 58 | 59 | afterEach(() => { 60 | googleDeploy.checkForExistingDeployment.restore(); 61 | googleDeploy.createIfNotExists.restore(); 62 | }); 63 | 64 | it('should run promise chain', () => 65 | googleDeploy.createDeployment().then(() => { 66 | expect(checkForExistingDeploymentStub.calledOnce).toEqual(true); 67 | expect(createIfNotExistsStub.calledAfter(checkForExistingDeploymentStub)); 68 | })); 69 | }); 70 | 71 | describe('#checkForExistingDeployment()', () => { 72 | it('should return "undefined" if no deployments are found', () => { 73 | requestStub.returns(BbPromise.resolve([])); 74 | 75 | return googleDeploy.checkForExistingDeployment().then((foundDeployment) => { 76 | expect(foundDeployment).toEqual(undefined); 77 | expect( 78 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 79 | project: 'my-project', 80 | }) 81 | ).toEqual(true); 82 | }); 83 | }); 84 | 85 | it('should return "undefined" if deployments do not contain deployment', () => { 86 | const response = { 87 | deployments: [{ name: 'some-other-deployment' }], 88 | }; 89 | requestStub.returns(BbPromise.resolve(response)); 90 | 91 | return googleDeploy.checkForExistingDeployment().then((foundDeployment) => { 92 | expect(foundDeployment).toEqual(undefined); 93 | expect( 94 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 95 | project: 'my-project', 96 | }) 97 | ).toEqual(true); 98 | }); 99 | }); 100 | 101 | it('should find the existing deployment', () => { 102 | const response = { 103 | deployments: [{ name: 'sls-my-service-dev' }, { name: 'some-other-deployment' }], 104 | }; 105 | requestStub.returns(BbPromise.resolve(response)); 106 | 107 | return googleDeploy.checkForExistingDeployment().then((foundDeployment) => { 108 | expect(foundDeployment).toEqual(response.deployments[0]); 109 | expect( 110 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'list', { 111 | project: 'my-project', 112 | }) 113 | ).toEqual(true); 114 | }); 115 | }); 116 | }); 117 | 118 | describe('#createIfNotExists()', () => { 119 | let consoleLogStub; 120 | let readFileSyncStub; 121 | let monitorDeploymentStub; 122 | 123 | beforeEach(() => { 124 | consoleLogStub = sinon.stub(googleDeploy.serverless.cli, 'log').returns(); 125 | readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('some content'); 126 | monitorDeploymentStub = sinon 127 | .stub(googleDeploy, 'monitorDeployment') 128 | .returns(BbPromise.resolve()); 129 | }); 130 | 131 | afterEach(() => { 132 | googleDeploy.serverless.cli.log.restore(); 133 | fs.readFileSync.restore(); 134 | googleDeploy.monitorDeployment.restore(); 135 | }); 136 | 137 | it('should resolve if there is no existing deployment', () => { 138 | const foundDeployment = true; 139 | 140 | return googleDeploy.createIfNotExists(foundDeployment).then(() => { 141 | expect(consoleLogStub.calledOnce).toEqual(false); 142 | expect(readFileSyncStub.called).toEqual(false); 143 | }); 144 | }); 145 | 146 | it('should create and hand over to monitor the deployment if it does not exist', () => { 147 | const foundDeployment = false; 148 | const params = { 149 | project: 'my-project', 150 | resource: { 151 | name: 'sls-my-service-dev', 152 | target: { 153 | config: { 154 | content: fs.readFileSync(configurationTemplateCreateFilePath).toString(), 155 | }, 156 | }, 157 | }, 158 | }; 159 | requestStub.returns(BbPromise.resolve()); 160 | 161 | return googleDeploy.createIfNotExists(foundDeployment).then(() => { 162 | expect(consoleLogStub.calledOnce).toEqual(true); 163 | expect(readFileSyncStub.called).toEqual(true); 164 | expect( 165 | requestStub.calledWithExactly('deploymentmanager', 'deployments', 'insert', params) 166 | ).toEqual(true); 167 | expect( 168 | monitorDeploymentStub.calledWithExactly('sls-my-service-dev', 'create', 5000) 169 | ).toEqual(true); 170 | }); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /shared/validate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const validate = require('./validate'); 7 | const GoogleProvider = require('../provider/googleProvider'); 8 | const Serverless = require('../test/serverless'); 9 | const GoogleCommand = require('../test/googleCommand'); 10 | 11 | describe('Validate', () => { 12 | let serverless; 13 | let googleCommand; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | serverless.config = { 18 | servicePath: true, 19 | }; 20 | serverless.service = { 21 | service: 'some-default-service', 22 | }; 23 | serverless.setProvider('google', new GoogleProvider(serverless)); 24 | googleCommand = new GoogleCommand(serverless, {}, validate); 25 | }); 26 | 27 | describe('#validate()', () => { 28 | let validateServicePathStub; 29 | let validateServiceNameStub; 30 | let validateHandlersStub; 31 | 32 | beforeEach(() => { 33 | validateServicePathStub = sinon 34 | .stub(googleCommand, 'validateServicePath') 35 | .returns(BbPromise.resolve()); 36 | validateServiceNameStub = sinon 37 | .stub(googleCommand, 'validateServiceName') 38 | .returns(BbPromise.resolve()); 39 | validateHandlersStub = sinon 40 | .stub(googleCommand, 'validateHandlers') 41 | .returns(BbPromise.resolve()); 42 | }); 43 | 44 | afterEach(() => { 45 | googleCommand.validateServicePath.restore(); 46 | googleCommand.validateServiceName.restore(); 47 | googleCommand.validateHandlers.restore(); 48 | }); 49 | 50 | it('should run promise chain', () => 51 | googleCommand.validate().then(() => { 52 | expect(validateServicePathStub.calledOnce).toEqual(true); 53 | expect(validateServiceNameStub.calledAfter(validateServicePathStub)); 54 | expect(validateHandlersStub.calledAfter(validateServiceNameStub)); 55 | })); 56 | }); 57 | 58 | describe('#validateServicePath()', () => { 59 | it('should throw an error if not inside service', () => { 60 | serverless.config.servicePath = false; 61 | 62 | expect(() => googleCommand.validateServicePath()).toThrow(Error); 63 | }); 64 | 65 | it('should not throw an error if inside service', () => { 66 | serverless.config.servicePath = true; 67 | 68 | expect(() => googleCommand.validateServicePath()).not.toThrow(Error); 69 | }); 70 | }); 71 | 72 | describe('#validateServiceName()', () => { 73 | it('should throw an error if the service name contains the string "goog"', () => { 74 | serverless.service.service = 'service-name-with-goog-in-it'; 75 | 76 | expect(() => googleCommand.validateServiceName()).toThrow(Error); 77 | }); 78 | 79 | it('should throw an error if the service name contains underscores', () => { 80 | serverless.service.service = 'service_name'; 81 | 82 | expect(() => googleCommand.validateServiceName()).toThrow(Error); 83 | }); 84 | 85 | it('should not throw an error if the service name is valid', () => { 86 | serverless.service.service = 'service-name'; 87 | 88 | expect(() => googleCommand.validateServiceName()).not.toThrow(Error); 89 | }); 90 | }); 91 | 92 | describe('#validateHandlers()', () => { 93 | it('should throw an error if the handler name contains an invalid character', () => { 94 | googleCommand.serverless.service.functions = { 95 | foo: { 96 | handler: 'invalid.handler', 97 | }, 98 | bar: { 99 | handler: 'invalid/handler', 100 | }, 101 | }; 102 | 103 | expect(() => googleCommand.validateHandlers()).toThrow(Error); 104 | }); 105 | 106 | it('should not throw an error if the function handler is valid', () => { 107 | googleCommand.serverless.service.functions = { 108 | foo: { 109 | handler: 'validHandler', 110 | }, 111 | }; 112 | 113 | expect(() => googleCommand.validateHandlers()).not.toThrow(Error); 114 | }); 115 | }); 116 | 117 | describe('#validateEventsProperty()', () => { 118 | const functionName = 'functionName'; 119 | const eventEvent = { 120 | event: {}, 121 | }; 122 | const httpEvent = { 123 | http: {}, 124 | }; 125 | const unknownEvent = { 126 | unknown: {}, 127 | }; 128 | 129 | it('should throw if the configuration of function has no events', () => { 130 | expect(() => validate.validateEventsProperty({}, functionName)).toThrow(); 131 | }); 132 | 133 | it('should throw if the configuration of function has an empty events array', () => { 134 | expect(() => validate.validateEventsProperty({ events: [] }, functionName)).toThrow(); 135 | }); 136 | 137 | it('should throw if the configuration of function has more than one events', () => { 138 | expect(() => 139 | validate.validateEventsProperty({ events: [eventEvent, httpEvent] }, functionName) 140 | ).toThrow(); 141 | }); 142 | 143 | it('should throw if the configuration of function has an unknown event', () => { 144 | expect(() => 145 | validate.validateEventsProperty({ events: [unknownEvent] }, functionName) 146 | ).toThrow(); 147 | }); 148 | 149 | it('should pass if the configuration of function has an http event', () => { 150 | expect(() => 151 | validate.validateEventsProperty({ events: [httpEvent] }, functionName) 152 | ).not.toThrow(); 153 | }); 154 | 155 | it('should pass if the configuration of function has an event event', () => { 156 | expect(() => 157 | validate.validateEventsProperty({ events: [eventEvent] }, functionName) 158 | ).not.toThrow(); 159 | }); 160 | 161 | it('should throw if the configuration of function has an http event but http event is not supported', () => { 162 | expect(() => 163 | validate.validateEventsProperty({ events: [unknownEvent] }, functionName, ['event']) 164 | ).toThrow(); 165 | }); 166 | 167 | it('should pass if the configuration of function has an event event and event is supported', () => { 168 | expect(() => 169 | validate.validateEventsProperty({ events: [eventEvent] }, functionName, ['event']) 170 | ).not.toThrow(); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /.github/workflows/integrate.yml: -------------------------------------------------------------------------------- 1 | # master only 2 | 3 | name: Integrate 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | 9 | env: 10 | SLS_IGNORE_WARNING: '*' 11 | FORCE_COLOR: 1 12 | 13 | jobs: 14 | linuxNode14: 15 | name: '[Linux] Node.js 14: Unit tests ' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Resolve last validated commit hash (for `git diff` purposes) 19 | env: 20 | # See https://github.com/serverlessinc/setup-cicd-resources 21 | GET_LAST_VALIDATED_COMMIT_HASH_URL: ${{ secrets.GET_LAST_VALIDATED_COMMIT_HASH_URL }} 22 | PUT_LAST_VALIDATED_COMMIT_HASH_URL: ${{ secrets.PUT_LAST_VALIDATED_COMMIT_HASH_URL }} 23 | run: | 24 | curl -f "$GET_LAST_VALIDATED_COMMIT_HASH_URL" -o /home/runner/last-validated-commit-hash || : 25 | curl -X PUT -H "User-Agent:" -H "Accept:" -H "Content-Type:" -d "$GITHUB_SHA" "$PUT_LAST_VALIDATED_COMMIT_HASH_URL" 26 | - name: Store last validated commit hash (as it's to be used in other job) 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: last-validated-commit-hash 30 | path: /home/runner/last-validated-commit-hash 31 | 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | - name: Install Node.js and npm 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: 14.x 39 | registry-url: https://registry.npmjs.org 40 | 41 | - name: Retrieve dependencies from cache 42 | id: cacheNpm 43 | uses: actions/cache@v2 44 | with: 45 | path: | 46 | ~/.npm 47 | node_modules 48 | key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 49 | restore-keys: npm-v14-${{ runner.os }}-${{ github.ref }}- 50 | 51 | - name: Install dependencies 52 | if: steps.cacheNpm.outputs.cache-hit != 'true' 53 | run: | 54 | npm update --no-save 55 | npm update --save-dev --no-save 56 | - name: Unit tests 57 | run: npm test 58 | 59 | linuxNode16: 60 | name: '[Linux] Node.js 16: Unit tests' 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout repository 64 | uses: actions/checkout@v2 65 | 66 | - name: Retrieve dependencies from cache 67 | id: cacheNpm 68 | uses: actions/cache@v2 69 | with: 70 | path: | 71 | ~/.npm 72 | node_modules 73 | key: npm-v16-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 74 | restore-keys: npm-v16-${{ runner.os }}-${{ github.ref }}- 75 | 76 | - name: Install Node.js and npm 77 | uses: actions/setup-node@v1 78 | with: 79 | node-version: 16.x 80 | 81 | - name: Install dependencies 82 | if: steps.cacheNpm.outputs.cache-hit != 'true' 83 | run: | 84 | npm update --no-save 85 | npm update --save-dev --no-save 86 | - name: Unit tests 87 | run: npm test 88 | 89 | linuxNode12: 90 | name: '[Linux] Node.js 12: Unit tests' 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: Checkout repository 94 | uses: actions/checkout@v2 95 | 96 | - name: Retrieve dependencies from cache 97 | id: cacheNpm 98 | uses: actions/cache@v2 99 | with: 100 | path: | 101 | ~/.npm 102 | node_modules 103 | key: npm-v12-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 104 | restore-keys: npm-v12-${{ runner.os }}-${{ github.ref }}- 105 | 106 | - name: Install Node.js and npm 107 | uses: actions/setup-node@v1 108 | with: 109 | node-version: 12.x 110 | 111 | - name: Install dependencies 112 | if: steps.cacheNpm.outputs.cache-hit != 'true' 113 | run: | 114 | npm update --no-save 115 | npm update --save-dev --no-save 116 | - name: Unit tests 117 | run: npm test 118 | 119 | linuxNode10: 120 | name: '[Linux] Node.js v10: Unit tests' 121 | runs-on: ubuntu-latest 122 | steps: 123 | - name: Checkout repository 124 | uses: actions/checkout@v2 125 | 126 | - name: Retrieve dependencies from cache 127 | id: cacheNpm 128 | uses: actions/cache@v2 129 | with: 130 | path: | 131 | ~/.npm 132 | node_modules 133 | key: npm-v10-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 134 | restore-keys: | 135 | npm-v10-${{ runner.os }}-${{ github.ref }}- 136 | npm-v10-${{ runner.os }}-refs/heads/master- 137 | 138 | - name: Install Node.js and npm 139 | uses: actions/setup-node@v1 140 | with: 141 | node-version: 10.x 142 | 143 | - name: Install dependencies 144 | if: steps.cacheNpm.outputs.cache-hit != 'true' 145 | run: | 146 | npm update --no-save 147 | npm update --save-dev --no-save 148 | - name: Unit tests 149 | run: npm test 150 | 151 | tagIfNewVersion: 152 | name: ubuntu-latest 153 | runs-on: ubuntu-latest 154 | needs: [linuxNode14, linuxNode16, linuxNode12, linuxNode10] 155 | timeout-minutes: 30 # Default is 360 156 | steps: 157 | - name: Checkout repository 158 | uses: actions/checkout@v2 159 | with: 160 | # Ensure to have complete history of commits pushed with given push operation 161 | # It's loose and imperfect assumption that no more than 30 commits will be pushed at once 162 | fetch-depth: 30 163 | # Tag needs to be pushed with real user token, otherwise pushed tag won't trigger the actions workflow 164 | # Hence we're passing 'serverless-ci' user authentication token 165 | token: ${{ secrets.USER_GITHUB_TOKEN }} 166 | 167 | - name: Resolve last validated commit hash (for `git diff` purposes) 168 | uses: actions/download-artifact@v2 169 | continue-on-error: true 170 | with: 171 | name: last-validated-commit-hash 172 | path: /home/runner 173 | - name: Tag if new version 174 | run: | 175 | LAST_VALIDATED_COMMIT_HASH=`cat /home/runner/last-validated-commit-hash` || : 176 | if [ -n "$LAST_VALIDATED_COMMIT_HASH" ]; 177 | then 178 | NEW_VERSION=`git diff -U0 $LAST_VALIDATED_COMMIT_HASH package.json | grep '"version": "' | tail -n 1 | grep -oE "[0-9]+\.[0-9]+\.[0-9]+"` || : 179 | if [ -n "$NEW_VERSION" ]; 180 | then 181 | git tag v$NEW_VERSION 182 | git push --tags 183 | fi 184 | fi 185 | -------------------------------------------------------------------------------- /invokeLocal/lib/nodeJs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const GoogleProvider = require('../../provider/googleProvider'); 5 | const GoogleInvokeLocal = require('../googleInvokeLocal'); 6 | const Serverless = require('../../test/serverless'); 7 | 8 | jest.spyOn(console, 'log'); 9 | describe('invokeLocalNodeJs', () => { 10 | const myVarValue = 'MY_VAR_VALUE'; 11 | let serverless; 12 | let googleInvokeLocal; 13 | 14 | beforeEach(() => { 15 | serverless = new Serverless(); 16 | serverless.setProvider('google', new GoogleProvider(serverless)); 17 | serverless.service.provider.environment = { 18 | MY_VAR: myVarValue, 19 | }; 20 | serverless.serviceDir = path.join(process.cwd(), 'invokeLocal', 'lib', 'testMocks'); // To load the index.js of the mock folder 21 | serverless.cli.consoleLog = jest.fn(); 22 | googleInvokeLocal = new GoogleInvokeLocal(serverless, {}); 23 | }); 24 | describe('event', () => { 25 | const eventName = 'eventName'; 26 | const contextName = 'contextName'; 27 | const event = { 28 | name: eventName, 29 | }; 30 | const context = { 31 | name: contextName, 32 | }; 33 | const baseConfig = { 34 | events: [{ event: {} }], 35 | }; 36 | it('should invoke a sync handler', async () => { 37 | const functionConfig = { 38 | ...baseConfig, 39 | handler: 'eventSyncHandler', 40 | }; 41 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context); 42 | // eslint-disable-next-line no-console 43 | expect(console.log).toHaveBeenCalledWith('EVENT_SYNC_HANDLER'); 44 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith(`{\n "result": "${eventName}"\n}`); 45 | }); 46 | 47 | it('should handle errors in a sync handler', async () => { 48 | const functionConfig = { 49 | ...baseConfig, 50 | handler: 'eventSyncHandlerWithError', 51 | }; 52 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context); 53 | // eslint-disable-next-line no-console 54 | expect(console.log).toHaveBeenCalledWith('EVENT_SYNC_HANDLER'); 55 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 56 | expect.stringContaining('"errorMessage": "SYNC_ERROR"') 57 | ); 58 | }); 59 | 60 | it('should invoke an async handler', async () => { 61 | const functionConfig = { 62 | ...baseConfig, 63 | handler: 'eventAsyncHandler', 64 | }; 65 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context); 66 | // eslint-disable-next-line no-console 67 | expect(console.log).toHaveBeenCalledWith('EVENT_ASYNC_HANDLER'); 68 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 69 | `{\n "result": "${contextName}"\n}` 70 | ); 71 | }); 72 | 73 | it('should handle errors in an async handler', async () => { 74 | const functionConfig = { 75 | ...baseConfig, 76 | handler: 'eventAsyncHandlerWithError', 77 | }; 78 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context); 79 | // eslint-disable-next-line no-console 80 | expect(console.log).toHaveBeenCalledWith('EVENT_ASYNC_HANDLER'); 81 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 82 | expect.stringContaining('"errorMessage": "ASYNC_ERROR"') 83 | ); 84 | }); 85 | 86 | it('should give the environment variables to the handler', async () => { 87 | const functionConfig = { 88 | ...baseConfig, 89 | handler: 'eventEnvHandler', 90 | }; 91 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, event, context); 92 | // eslint-disable-next-line no-console 93 | expect(console.log).toHaveBeenCalledWith(myVarValue); 94 | }); 95 | }); 96 | describe('http', () => { 97 | const message = 'httpBodyMessage'; 98 | const req = { 99 | body: { message }, 100 | }; 101 | const context = {}; 102 | const baseConfig = { 103 | events: [{ http: '' }], 104 | }; 105 | it('should invoke a sync handler', async () => { 106 | const functionConfig = { 107 | ...baseConfig, 108 | handler: 'httpSyncHandler', 109 | }; 110 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context); 111 | // eslint-disable-next-line no-console 112 | expect(console.log).toHaveBeenCalledWith('HTTP_SYNC_HANDLER'); 113 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 114 | JSON.stringify( 115 | { 116 | status: 200, 117 | headers: { 118 | 'x-test': 'headerValue', 119 | 'content-type': 'application/json; charset=utf-8', 120 | 'content-length': '37', 121 | 'etag': 'W/"25-F1uWAIMs2TbWZIN1zJauHXahSdU"', 122 | }, 123 | body: { responseMessage: message }, 124 | }, 125 | null, 126 | 4 127 | ) 128 | ); 129 | }); 130 | 131 | it('should handle errors in a sync handler', async () => { 132 | const functionConfig = { 133 | ...baseConfig, 134 | handler: 'httpSyncHandlerWithError', 135 | }; 136 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context); 137 | // eslint-disable-next-line no-console 138 | expect(console.log).toHaveBeenCalledWith('HTTP_SYNC_HANDLER'); 139 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 140 | expect.stringContaining('"errorMessage": "SYNC_ERROR"') 141 | ); 142 | }); 143 | 144 | it('should invoke an async handler', async () => { 145 | const functionConfig = { 146 | ...baseConfig, 147 | handler: 'httpAsyncHandler', 148 | }; 149 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context); 150 | // eslint-disable-next-line no-console 151 | expect(console.log).toHaveBeenCalledWith('HTTP_ASYNC_HANDLER'); 152 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 153 | JSON.stringify({ status: 404, headers: {} }, null, 4) 154 | ); 155 | }); 156 | 157 | it('should handle errors in an async handler', async () => { 158 | const functionConfig = { 159 | ...baseConfig, 160 | handler: 'httpAsyncHandlerWithError', 161 | }; 162 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context); 163 | // eslint-disable-next-line no-console 164 | expect(console.log).toHaveBeenCalledWith('HTTP_ASYNC_HANDLER'); 165 | expect(serverless.cli.consoleLog).toHaveBeenCalledWith( 166 | expect.stringContaining('"errorMessage": "ASYNC_ERROR"') 167 | ); 168 | }); 169 | 170 | it('should give the environment variables to the handler', async () => { 171 | const functionConfig = { 172 | ...baseConfig, 173 | handler: 'httpEnvHandler', 174 | }; 175 | await googleInvokeLocal.invokeLocalNodeJs(functionConfig, req, context); 176 | // eslint-disable-next-line no-console 177 | expect(console.log).toHaveBeenCalledWith(myVarValue); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /invokeLocal/googleInvokeLocal.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | 5 | const GoogleProvider = require('../provider/googleProvider'); 6 | const GoogleInvokeLocal = require('./googleInvokeLocal'); 7 | const Serverless = require('../test/serverless'); 8 | 9 | describe('GoogleInvokeLocal', () => { 10 | let serverless; 11 | const functionName = 'myFunction'; 12 | const rawOptions = { 13 | f: functionName, 14 | }; 15 | const processedOptions = { 16 | function: functionName, 17 | }; 18 | let googleInvokeLocal; 19 | 20 | beforeAll(() => { 21 | serverless = new Serverless(); 22 | serverless.setProvider('google', new GoogleProvider(serverless)); 23 | googleInvokeLocal = new GoogleInvokeLocal(serverless, rawOptions); 24 | serverless.processedInput.options = processedOptions; 25 | }); 26 | 27 | describe('#constructor()', () => { 28 | it('should set the serverless instance', () => { 29 | expect(googleInvokeLocal.serverless).toEqual(serverless); 30 | }); 31 | 32 | it('should set the raw options if provided', () => { 33 | expect(googleInvokeLocal.options).toEqual(rawOptions); 34 | }); 35 | 36 | it('should make the provider accessible', () => { 37 | expect(googleInvokeLocal.provider).toBeInstanceOf(GoogleProvider); 38 | }); 39 | 40 | it.each` 41 | method 42 | ${'validate'} 43 | ${'setDefaults'} 44 | ${'getDataAndContext'} 45 | ${'invokeLocalNodeJs'} 46 | ${'loadFileInOption'} 47 | ${'validateEventsProperty'} 48 | ${'addEnvironmentVariablesToProcessEnv'} 49 | `('should declare $method method', ({ method }) => { 50 | expect(googleInvokeLocal[method]).toBeDefined(); 51 | }); 52 | 53 | describe('hooks', () => { 54 | let validateStub; 55 | let setDefaultsStub; 56 | let getDataAndContextStub; 57 | let invokeLocalStub; 58 | 59 | beforeAll(() => { 60 | validateStub = sinon.stub(googleInvokeLocal, 'validate').resolves(); 61 | setDefaultsStub = sinon.stub(googleInvokeLocal, 'setDefaults').resolves(); 62 | getDataAndContextStub = sinon.stub(googleInvokeLocal, 'getDataAndContext').resolves(); 63 | invokeLocalStub = sinon.stub(googleInvokeLocal, 'invokeLocal').resolves(); 64 | }); 65 | 66 | afterEach(() => { 67 | googleInvokeLocal.validate.resetHistory(); 68 | googleInvokeLocal.setDefaults.resetHistory(); 69 | googleInvokeLocal.getDataAndContext.resetHistory(); 70 | googleInvokeLocal.invokeLocal.resetHistory(); 71 | }); 72 | 73 | afterAll(() => { 74 | googleInvokeLocal.validate.restore(); 75 | googleInvokeLocal.setDefaults.restore(); 76 | googleInvokeLocal.getDataAndContext.restore(); 77 | googleInvokeLocal.invokeLocal.restore(); 78 | }); 79 | 80 | it.each` 81 | hook 82 | ${'initialize'} 83 | ${'before:invoke:local:invoke'} 84 | ${'invoke:local:invoke'} 85 | `('should declare $hook hook', ({ hook }) => { 86 | expect(googleInvokeLocal.hooks[hook]).toBeDefined(); 87 | }); 88 | 89 | describe('initialize hook', () => { 90 | it('should override raw options with processed options', () => { 91 | googleInvokeLocal.hooks.initialize(); 92 | expect(googleInvokeLocal.options).toEqual(processedOptions); 93 | }); 94 | }); 95 | 96 | describe('before:invoke:local:invoke hook', () => { 97 | it('should validate the configuration', async () => { 98 | await googleInvokeLocal.hooks['before:invoke:local:invoke'](); 99 | expect(validateStub.calledOnce).toEqual(true); 100 | }); 101 | 102 | it('should set the defaults values', async () => { 103 | await googleInvokeLocal.hooks['before:invoke:local:invoke'](); 104 | expect(setDefaultsStub.calledOnce).toEqual(true); 105 | }); 106 | 107 | it('should resolve the data and the context of the invocation', async () => { 108 | await googleInvokeLocal.hooks['before:invoke:local:invoke'](); 109 | expect(getDataAndContextStub.calledOnce).toEqual(true); 110 | }); 111 | }); 112 | 113 | describe('invoke:local:invoke hook', () => { 114 | it('should invoke the function locally', () => { 115 | googleInvokeLocal.hooks['invoke:local:invoke'](); 116 | expect(invokeLocalStub.calledOnce).toEqual(true); 117 | }); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('#invokeLocal()', () => { 123 | const functionObj = Symbol('functionObj'); 124 | const data = Symbol('data'); 125 | const context = Symbol('context'); 126 | const runtime = 'nodejs14'; 127 | let getFunctionStub; 128 | let validateEventsPropertyStub; 129 | let getRuntimeStub; 130 | let invokeLocalNodeJsStub; 131 | 132 | beforeAll(() => { 133 | googleInvokeLocal.options = { 134 | ...processedOptions, // invokeLocal is called after the initialize hook which override the options 135 | data, // data and context are populated by getDataAndContext in before:invoke:local:invoke hook 136 | context, 137 | }; 138 | getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns(functionObj); 139 | validateEventsPropertyStub = sinon 140 | .stub(googleInvokeLocal, 'validateEventsProperty') 141 | .returns(); 142 | getRuntimeStub = sinon.stub(googleInvokeLocal.provider, 'getRuntime').returns(runtime); 143 | 144 | invokeLocalNodeJsStub = sinon.stub(googleInvokeLocal, 'invokeLocalNodeJs').resolves(); 145 | }); 146 | 147 | afterEach(() => { 148 | serverless.service.getFunction.resetHistory(); 149 | googleInvokeLocal.validateEventsProperty.resetHistory(); 150 | googleInvokeLocal.provider.getRuntime.resetHistory(); 151 | googleInvokeLocal.invokeLocalNodeJs.resetHistory(); 152 | }); 153 | 154 | afterAll(() => { 155 | serverless.service.getFunction.restore(); 156 | googleInvokeLocal.validateEventsProperty.restore(); 157 | googleInvokeLocal.provider.getRuntime.restore(); 158 | googleInvokeLocal.invokeLocalNodeJs.restore(); 159 | }); 160 | 161 | it('should get the function configuration', async () => { 162 | await googleInvokeLocal.invokeLocal(); 163 | expect(getFunctionStub.calledOnceWith(functionName)).toEqual(true); 164 | }); 165 | 166 | it('should validate the function configuration', async () => { 167 | await googleInvokeLocal.invokeLocal(); 168 | expect(validateEventsPropertyStub.calledOnceWith(functionObj, functionName)).toEqual(true); 169 | }); 170 | 171 | it('should get the runtime', async () => { 172 | await googleInvokeLocal.invokeLocal(); 173 | expect(getRuntimeStub.calledOnceWith(functionObj)).toEqual(true); 174 | }); 175 | 176 | it('should invoke locally the function with node js', async () => { 177 | await googleInvokeLocal.invokeLocal(); 178 | expect(invokeLocalNodeJsStub.calledOnceWith(functionObj, data, context)).toEqual(true); 179 | }); 180 | 181 | it('should throw if the runtime is not node js', async () => { 182 | getRuntimeStub.returns('python3'); 183 | await expect(googleInvokeLocal.invokeLocal()).rejects.toThrow( 184 | 'Local invocation with runtime python3 is not supported' 185 | ); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /package/lib/compileFunctions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-use-before-define: 0 */ 4 | 5 | const path = require('path'); 6 | 7 | const _ = require('lodash'); 8 | const BbPromise = require('bluebird'); 9 | const { validateEventsProperty } = require('../../shared/validate'); 10 | 11 | module.exports = { 12 | compileFunctions() { 13 | const artifactFilePath = this.serverless.service.package.artifact; 14 | const fileName = artifactFilePath.split(path.sep).pop(); 15 | const projectName = _.get(this, 'serverless.service.provider.project'); 16 | this.serverless.service.provider.region = 17 | this.serverless.service.provider.region || 'us-central1'; 18 | this.serverless.service.package.artifactFilePath = `${this.serverless.service.package.artifactDirectoryName}/${fileName}`; 19 | 20 | this.serverless.service.getAllFunctions().forEach((functionName) => { 21 | const funcObject = this.serverless.service.getFunction(functionName); 22 | 23 | let vpcEgress = funcObject.vpcEgress || this.serverless.service.provider.vpcEgress; 24 | 25 | this.serverless.cli.log(`Compiling function "${functionName}"...`); 26 | 27 | validateHandlerProperty(funcObject, functionName); 28 | validateEventsProperty(funcObject, functionName); 29 | validateVpcConnectorProperty(funcObject, functionName); 30 | validateVpcConnectorEgressProperty(vpcEgress); 31 | 32 | const funcTemplate = getFunctionTemplate( 33 | funcObject, 34 | projectName, 35 | this.serverless.service.provider.region, 36 | `gs://${this.serverless.service.provider.deploymentBucketName}/${this.serverless.service.package.artifactFilePath}` 37 | ); 38 | 39 | funcTemplate.properties.serviceAccountEmail = 40 | _.get(funcObject, 'serviceAccountEmail') || 41 | _.get(this, 'serverless.service.provider.serviceAccountEmail') || 42 | null; 43 | funcTemplate.properties.availableMemoryMb = 44 | _.get(funcObject, 'memorySize') || 45 | _.get(this, 'serverless.service.provider.memorySize') || 46 | 256; 47 | funcTemplate.properties.runtime = this.provider.getRuntime(funcObject); 48 | funcTemplate.properties.timeout = 49 | _.get(funcObject, 'timeout') || _.get(this, 'serverless.service.provider.timeout') || '60s'; 50 | funcTemplate.properties.environmentVariables = 51 | this.provider.getConfiguredEnvironment(funcObject); 52 | funcTemplate.properties.secretEnvironmentVariables = 53 | this.provider.getConfiguredSecrets(funcObject); 54 | 55 | if (!funcTemplate.properties.serviceAccountEmail) { 56 | delete funcTemplate.properties.serviceAccountEmail; 57 | } 58 | 59 | if (funcObject.vpc) { 60 | _.assign(funcTemplate.properties, { 61 | vpcConnector: _.get(funcObject, 'vpc') || _.get(this, 'serverless.service.provider.vpc'), 62 | }); 63 | } 64 | 65 | if (vpcEgress) { 66 | vpcEgress = vpcEgress.toUpperCase(); 67 | if (vpcEgress === 'ALL') vpcEgress = 'ALL_TRAFFIC'; 68 | if (vpcEgress === 'PRIVATE') vpcEgress = 'PRIVATE_RANGES_ONLY'; 69 | _.assign(funcTemplate.properties, { 70 | vpcConnectorEgressSettings: vpcEgress, 71 | }); 72 | } 73 | 74 | if (funcObject.maxInstances) { 75 | funcTemplate.properties.maxInstances = funcObject.maxInstances; 76 | } 77 | 78 | if (funcObject.minInstances) { 79 | funcTemplate.properties.minInstances = funcObject.minInstances; 80 | } 81 | 82 | if (!_.size(funcTemplate.properties.environmentVariables)) { 83 | delete funcTemplate.properties.environmentVariables; 84 | } 85 | if (!_.size(funcTemplate.properties.secretEnvironmentVariables)) { 86 | delete funcTemplate.properties.secretEnvironmentVariables; 87 | } 88 | 89 | funcTemplate.properties.labels = _.assign( 90 | {}, 91 | _.get(this, 'serverless.service.provider.labels') || {}, 92 | _.get(funcObject, 'labels') || {} // eslint-disable-line comma-dangle 93 | ); 94 | 95 | const eventType = Object.keys(funcObject.events[0])[0]; 96 | 97 | if (eventType === 'http') { 98 | const url = funcObject.events[0].http; 99 | 100 | funcTemplate.properties.httpsTrigger = {}; 101 | funcTemplate.properties.httpsTrigger.url = url; 102 | } 103 | if (eventType === 'event') { 104 | const type = funcObject.events[0].event.eventType; 105 | const path = funcObject.events[0].event.path; //eslint-disable-line 106 | const resource = funcObject.events[0].event.resource; 107 | const failurePolicy = funcObject.events[0].event.failurePolicy; 108 | const retry = _.get(funcObject.events[0].event, 'failurePolicy.retry'); 109 | 110 | funcTemplate.properties.eventTrigger = {}; 111 | funcTemplate.properties.eventTrigger.eventType = type; 112 | if (path) funcTemplate.properties.eventTrigger.path = path; 113 | funcTemplate.properties.eventTrigger.resource = resource; 114 | if (failurePolicy) { 115 | funcTemplate.properties.eventTrigger.failurePolicy = {}; 116 | funcTemplate.properties.eventTrigger.failurePolicy.retry = retry; 117 | } 118 | } 119 | 120 | this.serverless.service.provider.compiledConfigurationTemplate.resources.push(funcTemplate); 121 | }); 122 | 123 | return BbPromise.resolve(); 124 | }, 125 | }; 126 | 127 | const validateHandlerProperty = (funcObject, functionName) => { 128 | if (!funcObject.handler) { 129 | const errorMessage = [ 130 | `Missing "handler" property for function "${functionName}".`, 131 | ' Your function needs a "handler".', 132 | ' Please check the docs for more info.', 133 | ].join(''); 134 | throw new Error(errorMessage); 135 | } 136 | }; 137 | 138 | const validateVpcConnectorProperty = (funcObject, functionName) => { 139 | if (funcObject.vpc && typeof funcObject.vpc === 'string') { 140 | // vpcConnector argument can be one of two possible formats as described here: 141 | // https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions#resource:-cloudfunction 142 | if (funcObject.vpc.indexOf('/') > -1) { 143 | const vpcNamePattern = /projects\/[\s\S]*\/locations\/[\s\S]*\/connectors\/[\s\S]*/i; 144 | if (!vpcNamePattern.test(funcObject.vpc)) { 145 | const errorMessage = [ 146 | `The function "${functionName}" has invalid vpc connection name`, 147 | ' VPC Connector name should follow projects/{project_id}/locations/{region}/connectors/{connector_name}', 148 | ' or just {connector_name} if within the same project.', 149 | ' Please check the docs for more info at ', 150 | ' https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions#resource:-cloudfunction', 151 | ].join(''); 152 | throw new Error(errorMessage); 153 | } 154 | } 155 | } 156 | }; 157 | 158 | const validateVpcConnectorEgressProperty = (vpcEgress) => { 159 | if (vpcEgress && typeof vpcEgress !== 'string') { 160 | const errorMessage = [ 161 | 'Your provider/function has invalid vpc connection name', 162 | ' VPC Connector Egress Setting be either ALL_TRAFFIC or PRIVATE_RANGES_ONLY. ', 163 | ' You may shorten these to ALL or PRIVATE optionally.', 164 | ' Please check the docs for more info at', 165 | ' https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions#resource:-cloudfunction', 166 | ].join(''); 167 | throw new Error(errorMessage); 168 | } 169 | }; 170 | 171 | const getFunctionTemplate = (funcObject, projectName, region, sourceArchiveUrl) => { 172 | //eslint-disable-line 173 | return { 174 | type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', 175 | name: funcObject.name, 176 | properties: { 177 | parent: `projects/${projectName}/locations/${region}`, 178 | availableMemoryMb: 256, 179 | runtime: 'nodejs10', 180 | timeout: '60s', 181 | entryPoint: funcObject.handler, 182 | function: funcObject.name, 183 | sourceArchiveUrl, 184 | }, 185 | }; 186 | }; 187 | -------------------------------------------------------------------------------- /deploy/lib/cleanupDeploymentBucket.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const BbPromise = require('bluebird'); 5 | 6 | const GoogleProvider = require('../../provider/googleProvider'); 7 | const GoogleDeploy = require('../googleDeploy'); 8 | const Serverless = require('../../test/serverless'); 9 | 10 | describe('CleanupDeploymentBucket', () => { 11 | let serverless; 12 | let googleDeploy; 13 | let key; 14 | 15 | beforeEach(() => { 16 | serverless = new Serverless(); 17 | serverless.service = { 18 | service: 'my-service', 19 | provider: { 20 | deploymentBucketName: 'sls-my-service-dev-12345678', 21 | }, 22 | }; 23 | serverless.setProvider('google', new GoogleProvider(serverless)); 24 | const options = { 25 | stage: 'dev', 26 | region: 'us-central1', 27 | }; 28 | googleDeploy = new GoogleDeploy(serverless, options); 29 | key = `serverless/${serverless.service.service}/${options.stage}`; 30 | }); 31 | 32 | describe('#cleanupDeploymentBucket()', () => { 33 | let getObjectsToRemoveStub; 34 | let removeObjectsStub; 35 | 36 | beforeEach(() => { 37 | getObjectsToRemoveStub = sinon 38 | .stub(googleDeploy, 'getObjectsToRemove') 39 | .returns(BbPromise.resolve()); 40 | removeObjectsStub = sinon.stub(googleDeploy, 'removeObjects').returns(BbPromise.resolve()); 41 | }); 42 | 43 | afterEach(() => { 44 | googleDeploy.getObjectsToRemove.restore(); 45 | googleDeploy.removeObjects.restore(); 46 | }); 47 | 48 | it('should run promise chain', () => 49 | googleDeploy.cleanupDeploymentBucket().then(() => { 50 | expect(getObjectsToRemoveStub.calledOnce).toEqual(true); 51 | expect(removeObjectsStub.calledAfter(getObjectsToRemoveStub)); 52 | })); 53 | }); 54 | 55 | describe('#getObjectsToRemove()', () => { 56 | let requestStub; 57 | 58 | beforeEach(() => { 59 | requestStub = sinon.stub(googleDeploy.provider, 'request'); 60 | }); 61 | 62 | afterEach(() => { 63 | googleDeploy.provider.request.restore(); 64 | }); 65 | 66 | it('should return all to be removed objects (except the last 4)', () => { 67 | const response = { 68 | items: [ 69 | { 70 | bucket: 'sls-my-service-dev-12345678', 71 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 72 | }, 73 | { 74 | bucket: 'sls-my-service-dev-12345678', 75 | name: `${key}/141264711231-2016-08-18T15:43:00/artifact.zip`, 76 | }, 77 | { 78 | bucket: 'sls-my-service-dev-12345678', 79 | name: `${key}/141321321541-2016-08-18T11:23:02/artifact.zip`, 80 | }, 81 | { 82 | bucket: 'sls-my-service-dev-12345678', 83 | name: `${key}/142003031341-2016-08-18T12:46:04/artifact.zip`, 84 | }, 85 | { 86 | bucket: 'sls-my-service-dev-12345678', 87 | name: `${key}/113304333331-2016-08-18T13:40:06/artifact.zip`, 88 | }, 89 | { 90 | bucket: 'sls-my-service-dev-12345678', 91 | name: `${key}/903940390431-2016-08-18T23:42:08/artifact.zip`, 92 | }, 93 | ], 94 | }; 95 | requestStub.returns(BbPromise.resolve(response)); 96 | 97 | return googleDeploy.getObjectsToRemove().then((objects) => { 98 | expect(objects.length).toEqual(2); 99 | expect(objects).not.toContainEqual({ 100 | bucket: 'sls-my-service-dev-12345678', 101 | name: `${key}/141321321541-2016-08-18T11:23:02/artifact.zip`, 102 | }); 103 | expect(objects).not.toContainEqual({ 104 | bucket: 'sls-my-service-dev-12345678', 105 | name: `${key}/142003031341-2016-08-18T12:46:04/artifact.zip`, 106 | }); 107 | expect(objects).not.toContainEqual({ 108 | bucket: 'sls-my-service-dev-12345678', 109 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 110 | }); 111 | expect(objects).not.toContainEqual({ 112 | bucket: 'sls-my-service-dev-12345678', 113 | name: `${key}/903940390431-2016-08-18T23:42:08/artifact.zip`, 114 | }); 115 | expect( 116 | requestStub.calledWithExactly('storage', 'objects', 'list', { 117 | bucket: 'sls-my-service-dev-12345678', 118 | }) 119 | ).toEqual(true); 120 | }); 121 | }); 122 | 123 | it('should return an empty array if there are no objects which should be removed', () => { 124 | const response = { 125 | items: [ 126 | { 127 | bucket: 'sls-my-service-dev-12345678', 128 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 129 | }, 130 | { 131 | bucket: 'sls-my-service-dev-12345678', 132 | name: `${key}/141264711231-2016-08-18T15:43:00/artifact.zip`, 133 | }, 134 | { 135 | bucket: 'sls-my-service-dev-12345678', 136 | name: `${key}/141321321541-2016-08-18T11:23:02/artifact.zip`, 137 | }, 138 | { 139 | bucket: 'sls-my-service-dev-12345678', 140 | name: `${key}/142003031341-2016-08-18T12:46:04/artifact.zip`, 141 | }, 142 | ], 143 | }; 144 | requestStub.returns(BbPromise.resolve(response)); 145 | 146 | return googleDeploy.getObjectsToRemove().then((objects) => { 147 | expect(objects.length).toEqual(0); 148 | expect(objects).toEqual([]); 149 | expect( 150 | requestStub.calledWithExactly('storage', 'objects', 'list', { 151 | bucket: 'sls-my-service-dev-12345678', 152 | }) 153 | ).toEqual(true); 154 | }); 155 | }); 156 | 157 | it('should return an empty array if no objects are returned', () => { 158 | const response = { 159 | items: [], 160 | }; 161 | requestStub.returns(BbPromise.resolve(response)); 162 | 163 | return googleDeploy.getObjectsToRemove().then((objects) => { 164 | expect(objects.length).toEqual(0); 165 | expect(objects).toEqual([]); 166 | expect( 167 | requestStub.calledWithExactly('storage', 'objects', 'list', { 168 | bucket: 'sls-my-service-dev-12345678', 169 | }) 170 | ).toEqual(true); 171 | }); 172 | }); 173 | }); 174 | 175 | describe('#removeObjects()', () => { 176 | let requestStub; 177 | let consoleLogStub; 178 | 179 | beforeEach(() => { 180 | requestStub = sinon.stub(googleDeploy.provider, 'request'); 181 | consoleLogStub = sinon.stub(googleDeploy.serverless.cli, 'log').returns(); 182 | }); 183 | 184 | afterEach(() => { 185 | googleDeploy.provider.request.restore(); 186 | googleDeploy.serverless.cli.log.restore(); 187 | }); 188 | 189 | it('should resolve if no objects should be removed', () => { 190 | const objectsToRemove = []; 191 | 192 | return googleDeploy.removeObjects(objectsToRemove).then(() => { 193 | expect(requestStub.calledOnce).toEqual(false); 194 | expect(consoleLogStub.calledOnce).toEqual(false); 195 | }); 196 | }); 197 | 198 | it('should remove all given objects', () => { 199 | const objectsToRemove = [ 200 | { 201 | bucket: 'sls-my-service-dev-12345678', 202 | name: `${key}/151224711231-2016-08-18T15:42:00/artifact.zip`, 203 | }, 204 | { 205 | bucket: 'sls-my-service-dev-12345678', 206 | name: `${key}/141264711231-2016-08-18T15:43:00/artifact.zip`, 207 | }, 208 | { 209 | bucket: 'sls-my-service-dev-12345678', 210 | name: `${key}/141321321541-2016-08-18T11:23:02/artifact.zip`, 211 | }, 212 | { 213 | bucket: 'sls-my-service-dev-12345678', 214 | name: `${key}/142003031341-2016-08-18T12:46:04/artifact.zip`, 215 | }, 216 | ]; 217 | 218 | requestStub.returns(BbPromise.resolve('removePromise')); 219 | 220 | return googleDeploy.removeObjects(objectsToRemove).then((removePromises) => { 221 | expect(requestStub.called).toEqual(true); 222 | expect(consoleLogStub.calledOnce).toEqual(true); 223 | expect(removePromises).toEqual([ 224 | 'removePromise', 225 | 'removePromise', 226 | 'removePromise', 227 | 'removePromise', 228 | ]); 229 | }); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /provider/googleProvider.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | 5 | const sinon = require('sinon'); 6 | const google = require('googleapis').google; 7 | 8 | const GoogleProvider = require('./googleProvider'); 9 | const Serverless = require('../test/serverless'); 10 | 11 | describe('GoogleProvider', () => { 12 | let googleProvider; 13 | let serverless; 14 | let setProviderStub; 15 | let homedirStub; 16 | 17 | const providerRuntime = 'providerRuntime'; 18 | 19 | beforeEach(() => { 20 | serverless = new Serverless(); 21 | serverless.version = '1.0.0'; 22 | serverless.service = { 23 | provider: { 24 | project: 'example-project', 25 | runtime: providerRuntime, 26 | }, 27 | }; 28 | setProviderStub = sinon.stub(serverless, 'setProvider').returns(); 29 | homedirStub = sinon.stub(os, 'homedir').returns('/root'); 30 | googleProvider = new GoogleProvider(serverless); 31 | }); 32 | 33 | afterEach(() => { 34 | serverless.setProvider.restore(); 35 | os.homedir.restore(); 36 | }); 37 | 38 | describe('#getProviderName()', () => { 39 | it('should return the provider name', () => { 40 | expect(GoogleProvider.getProviderName()).toEqual('google'); 41 | }); 42 | }); 43 | 44 | describe('#constructor()', () => { 45 | it('should store an instance of serverless', () => { 46 | expect(googleProvider.serverless).toBeInstanceOf(Serverless); 47 | }); 48 | 49 | it('should store an instance of itself', () => { 50 | expect(googleProvider.provider).toBeInstanceOf(GoogleProvider); 51 | }); 52 | 53 | it('should set the provider with the Serverless instance', () => { 54 | expect(setProviderStub.calledOnce).toEqual(true); 55 | }); 56 | 57 | it('should set the used SDKs', () => { 58 | expect(googleProvider.sdk.google).toBeDefined(); 59 | 60 | expect(googleProvider.sdk.deploymentmanager).toBeDefined(); 61 | 62 | expect(googleProvider.sdk.storage).toBeDefined(); 63 | 64 | expect(googleProvider.sdk.logging).toBeDefined(); 65 | 66 | expect(googleProvider.sdk.cloudfunctions).toBeDefined(); 67 | }); 68 | 69 | it('should set the google options', () => { 70 | expect(google._options.headers['User-Agent']) // eslint-disable-line no-underscore-dangle 71 | .toMatch(/Serverless\/.+ Serverless-Google-Provider\/.+ Googleapis\/.+/); 72 | }); 73 | 74 | it('should define the schema of the provider', () => { 75 | expect(serverless.configSchemaHandler.defineProvider).toHaveBeenCalledWith( 76 | 'google', 77 | expect.any(Object) 78 | ); 79 | }); 80 | }); 81 | 82 | describe('#request()', () => { 83 | // NOTE: we're using our own custom services here to make 84 | // the tests SDK independent 85 | let savedSdk; 86 | 87 | beforeEach(() => { 88 | savedSdk = googleProvider.sdk; 89 | googleProvider.sdk = { 90 | service: { 91 | resource: { 92 | method: { 93 | // will be replaced for each individual test 94 | bind: null, 95 | }, 96 | }, 97 | }, 98 | }; 99 | sinon.stub(googleProvider, 'getAuthClient').returns({ 100 | getClient: sinon.stub().resolves(), 101 | }); 102 | sinon.stub(googleProvider, 'isServiceSupported').returns(); 103 | }); 104 | 105 | afterEach(() => { 106 | googleProvider.sdk = savedSdk; 107 | googleProvider.getAuthClient.restore(); 108 | googleProvider.isServiceSupported.restore(); 109 | }); 110 | 111 | it('should perform the given request', () => { 112 | googleProvider.sdk.service.resource.method.bind = () => 113 | sinon.stub().resolves({ data: 'result' }); 114 | 115 | return googleProvider.request('service', 'resource', 'method', {}).then((result) => { 116 | expect(result).toEqual('result'); 117 | }); 118 | }); 119 | 120 | it('should throw a custom error message when the project configuration is wrong', () => { 121 | googleProvider.sdk.service.resource.method.bind = () => 122 | sinon.stub().rejects({ errors: [{ message: 'project 1043443644444' }] }); 123 | 124 | return expect(googleProvider.request('service', 'resource', 'method', {})).rejects.toThrow( 125 | /Incorrect configuration/ 126 | ); 127 | }); 128 | 129 | it('should re-throw other errors', () => { 130 | googleProvider.sdk.service.resource.method.bind = () => 131 | sinon.stub().rejects(new Error('some error message')); 132 | 133 | return expect(googleProvider.request('service', 'resource', 'method', {})).rejects.toThrow( 134 | 'some error message' 135 | ); 136 | }); 137 | }); 138 | 139 | describe('#getAuthClient()', () => { 140 | it('should return a new authClient when using default credentials', () => { 141 | const authClient = googleProvider.getAuthClient(); 142 | 143 | expect(authClient.keyFilename).toEqual(undefined); 144 | expect(authClient).toBeInstanceOf(google.auth.GoogleAuth); 145 | }); 146 | 147 | it('should return a new authClient when using a credentials file', () => { 148 | googleProvider.serverless.service.provider.credentials = '/root/.gcloud/project-1234.json'; 149 | 150 | const authClient = googleProvider.getAuthClient(); 151 | 152 | expect(authClient.keyFilename).toEqual('/root/.gcloud/project-1234.json'); 153 | expect(authClient).toBeInstanceOf(google.auth.GoogleAuth); 154 | }); 155 | 156 | it('should expand tilde characters in credentials file paths', () => { 157 | googleProvider.serverless.service.provider.credentials = '~/.gcloud/project-1234.json'; 158 | 159 | const authClient = googleProvider.getAuthClient(); 160 | 161 | expect(homedirStub.calledOnce).toEqual(true); 162 | expect(authClient.keyFilename).toEqual('/root/.gcloud/project-1234.json'); 163 | expect(authClient).toBeInstanceOf(google.auth.GoogleAuth); 164 | }); 165 | }); 166 | 167 | describe('#isServiceSupported()', () => { 168 | it('should do nothing if service is available', () => { 169 | expect(() => { 170 | googleProvider.isServiceSupported('storage'); 171 | }).not.toThrow(Error); 172 | }); 173 | 174 | it('should throw error if service is not Supported', () => { 175 | expect(() => { 176 | googleProvider.isServiceSupported('unsupported'); 177 | }).toThrow(Error); 178 | }); 179 | }); 180 | 181 | describe('#getRuntime()', () => { 182 | it('should return the runtime of the function if defined', () => { 183 | const functionRuntime = 'functionRuntime'; 184 | expect(googleProvider.getRuntime({ runtime: functionRuntime })).toEqual(functionRuntime); 185 | }); 186 | 187 | it('should return the runtime of the provider if not defined in the function', () => { 188 | expect(googleProvider.getRuntime({})).toEqual(providerRuntime); 189 | }); 190 | 191 | it('should return nodejs10 if neither the runtime of the function nor the one of the provider are defined', () => { 192 | serverless.service.provider.runtime = undefined; 193 | expect(googleProvider.getRuntime({})).toEqual('nodejs10'); 194 | }); 195 | }); 196 | 197 | describe('#getConfiguredEnvironment()', () => { 198 | const functionEnvironment = { 199 | MY_VAR: 'myVarFunctionValue', 200 | FUNCTION_VAR: 'functionVarFunctionValue', 201 | }; 202 | const providerEnvironment = { 203 | MY_VAR: 'myVarProviderValue', 204 | PROVIDER_VAR: 'providerVarProviderValue', 205 | }; 206 | 207 | it('should return the environment of the function if defined', () => { 208 | expect(googleProvider.getConfiguredEnvironment({ environment: functionEnvironment })).toEqual( 209 | functionEnvironment 210 | ); 211 | }); 212 | 213 | it('should return the environment of the provider if defined', () => { 214 | serverless.service.provider.environment = providerEnvironment; 215 | expect(googleProvider.getConfiguredEnvironment({})).toEqual(providerEnvironment); 216 | }); 217 | 218 | it('should return an empty object if neither the environment of the function nor the one of the provider are defined', () => { 219 | expect(googleProvider.getConfiguredEnvironment({})).toEqual({}); 220 | }); 221 | 222 | it('should return the merged environment of the provider and the function. The function override the provider.', () => { 223 | serverless.service.provider.environment = providerEnvironment; 224 | expect(googleProvider.getConfiguredEnvironment({ environment: functionEnvironment })).toEqual( 225 | { 226 | MY_VAR: 'myVarFunctionValue', 227 | FUNCTION_VAR: 'functionVarFunctionValue', 228 | PROVIDER_VAR: 'providerVarProviderValue', 229 | } 230 | ); 231 | }); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [4.6.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v4.5.0...v4.6.0) (2022-05-09) 6 | 7 | ### Features 8 | 9 | - Add support for Node.js 16 and Go 1.16 Runtimes ([#285](https://github.com/serverless/serverless-google-cloudfunctions/issues/285)) ([24e62d8](https://github.com/serverless/serverless-google-cloudfunctions/commit/24e62d8e12208049e2de1991fd30614545994e00)) ([Mark Davydov](https://github.com/markrity)) 10 | - Add support for secrets manager environment variables ([#287](https://github.com/serverless/serverless-google-cloudfunctions/issues/287)) ([4e59429](https://github.com/serverless/serverless-google-cloudfunctions/commit/4e59429ad2857cbc8d95ce70db6b41bed76b67ad)) ([Christopher Chow](https://github.com/christophersjchow)) 11 | - Support `provider.minInstances`([#283](https://github.com/serverless/serverless-google-cloudfunctions/issues/283)) ([66ba98f](https://github.com/serverless/serverless-google-cloudfunctions/commit/66ba98f3307110f8a19b4d5b3f798b71e791d2f5)) ([Jorge Montanez](https://github.com/jmontanezphysna)) 12 | 13 | ## [4.5.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v4.4.0...v4.5.0) (2021-11-25) 14 | 15 | ### Features 16 | 17 | - Recognize `python39` runtime ([#278](https://github.com/serverless/serverless-google-cloudfunctions/issues/278)) ([1dfcab0](https://github.com/serverless/serverless-google-cloudfunctions/commit/1dfcab013cb2bf77b5609c908fcfe617dc5da5eb)) ([Harry Robbins](https://github.com/harryrobbins)) 18 | - Support new Serverless Framework variables resolver ([#280](https://github.com/serverless/serverless-google-cloudfunctions/issues/280)) ([65cf774](https://github.com/serverless/serverless-google-cloudfunctions/commit/65cf774a90af0e673108b5164470f094f9f242a4)) ([Mariusz Nowak](https://github.com/medikoo)) 19 | - Update Cloud Functions locations lit ([#274](https://github.com/serverless/serverless-google-cloudfunctions/issues/274)) ([390bba5](https://github.com/serverless/serverless-google-cloudfunctions/commit/390bba5bee3796969c9798629bd588917759616d)) ([Eric Ho](https://github.com/dhoeric)) 20 | 21 | ## [4.4.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v4.3.0...v4.4.0) (2021-09-03) 22 | 23 | ### Features 24 | 25 | - Support `provider.vpcEgress` configuration ([#271](https://github.com/serverless/serverless-google-cloudfunctions/issues/271)) ([269df15](https://github.com/serverless/serverless-google-cloudfunctions/commit/269df15e77eb51895c497608eaf4265005d83a5d)) ([Federico Rodríguez](https://github.com/fcr1193)) 26 | 27 | ## [4.3.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v4.2.0...v4.3.0) (2021-08-03) 28 | 29 | ### Features 30 | 31 | - Support retry failure policy for event based functions ([#247](https://github.com/serverless/serverless-google-cloudfunctions/issues/247)) ([6ab7711](https://github.com/serverless/serverless-google-cloudfunctions/commit/6ab77112266646bd2b771a91cd9bf30487ab1abd)) ([Flavio Peralta](https://github.com/flaviomp)) 32 | 33 | ## [4.2.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v4.1.0...v4.2.0) (2021-06-17) 34 | 35 | ### Features 36 | 37 | - Add support for http events in invoke local ([#264](https://github.com/serverless/serverless-google-cloudfunctions/issues/264)) ([446d161](https://github.com/serverless/serverless-google-cloudfunctions/commit/446d161a3ddff8e3eaed41af0f9e415726cd23dd)) ([Corentin Doue](https://github.com/CorentinDoue)) 38 | 39 | ## [4.1.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v4.0.0...v4.1.0) (2021-06-07) 40 | 41 | ### Features 42 | 43 | - Add support for invoke local ([#258](https://github.com/serverless/serverless-google-cloudfunctions/issues/258)) ([9e07fed](https://github.com/serverless/serverless-google-cloudfunctions/commit/9e07fedf8049836a45b038ddd2b972526c8aee6a)) ([Corentin Doue](https://github.com/CorentinDoue)) 44 | 45 | ### Bug Fixes 46 | 47 | - CLI option `count` type deprecation warning ([#257](https://github.com/serverless/serverless-google-cloudfunctions/issues/257)) ([8b97064](https://github.com/serverless/serverless-google-cloudfunctions/commit/8b970648f08ee39c1e8d60a373c2c1798c8cde3f)) ([Michael Haglund](https://github.com/hagmic)) 48 | 49 | ## [4.0.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v3.1.1...v4.0.0) (2021-04-12) 50 | 51 | ### ⚠ BREAKING CHANGES 52 | 53 | - Node.js version 10 or later is required (dropped support for v6 and v8) 54 | - Default runtime has been changed to `nodejs10` 55 | 56 | ### Features 57 | 58 | - Add schema validation ([#252](https://github.com/serverless/serverless-google-cloudfunctions/issues/252)) ([c332d3d](https://github.com/serverless/serverless-google-cloudfunctions/commit/c332d3d909b6984395cee003f4a139d5aa9e0729)) ([Corentin Doue](https://github.com/CorentinDoue)) 59 | 60 | ### Bug Fixes 61 | 62 | - Authentication and Logs ([#244](https://github.com/serverless/serverless-google-cloudfunctions/issues/244)) ([8a34b88](https://github.com/serverless/serverless-google-cloudfunctions/commit/8a34b88250e4cacda46f34024ba482b2051deac9)) ([Rothanak So](https://github.com/rothso) & [Mahamed](https://github.com/upodroid)) 63 | 64 | ## [3.1.1](https://github.com/serverless/serverless-google-cloudfunctions/compare/v3.1.0...v3.1.1) (2020-08-27) 65 | 66 | ### Bug Fixes 67 | 68 | - Deploy bucket in configured location ([#227](https://github.com/serverless/serverless-google-cloudfunctions/issues/227)) ([86494b4](https://github.com/serverless/serverless-google-cloudfunctions/commit/86494b4ef88ac54ccb0d29014a2bb3806c055ea9)) 69 | - Fix deployment status when the function name is specified ([#225](https://github.com/serverless/serverless-google-cloudfunctions/issues/225)) ([251e1cf](https://github.com/serverless/serverless-google-cloudfunctions/commit/251e1cf61c04a0d28509eea08b603a91f6d73440)) 70 | 71 | ## [3.1.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v3.0.0...v3.1.0) (2020-05-11) 72 | 73 | ### Features 74 | 75 | - Support serviceAccount option for deployment ([#215](https://github.com/serverless/serverless-google-cloudfunctions/issues/215)) ([1164495](https://github.com/serverless/serverless-google-cloudfunctions/commit/11644956771bc64dc0259b6316502f104fadf1ea)) 76 | 77 | ### Bug Fixes 78 | 79 | - Fix function name resolution on invoke ([#216](https://github.com/serverless/serverless-google-cloudfunctions/issues/216)) ([86d40aa](https://github.com/serverless/serverless-google-cloudfunctions/commit/86d40aa3ab07e512eb7e6a92424db399335a8201)) 80 | 81 | ## [3.0.0](https://github.com/serverless/serverless-google-cloudfunctions/compare/v2.4.3...v3.0.0) (2020-04-01) 82 | 83 | ### ⚠ BREAKING CHANGES 84 | 85 | - Drop support for Node.js v6 86 | - Services deployed with v1 Beta cannot be updated with v1. 87 | 88 | Co-authored-by: Jeremy Minhua Bao (US - ADVS) 89 | Co-authored-by: zxhaaa 90 | Co-authored-by: Peachey_Chen 91 | 92 | ### Features 93 | 94 | - Switch from CloudFunctions v1 beta to v1 ([#206](https://github.com/serverless/serverless-google-cloudfunctions/issues/206)) ([482ee0e](https://github.com/serverless/serverless-google-cloudfunctions/commit/482ee0e63a1f72dec8cce6c80dfe66ab406671ae)) 95 | - Upgrade googleapis to latest version ([#209](https://github.com/serverless/serverless-google-cloudfunctions/issues/209)) ([ab0d8ba](https://github.com/serverless/serverless-google-cloudfunctions/commit/ab0d8ba802d5999c9848232e836651c577a9f0cd)) 96 | 97 | ### [2.4.3](https://github.com/serverless/serverless-google-cloudfunctions/compare/v2.4.2...v2.4.3) (2020-04-01) 98 | 99 | ### Bug Fixes 100 | 101 | - Revert breaking switch from v1 beta to v1 CloudFunctions ([#207](https://github.com/serverless/serverless-google-cloudfunctions/issues/207)) ([fc1dbe2](https://github.com/serverless/serverless-google-cloudfunctions/commit/fc1dbe28be4b1dab0abe4216993c63c543e547eb)) 102 | 103 | ### [2.4.2](https://github.com/serverless/serverless-google-cloudfunctions/compare/v2.4.1...v2.4.2) (2020-03-25) 104 | 105 | - Ensure to rely internally on v1 and not v1 Beta CloudFunctions API ([#165](https://github.com/serverless/serverless-google-cloudfunctions/issues/165)) ([Eisuke Kuwahata](https://github.com/mather)) 106 | 107 | ### [2.4.1](https://github.com/serverless/serverless-google-cloudfunctions/compare/v2.4.0...v2.4.1) (2020-02-28) 108 | 109 | ### Bug Fixes 110 | 111 | - Bring back Node.js v6 support ([#197](https://github.com/serverless/serverless-google-cloudfunctions/issues/197)) ([f3b9881](https://github.com/serverless/serverless-google-cloudfunctions/commit/f3b9881086ff39416861c7b0549a4ded14fe7268)) ([Mariusz Nowak](https://github.com/medikoo)) 112 | - Fix handling of `maxInstances` setting ([#199](https://github.com/serverless/serverless-google-cloudfunctions/issues/199)) ([4ea8418](https://github.com/serverless/serverless-google-cloudfunctions/commit/4ea841879edf8605fe5b38668f6d1fb875347aae)) ([holmerjohn](https://github.com/holmerjohn)) 113 | --------------------------------------------------------------------------------