Skip to content

Commit 4c37db7

Browse files
committed
Add temporary machines support
1 parent e0d49eb commit 4c37db7

File tree

4 files changed

+147
-37
lines changed

4 files changed

+147
-37
lines changed

api/hosts-api.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,20 +409,31 @@ containersAPI.put({
409409
title: 'Create a container',
410410

411411
handler (request, response) {
412-
const { user } = request;
413-
if (!user) {
414-
response.statusCode = 403; // Forbidden
415-
response.json({ error: 'Unauthorized' }, null, 2);
416-
return;
417-
}
418-
419412
const projectId = request.query.project;
420413
if (!(projectId in db.get('projects'))) {
421414
response.statusCode = 404; // Not Found
422415
response.json({ error: 'Project not found' }, null, 2);
423416
return;
424417
}
425418

419+
const { user, session } = request;
420+
if (!user) {
421+
if (!session) {
422+
return;
423+
}
424+
425+
machines.spawnTemporary(session.id, projectId, (error, machine) => {
426+
if (error) {
427+
response.statusCode = 500; // Internal Server Error
428+
response.json({ error: 'Could not create container' }, null, 2);
429+
}
430+
response.json({
431+
container: machine.docker.container
432+
}, null, 2);
433+
});
434+
return;
435+
}
436+
426437
machines.spawn(user, projectId, (error, machine) => {
427438
if (error) {
428439
log('[fail] could not spawn machine', error);

lib/boot.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@ exports.registerDockerClient = function (next) {
348348
};
349349

350350
exports.loadTasks = function (callback) {
351+
tasks.addType('destroy-temporary', ({ session, container }) => {
352+
const machines = require('./machines');
353+
machines.destroyTemporary(session, container, log);
354+
});
351355
setInterval(() => {
352356
tasks.check();
353357
}, 60000);

lib/machines.js

Lines changed: 122 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const docker = require('./docker');
88
const log = require('./log');
99
const metrics = require('./metrics');
1010
const streams = require('./streams');
11+
const tasks = require('./tasks');
1112

1213
// Get an existing user machine with the given project and machine ID.
1314
exports.getMachineById = function (user, projectId, machineId) {
@@ -185,6 +186,56 @@ exports.spawn = function (user, projectId, callback) {
185186

186187
const machine = getOrCreateNewMachine(user, projectId);
187188

189+
_spawn(machine, project, function (error, machine) {
190+
if (error) {
191+
callback(error);
192+
return;
193+
}
194+
195+
const containerId = machine.docker.container;
196+
// Quickly authorize the user's public SSH keys to access this container.
197+
deploySSHAuthorizedKeys(user, machine, error => {
198+
log('spawn-sshkeys', containerId.slice(0, 16), error || 'success');
199+
db.save();
200+
});
201+
202+
// Install all non-empty user configuration files into this container.
203+
Object.keys(user.configurations).forEach(file => {
204+
if (!user.configurations[file]) {
205+
return;
206+
}
207+
exports.deployConfiguration(user, machine, file).then(() => {
208+
log('spawn-config', file, containerId.slice(0, 16), 'success');
209+
}).catch(error => {
210+
log('spawn-config', file, containerId.slice(0, 16), error);
211+
});
212+
});
213+
callback(null, machine);
214+
});
215+
};
216+
217+
// Instantiate a new temporary machine for a project. (Fast!)
218+
exports.spawnTemporary = function (sessionId, projectId, callback) {
219+
const machine = createNewTemporaryMachine(sessionId, projectId);
220+
221+
_spawn(machine, getProject(projectId), (error, machine) => {
222+
if (error) {
223+
callback(error);
224+
return;
225+
}
226+
227+
const destroyDate = new Date(Date.now());
228+
destroyDate.setHours(destroyDate.getHours() + 8);
229+
tasks.add(destroyDate, 'destroy-temporary', {
230+
session: sessionId,
231+
container: machine.docker.container
232+
});
233+
234+
callback(null, machine);
235+
});
236+
};
237+
238+
function _spawn (machine, project, callback) {
188239
// Keep track of the last project update this machine will be based on.
189240
metrics.set(machine, 'updated', project.data.updated);
190241

@@ -207,7 +258,7 @@ exports.spawn = function (user, projectId, callback) {
207258
log('spawn', image, error);
208259
machine.status = 'start-failed';
209260
db.save();
210-
callback(new Error('Unable to start machine for project: ' + projectId));
261+
callback(new Error('Unable to start machine for project: ' + project.id));
211262
return;
212263
}
213264

@@ -220,27 +271,9 @@ exports.spawn = function (user, projectId, callback) {
220271
metrics.push(project, 'spawn-time', [ now, now - time ]);
221272
db.save();
222273

223-
// Quickly authorize the user's public SSH keys to access this container.
224-
deploySSHAuthorizedKeys(user, machine, error => {
225-
log('spawn-sshkeys', container.id.slice(0, 16), error || 'success');
226-
db.save();
227-
});
228-
229-
// Install all non-empty user configuration files into this container.
230-
Object.keys(user.configurations).forEach(file => {
231-
if (!user.configurations[file]) {
232-
return;
233-
}
234-
exports.deployConfiguration(user, machine, file).then(() => {
235-
log('spawn-config', file, container.id.slice(0, 16), 'success');
236-
}).catch(error => {
237-
log('spawn-config', file, container.id.slice(0, 16), error);
238-
});
239-
});
240-
241274
callback(null, machine);
242275
});
243-
};
276+
}
244277

245278
// Destroy a given user machine and recycle its ports.
246279
exports.destroy = function (user, projectId, machineId, callback) {
@@ -257,7 +290,7 @@ exports.destroy = function (user, projectId, machineId, callback) {
257290
return;
258291
}
259292

260-
const { container: containerId, host } = machine.docker;
293+
const containerId = machine.docker.container;
261294
if (!containerId) {
262295
// This machine has no associated container, just recycle it as is.
263296
machine.status = 'new';
@@ -266,19 +299,55 @@ exports.destroy = function (user, projectId, machineId, callback) {
266299
return;
267300
}
268301

269-
log('destroy', containerId.slice(0, 16), 'started');
270-
docker.removeContainer({ host, container: containerId }, error => {
302+
_destroy(machine, (error) => {
271303
if (error) {
272-
log('destroy', containerId.slice(0, 16), error);
273304
callback(error);
274-
return;
275305
}
276306

277307
// Recycle the machine's name and ports.
278308
machine.status = 'new';
279309
machine.docker.container = '';
280310
db.save();
281311
callback();
312+
});
313+
};
314+
315+
exports.destroyTemporary = function (sessionId, containerId, callback) {
316+
const machines = db.get('temporaryMachines')[sessionId];
317+
if (!machines) {
318+
callback(new Error('No machines for session ' + sessionId));
319+
}
320+
321+
const machineIndex = machines.findIndex(machine =>
322+
machine.docker.container === containerId);
323+
324+
if (machineIndex === -1) {
325+
callback(new Error('Wrong container ID'));
326+
}
327+
328+
_destroy(machines[machineIndex], (error) => {
329+
if (error) {
330+
callback(error);
331+
}
332+
333+
machines.splice(machineIndex, 1);
334+
db.save();
335+
callback();
336+
});
337+
};
338+
339+
function _destroy (machine, callback) {
340+
const { container: containerId, host } = machine.docker;
341+
342+
log('destroy', containerId.slice(0, 16), 'started');
343+
docker.removeContainer({ host, container: containerId }, error => {
344+
if (error) {
345+
log('destroy', containerId.slice(0, 16), error);
346+
callback(error);
347+
return;
348+
}
349+
350+
callback();
282351

283352
if (!machine.docker.image) {
284353
log('destroy', containerId.slice(0, 16), 'success');
@@ -297,7 +366,7 @@ exports.destroy = function (user, projectId, machineId, callback) {
297366
db.save();
298367
});
299368
});
300-
};
369+
}
301370

302371
// Install or overwrite a configuration file in all the user's containers.
303372
exports.deployConfigurationInAllContainers = function (user, file) {
@@ -445,6 +514,33 @@ function getOrCreateNewMachine (user, projectId) {
445514
return machine;
446515
}
447516

517+
function createNewTemporaryMachine (sessionId, projectId) {
518+
const project = getProject(projectId);
519+
const temporaryMachines = db.get('temporaryMachines');
520+
if (!(sessionId in temporaryMachines)) {
521+
temporaryMachines[sessionId] = [];
522+
}
523+
524+
const machines = temporaryMachines[sessionId];
525+
526+
const machine = {
527+
properties: {
528+
name: project.name + ' #' + machines.length,
529+
},
530+
status: 'new',
531+
docker: {
532+
host: '',
533+
container: '',
534+
ports: {},
535+
logs: ''
536+
},
537+
data: {}
538+
};
539+
machines.push(machine);
540+
541+
return machine;
542+
}
543+
448544
// Get a unique available port starting from 42000.
449545
function getPort () {
450546
const ports = db.get('ports');

templates/projects.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ <h4>In just a single click, enter a fully-functional development environment. Ye
99
<div class="panel-heading">
1010
<img class="project-icon" src="{{= project.icon in xmlattr}}" alt="{{= project.name in xmlattr}} Logo">
1111
<h4 class="project-title">{{= project.name in html}}</h4>
12-
<div class="project-actions">{{if user then {{
13-
<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">
12+
<div class="project-actions">
13+
<form action="/api/hosts/{{= project.docker.host in uri}}/containers?project={{= id in id}}" class="ajax-form has-feedback is-submit" method="put">
1414
<button class="btn btn-primary" title="Create a new container for this project" type="submit">New Container</button>
15-
</form>}} else {{
16-
<a class="btn btn-primary" href="/login">New Container</a>}}}}
15+
</form>
1716
</div>
1817
</div>
1918
<div class="panel-body">

0 commit comments

Comments
 (0)