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 8, 2017
1 parent e0d49eb commit 4c37db7
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 37 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
4 changes: 4 additions & 0 deletions lib/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@ exports.registerDockerClient = function (next) {
};

exports.loadTasks = function (callback) {
tasks.addType('destroy-temporary', ({ session, container }) => {
const machines = require('./machines');
machines.destroyTemporary(session, container, log);
});
setInterval(() => {
tasks.check();
}, 60000);
Expand Down
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');

// Get an existing user machine with the given project and machine ID.
exports.getMachineById = function (user, projectId, machineId) {
Expand Down Expand Up @@ -185,6 +186,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 @@ -207,7 +258,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 @@ -220,27 +271,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 @@ -257,7 +290,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 @@ -266,19 +299,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 @@ -297,7 +366,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 @@ -445,6 +514,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
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 4c37db7

Please sign in to comment.