diff --git a/lib/backends/sdc/containers.js b/lib/backends/sdc/containers.js index 11938cda..2c48d57b 100644 --- a/lib/backends/sdc/containers.js +++ b/lib/backends/sdc/containers.js @@ -749,6 +749,67 @@ function addRulesToPayload(payload, rules) { })); } +/** + * Add CNS DNS search entries to the vmobj payload. + */ +function addCnsEntriesToPayload(opts, payload, callback) { + assert.object(opts, 'opts'); + assert.object(opts.account, 'opts.account'); + assert.object(opts.app, 'opts.app'); + assert.object(opts.log, 'opt.log'); + assert.object(opts.config, 'opts.config'); + assert.object(opts.log, 'opts.log'); + assert.string(opts.req_id, 'opts.req_id'); + assert.object(payload, 'payload'); + assert.object(payload.internal_metadata, 'payload.internal_metadata'); + assert.func(callback, 'callback'); + + var log = opts.log; + + if (!payload.networks || payload.networks.length === 0) { + // No networks, then nothing to do. + callback(); + return; + } + + mod_networks.getCnsDnsSearchEntriesForNetworks(payload.networks, + opts, function _getCnsEntriesCb(err, cnsEntries) { + + var dnsSearchEntries; + + if (err) { + callback(err); + return; + } + + if (!cnsEntries || cnsEntries.length === 0) { + callback(); + return; + } + + // Note that the 'docker:dnssearch' value must be a JSON + // stringified array. + try { + dnsSearchEntries = JSON.parse( + payload.internal_metadata['docker:dnssearch'] || '[]'); + } catch (ex) { + log.error('Unable to parse "docker:dnssearch" value: %s', + payload.internal_metadata['docker:dnssearch']); + callback(new errors.InternalError('Invalid docker:dnssearch')); + return; + } + + assert.arrayOfString(cnsEntries, 'cnsEntries'); + assert.arrayOfString(dnsSearchEntries, 'dnsSearchEntries'); + + payload.internal_metadata['docker:dnssearch'] = JSON.stringify( + dnsSearchEntries.concat(cnsEntries)); + log.debug('%d dnssearch entries added for CNS', cnsEntries.length); + + callback(); + }); +} + /** * Updates the internal docker: metadata to include link information, and * returns the link details via the callback. @@ -1859,6 +1920,10 @@ function buildVmPayload(opts, container, callback) { addNetworksToPayload(opts, container, payload, cb); }, + function addCns(_, cb) { + addCnsEntriesToPayload(opts, payload, cb); + }, + function handleVolumesFrom(_, cb) { // This must happen after we've added the owner_uuid to the payload. // ...and is where we add --volumes-from volumes. diff --git a/lib/backends/sdc/networks.js b/lib/backends/sdc/networks.js index 04eac3a6..66e20bfc 100644 --- a/lib/backends/sdc/networks.js +++ b/lib/backends/sdc/networks.js @@ -548,9 +548,93 @@ function findNetworkOrPoolByNameOrId(name, opts, callback) { } +/** + * Get CNS information for the given array of network objects. + * + * @param {Array(String)} networks The vmapi network objects. + * @param {Object} opts Configurable options for this call + * @param {Function} callback invoked as fn(err, dnsSearchSuffixes) + * where dnsSearchSuffixes is an array of strings. + */ +function getCnsDnsSearchEntriesForNetworks(networks, opts, callback) { + assert.arrayOfObject(networks, 'networks'); + assert.object(opts, 'opts'); + assert.object(opts.account, 'opts.account'); + assert.object(opts.app, 'opts.app'); + assert.object(opts.log, 'opts.log'); + assert.string(opts.req_id, 'opts.req_id'); + assert.func(callback, 'callback'); + + if (!opts.app.cns || opts.account.triton_cns_enabled !== 'true') { + // CNS is not enabled. + callback(); + return; + } + + /* + * Ask CNS for the DNS suffixes we should add. + * + * This lets machines on a CNS-enabled account have a DNS which resolves + * other machines on the account in the same DC by their service names. + */ + var cnsOpts = { + headers: { + 'x-request-id': opts.req_id, + 'accept-version': '~1' + } + }; + var log = opts.log; + var netUuids = new Set(); + + networks.forEach(function (network) { + if (network.ipv4_uuid) { + netUuids.add(network.ipv4_uuid); + } + if (network.ipv6_uuid) { + netUuids.add(network.ipv6_uuid); + } + if (network.uuid) { + netUuids.add(network.uuid); + } + }); + + opts.app.cns.getSuffixesForVM(opts.account.uuid, Array.from(netUuids), + cnsOpts, + function _getSuffixesForVMCb(err, result) { + + if (err) { + if (err.name === 'NotFoundError' + || err.name === 'ResourceNotFoundError') { + + log.warn('failed to retrieve DNS suffixes from ' + + 'CNS REST API because the endpoint is not supported' + + ' (have you updated CNS?)'); + callback(); + return; + } + + log.error(err, 'failed to retrieve DNS suffixes from CNS REST API'); + callback(new errors.InternalError('Triton CNS API failed')); + return; + } + + log.trace({result: result}, 'CNS result'); + + if (!result.suffixes || result.suffixes.length === 0) { + log.info('no suffixes returned from CNS REST API'); + callback(); + return; + } + + callback(null, result.suffixes); + }); +} + + module.exports = { findNetworkOrPoolByNameOrId: findNetworkOrPoolByNameOrId, inspectNetwork: inspectNetwork, + getCnsDnsSearchEntriesForNetworks: getCnsDnsSearchEntriesForNetworks, getNetworksOrPools: getNetworksOrPools, listNetworks: listNetworks }; diff --git a/lib/docker.js b/lib/docker.js index 31c0e6a3..d73f3013 100644 --- a/lib/docker.js +++ b/lib/docker.js @@ -30,6 +30,7 @@ var UFDS = require('ufds'); var vasync = require('vasync'); var verror = require('verror'); var CNAPI = require('sdc-clients').CNAPI; +var CNS = require('sdc-clients').CNS; var IMGAPI = require('sdc-clients').IMGAPI; var VMAPI = require('sdc-clients').VMAPI; @@ -167,6 +168,10 @@ App.prototype.setupConnections = function setupConnections() { self.cnapi = new CNAPI(self.config.cnapi); self.vmapi = new VMAPI(self.config.vmapi); self.imgapi = new IMGAPI(self.config.imgapi); + + if (self.config.cns) { + self.cns = new CNS(self.config.cns); + } }; @@ -269,6 +274,26 @@ App.prototype.setupConnectionsWatcher = function setupConnectionsWatcher() { }); self.ufds = self.createUfdsClient(self.config.ufds); + + if (self.config.cns) { + self.connWatcher.register({ + name: 'cns', + init: function (cb) { + var cns = new CNS(self.config.cns); + cb(null, cns); + }, + pingIntervalSecs: 10, + ping: function (cns, cb) { + cns.ping(function (err) { + if (err) { + cb(new verror.VError(err, 'could not ping CNS')); + return; + } + cb(); + }); + } + }); + } }; diff --git a/sapi_manifests/docker/template b/sapi_manifests/docker/template index dc78d0aa..e9ad55bc 100644 --- a/sapi_manifests/docker/template +++ b/sapi_manifests/docker/template @@ -52,6 +52,11 @@ "cnapi": { "url": "http://cnapi.{{{datacenter_name}}}.{{{dns_domain}}}" }, + {{#CNS_SERVICE}} + "cns": { + "url": "http://{{{cns_domain}}}" + }, + {{/CNS_SERVICE}} "wfapi": { "forceMd5Check": true, "workflows": ["pull-image-v2"], diff --git a/test/integration/api-create.test.js b/test/integration/api-create.test.js index 60aeb456..6c788d20 100644 --- a/test/integration/api-create.test.js +++ b/test/integration/api-create.test.js @@ -334,7 +334,7 @@ test('api: create', function (tt) { }); -test('api: create with env var that has no value (DOCKER-741)', function (tt) { +test('api: test DOCKER-741 and DOCKER-898', function (tt) { tt.test('create empty-env-var container', function (t) { h.createDockerContainer({ vmapiClient: VMAPI, @@ -347,6 +347,29 @@ test('api: create with env var that has no value (DOCKER-741)', function (tt) { function oncreate(err, result) { t.ifErr(err, 'create empty-env-var container'); t.equal(result.vm.state, 'running', 'Check container running'); + + if (err) { + t.end(); + return; + } + + checkForCnsDnsEntries(result); + } + + function checkForCnsDnsEntries(result) { + var cmd = format('cat %s/root/etc/resolv.conf', result.vm.zonepath); + ALICE.execGz(cmd, STATE, function (cmdErr, stdout) { + t.ifErr(cmdErr, 'Check cat /etc/resolv.conf result'); + + // Stdout should contain a CNS 'search' entry. + var hasCnsSearch = stdout.match(/^search\s.*?\.cns\./m); + t.ok(hasCnsSearch, 'find cns entry in /etc/resolv.conf'); + if (!hasCnsSearch) { + t.fail('cns not found in /etc/resolv.conf file: ' + stdout); + } + + }); + DOCKER_ALICE.del('/containers/' + result.id + '?force=1', ondelete); } diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 23007a82..6b20cea3 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -782,6 +782,26 @@ GzDockerEnv.prototype.exec = function denvExec(cmd, opts, cb) { }; +/* + * Run '$cmd' in the global zone (Gz). + * + * @param cmd {String} The command to run. + * @param opts {Object} Optional: {log: Logger} + * @param callback {Function} `function (err, stdout, stderr)` + */ +GzDockerEnv.prototype.execGz = function execGz(cmd, opts, callback) { + assert.string(cmd, 'cmd'); + assert.object(opts, 'opts'); + assert.optionalObject(opts.log, 'opts.log'); + assert.func(callback, 'callback'); + + common.execPlus({ + command: cmd, + log: opts.log + }, callback); +}; + + /* * --- LocalDockerEnv * @@ -956,6 +976,28 @@ LocalDockerEnv.prototype.exec = function ldenvExec(cmd, opts, cb) { }; +/* + * Run '$cmd' in the global zone (Gz). + * + * @param cmd {String} The command to run. + * @param opts {Object} Optional: {log: Logger} + * @param callback {Function} `function (err, stdout, stderr)` + */ +LocalDockerEnv.prototype.execGz = function ldenvExecGz(cmd, opts, callback) { + assert.string(cmd, 'cmd'); + assert.object(opts, 'opts'); + assert.string(opts.headnodeSsh, 'opts.headnodeSsh'); + assert.optionalObject(opts.log, 'opts.log'); + assert.func(callback, 'callback'); + + var sshCmd = fmt('ssh %s %s', opts.headnodeSsh, cmd); + + common.execPlus({ + command: sshCmd, + log: opts.log + }, callback); +}; + /* * --- Test helper functions @@ -989,20 +1031,19 @@ function initDockerEnv(t, state, opts, cb) { assert.object(opts, 'opts'); assert.func(cb, 'cb'); - // if account does not have approved_for_provisioning set to val, set it - function setProvisioning(env, val, next) { + // If account does not have 'attr' set to 'val', then make it so. + function setAccountAttribute(env, attr, val, next) { assert.object(env, 'env'); assert.bool(val, 'val'); assert.func(next, 'next'); - if (env.account.approved_for_provisioning === '' + val) { + if (env.account[attr] === '' + val) { next(null); return; } - var s = '/opt/smartdc/bin/sdc sdc-useradm replace-attr %s \ - approved_for_provisioning %s'; - var cmd = fmt(s, env.login, val); + var cmd = fmt('/opt/smartdc/bin/sdc sdc-useradm replace-attr %s %s %s', + env.login, attr, val); if (env.state.runningFrom === 'remote') { cmd = 'ssh ' + env.state.headnodeSsh + ' ' + cmd; @@ -1016,6 +1057,15 @@ function initDockerEnv(t, state, opts, cb) { t.ifErr(err, 'docker env: alice'); t.ok(alice, 'have a DockerEnv for alice'); + setAccountAttribute(alice, 'triton_cns_enabled', true, + function (err2) { + + t.ifErr(err2, 'docker env: alice set triton_cns_enabled true'); + setupBob(alice); + }); + }); + + function setupBob(alice) { // We create Bob here, who is permanently set as unprovisionable // below. Docker's ufds client caches account values, so mutating // Alice isn't in the cards (nor is Bob -- which is why we don't @@ -1025,7 +1075,9 @@ function initDockerEnv(t, state, opts, cb) { t.ifErr(err2, 'docker env: bob'); t.ok(bob, 'have a DockerEnv for bob'); - setProvisioning(bob, false, function (err3) { + setAccountAttribute(bob, 'approved_for_provisioning', false, + function (err3) { + t.ifErr(err3, 'set bob unprovisionable'); var accounts = { @@ -1037,7 +1089,7 @@ function initDockerEnv(t, state, opts, cb) { return; }); }); - }); + } } /*