Skip to content

Commit d32ef00

Browse files
committed
Add temporary machines support
1 parent 548d914 commit d32ef00

File tree

7 files changed

+211
-38
lines changed

7 files changed

+211
-38
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);

app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ const users = require('./lib/users');
1818
boot.executeInParallel([
1919
boot.forwardHttp,
2020
boot.ensureHttpsCertificates,
21-
boot.ensureDockerTlsCertificates
21+
boot.ensureDockerTlsCertificates,
2222
], () => {
23+
boot.loadTasks();
2324
// You can customize these values in './db.json'.
2425
const hostname = db.get('hostname', 'localhost');
2526
const https = db.get('https');

join.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ boot.executeInParallel([
3232
boot.registerDockerClient(() => {
3333
log('[ok] joined cluster as [hostname = ' + hostname + ']');
3434

35+
boot.loadTasks();
36+
3537
const https = db.get('https');
3638
const ports = db.get('ports');
3739
const security = db.get('security');

lib/boot.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const certificates = require('./certificates');
88
const db = require('./db');
99
const log = require('./log');
1010
const oauth2 = require('./oauth2');
11+
const tasks = require('./tasks');
1112

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

@@ -345,3 +346,13 @@ exports.registerDockerClient = function (next) {
345346
next();
346347
});
347348
};
349+
350+
exports.loadTasks = function () {
351+
tasks.addType('destroy-temporary', ({ session, container }) => {
352+
const machines = require('./machines');
353+
machines.destroyTemporary(session, container, log);
354+
});
355+
setInterval(() => {
356+
tasks.check();
357+
}, 60000);
358+
};

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
// List available user machines for each project, create when necessary.
1314
exports.getAvailableMachines = function (user) {
@@ -195,6 +196,56 @@ exports.spawn = function (user, projectId, callback) {
195196

196197
const machine = getOrCreateNewMachine(user, projectId);
197198

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

@@ -217,7 +268,7 @@ exports.spawn = function (user, projectId, callback) {
217268
log('spawn', image, error);
218269
machine.status = 'start-failed';
219270
db.save();
220-
callback(new Error('Unable to start machine for project: ' + projectId));
271+
callback(new Error('Unable to start machine for project: ' + project.id));
221272
return;
222273
}
223274

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

233-
// Quickly authorize the user's public SSH keys to access this container.
234-
deploySSHAuthorizedKeys(user, machine, error => {
235-
log('spawn-sshkeys', container.id.slice(0, 16), error || 'success');
236-
db.save();
237-
});
238-
239-
// Install all non-empty user configuration files into this container.
240-
Object.keys(user.configurations).forEach(file => {
241-
if (!user.configurations[file]) {
242-
return;
243-
}
244-
exports.deployConfiguration(user, machine, file).then(() => {
245-
log('spawn-config', file, container.id.slice(0, 16), 'success');
246-
}).catch(error => {
247-
log('spawn-config', file, container.id.slice(0, 16), error);
248-
});
249-
});
250-
251284
callback(null, machine);
252285
});
253-
};
286+
}
254287

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

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

279-
log('destroy', containerId.slice(0, 16), 'started');
280-
docker.removeContainer({ host, container: containerId }, error => {
312+
_destroy(machine, (error) => {
281313
if (error) {
282-
log('destroy', containerId.slice(0, 16), error);
283314
callback(error);
284-
return;
285315
}
286316

287317
// Recycle the machine's name and ports.
288318
machine.status = 'new';
289319
machine.docker.container = '';
290320
db.save();
291321
callback();
322+
});
323+
};
324+
325+
exports.destroyTemporary = function (sessionId, containerId, callback) {
326+
const machines = db.get('temporaryMachines')[sessionId];
327+
if (!machines) {
328+
callback(new Error('No machines for session ' + sessionId));
329+
}
330+
331+
const machineIndex = machines.findIndex(machine =>
332+
machine.docker.container === containerId);
333+
334+
if (machineIndex === -1) {
335+
callback(new Error('Wrong container ID'));
336+
}
337+
338+
_destroy(machines[machineIndex], (error) => {
339+
if (error) {
340+
callback(error);
341+
}
342+
343+
machines.splice(machineIndex, 1);
344+
db.save();
345+
callback();
346+
});
347+
};
348+
349+
function _destroy (machine, callback) {
350+
const { container: containerId, host } = machine.docker;
351+
352+
log('destroy', containerId.slice(0, 16), 'started');
353+
docker.removeContainer({ host, container: containerId }, error => {
354+
if (error) {
355+
log('destroy', containerId.slice(0, 16), error);
356+
callback(error);
357+
return;
358+
}
359+
360+
callback();
292361

293362
if (!machine.docker.image) {
294363
log('destroy', containerId.slice(0, 16), 'success');
@@ -307,7 +376,7 @@ exports.destroy = function (user, projectId, machineId, callback) {
307376
db.save();
308377
});
309378
});
310-
};
379+
}
311380

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

524+
function createNewTemporaryMachine (sessionId, projectId) {
525+
const project = getProject(projectId);
526+
const temporaryMachines = db.get('temporaryMachines');
527+
if (!(sessionId in temporaryMachines)) {
528+
temporaryMachines[sessionId] = [];
529+
}
530+
531+
const machines = temporaryMachines[sessionId];
532+
533+
const machine = {
534+
properties: {
535+
name: project.name + ' #' + machines.length,
536+
},
537+
status: 'new',
538+
docker: {
539+
host: '',
540+
container: '',
541+
ports: {},
542+
logs: ''
543+
},
544+
data: {}
545+
};
546+
machines.push(machine);
547+
548+
return machine;
549+
}
550+
455551
// Get a unique available port starting from 42000.
456552
function getPort () {
457553
const ports = db.get('ports');

lib/tasks.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const db = require('./db');
2+
const log = require('./log');
3+
let taskCounter = 0; // Used to generate unique task IDs
4+
5+
exports.tasks = db.get('tasks');
6+
exports.taskTypes = new Map();
7+
8+
exports.check = function () {
9+
const now = Date.now();
10+
for (const task of Object.values(exports.tasks)) {
11+
if (now > Number(task.date)) {
12+
exports.execute(task);
13+
}
14+
}
15+
};
16+
17+
exports.addType = function (type, task) {
18+
if (exports.taskTypes.has(type)) {
19+
throw new Error('[fail] task', task, 'already exists');
20+
}
21+
22+
exports.taskTypes.set(type, task);
23+
};
24+
25+
exports.add = function (date, type, data) {
26+
taskCounter++;
27+
28+
const dateHash = date.getTime();
29+
const taskId = `${type}-${dateHash}-${taskCounter}`;
30+
31+
const task = {
32+
id: taskId,
33+
date: dateHash,
34+
type,
35+
data,
36+
};
37+
38+
exports.tasks[taskId] = task;
39+
db.save();
40+
return task;
41+
};
42+
43+
exports.remove = function (id) {
44+
delete exports.tasks[id];
45+
db.save();
46+
};
47+
48+
exports.execute = function ({ type, data, id }) {
49+
const task = exports.taskTypes.get(type);
50+
log("[ok] Task", id, "executed", data);
51+
task(data);
52+
exports.remove(id);
53+
};

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)