From 8e6cf271210ddf2d2704682b74df4fd934ea283d Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sat, 24 Feb 2018 02:16:23 +1300 Subject: [PATCH] TRITON-19 Triton equivalent to AWS' termination protection Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 8 + lib/cloudapi2.js | 70 ++++++- lib/do_fwrule/do_instances.js | 2 + lib/do_instance/do_create.js | 9 + .../do_disable_deletion_protection.js | 125 ++++++++++++ .../do_enable_deletion_protection.js | 125 ++++++++++++ lib/do_instance/do_list.js | 2 + lib/do_instance/index.js | 10 +- lib/tritonapi.js | 86 ++++++++ .../cli-deletion-protection.test.js | 191 ++++++++++++++++++ test/integration/cli-fwrules.test.js | 2 +- test/integration/cli-nics.test.js | 2 +- test/integration/cli-snapshots.test.js | 2 +- test/integration/cli-subcommands.test.js | 2 + test/integration/helpers.js | 12 +- 15 files changed, 642 insertions(+), 6 deletions(-) create mode 100644 lib/do_instance/do_disable_deletion_protection.js create mode 100644 lib/do_instance/do_enable_deletion_protection.js create mode 100644 test/integration/cli-deletion-protection.test.js diff --git a/CHANGES.md b/CHANGES.md index ab1ae43..3484a88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,14 @@ Known issues: ## not yet released +- [TRITON-19] add support for deletion protection on instances. An instance with + the deletion protection flag set true cannot be destroyed until the flag is + set false. It is exposed through + `triton instance create --deletion-protection ...`, + `triton instance enable-deletion-protection ...`, and + `triton instance disable-deletion-protection ...`. This flag is only supported + on cloudapi versions 8.7.0 or above. + ## 5.9.0 - [TRITON-190] remove support for `triton instance create --brand=bhyve ...`. diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 5412cd5..98fcacb 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -1012,7 +1012,6 @@ function enableMachineFirewall(uuid, callback) { return this._doMachine('enable_firewall', uuid, callback); }; - /** * Disables machine firewall. * @@ -1024,6 +1023,28 @@ function disableMachineFirewall(uuid, callback) { return this._doMachine('disable_firewall', uuid, callback); }; +/** + * Enables machine deletion protection. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, null, res)` + */ +CloudApi.prototype.enableMachineDeletionProtection = +function enableMachineDeletionProtection(uuid, callback) { + return this._doMachine('enable_deletion_protection', uuid, callback); +}; + +/** + * Disables machine deletion protection. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, null, res)` + */ +CloudApi.prototype.disableMachineDeletionProtection = +function disableMachineDeletionProtection(uuid, callback) { + return this._doMachine('disable_deletion_protection', uuid, callback); +}; + /** * internal function for start/stop/reboot/enable_firewall/disable_firewall */ @@ -1234,6 +1255,53 @@ function waitForMachineFirewallEnabled(opts, cb) { }; + +/** + * Wait for a machine's `deletion_protection` field to go true or + * false/undefined. + * + * @param {Object} options + * - {String} id: Required. The machine UUID. + * - {Boolean} state: Required. The desired `deletion_protection` state. + * - {Number} interval: Optional. Time (in ms) to poll. + * @param {Function} callback of the form f(err, machine, res). + */ +CloudApi.prototype.waitForDeletionProtectionEnabled = +function waitForDeletionProtectionEnabled(opts, cb) { + var self = this; + + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.bool(opts.state, 'opts.state'); + assert.optionalNumber(opts.interval, 'opts.interval'); + assert.func(cb, 'cb'); + + var interval = opts.interval || 1000; + assert.ok(interval > 0, 'interval must be a positive number'); + + poll(); + + function poll() { + self.getMachine({ + id: opts.id + }, function getMachineCb(err, machine, res) { + if (err) { + cb(err, null, res); + return; + } + + // !! converts an undefined to a false + if (opts.state === !!machine.deletion_protection) { + cb(null, machine, res); + return; + } + + setTimeout(poll, interval); + }); + } +}; + + // --- machine tags /** diff --git a/lib/do_fwrule/do_instances.js b/lib/do_fwrule/do_instances.js index b549f16..f16d8ec 100644 --- a/lib/do_fwrule/do_instances.js +++ b/lib/do_fwrule/do_instances.js @@ -115,6 +115,7 @@ function do_instances(subcmd, opts, args, cb) { if (inst.docker) flags.push('D'); if (inst.firewall_enabled) flags.push('F'); if (inst.brand === 'kvm') flags.push('K'); + if (inst.deletion_protection) flags.push('P'); inst.flags = flags.length ? flags.join('') : undefined; }); @@ -164,6 +165,7 @@ do_instances.help = [ ' "D" docker instance', ' "F" firewall is enabled', ' "K" the brand is "kvm"', + ' "P" deletion protected', ' age* Approximate time since created, e.g. 1y, 2w.', ' img* The image "name@version", if available, else its', ' "shortid".' diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index b2e9631..273b1e7 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -397,6 +397,8 @@ function do_create(subcmd, opts, args, cb) { var opt = opts._order[i]; if (opt.key === 'firewall') { createOpts.firewall_enabled = opt.value; + } else if (opt.key === 'deletion_protection') { + createOpts.deletion_protection = opt.value; } } @@ -544,6 +546,13 @@ do_create.options = [ help: 'Enable Cloud Firewall on this instance. See ' + '' }, + { + names: ['deletion-protection'], + type: 'bool', + help: 'Enable Deletion Protection on this instance. Such an instance ' + + 'cannot be deleted until the protection is disabled. See ' + + '' + }, { names: ['volume', 'v'], type: 'arrayOfString', diff --git a/lib/do_instance/do_disable_deletion_protection.js b/lib/do_instance/do_disable_deletion_protection.js new file mode 100644 index 0000000..011201b --- /dev/null +++ b/lib/do_instance/do_disable_deletion_protection.js @@ -0,0 +1,125 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2018 Joyent, Inc. + * + * `triton instance disable-deletion-protection ...` + */ + +var assert = require('assert-plus'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_disable_deletion_protection(subcmd, opts, args, cb) { + assert.object(opts, 'opts'); + assert.arrayOfString(args, 'args'); + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('missing INST argument(s)')); + return; + } + + var cli = this.top; + + function wait(name, id, next) { + assert.string(name, 'name'); + assert.uuid(id, 'id'); + assert.func(next, 'next'); + + cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({ + id: id, + state: false + }, function (err, inst) { + if (err) { + next(err); + return; + } + + assert.ok(!inst.deletion_protection, 'inst ' + id + + ' deletion_protection not in expected state after ' + + 'waitForDeletionProtectionEnabled'); + + console.log('Disabled deletion protection for instance "%s"', name); + next(); + }); + } + + function disableOne(name, next) { + assert.string(name, 'name'); + assert.func(next, 'next'); + + cli.tritonapi.disableInstanceDeletionProtection({ + id: name + }, function disableProtectionCb(err, fauxInst) { + if (err) { + next(err); + return; + } + + console.log('Disabling deletion protection for instance "%s"', + name); + + if (opts.wait) { + wait(name, fauxInst.id, next); + } else { + next(); + } + }); + } + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + vasync.forEachParallel({ + inputs: args, + func: disableOne + }, function vasyncCb(err) { + cb(err); + }); + }); +} + + +do_disable_deletion_protection.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['wait', 'w'], + type: 'bool', + help: 'Wait for deletion protection to be removed.' + } +]; +do_disable_deletion_protection.synopses = [ + '{{name}} disable-deletion-protection [OPTIONS] INST [INST ...]' +]; +do_disable_deletion_protection.help = [ + 'Disable deletion protection on one or more instances.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where "INST" is an instance name, id, or short id.' +].join('\n'); + +do_disable_deletion_protection.completionArgtypes = ['tritoninstance']; + +module.exports = do_disable_deletion_protection; diff --git a/lib/do_instance/do_enable_deletion_protection.js b/lib/do_instance/do_enable_deletion_protection.js new file mode 100644 index 0000000..a598379 --- /dev/null +++ b/lib/do_instance/do_enable_deletion_protection.js @@ -0,0 +1,125 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2018 Joyent, Inc. + * + * `triton instance enable-deletion-protection ...` + */ + +var assert = require('assert-plus'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_enable_deletion_protection(subcmd, opts, args, cb) { + assert.object(opts, 'opts'); + assert.arrayOfString(args, 'args'); + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('missing INST argument(s)')); + return; + } + + var cli = this.top; + + function wait(name, id, next) { + assert.string(name, 'name'); + assert.uuid(id, 'id'); + assert.func(next, 'next'); + + cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({ + id: id, + state: true + }, function (err, inst) { + if (err) { + next(err); + return; + } + + assert.ok(inst.deletion_protection, 'inst ' + id + + ' deletion_protection not in expected state after ' + + 'waitForDeletionProtectionEnabled'); + + console.log('Enabled deletion protection for instance "%s"', name); + next(); + }); + } + + function enableOne(name, next) { + assert.string(name, 'name'); + assert.func(next, 'next'); + + cli.tritonapi.enableInstanceDeletionProtection({ + id: name + }, function enableProtectionCb(err, fauxInst) { + if (err) { + next(err); + return; + } + + console.log('Enabling deletion protection for instance "%s"', + name); + + if (opts.wait) { + wait(name, fauxInst.id, next); + } else { + next(); + } + }); + } + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + vasync.forEachParallel({ + inputs: args, + func: enableOne + }, function vasyncCb(err) { + cb(err); + }); + }); +} + + +do_enable_deletion_protection.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['wait', 'w'], + type: 'bool', + help: 'Wait for deletion protection to be enabled.' + } +]; +do_enable_deletion_protection.synopses = [ + '{{name}} enable-deletion-protection [OPTIONS] INST [INST ...]' +]; +do_enable_deletion_protection.help = [ + 'Enable deletion protection for one or more instances.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where "INST" is an instance name, id, or short id.' +].join('\n'); + +do_enable_deletion_protection.completionArgtypes = ['tritoninstance']; + +module.exports = do_enable_deletion_protection; diff --git a/lib/do_instance/do_list.js b/lib/do_instance/do_list.js index 3892efc..19b04ea 100644 --- a/lib/do_instance/do_list.js +++ b/lib/do_instance/do_list.js @@ -154,6 +154,7 @@ function do_list(subcmd, opts, args, callback) { if (inst.docker) flags.push('D'); if (inst.firewall_enabled) flags.push('F'); if (inst.brand === 'kvm') flags.push('K'); + if (inst.deletion_protection) flags.push('P'); inst.flags = flags.length ? flags.join('') : undefined; }); @@ -213,6 +214,7 @@ do_list.help = [ ' "D" docker instance', ' "F" firewall is enabled', ' "K" the brand is "kvm"', + ' "P" deletion protected', ' age* Approximate time since created, e.g. 1y, 2w.', ' img* The image "name@version", if available, else its', ' "shortid".', diff --git a/lib/do_instance/index.js b/lib/do_instance/index.js index 8dfb985..483d407 100644 --- a/lib/do_instance/index.js +++ b/lib/do_instance/index.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2015 Joyent, Inc. + * Copyright 2018 Joyent, Inc. * * `triton instance ...` */ @@ -45,6 +45,9 @@ function InstanceCLI(top) { 'enable-firewall', 'disable-firewall', { group: '' }, + 'enable-deletion-protection', + 'disable-deletion-protection', + { group: '' }, 'ssh', 'ip', 'wait', @@ -78,6 +81,11 @@ InstanceCLI.prototype.do_fwrules = require('./do_fwrules'); InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall'); InstanceCLI.prototype.do_disable_firewall = require('./do_disable_firewall'); +InstanceCLI.prototype.do_enable_deletion_protection = + require('./do_enable_deletion_protection'); +InstanceCLI.prototype.do_disable_deletion_protection = + require('./do_disable_deletion_protection'); + InstanceCLI.prototype.do_ssh = require('./do_ssh'); InstanceCLI.prototype.do_ip = require('./do_ip'); InstanceCLI.prototype.do_wait = require('./do_wait'); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index c692e82..5f23335 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -1364,6 +1364,92 @@ function disableInstanceFirewall(opts, cb) { }; +// ---- instance enable/disable deletion protection + +/** + * Enable deletion protection on an instance. + * + * @param {Object} opts + * - {String} id: Required. The instance ID, name, or short ID. + * @param {Function} callback `function (err, fauxInst, res)` + * On failure `err` is an error instance, else it is null. + * On success: `fauxInst` is an object with just the instance id, + * `{id: }` and `res` is the CloudAPI + * `EnableMachineDeletionProtection` response. + * The API call does not return the instance/machine object, hence we + * are limited to just the id for `fauxInst`. + */ +TritonApi.prototype.enableInstanceDeletionProtection = +function enableInstanceDeletionProtection(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var fauxInst; + + function enableDeletionProtection(arg, next) { + fauxInst = {id: arg.instId}; + + self.cloudapi.enableMachineDeletionProtection(arg.instId, + function enableCb(err, _, _res) { + res = _res; + next(err); + }); + } + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + enableDeletionProtection + ]}, function vasyncCb(err) { + cb(err, fauxInst, res); + }); +}; + + +/** + * Disable deletion protection on an instance. + * + * @param {Object} opts + * - {String} id: Required. The instance ID, name, or short ID. + * @param {Function} callback `function (err, fauxInst, res)` + * On failure `err` is an error instance, else it is null. + * On success: `fauxInst` is an object with just the instance id, + * `{id: }` and `res` is the CloudAPI + * `DisableMachineDeletionProtectiomn` response. + * The API call does not return the instance/machine object, hence we + * are limited to just the id for `fauxInst`. + */ +TritonApi.prototype.disableInstanceDeletionProtection = +function disableInstanceDeletionProtection(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var fauxInst; + + function disableDeletionProtection(arg, next) { + fauxInst = {id: arg.instId}; + + self.cloudapi.disableMachineDeletionProtection(arg.instId, + function (err, _, _res) { + res = _res; + next(err); + }); + } + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + disableDeletionProtection + ]}, function vasyncCb(err) { + cb(err, fauxInst, res); + }); +}; + + // ---- instance snapshots /** diff --git a/test/integration/cli-deletion-protection.test.js b/test/integration/cli-deletion-protection.test.js new file mode 100644 index 0000000..db01e6f --- /dev/null +++ b/test/integration/cli-deletion-protection.test.js @@ -0,0 +1,191 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2018, Joyent, Inc. + */ + +/* + * Integration tests for `triton instance enable-deletion-protection ...` and + * `triton instance disable-deletion-protection ...` + */ + +var h = require('./helpers'); +var f = require('util').format; +var os = require('os'); +var test = require('tape'); + +// --- Globals + +var INST_ALIAS = f('nodetritontest-deletion-protection-%s', os.hostname()); +var INST; +var OPTS = { + skip: !h.CONFIG.allowWriteActions +}; + +// --- Helpers + +function cleanup(t) { + var cmd = 'instance disable-deletion-protection ' + INST_ALIAS + ' -w'; + + h.triton(cmd, function (err, stdout, stderr) { + if (err) + return t.end(); + + h.deleteTestInst(t, INST_ALIAS, function (err2) { + t.ifErr(err2, 'delete inst err'); + t.end(); + }); + }); +} + +// --- Tests + +if (OPTS.skip) { + console.error('** skipping %s tests', __filename); + console.error('** set "allowWriteActions" in test config to enable'); +} + +test('triton instance', OPTS, function (tt) { + h.printConfig(tt); + + tt.test(' cleanup existing inst with alias ' + INST_ALIAS, cleanup); + + + tt.test(' triton create --deletion-protection', function (t) { + h.createTestInst(t, INST_ALIAS, { + extraFlags: ['--deletion-protection'] + }, function onInst(err2, instId) { + if (h.ifErr(t, err2, 'triton instance create')) + return t.end(); + + INST = instId; + + h.triton('instance get -j ' + INST, function (err3, stdout) { + if (h.ifErr(t, err3, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout); + t.ok(inst.deletion_protection, 'deletion_protection'); + + t.end(); + }); + }); + }); + + + tt.test(' attempt to delete deletion-protected instance', function (t) { + var cmd = 'instance rm ' + INST + ' -w'; + + h.triton(cmd, function (err, stdout, stderr) { + t.ok(err, 'err expected'); + /* JSSTYLED */ + t.ok(stderr.match(/Instance has "deletion_protection" enabled/)); + t.end(); + }); + }); + + + tt.test(' triton instance disable-deletion-protection', function (t) { + var cmd = 'instance disable-deletion-protection ' + INST + ' -w'; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance disable-deletion-protection')) + return t.end(); + + t.ok(stdout.match('Disabled deletion protection for instance "' + + INST + '"'), 'deletion protection disabled'); + + h.triton('instance get -j ' + INST, function (err2, stdout2) { + if (h.ifErr(t, err2, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout2); + t.ok(!inst.deletion_protection, 'deletion_protection'); + + t.end(); + }); + }); + }); + + + tt.test(' triton instance disable-deletion-protection (already enabled)', + function (t) { + var cmd = 'instance disable-deletion-protection ' + INST + ' -w'; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance disable-deletion-protection')) + return t.end(); + + t.ok(stdout.match('Disabled deletion protection for instance "' + + INST + '"'), 'deletion protection disabled'); + + h.triton('instance get -j ' + INST, function (err2, stdout2) { + if (h.ifErr(t, err2, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout2); + t.ok(!inst.deletion_protection, 'deletion_protection'); + + t.end(); + }); + }); + }); + + + tt.test(' triton instance enable-deletion-protection', function (t) { + var cmd = 'instance enable-deletion-protection ' + INST + ' -w'; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance enable-deletion-protection')) + return t.end(); + + t.ok(stdout.match('Enabled deletion protection for instance "' + + INST + '"'), 'deletion protection enabled'); + + h.triton('instance get -j ' + INST, function (err2, stdout2) { + if (h.ifErr(t, err2, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout2); + t.ok(inst.deletion_protection, 'deletion_protection'); + + t.end(); + }); + }); + }); + + + tt.test(' triton instance enable-deletion-protection (already enabled)', + function (t) { + var cmd = 'instance enable-deletion-protection ' + INST + ' -w'; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance enable-deletion-protection')) + return t.end(); + + t.ok(stdout.match('Enabled deletion protection for instance "' + + INST + '"'), 'deletion protection enabled'); + + h.triton('instance get -j ' + INST, function (err2, stdout2) { + if (h.ifErr(t, err2, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout2); + t.ok(inst.deletion_protection, 'deletion_protection'); + + t.end(); + }); + }); + }); + + + /* + * Use a timeout, because '-w' on delete doesn't have a way to know if the + * attempt failed or if it is just taking a really long time. + */ + tt.test(' cleanup: triton rm INST', {timeout: 10 * 60 * 1000}, cleanup); +}); diff --git a/test/integration/cli-fwrules.test.js b/test/integration/cli-fwrules.test.js index 6d93974..3b4d775 100644 --- a/test/integration/cli-fwrules.test.js +++ b/test/integration/cli-fwrules.test.js @@ -47,7 +47,7 @@ test('triton fwrule', OPTS, function (tt) { }); tt.test(' setup: triton create', function (t) { - h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) { + h.createTestInst(t, INST_ALIAS, {}, function onInst(err2, instId) { if (h.ifErr(t, err2, 'triton instance create')) return t.end(); diff --git a/test/integration/cli-nics.test.js b/test/integration/cli-nics.test.js index 7131a3b..7850673 100644 --- a/test/integration/cli-nics.test.js +++ b/test/integration/cli-nics.test.js @@ -48,7 +48,7 @@ test('triton instance nics', OPTS, function (tt) { }); tt.test(' setup: triton instance create', function (t) { - h.createTestInst(t, INST_ALIAS, function onInst(err, instId) { + h.createTestInst(t, INST_ALIAS, {}, function onInst(err, instId) { if (h.ifErr(t, err, 'triton instance create')) return t.end(); diff --git a/test/integration/cli-snapshots.test.js b/test/integration/cli-snapshots.test.js index 47ed0e3..194424c 100644 --- a/test/integration/cli-snapshots.test.js +++ b/test/integration/cli-snapshots.test.js @@ -44,7 +44,7 @@ test('triton instance snapshot', OPTS, function (tt) { }); tt.test(' setup: triton instance create', function (t) { - h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) { + h.createTestInst(t, INST_ALIAS, {}, function onInst(err2, instId) { if (h.ifErr(t, err2, 'triton instance create')) return t.end(); diff --git a/test/integration/cli-subcommands.test.js b/test/integration/cli-subcommands.test.js index 1faf378..33173bf 100644 --- a/test/integration/cli-subcommands.test.js +++ b/test/integration/cli-subcommands.test.js @@ -45,6 +45,8 @@ var subs = [ ['instance delete', 'instance rm', 'delete', 'rm'], ['instance enable-firewall'], ['instance disable-firewall'], + ['instance enable-deletion-protection'], + ['instance disable-deletion-protection'], ['instance rename'], ['instance ssh'], ['instance ip'], diff --git a/test/integration/helpers.js b/test/integration/helpers.js index e091dda..360ee82 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -369,7 +369,13 @@ function createClient(cb) { /* * Create a small test instance. */ -function createTestInst(t, name, cb) { +function createTestInst(t, name, opts, cb) { + assert.object(t, 't'); + assert.string(name, 'name'); + assert.object(opts, 'opts'); + assert.optionalArrayOfString(opts.extraFlags, 'opts.extraFlags'); + assert.func(cb, 'cb'); + getTestPkg(t, function (err, pkgId) { t.ifErr(err); if (err) { @@ -385,6 +391,10 @@ function createTestInst(t, name, cb) { } var cmd = f('instance create -w -n %s %s %s', name, imgId, pkgId); + if (opts.extraFlags) { + cmd += ' ' + opts.extraFlags.join(' '); + } + triton(cmd, function (err3, stdout) { t.ifErr(err3, 'create test instance'); if (err3) {