statically allocate app container IPs

We removed httpPort with the assumption that docker allocated IPs
and kept them as long as the container is around. This turned out
to be not true because the IP changes on even container restart.

So we now allocate IPs statically. The iprange makes sure we don't
overlap with addons and other CI app or JupyterHub apps.

https://github.com/moby/moby/issues/6743
https://github.com/moby/moby/pull/19001
This commit is contained in:
Girish Ramakrishnan
2020-11-20 14:13:16 -08:00
parent 64af278f39
commit c0b0029935
13 changed files with 165 additions and 59 deletions
+40 -6
View File
@@ -33,6 +33,7 @@ var addons = require('./addons.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
iputils = require('./iputils.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
path = require('path'),
@@ -87,6 +88,18 @@ function updateApp(app, values, callback) {
});
}
function allocateContainerIp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10 }, function (retryCallback) {
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
let rnd = Math.floor(Math.random() * iprange);
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
updateApp(app, { containerIp }, retryCallback);
}, callback);
}
function configureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -144,7 +157,6 @@ function deleteContainers(app, options, callback) {
removeLogrotateConfig.bind(null, app),
docker.stopContainers.bind(null, app.id),
docker.deleteContainers.bind(null, app.id, options),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { containerId: null })
], callback);
}
@@ -469,10 +481,7 @@ function startApp(app, callback){
if (app.runState === apps.RSTATE_STOPPED) return callback();
async.series([
docker.startContainer.bind(null, app.id),
configureReverseProxy.bind(null, app),
], callback);
docker.startContainer(app.id, callback);
}
function install(app, args, progressCallback, callback) {
@@ -492,6 +501,7 @@ function install(app, args, progressCallback, callback) {
// teardown for re-installs
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
@@ -517,6 +527,9 @@ function install(app, args, progressCallback, callback) {
docker.deleteImage(oldManifest, done);
},
// allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time
allocateContainerIp.bind(null, app),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
@@ -563,6 +576,9 @@ function install(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
progressCallback.bind(null, { percent: 95, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
@@ -640,6 +656,7 @@ function changeLocation(app, args, progressCallback, callback) {
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function (next) {
let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) {
@@ -668,6 +685,9 @@ function changeLocation(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
@@ -728,6 +748,7 @@ function configure(app, args, progressCallback, callback) {
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
@@ -748,6 +769,9 @@ function configure(app, args, progressCallback, callback) {
startApp.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
@@ -771,7 +795,8 @@ function update(app, args, progressCallback, callback) {
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
async.series([
// this protects against the theoretical possibility of an app being marked for update from
@@ -846,6 +871,14 @@ function update(app, args, progressCallback, callback) {
startApp.bind(null, app),
// needed for httpPaths changes
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
function (next) {
if (!httpPathsChanged) return next();
configureReverseProxy(app, next);
},
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) {
@@ -948,6 +981,7 @@ function uninstall(app, args, progressCallback, callback) {
async.series([
progressCallback.bind(null, { percent: 20, message: 'Deleting container' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, {}),
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),