Skip to content

Commit

Permalink
DOCKER-898 Add CNS search domain to docker provisioned machines
Browse files Browse the repository at this point in the history
Reviewed by: Alex Wilson <alex.wilson@joyent.com>
Approved by: Alex Wilson <alex.wilson@joyent.com>
  • Loading branch information
twhiteman committed Oct 17, 2017
1 parent 0affa5c commit 1180212
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 9 deletions.
65 changes: 65 additions & 0 deletions lib/backends/sdc/containers.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
84 changes: 84 additions & 0 deletions lib/backends/sdc/networks.js
Expand Up @@ -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
};
25 changes: 25 additions & 0 deletions lib/docker.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
};


Expand Down Expand Up @@ -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();
});
}
});
}
};


Expand Down
5 changes: 5 additions & 0 deletions sapi_manifests/docker/template
Expand Up @@ -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"],
Expand Down
25 changes: 24 additions & 1 deletion test/integration/api-create.test.js
Expand Up @@ -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,
Expand All @@ -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);
}

Expand Down
68 changes: 60 additions & 8 deletions test/integration/helpers.js
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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 = {
Expand All @@ -1037,7 +1089,7 @@ function initDockerEnv(t, state, opts, cb) {
return;
});
});
});
}
}

/*
Expand Down

0 comments on commit 1180212

Please sign in to comment.