diff --git a/README.md b/README.md index 2f4f22b149d..1e056347fa9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This client supports the following Google Cloud Platform services: * [Google Cloud DNS](#google-cloud-dns) * [Google Cloud Pub/Sub](#google-cloud-pubsub) * [Google Cloud Storage](#google-cloud-storage) +* [Google Compute Engine](#google-compute-engine) * [Google Cloud Search](#google-cloud-search-alpha) (Alpha) If you need support for other Google APIs, check out the [Google Node.js API Client library][googleapis]. @@ -300,6 +301,40 @@ localReadStream.pipe(remoteWriteStream); ``` +## Google Compute Engine + +- [API Documentation][gcloud-compute-docs] +- [Official Documentation][cloud-compute-docs] + +#### Preview + +```js +var gcloud = require('gcloud'); + +// Authorizing on a per-API-basis. You don't need to do this if you auth on a +// global basis (see Authorization section above). + +var gce = gcloud.compute({ + projectId: 'my-project', + keyFilename: '/path/to/keyfile.json' +}); + +// Create a new VM using the latest OS image of your choice. +var zone = gce.zone('us-central1-a'); +var name = 'ubuntu-http'; + +zone.createVM(name, { os: 'ubuntu' }, function(err, vm, operation) { + // `operation` lets you check the status of long-running tasks. + + operation.onComplete(function(err, metadata) { + if (!err) { + // Virtual machine created! + } + }); +}); +``` + + ## Google Cloud Search (Alpha) > This is an *Alpha* release of Google Cloud Search. This feature is not covered by any SLA or deprecation policy and may be subject to backward-incompatible changes. @@ -357,6 +392,7 @@ Apache 2.0 - See [COPYING](COPYING) for more information. [gcloud-homepage]: https://googlecloudplatform.github.io/gcloud-node/ [gcloud-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs [gcloud-bigquery-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/bigquery +[gcloud-compute-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/compute [gcloud-datastore-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/datastore [gcloud-dns-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/dns [gcloud-pubsub-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/pubsub @@ -376,6 +412,8 @@ Apache 2.0 - See [COPYING](COPYING) for more information. [cloud-bigquery-docs]: https://cloud.google.com/bigquery/what-is-bigquery +[cloud-compute-docs]: https://cloud.google.com/compute/docs + [cloud-datastore-docs]: https://cloud.google.com/datastore/docs [cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate diff --git a/docs/json/master/compute/.gitkeep b/docs/json/master/compute/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/site/components/docs/compute-overview.html b/docs/site/components/docs/compute-overview.html new file mode 100644 index 00000000000..b0198ac00d9 --- /dev/null +++ b/docs/site/components/docs/compute-overview.html @@ -0,0 +1,7 @@ +

Compute Engine Overview

+

+ The object returned from gcloud.compute gives you complete control of your Compute Engine virtual machines, disks, networks, snapshots, addresses, firewalls, and more. +

+

+ To learn more about Compute Engine, see What is Google Compute Engine? +

diff --git a/docs/site/components/docs/docs-values.js b/docs/site/components/docs/docs-values.js index 3bd5b3d4e39..59c2ef1638f 100644 --- a/docs/site/components/docs/docs-values.js +++ b/docs/site/components/docs/docs-values.js @@ -39,6 +39,49 @@ angular.module('gcloud.docs') ] }, + compute: { + title: 'Compute', + _url: '{baseUrl}/compute', + pages: [ + { + title: 'Address', + url: '/address' + }, + { + title: 'Disk', + url: '/disk' + }, + { + title: 'Firewall', + url: '/firewall' + }, + { + title: 'Network', + url: '/network' + }, + { + title: 'Operation', + url: '/operation' + }, + { + title: 'Region', + url: '/region' + }, + { + title: 'Snapshot', + url: '/snapshot' + }, + { + title: 'VM', + url: '/vm' + }, + { + title: 'Zone', + url: '/zone' + } + ] + }, + datastore: { title: 'Datastore', _url: '{baseUrl}/datastore', @@ -180,6 +223,9 @@ angular.module('gcloud.docs') '>=0.16.0': ['search'], // introduce dns api. - '>=0.18.0': ['dns'] + '>=0.18.0': ['dns'], + + // introduce compute api. + '>=0.20.0': ['compute'] } }); diff --git a/docs/site/components/docs/docs.html b/docs/site/components/docs/docs.html index 3b4450ee0d3..1d94fa00dbd 100644 --- a/docs/site/components/docs/docs.html +++ b/docs/site/components/docs/docs.html @@ -44,7 +44,7 @@


-
diff --git a/lib/compute/address.js b/lib/compute/address.js new file mode 100644 index 00000000000..45e3b3e8771 --- /dev/null +++ b/lib/compute/address.js @@ -0,0 +1,145 @@ +/*! + * 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. + */ + +/*! + * @module compute/address + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:region} region - Region this address belongs to. + * @param {string} name - The name of the address. + */ +/** + * An Address object allows you to interact with a Google Compute Engine + * address. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Address Resource]{@link https://cloud.google.com/compute/docs/reference/v1/addresses} * + * + * @constructor + * @alias module:compute/address + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var region = gce.region('region-name'); + * + * var address = region.address('address1'); + */ +function Address(region, name) { + this.region = region; + this.name = name; + this.metadata = {}; +} + +/** + * Delete the address. + * + * @resource [Addresses: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/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 + * address.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Address.prototype.delete = function(callback) { + callback = callback || util.noop; + + var region = this.region; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = region.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Get the metadata of this address. + * + * @resource [Address Resource]{@link https://cloud.google.com/compute/docs/reference/v1/addresses} + * @resource [Addresses: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The address's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * address.getMetadata(function(err, metadata, apiResponse) {}); + */ +Address.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + + var self = this; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Address.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/addresses/' + this.name + path; + this.region.makeReq_(method, path, query, body, callback); +}; + +module.exports = Address; diff --git a/lib/compute/disk.js b/lib/compute/disk.js new file mode 100644 index 00000000000..87e1cf0684e --- /dev/null +++ b/lib/compute/disk.js @@ -0,0 +1,220 @@ +/*! + * 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. + */ + +/*! + * @module compute/disk + */ + +'use strict'; + +var extend = require('extend'); +var format = require('string-format-obj'); +var is = require('is'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:zone} zone - Zone this disk belongs to. + * @param {string} name - The name of the disk. + */ +/** + * A Disk object allows you to interact with a Google Compute Engine disk. + * + * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks} + * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks} + * + * @constructor + * @alias module:compute/disk + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var zone = gce.zone('zone-name'); + * + * var disk = zone.disk('disk1'); + */ +function Disk(zone, name) { + this.zone = zone; + this.name = name; + this.metadata = {}; + + this.formattedName = Disk.formatName_(zone, name); +} + +/** + * Format a disk's name how the API expects. + * + * @param {module:compute/zone} zone - The Zone this disk belongs to. + * @param {string} name - The name of the disk. + * @return {string} - The formatted name. + */ +Disk.formatName_ = function(zone, name) { + return format('projects/{pId}/zones/{zoneName}/disks/{diskName}', { + pId: zone.compute.projectId, + zoneName: zone.name, + diskName: name + }); +}; + +/** + * Create a snapshot of a disk. + * + * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots} + * @resource [Disks: createSnapshot API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/createSnapshot} + * + * @param {string} name - Name of the snapshot. + * @param {object=} options - See the + * [Disks: createSnapshot](https://cloud.google.com/compute/docs/reference/v1/disks/createSnapshot) + * request body. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/snapshot} callback.snapshot - The created Snapshot + * 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 callback(err, snapshot, operation, apiResponse) { + * // `snapshot` is a Snapshot object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * disk.createSnapshot('new-snapshot-name', callback); + */ +Disk.prototype.createSnapshot = function(name, options, callback) { + var zone = this.zone; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + var body = extend({}, options, { + name: name + }); + + this.makeReq_('POST', '/createSnapshot', null, body, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var snapshot = zone.compute.snapshot(name); + + var operation = zone.operation(resp.name); + operation.metadata = resp; + + callback(null, snapshot, operation, resp); + }); +}; + +/** + * Delete the disk. + * + * @resource [Disks: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/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 + * disk.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Disk.prototype.delete = function(callback) { + var zone = this.zone; + + callback = callback || util.noop; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = zone.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Get the disk's metadata. + * + * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks} + * @resource [Disks: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The disk's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * disk.getMetadata(function(err, metadata, apiResponse) {}); + */ +Disk.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Disk.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/disks/' + this.name + path; + this.zone.makeReq_(method, path, query, body, callback); +}; + +module.exports = Disk; diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js new file mode 100644 index 00000000000..fc734fa52bc --- /dev/null +++ b/lib/compute/firewall.js @@ -0,0 +1,191 @@ +/*! + * 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. + */ + +/*! + * @module compute/firewall + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:compute} compute - Compute object this firewall belongs to. + * @param {string} name - Name of the firewall. + */ +/** + * A Firewall object allows you to interact with a Google Compute Engine + * firewall. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * @resource [Firewall Resource]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls} + * + * @constructor + * @alias module:compute/firewall + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var firewall = gce.firewall('tcp-3000'); + */ +function Firewall(compute, name) { + this.compute = compute; + this.name = name; + + this.metadata = { + network: 'global/networks/default' + }; +} + +/** + * Delete the firewall. + * + * @resource [Firewalls: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/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 + * firewall.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Firewall.prototype.delete = function(callback) { + var compute = this.compute; + + callback = callback || util.noop; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = compute.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Get the firewall's metadata. + * + * @resource [Firewalls: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/get} + * @resource [Firewall Resource]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The network's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * firewall.getMetadata(function(err, metadata, apiResponse) {}); + */ +Firewall.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Set the firewall's metadata. + * + * @resource [Firewall Resource]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls} + * + * @param {object} metadata - See a + * [Firewall resource](https://cloud.google.com/compute/docs/reference/v1/firewalls). + * @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 metadata = { + * description: 'New description' + * }; + * + * firewall.setMetadata(metadata, function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Firewall.prototype.setMetadata = function(metadata, callback) { + var compute = this.compute; + + callback = callback || util.noop; + + metadata = metadata || {}; + metadata.name = this.name; + metadata.network = this.metadata.network; + + this.makeReq_('PATCH', '', null, metadata, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = compute.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Firewall.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/global/firewalls/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +module.exports = Firewall; diff --git a/lib/compute/index.js b/lib/compute/index.js new file mode 100644 index 00000000000..eed2cb9bc44 --- /dev/null +++ b/lib/compute/index.js @@ -0,0 +1,1391 @@ +/*! + * 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. + */ + +/*! + * @module compute + */ + +'use strict'; + +var arrify = require('arrify'); +var extend = require('extend'); +var is = require('is'); + +/** + * @type {module:compute/firewall} + * @private + */ +var Firewall = require('./firewall.js'); + +/** + * @type {module:compute/network} + * @private + */ +var Network = require('./network.js'); + +/** + * @type {module:compute/operation} + * @private + */ +var Operation = require('./operation.js'); + +/** + * @type {module:compute/region} + * @private + */ +var Region = require('./region.js'); + +/** + * @type {module:compute/snapshot} + * @private + */ +var Snapshot = require('./snapshot.js'); + +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @type {module:compute/zone} + * @private + */ +var Zone = require('./zone.js'); + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Required scopes for Google Compute Engine API. + * @const {array} + * @private + */ +var SCOPES = ['https://www.googleapis.com/auth/compute']; + +/** + * A Compute object allows you to interact with the Google Compute Engine API. + * Using this object, you can access your instances with {module:compute/vm}, + * disks with {module:compute/disk}, and firewalls with + * {module:compute/firewall}. + * + * @alias module:compute + * @constructor + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + */ +function Compute(options) { + if (!(this instanceof Compute)) { + return new Compute(options); + } + + options = options || {}; + + if (!options.projectId) { + throw util.missingProjectIdError; + } + + var authConfig = { + credentials: options.credentials, + keyFile: options.keyFilename, + scopes: SCOPES, + email: options.email + }; + + // We store the authConfig for use with gceImages in Zone. + this.authConfig = authConfig; + + this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory(authConfig); + this.projectId = options.projectId; +} + +/** + * Create a firewall. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * @resource [Firewalls: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/insert} + * + * @throws {Error} if a name is not provided. + * @throws {Error} if a config object is not provided. + * + * @param {string} name - Name of the firewall. + * @param {object} config - See a + * [Firewall resource](https://cloud.google.com/compute/docs/reference/v1/firewalls#resource). + * @param {object} config.protocols - A map of protocol to port range. The keys + * of the object refer to a protocol (e.g. `tcp`, `udp`) and the value for + * the key are the ports/port-ranges that are allowed to make a connection. + * @param {string[]} config.ranges - The IP address blocks that this rule + * applies to, expressed in + * [CIDR](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) + * format. + * @param {string[]} config.tags - Instance tags which this rule applies to. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/firewall} callback.firewall - The created Firewall + * 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 + * var config = { + * protocols: { + * tcp: [3000], + * udp: [] // An empty array means all ports are allowed. + * }, + * + * ranges: ['0.0.0.0/0'] + * }; + * + * function callback(err, firewall, operation, apiResponse) { + * // `firewall` is a Firewall object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * gce.createFirewall('new-firewall-name', config, callback); + */ +Compute.prototype.createFirewall = function(name, config, callback) { + var self = this; + + if (!is.string(name)) { + throw new Error('A firewall name must be provided.'); + } + + if (!is.object(config)) { + throw new Error('A firewall configuration object must be provided.'); + } + + var body = extend({}, config, { + name: name + }); + + if (body.protocols) { + body.allowed = arrify(body.allowed); + + for (var protocol in body.protocols) { + var allowedConfig = { + IPProtocol: protocol + }; + + var ports = arrify(body.protocols[protocol]); + if (ports.length > 0) { + allowedConfig.ports = ports; + } + + body.allowed.push(allowedConfig); + } + + delete body.protocols; + } + + if (body.ranges) { + body.sourceRanges = arrify(body.ranges); + delete body.ranges; + } + + if (body.tags) { + body.sourceTags = arrify(body.tags); + delete body.tags; + } + + var path = '/global/firewalls'; + + this.makeReq_('POST', path, null, body, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var firewall = self.firewall(name); + + var operation = self.operation(resp.name); + operation.metadata = resp; + + callback(null, firewall, operation, resp); + }); +}; + +/** + * Create a network. + * + * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks} + * @resource [Networks: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/insert} + * + * @param {string} name - Name of the network. + * @param {object} config - See a + * [Network resource](https://cloud.google.com/compute/docs/reference/v1/networks#resource). + * @param {string} config.gateway - A gateway address for default routing to + * other networks. (Alias for `config.gatewayIPv4`) + * @param {string} config.range - + * [CIDR](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) range + * of addresses that are legal on this network. (Alias for + * `config.IPv4Range`) + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/network} callback.network - The created Network + * 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 + * var config = { + * range: '10.240.0.0/16' + * }; + * + * function callback(err, network, operation, apiResponse) { + * // `network` is a Network object. + * + * // `operation` is an Operation object and can be used to check the status + * // of network creation. + * } + * + * gce.createNetwork('new-network', config, callback); + */ +Compute.prototype.createNetwork = function(name, config, callback) { + var self = this; + + var body = extend({}, config, { + name: name + }); + + if (body.range) { + body.IPv4Range = body.range; + delete body.range; + } + + if (body.gateway) { + body.gatewayIPv4 = body.gateway; + delete body.gateway; + } + + this.makeReq_('POST', '/global/networks', null, body, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var network = self.network(name); + + var operation = self.operation(resp.name); + operation.metadata = resp; + + callback(null, network, operation, resp); + }); +}; + +/** + * Get a reference to a Google Compute Engine firewall. + * + * See {module:compute/network#firewall} to get a Firewall object for a specific + * network. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * + * @param {string} name - Name of the existing firewall. + * @return {module:compute/firewall} + * + * @example + * var firewall = gce.firewall('existing-firewall'); + */ +Compute.prototype.firewall = function(name) { + return new Firewall(this, name); +}; + +/** + * Get a list of addresses. For a detailed description of method's options see + * [API reference](https://goo.gl/r9XmXJ). + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Addresses: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/aggregatedList} + * + * @param {object=} options - Address 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 addresses 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/address} callback.addresses - Address 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.getAddresses(function(err, addresses) { + * // addresses is an array of `Address` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, addresses, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getAddresses(nextQuery, callback); + * } + * } + * + * gce.getAddresses({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the addresses from your project as a readable object stream. + * //- + * gce.getAddresses() + * .on('error', console.error) + * .on('data', function(address) { + * // `address` is an `Address` object. + * }) + * .on('end', function() { + * // All addresses retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getAddresses() + * .on('data', function(address) { + * this.end(); + * }); + */ +Compute.prototype.getAddresses = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + var path = '/aggregated/addresses'; + + this.makeReq_('GET', path, options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var regions = resp.items || {}; + + var addresses = Object.keys(regions).reduce(function(acc, regionName) { + var region = self.region(regionName.replace('regions/', '')); + var regionAddresses = regions[regionName].addresses || []; + + regionAddresses.forEach(function(address) { + var addressInstance = region.address(address.name); + addressInstance.metadata = address; + acc.push(addressInstance); + }); + + return acc; + }, []); + + callback(null, addresses, nextQuery, resp); + }); +}; + +/** + * Get a list of disks. + * + * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks} + * @resource [Disks: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/aggregatedList} + * + * @param {object=} options - Disk 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 disks 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/disk} callback.disks - Disk 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.getDisks(function(err, disks) { + * // `disks` is an array of `Disk` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, disks, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getDisks(nextQuery, callback); + * } + * } + * + * gce.getDisks({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the disks from your project as a readable object stream. + * //- + * gce.getDisks() + * .on('error', console.error) + * .on('data', function(disk) { + * // `disk` is a `Disk` object. + * }) + * .on('end', function() { + * // All disks retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getDisks() + * .on('data', function(disk) { + * this.end(); + * }); + */ +Compute.prototype.getDisks = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/aggregated/disks', options, null, 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 disks = Object.keys(zones).reduce(function(acc, zoneName) { + var zone = self.zone(zoneName.replace('zones/', '')); + var disks = zones[zoneName].disks || []; + + disks.forEach(function(disk) { + var diskInstance = zone.disk(disk.name); + diskInstance.metadata = disk; + acc.push(diskInstance); + }); + + return acc; + }, []); + + callback(null, disks, nextQuery, resp); + }); +}; + +/** + * Get a list of firewalls. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * @resource [Firewalls: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/list} + * + * @param {object=} options - Firewall 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 firewalls 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/firewall} callback.firewalls - Firewall 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.getFirewalls(function(err, firewalls) { + * // `firewalls` is an array of `Firewall` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, firewalls, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getFirewalls(nextQuery, callback); + * } + * } + * + * gce.getFirewalls({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the firewalls from your project as a readable object stream. + * //- + * gce.getFirewalls() + * .on('error', console.error) + * .on('data', function(firewall) { + * // `firewall` is a `Firewall` object. + * }) + * .on('end', function() { + * // All firewalls retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getFirewalls() + * .on('data', function(firewall) { + * this.end(); + * }); + */ +Compute.prototype.getFirewalls = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/global/firewalls', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var firewalls = (resp.items || []).map(function(firewall) { + var firewallInstance = self.firewall(firewall.name); + firewallInstance.metadata = firewall; + return firewallInstance; + }); + + callback(null, firewalls, nextQuery, resp); + }); +}; + +/** + * Get a list of networks. + * + * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks} + * @resource [Networks: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/list} + * + * @param {object=} options - Network 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 networks 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/network} callback.networks - Network 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.getNetworks(function(err, networks) { + * // `networks` is an array of `Network` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, networks, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getNetworks(nextQuery, callback); + * } + * } + * + * gce.getNetworks({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the networks from your project as a readable object stream. + * //- + * gce.getNetworks() + * .on('error', console.error) + * .on('data', function(network) { + * // `network` is a `Network` object. + * }) + * .on('end', function() { + * // All networks retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getNetworks() + * .on('data', function(network) { + * this.end(); + * }); + */ +Compute.prototype.getNetworks = function(options, callback) { + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + var self = this; + this.makeReq_('GET', '/global/networks', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var networks = (resp.items || []).map(function(network) { + var networkInstance = self.network(network.name); + networkInstance.metadata = network; + return networkInstance; + }); + + callback(null, networks, nextQuery, resp); + }); +}; + +/** + * Get a list of global operations. + * + * @resource [Global Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations} + * @resource [GlobalOperations: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations/list} + * + * @param {object=} options - Operation 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 operations 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/operation} callback.operations - Operation 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.getOperations(function(err, operations) { + * // `operations` is an array of `Operation` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, operations, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getOperations(nextQuery, callback); + * } + * } + * + * gce.getOperations({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the operations from your project as a readable object stream. + * //- + * gce.getOperations() + * .on('error', console.error) + * .on('data', function(operation) { + * // `operation` is a `Operation` object. + * }) + * .on('end', function() { + * // All operations retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getOperations() + * .on('data', function(operation) { + * this.end(); + * }); + */ +Compute.prototype.getOperations = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + var path = '/global/operations'; + + this.makeReq_('GET', path, options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var operations = (resp.items || []).map(function(operation) { + var operationInstance = self.operation(operation.name); + operationInstance.metadata = operation; + return operationInstance; + }); + + callback(null, operations, nextQuery, resp); + }); +}; + +/** + * Return the regions available to your project. + * + * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones} + * @resource [Regions: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regions/list} + * + * @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 {number} options.maxResults - Maximum number of instances 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/region} callback.regions - Region objects that are + * available to 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.getRegions(function(err, regions) { + * // `regions` is an array of `Region` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, regions, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getRegions(nextQuery, callback); + * } + * } + * + * gce.getRegions({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the regions available to your project as a readable object stream. + * //- + * gce.getRegions() + * .on('error', console.error) + * .on('data', function(region) { + * // `region` is a `Region` object. + * }) + * .on('end', function() { + * // All regions retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getRegions() + * .on('data', function(region) { + * this.end(); + * }); + */ +Compute.prototype.getRegions = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + this.makeReq_('GET', '/regions', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var regions = resp.items.map(function(region) { + var regionInstance = self.region(region.name); + regionInstance.metadata = region; + return regionInstance; + }); + + callback(null, regions, nextQuery, resp); + }); +}; + +/** + * Get a list of snapshots. + * + * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots} + * @resource [Snapshots: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots/list} + * + * @param {object=} options - Snapshot 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 snapshots 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/snapshot} callback.snapshots - Snapshot 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.getSnapshots(function(err, snapshots) { + * // `snapshots` is an array of `Snapshot` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, snapshots, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getSnapshots(nextQuery, callback); + * } + * } + * + * gce.getSnapshots({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the snapshots from your project as a readable object stream. + * //- + * gce.getSnapshots() + * .on('error', console.error) + * .on('data', function(snapshot) { + * // `snapshot` is a `Snapshot` object. + * }) + * .on('end', function() { + * // All snapshots retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getSnapshots() + * .on('data', function(snapshot) { + * this.end(); + * }); + */ +Compute.prototype.getSnapshots = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + + this.makeReq_('GET', '/global/snapshots', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var snapshots = (resp.items || []).map(function(snapshot) { + var snapshotInstance = self.snapshot(snapshot.name); + snapshotInstance.metadata = snapshot; + return snapshotInstance; + }); + + callback(null, snapshots, nextQuery, resp); + }); +}; + +/** + * Get a list of virtual machine instances. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Instances: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/aggregatedList} + * + * @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 {number} options.maxResults - Maximum number of instances 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/vm} callback.vms - VM 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.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. + * gce.getVMs(nextQuery, callback); + * } + * } + * + * gce.getVMs({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the VM instances from your project as a readable object stream. + * //- + * gce.getVMs() + * .on('error', console.error) + * .on('data', function(vm) { + * // `vm` is a `VM` object. + * }) + * .on('end', function() { + * // All vms retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getVMs() + * .on('data', function(vm) { + * this.end(); + * }); + */ +Compute.prototype.getVMs = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + var path = '/aggregated/instances'; + + this.makeReq_('GET', path, options, null, 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 vms = Object.keys(zones).reduce(function(acc, zoneName) { + var zone = self.zone(zoneName.replace('zones/', '')); + var instances = zones[zoneName].instances || []; + + instances.forEach(function(instance) { + var vmInstance = zone.vm(instance.name); + vmInstance.metadata = instance; + acc.push(vmInstance); + }); + + return acc; + }, []); + + callback(null, vms, nextQuery, resp); + }); +}; + +/** + * Return the zones available to your project. + * + * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones} + * @resource [Zones: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zones/list} + * + * @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 {number} options.maxResults - Maximum number of instances 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/zone} callback.zones - Zone objects that are available + * to 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.getZones(function(err, zones) { + * // `zones` is an array of `Zone` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, zones, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * gce.getZones(nextQuery, callback); + * } + * } + * + * gce.getZones({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the zones available to your project as a readable object stream. + * //- + * gce.getZones() + * .on('error', console.error) + * .on('data', function(zone) { + * // `zone` is a `Zone` object. + * }) + * .on('end', function() { + * // All zones retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * gce.getZones() + * .on('data', function(zone) { + * this.end(); + * }); + */ +Compute.prototype.getZones = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + this.makeReq_('GET', '/zones', options, null, 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.map(function(zone) { + var zoneInstance = self.zone(zone.name); + zoneInstance.metadata = zone; + return zoneInstance; + }); + + callback(null, zones, nextQuery, resp); + }); +}; + +/** + * Get a reference to a Google Compute Engine network. + * + * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks} + * + * @param {string} name - Name of the existing network. + * @return {module:compute/network} + * + * @example + * var network = gce.network('network-name'); + */ +Compute.prototype.network = function(name) { + return new Network(this, name); +}; + +/** + * Get a reference to a global Google Compute Engine operation. + * + * @resource [Global Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations} + * + * @param {string} name - Name of the existing operation. + * @return {module:compute/operation} + * + * @example + * var operation = gce.operation('operation-name'); + */ +Compute.prototype.operation = function(name) { + return new Operation(this, name); +}; + +/** + * Get a reference to a Google Compute Engine region. + * + * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones} + * + * @param {string} name - Name of the region. + * @return {module:compute/region} + * + * @example + * var region = gce.region('region-name'); + */ +Compute.prototype.region = function(name) { + return new Region(this, name); +}; + +/** + * Get a reference to a Google Compute Engine snapshot. + * + * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots} + * + * @param {string} name - Name of the existing snapshot. + * @return {module:compute/snapshot} + * + * @example + * var snapshot = gce.snapshot('snapshot-name'); + */ +Compute.prototype.snapshot = function(name) { + return new Snapshot(this, name); +}; + +/** + * Get a reference to a Google Compute Engine zone. + * + * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones} + * + * @param {string} name - Name of the zone. + * @return {module:compute/zone} + * + * @example + * var zone = gce.zone('zone-name'); + */ +Compute.prototype.zone = function(name) { + return new Zone(this, name); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Compute.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.projectId + path + }; + + if (body) { + reqOpts.json = body; + } + + this.makeAuthorizedRequest_(reqOpts, callback); +}; + +/*! 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(Compute, [ + 'getAddresses', + 'getDisks', + 'getFirewalls', + 'getNetworks', + 'getOperations', + 'getRegions', + 'getSnapshots', + 'getVMs', + 'getZones' +]); + +module.exports = Compute; diff --git a/lib/compute/network.js b/lib/compute/network.js new file mode 100644 index 00000000000..3ec6d06cfb9 --- /dev/null +++ b/lib/compute/network.js @@ -0,0 +1,307 @@ +/*! + * 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. + */ + +/*! + * @module compute/network + */ + +'use strict'; + +var extend = require('extend'); +var format = require('string-format-obj'); +var is = require('is'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:compute} compute - The Compute module this network belongs to. + * @param {string} name - Network name. + */ +/** + * A Network object allows you to interact with a Google Compute Engine network. + * + * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks} + * @resource [Network Resource]{@link https://cloud.google.com/compute/docs/reference/v1/networks} + * + * @constructor + * @alias module:compute/network + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var network = gce.network('network-name'); + */ +function Network(compute, name) { + this.compute = compute; + this.name = name; + this.metadata = {}; + + this.formattedName = Network.formatName_(compute, name); +} + +/** + * Format a network's name how the API expects. + * + * @param {module:compute} compute - The Compute object this network belongs to. + * @param {string} name - The name of the network. + * @return {string} - The formatted name. + */ +Network.formatName_ = function(compute, name) { + return format('projects/{projectId}/global/networks/{name}', { + projectId: compute.projectId, + name: name + }); +}; + +/** + * Create a firewall for this network. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * @resource [Firewalls: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/insert} + * + * @param {string} name - Name of the firewall. + * @param {object} config - See a + * [Firewall resource](https://cloud.google.com/compute/docs/reference/v1/firewalls#resource). + * @param {object} config.protocols - A map of protocol to port range. The keys + * of the object refer to a protocol (e.g. `tcp`, `udp`) and the value for + * the key are the ports/port-ranges that are allowed to make a connection. + * @param {string[]} config.ranges - The IP address blocks that this rule + * applies to, expressed in + * [CIDR](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) + * format. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/firewall} callback.firewall - The created Firewall + * 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 + * var config = { + * protocols: { + * tcp: [3000], + * udp: [] // An empty array means all ports are allowed. + * }, + * + * ranges: ['0.0.0.0/0'] + * }; + * + * function callback(err, firewall, operation, apiResponse) { + * // `firewall` is a Firewall object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * network.createFirewall('new-firewall-name', config, callback); + */ +Network.prototype.createFirewall = function(name, config, callback) { + config = extend({}, config, { + network: this.formattedName + }); + + this.compute.createFirewall(name, config, callback); +}; + +/** + * Delete the network. + * + * @resource [Networks: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/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 + * network.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Network.prototype.delete = function(callback) { + var compute = this.compute; + + callback = callback || util.noop; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = compute.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Get a reference to a Google Compute Engine firewall in this network. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * + * @param {string} name - Name of the existing firewall. + * + * @example + * var firewall = network.firewall('firewall-name'); + */ +Network.prototype.firewall = function(name) { + var firewall = this.compute.firewall(name); + + firewall.metadata = { + network: this.formattedName + }; + + return firewall; +}; + +/** + * Get a list of firewalls for this network. + * + * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls} + * @resource [Firewalls: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/list} + * + * @param {object=} options - Firewall search options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number} options.maxResults - Maximum number of firewalls 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/firewall} callback.firewalls - Firewall objects from + * this network. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * network.getFirewalls(function(err, firewalls) { + * // `firewalls` is an array of `Firewall` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, firewalls, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * network.getFirewalls(nextQuery, callback); + * } + * } + * + * network.getFirewalls({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the firewalls from your project as a readable object stream. + * //- + * network.getFirewalls() + * .on('error', console.error) + * .on('data', function(firewall) { + * // `firewall` is a `Firewall` object. + * }) + * .on('end', function() { + * // All firewalls retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * network.getFirewalls() + * .on('data', function(firewall) { + * this.end(); + * }); + */ +Network.prototype.getFirewalls = function(options, callback) { + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = extend({}, options, { + filter: 'network eq .*' + this.formattedName + }); + + return this.compute.getFirewalls(options, callback); +}; + +/** + * Get the network's metadata. + * + * @resource [Network Resource]{@link https://cloud.google.com/compute/docs/reference/v1/networks} + * @resource [Networks: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/delete} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.metadata - The network's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * network.getMetadata(function(err, metadata, apiResponse) {}); + */ +Network.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Network.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/global/networks/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +module.exports = Network; diff --git a/lib/compute/operation.js b/lib/compute/operation.js new file mode 100644 index 00000000000..a26c2cf412d --- /dev/null +++ b/lib/compute/operation.js @@ -0,0 +1,246 @@ +/*! + * 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. + */ + +/*! + * @module compute/operation + */ + +'use strict'; + +var extend = require('extend'); +var is = require('is'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:compute} scope - The scope of the operation: a `Compute`, + * `Zone`, or `Region` object. + * @param {string} name - Operation name. + */ +/** + * An Operation object allows you to interact with a Google Compute Engine + * operation. + * + * An operation can be a + * [GlobalOperation](https://cloud.google.com/compute/docs/reference/v1/globalOperations), + * [RegionOperation](https://cloud.google.com/compute/docs/reference/v1/regionOperations), + * or + * [ZoneOperation](https://cloud.google.com/compute/docs/reference/v1/zoneOperations). + * + * @constructor + * @alias module:compute/operation + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * //- + * // Reference a global operation. + * //- + * var operation = gce.operation('operation-id'); + * + * //- + * // Reference a region operation. + * //- + * var region = gce.region('us-central1'); + * var operation = region.operation('operation-id'); + * + * //- + * // Reference a zone operation. + * //- + * var zone = gce.zone('us-central1-a'); + * var operation = zone.operation('operation-id'); + */ +function Operation(scope, name) { + this.scope = scope; + this.name = name; + this.metadata = {}; +} + +/** + * Delete the operation. + * + * @resource [GlobalOperations: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations/delete} + * @resource [RegionOperations: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations/delete} + * @resource [ZoneOperations: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations/delete} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * operation.delete(function(err, apiResponse) {}); + */ +Operation.prototype.delete = function(callback) { + callback = callback || util.noop; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + callback(err, resp); + }); +}; + +/** + * Get the operation's metadata. For a detailed description of metadata see + * [Operation resource](https://goo.gl/sWm1rt). + * + * @resource [GlobalOperations: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations/get} + * @resource [RegionOperations: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations/get} + * @resource [ZoneOperations: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The disk's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * operation.getMetadata(function(err, metadata, apiResponse) { + * // `metadata.error`: Contains errors if the operation failed. + * // `metadata.warnings`: Contains warnings. + * }); + */ +Operation.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + // An Operation entity contains a property named `error`. This makes + // `makeReq_` think the operation failed, and will return an ApiError to + // this callback. We have to make sure this isn't a false error by seeing if + // the response body contains a property that wouldn't exist on a failed API + // request (`name`). + var isActualError = err && (!resp || resp.name !== self.name); + + if (isActualError) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Register a callback for when the operation is complete. + * + * If the operation doesn't complete after the maximum number of attempts have + * been made (see `options.maxAttempts` and `options.interval`), an error will + * be provided to your callback with code: `OPERATION_INCOMPLETE`. + * + * @param {object=} options - Configuration object. + * @param {number} options.maxAttempts - Maximum number of attempts to make an + * API request to check if the operation is complete. (Default: `10`) + * @param {number} options.interval - Amount of time in milliseconds between + * each request. (Default: `3000`) + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.metadata - The operation's metadata. + * + * @example + * operation.onComplete(function(err, metadata) { + * if (err.code === 'OPERATION_INCOMPLETE') { + * // The operation is not complete yet. You may want to register another + * // `onComplete` listener or queue for later. + * } + * + * if (!err) { + * // Operation complete! + * } + * }); + */ +Operation.prototype.onComplete = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = extend({ + maxAttempts: 10, + interval: 3000 + }, options); + + var didNotCompleteError = new Error('Operation did not complete.'); + didNotCompleteError.code = 'OPERATION_INCOMPLETE'; + + var numAttempts = 0; + + function checkMetadata() { + numAttempts++; + + if (numAttempts > options.maxAttempts) { + callback(didNotCompleteError, self.metadata); + return; + } + + setTimeout(function() { + self.getMetadata(onMetadata); + }, options.interval); + } + + function onMetadata(err, metadata) { + if (err) { + callback(err, metadata); + return; + } + + if (metadata.status !== 'DONE') { + checkMetadata(); + return; + } + + // The operation is complete. + callback(null, metadata); + } + + checkMetadata(); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Operation.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/operations/' + this.name + path; + + if (this.scope.constructor.name === 'Compute') { + path = '/global' + path; + } + + this.scope.makeReq_(method, path, query, body, callback); +}; + +module.exports = Operation; diff --git a/lib/compute/region.js b/lib/compute/region.js new file mode 100644 index 00000000000..50178338be3 --- /dev/null +++ b/lib/compute/region.js @@ -0,0 +1,424 @@ +/*! + * 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. + */ + +/*! + * @module compute/region + */ + +'use strict'; + +var extend = require('extend'); +var is = require('is'); + +/** + * @type {module:compute/address} + * @private + */ +var Address = require('./address.js'); + +/** + * @type {module:compute/operation} + * @private + */ +var Operation = require('./operation.js'); + +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:compute} compute - Compute object this region belongs to. + * @param {string} name - Name of the region. + */ +/** + * A Region object allows you to interact with a Google Compute Engine region. + * + * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones} + * @resource [Region Resource]{@link https://cloud.google.com/compute/docs/reference/v1/regions} + * + * @constructor + * @alias module:compute/region + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var region = gce.region('us-central1'); + */ +function Region(compute, name) { + this.compute = compute; + this.name = name; + this.metadata = {}; +} + +/** + * Get a reference to a Google Compute Engine address in this region. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * + * @param {string} name - Name of the existing address. + * @return {module:compute/address} + * + * @example + * var address = region.address('address-name'); + */ +Region.prototype.address = function(name) { + return new Address(this, name); +}; + +/** + * Create an address in this region. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Address Resource]{@link https://cloud.google.com/compute/docs/reference/v1/addresses} + * @resource [Addresses: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/insert} + * + * @param {string} name - Name of the address. + * @param {object=} options - See an + * [Address resource](https://cloud.google.com/compute/docs/reference/v1/addresses). + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/address} callback.address - The created Address + * 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 callback(err, address, operation, apiResponse) { + * // `address` is an Address object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * region.createAddress('new-address', callback); + */ +Region.prototype.createAddress = function(name, options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + var body = extend({}, options, { + name: name + }); + + this.makeReq_('POST', '/addresses', null, body, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var address = self.address(name); + + var operation = self.operation(resp.name); + operation.metadata = resp; + + callback(null, address, operation, resp); + }); +}; + +/** + * Get a list of addresses in this region. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Addresses: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/list} + * + * @param {object=} options - Address 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 addresses 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/address} callback.addresses - Address objects from + * this region. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * region.getAddresses(function (err, addresses) { + * // `addresses` is an array of `Address` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, addresses, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * region.getAddresses(nextQuery, callback); + * } + * } + * + * region.getAddresses({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the addresses from your project as a readable object stream. + * //- + * region.getAddresses() + * .on('error', console.error) + * .on('data', function(address) { + * // `address` is an `Address` object. + * }) + * .on('end', function() { + * // All addresses retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * region.getAddresses() + * .on('data', function(address) { + * this.end(); + * }); + */ +Region.prototype.getAddresses = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/addresses', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var addresses = (resp.items || []).map(function(address) { + var addressInstance = self.address(address.name); + addressInstance.metadata = address; + return addressInstance; + }); + + callback(null, addresses, nextQuery, resp); + }); +}; + +/** + * Get the region's metadata. + * + * @resource [Region Resource]{@link https://cloud.google.com/compute/docs/reference/v1/regions} + * @resource [Regions: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regions/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The region's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * region.getMetadata(function(err, metadata, apiResponse) {}); + */ +Region.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Get a list of operations for this region. + * + * @resource [Region Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations} + * @resource [RegionOperations: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations/list} + * + * @param {object=} options - Operation 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 operations 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/operation} callback.operations - Operation objects + * from this region. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * region.getOperations(function(err, operations) { + * // `operations` is an array of `Operation` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, operations, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * region.getOperations(nextQuery, callback); + * } + * } + * + * region.getOperations({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the operations from your project as a readable object stream. + * //- + * region.getOperations() + * .on('error', console.error) + * .on('data', function(operation) { + * // `operation` is an `Operation` object. + * }) + * .on('end', function() { + * // All operations retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * region.getOperations() + * .on('data', function(operation) { + * this.end(); + * }); + */ +Region.prototype.getOperations = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/operations', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var operations = (resp.items || []).map(function(operation) { + var operationInstance = self.operation(operation.name); + operationInstance.metadata = operation; + return operationInstance; + }); + + callback(null, operations, nextQuery, resp); + }); +}; + +/** + * Get a reference to a Google Compute Engine region operation. + * + * @resource [Region Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations} + * + * @param {string} name - Name of the existing operation. + * @return {module:compute/operation} + * + * @example + * var operation = region.operation('operation-name'); + */ +Region.prototype.operation = function(name) { + return new Operation(this, name); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Region.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/regions/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +/*! 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(Region, ['getAddresses', 'getOperations']); + +module.exports = Region; diff --git a/lib/compute/snapshot.js b/lib/compute/snapshot.js new file mode 100644 index 00000000000..256c1192afb --- /dev/null +++ b/lib/compute/snapshot.js @@ -0,0 +1,143 @@ +/*! + * 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. + */ + +/*! + * @module compute/snapshot + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:compute} compute - Compute object this snapshot belongs to. + * @param {string} name - Snapshot name. + */ +/** + * A Snapshot object allows you to interact with a Google Compute Engine + * snapshot. + * + * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots} + * @resource [Snapshot Resource]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots} + * + * @constructor + * @alias module:compute/snapshot + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var snapshot = gce.snapshot('snapshot-name'); + */ +function Snapshot(compute, name) { + this.compute = compute; + this.name = name; + this.metadata = {}; +} + +/** + * Delete the snapshot. + * + * @resource [Snapshots: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots/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 + * snapshot.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Snapshot.prototype.delete = function(callback) { + callback = callback || util.noop; + + var compute = this.compute; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = compute.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +/** + * Get the snapshots's metadata. + * + * @resource [Snapshot Resource]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots} + * @resource [Snapshots: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The zone's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * snapshot.getMetadata(function(err, metadata, apiResponse) {}); + */ +Snapshot.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Snapshot.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/global/snapshots/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +module.exports = Snapshot; diff --git a/lib/compute/vm.js b/lib/compute/vm.js new file mode 100644 index 00000000000..7adfb1732c8 --- /dev/null +++ b/lib/compute/vm.js @@ -0,0 +1,411 @@ +/*! + * 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. + */ + +/*! + * @module compute/vm + */ + +'use strict'; + +var extend = require('extend'); +var is = require('is'); + +/** + * @type {module:compute/disk} + * @private + */ +var Disk = require('./disk.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:zone} zone - Zone object this instance belongs to. + * @param {string} name - Name of the instance. + */ +/** + * An Instance object allows you to interact with a Google Compute Engine + * instance. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Instance Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instances} + * + * @constructor + * @alias module:compute/vm + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var gce = gcloud.compute(); + * + * var zone = gce.zone('zone-name'); + * + * var vm = zone.vm('vm-name'); + */ +function VM(zone, name) { + this.zone = zone; + this.name = name; +} + +/** + * Attach a disk to the instance. + * + * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks} + * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks} + * @resource [Instance: attachDisk API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/attachDisk} + * + * @throws {Error} if a {module:compute/disk} is not provided. + * + * @param {module:compute/disk} disk - The disk to attach. + * @param {object=} options - See the + * [Instances: attachDisk](https://cloud.google.com/compute/docs/reference/v1/instances/attachDisk) + * request body. + * @param {boolean} options.readOnly - Attach the disk in read-only mode. (Alias + * for `options.mode = READ_ONLY`) + * @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 disk = zone.disk('my-disk'); + * + * function callback(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * vm.attachDisk(disk, callback); + * + * //- + * // Provide an options object to customize the request. + * //- + * var options = { + * autoDelete: true, + * readOnly: true + * }; + * + * vm.attachDisk(disk, options, callback); + */ +VM.prototype.attachDisk = function(disk, options, callback) { + if (!(disk instanceof Disk)) { + throw new Error('A Disk object must be provided.'); + } + + if (is.fn(options)) { + callback = options; + options = {}; + } + + var body = extend({}, options, { + source: disk.formattedName + }); + + if (body.readOnly) { + body.mode = 'READ_ONLY'; + delete body.readOnly; + } + + this.makeReq_('POST', '/attachDisk', null, body, callback); +}; + +/** + * Delete the instance. + * + * @resource [Instance: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/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 + * vm.delete(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +VM.prototype.delete = function(callback) { + this.makeReq_('DELETE', '', null, null, callback || util.noop); +}; + +/** + * Detach a disk from the instance. + * + * @resource [Instance: detachDisk API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/detachDisk} + * + * @throws {Error} if a {module:compute/disk} is not provided. + * + * @param {module:compute/disk} disk - The disk to detach. + * @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 disk = zone.disk('my-disk'); + * + * vm.detachDisk(disk, function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +VM.prototype.detachDisk = function(disk, callback) { + if (!(disk instanceof Disk)) { + throw new Error('A Disk object must be provided.'); + } + + var query = { + deviceName: disk.name + }; + + this.makeReq_('POST', '/detachDisk', query, null, callback || util.noop); +}; + +/** + * Get the instances's metadata. + * + * @resource [Instance Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instances} + * @resource [Instance: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The instance's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * vm.getMetadata(function(err, metadata, apiResponse) {}); + */ +VM.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, _, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Returns the serial port output for the instance. + * + * @resource [Instances: getSerialPortOutput API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/getSerialPortOutput} + * + * @param {number=} port - The port from which the output is retrieved (1-4). + * Default: `1`. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.output - The output from the port. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * vm.getSerialPortOutput(function(err, output, apiResponse) {}); + */ +VM.prototype.getSerialPortOutput = function(port, callback) { + if (is.fn(port)) { + callback = port; + port = 1; + } + + var query = { + port: port + }; + + this.makeReq_('GET', '/serialPort', query, null, function(err, _, resp) { + if (err) { + callback(err, null, resp); + return; + } + + callback(null, resp.contents, resp); + }); +}; + +/** + * Get the instance's tags and their fingerprint. + * + * This method wraps {module:compute/vm#getMetadata}, returning only the `tags` + * property. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object[]} callback.tags - Tag objects from this VM. + * @param {string} callback.fingerprint - The current tag fingerprint. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * vm.getTags(function(err, tags, fingerprint, apiResponse) {}); + */ +VM.prototype.getTags = function(callback) { + this.getMetadata(function(err, metadata, apiResponse) { + if (err) { + callback(err, null, null, apiResponse); + return; + } + + callback(null, metadata.tags.items, metadata.tags.fingerprint, apiResponse); + }); +}; + +/** + * Reset the instance. + * + * @resource [Instances: reset API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/reset} + * + * @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 + * vm.reset(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +VM.prototype.reset = function(callback) { + this.makeReq_('POST', '/reset', null, null, callback || util.noop); +}; + +/** + * Set the instance's tags. + * + * @resource [Instances: setTags API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/setTags} + * + * @param {string[]} tags - The new tags for the instance. + * @param {string} fingerprint - The current tags fingerprint. An up-to-date + * fingerprint must be provided. + * @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 + * vm.getTags(function(err, tags, fingerprint) { + * tags.push('new-tag'); + * + * vm.setTags(tags, fingerprint, function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the + * // status of the request. + * }); + * }); + */ +VM.prototype.setTags = function(tags, fingerprint, callback) { + var body = { + items: tags, + fingerprint: fingerprint + }; + + this.makeReq_('POST', '/setTags', null, body, callback || util.noop); +}; + +/** + * Start the instance. + * + * @resource [Instances: start API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/start} + * + * @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 + * vm.start(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +VM.prototype.start = function(callback) { + this.makeReq_('POST', '/start', null, null, callback || util.noop); +}; + +/** + * Stop the instance. + * + * @resource [Instances: stop API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/stop} + * + * @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 + * vm.stop(function(err, operation, apiResponse) { + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +VM.prototype.stop = function(callback) { + this.makeReq_('POST', '/stop', null, null, callback || util.noop); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * Most operations on a VM are long-running. This method handles building an + * operation and returning it to the user's provided callback. In methods that + * don't require an operation, we simply don't do anything with the `Operation` + * object. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +VM.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/instances/' + this.name + path; + + var zone = this.zone; + + zone.makeReq_(method, path, query, body, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = zone.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +module.exports = VM; diff --git a/lib/compute/zone.js b/lib/compute/zone.js new file mode 100644 index 00000000000..bb2fe1a782b --- /dev/null +++ b/lib/compute/zone.js @@ -0,0 +1,739 @@ +/*! + * 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. + */ + +/*! + * @module compute/zone + */ + +'use strict'; + +var extend = require('extend'); +var format = require('string-format-obj'); +var gceImages = require('gce-images'); +var is = require('is'); + +/** + * @type {module:compute/disk} + * @private + */ +var Disk = require('./disk.js'); + +/** + * @type {module:compute/operation} + * @private + */ +var Operation = require('./operation.js'); + +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @type {module:compute/vm} + * @private + */ +var VM = require('./vm.js'); + +/*! Developer Documentation + * + * @param {module:compute} compute - Compute object this zone belongs to. + * @param {string} name - Name of the zone. + */ +/** + * A Zone object allows you to interact with a Google Compute Engine zone. + * + * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones} + * @resource [Zone Resource]{@link https://cloud.google.com/compute/docs/reference/v1/zones} + * + * @constructor + * @alias module:compute/zone + * + * @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'); + */ +function Zone(compute, name) { + this.compute = compute; + this.name = name; + this.metadata = {}; + + this.gceImages = gceImages(compute.authConfig); +} + +/** + * Create a persistent disk in this zone. + * + * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks} + * @resource [Disks: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/insert} + * + * @param {string} name - Name of the disk. + * @param {object} config - See a + * [Disk resource](https://cloud.google.com/compute/docs/reference/v1/disks). + * @param {string=} config.os - Specify the name of an OS, and we will use the + * latest version as the source image of a new boot disk. See + * [this list of accepted OS names](https://github.com/stephenplusplus/gce-images#accepted-os-names). + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/disk} callback.disk - The created Disk 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 + * var config = { + * os: 'ubuntu', + * sizeGb: 10 + * }; + * + * zone.createDisk('name', config, function(err, disk, operation, apiResponse) { + * // `disk` is a Disk object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * }); + */ +Zone.prototype.createDisk = function(name, config, callback) { + var self = this; + + var query = {}; + var body = extend({}, config, { + name: name + }); + + if (body.image) { + query.sourceImage = body.image; + delete body.image; + } + + if (body.os) { + this.gceImages.getLatest(body.os, function(err, image) { + if (err) { + callback(err); + return; + } + + delete body.os; + body.sourceImage = image.selfLink; + + self.createDisk(name, body, callback); + }); + return; + } + + this.makeReq_('POST', '/disks', query, body, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var disk = self.disk(name); + + var operation = self.operation(resp.name); + operation.metadata = resp; + + callback(null, disk, operation, resp); + }); +}; + +/** + * Create a virtual machine in this zone. + * + * @resource [Instance Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instances} + * @resource [Instances: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/insert} + * + * @param {string} name - Name of the instance. + * @param {object} config - See an + * [Instance resource](https://cloud.google.com/compute/docs/reference/v1/instances). + * @param {object[]=} config.disks - See a + * [Disk resource](https://cloud.google.com/compute/docs/reference/v1/disks). + * @param {boolean=} config.http - Allow HTTP traffic. Default: `false` + * @param {boolean=} config.https - Allow HTTPS traffic. Default: `false` + * @param {object[]=} config.networkInterfaces - An array of configurations for + * this interface. This specifies how this interface should interact with + * other network services, such as connecting to the internet. Default: + * `[ { network: 'global/networks/default' } ]` + * @param {string=} config.machineType - The machine type resource to use. + * Provide only the name of the machine, e.g. `n1-standard-16`. Refer to + * [Available Machine Types](https://goo.gl/jrHEbo). Default: + * `n1-standard-1` + * @param {string=} config.os - Specify the name of an OS, and we will use the + * latest version as the source image of a new boot disk. See + * [this list of accepted OS names](https://github.com/stephenplusplus/gce-images#accepted-os-names). + * @param {string[]=} config.tags - An array of tags. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:compute/vm} callback.vm - The created VM 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 + * //- + * // Create a new instance using the latest Debian version as the source image + * // for a new boot disk. + * //- + * var config = { + * os: 'debian', + * http: true, + * tags: ['debian-server'] + * }; + * + * //- + * // The above object will auto-expand behind the scenes to something like the + * // following. The Debian version may be different when you run the command. + * //- + * var config = { + * machineType: 'n1-standard-1', + * disks: [ + * { + * boot: true, + * initializeParams: { + * sourceImage: + * 'https://www.googleapis.com/compute/v1/projects' + + * '/debian-cloud/global/images/debian-7-wheezy-v20150710' + * } + * } + * ], + * networkInterfaces: [ + * { + * network: 'global/networks/default' + * } + * ], + * tags: [ + * { + * items: [ + * 'debian-server', + * 'http-server' + * ] + * } + * ] + * }; + * + * function callback(err, vm, operation, apiResponse) { + * // `vm` is a VM object. + * + * // `operation` is an Operation object that can be used to check the status + * // of the request. + * } + * + * zone.createVM('new-vm-name', config, callback); + */ +Zone.prototype.createVM = function(name, config, callback) { + var self = this; + + var body = extend({ + name: name, + machineType: 'n1-standard-1', + networkInterfaces: [ + { + network: 'global/networks/default' + } + ] + }, config); + + if (body.machineType.indexOf('/') === -1) { + // The specified machineType is only a partial name, e.g. 'n1-standard-1'. + body.machineType = format('zones/{zoneName}/machineTypes/{machineType}', { + zoneName: this.name, + machineType: body.machineType + }); + } + + if (is.array(body.tags)) { + body.tags = { + items: body.tags + }; + } + + if (body.http || body.https) { + body.networkInterfaces[0].accessConfigs = [ + { + type: 'ONE_TO_ONE_NAT' + } + ]; + + body.tags = body.tags || {}; + body.tags.items = body.tags.items || []; + + if (body.http) { + delete body.http; + if (body.tags.items.indexOf('http-server') === -1) { + body.tags.items.push('http-server'); + } + } + + if (body.https) { + delete body.https; + if (body.tags.items.indexOf('https-server') === -1) { + body.tags.items.push('https-server'); + } + } + } + + if (body.os) { + this.gceImages.getLatest(body.os, function(err, image) { + if (err) { + callback(err); + return; + } + + delete body.os; + body.disks = body.disks || []; + body.disks.push({ + boot: true, + initializeParams: { + sourceImage: image.selfLink + } + }); + + self.createVM(name, body, callback); + }); + return; + } + + this.makeReq_('POST', '/instances', null, body, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var vm = self.vm(name); + + var operation = self.operation(resp.name); + operation.metadata = resp; + + callback(null, vm, operation, resp); + }); +}; + +/** + * Get a reference to a Google Compute Engine disk in this zone. + * + * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks} + * + * @param {string} name - Name of the existing disk. + * @return {module:compute/disk} + * + * @example + * var disk = zone.disk('disk1'); + */ +Zone.prototype.disk = function(name) { + return new Disk(this, name); +}; + +/** + * Get a list of disks in this zone. + * + * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks} + * @resource [Disks: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/list} + * + * @param {object=} options - Disk 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 disks 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/disk} callback.disks - Disk 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.getDisks(function(err, disks) { + * // `disks` is an array of `Disk` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, disks, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * zone.getDisks(nextQuery, callback); + * } + * } + * + * zone.getDisks({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the disks from your project as a readable object stream. + * //- + * zone.getDisks() + * .on('error', console.error) + * .on('data', function(disk) { + * // `disk` is a `Disk` object. + * }) + * .on('end', function() { + * // All disks retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * zone.getDisks() + * .on('data', function(disk) { + * this.end(); + * }); + */ +Zone.prototype.getDisks = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/disks', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var disks = (resp.items || []).map(function(disk) { + var diskInstance = self.disk(disk.name); + diskInstance.metadata = disk; + return diskInstance; + }); + + callback(null, disks, nextQuery, resp); + }); +}; + +/** + * Get the zone's metadata. + * + * @resource [Zone Resource]{@link https://cloud.google.com/compute/docs/reference/v1/zones} + * @resource [Zones: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zones/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request + * @param {object} callback.metadata - The zone's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * zone.getMetadata(function(err, metadata, apiResponse) {}); + */ +Zone.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Get a list of operations for this zone. + * + * @resource [Zone Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations} + * @resource [ZoneOperations: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations/list} + * + * @param {object=} options - Operation 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 operations 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/operation} callback.operations - Operation 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.getOperations(function(err, operations) { + * // `operations` is an array of `Operation` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, operations, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * zone.getOperations(nextQuery, callback); + * } + * } + * + * zone.getOperations({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the operations from your project as a readable object stream. + * //- + * zone.getOperations() + * .on('error', console.error) + * .on('data', function(operation) { + * // `operation` is an `Operation` object. + * }) + * .on('end', function() { + * // All operations retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * zone.getOperations() + * .on('data', function(operation) { + * this.end(); + * }); + */ +Zone.prototype.getOperations = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/operations', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var operations = (resp.items || []).map(function(operation) { + var operationInstance = self.operation(operation.name); + operationInstance.metadata = operation; + return operationInstance; + }); + + callback(null, operations, nextQuery, resp); + }); +}; + +/** + * Get a list of VM instances in this zone. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * @resource [Instances: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/list} + * + * @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 {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 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.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. + * zone.getVMs(nextQuery, callback); + * } + * } + * + * zone.getVMs({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the VM instances from your project as a readable object stream. + * //- + * zone.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. + * //- + * zone.getVMs() + * .on('data', function(vm) { + * this.end(); + * }); + */ +Zone.prototype.getVMs = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/instances', options, null, 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 = (resp.items || []).map(function(instance) { + var vmInstance = self.vm(instance.name); + vmInstance.metadata = instance; + return vmInstance; + }); + + callback(null, vms, nextQuery, resp); + }); +}; + +/** + * Get a reference to a Google Compute Engine zone operation. + * + * @resource [Zone Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations} + * + * @param {string} name - Name of the existing operation. + * @return {module:compute/operation} + * + * @example + * var operation = zone.operation('operation-name'); + */ +Zone.prototype.operation = function(name) { + return new Operation(this, name); +}; + +/** + * Get a reference to a Google Compute Engine virtual machine instance. + * + * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network} + * + * @param {string} name - Name of the existing virtual machine. + * @return {module:compute/vm} + * + * @example + * var vm = zone.vm('vm-name'); + */ +Zone.prototype.vm = function(name) { + return new VM(this, name); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Zone.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/zones/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +/*! 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(Zone, ['getDisks', 'getOperations', 'getVMs']); + +module.exports = Zone; diff --git a/lib/index.js b/lib/index.js index 0c2e809316b..a3537d1b511 100644 --- a/lib/index.js +++ b/lib/index.js @@ -54,6 +54,25 @@ var apis = { */ bigquery: require('./bigquery'), + /** + * With [Compute Engine](https://cloud.google.com/compute/), you can run + * large-scale workloads on virtual machines hosted on Google's + * infrastructure. Choose a VM that fits your needs and gain the performance + * of Google’s worldwide fiber network. + * + * @type {module:compute} + * + * @return {module:compute} + * + * @example + * var gcloud = require('gcloud'); + * var gce = gcloud.compute({ + * projectId: 'grape-spaceship-123', + * keyFilename: '/path/to/keyfile.json' + * }); + */ + compute: require('./compute'), + /** * [Google Cloud Datastore](https://developers.google.com/datastore/) is a * fully managed, schemaless database for storing non-relational data. Use diff --git a/package.json b/package.json index 4d145b69520..144222faf34 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dns-zonefile": "0.1.9", "duplexify": "^3.2.0", "extend": "^2.0.0", + "gce-images": "^0.1.0", "google-auth-library": "^0.9.4", "is": "^3.0.1", "methmeth": "^1.0.0", diff --git a/scripts/docs.sh b/scripts/docs.sh index 9fa6e898d50..a4f6c6f2f70 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -21,6 +21,17 @@ ./node_modules/.bin/dox < lib/bigquery/job.js > docs/json/master/bigquery/job.json & ./node_modules/.bin/dox < lib/bigquery/table.js > docs/json/master/bigquery/table.json & +./node_modules/.bin/dox < lib/compute/address.js > docs/json/master/compute/address.json & +./node_modules/.bin/dox < lib/compute/disk.js > docs/json/master/compute/disk.json & +./node_modules/.bin/dox < lib/compute/firewall.js > docs/json/master/compute/firewall.json & +./node_modules/.bin/dox < lib/compute/index.js > docs/json/master/compute/index.json & +./node_modules/.bin/dox < lib/compute/network.js > docs/json/master/compute/network.json & +./node_modules/.bin/dox < lib/compute/operation.js > docs/json/master/compute/operation.json & +./node_modules/.bin/dox < lib/compute/region.js > docs/json/master/compute/region.json & +./node_modules/.bin/dox < lib/compute/snapshot.js > docs/json/master/compute/snapshot.json & +./node_modules/.bin/dox < lib/compute/vm.js > docs/json/master/compute/vm.json & +./node_modules/.bin/dox < lib/compute/zone.js > docs/json/master/compute/zone.json & + ./node_modules/.bin/dox < lib/dns/change.js > docs/json/master/dns/change.json & ./node_modules/.bin/dox < lib/dns/index.js > docs/json/master/dns/index.json & ./node_modules/.bin/dox < lib/dns/record.js > docs/json/master/dns/record.json & diff --git a/system-test/compute.js b/system-test/compute.js new file mode 100644 index 00000000000..edd2ab07031 --- /dev/null +++ b/system-test/compute.js @@ -0,0 +1,611 @@ +'use strict'; + +var assert = require('assert'); +var async = require('async'); +var exec = require('methmeth'); + +var env = require('./env.js'); +var Compute = require('../lib/compute/index.js'); + +describe('Compute', function() { + // Since the Compute Engine API is rather large and involves long-running + // tasks for nearly every request, our system tests follow a pattern designed + // to create a minimal amount of resources. + // + // Each `describe` block tests one object type. Before the tests run, the + // object is created. + // + // The created object is then used and expected to exist for the rest of the + // tests in that `describe` block. + // + // After all describe blocks have run, all of the created objects are + // deleted.* This will also pick up any previously-created objects that were + // unable to be removed if a prior test run had unexpectedly quit. + // + // * What we really send are delete requests. If we were to wait on all of the + // delete operations to complete, it could be several minutes. + + var TESTS_PREFIX = 'gcloud-tests-'; + var REGION_NAME = 'us-central1'; + var ZONE_NAME = 'us-central1-a'; + + var compute = new Compute(env); + var region = compute.region(REGION_NAME); + var zone = compute.zone(ZONE_NAME); + + after(function(done) { + deleteAllTestObjects(done); + }); + + describe('addresses', function() { + var ADDRESS_NAME; + var address; + + before(function(done) { + ADDRESS_NAME = generateName(); + + region.createAddress(ADDRESS_NAME, function(err, address_, operation) { + assert.ifError(err); + address = address_; + operation.onComplete(done); + }); + }); + + it('should have created the address', function(done) { + address.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.strictEqual(metadata.name, ADDRESS_NAME); + done(); + }); + }); + + it('should get a list of addresses', function(done) { + compute.getAddresses(function(err, addresses) { + assert.ifError(err); + assert(addresses.length > 0); + done(); + }); + }); + + it('should get a list of addresses in stream mode', function(done) { + var resultsMatched = 0; + + compute.getAddresses() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should access an address through a Region', function(done) { + region.address(ADDRESS_NAME).getMetadata(done); + }); + }); + + describe('disks', function() { + var DISK_NAME; + var disk; + + before(function(done) { + DISK_NAME = generateName(); + + var config = { + os: 'ubuntu' + }; + + zone.createDisk(DISK_NAME, config, function(err, disk_, operation) { + assert.ifError(err); + disk = disk_; + operation.onComplete(done); + }); + }); + + it('should have created the disk', function(done) { + disk.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.strictEqual(metadata.name, DISK_NAME); + done(); + }); + }); + + it('should get a list of disks', function(done) { + compute.getDisks(function(err, disks) { + assert.ifError(err); + assert(disks.length > 0); + done(); + }); + }); + + it('should get a list of disks in stream mode', function(done) { + var resultsMatched = 0; + + compute.getDisks() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should access a disk through a Zone', function(done) { + zone.disk(DISK_NAME).getMetadata(done); + }); + + it('should take a snapshot', function(done) { + disk.createSnapshot(generateName(), done); + }); + }); + + describe('firewalls', function() { + var FIREWALL_NAME; + + var CONFIG = { + protocols: { + tcp: [3000], + udp: [] + }, + + ranges: ['0.0.0.0/0'] + }; + + var expectedMetadata = { + allowed: [ + { + IPProtocol: 'tcp', + ports: ['3000'] + }, + { + IPProtocol: 'udp' + } + ], + + sourceRanges: CONFIG.ranges + }; + + var firewall; + + before(function(done) { + FIREWALL_NAME = generateName(); + + compute.createFirewall( + FIREWALL_NAME, CONFIG, function(err, firewall_, operation) { + assert.ifError(err); + firewall = firewall_; + operation.onComplete(done); + }); + }); + + it('should have opened the correct connections', function(done) { + firewall.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.deepEqual(metadata.allowed, expectedMetadata.allowed); + assert.deepEqual(metadata.sourceRanges, expectedMetadata.sourceRanges); + done(); + }); + }); + + it('should get a list of firewalls', function(done) { + compute.getFirewalls(function(err, firewalls) { + assert.ifError(err); + assert(firewalls.length > 0); + done(); + }); + }); + + it('should get a list of firewalls in stream mode', function(done) { + var resultsMatched = 0; + + compute.getFirewalls() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + describe('networks', function() { + var NETWORK_NAME; + + var CONFIG = { + range: '10.240.0.0/16' + }; + + var network; + + before(function(done) { + NETWORK_NAME = generateName(); + + compute.createNetwork( + NETWORK_NAME, CONFIG, function(err, network_, operation) { + assert.ifError(err); + network = network_; + operation.onComplete(done); + }); + }); + + it('should have opened the correct range', function(done) { + network.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.strictEqual(metadata.IPv4Range, CONFIG.range); + done(); + }); + }); + + it('should get a list of networks', function(done) { + compute.getNetworks(function(err, networks) { + assert.ifError(err); + assert(networks.length > 0); + done(); + }); + }); + + it('should get a list of networks in stream mode', function(done) { + var resultsMatched = 0; + + compute.getNetworks() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + describe('operations', function() { + it('should get a list of operations', function(done) { + compute.getOperations(function(err, operations) { + assert.ifError(err); + assert(operations.length > 0); + done(); + }); + }); + + it('should get a list of operations in stream mode', function(done) { + var resultsMatched = 0; + + compute.getOperations() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + describe('regions', function() { + it('should get a list of regions', function(done) { + compute.getRegions(function(err, regions) { + assert.ifError(err); + assert(regions.length > 0); + done(); + }); + }); + + it('should get a list of regions in stream mode', function(done) { + var resultsMatched = 0; + + compute.getRegions() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should get a list of addresses', function(done) { + region.getOperations(function(err, addresses) { + assert.ifError(err); + assert(addresses.length > 0); + done(); + }); + }); + + it('should get a list of addresses in stream mode', function(done) { + var resultsMatched = 0; + + region.getOperations() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should get a list of operations', function(done) { + region.getOperations(function(err, operations) { + assert.ifError(err); + assert(operations.length > 0); + done(); + }); + }); + + it('should get a list of operations in stream mode', function(done) { + var resultsMatched = 0; + + region.getOperations() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + describe('snapshots', function() { + it('should get a list of snapshots', function(done) { + compute.getSnapshots(function(err, snapshots) { + assert.ifError(err); + assert(snapshots.length > 0); + done(); + }); + }); + + it('should get a list of snapshots in stream mode', function(done) { + var resultsMatched = 0; + + compute.getSnapshots() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + describe('vms', function() { + var VM_NAME; + var vm; + + before(function(done) { + VM_NAME = generateName(); + + var config = { + os: 'ubuntu', + http: true + }; + + zone.createVM(VM_NAME, config, function(err, vm_, operation) { + assert.ifError(err); + vm = vm_; + operation.onComplete(done); + }); + }); + + after(function(done) { + // 90s is the minimum time for this operation to complete as per: + // https://cloud.google.com/compute/docs/instances/#deleting_an_instance + // + // In practice, it seems to take around 1.5x that, so we allow 2x. + var MAX_TIME_ALLOWED = 90000 * 2; + this.timeout(MAX_TIME_ALLOWED); + + vm.delete(function(err, operation) { + if (err) { + done(err); + return; + } + + operation.onComplete({ + maxAttempts: MAX_TIME_ALLOWED / 10000, + interval: 10000 + }, done); + }); + }); + + it('should have enabled HTTP connections', function(done) { + vm.getTags(function(err, tags) { + assert.ifError(err); + assert.deepEqual(tags, ['http-server']); + done(); + }); + }); + + it('should get a list of vms', function(done) { + compute.getVMs(function(err, vms) { + assert.ifError(err); + assert(vms.length > 0); + done(); + }); + }); + + it('should get a list of vms in stream mode', function(done) { + var resultsMatched = 0; + + compute.getVMs() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should access a VM through a Zone', function(done) { + zone.vm(VM_NAME).getMetadata(done); + }); + + it('should attach and detach a disk', function(done) { + compute.getDisks() + .on('error', done) + .once('data', function(disk) { + this.end(); + + vm.attachDisk(disk, function(err) { + assert.ifError(err); + + vm.detachDisk(disk, function(err, operation) { + assert.ifError(err); + operation.onComplete(done); + }); + }); + }); + }); + + it('should get serial port output', function(done) { + vm.getSerialPortOutput(done); + }); + + it('should set tags', function(done) { + var newTagName = 'new-tag'; + + vm.getTags(function(err, tags, fingerprint) { + assert.ifError(err); + + tags.push(newTagName); + + vm.setTags(tags, fingerprint, function(err, operation) { + assert.ifError(err); + + operation.onComplete(function(err) { + assert.ifError(err); + + vm.getTags(function(err, tags) { + assert.ifError(err); + assert(tags.indexOf(newTagName) > -1); + done(); + }); + }); + }); + }); + }); + + it('should reset', function(done) { + vm.reset(done); + }); + + it('should start', function(done) { + vm.start(done); + }); + + it('should stop', function(done) { + vm.stop(done); + }); + }); + + describe('zones', function() { + it('should get a list of zones', function(done) { + compute.getZones(function(err, zones) { + assert.ifError(err); + assert(zones.length > 0); + done(); + }); + }); + + it('should get a list of zones in stream mode', function(done) { + var resultsMatched = 0; + + compute.getZones() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should get a list of disks', function(done) { + zone.getDisks(function(err, disks) { + assert.ifError(err); + assert(disks.length > 0); + done(); + }); + }); + + it('should get a list of disks in stream mode', function(done) { + var resultsMatched = 0; + + zone.getDisks() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + + it('should get a list of operations', function(done) { + zone.getOperations(function(err, operations) { + assert.ifError(err); + assert(operations.length > 0); + done(); + }); + }); + + it('should get a list of operations in stream mode', function(done) { + var resultsMatched = 0; + + zone.getOperations() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + function generateName() { + return TESTS_PREFIX + Date.now(); + } + + function deleteAllTestObjects(callback) { + async.each([ + 'getAddresses', + 'getDisks', + 'getFirewalls', + 'getNetworks', + 'getSnapshots', + 'getVMs' + ], callAndDelete, callback); + } + + function callAndDelete(methodName, callback) { + compute[methodName]({ + filter: 'name eq ' + TESTS_PREFIX + '.*' + }, function(err, objects) { + if (err) { + callback(err); + return; + } + + async.each(objects, exec('delete'), callback); + }); + } +}); diff --git a/test/compute/address.js b/test/compute/address.js new file mode 100644 index 00000000000..17d6bbb8d4f --- /dev/null +++ b/test/compute/address.js @@ -0,0 +1,221 @@ +/** + * 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 assert = require('assert'); +var Address = require('../../lib/compute/address'); + +describe('Address', function() { + var address; + + var ADDRESS_NAME = 'us-central1'; + var REGION = {}; + + beforeEach(function() { + address = new Address(REGION, ADDRESS_NAME); + }); + + describe('instantiation', function() { + it('should localize the region', function() { + assert.strictEqual(address.region, REGION); + }); + + it('should localize the name', function() { + assert.strictEqual(address.name, ADDRESS_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof address.metadata, 'object'); + assert.strictEqual(Object.keys(address.metadata).length, 0); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + address.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + done(); + }; + + address.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + address.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + address.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() { + address.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + address.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Operation & Response', function(done) { + var operation = {}; + + address.region.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + address.delete(function(err, operation_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + address.delete(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + address.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + address.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + address.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + address.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + address.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + address.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + address.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(address.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + address.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + address.getMetadata(); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/addresses/' + address.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + address.region.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + address.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/disk.js b/test/compute/disk.js new file mode 100644 index 00000000000..27486da5361 --- /dev/null +++ b/test/compute/disk.js @@ -0,0 +1,340 @@ +/** + * 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 assert = require('assert'); +var format = require('string-format-obj'); + +var Disk = require('../../lib/compute/disk'); +var util = require('../../lib/common/util'); + +describe('Disk', function() { + var disk; + + var COMPUTE = { projectId: 'project-id' }; + var ZONE = { compute: COMPUTE, name: 'us-central1-a' }; + var DISK_NAME = 'disk-name'; + var DISK_FULL_NAME = format('projects/{pId}/zones/{zName}/disks/{dName}', { + pId: COMPUTE.projectId, + zName: ZONE.name, + dName: DISK_NAME + }); + + beforeEach(function() { + disk = new Disk(ZONE, DISK_NAME); + }); + + describe('instantiation', function() { + it('should localize the zone', function() { + assert.strictEqual(disk.zone, ZONE); + }); + + it('should localize the name', function() { + assert.strictEqual(disk.name, DISK_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof disk.metadata, 'object'); + assert.strictEqual(Object.keys(disk.metadata).length, 0); + }); + + it('should format the disk name', function() { + var formatName_ = Disk.formatName_; + var formattedName = 'projects/a/zones/b/disks/c'; + + Disk.formatName_ = function(zone, name) { + Disk.formatName_ = formatName_; + + assert.strictEqual(zone, ZONE); + assert.strictEqual(name, DISK_NAME); + + return formattedName; + }; + + var disk = new Disk(ZONE, DISK_NAME); + assert(disk.formattedName, formattedName); + }); + }); + + describe('formatName_', function() { + it('should format the name', function() { + var formattedName_ = Disk.formatName_(ZONE, DISK_NAME); + assert.strictEqual(formattedName_, DISK_FULL_NAME); + }); + }); + + describe('createSnapshot', function() { + it('should make the correct API request', function(done) { + disk.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/createSnapshot'); + assert.strictEqual(query, null); + assert.deepEqual(body, { name: 'test', a: 'b' }); + done(); + }; + + disk.createSnapshot('test', { a: 'b' }, util.noop); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + disk.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + disk.createSnapshot('test', {}, function(err, snap, op, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(snap, null); + assert.strictEqual(op, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require options', function() { + assert.doesNotThrow(function() { + disk.createSnapshot('test', util.noop); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + disk.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Snapshot & Operation', function(done) { + var snapshot = {}; + var operation = {}; + + disk.zone.compute.snapshot = function(name) { + assert.strictEqual(name, 'test'); + return snapshot; + }; + + disk.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + disk.createSnapshot('test', {}, function(err, snap, op, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(snap, snapshot); + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + + it('should not require options', function() { + assert.doesNotThrow(function() { + disk.createSnapshot('test', util.noop); + }); + }); + }); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + disk.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + done(); + }; + + disk.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + disk.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + disk.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() { + disk.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + disk.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Operation & Response', function(done) { + var operation = {}; + + disk.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + disk.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() { + disk.delete(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + disk.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + disk.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + disk.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + disk.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + disk.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + disk.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + disk.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(disk.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + disk.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + disk.getMetadata(); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/disks/' + disk.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + disk.zone.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + disk.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/firewall.js b/test/compute/firewall.js new file mode 100644 index 00000000000..f6a8b4c776d --- /dev/null +++ b/test/compute/firewall.js @@ -0,0 +1,296 @@ +/** + * 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 assert = require('assert'); +var Firewall = require('../../lib/compute/firewall'); + +describe('Firewall', function() { + var firewall; + + var COMPUTE = { projectId: 'project-id' }; + var FIREWALL_NAME = 'tcp-3000'; + var FIREWALL_NETWORK = 'global/networks/default'; + + beforeEach(function() { + firewall = new Firewall(COMPUTE, FIREWALL_NAME); + }); + + describe('instantiation', function() { + it('should localize compute instance', function() { + assert.strictEqual(firewall.compute, COMPUTE); + }); + + it('should localize the firewall name', function() { + assert.strictEqual(firewall.name, FIREWALL_NAME); + }); + + it('should create default metadata', function() { + assert.deepEqual(firewall.metadata, { network: FIREWALL_NETWORK }); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + firewall.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + done(); + }; + + firewall.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + firewall.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + firewall.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() { + firewall.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + firewall.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Operation & Response', function(done) { + var operation = {}; + + firewall.compute.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + firewall.delete(function(err, operation_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + firewall.delete(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + firewall.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + firewall.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + firewall.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + firewall.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + firewall.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + firewall.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + firewall.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(firewall.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + firewall.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + firewall.getMetadata(); + }); + }); + }); + }); + + describe('setMetadata', function() { + it('should make the correct API request', function(done) { + firewall.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'PATCH'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.deepEqual(body, { + name: FIREWALL_NAME, + network: FIREWALL_NETWORK, + a: 'b' + }); + + done(); + }; + + firewall.setMetadata({ a: 'b' }, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + firewall.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + firewall.setMetadata({ e: 'f' }, function(err, op, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(op, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + firewall.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with operation & response', function(done) { + var operation = {}; + var metadata = { a: 'b' }; + + firewall.compute.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + firewall.setMetadata(metadata, function(err, op, apiResponse_) { + assert.ifError(err); + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + firewall.setMetadata({ a: 'b' }); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/global/firewalls/' + firewall.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + firewall.compute.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + firewall.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/index.js b/test/compute/index.js new file mode 100644 index 00000000000..31937060d00 --- /dev/null +++ b/test/compute/index.js @@ -0,0 +1,1403 @@ +/** + * 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 assert = require('assert'); +var mockery = require('mockery'); +var extend = require('extend'); +var arrify = require('arrify'); + +var util = require('../../lib/common/util.js'); +var slice = Array.prototype.slice; + +var fakeUtil = extend({}, util, { + makeAuthorizedRequestFactory: util.noop +}); + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'Compute') { + return; + } + + extended = true; + methods = arrify(methods); + assert.deepEqual(methods, [ + 'getAddresses', + 'getDisks', + 'getFirewalls', + 'getNetworks', + 'getOperations', + 'getRegions', + 'getSnapshots', + 'getVMs', + 'getZones' + ]); + } +}; + +function FakeFirewall() { + this.calledWith_ = slice.call(arguments); +} + +function FakeNetwork() { + this.calledWith_ = slice.call(arguments); +} + +function FakeOperation() { + this.calledWith_ = slice.call(arguments); +} + +function FakeRegion() { + this.calledWith_ = slice.call(arguments); + this.address = function() { return {}; }; +} + +function FakeSnapshot() { + this.calledWith_ = slice.call(arguments); +} + +function FakeZone() { + this.calledWith_ = slice.call(arguments); + this.disk = function() { return {}; }; + this.vm = function() { return {}; }; +} + +describe('Compute', function() { + var Compute; + var compute; + + var PROJECT_ID = 'project-id'; + + before(function() { + mockery.registerMock('../common/util.js', fakeUtil); + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.registerMock('./firewall.js', FakeFirewall); + mockery.registerMock('./network.js', FakeNetwork); + mockery.registerMock('./operation.js', FakeOperation); + mockery.registerMock('./region.js', FakeRegion); + mockery.registerMock('./snapshot.js', FakeSnapshot); + mockery.registerMock('./zone.js', FakeZone); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Compute = require('../../lib/compute'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + compute = new Compute({ + projectId: PROJECT_ID + }); + }); + + describe('instantiation', function() { + var options = { + projectId: PROJECT_ID, + credentials: 'credentials', + email: 'email', + keyFilename: 'keyFile' + }; + + it('should return a new instance of Compute', function() { + var compute = new Compute({ projectId: PROJECT_ID }); + assert(compute instanceof Compute); + }); + + it('should throw if projectId is not provided', function() { + assert.throws(Compute, new RegExp(util.missingProjectIdError)); + }); + + it('should localize authConfig', function() { + var compute = new Compute(options); + + assert.deepEqual(compute.authConfig, { + credentials: options.credentials, + keyFile: options.keyFilename, + email: options.email, + scopes: ['https://www.googleapis.com/auth/compute'] + }); + }); + + it('should create a makeAuthorizedRequest method', function(done) { + fakeUtil.makeAuthorizedRequestFactory = function(options_) { + assert.deepEqual(options_, { + credentials: options.credentials, + email: options.email, + keyFile: options.keyFilename, + scopes: ['https://www.googleapis.com/auth/compute'] + }); + fakeUtil.makeAuthorizedRequestFactory = util.noop; + return done; + }; + + var compute = new Compute(options); + compute.makeAuthorizedRequest_(); + }); + + it('should localize the project id', function() { + assert.strictEqual(compute.projectId, PROJECT_ID); + }); + }); + + describe('createFirewall', function() { + it('should throw if a name is not provided', function() { + assert.throws(function() { + compute.createFirewall({}, assert.ifError); + }, /A firewall name must be provided./); + }); + + it('should throw if config is not provided', function() { + assert.throws(function() { + compute.createFirewall('name', assert.ifError); + }, /A firewall configuration object must be provided./); + }); + + describe('config.protocols', function() { + it('should format protocols', function(done) { + var options = { + allowed: { + IPProtocol: 'http', + ports: [8000] + }, + protocols: { + https: [8080, 9000], + ssh: 22, + ftp: [] + } + }; + + compute.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.allowed, [ + { IPProtocol: 'http', ports: [8000] }, + { IPProtocol: 'https', ports: [8080, 9000] }, + { IPProtocol: 'ssh', ports: [22] }, + { IPProtocol: 'ftp' } + ]); + assert.strictEqual(body.protocols, undefined); + done(); + }; + + compute.createFirewall('name', options, assert.ifError); + }); + }); + + describe('config.ranges', function() { + it('should format ranges to sourceRanges', function(done) { + var options = { + ranges: '0.0.0.0/0' // non-array to test that it's arrified. + }; + + compute.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.sourceRanges, [options.ranges]); + assert.strictEqual(body.ranges, undefined); + done(); + }; + + compute.createFirewall('name', options, assert.ifError); + }); + }); + + describe('config.tags', function() { + it('should format tags to sourceTags', function(done) { + var options = { + tags: 'tag' // non-array to test that it's arrified. + }; + + compute.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.sourceTags, [options.tags]); + assert.strictEqual(body.tags, undefined); + done(); + }; + + compute.createFirewall('name', options, assert.ifError); + }); + }); + + it('should make the correct API request', function(done) { + var name = 'new-firewall-name'; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/global/firewalls'); + assert.strictEqual(query, null); + assert.deepEqual(body, { name: name }); + done(); + }; + + compute.createFirewall(name, {}, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should exec the callback with error & API response', function(done) { + compute.createFirewall('name', {}, function(err, firewall, op, resp) { + assert.strictEqual(err, error); + assert.strictEqual(firewall, null); + assert.strictEqual(op, null); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec cb with Firewall, Operation & apiResp', function(done) { + var name = 'name'; + var firewall = {}; + var operation = {}; + + compute.firewall = function(name_) { + assert.strictEqual(name_, name); + return firewall; + }; + + compute.operation = function(name_) { + assert.strictEqual(name_, apiResponse.name); + return operation; + }; + + compute.createFirewall('name', {}, function(err, fw, op, resp) { + assert.strictEqual(err, null); + assert.strictEqual(fw, firewall); + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, apiResponse); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + }); + + describe('createNetwork', function() { + describe('config.range', function() { + it('should set the IPv4Range', function(done) { + var options = { + range: '10.240.0.0/16' + }; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(body.IPv4Range, options.range); + assert.strictEqual(body.range, undefined); + done(); + }; + + compute.createNetwork('name', options, assert.ifError); + }); + }); + + describe('config.gateway', function() { + it('should set the gatewayIPv4', function(done) { + var options = { + gateway: '10.1.1.1' + }; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(body.gatewayIPv4, options.gateway); + assert.strictEqual(body.gateway, undefined); + done(); + }; + + compute.createNetwork('name', options, assert.ifError); + }); + }); + + it('should make the correct API request', function(done) { + var name = 'new-network'; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/global/networks'); + assert.strictEqual(query, null); + assert.deepEqual(body, { name: name }); + done(); + }; + + compute.createNetwork(name, {}, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should exec the callback with error & API response', function(done) { + compute.createNetwork('name', {}, function(err, network, op, resp) { + assert.strictEqual(err, error); + assert.strictEqual(network, null); + assert.strictEqual(op, null); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var NAME = 'network-name'; + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec cb with Network, Operation & apiResp', function(done) { + var network = {}; + var operation = {}; + + compute.network = function(name_) { + assert.strictEqual(name_, NAME); + return network; + }; + + compute.operation = function(name_) { + assert.strictEqual(name_, apiResponse.name); + return operation; + }; + + compute.createNetwork(NAME, {}, function(err, network_, op, resp) { + assert.strictEqual(err, null); + assert.strictEqual(network_, network); + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, apiResponse); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + }); + + describe('firewall', function() { + var NAME = 'firewall-name'; + + it('should return a Firewall object', function() { + var firewall = compute.firewall(NAME); + assert(firewall instanceof FakeFirewall); + assert.strictEqual(firewall.calledWith_[0], compute); + assert.strictEqual(firewall.calledWith_[1], NAME); + }); + }); + + describe('getAddresses', function() { + it('should accept only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getAddresses(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/aggregated/addresses'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getAddresses(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getAddresses({}, function(err, addresses, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(addresses, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var REGION_NAME = 'region-1'; + var FULL_REGION_NAME = 'regions/' + REGION_NAME; + + var address = { name: 'address-1' }; + var apiResponse = { + items: {} + }; + + apiResponse.items[FULL_REGION_NAME] = { + addresses: [address] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Address objects from the response', function(done) { + var region = {}; + + compute.region = function(name) { + assert.strictEqual(name, REGION_NAME); + return region; + }; + + region.address = function(name) { + assert.strictEqual(name, address.name); + setImmediate(done); + return address; + }; + + compute.getAddresses({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getAddresses(query, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getDisks', function() { + it('should accept only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getDisks(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/aggregated/disks'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getDisks(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getDisks({}, function(err, disks, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(disks, 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 disk = { name: 'disk-1' }; + var apiResponse = { + items: {} + }; + + apiResponse.items[FULL_ZONE_NAME] = { + disks: [disk] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Disk objects from the response', function(done) { + var zone = {}; + + compute.zone = function(name) { + assert.strictEqual(name, ZONE_NAME); + return zone; + }; + + zone.disk = function(name) { + assert.strictEqual(name, disk.name); + setImmediate(done); + return disk; + }; + + compute.getDisks({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getDisks(query, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getFirewalls', function() { + it('should accept only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getFirewalls(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/global/firewalls'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getFirewalls(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getFirewalls({}, function(err, firewalls, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(firewalls, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var firewall = { name: 'firewall-1' }; + var apiResponse = { + items: [firewall] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Firewall objects from the response', function(done) { + compute.firewall = function(name) { + assert.strictEqual(name, firewall.name); + setImmediate(done); + return firewall; + }; + + compute.getFirewalls({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getFirewalls(query, function(err, addresses, 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.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getNetworks(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/global/networks'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getNetworks(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getNetworks({}, function(err, networks, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(networks, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var network = { name: 'network-1' }; + var apiResponse = { + items: [network] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Network objects from the response', function(done) { + compute.network = function(name) { + assert.strictEqual(name, network.name); + setImmediate(done); + return network; + }; + + compute.getNetworks({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getNetworks(query, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getOperations', function() { + it('should work with only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getOperations(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/global/operations'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getOperations(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getOperations({}, function(err, ops, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(ops, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var operation = { name: 'op-1' }; + var apiResponse = { + items: [operation] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Operation objects from the response', function(done) { + compute.operation = function(name) { + assert.strictEqual(name, operation.name); + setImmediate(done); + return operation; + }; + + compute.getOperations({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getOperations(query, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getRegions', function() { + it('should work with only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getRegions(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/regions'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getRegions(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getRegions({}, function(err, regions, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(regions, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var region = { name: 'region-1' }; + var apiResponse = { + items: [region] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Region objects from the response', function(done) { + compute.region = function(name) { + assert.strictEqual(name, region.name); + setImmediate(done); + return region; + }; + + compute.getRegions({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getRegions(query, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getSnapshots', function() { + it('should work with only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getSnapshots(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/global/snapshots'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getSnapshots(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getSnapshots({}, function(err, snapshots, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(snapshots, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var snapshot = { name: 'snapshot-1' }; + var apiResponse = { + items: [snapshot] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Snapshot objects from the response', function(done) { + compute.snapshot = function(name) { + assert.strictEqual(name, snapshot.name); + setImmediate(done); + return snapshot; + }; + + compute.getSnapshots({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getSnapshots(query, function(err, snapshots, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getVMs', function() { + it('should work with only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getVMs(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/aggregated/instances'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getVMs(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getVMs({}, function(err, vms, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(vms, 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 vm = { name: 'vm-1' }; + var apiResponse = { + items: {} + }; + + apiResponse.items[FULL_ZONE_NAME] = { + instances: [vm] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create VM objects from the response', function(done) { + var zone = {}; + + compute.zone = function(name) { + assert.strictEqual(name, ZONE_NAME); + return zone; + }; + + zone.vm = function(name) { + assert.strictEqual(name, vm.name); + setImmediate(done); + return vm; + }; + + compute.getVMs({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getVMs(query, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('getZones', function() { + it('should work with only a callback', function(done) { + compute.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + compute.getZones(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var options = {}; + + compute.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/zones'); + assert.strictEqual(query, options); + assert.strictEqual(body, null); + done(); + }; + + compute.getZones(options, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + compute.getZones({}, function(err, zones, nextQuery, resp) { + assert.strictEqual(err, error); + assert.strictEqual(zones, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var zone = { name: 'zone-1' }; + var apiResponse = { + items: [zone] + }; + + beforeEach(function() { + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should create Zone objects from the response', function(done) { + compute.zone = function(name) { + assert.strictEqual(name, zone.name); + setImmediate(done); + return zone; + }; + + compute.getZones({}, assert.ifError); + }); + + it('should build a nextQuery if necessary', function(done) { + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: 'next-page-token' + }); + + var query = { a: 'b', c: 'd' }; + var originalQuery = extend({}, query); + + compute.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + compute.getZones(query, function(err, snapshots, nextQuery) { + assert.ifError(err); + + assert.deepEqual(query, originalQuery); + + assert.deepEqual(nextQuery, extend({}, query, { + pageToken: apiResponseWithNextPageToken.nextPageToken + })); + + done(); + }); + }); + }); + }); + + describe('network', function() { + var NAME = 'network-name'; + + it('should return a Network object', function() { + var network = compute.network(NAME); + assert(network instanceof FakeNetwork); + assert.strictEqual(network.calledWith_[0], compute); + assert.strictEqual(network.calledWith_[1], NAME); + }); + }); + + describe('operation', function() { + var NAME = 'op-name'; + + it('should return an Operation object', function() { + var op = compute.operation(NAME); + assert(op instanceof FakeOperation); + assert.strictEqual(op.calledWith_[0], compute); + assert.strictEqual(op.calledWith_[1], NAME); + }); + }); + + describe('region', function() { + var NAME = 'region-name'; + + it('should return a Region object', function() { + var region = compute.region(NAME); + assert(region instanceof FakeRegion); + assert.strictEqual(region.calledWith_[0], compute); + assert.strictEqual(region.calledWith_[1], NAME); + }); + }); + + describe('snapshot', function() { + var NAME = 'snapshot-name'; + + it('should return a Snapshot object', function() { + var snapshot = compute.snapshot(NAME); + assert(snapshot instanceof FakeSnapshot); + assert.strictEqual(snapshot.calledWith_[0], compute); + assert.strictEqual(snapshot.calledWith_[1], NAME); + }); + }); + + describe('zone', function() { + var NAME = 'zone-name'; + + it('should return a Zone object', function() { + var zone = compute.zone(NAME); + assert(zone instanceof FakeZone); + assert.strictEqual(zone.calledWith_[0], compute); + assert.strictEqual(zone.calledWith_[1], NAME); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var method = 'POST'; + var path = '/'; + var query = 'query'; + var body = 'body'; + + compute.makeAuthorizedRequest_ = function(reqOpts, callback) { + assert.equal(reqOpts.method, method); + assert.equal(reqOpts.qs, query); + + var baseUri = 'https://www.googleapis.com/compute/v1/'; + assert.equal(reqOpts.uri, baseUri + 'projects/' + PROJECT_ID + path); + + assert.equal(reqOpts.json, body); + + callback(); + }; + + compute.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/network.js b/test/compute/network.js new file mode 100644 index 00000000000..893f46fb344 --- /dev/null +++ b/test/compute/network.js @@ -0,0 +1,334 @@ +/** + * 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 assert = require('assert'); +var extend = require('extend'); +var format = require('string-format-obj'); + +var Network = require('../../lib/compute/network.js'); + +describe('Network', function() { + var network; + + var COMPUTE = { projectId: 'project-id' }; + var NETWORK_NAME = 'network-name'; + var NETWORK_FULL_NAME = format('projects/{pId}/global/networks/{name}', { + pId: COMPUTE.projectId, + name: NETWORK_NAME + }); + + beforeEach(function() { + network = new Network(COMPUTE, NETWORK_NAME); + }); + + describe('instantiation', function() { + it('should localize the compute instance', function() { + assert.strictEqual(network.compute, COMPUTE); + }); + + it('should localize the name', function() { + assert.strictEqual(network.name, NETWORK_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof network.metadata, 'object'); + assert.strictEqual(Object.keys(network.metadata).length, 0); + }); + + it('should format the network name', function() { + var formatName_ = Network.formatName_; + var formattedName = 'projects/a/global/networks/b'; + + Network.formatName_ = function(compute, name) { + Network.formatName_ = formatName_; + + assert.strictEqual(compute, COMPUTE); + assert.strictEqual(name, NETWORK_NAME); + + return formattedName; + }; + + var network = new Network(COMPUTE, NETWORK_NAME); + assert(network.formattedName, formattedName); + }); + }); + + describe('formatName_', function() { + it('should format the name', function() { + var formattedName_ = Network.formatName_(COMPUTE, NETWORK_NAME); + assert.strictEqual(formattedName_, NETWORK_FULL_NAME); + }); + }); + + describe('createFirewall', function() { + it('should make the correct call to Compute', function(done) { + var name = 'firewall-name'; + var config = { a: 'b', c: 'd' }; + var expectedConfig = extend({}, config, { + network: network.formattedName + }); + + network.compute.createFirewall = function(name_, config_, callback) { + assert.strictEqual(name_, name); + assert.deepEqual(config_, expectedConfig); + callback(); + }; + + network.createFirewall(name, config, done); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + network.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + done(); + }; + + network.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + network.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + network.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() { + network.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + name: 'op-name' + }; + + beforeEach(function() { + network.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with Operation & Response', function(done) { + var operation = {}; + + network.compute.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + network.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() { + network.delete(); + }); + }); + }); + }); + + describe('firewall', function() { + it('should return a Firewall with the correct metadata', function() { + var name = 'firewall-name'; + var firewall = {}; + + network.compute.firewall = function(name_) { + assert.strictEqual(name_, name); + return firewall; + }; + + var firewallInstance = network.firewall(name); + assert.deepEqual(firewallInstance.metadata, { + network: network.formattedName + }); + }); + }); + + describe('getFirewalls', function() { + it('should make the correct call to Compute', function(done) { + var options = { a: 'b', c: 'd' }; + var expectedOptions = extend({}, options, { + filter: 'network eq .*' + network.formattedName + }); + + network.compute.getFirewalls = function(options, callback) { + assert.deepEqual(options, expectedOptions); + callback(); + }; + + network.getFirewalls(options, done); + }); + + it('should not require options', function(done) { + network.compute.getFirewalls = function(options, callback) { + callback(); + }; + + network.getFirewalls(done); + }); + + it('should not require any arguments', function(done) { + network.compute.getFirewalls = function(options, callback) { + assert.deepEqual(options, { + filter: 'network eq .*' + network.formattedName + }); + assert.strictEqual(typeof callback, 'undefined'); + done(); + }; + + network.getFirewalls(); + }); + + it('should return the result of calling Compute', function() { + var resultOfGetFirewalls = {}; + + network.compute.getFirewalls = function() { + return resultOfGetFirewalls; + }; + + assert.strictEqual(network.getFirewalls(), resultOfGetFirewalls); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + network.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + network.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + network.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + network.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + network.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + network.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + network.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(network.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + network.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + network.getMetadata(); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/global/networks/' + network.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + network.compute.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + network.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/operation.js b/test/compute/operation.js new file mode 100644 index 00000000000..54ad6935eb2 --- /dev/null +++ b/test/compute/operation.js @@ -0,0 +1,381 @@ +/** + * 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 assert = require('assert'); + +var Operation = require('../../lib/compute/operation.js'); +var util = require('../../lib/common/util.js'); + +describe('Operation', function() { + var SCOPE = {}; + var OPERATION_NAME = 'operation-name'; + + var operation; + + beforeEach(function() { + operation = new Operation(SCOPE, OPERATION_NAME); + }); + + describe('instantiation', function() { + it('should localize the scope', function() { + assert.strictEqual(operation.scope, SCOPE); + }); + + it('should localize the name', function() { + assert.strictEqual(operation.name, OPERATION_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof operation.metadata, 'object'); + assert.strictEqual(Object.keys(operation.metadata).length, 0); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + operation.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + operation.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = {}; + + beforeEach(function() { + operation.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API resp', function(done) { + operation.delete(function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + operation.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = {}; + + beforeEach(function() { + operation.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with error & API resp', function(done) { + operation.delete(function(err, apiResponse_) { + assert.ifError(err); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + operation.delete(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + operation.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + operation.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + operation.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should ignore false errors', function(done) { + var apiResponse = { + name: operation.name, + error: { + errors: [] + } + }; + + operation.makeReq_ = function(method, path, query, body, callback) { + callback(apiResponse.error, apiResponse); + }; + + operation.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should execute callback with error and API response', function(done) { + operation.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + operation.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + operation.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + operation.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(operation.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + operation.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + operation.getMetadata(); + }); + }); + }); + }); + + describe('onComplete', function() { + // Set interval to 0 so our tests don't waste time. + var OPTIONS = { interval: 0 }; + + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + var apiResponseWithIncompleteStatus = { status: 'INCOMPLETE' }; + var apiResponseWithCompleteStatus = { status: 'DONE' }; + + function getMetadataRespondsWithError(callback) { + callback(error, apiResponse); + } + + function getMetadataRespondsWithIncompleteStatus(callback) { + callback(null, apiResponseWithIncompleteStatus); + } + + function getMetadataRespondsWithCompleteStatus(callback) { + callback(null, apiResponseWithCompleteStatus); + } + + describe('options.maxAttempts', function() { + it('should default to 10', function(done) { + var numAttemptsMade = 0; + + operation.getMetadata = function(callback) { + numAttemptsMade++; + getMetadataRespondsWithIncompleteStatus(callback); + }; + + operation.onComplete(OPTIONS, function() { + assert.strictEqual(numAttemptsMade, 10); + done(); + }); + }); + + it('should allow overriding', function(done) { + var options = { maxAttempts: 3, interval: 0 }; + var numAttemptsMade = 0; + + operation.getMetadata = function(callback) { + numAttemptsMade++; + getMetadataRespondsWithIncompleteStatus(callback); + }; + + operation.onComplete(options, function() { + assert.strictEqual(numAttemptsMade, options.maxAttempts); + done(); + }); + }); + }); + + describe('options.interval', function() { + it('should default to 3000ms', function(done) { + this.timeout(3100); + + operation.getMetadata = getMetadataRespondsWithIncompleteStatus; + + var started = Date.now(); + operation.onComplete({ maxAttempts: 1 }, function() { + var ended = Date.now(); + + assert(ended - started > 2900 && ended - started < 3100); + done(); + }); + }); + + it('should allow overriding', function(done) { + operation.getMetadata = getMetadataRespondsWithIncompleteStatus; + + var started = Date.now(); + operation.onComplete({ maxAttempts: 1, interval: 1000 }, function() { + var ended = Date.now(); + + assert(ended - started > 900 && ended - started < 1100); + done(); + }); + }); + }); + + it('should put the interval on the leading side', function(done) { + // (It should wait interval before making first request) + var started = Date.now(); + operation.getMetadata = function() { + var ended = Date.now(); + + assert(ended - started > 900 && ended - started < 1100); + done(); + }; + + operation.onComplete({ maxAttempts: 1, interval: 1000 }, util.noop); + }); + + it('should return an error if maxAttempts is exceeded', function(done) { + var options = { maxAttempts: 1, interval: 0 }; + + operation.getMetadata = getMetadataRespondsWithIncompleteStatus; + + operation.onComplete(options, function(err, metadata) { + assert.strictEqual(err.code, 'OPERATION_INCOMPLETE'); + assert.strictEqual(err.message, 'Operation did not complete.'); + + assert.strictEqual(metadata, operation.metadata); + done(); + }); + }); + + describe('getMetadata', function() { + describe('error', function() { + it('should execute callback with error & API response', function(done) { + operation.getMetadata = getMetadataRespondsWithError; + + operation.onComplete(OPTIONS, function(err, metadata) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + it('should exec callback with metadata when done', function(done) { + operation.getMetadata = getMetadataRespondsWithCompleteStatus; + + operation.onComplete(OPTIONS, function(err, metadata) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponseWithCompleteStatus); + done(); + }); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Scope', function(done) { + var expectedPathPrefix = '/operations/' + operation.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + operation.scope.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + operation.makeReq_(method, path, query, body, done); + }); + + it('should prefix the path with /global if Compute', function(done) { + var expectedPathPrefix = '/global/operations/' + operation.name; + + function Compute() {} + operation.scope = new Compute(); + + operation.scope.makeReq_ = function(method, path) { + assert.strictEqual(path, expectedPathPrefix + '/test'); + done(); + }; + + operation.makeReq_(null, '/test'); + }); + }); +}); diff --git a/test/compute/region.js b/test/compute/region.js new file mode 100644 index 00000000000..f52373d5bb7 --- /dev/null +++ b/test/compute/region.js @@ -0,0 +1,512 @@ +/** + * 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'); + +function FakeAddress() { + this.calledWith_ = [].slice.call(arguments); +} + +function FakeOperation() { + this.calledWith_ = [].slice.call(arguments); +} + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'Region') { + return; + } + + extended = true; + methods = arrify(methods); + assert.equal(Class.name, 'Region'); + assert.deepEqual(methods, ['getAddresses', 'getOperations']); + } +}; + +describe('Region', function() { + var Region; + var region; + + var COMPUTE = { + authConfig: { a: 'b', c: 'd' } + }; + var REGION_NAME = 'us-central1'; + + before(function() { + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.registerMock('./address.js', FakeAddress); + mockery.registerMock('./operation.js', FakeOperation); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Region = require('../../lib/compute/region.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + region = new Region(COMPUTE, REGION_NAME); + }); + + describe('instantiation', function() { + it('should extend the correct methods', function() { + assert(extended); // See `fakeStreamRouter.extend` + }); + + it('should localize the compute instance', function() { + assert.strictEqual(region.compute, COMPUTE); + }); + + it('should localize the name', function() { + assert.strictEqual(region.name, REGION_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof region.metadata, 'object'); + assert.strictEqual(Object.keys(region.metadata).length, 0); + }); + }); + + describe('address', function() { + var NAME = 'address-name'; + + it('should return an Address object', function() { + var address = region.address(NAME); + assert(address instanceof FakeAddress); + assert.strictEqual(address.calledWith_[0], region); + assert.strictEqual(address.calledWith_[1], NAME); + }); + }); + + describe('createAddress', function() { + var NAME = 'address-name'; + var OPTIONS = { a: 'b', c: 'd' }; + var EXPECTED_BODY = extend({}, OPTIONS, { name: NAME }); + + it('should not require any options', function(done) { + var expectedBody = { name: NAME }; + + region.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body, expectedBody); + done(); + }; + + region.createAddress(NAME, assert.ifError); + }); + + it('should make the correct API request', function(done) { + region.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/addresses'); + assert.strictEqual(query, null); + assert.deepEqual(body, EXPECTED_BODY); + + done(); + }; + + region.createAddress(NAME, OPTIONS, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + region.createAddress(NAME, OPTIONS, function(err, address_, op, resp) { + assert.strictEqual(err, error); + assert.strictEqual(address_, null); + assert.strictEqual(op, null); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'operation-name' }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Address, Op & apiResponse', function(done) { + var address = {}; + var operation = {}; + + region.address = function(name) { + assert.strictEqual(name, NAME); + return address; + }; + + region.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + region.createAddress(NAME, OPTIONS, function(err, address_, op, resp) { + assert.ifError(err); + + assert.strictEqual(address_, address); + + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, resp); + + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + }); + + describe('getAddresses', function() { + it('should accept only a callback', function(done) { + region.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + region.getAddresses(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + region.makeReq_ = function(method, path, query_, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/addresses'); + assert.strictEqual(query_, query); + assert.strictEqual(body, null); + + done(); + }; + + region.getAddresses(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + region.getAddresses({}, function(err, addresses, nextQuery, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(addresses, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { name: 'operation-name' } + ] + }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, 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 + }; + + region.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + region.getAddresses({}, function(err, addresses, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Operations & API resp', function(done) { + var address = {}; + + region.address = function(name) { + assert.strictEqual(name, apiResponse.items[0].name); + return address; + }; + + region.getAddresses({}, function(err, addresses, nextQuery, apiResp) { + assert.ifError(err); + + assert.strictEqual(addresses[0], address); + assert.strictEqual(addresses[0].metadata, apiResponse.items[0]); + + assert.strictEqual(apiResp, apiResponse); + + done(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + region.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + region.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + region.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + region.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + region.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(region.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + region.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + region.getMetadata(); + }); + }); + }); + }); + + describe('getOperations', function() { + it('should accept only a callback', function(done) { + region.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + region.getOperations(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + region.makeReq_ = function(method, path, query_, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/operations'); + assert.strictEqual(query_, query); + assert.strictEqual(body, null); + + done(); + }; + + region.getOperations(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + region.getOperations({}, function(err, operations, nextQuery, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(operations, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { name: 'operation-name' } + ] + }; + + beforeEach(function() { + region.makeReq_ = function(method, path, query, body, 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 + }; + + region.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + region.getOperations({}, function(err, operations, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Operations & API resp', function(done) { + var operation = {}; + + region.operation = function(name) { + assert.strictEqual(name, apiResponse.items[0].name); + return operation; + }; + + region.getOperations({}, function(err, operations, nextQuery, apiResp) { + assert.ifError(err); + + assert.strictEqual(operations[0], operation); + assert.strictEqual(operations[0].metadata, apiResponse.items[0]); + + assert.strictEqual(apiResp, apiResponse); + + done(); + }); + }); + }); + }); + + describe('operation', function() { + var NAME = 'operation-name'; + + it('should return a Operation object', function() { + var operation = region.operation(NAME); + assert(operation instanceof FakeOperation); + assert.strictEqual(operation.calledWith_[0], region); + assert.strictEqual(operation.calledWith_[1], NAME); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/regions/' + region.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + region.compute.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + region.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/snapshot.js b/test/compute/snapshot.js new file mode 100644 index 00000000000..6dc6dbb9ecc --- /dev/null +++ b/test/compute/snapshot.js @@ -0,0 +1,223 @@ +/** + * 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 assert = require('assert'); +var Snapshot = require('../../lib/compute/snapshot.js'); + +describe('Snapshot', function() { + var COMPUTE = {}; + var SNAPSHOT_NAME = 'snapshot-name'; + + var snapshot; + + beforeEach(function() { + snapshot = new Snapshot(COMPUTE, SNAPSHOT_NAME); + }); + + describe('instantiation', function() { + it('should localize the compute instance', function() { + assert.strictEqual(snapshot.compute, COMPUTE); + }); + + it('should localize the name', function() { + assert.strictEqual(snapshot.name, SNAPSHOT_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof snapshot.metadata, 'object'); + assert.strictEqual(Object.keys(snapshot.metadata).length, 0); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + snapshot.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + snapshot.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + snapshot.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should exec the callback with error & API response', function(done) { + snapshot.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() { + snapshot.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'operation-name' }; + + beforeEach(function() { + snapshot.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Operation & API response', function(done) { + var operation = {}; + + snapshot.compute.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + snapshot.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() { + snapshot.delete(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + snapshot.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + snapshot.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + snapshot.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + snapshot.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + snapshot.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + snapshot.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + snapshot.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(snapshot.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + snapshot.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + snapshot.getMetadata(); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/global/snapshots/' + snapshot.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + snapshot.compute.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + snapshot.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/compute/vm.js b/test/compute/vm.js new file mode 100644 index 00000000000..fa8792a3089 --- /dev/null +++ b/test/compute/vm.js @@ -0,0 +1,538 @@ +/** + * 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 assert = require('assert'); +var extend = require('extend'); + +var Disk = require('../../lib/compute/disk.js'); +var util = require('../../lib/common/util.js'); +var VM = require('../../lib/compute/vm.js'); + +describe('VM', function() { + var vm; + + var COMPUTE = { projectId: 'project-id' }; + var ZONE = { compute: COMPUTE, name: 'us-central1-a' }; + var VM_NAME = 'vm-name'; + + var DISK = new Disk(ZONE, 'disk-name'); + + beforeEach(function() { + vm = new VM(ZONE, VM_NAME); + }); + + describe('instantiation', function() { + it('should localize the zone', function() { + assert.strictEqual(vm.zone, ZONE); + }); + + it('should localize the name', function() { + assert.strictEqual(vm.name, VM_NAME); + }); + }); + + describe('attachDisk', function() { + var CONFIG = {}; + var EXPECTED_BODY = { source: DISK.formattedName }; + + it('should throw if a Disk object is not provided', function() { + assert.throws(function() { + vm.attachDisk('disk-3', CONFIG, assert.ifError); + }, /A Disk object must be provided/); + + assert.doesNotThrow(function() { + vm.makeReq_ = util.noop; + vm.attachDisk(DISK, CONFIG, assert.ifError); + }); + }); + + it('should not require an options object', function(done) { + vm.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body, EXPECTED_BODY); + done(); + }; + + vm.attachDisk(DISK, assert.ifError); + }); + + describe('options.readOnly', function() { + var CONFIG = extend({}, CONFIG, { readOnly: true }); + + it('should set the correct mode', function(done) { + var expectedBody = extend({}, EXPECTED_BODY, { mode: 'READ_ONLY' }); + + vm.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body, expectedBody); + done(); + }; + + vm.attachDisk(DISK, CONFIG, assert.ifError); + }); + + it('should delete the readOnly property', function(done) { + vm.makeReq_ = function(method, path, query, body) { + assert.strictEqual(typeof body.readOnly, 'undefined'); + done(); + }; + + vm.attachDisk(DISK, CONFIG, assert.ifError); + }); + }); + + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/attachDisk'); + assert.strictEqual(query, null); + assert.deepEqual(body, EXPECTED_BODY); + + callback(); + }; + + vm.attachDisk(DISK, CONFIG, done); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + callback(); + }; + + vm.delete(done); + }); + + it('should not require a callback', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.doesNotThrow(function() { + callback(); + done(); + }); + }; + + vm.delete(); + }); + }); + + describe('detachDisk', function() { + it('should throw if a Disk is not provided', function() { + assert.throws(function() { + vm.detachDisk('disk-name'); + }, /A Disk object must be provided/); + + assert.doesNotThrow(function() { + vm.makeReq_ = util.noop; + vm.detachDisk(DISK); + }); + }); + + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/detachDisk'); + assert.deepEqual(query, { deviceName: DISK.name }); + assert.strictEqual(body, null); + + callback(); + }; + + vm.detachDisk(DISK, done); + }); + + it('should not require a callback', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.doesNotThrow(function() { + callback(); + done(); + }); + }; + + vm.detachDisk(DISK); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + vm.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + vm.makeReq_ = function(method, path, query, body, callback) { + callback(error, null/*usually an operation*/, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + vm.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + vm.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + vm.makeReq_ = function(method, path, query, body, callback) { + callback(null, null/*usually an operation*/, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + vm.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(vm.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + vm.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + vm.getMetadata(); + }); + }); + }); + }); + + describe('getSerialPortOutput', function() { + var EXPECTED_QUERY = { port: 1 }; + + it('should default to port 1', function(done) { + vm.makeReq_ = function(method, path, query) { + assert.strictEqual(query.port, 1); + done(); + }; + + vm.getSerialPortOutput(assert.ifError); + }); + + it('should override the default port', function(done) { + var port = 8001; + + vm.makeReq_ = function(method, path, query) { + assert.strictEqual(query.port, port); + done(); + }; + + vm.getSerialPortOutput(port, assert.ifError); + }); + + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/serialPort'); + assert.deepEqual(query, EXPECTED_QUERY); + assert.strictEqual(body, null); + + done(); + }; + + vm.getSerialPortOutput(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + vm.makeReq_ = function(method, path, query, body, callback) { + callback(error, null/*usually an operation*/, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + vm.getSerialPortOutput(function(err, output, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(output, null); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { contents: 'contents' }; + + beforeEach(function() { + vm.makeReq_ = function(method, path, query, body, callback) { + callback(null, null/*usually an operation*/, apiResponse); + }; + }); + + it('should exec callback with contents & API response', function(done) { + vm.getSerialPortOutput(function(err, output, apiResponse_) { + assert.ifError(err); + assert.strictEqual(output, apiResponse.contents); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); + + describe('getTags', function() { + it('should get metadata', function(done) { + vm.getMetadata = function() { + done(); + }; + + vm.getTags(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + vm.getMetadata = function(callback) { + callback(error, null, apiResponse); + }; + }); + + it('should execute callback with error', function(done) { + vm.getTags(function(err, tags, fingerprint, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(tags, null); + assert.strictEqual(fingerprint, null); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + + describe('success', function() { + var metadata = { + tags: { + items: [], + fingerprint: 'fingerprint' + } + }; + + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + vm.getMetadata = function(callback) { + callback(null, metadata, apiResponse); + }; + }); + + it('should execute callback with tags and fingerprint', function(done) { + vm.getTags(function(err, tags, fingerprint, apiResponse_) { + assert.ifError(err); + + assert.strictEqual(tags, metadata.tags.items); + assert.strictEqual(fingerprint, metadata.tags.fingerprint); + assert.strictEqual(apiResponse_, apiResponse); + + done(); + }); + }); + }); + }); + + describe('reset', function() { + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/reset'); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + callback(); + }; + + vm.reset(done); + }); + + it('should not require a callback', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.doesNotThrow(function() { + callback(); + done(); + }); + }; + + vm.reset(); + }); + }); + + describe('start', function() { + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/start'); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + callback(); + }; + + vm.start(done); + }); + + it('should not require a callback', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.doesNotThrow(function() { + callback(); + done(); + }); + }; + + vm.start(); + }); + }); + + describe('stop', function() { + it('should make the correct API request', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/stop'); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + callback(); + }; + + vm.stop(done); + }); + + it('should not require a callback', function(done) { + vm.makeReq_ = function(method, path, query, body, callback) { + assert.doesNotThrow(function() { + callback(); + done(); + }); + }; + + vm.stop(); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/instances/' + vm.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + vm.zone.makeReq_ = function(method_, path_, query_, body_) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + + done(); + }; + + vm.makeReq_(method, path, query, body, assert.ifError); + }); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + vm.zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + vm.makeReq_('POST', '/', {}, {}, function(err, operation, resp) { + assert.strictEqual(err, error); + assert.strictEqual(operation, null); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'operation-name' }; + + beforeEach(function() { + vm.zone.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with a Zone object & API resp', function(done) { + var operation = {}; + + vm.zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + vm.makeReq_('POST', '/', {}, {}, function(err, operation_, resp) { + assert.ifError(err); + assert.strictEqual(operation_, operation); + assert.strictEqual(resp, apiResponse); + done(); + }); + }); + }); +}); diff --git a/test/compute/zone.js b/test/compute/zone.js new file mode 100644 index 00000000000..684342d2ce2 --- /dev/null +++ b/test/compute/zone.js @@ -0,0 +1,1004 @@ +/** + * 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 gceImages = require('gce-images'); +var mockery = require('mockery'); + +var util = require('../../lib/common/util.js'); + +var gceImagesOverride = null; +function fakeGceImages() { + return (gceImagesOverride || gceImages).apply(null, arguments); +} + +function FakeDisk() { + this.calledWith_ = [].slice.call(arguments); +} + +function FakeOperation() { + this.calledWith_ = [].slice.call(arguments); +} + +function FakeVM() { + this.calledWith_ = [].slice.call(arguments); +} + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'Zone') { + return; + } + + extended = true; + methods = arrify(methods); + assert.equal(Class.name, 'Zone'); + assert.deepEqual(methods, ['getDisks', 'getOperations', 'getVMs']); + } +}; + +describe('Zone', function() { + var Zone; + var zone; + + var COMPUTE = { + authConfig: { a: 'b', c: 'd' } + }; + var ZONE_NAME = 'us-central1-a'; + + before(function() { + mockery.registerMock('gce-images', fakeGceImages); + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.registerMock('./disk.js', FakeDisk); + mockery.registerMock('./operation.js', FakeOperation); + mockery.registerMock('./vm.js', FakeVM); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Zone = require('../../lib/compute/zone.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + gceImagesOverride = null; + zone = new Zone(COMPUTE, ZONE_NAME); + }); + + describe('instantiation', function() { + it('should extend the correct methods', function() { + assert(extended); // See `fakeStreamRouter.extend` + }); + + it('should localize the compute instance', function() { + assert.strictEqual(zone.compute, COMPUTE); + }); + + it('should localize the name', function() { + assert.strictEqual(zone.name, ZONE_NAME); + }); + + it('should default metadata to an empty object', function() { + assert.strictEqual(typeof zone.metadata, 'object'); + assert.strictEqual(Object.keys(zone.metadata).length, 0); + }); + + it('should create a gceImages instance', function() { + var gceVal = 'ok'; + + gceImagesOverride = function(authConfig) { + assert.strictEqual(authConfig, COMPUTE.authConfig); + return gceVal; + }; + + var newZone = new Zone(COMPUTE, ZONE_NAME); + assert.strictEqual(newZone.gceImages, gceVal); + }); + }); + + describe('createDisk', function() { + var NAME = 'disk-name'; + + beforeEach(function() { + zone.makeReq_ = util.noop; + }); + + it('should use the image property as qs.sourceImages', function(done) { + var config = { + image: 'abc' + }; + + zone.makeReq_ = function(method, path, query) { + assert.strictEqual(query.sourceImage, config.image); + done(); + }; + + zone.createDisk(NAME, config, assert.ifError); + }); + + describe('config.os', function() { + var CONFIG = { + os: 'os-name' + }; + + it('should get the latest image', function(done) { + zone.gceImages.getLatest = function(os) { + assert.strictEqual(os, CONFIG.os); + done(); + }; + + zone.createDisk(NAME, CONFIG, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + + beforeEach(function() { + zone.gceImages.getLatest = function(os, callback) { + callback(error); + }; + }); + + it('should execute callback with error', function(done) { + zone.createDisk(NAME, CONFIG, function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + }); + + describe('success', function() { + var gceImagesResp = { + selfLink: 'http://selflink' + }; + + var expectedConfig = { + name: NAME, + sourceImage: gceImagesResp.selfLink + }; + + it('should call createDisk with the correct config', function(done) { + zone.gceImages.getLatest = function(os, callback) { + zone.createDisk = function(name, config, callback) { + assert.strictEqual(name, NAME); + assert.deepEqual(config, expectedConfig); + callback(); + }; + + callback(null, gceImagesResp); + }; + + zone.createDisk(NAME, CONFIG, done); + }); + }); + }); + + describe('API request', function() { + var CONFIG = { + a: 'b', + c: 'd' + }; + + var expectedBody = { + name: NAME, + a: 'b', + c: 'd' + }; + + it('should make the correct API request', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/disks'); + assert.deepEqual(query, {}); + assert.deepEqual(body, expectedBody); + + done(); + }; + + zone.createDisk(NAME, CONFIG, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.createDisk(NAME, CONFIG, function(err, disk, op, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(disk, null); + assert.strictEqual(op, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'operation-name' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Disk, Op & apiResponse', function(done) { + var disk = {}; + var operation = {}; + + zone.disk = function(name) { + assert.strictEqual(name, NAME); + return disk; + }; + + zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + zone.createDisk(NAME, CONFIG, function(err, disk_, op, apiResp) { + assert.ifError(err); + + assert.strictEqual(disk_, disk); + + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, apiResp); + + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + }); + }); + + describe('createVM', function() { + var NAME = 'new-vm'; + + var CONFIG = {}; + + var EXPECTED_CONFIG = { + name: NAME, + machineType: 'zones/' + ZONE_NAME + '/machineTypes/n1-standard-1', + networkInterfaces: [ + { + network: 'global/networks/default' + } + ] + }; + + describe('config.machineType', function() { + var CONFIG = { + machineType: 'f1-micro' + }; + + it('should format a given machine type', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.strictEqual( + body.machineType, + 'zones/' + ZONE_NAME + '/machineTypes/' + CONFIG.machineType + ); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + }); + + describe('config.tags', function() { + var CONFIG = { + tags: ['a', 'b'] + }; + + it('should accept an array of tags', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.tags.items, CONFIG.tags); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + }); + + describe('config.http', function() { + var CONFIG = { + http: true + }; + + it('should add a network interface accessConfig', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.networkInterfaces[0].accessConfigs[0], { + type: 'ONE_TO_ONE_NAT' + }); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + + it('should add an http tag', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert(body.tags.items.indexOf('http-server') > -1); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + + it('should not overwrite existing tags', function(done) { + var config = { + http: true, + tags: { + items: ['a', 'b'] + } + }; + + var expectedTags = ['a', 'b', 'http-server']; + + zone.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.tags.items, expectedTags); + done(); + }; + + zone.createVM(NAME, config, assert.ifError); + }); + + it('should delete the https property', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.strictEqual(body.https, undefined); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + }); + + describe('config.https', function() { + var CONFIG = { + https: true + }; + + it('should add a network interface accessConfig', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.networkInterfaces[0].accessConfigs[0], { + type: 'ONE_TO_ONE_NAT' + }); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + + it('should add an https tag', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert(body.tags.items.indexOf('https-server') > -1); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + + it('should not overwrite existing tags', function(done) { + var config = { + https: true, + tags: { + items: ['a', 'b'] + } + }; + + var expectedTags = ['a', 'b', 'https-server']; + + zone.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body.tags.items, expectedTags); + done(); + }; + + zone.createVM(NAME, config, assert.ifError); + }); + + it('should delete the https property', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.strictEqual(body.https, undefined); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + }); + + describe('config.os', function() { + var CONFIG = { + os: 'os-name' + }; + + it('should get the latest image', function(done) { + zone.gceImages.getLatest = function(os) { + assert.strictEqual(os, CONFIG.os); + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + + beforeEach(function() { + zone.gceImages.getLatest = function(os, callback) { + callback(error); + }; + }); + + it('should execute callback with error', function(done) { + zone.createVM(NAME, CONFIG, function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + }); + + describe('success', function() { + var gceImagesResp = { + selfLink: 'http://selflink' + }; + + var expectedConfig = extend({}, EXPECTED_CONFIG, { + disks: [ + { + boot: true, + initializeParams: { + sourceImage: gceImagesResp.selfLink + } + } + ] + }); + + it('should call createVM with the correct config', function(done) { + zone.gceImages.getLatest = function(os, callback) { + zone.createVM = function(name, config, callback) { + assert.strictEqual(name, NAME); + assert.deepEqual(config, expectedConfig); + callback(); + }; + + callback(null, gceImagesResp); + }; + + zone.createVM(NAME, CONFIG, done); + }); + }); + }); + + describe('API request', function() { + it('should make the correct API request', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/instances'); + assert.deepEqual(query, null); + assert.deepEqual(body, EXPECTED_CONFIG); + + done(); + }; + + zone.createVM(NAME, CONFIG, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.createVM(NAME, CONFIG, function(err, vm, op, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(vm, null); + assert.strictEqual(op, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { name: 'operation-name' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Disk, Op & apiResponse', function(done) { + var vm = {}; + var operation = {}; + + zone.vm = function(name) { + assert.strictEqual(name, NAME); + return vm; + }; + + zone.operation = function(name) { + assert.strictEqual(name, apiResponse.name); + return operation; + }; + + zone.createVM(NAME, CONFIG, function(err, vm_, op, apiResp) { + assert.ifError(err); + + assert.strictEqual(vm_, vm); + + assert.strictEqual(op, operation); + assert.strictEqual(op.metadata, apiResp); + + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + }); + }); + + describe('disk', function() { + var NAME = 'disk-name'; + + it('should return a Disk object', function() { + var disk = zone.disk(NAME); + assert(disk instanceof FakeDisk); + assert.strictEqual(disk.calledWith_[0], zone); + assert.strictEqual(disk.calledWith_[1], NAME); + }); + }); + + describe('getDisks', function() { + it('should accept only a callback', function(done) { + zone.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + zone.getDisks(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + zone.makeReq_ = function(method, path, query_, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/disks'); + assert.strictEqual(query_, query); + assert.strictEqual(body, null); + + done(); + }; + + zone.getDisks(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.getDisks({}, function(err, disks, nextQuery, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(disks, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { name: 'disk-name' } + ] + }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, 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.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + zone.getDisks({}, function(err, disks, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Disks & API resp', function(done) { + var disk = {}; + + zone.disk = function(name) { + assert.strictEqual(name, apiResponse.items[0].name); + return disk; + }; + + zone.getDisks({}, function(err, disks, nextQuery, apiResp) { + assert.ifError(err); + + assert.strictEqual(disks[0], disk); + assert.strictEqual(disks[0].metadata, apiResponse.items[0]); + + assert.strictEqual(apiResp, apiResponse); + + done(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + zone.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + zone.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + zone.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + zone.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(zone.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + zone.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + }); + + describe('getOperations', function() { + it('should accept only a callback', function(done) { + zone.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + zone.getOperations(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + zone.makeReq_ = function(method, path, query_, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/operations'); + assert.strictEqual(query_, query); + assert.strictEqual(body, null); + + done(); + }; + + zone.getOperations(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.getOperations({}, function(err, operations, nextQuery, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { name: 'operation-name' } + ] + }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, 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.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + zone.getOperations({}, function(err, operations, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Operations & API resp', function(done) { + var operation = {}; + + zone.operation = function(name) { + assert.strictEqual(name, apiResponse.items[0].name); + return operation; + }; + + zone.getOperations({}, function(err, operations, nextQuery, apiResp) { + assert.ifError(err); + + assert.strictEqual(operations[0], operation); + assert.strictEqual(operations[0].metadata, apiResponse.items[0]); + + assert.strictEqual(apiResp, apiResponse); + + done(); + }); + }); + }); + }); + + describe('getVMs', function() { + it('should accept only a callback', function(done) { + zone.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + zone.getVMs(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + zone.makeReq_ = function(method, path, query_, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/instances'); + assert.strictEqual(query_, query); + assert.strictEqual(body, null); + + done(); + }; + + zone.getVMs(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + zone.getVMs({}, function(err, vms, nextQuery, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + items: [ + { name: 'vm-name' } + ] + }; + + beforeEach(function() { + zone.makeReq_ = function(method, path, query, body, 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.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + zone.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 = {}; + + zone.vm = function(name) { + assert.strictEqual(name, apiResponse.items[0].name); + return vm; + }; + + zone.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('operation', function() { + var NAME = 'operation-name'; + + it('should return an Operation object', function() { + var operation = zone.operation(NAME); + assert(operation instanceof FakeOperation); + assert.strictEqual(operation.calledWith_[0], zone); + assert.strictEqual(operation.calledWith_[1], NAME); + }); + }); + + describe('vm', function() { + var NAME = 'vm-name'; + + it('should return a VM object', function() { + var vm = zone.vm(NAME); + assert(vm instanceof FakeVM); + assert.strictEqual(vm.calledWith_[0], zone); + assert.strictEqual(vm.calledWith_[1], NAME); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Compute', function(done) { + var expectedPathPrefix = '/zones/' + zone.name; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + zone.compute.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + zone.makeReq_(method, path, query, body, done); + }); + }); +});