From 14badeceef8f8d066532e90eccfabb94933a5229 Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Wed, 31 Oct 2018 14:30:08 -0700 Subject: [PATCH 01/11] NodeJS 6 code sample for Cloud Function to start/stop GCE instances --- functions/scheduleinstance/index.js | 143 +++++++++ functions/scheduleinstance/package.json | 26 ++ functions/scheduleinstance/test/index.test.js | 279 ++++++++++++++++++ 3 files changed, 448 insertions(+) create mode 100644 functions/scheduleinstance/index.js create mode 100644 functions/scheduleinstance/package.json create mode 100644 functions/scheduleinstance/test/index.test.js diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js new file mode 100644 index 0000000000..dc9bd80c3a --- /dev/null +++ b/functions/scheduleinstance/index.js @@ -0,0 +1,143 @@ +// [START functions_start_instance_http] +// [START functions_stop_instance_http] +const Compute = require('@google-cloud/compute'); +const compute = new Compute(); + +// [END functions_stop_instance_http] +/** + * Starts a Compute Engine instance. + * + * Expects an HTTP POST request with a request body containing the following + * attributes: + * zone - the GCP zone the instance is located in. + * instance - the name of the instance. + * + * @param {!object} req Cloud Function HTTP request data. + * @param {!object} res Cloud Function HTTP response data. + * @returns {!object} Cloud Function response data with status code and message. + */ +exports.startInstance = (req, res) => { + try { + const reqBody = _validateReqBody(_parseReqBody(_validateReq(req))); + compute.zone(reqBody.zone) + .vm(reqBody.instance) + .start() + .then(data => { + // Operation pending. + const operation = data[0]; + return operation.promise(); + }) + .then(() => { + // Operation complete. Instance successfully started. + const message = 'Successfully started instance ' + reqBody.instance; + console.log(message); + res.status(200).send(message); + }) + .catch(err => { + console.log(err); + res.status(500).send({error: err.message}); + }); + } catch (err) { + console.log(err); + res.status(500).send({error: err.message}); + } finally { + return res; + } +}; +// [END functions_start_instance_http] + +// [START functions_stop_instance_http] +/** + * Stops a Compute Engine instance. + * + * Expects an HTTP POST request with a request body containing the following + * attributes: + * zone - the GCP zone the instance is located in. + * instance - the name of the instance. + * + * @param {!object} req Cloud Function HTTP request data. + * @param {!object} res Cloud Function HTTP response data. + * @returns {!object} Cloud Function response data with status code and message. + */ +exports.stopInstance = (req, res) => { + try { + const reqBody = _validateReqBody(_parseReqBody(_validateReq(req))); + compute.zone(reqBody.zone) + .vm(reqBody.instance) + .stop() + .then(data => { + // Operation pending. + const operation = data[0]; + return operation.promise(); + }) + .then(() => { + // Operation complete. Instance successfully stopped. + const message = 'Successfully stopped instance ' + reqBody.instance; + console.log(message); + res.status(200).send(message); + }) + .catch(err => { + console.log(err); + res.status(500).send({error: err.message}); + }); + } catch (err) { + console.log(err); + res.status(500).send({error: err.message}); + } finally { + return res; + } +}; +// [START functions_start_instance_http] + +/** + * Parses the request body attributes of an HTTP request based on content-type. + * + * @param {!object} req a Cloud Functions HTTP request object. + * @returns {!object} an object with attributes matching the HTTP request body. + */ +function _parseReqBody(req) { + const contentType = req.get('content-type'); + if (contentType === 'application/json') { + // Request.body automatically parsed as an object. + return req.body; + } else if (contentType === 'application/octet-stream') { + // Convert buffer to a string and parse as JSON string. + return JSON.parse(req.body.toString()); + } else { + throw new Error('Unsupported HTTP content-type ' + req.get('content-type') + + '; use application/json or application/octet-stream'); + } +} + +/** + * Validates that a request body contains the expected fields. + * + * @param {!object} reqBody the request body to validate. + * @returns {!object} the request body object. + */ +function _validateReqBody(reqBody) { + if (!reqBody.zone) { + throw new Error(`Attribute 'zone' missing from POST request`); + } else if (!reqBody.instance) { + throw new Error(`Attribute 'instance' missing from POST request`); + } + return reqBody; +} + +/** + * Validates that a HTTP request contains the expected fields. + * + * @param {!object} req the request to validate. + * @returns {!object} the request object. + */ +function _validateReq(req) { + if (req.method !== 'POST') { + throw new Error( + 'Unsupported HTTP method ' + req.method + '; use method POST'); + } else if (typeof req.get('content-type') === 'undefined') { + throw new Error('HTTP content-type missing'); + } + return req; +} +// [END functions_start_instance_http] +// [END functions_stop_instance_http] diff --git a/functions/scheduleinstance/package.json b/functions/scheduleinstance/package.json new file mode 100644 index 0000000000..7a0a73ffdc --- /dev/null +++ b/functions/scheduleinstance/package.json @@ -0,0 +1,26 @@ +{ + "name": "schedule-instance", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=6.0" + }, + "scripts": { + "test": "ava -T 20s --verbose test/*.test.js" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "2.2.1", + "ava": "0.25.0", + "proxyquire": "2.0.0", + "sinon": "4.4.2" + }, + "dependencies": { + "@google-cloud/compute": "^0.10.0" + } +} diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js new file mode 100644 index 0000000000..143d91dea6 --- /dev/null +++ b/functions/scheduleinstance/test/index.test.js @@ -0,0 +1,279 @@ +'use strict'; + +const Buffer = require('safe-buffer').Buffer; +const proxyquire = require(`proxyquire`).noCallThru(); +const sinon = require(`sinon`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +function getSample () { + const requestPromise = sinon.stub().returns(new Promise((resolve) => resolve(`request sent`))); + + return { + program: proxyquire(`../`, { + 'request-promise': requestPromise + }), + mocks: { + requestPromise: requestPromise + } + }; +} + +function getMocks () { + const req = { + headers: {}, + body: {}, + get: function (header) { + return this.headers[header]; + } + }; + sinon.spy(req, `get`); + + const res = { + set: sinon.stub().returnsThis(), + send: function (message) { + this.message = message; + return this; + }, + json: sinon.stub().returnsThis(), + end: sinon.stub().returnsThis(), + status: function (statusCode) { + this.statusCode = statusCode; + return this; + } + }; + sinon.spy(res, 'status'); + sinon.spy(res, 'send'); + + return { + req: req, + res: res + }; +} + +test.beforeEach(tools.stubConsole); +test.afterEach.always(tools.restoreConsole); + +test(`startInstance: should accept application/json`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + sample.program.startInstance(mocks.req, mocks.res); + + sample.mocks.requestPromise() + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); +}); + +test(`startInstance: should accept application/octect-stream`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/octet-stream`; + mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + sample.program.startInstance(mocks.req, mocks.res); + + sample.mocks.requestPromise() + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); +}); + +test(`startInstance: should fail missing HTTP request method`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.startInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method undefined; use method POST'}); +}); + +test(`startInstance: should reject HTTP GET request`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `GET`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.startInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method GET; use method POST'}); +}); + +test(`startInstance: should fail missing content-type header`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.startInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'HTTP content-type missing'}); +}); + +test(`startInstance: should reject unsupported HTTP content-type`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `text/plain`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.startInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); +}); + +test(`startInstance: should fail with missing 'zone' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"instance":"test-instance"}; + sample.program.startInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'zone' missing from POST request`}); +}); + +test(`startInstance: should fail with missing 'instance' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"zone":"test-zone"}; + sample.program.startInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'instance' missing from POST request`}); +}); + +test(`stopInstance: should accept application/json`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + sample.program.stopInstance(mocks.req, mocks.res); + + sample.mocks.requestPromise() + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); +}); + +test(`stopInstance: should accept application/octect-stream`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/octet-stream`; + mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + sample.program.stopInstance(mocks.req, mocks.res); + + sample.mocks.requestPromise() + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); +}); + +test(`stopInstance: should fail missing HTTP request method`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.stopInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method undefined; use method POST'}); +}); + +test(`stopInstance: should reject HTTP GET request`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `GET`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.stopInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method GET; use method POST'}); +}); + +test(`stopInstance: should fail missing content-type header`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.stopInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'HTTP content-type missing'}); +}); + +test(`stopInstance: should reject unsupported HTTP content-type`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `text/plain`; + mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + sample.program.stopInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); +}); + +test(`stopInstance: should fail with missing 'zone' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"instance":"test-instance"}; + sample.program.stopInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'zone' missing from POST request`}); +}); + +test(`stopInstance: should fail with missing 'instance' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {"zone":"test-zone"}; + sample.program.stopInstance(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 500); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'instance' missing from POST request`}); +}); From 3bb9df7efe8c0e4666c058bfe8a1ea81fe6f450d Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Wed, 31 Oct 2018 14:37:31 -0700 Subject: [PATCH 02/11] start/stop GCE instances Cloud Function updated with 400 status codes --- functions/scheduleinstance/index.js | 4 ++-- functions/scheduleinstance/test/index.test.js | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index dc9bd80c3a..f607223d76 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -39,7 +39,7 @@ exports.startInstance = (req, res) => { }); } catch (err) { console.log(err); - res.status(500).send({error: err.message}); + res.status(400).send({error: err.message}); } finally { return res; } @@ -82,7 +82,7 @@ exports.stopInstance = (req, res) => { }); } catch (err) { console.log(err); - res.status(500).send({error: err.message}); + res.status(400).send({error: err.message}); } finally { return res; } diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 143d91dea6..1d40629ae8 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -92,7 +92,7 @@ test(`startInstance: should fail missing HTTP request method`, async (t) => { sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method undefined; use method POST'}); }); @@ -106,7 +106,7 @@ test(`startInstance: should reject HTTP GET request`, async (t) => { sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method GET; use method POST'}); }); @@ -119,7 +119,7 @@ test(`startInstance: should fail missing content-type header`, async (t) => { sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'HTTP content-type missing'}); }); @@ -133,7 +133,7 @@ test(`startInstance: should reject unsupported HTTP content-type`, async (t) => sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); }); @@ -147,7 +147,7 @@ test(`startInstance: should fail with missing 'zone' attribute`, async (t) => { sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'zone' missing from POST request`}); }); @@ -161,7 +161,7 @@ test(`startInstance: should fail with missing 'instance' attribute`, async (t) = sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'instance' missing from POST request`}); }); @@ -204,7 +204,7 @@ test(`stopInstance: should fail missing HTTP request method`, async (t) => { sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method undefined; use method POST'}); }); @@ -218,7 +218,7 @@ test(`stopInstance: should reject HTTP GET request`, async (t) => { sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method GET; use method POST'}); }); @@ -231,7 +231,7 @@ test(`stopInstance: should fail missing content-type header`, async (t) => { sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'HTTP content-type missing'}); }); @@ -245,7 +245,7 @@ test(`stopInstance: should reject unsupported HTTP content-type`, async (t) => { sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); }); @@ -259,7 +259,7 @@ test(`stopInstance: should fail with missing 'zone' attribute`, async (t) => { sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'zone' missing from POST request`}); }); @@ -273,7 +273,7 @@ test(`stopInstance: should fail with missing 'instance' attribute`, async (t) => sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); - t.is(mocks.res.status.firstCall.args[0], 500); + t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'instance' missing from POST request`}); }); From 1c3740e61546a0833fdafc43d4e685c747958cad Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Wed, 31 Oct 2018 14:48:51 -0700 Subject: [PATCH 03/11] start/stop GCE instances Cloud Function update octet-stream tests --- functions/scheduleinstance/test/index.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 1d40629ae8..4b9d55c337 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -74,7 +74,7 @@ test(`startInstance: should accept application/octect-stream`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/octet-stream`; - mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + mocks.req.body = Buffer.from(`{'zone':'test-zone', 'instance':'test-instance'}`); sample.program.startInstance(mocks.req, mocks.res); sample.mocks.requestPromise() @@ -186,7 +186,7 @@ test(`stopInstance: should accept application/octect-stream`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/octet-stream`; - mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + mocks.req.body = Buffer.from(`{'zone':'test-zone', 'instance':'test-instance'}`); sample.program.stopInstance(mocks.req, mocks.res); sample.mocks.requestPromise() From e450d819c994aa18f815ac4455ad2b569c91185c Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Wed, 31 Oct 2018 15:07:23 -0700 Subject: [PATCH 04/11] start/stop GCE instances Cloud Function linting updates --- functions/scheduleinstance/index.js | 84 +++++++++--------- functions/scheduleinstance/test/index.test.js | 88 +++++++++---------- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index f607223d76..50f28be94a 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -20,29 +20,28 @@ exports.startInstance = (req, res) => { try { const reqBody = _validateReqBody(_parseReqBody(_validateReq(req))); compute.zone(reqBody.zone) - .vm(reqBody.instance) - .start() - .then(data => { - // Operation pending. - const operation = data[0]; - return operation.promise(); - }) - .then(() => { - // Operation complete. Instance successfully started. - const message = 'Successfully started instance ' + reqBody.instance; - console.log(message); - res.status(200).send(message); - }) - .catch(err => { - console.log(err); - res.status(500).send({error: err.message}); - }); + .vm(reqBody.instance) + .start() + .then(data => { + // Operation pending. + const operation = data[0]; + return operation.promise(); + }) + .then(() => { + // Operation complete. Instance successfully started. + const message = 'Successfully started instance ' + reqBody.instance; + console.log(message); + res.status(200).send(message); + }) + .catch(err => { + console.log(err); + res.status(500).send({error: err.message}); + }); } catch (err) { console.log(err); res.status(400).send({error: err.message}); - } finally { - return res; } + return res; }; // [END functions_start_instance_http] @@ -63,29 +62,28 @@ exports.stopInstance = (req, res) => { try { const reqBody = _validateReqBody(_parseReqBody(_validateReq(req))); compute.zone(reqBody.zone) - .vm(reqBody.instance) - .stop() - .then(data => { - // Operation pending. - const operation = data[0]; - return operation.promise(); - }) - .then(() => { - // Operation complete. Instance successfully stopped. - const message = 'Successfully stopped instance ' + reqBody.instance; - console.log(message); - res.status(200).send(message); - }) - .catch(err => { - console.log(err); - res.status(500).send({error: err.message}); - }); + .vm(reqBody.instance) + .stop() + .then(data => { + // Operation pending. + const operation = data[0]; + return operation.promise(); + }) + .then(() => { + // Operation complete. Instance successfully stopped. + const message = 'Successfully stopped instance ' + reqBody.instance; + console.log(message); + res.status(200).send(message); + }) + .catch(err => { + console.log(err); + res.status(500).send({error: err.message}); + }); } catch (err) { console.log(err); res.status(400).send({error: err.message}); - } finally { - return res; } + return res; }; // [START functions_start_instance_http] @@ -95,7 +93,7 @@ exports.stopInstance = (req, res) => { * @param {!object} req a Cloud Functions HTTP request object. * @returns {!object} an object with attributes matching the HTTP request body. */ -function _parseReqBody(req) { +function _parseReqBody (req) { const contentType = req.get('content-type'); if (contentType === 'application/json') { // Request.body automatically parsed as an object. @@ -115,7 +113,7 @@ function _parseReqBody(req) { * @param {!object} reqBody the request body to validate. * @returns {!object} the request body object. */ -function _validateReqBody(reqBody) { +function _validateReqBody (reqBody) { if (!reqBody.zone) { throw new Error(`Attribute 'zone' missing from POST request`); } else if (!reqBody.instance) { @@ -130,10 +128,10 @@ function _validateReqBody(reqBody) { * @param {!object} req the request to validate. * @returns {!object} the request object. */ -function _validateReq(req) { +function _validateReq (req) { if (req.method !== 'POST') { - throw new Error( - 'Unsupported HTTP method ' + req.method + '; use method POST'); + throw new Error('Unsupported HTTP method ' + req.method + + '; use method POST'); } else if (typeof req.get('content-type') === 'undefined') { throw new Error('HTTP content-type missing'); } diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 4b9d55c337..d550600b38 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -59,14 +59,14 @@ test(`startInstance: should accept application/json`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + mocks.req.body = {zone: `test-zone`, instance: `test-instance`}; sample.program.startInstance(mocks.req, mocks.res); sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); }); test(`startInstance: should accept application/octect-stream`, async (t) => { @@ -74,27 +74,27 @@ test(`startInstance: should accept application/octect-stream`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/octet-stream`; - mocks.req.body = Buffer.from(`{'zone':'test-zone', 'instance':'test-instance'}`); + mocks.req.body = Buffer.from(`{'zone': 'test-zone', 'instance': 'test-instance'}`); sample.program.startInstance(mocks.req, mocks.res); sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); }); test(`startInstance: should fail missing HTTP request method`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method undefined; use method POST'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method undefined; use method POST'}); }); test(`startInstance: should reject HTTP GET request`, async (t) => { @@ -102,26 +102,26 @@ test(`startInstance: should reject HTTP GET request`, async (t) => { const sample = getSample(); mocks.req.method = `GET`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method GET; use method POST'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method GET; use method POST'}); }); test(`startInstance: should fail missing content-type header`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'HTTP content-type missing'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'HTTP content-type missing'}); }); test(`startInstance: should reject unsupported HTTP content-type`, async (t) => { @@ -129,13 +129,13 @@ test(`startInstance: should reject unsupported HTTP content-type`, async (t) => const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `text/plain`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); }); test(`startInstance: should fail with missing 'zone' attribute`, async (t) => { @@ -143,13 +143,13 @@ test(`startInstance: should fail with missing 'zone' attribute`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"instance":"test-instance"}; + mocks.req.body = {'instance': 'test-instance'}; sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'zone' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from POST request`}); }); test(`startInstance: should fail with missing 'instance' attribute`, async (t) => { @@ -157,13 +157,13 @@ test(`startInstance: should fail with missing 'instance' attribute`, async (t) = const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"zone":"test-zone"}; + mocks.req.body = {'zone': 'test-zone'}; sample.program.startInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'instance' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'instance' missing from POST request`}); }); test(`stopInstance: should accept application/json`, async (t) => { @@ -171,14 +171,14 @@ test(`stopInstance: should accept application/json`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {zone:`test-zone`, instance:`test-instance`}; + mocks.req.body = {zone: `test-zone`, instance: `test-instance`}; sample.program.stopInstance(mocks.req, mocks.res); sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); }); test(`stopInstance: should accept application/octect-stream`, async (t) => { @@ -186,27 +186,27 @@ test(`stopInstance: should accept application/octect-stream`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/octet-stream`; - mocks.req.body = Buffer.from(`{'zone':'test-zone', 'instance':'test-instance'}`); + mocks.req.body = Buffer.from(`{'zone': 'test-zone', 'instance': 'test-instance'}`); sample.program.stopInstance(mocks.req, mocks.res); sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); }); test(`stopInstance: should fail missing HTTP request method`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method undefined; use method POST'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method undefined; use method POST'}); }); test(`stopInstance: should reject HTTP GET request`, async (t) => { @@ -214,26 +214,26 @@ test(`stopInstance: should reject HTTP GET request`, async (t) => { const sample = getSample(); mocks.req.method = `GET`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP method GET; use method POST'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method GET; use method POST'}); }); test(`stopInstance: should fail missing content-type header`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'HTTP content-type missing'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'HTTP content-type missing'}); }); test(`stopInstance: should reject unsupported HTTP content-type`, async (t) => { @@ -241,13 +241,13 @@ test(`stopInstance: should reject unsupported HTTP content-type`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `text/plain`; - mocks.req.body = {"zone":"test-zone", "instance":"test-instance"}; + mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); }); test(`stopInstance: should fail with missing 'zone' attribute`, async (t) => { @@ -255,13 +255,13 @@ test(`stopInstance: should fail with missing 'zone' attribute`, async (t) => { const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"instance":"test-instance"}; + mocks.req.body = {'instance': 'test-instance'}; sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'zone' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from POST request`}); }); test(`stopInstance: should fail with missing 'instance' attribute`, async (t) => { @@ -269,11 +269,11 @@ test(`stopInstance: should fail with missing 'instance' attribute`, async (t) => const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; - mocks.req.body = {"zone":"test-zone"}; + mocks.req.body = {'zone': 'test-zone'}; sample.program.stopInstance(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error:`Attribute 'instance' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'instance' missing from POST request`}); }); From be15496dea91b4cf9811e1639ddff14617b055d8 Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Wed, 31 Oct 2018 15:13:18 -0700 Subject: [PATCH 05/11] start/stop GCE instances Cloud Function package.json updates --- functions/scheduleinstance/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/functions/scheduleinstance/package.json b/functions/scheduleinstance/package.json index 7a0a73ffdc..8a5b57dd86 100644 --- a/functions/scheduleinstance/package.json +++ b/functions/scheduleinstance/package.json @@ -12,6 +12,8 @@ "node": ">=6.0" }, "scripts": { + "lint": "repo-tools lint", + "pretest": "npm run lint", "test": "ava -T 20s --verbose test/*.test.js" }, "devDependencies": { @@ -21,6 +23,7 @@ "sinon": "4.4.2" }, "dependencies": { - "@google-cloud/compute": "^0.10.0" + "@google-cloud/compute": "^0.10.0", + "safe-buffer": "5.1.1" } } From 49d9c542b30040708d2175e5217be567e0cf22ec Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Thu, 1 Nov 2018 15:11:58 -0700 Subject: [PATCH 06/11] Add .kokoro build file for functions/scheduleinstance --- .kokoro/functions/scheduleinstance.cfg | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .kokoro/functions/scheduleinstance.cfg diff --git a/.kokoro/functions/scheduleinstance.cfg b/.kokoro/functions/scheduleinstance.cfg new file mode 100644 index 0000000000..ca23c97712 --- /dev/null +++ b/.kokoro/functions/scheduleinstance.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Set the folder in which the tests are run +env_vars: { + key: "PROJECT" + value: "functions/scheduleinstance" +} + +# Tell the trampoline which build file to use. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/nodejs-docs-samples/.kokoro/build.sh" +} From 29c123f0b549ee0b12c31c5232fccc6cda4c1ebb Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Fri, 2 Nov 2018 11:10:24 -0700 Subject: [PATCH 07/11] Added Apache license text to top of source files --- functions/scheduleinstance/index.js | 15 +++++++++++++++ functions/scheduleinstance/test/index.test.js | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index 50f28be94a..ab05cae493 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -1,3 +1,18 @@ +/** + * Copyright 2018, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // [START functions_start_instance_http] // [START functions_stop_instance_http] const Compute = require('@google-cloud/compute'); diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index d550600b38..8085e73d8a 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -1,3 +1,19 @@ + +/** + * Copyright 2018, Google, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use strict'; const Buffer = require('safe-buffer').Buffer; From d5faf183cd8e85d3b4d66c47a71b83dc80794d73 Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Fri, 2 Nov 2018 11:10:49 -0700 Subject: [PATCH 08/11] Added README file --- functions/scheduleinstance/README.md | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 functions/scheduleinstance/README.md diff --git a/functions/scheduleinstance/README.md b/functions/scheduleinstance/README.md new file mode 100644 index 0000000000..01132b6b77 --- /dev/null +++ b/functions/scheduleinstance/README.md @@ -0,0 +1,31 @@ +Google Cloud Platform logo + +# Google Cloud Functions - Scheduling GCE Instances sample + +## Deploy and run the sample + +See the [Scheduling Instances with Cloud Scheduler tutorial][tutorial]. + +[tutorial]: https://cloud.google.com/scheduler/docs/scheduling-instances-with-cloud-scheduler + +## Run the tests + +1. Read and follow the [prerequisites](../../#how-to-run-the-tests). + +1. Install dependencies: + + npm install + +1. Run the tests: + + npm test + +## Additional resources + +* [Cloud Scheduler documentation][docs] +* [HTTP Cloud Functions documentation][http_docs] +* [HTTP Cloud Functions tutorial][http_tutorial] + +[docs]: https://cloud.google.com/scheduler/docs/ +[http_docs]: https://cloud.google.com/functions/docs/writing/http +[http_tutorial]: https://cloud.google.com/functions/docs/tutorials/http From df6f89fc5db9a7870233b534e9aecf3119632396 Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Mon, 5 Nov 2018 13:08:08 -0800 Subject: [PATCH 09/11] Add PubSub functions to scheduleinstance function --- functions/scheduleinstance/index.js | 158 ++++++++++--- functions/scheduleinstance/test/index.test.js | 208 ++++++++++++++---- 2 files changed, 295 insertions(+), 71 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index ab05cae493..a64127c42c 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -15,15 +15,19 @@ // [START functions_start_instance_http] // [START functions_stop_instance_http] +// [START functions_start_instance_pubsub] +// [START functions_stop_instance_pubsub] const Compute = require('@google-cloud/compute'); const compute = new Compute(); // [END functions_stop_instance_http] +// [END functions_start_instance_pubsub] +// [END functions_stop_instance_pubsub] /** * Starts a Compute Engine instance. * - * Expects an HTTP POST request with a request body containing the following - * attributes: + * Expects an HTTP POST request with a JSON-formatted request body containing + * the following attributes: * zone - the GCP zone the instance is located in. * instance - the name of the instance. * @@ -31,11 +35,11 @@ const compute = new Compute(); * @param {!object} res Cloud Function HTTP response data. * @returns {!object} Cloud Function response data with status code and message. */ -exports.startInstance = (req, res) => { +exports.startInstanceHttp = (req, res) => { try { - const reqBody = _validateReqBody(_parseReqBody(_validateReq(req))); - compute.zone(reqBody.zone) - .vm(reqBody.instance) + const payload = _validatePayload(_parseHttpPayload(_validateHttpReq(req))); + compute.zone(payload.zone) + .vm(payload.instance) .start() .then(data => { // Operation pending. @@ -44,7 +48,7 @@ exports.startInstance = (req, res) => { }) .then(() => { // Operation complete. Instance successfully started. - const message = 'Successfully started instance ' + reqBody.instance; + const message = 'Successfully started instance ' + payload.instance; console.log(message); res.status(200).send(message); }) @@ -58,14 +62,14 @@ exports.startInstance = (req, res) => { } return res; }; -// [END functions_start_instance_http] +// [END functions_start_instance_http] // [START functions_stop_instance_http] /** * Stops a Compute Engine instance. * - * Expects an HTTP POST request with a request body containing the following - * attributes: + * Expects an HTTP POST request with a JSON-formatted request body containing + * the following attributes: * zone - the GCP zone the instance is located in. * instance - the name of the instance. * @@ -73,11 +77,11 @@ exports.startInstance = (req, res) => { * @param {!object} res Cloud Function HTTP response data. * @returns {!object} Cloud Function response data with status code and message. */ -exports.stopInstance = (req, res) => { +exports.stopInstanceHttp = (req, res) => { try { - const reqBody = _validateReqBody(_parseReqBody(_validateReq(req))); - compute.zone(reqBody.zone) - .vm(reqBody.instance) + const payload = _validatePayload(_parseHttpPayload(_validateHttpReq(req))); + compute.zone(payload.zone) + .vm(payload.instance) .stop() .then(data => { // Operation pending. @@ -86,7 +90,7 @@ exports.stopInstance = (req, res) => { }) .then(() => { // Operation complete. Instance successfully stopped. - const message = 'Successfully stopped instance ' + reqBody.instance; + const message = 'Successfully stopped instance ' + payload.instance; console.log(message); res.status(200).send(message); }) @@ -100,15 +104,116 @@ exports.stopInstance = (req, res) => { } return res; }; + +// [END functions_stop_instance_http] +// [START functions_start_instance_pubsub] +/** + * Starts a Compute Engine instance. + * + * Expects a PubSub message with JSON-formatted event data containing the + * following attributes: + * zone - the GCP zone the instance is located in. + * instance - the name of the instance. + * + * @param {!object} event Cloud Function PubSub message event. + * @param {!object} callback Cloud Function PubSub callback indicating completion. + */ +exports.startInstancePubSub = (event, callback) => { + try { + const pubsubMessage = event.data; + const payload = _validatePayload(JSON.parse(Buffer.from(pubsubMessage.data, 'base64').toString())); + compute.zone(payload.zone) + .vm(payload.instance) + .start() + .then(data => { + // Operation pending. + const operation = data[0]; + return operation.promise(); + }) + .then(() => { + // Operation complete. Instance successfully started. + const message = 'Successfully started instance ' + payload.instance; + console.log(message); + callback(null, message); + }) + .catch(err => { + console.log(err); + callback(err); + }); + } catch (err) { + console.log(err); + callback(err); + } +}; + +// [END functions_start_instance_pubsub] +// [START functions_stop_instance_pubsub] +/** + * Stops a Compute Engine instance. + * + * Expects a PubSub message with JSON-formatted event data containing the + * following attributes: + * zone - the GCP zone the instance is located in. + * instance - the name of the instance. + * + * @param {!object} event Cloud Function PubSub message event. + * @param {!object} callback Cloud Function PubSub callback indicating completion. + */ +exports.stopInstancePubSub = (event, callback) => { + try { + const pubsubMessage = event.data; + const payload = _validatePayload(JSON.parse(Buffer.from(pubsubMessage.data, 'base64').toString())); + compute.zone(payload.zone) + .vm(payload.instance) + .stop() + .then(data => { + // Operation pending. + const operation = data[0]; + return operation.promise(); + }) + .then(() => { + // Operation complete. Instance successfully stopped. + const message = 'Successfully stopped instance ' + payload.instance; + console.log(message); + callback(null, message); + }) + .catch(err => { + console.log(err); + callback(err); + }); + } catch (err) { + console.log(err); + callback(err); + } +}; + // [START functions_start_instance_http] +// [START functions_stop_instance_http] +// [START functions_start_instance_pubsub] +/** + * Validates that a request payload contains the expected fields. + * + * @param {!object} payload the request payload to validate. + * @returns {!object} the payload object. + */ +function _validatePayload (payload) { + if (!payload.zone) { + throw new Error(`Attribute 'zone' missing from payload`); + } else if (!payload.instance) { + throw new Error(`Attribute 'instance' missing from payload`); + } + return payload; +} +// [END functions_start_instance_pubsub] +// [END functions_stop_instance_pubsub] /** - * Parses the request body attributes of an HTTP request based on content-type. + * Parses the request payload of an HTTP request based on content-type. * * @param {!object} req a Cloud Functions HTTP request object. - * @returns {!object} an object with attributes matching the HTTP request body. + * @returns {!object} an object with attributes matching the request payload. */ -function _parseReqBody (req) { +function _parseHttpPayload (req) { const contentType = req.get('content-type'); if (contentType === 'application/json') { // Request.body automatically parsed as an object. @@ -122,28 +227,13 @@ function _parseReqBody (req) { } } -/** - * Validates that a request body contains the expected fields. - * - * @param {!object} reqBody the request body to validate. - * @returns {!object} the request body object. - */ -function _validateReqBody (reqBody) { - if (!reqBody.zone) { - throw new Error(`Attribute 'zone' missing from POST request`); - } else if (!reqBody.instance) { - throw new Error(`Attribute 'instance' missing from POST request`); - } - return reqBody; -} - /** * Validates that a HTTP request contains the expected fields. * * @param {!object} req the request to validate. * @returns {!object} the request object. */ -function _validateReq (req) { +function _validateHttpReq (req) { if (req.method !== 'POST') { throw new Error('Unsupported HTTP method ' + req.method + '; use method POST'); diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 8085e73d8a..d1ea4fbf5c 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -61,22 +61,34 @@ function getMocks () { sinon.spy(res, 'status'); sinon.spy(res, 'send'); + const event = { + data: { + data: {} + } + }; + + const callback = sinon.spy(); + return { req: req, - res: res + res: res, + event: event, + callback: callback }; } test.beforeEach(tools.stubConsole); test.afterEach.always(tools.restoreConsole); -test(`startInstance: should accept application/json`, async (t) => { +/////////////////////////// startInstanceHttp ////////////////////////////// + +test(`startInstanceHttp: should accept application/json`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {zone: `test-zone`, instance: `test-instance`}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); sample.mocks.requestPromise() .then((data) => { @@ -85,13 +97,13 @@ test(`startInstance: should accept application/json`, async (t) => { }); }); -test(`startInstance: should accept application/octect-stream`, async (t) => { +test(`startInstanceHttp: should accept application/octect-stream`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/octet-stream`; mocks.req.body = Buffer.from(`{'zone': 'test-zone', 'instance': 'test-instance'}`); - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); sample.mocks.requestPromise() .then((data) => { @@ -100,12 +112,12 @@ test(`startInstance: should accept application/octect-stream`, async (t) => { }); }); -test(`startInstance: should fail missing HTTP request method`, async (t) => { +test(`startInstanceHttp: should fail missing HTTP request method`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -113,13 +125,13 @@ test(`startInstance: should fail missing HTTP request method`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method undefined; use method POST'}); }); -test(`startInstance: should reject HTTP GET request`, async (t) => { +test(`startInstanceHttp: should reject HTTP GET request`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `GET`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -127,12 +139,12 @@ test(`startInstance: should reject HTTP GET request`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method GET; use method POST'}); }); -test(`startInstance: should fail missing content-type header`, async (t) => { +test(`startInstanceHttp: should fail missing content-type header`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -140,13 +152,13 @@ test(`startInstance: should fail missing content-type header`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'HTTP content-type missing'}); }); -test(`startInstance: should reject unsupported HTTP content-type`, async (t) => { +test(`startInstanceHttp: should reject unsupported HTTP content-type`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `text/plain`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -154,41 +166,57 @@ test(`startInstance: should reject unsupported HTTP content-type`, async (t) => t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); }); -test(`startInstance: should fail with missing 'zone' attribute`, async (t) => { +test(`startInstanceHttp: should fail with missing 'zone' attribute`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'instance': 'test-instance'}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from payload`}); }); -test(`startInstance: should fail with missing 'instance' attribute`, async (t) => { +test(`startInstanceHttp: should fail with missing 'instance' attribute`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'zone': 'test-zone'}; - sample.program.startInstance(mocks.req, mocks.res); + sample.program.startInstanceHttp(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 400); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'instance' missing from payload`}); +}); + +test(`startInstanceHttp: should fail with empty request body`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {}; + sample.program.startInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'instance' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from payload`}); }); -test(`stopInstance: should accept application/json`, async (t) => { +/////////////////////////// stopInstanceHttp ////////////////////////////// + +test(`stopInstanceHttp: should accept application/json`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {zone: `test-zone`, instance: `test-instance`}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); sample.mocks.requestPromise() .then((data) => { @@ -197,13 +225,13 @@ test(`stopInstance: should accept application/json`, async (t) => { }); }); -test(`stopInstance: should accept application/octect-stream`, async (t) => { +test(`stopInstanceHttp: should accept application/octect-stream`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/octet-stream`; mocks.req.body = Buffer.from(`{'zone': 'test-zone', 'instance': 'test-instance'}`); - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); sample.mocks.requestPromise() .then((data) => { @@ -212,12 +240,12 @@ test(`stopInstance: should accept application/octect-stream`, async (t) => { }); }); -test(`stopInstance: should fail missing HTTP request method`, async (t) => { +test(`stopInstanceHttp: should fail missing HTTP request method`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -225,13 +253,13 @@ test(`stopInstance: should fail missing HTTP request method`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method undefined; use method POST'}); }); -test(`stopInstance: should reject HTTP GET request`, async (t) => { +test(`stopInstanceHttp: should reject HTTP GET request`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `GET`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -239,12 +267,12 @@ test(`stopInstance: should reject HTTP GET request`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP method GET; use method POST'}); }); -test(`stopInstance: should fail missing content-type header`, async (t) => { +test(`stopInstanceHttp: should fail missing content-type header`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -252,13 +280,13 @@ test(`stopInstance: should fail missing content-type header`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'HTTP content-type missing'}); }); -test(`stopInstance: should reject unsupported HTTP content-type`, async (t) => { +test(`stopInstanceHttp: should reject unsupported HTTP content-type`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `text/plain`; mocks.req.body = {'zone': 'test-zone', 'instance': 'test-instance'}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); @@ -266,30 +294,136 @@ test(`stopInstance: should reject unsupported HTTP content-type`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: 'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); }); -test(`stopInstance: should fail with missing 'zone' attribute`, async (t) => { +test(`stopInstanceHttp: should fail with missing 'zone' attribute`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'instance': 'test-instance'}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from payload`}); }); -test(`stopInstance: should fail with missing 'instance' attribute`, async (t) => { +test(`stopInstanceHttp: should fail with missing 'instance' attribute`, async (t) => { const mocks = getMocks(); const sample = getSample(); mocks.req.method = `POST`; mocks.req.headers[`content-type`] = `application/json`; mocks.req.body = {'zone': 'test-zone'}; - sample.program.stopInstance(mocks.req, mocks.res); + sample.program.stopInstanceHttp(mocks.req, mocks.res); t.true(mocks.res.status.calledOnce); t.is(mocks.res.status.firstCall.args[0], 400); t.true(mocks.res.send.calledOnce); - t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'instance' missing from POST request`}); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'instance' missing from payload`}); +}); + +test(`stopInstanceHttp: should fail with empty request body`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + mocks.req.method = `POST`; + mocks.req.headers[`content-type`] = `application/json`; + mocks.req.body = {}; + sample.program.stopInstanceHttp(mocks.req, mocks.res); + + t.true(mocks.res.status.calledOnce); + t.is(mocks.res.status.firstCall.args[0], 400); + t.true(mocks.res.send.calledOnce); + t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from payload`}); +}); + +/////////////////////////// startInstancePubSub ////////////////////////////// + +test(`startInstancePubSub: should accept JSON-formatted event payload`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {'zone': 'test-zone', 'instance': 'test-instance'}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.startInstancePubSub(mocks.event, mocks.callback); + + sample.mocks.requestPromise() + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); +}); + +test(`startInstancePubSub: should fail with missing 'zone' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {'instance': 'test-instance'}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.startInstancePubSub(mocks.event, mocks.callback); + + t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'zone' missing from payload`)); +}); + +test(`startInstancePubSub: should fail with missing 'instance' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {'zone': 'test-zone'}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.startInstancePubSub(mocks.event, mocks.callback); + + t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'instance' missing from payload`)); +}); + +test(`startInstancePubSub: should fail with empty event payload`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.startInstancePubSub(mocks.event, mocks.callback); + + t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'zone' missing from payload`)); +}); + +/////////////////////////// stopInstancePubSub ////////////////////////////// + +test(`stopInstancePubSub: should accept JSON-formatted event payload`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {'zone': 'test-zone', 'instance': 'test-instance'}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.stopInstancePubSub(mocks.event, mocks.callback); + + sample.mocks.requestPromise() + .then((data) => { + // The request was successfully sent. + t.deepEqual(data, 'request sent'); + }); +}); + +test(`stopInstancePubSub: should fail with missing 'zone' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {'instance': 'test-instance'}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.stopInstancePubSub(mocks.event, mocks.callback); + + t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'zone' missing from payload`)); +}); + +test(`stopInstancePubSub: should fail with missing 'instance' attribute`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {'zone': 'test-zone'}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.stopInstancePubSub(mocks.event, mocks.callback); + + t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'instance' missing from payload`)); +}); + +test(`stopInstancePubSub: should fail with empty event payload`, async (t) => { + const mocks = getMocks(); + const sample = getSample(); + const pubsubData = {}; + mocks.event.data.data = Buffer.from(JSON.stringify(pubsubData)).toString('base64'); + sample.program.stopInstancePubSub(mocks.event, mocks.callback); + + t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'zone' missing from payload`)); }); From 2dde55397231e131a79f5dd10cade0bd5583dd5c Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Mon, 5 Nov 2018 13:13:01 -0800 Subject: [PATCH 10/11] Update package.json for scheduleinstance sample code --- functions/scheduleinstance/index.js | 1 + functions/scheduleinstance/package.json | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index a64127c42c..e771ea63b0 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -17,6 +17,7 @@ // [START functions_stop_instance_http] // [START functions_start_instance_pubsub] // [START functions_stop_instance_pubsub] +const Buffer = require('safe-buffer').Buffer; const Compute = require('@google-cloud/compute'); const compute = new Compute(); diff --git a/functions/scheduleinstance/package.json b/functions/scheduleinstance/package.json index 8a5b57dd86..3520b565a2 100644 --- a/functions/scheduleinstance/package.json +++ b/functions/scheduleinstance/package.json @@ -1,5 +1,5 @@ { - "name": "schedule-instance", + "name": "cloud-functions-schedule-instance", "version": "0.0.1", "private": true, "license": "Apache-2.0", @@ -17,10 +17,10 @@ "test": "ava -T 20s --verbose test/*.test.js" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "2.2.1", - "ava": "0.25.0", - "proxyquire": "2.0.0", - "sinon": "4.4.2" + "@google-cloud/nodejs-repo-tools": "^2.2.5", + "ava": "^0.25.0", + "proxyquire": "^2.0.0", + "sinon": "^4.4.2" }, "dependencies": { "@google-cloud/compute": "^0.10.0", From 77a649505c3c652a16951dbd60decad0d6501e4b Mon Sep 17 00:00:00 2001 From: David Mailhot Date: Mon, 5 Nov 2018 14:05:56 -0800 Subject: [PATCH 11/11] Fix linting errors for scheduleinstance code sample --- functions/scheduleinstance/test/index.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/scheduleinstance/test/index.test.js b/functions/scheduleinstance/test/index.test.js index d1ea4fbf5c..90bc0c73b6 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -80,7 +80,7 @@ function getMocks () { test.beforeEach(tools.stubConsole); test.afterEach.always(tools.restoreConsole); -/////////////////////////// startInstanceHttp ////////////////////////////// +/** Tests for startInstanceHttp */ test(`startInstanceHttp: should accept application/json`, async (t) => { const mocks = getMocks(); @@ -208,7 +208,7 @@ test(`startInstanceHttp: should fail with empty request body`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from payload`}); }); -/////////////////////////// stopInstanceHttp ////////////////////////////// +/** Tests for stopInstanceHttp */ test(`stopInstanceHttp: should accept application/json`, async (t) => { const mocks = getMocks(); @@ -336,7 +336,7 @@ test(`stopInstanceHttp: should fail with empty request body`, async (t) => { t.deepEqual(mocks.res.send.firstCall.args[0], {error: `Attribute 'zone' missing from payload`}); }); -/////////////////////////// startInstancePubSub ////////////////////////////// +/** Tests for startInstancePubSub */ test(`startInstancePubSub: should accept JSON-formatted event payload`, async (t) => { const mocks = getMocks(); @@ -382,7 +382,7 @@ test(`startInstancePubSub: should fail with empty event payload`, async (t) => { t.deepEqual(mocks.callback.firstCall.args[0], new Error(`Attribute 'zone' missing from payload`)); }); -/////////////////////////// stopInstancePubSub ////////////////////////////// +/** Tests for stopInstancePubSub */ test(`stopInstancePubSub: should accept JSON-formatted event payload`, async (t) => { const mocks = getMocks();