Skip to content

Commit

Permalink
Add temporary machines support
Browse files Browse the repository at this point in the history
  • Loading branch information
nt1m committed Aug 7, 2017
1 parent 548d914 commit d32ef00
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 38 deletions.
25 changes: 18 additions & 7 deletions api/hosts-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,20 +409,31 @@ containersAPI.put({
title: 'Create a container',

handler (request, response) {
const { user } = request;
if (!user) {
response.statusCode = 403; // Forbidden
response.json({ error: 'Unauthorized' }, null, 2);
return;
}

const projectId = request.query.project;
if (!(projectId in db.get('projects'))) {
response.statusCode = 404; // Not Found
response.json({ error: 'Project not found' }, null, 2);
return;
}

const { user, session } = request;
if (!user) {
if (!session) {
return;
}

machines.spawnTemporary(session.id, projectId, (error, machine) => {
if (error) {
response.statusCode = 500; // Internal Server Error
response.json({ error: 'Could not create container' }, null, 2);
}
response.json({
container: machine.docker.container
}, null, 2);
});
return;
}

machines.spawn(user, projectId, (error, machine) => {
if (error) {
log('[fail] could not spawn machine', error);
Expand Down
3 changes: 2 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ const users = require('./lib/users');
boot.executeInParallel([
boot.forwardHttp,
boot.ensureHttpsCertificates,
boot.ensureDockerTlsCertificates
boot.ensureDockerTlsCertificates,
], () => {
boot.loadTasks();
// You can customize these values in './db.json'.
const hostname = db.get('hostname', 'localhost');
const https = db.get('https');
Expand Down
2 changes: 2 additions & 0 deletions join.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ boot.executeInParallel([
boot.registerDockerClient(() => {
log('[ok] joined cluster as [hostname = ' + hostname + ']');

boot.loadTasks();

const https = db.get('https');
const ports = db.get('ports');
const security = db.get('security');
Expand Down
11 changes: 11 additions & 0 deletions lib/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const certificates = require('./certificates');
const db = require('./db');
const log = require('./log');
const oauth2 = require('./oauth2');
const tasks = require('./tasks');

const hostname = db.get('hostname', 'localhost');

Expand Down Expand Up @@ -345,3 +346,13 @@ exports.registerDockerClient = function (next) {
next();
});
};

exports.loadTasks = function () {
tasks.addType('destroy-temporary', ({ session, container }) => {
const machines = require('./machines');
machines.destroyTemporary(session, container, log);
});
setInterval(() => {
tasks.check();
}, 60000);
};
148 changes: 122 additions & 26 deletions lib/machines.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const docker = require('./docker');
const log = require('./log');
const metrics = require('./metrics');
const streams = require('./streams');
const tasks = require('./tasks');

// List available user machines for each project, create when necessary.
exports.getAvailableMachines = function (user) {
Expand Down Expand Up @@ -195,6 +196,56 @@ exports.spawn = function (user, projectId, callback) {

const machine = getOrCreateNewMachine(user, projectId);

_spawn(machine, project, function (error, machine) {
if (error) {
callback(error);
return;
}

const containerId = machine.docker.container;
// Quickly authorize the user's public SSH keys to access this container.
deploySSHAuthorizedKeys(user, machine, error => {
log('spawn-sshkeys', containerId.slice(0, 16), error || 'success');
db.save();
});

// Install all non-empty user configuration files into this container.
Object.keys(user.configurations).forEach(file => {
if (!user.configurations[file]) {
return;
}
exports.deployConfiguration(user, machine, file).then(() => {
log('spawn-config', file, containerId.slice(0, 16), 'success');
}).catch(error => {
log('spawn-config', file, containerId.slice(0, 16), error);
});
});
callback(null, machine);
});
};

// Instantiate a new temporary machine for a project. (Fast!)
exports.spawnTemporary = function (sessionId, projectId, callback) {
const machine = createNewTemporaryMachine(sessionId, projectId);

_spawn(machine, getProject(projectId), (error, machine) => {
if (error) {
callback(error);
return;
}

const destroyDate = new Date(Date.now());
destroyDate.setHours(destroyDate.getHours() + 8);
tasks.add(destroyDate, 'destroy-temporary', {
session: sessionId,
container: machine.docker.container
});

callback(null, machine);
});
};

function _spawn (machine, project, callback) {
// Keep track of the last project update this machine will be based on.
metrics.set(machine, 'updated', project.data.updated);

Expand All @@ -217,7 +268,7 @@ exports.spawn = function (user, projectId, callback) {
log('spawn', image, error);
machine.status = 'start-failed';
db.save();
callback(new Error('Unable to start machine for project: ' + projectId));
callback(new Error('Unable to start machine for project: ' + project.id));
return;
}

Expand All @@ -230,27 +281,9 @@ exports.spawn = function (user, projectId, callback) {
metrics.push(project, 'spawn-time', [ now, now - time ]);
db.save();

// Quickly authorize the user's public SSH keys to access this container.
deploySSHAuthorizedKeys(user, machine, error => {
log('spawn-sshkeys', container.id.slice(0, 16), error || 'success');
db.save();
});

// Install all non-empty user configuration files into this container.
Object.keys(user.configurations).forEach(file => {
if (!user.configurations[file]) {
return;
}
exports.deployConfiguration(user, machine, file).then(() => {
log('spawn-config', file, container.id.slice(0, 16), 'success');
}).catch(error => {
log('spawn-config', file, container.id.slice(0, 16), error);
});
});

callback(null, machine);
});
};
}

// Destroy a given user machine and recycle its ports.
exports.destroy = function (user, projectId, machineId, callback) {
Expand All @@ -267,7 +300,7 @@ exports.destroy = function (user, projectId, machineId, callback) {
return;
}

const { container: containerId, host } = machine.docker;
const containerId = machine.docker.container;
if (!containerId) {
// This machine has no associated container, just recycle it as is.
machine.status = 'new';
Expand All @@ -276,19 +309,55 @@ exports.destroy = function (user, projectId, machineId, callback) {
return;
}

log('destroy', containerId.slice(0, 16), 'started');
docker.removeContainer({ host, container: containerId }, error => {
_destroy(machine, (error) => {
if (error) {
log('destroy', containerId.slice(0, 16), error);
callback(error);
return;
}

// Recycle the machine's name and ports.
machine.status = 'new';
machine.docker.container = '';
db.save();
callback();
});
};

exports.destroyTemporary = function (sessionId, containerId, callback) {
const machines = db.get('temporaryMachines')[sessionId];
if (!machines) {
callback(new Error('No machines for session ' + sessionId));
}

const machineIndex = machines.findIndex(machine =>
machine.docker.container === containerId);

if (machineIndex === -1) {
callback(new Error('Wrong container ID'));
}

_destroy(machines[machineIndex], (error) => {
if (error) {
callback(error);
}

machines.splice(machineIndex, 1);
db.save();
callback();
});
};

function _destroy (machine, callback) {
const { container: containerId, host } = machine.docker;

log('destroy', containerId.slice(0, 16), 'started');
docker.removeContainer({ host, container: containerId }, error => {
if (error) {
log('destroy', containerId.slice(0, 16), error);
callback(error);
return;
}

callback();

if (!machine.docker.image) {
log('destroy', containerId.slice(0, 16), 'success');
Expand All @@ -307,7 +376,7 @@ exports.destroy = function (user, projectId, machineId, callback) {
db.save();
});
});
};
}

// Install or overwrite a configuration file in all the user's containers.
exports.deployConfigurationInAllContainers = function (user, file) {
Expand Down Expand Up @@ -452,6 +521,33 @@ function getOrCreateNewMachine (user, projectId) {
return machine;
}

function createNewTemporaryMachine (sessionId, projectId) {
const project = getProject(projectId);
const temporaryMachines = db.get('temporaryMachines');
if (!(sessionId in temporaryMachines)) {
temporaryMachines[sessionId] = [];
}

const machines = temporaryMachines[sessionId];

const machine = {
properties: {
name: project.name + ' #' + machines.length,
},
status: 'new',
docker: {
host: '',
container: '',
ports: {},
logs: ''
},
data: {}
};
machines.push(machine);

return machine;
}

// Get a unique available port starting from 42000.
function getPort () {
const ports = db.get('ports');
Expand Down
53 changes: 53 additions & 0 deletions lib/tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const db = require('./db');
const log = require('./log');
let taskCounter = 0; // Used to generate unique task IDs

exports.tasks = db.get('tasks');
exports.taskTypes = new Map();

exports.check = function () {
const now = Date.now();
for (const task of Object.values(exports.tasks)) {
if (now > Number(task.date)) {
exports.execute(task);
}
}
};

exports.addType = function (type, task) {
if (exports.taskTypes.has(type)) {
throw new Error('[fail] task', task, 'already exists');
}

exports.taskTypes.set(type, task);
};

exports.add = function (date, type, data) {
taskCounter++;

const dateHash = date.getTime();
const taskId = `${type}-${dateHash}-${taskCounter}`;

const task = {
id: taskId,
date: dateHash,
type,
data,
};

exports.tasks[taskId] = task;
db.save();
return task;
};

exports.remove = function (id) {
delete exports.tasks[id];
db.save();
};

exports.execute = function ({ type, data, id }) {
const task = exports.taskTypes.get(type);
log("[ok] Task", id, "executed", data);
task(data);
exports.remove(id);
};
7 changes: 3 additions & 4 deletions templates/projects.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ <h4>In just a single click, enter a fully-functional development environment. Ye
<div class="panel-heading">
<img class="project-icon" src="{{= project.icon in xmlattr}}" alt="{{= project.name in xmlattr}} Logo">
<h4 class="project-title">{{= project.name in html}}</h4>
<div class="project-actions">{{if user then {{
<form action="/api/hosts/{{= project.docker.host in uri}}/containers?project={{= id in id}}" class="ajax-form has-feedback is-submit" data-redirect-after-success="/contributions/" method="put">
<div class="project-actions">
<form action="/api/hosts/{{= project.docker.host in uri}}/containers?project={{= id in id}}" class="ajax-form has-feedback is-submit" method="put">
<button class="btn btn-primary" title="Create a new container for this project" type="submit">New Container</button>
</form>}} else {{
<a class="btn btn-primary" href="/login">New Container</a>}}}}
</form>
</div>
</div>
<div class="panel-body">
Expand Down

0 comments on commit d32ef00

Please sign in to comment.