diff --git a/docs/toc.json b/docs/toc.json index f19b5b276b7..443f6107b7a 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -64,6 +64,9 @@ }, { "title": "HealthCheck", "type": "compute/health-check" + }, { + "title": "InstanceGroup", + "type": "compute/instance-group" }, { "title": "Network", "type": "compute/network" diff --git a/lib/compute/index.js b/lib/compute/index.js index f74cba499cc..12f1ee9f283 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -931,6 +931,121 @@ Compute.prototype.getDisks = function(options, callback) { }); }; +/** + * Get a list of instance groups. + * + * @resource [InstanceGroups Overview]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups} + * @resource [InstanceGroups: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/aggregatedList} + * + * @param {object=} options - Instance group search options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {string} options.filter - Search filter in the format of + * `{name} {comparison} {filterString}`. + * - **`name`**: the name of the field to compare + * - **`comparison`**: the comparison operator, `eq` (equal) or `ne` + * (not equal) + * - **`filterString`**: the string to filter to. For string fields, this + * can be a regular expression. + * @param {number} options.maxResults - Maximum number of instance groups to + * return. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/instance-group[]} callback.instanceGroups - + * InstanceGroup objects from your project. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * gce.getInstanceGroups(function(err, instanceGroups) { + * // `instanceGroups` is an array of `InstanceGroup` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, instanceGroups, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getInstanceGroups(nextQuery, callback); + * } + * } + * + * gce.getInstanceGroups({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the instance groups from your project as a readable object stream. + * //- + * gce.getInstanceGroups() + * .on('error', console.error) + * .on('data', function(instanceGroup) { + * // `instanceGroup` is an `InstanceGroup` object. + * }) + * .on('end', function() { + * // All instance groups retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getInstanceGroups() + * .on('data', function(instanceGroup) { + * this.end(); + * }); + */ +Compute.prototype.getInstanceGroups = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.request({ + uri: '/aggregated/instanceGroups', + qs: options + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var zones = resp.items || {}; + + var instanceGroups = Object.keys(zones).reduce(function(acc, zoneName) { + var zone = self.zone(zoneName.replace('zones/', '')); + var instanceGroups = zones[zoneName].instanceGroups || []; + + instanceGroups.forEach(function(group) { + var instanceGroupInstance = zone.instanceGroup(group.name); + instanceGroupInstance.metadata = group; + acc.push(instanceGroupInstance); + }); + + return acc; + }, []); + + callback(null, instanceGroups, nextQuery, resp); + }); +}; + /** * Get a list of firewalls. * @@ -2130,6 +2245,7 @@ streamRouter.extend(Compute, [ 'getDisks', 'getFirewalls', 'getHealthChecks', + 'getInstanceGroups', 'getNetworks', 'getOperations', 'getRegions', diff --git a/lib/compute/instance-group.js b/lib/compute/instance-group.js new file mode 100644 index 00000000000..c9ca79ec22e --- /dev/null +++ b/lib/compute/instance-group.js @@ -0,0 +1,480 @@ +/*! + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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. + */ + +/*! + * @module compute/instance-group + */ + +'use strict'; + +var arrify = require('arrify'); +var extend = require('extend'); +var is = require('is'); +var nodeutil = require('util'); + +/** + * @type {module:common/service-object} + * @private + */ +var ServiceObject = require('../common/service-object.js'); + +/** + * @type {module:common/stream-router} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:compute/zone} zone - Zone object this instance group belongs + * to. + * @param {string} name - Name of the instance group. + */ +/** + * You can create and manage groups of virtual machine instances so that you + * don't have to individually control each instance in your project. + * + * @resource [Creating Groups of Instances]{@link https://cloud.google.com/compute/docs/instance-groups} + * @resource [Unmanaged Instance Groups]{@link https://cloud.google.com/compute/docs/instance-groups/unmanaged-groups} + * + * @constructor + * @alias module:compute/instance-group + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var zone = gce.zone('us-central1-a'); + * + * var instanceGroup = zone.instanceGroup('web-servers'); + */ +function InstanceGroup(zone, name) { + var methods = { + /** + * Create an instance group. + * + * @param {object=} options - See {module:zone#createInstanceGroup}. + * + * @example + * function onCreated(err, instanceGroup, operation, apiResponse) { + * // `instanceGroup` is an InstanceGroup object. + * + * // `operation` is an Operation object that can be used to check the + * // status of the request. + * } + * + * instanceGroup.create(onCreated); + */ + create: true, + + /** + * Check if the instance group exists. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {boolean} callback.exists - Whether the instance group exists or + * not. + * + * @example + * instanceGroup.exists(function(err, exists) {}); + */ + exists: true, + + /** + * Get an instance group if it exists. + * + * You may optionally use this to "get or create" an object by providing an + * object with `autoCreate` set to `true`. Any extra configuration that is + * normally required for the `create` method must be contained within this + * object as well. + * + * @param {options=} options - Configuration object. + * @param {boolean} options.autoCreate - Automatically create the object if + * it does not exist. Default: `false` + * + * @example + * instanceGroup.get(function(err, instanceGroup, apiResponse) { + * // `instanceGroup` is an InstanceGroup object. + * }); + */ + get: true, + + /** + * Get the instance group's metadata. + * + * @resource [InstanceGroups: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/get} + * @resource [InstanceGroups Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.metadata - The instance group's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instanceGroup.getMetadata(function(err, metadata, apiResponse) {}); + */ + getMetadata: true + }; + + ServiceObject.call(this, { + parent: zone, + baseUrl: '/instanceGroups', + id: name, + createMethod: zone.createInstanceGroup.bind(zone), + methods: methods + }); + + this.zone = zone; + this.name = name; +} + +nodeutil.inherits(InstanceGroup, ServiceObject); + +/** + * Format a map of named ports in the way the API expects. + * + * @private + * + * @param {object} ports - A map of names to ports. The key should be the name, + * and the value the port number. + * @return {object[]} - The formatted array of named ports. + */ +InstanceGroup.formatPorts_ = function(ports) { + return Object.keys(ports).map(function(port) { + return { + name: port, + port: ports[port] + }; + }); +}; + +/** + * Add one or more VMs to this instance group. + * + * @resource [InstanceGroups: addInstances API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/addInstances} + * + * @param {module:compute/vm|module:compute/vm[]} vms - VM instances to add to + * this instance group. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/operation} callback.operation - An operation object + * that can be used to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var vms = [ + * gce.zone('us-central1-a').vm('http-server'), + * gce.zone('us-central1-a').vm('https-server') + * ]; + * + * instanceGroup.add(vms, function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +InstanceGroup.prototype.add = function(vms, callback) { + var self = this; + + this.request({ + method: 'POST', + uri: '/addInstances', + json: { + instances: arrify(vms).map(function(vm) { + return { + instance: vm.url + }; + }) + } + }, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = self.zone.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Delete the instance group. + * + * @resource [InstanceGroups: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/delete} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/operation} callback.operation - An operation object + * that can be used to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instanceGroup.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +InstanceGroup.prototype.delete = function(callback) { + var self = this; + + callback = callback || util.noop; + + ServiceObject.prototype.delete.call(this, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = self.zone.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Get a list of VM instances in this instance group. + * + * @resource [InstaceGroups: listInstances API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/listInstances} + * + * @param {object=} options - Instance search options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {string} options.filter - Search filter in the format of + * `{name} {comparison} {filterString}`. + * - **`name`**: the name of the field to compare + * - **`comparison`**: the comparison operator, `eq` (equal) or `ne` + * (not equal) + * - **`filterString`**: the string to filter to. For string fields, this + * can be a regular expression. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {boolean} options.running - Only return instances which are running. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/vm[]} callback.vms - VM objects from this isntance + * group. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instanceGroup.getVMs(function(err, vms) { + * // `vms` is an array of `VM` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, vms, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * instanceGroup.getVMs(nextQuery, callback); + * } + * } + * + * instanceGroup.getVMs({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the VM instances from your project as a readable object stream. + * //- + * instanceGroup.getVMs() + * .on('error', console.error) + * .on('data', function(vm) { + * // `vm` is a `VM` object. + * }) + * .on('end', function() { + * // All instances retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * instanceGroup.getVMs() + * .on('data', function(vm) { + * this.end(); + * }); + */ +InstanceGroup.prototype.getVMs = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + var body; + + if (options.running) { + body = { + instanceState: 'RUNNING' + }; + } + + this.request({ + method: 'POST', + uri: '/listInstances', + qs: options, + json: body + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var vms = arrify(resp.items).map(function(vm) { + var vmInstance = self.zone.vm(vm.instance); + vmInstance.metadata = vm; + return vmInstance; + }); + + callback(null, vms, nextQuery, resp); + }); +}; + +/** + * Remove one or more VMs from this instance group. + * + * @resource [InstanceGroups: removeInstances API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/removeInstances} + * + * @param {module:compute/vm|module:compute/vm[]} vms - VM instances to remove + * from this instance group. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/operation} callback.operation - An operation object + * that can be used to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var vms = [ + * gce.zone('us-central1-a').vm('http-server'), + * gce.zone('us-central1-a').vm('https-server') + * ]; + * + * instanceGroup.remove(vms, function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +InstanceGroup.prototype.remove = function(vms, callback) { + var self = this; + + this.request({ + method: 'POST', + uri: '/removeInstances', + json: { + instances: arrify(vms).map(function(vm) { + return { + instance: vm.url + }; + }) + } + }, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = self.zone.operation(resp.name); + operation.metadata = resp; + + callback(err, operation, resp); + }); +}; + +/** + * Set the named ports for this instance group. + * + * @resource [InstanceGroups: setNamedPorts API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/setNamedPorts} + * + * @param {object} ports - A map of names to ports. The key should be the name, + * and the value the port number. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/operation} callback.operation - An operation object + * that can be used to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var ports = { + * http: 80, + * https: 443 + * }; + * + * instanceGroup.setPorts(ports, function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +InstanceGroup.prototype.setPorts = function(ports, callback) { + var self = this; + + callback = callback || util.noop; + + this.request({ + method: 'POST', + uri: '/setNamedPorts', + json: { + namedPorts: InstanceGroup.formatPorts_(ports) + } + }, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = self.zone.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(InstanceGroup, ['getVMs']); + +module.exports = InstanceGroup; diff --git a/lib/compute/vm.js b/lib/compute/vm.js index 1ab3dacd8ea..e4dd276817e 100644 --- a/lib/compute/vm.js +++ b/lib/compute/vm.js @@ -22,6 +22,7 @@ var createErrorClass = require('create-error-class'); var extend = require('extend'); +var format = require('string-format-obj'); var is = require('is'); var nodeutil = require('util'); @@ -83,6 +84,16 @@ var DetachDiskError = createErrorClass('DetachDiskError', function(message) { * var vm = zone.vm('vm-name'); */ function VM(zone, name) { + this.name = name.replace(/.*\/([^/]+)$/, '$1'); // Just the instance name. + this.zone = zone; + + this.url = format('{base}/{project}/zones/{zone}/instances/{name}', { + base: 'https://www.googleapis.com/compute/v1/projects', + project: zone.compute.projectId, + zone: zone.name, + name: this.name + }); + var methods = { /** * Create a virtual machine. @@ -156,13 +167,10 @@ function VM(zone, name) { ServiceObject.call(this, { parent: zone, baseUrl: '/instances', - id: name, + id: this.name, createMethod: zone.createVM.bind(zone), methods: methods }); - - this.name = name; - this.zone = zone; } nodeutil.inherits(VM, ServiceObject); diff --git a/lib/compute/zone.js b/lib/compute/zone.js index b1133e97870..a6568cb8bdf 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -40,6 +40,12 @@ var Autoscaler = require('./autoscaler.js'); */ var Disk = require('./disk.js'); +/** + * @type {module:compute/instance-group} + * @private + */ +var InstanceGroup = require('./instance-group.js'); + /** * @type {module:compute/operation} * @private @@ -368,6 +374,71 @@ Zone.prototype.createDisk = function(name, config, callback) { }); }; +/** + * Create an instance group in this zone. + * + * @resource [InstanceGroup Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups#resource} + * @resource [InstanceGroups: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/insert} + * + * @param {string} name - Name of the instance group. + * @param {object} options - See an + * [InstanceGroup resource](https://cloud.google.com/compute/docs/reference/v1/instanceGroups#resource). + * @param {object} options.ports - A map of names to ports. The key should be + * the name, and the value the port number. Maps to `options.namedPorts`. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/instance-group} callback.instanceGroup - The created + * InstanceGroup object. + * @param {module:compute/operation} callback.operation - An operation object + * that can be used to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * function onCreated(err, instanceGroup, operation, apiResponse) { + * // `instanceGroup` is an InstanceGroup object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * zone.createInstanceGroup('instance-group-name', onCreated); + */ +Zone.prototype.createInstanceGroup = function(name, options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + var body = extend({}, options, { + name: name + }); + + if (body.ports) { + body.namedPorts = InstanceGroup.formatPorts_(body.ports); + delete body.ports; + } + + this.request({ + method: 'POST', + uri: '/instanceGroups', + json: body + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var instanceGroup = self.instanceGroup(name); + + var operation = self.operation(resp.name); + operation.metadata = resp; + + callback(null, instanceGroup, operation, resp); + }); +}; + /** * Create a virtual machine in this zone. * @@ -794,6 +865,112 @@ Zone.prototype.getDisks = function(options, callback) { }); }; +/** + * Get a list of instance groups for this zone. + * + * @resource [InstanceGroups Overview]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups} + * @resource [InstanceGroups: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups/list} + * + * @param {object=} options - Instance group search options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {string} options.filter - Search filter in the format of + * `{name} {comparison} {filterString}`. + * - **`name`**: the name of the field to compare + * - **`comparison`**: the comparison operator, `eq` (equal) or `ne` + * (not equal) + * - **`filterString`**: the string to filter to. For string fields, this + * can be a regular expression. + * @param {number} options.maxResults - Maximum number of instance groups to + * return. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/instance-group[]} callback.instanceGroups - + * InstanceGroup objects from this zone. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * zone.getInstanceGroups(function(err, instanceGroups) { + * // `instanceGroups` is an array of `InstanceGroup` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, instanceGroups, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * zone.getInstanceGroups(nextQuery, callback); + * } + * } + * + * zone.getInstanceGroups({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the instance groups from your project as a readable object stream. + * //- + * zone.getInstanceGroups() + * .on('error', console.error) + * .on('data', function(instanceGroup) { + * // `instanceGroup` is an `InstanceGroup` object. + * }) + * .on('end', function() { + * // All instance groups retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * zone.getInstanceGroups() + * .on('data', function(instanceGroup) { + * this.end(); + * }); + */ +Zone.prototype.getInstanceGroups = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.request({ + uri: '/instanceGroups', + qs: options + }, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var instanceGroups = (resp.items || []).map(function(instanceGroup) { + var instanceGroupInstance = self.instanceGroup(instanceGroup.name); + instanceGroupInstance.metadata = instanceGroup; + return instanceGroupInstance; + }); + + callback(null, instanceGroups, nextQuery, resp); + }); +}; + /** * Get a list of operations for this zone. * @@ -1002,6 +1179,21 @@ Zone.prototype.getVMs = function(options, callback) { }); }; +/** + * Get a reference to a Google Compute Engine instance group. + * + * @resource [InstanceGroups Overview]{@link https://cloud.google.com/compute/docs/reference/v1/instanceGroups} + * + * @param {string} name - Name of the existing instance group. + * @return {module:compute/instance-group} + * + * @example + * var instanceGroup = zone.instanceGroup('my-instance-group'); + */ +Zone.prototype.instanceGroup = function(name) { + return new InstanceGroup(this, name); +}; + /** * Get a reference to a Google Compute Engine zone operation. * @@ -1084,6 +1276,7 @@ Zone.prototype.createHttpsServerFirewall_ = function(callback) { streamRouter.extend(Zone, [ 'getAutoscalers', 'getDisks', + 'getInstanceGroups', 'getOperations', 'getVMs' ]); diff --git a/system-test/compute.js b/system-test/compute.js index b4c17af603e..4f681029a39 100644 --- a/system-test/compute.js +++ b/system-test/compute.js @@ -18,6 +18,7 @@ var assert = require('assert'); var async = require('async'); +var concat = require('concat-stream'); var is = require('is'); var prop = require('propprop'); @@ -47,29 +48,14 @@ describe('Compute', function() { var region = compute.region(REGION_NAME); var zone = compute.zone(ZONE_NAME); - before(function(done) { - deleteAllTestObjects(done); - }); - - after(function(done) { - deleteAllTestObjects(done); - }); + before(deleteAllTestObjects); + after(deleteAllTestObjects); describe('addresses', function() { var ADDRESS_NAME = generateName('address'); var address = region.address(ADDRESS_NAME); - before(function(done) { - address.create(function(err, disk, operation) { - assert.ifError(err); - - operation - .on('error', done) - .on('complete', function() { - done(); - }); - }); - }); + before(create(address)); it('should have created the address', function(done) { address.getMetadata(function(err, metadata) { @@ -110,27 +96,20 @@ describe('Compute', function() { var AUTOSCALER_NAME = generateName('autoscaler'); var autoscaler = zone.autoscaler(AUTOSCALER_NAME); - // Some of the services we support require an instance group to be created. - // Until `instanceGroups` are officially supported by gcloud-node, we make - // manual requests to create and delete them. var INSTANCE_GROUP_NAME = generateName('instance-group'); + var instanceGroup = zone.instanceGroup(INSTANCE_GROUP_NAME); before(function(done) { async.series([ - function(callback) { - createInstanceGroup(INSTANCE_GROUP_NAME, callback); - }, - - function(callback) { - autoscaler.create({ - coolDown: 30, - cpu: 80, - loadBalance: 40, - maxReplicas: 5, - minReplicas: 0, - target: INSTANCE_GROUP_NAME - }, execAfterOperationComplete(callback)); - } + create(instanceGroup), + create(autoscaler, { + coolDown: 30, + cpu: 80, + loadBalance: 40, + maxReplicas: 5, + minReplicas: 0, + target: INSTANCE_GROUP_NAME + }) ], done); }); @@ -199,21 +178,7 @@ describe('Compute', function() { var DISK_NAME = generateName('disk'); var disk = zone.disk(DISK_NAME); - before(function(done) { - var config = { - os: 'ubuntu' - }; - - disk.create(config, function(err, disk, operation) { - assert.ifError(err); - - operation - .on('error', done) - .on('complete', function() { - done(); - }); - }); - }); + before(create(disk, { os: 'ubuntu' })); it('should have created the disk', function(done) { disk.getMetadata(function(err, metadata) { @@ -290,17 +255,7 @@ describe('Compute', function() { sourceRanges: CONFIG.ranges }; - before(function(done) { - firewall.create(CONFIG, function(err, firewall, operation) { - assert.ifError(err); - - operation - .on('error', done) - .on('complete', function() { - done(); - }); - }); - }); + before(create(firewall, CONFIG)); it('should have opened the correct connections', function(done) { firewall.getMetadata(function(err, metadata) { @@ -344,9 +299,7 @@ describe('Compute', function() { timeout: 25 }; - before(function(done) { - healthCheck.create(OPTIONS, execAfterOperationComplete(done)); - }); + before(create(healthCheck, OPTIONS)); it('should have created the correct health check', function(done) { healthCheck.getMetadata(function(err, metadata) { @@ -419,9 +372,7 @@ describe('Compute', function() { timeout: 25 }; - before(function(done) { - healthCheck.create(OPTIONS, execAfterOperationComplete(done)); - }); + before(create(healthCheck, OPTIONS)); it('should have created the correct health check', function(done) { healthCheck.getMetadata(function(err, metadata) { @@ -482,6 +433,136 @@ describe('Compute', function() { }); }); + describe('instance groups', function() { + var INSTANCE_GROUP_NAME = generateName('instance-group'); + var instanceGroup = zone.instanceGroup(INSTANCE_GROUP_NAME); + + var OPTIONS = { + description: 'new instance group', + ports: { + http: 80 + } + }; + + before(create(instanceGroup, OPTIONS)); + + it('should have created an instance group', function(done) { + instanceGroup.getMetadata(function(err, metadata) { + assert.ifError(err); + + assert.strictEqual(metadata.name, INSTANCE_GROUP_NAME); + assert.strictEqual(metadata.description, OPTIONS.description); + assert.deepEqual(metadata.namedPorts, [ + { + name: 'http', + port: 80 + } + ]); + + done(); + }); + }); + + it('should list project instance groups', function(done) { + compute.getInstanceGroups(function(err, instanceGroups) { + assert.ifError(err); + assert(instanceGroups.length > 0); + done(); + }); + }); + + it('should list project instance groups in stream mode', function(done) { + var resultsMatched = 0; + + compute.getInstanceGroups() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should list zonal instance groups', function(done) { + zone.getInstanceGroups(function(err, instanceGroups) { + assert.ifError(err); + assert(instanceGroups.length > 0); + done(); + }); + }); + + it('should list zonal instance groups in stream mode', function(done) { + var resultsMatched = 0; + + zone.getInstanceGroups() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should set named ports', function(done) { + var ports = OPTIONS.ports; + + instanceGroup.setPorts(ports, execAfterOperationComplete(function(err) { + assert.ifError(err); + + instanceGroup.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.deepEqual(metadata.namedPorts, [ + { + name: 'http', + port: 80 + } + ]); + done(); + }); + })); + }); + + describe('adding and removing VMs', function() { + var vm = zone.vm(generateName('vm')); + + before(create(vm, { os: 'ubuntu' })); + + it('should add a VM to the instance group', function(done) { + instanceGroup.add(vm, execAfterOperationComplete(done)); + }); + + it('should list the VMs', function(done) { + instanceGroup.getVMs(function(err, vms) { + assert.ifError(err); + + var vmNamesInGroup = vms.map(prop('name')); + assert(vmNamesInGroup.indexOf(vm.name) > -1); + + done(); + }); + }); + + it('should list the VMs in stream mode', function(done) { + instanceGroup.getVMs() + .on('error', done) + .pipe(concat(function(vms) { + var vmNamesInGroup = vms.map(prop('name')); + assert(vmNamesInGroup.indexOf(vm.name) > -1); + + done(); + })); + }); + + it('should remove a VM from the instance group', function(done) { + instanceGroup.remove(vm, execAfterOperationComplete(done)); + }); + }); + }); + describe('networks', function() { var NETWORK_NAME = generateName('network'); var network = compute.network(NETWORK_NAME); @@ -490,9 +571,7 @@ describe('Compute', function() { range: '10.240.0.0/16' }; - before(function(done) { - network.create(CONFIG, execAfterOperationComplete(done)); - }); + before(create(network, CONFIG)); it('should have opened the correct range', function(done) { network.getMetadata(function(err, metadata) { @@ -657,13 +736,11 @@ describe('Compute', function() { }, callback); }, - function(callback) { - rule.create({ - target: 'global/targetHttpProxies/' + TARGET_PROXY_NAME, - portRange: '8080', - IPProtocol: 'TCP' - }, execAfterOperationComplete(callback)); - } + create(rule, { + target: 'global/targetHttpProxies/' + TARGET_PROXY_NAME, + portRange: '8080', + IPProtocol: 'TCP' + }) ], done); }); @@ -720,25 +797,21 @@ describe('Compute', function() { before(function(done) { async.series([ - function(callback) { - vm.create({ - os: 'ubuntu', - http: true - }, execAfterOperationComplete(callback)); - }, + create(vm, { + os: 'ubuntu', + http: true + }), function(callback) { createTargetInstance(TARGET_INSTANCE_NAME, VM_NAME, callback); }, - function(callback) { - rule.create({ - target: [ - 'zones/' + zone.name + '/targetInstances/' + TARGET_INSTANCE_NAME - ].join(''), - range: '8000-9000' - }, execAfterOperationComplete(callback)); - } + create(rule, { + target: [ + 'zones/' + zone.name + '/targetInstances/' + TARGET_INSTANCE_NAME + ].join(''), + range: '8000-9000' + }) ], done); }); @@ -863,22 +936,7 @@ describe('Compute', function() { var VM_NAME = generateName('vm'); var vm = zone.vm(VM_NAME); - before(function(done) { - var config = { - os: 'ubuntu', - http: true - }; - - vm.create(config, function(err, vm, operation) { - assert.ifError(err); - - operation - .on('error', done) - .on('complete', function() { - done(); - }); - }); - }); + before(create(vm, { os: 'ubuntu', http: true })); it('should have enabled HTTP connections', function(done) { vm.getTags(function(err, tags) { @@ -1082,7 +1140,6 @@ describe('Compute', function() { deleteUrlMaps, deleteServices, deleteHttpsHealthChecks, - deleteInstanceGroups, deleteTargetInstances, deleteAllGcloudTestObjects ], callback); @@ -1094,6 +1151,7 @@ describe('Compute', function() { 'getAddresses', 'getAutoscalers', 'getDisks', + 'getInstanceGroups', 'getFirewalls', 'getHealthChecks', 'getNetworks', @@ -1117,6 +1175,12 @@ describe('Compute', function() { }); } + function create(object, cfg) { + return function(callback) { + object.create(cfg, execAfterOperationComplete(callback)); + }; + } + function execAfterOperationComplete(callback) { return function(err) { if (err) { @@ -1181,46 +1245,42 @@ describe('Compute', function() { function createService(name, instanceGroupName, healthCheckName, callback) { var service = compute.service(name); + var group = zone.instanceGroup(instanceGroupName); var healthCheck = compute.healthCheck(healthCheckName); var groupUrl; var healthCheckUrl; async.series([ + create(group), + function(callback) { - createInstanceGroup(instanceGroupName, function(err, metadata) { + group.getMetadata(function(err, metadata) { if (err) { callback(err); return; } groupUrl = metadata.selfLink; - callback(); }); }, + create(healthCheck), + function(callback) { - healthCheck.create(execAfterOperationComplete(function(err) { + healthCheck.getMetadata(function(err, metadata) { if (err) { callback(err); return; } - healthCheck.getMetadata(function(err, metadata) { - if (err) { - callback(err); - return; - } - - healthCheckUrl = metadata.selfLink; - - callback(); - }); - })); + healthCheckUrl = metadata.selfLink; + callback(); + }); }, function(callback) { - service.create({ + create(service, { backends: [ { group: groupUrl @@ -1229,81 +1289,11 @@ describe('Compute', function() { healthChecks: [ healthCheckUrl ] - }, execAfterOperationComplete(callback)); + })(callback); } ], callback); } - function getInstanceGroups(callback) { - zone.request({ - uri: '/instanceGroups', - qs: { - filter: 'name eq ' + TESTS_PREFIX + '.*' - } - }, callback); - } - - function deleteInstanceGroups(callback) { - getInstanceGroups(function(err, resp) { - if (err) { - callback(err); - return; - } - - if (!resp.items) { - callback(); - return; - } - - async.each(resp.items.map(prop('name')), deleteInstanceGroup, callback); - }); - } - - function createInstanceGroup(name, callback) { - zone.request({ - method: 'POST', - uri: '/instanceGroups', - json: { - name: name - } - }, function(err, resp) { - if (err) { - callback(err); - return; - } - - var operation = zone.operation(resp.name); - operation - .on('error', callback) - .on('complete', function() { - zone.request({ - uri: '/instanceGroups/' + name - }, function(err, resp) { - callback(null, resp); - }); - }); - }); - } - - function deleteInstanceGroup(name, callback) { - zone.request({ - method: 'DELETE', - uri: '/instanceGroups/' + name - }, function(err, resp) { - if (err) { - callback(err); - return; - } - - var operation = zone.operation(resp.name); - operation - .on('error', callback) - .on('complete', function() { - callback(); - }); - }); - } - function deleteHttpsHealthChecks(callback) { compute.getHealthChecks({ filter: 'name eq ' + TESTS_PREFIX + '.*', diff --git a/test/compute/index.js b/test/compute/index.js index 7e9faeb3d34..d86893e7e32 100644 --- a/test/compute/index.js +++ b/test/compute/index.js @@ -46,6 +46,7 @@ var fakeStreamRouter = { 'getDisks', 'getFirewalls', 'getHealthChecks', + 'getInstanceGroups', 'getNetworks', 'getOperations', 'getRegions', @@ -1348,6 +1349,115 @@ describe('Compute', function() { }); }); + describe('getInstanceGroups', function() { + it('should accept only a callback', function(done) { + compute.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, {}); + done(); + }; + + compute.getInstanceGroups(assert.ifError); + }); + + it('should make the correct default API request', function(done) { + var options = {}; + + compute.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/aggregated/instanceGroups'); + assert.deepEqual(reqOpts.qs, options); + done(); + }; + + compute.getInstanceGroups(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getInstanceGroups({}, function(err, groups, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(groups, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var ZONE_NAME = 'zone-1'; + var FULL_ZONE_NAME = 'zones/' + ZONE_NAME; + + var instanceGroup = { name: 'isntance-group-1' }; + var apiResponse = { + items: {} + }; + + apiResponse.items[FULL_ZONE_NAME] = { + instanceGroups: [instanceGroup] + }; + + beforeEach(function() { + compute.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should create InstanceGroup objects from the resp', function(done) { + var zone = {}; + + compute.zone = function(name) { + assert.strictEqual(name, ZONE_NAME); + return zone; + }; + + zone.instanceGroup = function(name) { + assert.strictEqual(name, instanceGroup.name); + setImmediate(done); + return instanceGroup; + }; + + compute.getInstanceGroups({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + delete apiResponseWithNextPageToken.items; + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getInstanceGroups(query, function(err, groups, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + describe('getNetworks', function() { it('should work with only a callback', function(done) { compute.request = function(reqOpts) { diff --git a/test/compute/instance-group.js b/test/compute/instance-group.js new file mode 100644 index 00000000000..8e9560a3a47 --- /dev/null +++ b/test/compute/instance-group.js @@ -0,0 +1,574 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * 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'; + +var arrify = require('arrify'); +var assert = require('assert'); +var extend = require('extend'); +var mockery = require('mockery-next'); +var nodeutil = require('util'); + +var ServiceObject = require('../../lib/common/service-object.js'); +var util = require('../../lib/common/util.js'); + +function FakeServiceObject() { + this.calledWith_ = arguments; + ServiceObject.apply(this, arguments); +} + +nodeutil.inherits(FakeServiceObject, ServiceObject); + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'InstanceGroup') { + return; + } + + extended = true; + methods = arrify(methods); + assert.equal(Class.name, 'InstanceGroup'); + assert.deepEqual(methods, ['getVMs']); + } +}; + +describe('InstanceGroup', function() { + var InstanceGroup; + var instanceGroup; + + var staticMethods = {}; + + var ZONE = { + createInstanceGroup: util.noop, + vm: util.noop + }; + var NAME = 'instance-group-name'; + + before(function() { + mockery.registerMock( + '../../lib/common/service-object.js', + FakeServiceObject + ); + mockery.registerMock('../../lib/common/stream-router.js', fakeStreamRouter); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + InstanceGroup = require('../../lib/compute/instance-group.js'); + staticMethods.formatPorts_ = InstanceGroup.formatPorts_; + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + extend(InstanceGroup, staticMethods); + instanceGroup = new InstanceGroup(ZONE, NAME); + }); + + describe('instantiation', function() { + it('should extend the correct methods', function() { + assert(extended); // See `fakeStreamRouter.extend` + }); + + it('should localize the zone instance', function() { + assert.strictEqual(instanceGroup.zone, ZONE); + }); + + it('should localize the name', function() { + assert.strictEqual(instanceGroup.name, NAME); + }); + + it('should inherit from ServiceObject', function(done) { + var instanceGroup; + + var zoneInstance = extend({}, ZONE, { + createInstanceGroup: { + bind: function(context) { + assert.strictEqual(context, zoneInstance); + + setImmediate(function() { + assert(instanceGroup instanceof ServiceObject); + + var calledWith = instanceGroup.calledWith_[0]; + + assert.strictEqual(calledWith.parent, zoneInstance); + assert.strictEqual(calledWith.baseUrl, '/instanceGroups'); + assert.strictEqual(calledWith.id, NAME); + assert.deepEqual(calledWith.methods, { + create: true, + exists: true, + get: true, + getMetadata: true + }); + + done(); + }); + } + } + }); + + instanceGroup = new InstanceGroup(zoneInstance, NAME); + }); + }); + + describe('formatPorts_', function() { + var PORTS = { + http: 80, + https: 443 + }; + + it('should format an object of named ports', function() { + assert.deepEqual(InstanceGroup.formatPorts_(PORTS), [ + { name: 'http', port: 80 }, + { name: 'https', port: 443 } + ]); + }); + }); + + describe('add', function() { + var VMS = [ + { url: 'vm-url' }, + { url: 'vm-url-2' } + ]; + + it('should make the correct API request', function(done) { + instanceGroup.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/addInstances'); + assert.deepEqual(reqOpts.json, { + instances: VMS.map(function(vm) { + return { + instance: vm.url + }; + }) + }); + + done(); + }; + + instanceGroup.add(VMS, assert.ifError); + }); + + describe('error', function() { + var apiResponse = {}; + var error = new Error('Error.'); + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error and API response', function(done) { + instanceGroup.add(VMS, function(err, operation, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(operation, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'op-name' }; + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should return an Operation and API response', function(done) { + var operation = {}; + + instanceGroup.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + instanceGroup.add(VMS, function(err, operation_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(operation.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + }); + + describe('delete', function() { + it('should call ServiceObject.delete', function(done) { + FakeServiceObject.prototype.delete = function() { + assert.strictEqual(this, instanceGroup); + done(); + }; + + instanceGroup.delete(); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + FakeServiceObject.prototype.delete = function(callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + instanceGroup.delete(function(err, operation, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(operation, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + instanceGroup.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + FakeServiceObject.prototype.delete = function(callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Operation & Response', function(done) { + var operation = {}; + + instanceGroup.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + instanceGroup.delete(function(err, operation_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(operation_.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + instanceGroup.delete(); + }); + }); + }); + }); + + describe('getVMs', function() { + beforeEach(function() { + instanceGroup.zone.vm = function() { + return {}; + }; + }); + + it('should accept only a callback', function(done) { + instanceGroup.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, {}); + done(); + }; + + instanceGroup.getVMs(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + instanceGroup.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/listInstances'); + assert.strictEqual(reqOpts.qs, query); + assert.strictEqual(reqOpts.json, undefined); + + done(); + }; + + instanceGroup.getVMs(query, assert.ifError); + }); + + describe('options.running', function() { + var OPTIONS = { + running: true + }; + + it('should set the instanceState filter', function(done) { + instanceGroup.request = function(reqOpts) { + assert.deepEqual(reqOpts.json, { + instanceState: 'RUNNING' + }); + done(); + }; + + instanceGroup.getVMs(OPTIONS, assert.ifError); + }); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + instanceGroup.getVMs({}, function(err, vms, nextQuery, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(vms, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { instance: 'vm-name' } + ] + }; + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + instanceGroup.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + instanceGroup.getVMs({}, function(err, vms, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with VMs & API response', function(done) { + var vm = {}; + + instanceGroup.zone.vm = function(name) { + assert.strictEqual(name, apiResponse.items[0].instance); + return vm; + }; + + instanceGroup.getVMs({}, function(err, vms, nextQuery, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(vms[0], vm); + assert.strictEqual(vms[0].metadata, apiResponse.items[0]); + + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); + + describe('remove', function() { + var VMS = [ + { url: 'vm-url' }, + { url: 'vm-url-2' } + ]; + + it('should make the correct API request', function(done) { + instanceGroup.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/removeInstances'); + assert.deepEqual(reqOpts.json, { + instances: VMS.map(function(vm) { + return { + instance: vm.url + }; + }) + }); + + done(); + }; + + instanceGroup.remove(VMS, assert.ifError); + }); + + describe('error', function() { + var apiResponse = {}; + var error = new Error('Error.'); + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error and API response', function(done) { + instanceGroup.remove(VMS, function(err, operation, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(operation, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'op-name' }; + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should return an Operation and API response', function(done) { + var operation = {}; + + instanceGroup.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + instanceGroup.remove(VMS, function(err, operation_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(operation.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + }); + + describe('setPorts', function() { + var PORTS = { + http: 80, + https: 443 + }; + + it('should format the named ports', function(done) { + var expectedNamedPorts = []; + + InstanceGroup.formatPorts_ = function(ports) { + assert.strictEqual(ports, PORTS); + return expectedNamedPorts; + }; + + instanceGroup.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/setNamedPorts'); + assert.strictEqual(reqOpts.json.namedPorts, expectedNamedPorts); + done(); + }; + + instanceGroup.setPorts(PORTS, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + instanceGroup.setPorts(PORTS, function(err, operation, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(operation, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + instanceGroup.setPorts(PORTS); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + instanceGroup.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Operation & Response', function(done) { + var operation = {}; + + instanceGroup.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + instanceGroup.setPorts(PORTS, function(err, operation_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(operation_.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + instanceGroup.setPorts(PORTS); + }); + }); + }); + }); +}); diff --git a/test/compute/vm.js b/test/compute/vm.js index f5386379e2f..5d4ba93ae88 100644 --- a/test/compute/vm.js +++ b/test/compute/vm.js @@ -46,6 +46,10 @@ describe('VM', function() { createVM: util.noop }; var VM_NAME = 'vm-name'; + var FULLY_QUALIFIED_NAME = [ + 'project/project-id/zones/zone-name/instances/', + VM_NAME + ].join(''); before(function() { mockery.registerMock( @@ -80,6 +84,22 @@ describe('VM', function() { assert.strictEqual(vm.name, VM_NAME); }); + it('should strip a qualified name to just the instance name', function() { + var vm = new VM(ZONE, FULLY_QUALIFIED_NAME); + assert.strictEqual(vm.name, VM_NAME); + }); + + it('should localize the URL of the VM', function() { + assert.strictEqual(vm.url, [ + 'https://www.googleapis.com/compute/v1/projects', + COMPUTE.projectId, + 'zones', + ZONE.name, + 'instances', + VM_NAME + ].join('/')); + }); + it('should inherit from ServiceObject', function(done) { var zoneInstance = extend({}, ZONE, { createVM: { diff --git a/test/compute/zone.js b/test/compute/zone.js index 4eebcf234e8..c1e669fc0e1 100644 --- a/test/compute/zone.js +++ b/test/compute/zone.js @@ -39,6 +39,14 @@ function FakeDisk() { this.calledWith_ = [].slice.call(arguments); } +var formatPortsOverride; +function FakeInstanceGroup() { + this.calledWith_ = [].slice.call(arguments); +} +FakeInstanceGroup.formatPorts_ = function() { + return (formatPortsOverride || util.noop).apply(null, arguments); +}; + function FakeOperation() { this.calledWith_ = [].slice.call(arguments); } @@ -67,6 +75,7 @@ var fakeStreamRouter = { assert.deepEqual(methods, [ 'getAutoscalers', 'getDisks', + 'getInstanceGroups', 'getOperations', 'getVMs' ]); @@ -92,6 +101,10 @@ describe('Zone', function() { mockery.registerMock('../../lib/common/stream-router.js', fakeStreamRouter); mockery.registerMock('../../lib/compute/autoscaler.js', FakeAutoscaler); mockery.registerMock('../../lib/compute/disk.js', FakeDisk); + mockery.registerMock( + '../../lib/compute/instance-group.js', + FakeInstanceGroup + ); mockery.registerMock('../../lib/compute/operation.js', FakeOperation); mockery.registerMock('../../lib/compute/vm.js', FakeVM); @@ -109,6 +122,7 @@ describe('Zone', function() { }); beforeEach(function() { + formatPortsOverride = null; gceImagesOverride = null; zone = new Zone(COMPUTE, ZONE_NAME); }); @@ -602,6 +616,130 @@ describe('Zone', function() { }); }); + describe('createInstanceGroup', function() { + var NAME = 'instance-group'; + + beforeEach(function() { + zone.request = util.noop; + }); + + describe('options.ports', function() { + var PORTS = { + http: 80, + https: 443 + }; + + it('should format named ports', function(done) { + var expectedNamedPorts = []; + + formatPortsOverride = function(ports) { + assert.strictEqual(ports, PORTS); + return expectedNamedPorts; + }; + + zone.request = function(reqOpts) { + assert.strictEqual(reqOpts.json.namedPorts, expectedNamedPorts); + assert.strictEqual(reqOpts.json.ports, undefined); + done(); + }; + + zone.createInstanceGroup(NAME, { ports: PORTS }, assert.ifError); + }); + }); + + describe('API request', function() { + var OPTIONS = { + a: 'b', + c: 'd' + }; + + var expectedBody = { + name: NAME, + a: 'b', + c: 'd' + }; + + it('should make the correct API request', function(done) { + zone.request = function(reqOpts) { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual(reqOpts.uri, '/instanceGroups'); + assert.deepEqual(reqOpts.json, expectedBody); + + done(); + }; + + zone.createInstanceGroup(NAME, OPTIONS, assert.ifError); + }); + + it('should not require options', function(done) { + zone.request = function(reqOpts) { + assert.deepEqual(reqOpts.json, { name: NAME }); + done(); + }; + + zone.createInstanceGroup(NAME, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.createInstanceGroup(NAME, OPTIONS, function(err, ig, op, resp) { + assert.strictEqual(err, error); + assert.strictEqual(ig, null); + assert.strictEqual(op, null); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'operation-name' }; + + beforeEach(function() { + zone.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Group, Op & apiResponse', function(done) { + var instanceGroup = {}; + var operation = {}; + + zone.instanceGroup = function(name) { + assert.strictEqual(name, NAME); + return instanceGroup; + }; + + zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + zone.createInstanceGroup(NAME, OPTIONS, function(err, ig, op, resp) { + assert.ifError(err); + + assert.strictEqual(ig, instanceGroup); + + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, resp); + + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + }); + }); + describe('createVM', function() { var NAME = 'new-vm'; @@ -1160,6 +1298,107 @@ describe('Zone', function() { }); }); + describe('getInstanceGroups', function() { + it('should accept only a callback', function(done) { + zone.request = function(reqOpts) { + assert.deepEqual(reqOpts.qs, {}); + done(); + }; + + zone.getInstanceGroups(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + zone.request = function(reqOpts) { + assert.strictEqual(reqOpts.uri, '/instanceGroups'); + assert.strictEqual(reqOpts.qs, query); + + done(); + }; + + zone.getInstanceGroups(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.getInstanceGroups({}, function(err, groups, nextQuery, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(groups, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { name: 'operation-name' } + ] + }; + + beforeEach(function() { + zone.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + zone.request = function(reqOpts, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + zone.getInstanceGroups({}, function(err, groups, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Groups & API resp', function(done) { + var group = {}; + + zone.instanceGroup = function(name) { + assert.strictEqual(name, apiResponse.items[0].name); + return group; + }; + + zone.getInstanceGroups({}, function(err, groups, nextQuery, apiResp) { + assert.ifError(err); + + assert.strictEqual(groups[0], group); + assert.strictEqual(groups[0].metadata, apiResponse.items[0]); + + assert.strictEqual(apiResp, apiResponse); + + done(); + }); + }); + }); + }); + describe('getOperations', function() { it('should accept only a callback', function(done) { zone.request = function(reqOpts) { @@ -1360,6 +1599,17 @@ describe('Zone', function() { }); }); + describe('instanceGroup', function() { + var NAME = 'instance-group'; + + it('should return an InstanceGroup object', function() { + var instanceGroup = zone.instanceGroup(NAME); + assert(instanceGroup instanceof FakeInstanceGroup); + assert.strictEqual(instanceGroup.calledWith_[0], zone); + assert.strictEqual(instanceGroup.calledWith_[1], NAME); + }); + }); + describe('operation', function() { var NAME = 'operation-name';