diff --git a/functions/scheduleinstance/README.md b/functions/scheduleinstance/README.md index 01132b6b77..d753c213ac 100644 --- a/functions/scheduleinstance/README.md +++ b/functions/scheduleinstance/README.md @@ -22,10 +22,8 @@ See the [Scheduling Instances with Cloud Scheduler tutorial][tutorial]. ## Additional resources -* [Cloud Scheduler documentation][docs] -* [HTTP Cloud Functions documentation][http_docs] -* [HTTP Cloud Functions tutorial][http_tutorial] +* [GCE NodeJS Client Library documentation][compute_nodejs_docs] +* [Background Cloud Functions documentation][background_functions_docs] -[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 +[compute_nodejs_docs]: https://cloud.google.com/compute/docs/tutorials/nodejs-guide +[background_functions_docs]: https://cloud.google.com/functions/docs/writing/background diff --git a/functions/scheduleinstance/index.js b/functions/scheduleinstance/index.js index e771ea63b0..f12fcec6e4 100644 --- a/functions/scheduleinstance/index.js +++ b/functions/scheduleinstance/index.js @@ -13,101 +13,13 @@ * limitations under the License. */ -// [START functions_start_instance_http] -// [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(); - -// [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 JSON-formatted 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.startInstanceHttp = (req, res) => { - try { - const payload = _validatePayload(_parseHttpPayload(_validateHttpReq(req))); - 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); - 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}); - } - return res; -}; - -// [END functions_start_instance_http] -// [START functions_stop_instance_http] -/** - * Stops a Compute Engine instance. - * - * 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. - * - * @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.stopInstanceHttp = (req, res) => { - try { - const payload = _validatePayload(_parseHttpPayload(_validateHttpReq(req))); - 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); - 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}); - } - return res; -}; -// [END functions_stop_instance_http] -// [START functions_start_instance_pubsub] /** * Starts a Compute Engine instance. * @@ -146,9 +58,9 @@ exports.startInstancePubSub = (event, callback) => { callback(err); } }; - // [END functions_start_instance_pubsub] // [START functions_stop_instance_pubsub] + /** * Stops a Compute Engine instance. * @@ -187,10 +99,8 @@ exports.stopInstancePubSub = (event, callback) => { 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. * @@ -207,41 +117,3 @@ function _validatePayload (payload) { } // [END functions_start_instance_pubsub] // [END functions_stop_instance_pubsub] - -/** - * 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 request payload. - */ -function _parseHttpPayload (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 HTTP request contains the expected fields. - * - * @param {!object} req the request to validate. - * @returns {!object} the request object. - */ -function _validateHttpReq (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/test/index.test.js b/functions/scheduleinstance/test/index.test.js index 90bc0c73b6..f3741d380b 100644 --- a/functions/scheduleinstance/test/index.test.js +++ b/functions/scheduleinstance/test/index.test.js @@ -36,31 +36,6 @@ function getSample () { } 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'); - const event = { data: { data: {} @@ -70,8 +45,6 @@ function getMocks () { const callback = sinon.spy(); return { - req: req, - res: res, event: event, callback: callback }; @@ -80,262 +53,6 @@ function getMocks () { test.beforeEach(tools.stubConsole); test.afterEach.always(tools.restoreConsole); -/** Tests for 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.startInstanceHttp(mocks.req, mocks.res); - - sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); -}); - -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.startInstanceHttp(mocks.req, mocks.res); - - sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); -}); - -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.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: 'Unsupported HTTP method undefined; use method POST'}); -}); - -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.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: 'Unsupported HTTP method GET; use method POST'}); -}); - -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.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: 'HTTP content-type missing'}); -}); - -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.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: 'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); -}); - -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.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 payload`}); -}); - -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.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 'zone' missing from payload`}); -}); - -/** Tests for 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.stopInstanceHttp(mocks.req, mocks.res); - - sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); -}); - -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.stopInstanceHttp(mocks.req, mocks.res); - - sample.mocks.requestPromise() - .then((data) => { - // The request was successfully sent. - t.deepEqual(data, 'request sent'); - }); -}); - -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.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: 'Unsupported HTTP method undefined; use method POST'}); -}); - -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.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: 'Unsupported HTTP method GET; use method POST'}); -}); - -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.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: 'HTTP content-type missing'}); -}); - -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.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: 'Unsupported HTTP content-type text/plain; use application/json or application/octet-stream'}); -}); - -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.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`}); -}); - -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.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 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`}); -}); - /** Tests for startInstancePubSub */ test(`startInstancePubSub: should accept JSON-formatted event payload`, async (t) => {