Skip to content
This repository was archived by the owner on May 17, 2024. It is now read-only.

Commit aef52bf

Browse files
committed
feat(serve): Added /stop endpoint to stop containers by ID
1 parent 8f5857d commit aef52bf

File tree

2 files changed

+123
-28
lines changed

2 files changed

+123
-28
lines changed

src/Environment.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export default class Environment {
172172
* @param options Optional attributes for the environment e.g. `packages`, `meta`
173173
* @param force If the environment already exists should it be overitten?
174174
*/
175-
static async create (name: string, options: {[key: string]: any} = {}, force: boolean = false): Promise<Environment> {
175+
static async create (name: string, options: { [key: string]: any } = {}, force: boolean = false): Promise<Environment> {
176176
const env = new Environment(name, false)
177177

178178
if (!force) {
@@ -380,7 +380,7 @@ export default class Environment {
380380
*
381381
* @param pure Should the shell that this command is executed in be 'pure'?
382382
*/
383-
async vars (pure: boolean = false): Promise<{[key: string]: string}> {
383+
async vars (pure: boolean = false): Promise<{ [key: string]: string }> {
384384
const location = nix.location(this.name)
385385

386386
let PATH = `${location}/bin:${location}/sbin`
@@ -422,25 +422,25 @@ export default class Environment {
422422
const { command, cpuShares, memoryLimit } = sessionParameters
423423
const nixLocation = nix.location(this.name)
424424
const shellArgs = [
425-
dockerCommand, '--interactive', '--tty', '--rm',
426-
// Prepend the environment path to the PATH variable
427-
'--env', `PATH=${nixLocation}/bin:${nixLocation}/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
428-
// We also need to tell R where to find libraries
429-
'--env', `R_LIBS_SITE=${nixLocation}/library`,
430-
// Read-only bind mount of the Nix store
431-
'--volume', '/nix/store:/nix/store:ro',
432-
// Apply CPU shares
433-
`--cpu-shares=${cpuShares}`,
434-
// Apply memory limit
435-
`--memory=${memoryLimit}`,
436-
// We use Alpine Linux as a base image because it is very small but has some basic
437-
// shell utilities (lkike ls and uname) that are good for debugging but also sometimes
438-
// required for things like R
439-
'alpine'
440-
].concat(
441-
// Command to execute in the container
442-
command ? command.split(' ') : DOCKER_DEFAULT_COMMAND
443-
)
425+
dockerCommand, '--interactive', '--tty', '--rm',
426+
// Prepend the environment path to the PATH variable
427+
'--env', `PATH=${nixLocation}/bin:${nixLocation}/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
428+
// We also need to tell R where to find libraries
429+
'--env', `R_LIBS_SITE=${nixLocation}/library`,
430+
// Read-only bind mount of the Nix store
431+
'--volume', '/nix/store:/nix/store:ro',
432+
// Apply CPU shares
433+
`--cpu-shares=${cpuShares}`,
434+
// Apply memory limit
435+
`--memory=${memoryLimit}`,
436+
// We use Alpine Linux as a base image because it is very small but has some basic
437+
// shell utilities (lkike ls and uname) that are good for debugging but also sometimes
438+
// required for things like R
439+
'alpine'
440+
].concat(
441+
// Command to execute in the container
442+
command ? command.split(' ') : DOCKER_DEFAULT_COMMAND
443+
)
444444

445445
if (daemonize) shellArgs.splice(1, 0, '-d')
446446

@@ -562,9 +562,9 @@ export default class Environment {
562562
/**
563563
* Determine if a Docker container is running using 'docker ps'
564564
*
565-
* @param containerId The ID of the container, can either be the long or truncated version.
565+
* @param containerId The ID of the container, must be truncated version (12 alphanumeric characters).
566566
*/
567-
private async checkContainerRunning (containerId: string): Promise<boolean> {
567+
async containerIsRunning (containerId: string): Promise<boolean> {
568568
const containerRegex = new RegExp(/^[^_\W]{12}$/)
569569
if (containerRegex.exec(containerId) === null) {
570570
throw new Error(`'${containerId}' is not a valid docker container ID.`)
@@ -593,6 +593,16 @@ export default class Environment {
593593
return dockerProcess.toString().trim().substr(0, DOCKER_CONTAINER_ID_SHORT_LENGTH)
594594
}
595595

596+
/**
597+
* Stop a running Docker container. Return true if the container was stopped or false if there was an error.
598+
*
599+
* @param containerId
600+
*/
601+
async stopContainer (containerId: string): Promise<boolean> {
602+
const dockerStopProcess = await spawn('docker', ['stop', containerId])
603+
return dockerStopProcess.toString().trim().substr(0, DOCKER_CONTAINER_ID_SHORT_LENGTH) === containerId.substr(0, DOCKER_CONTAINER_ID_SHORT_LENGTH)
604+
}
605+
596606
/**
597607
* Build a Docker container for this environment
598608
*/

src/serve.ts

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,64 @@ app.use(express.static(path.join(__dirname, 'static')))
3535
const jsonParser = require('body-parser').json()
3636
app.use(jsonParser)
3737

38+
/**
39+
* Validates that all the `requiredParameters` are properties of `body`.
40+
* Returns an array of all parameters that are missing.
41+
*
42+
* @param body
43+
* @param requiredParameters
44+
*/
45+
function validateParameters (body: any, requiredParameters: Array<string>): Array<string> {
46+
let missingParameters: Array<string> = []
47+
48+
requiredParameters.forEach(parameter => {
49+
if (!body[parameter]) {
50+
missingParameters.push(parameter)
51+
}
52+
})
53+
54+
return missingParameters
55+
}
56+
57+
/**
58+
* Send a 400 status code to the response, with the `errorMessage` as a JSON response.
59+
*
60+
* @param response
61+
* @param errorMessage
62+
*/
63+
function sendBadRequestResponse (response: express.Response, errorMessage: string) {
64+
response.status(400).json({
65+
error: errorMessage
66+
})
67+
}
68+
69+
/**
70+
* Validate that a request has a valid JSON body and has all the `requiredParameters`. It sends appropriate error
71+
* messages back to the client, and then returns false, if the request is not valid; the calling function should
72+
* then stop sending its response. If this function returns true the calling function should continue as normal.
73+
*
74+
* @param request
75+
* @param response
76+
* @param requiredParameters
77+
*/
78+
function doRequestValidation (request: express.Request, response: express.Response,
79+
requiredParameters: Array<string>): boolean {
80+
if (!request.body) {
81+
sendBadRequestResponse(response, 'Valid JSON body was not found.')
82+
return false
83+
}
84+
85+
const missingParameters = validateParameters(request.body, requiredParameters)
86+
87+
if (missingParameters.length) {
88+
const missingParametersStr = missingParameters.join(', ')
89+
sendBadRequestResponse(response, `Missing parameter(s) in body: ${missingParametersStr}.`)
90+
return false
91+
}
92+
93+
return true
94+
}
95+
3896
// todo: rename shell to interact
3997
// Instantiate shell and set up data handlers
4098
expressWs.app.ws('/shell', async (ws: any, req: express.Request) => {
@@ -76,23 +134,50 @@ app.use((error: Error, req: express.Request, res: express.Response, next: any) =
76134
next(error)
77135
})
78136

79-
expressWs.app.post('/execute', jsonParser, async (req: any, res: any) => {
80-
// req: some JSON -> new SessionParameters object to start in env.execute (new execute method)
81-
if (!req.body) return res.sendStatus(400)
137+
expressWs.app.post('/execute', jsonParser, async (req: express.Request, res: express.Response) => {
138+
if (!doRequestValidation(req, res, ['environmentId'])) {
139+
return res.end()
140+
}
82141

83142
const env = new Environment(req.body.environmentId)
143+
84144
const sessionParameters = new SessionParameters()
85145
sessionParameters.platform = Platform.DOCKER
86146
sessionParameters.command = req.body.command || ''
87147

88148
const containerId = await env.execute(sessionParameters)
89-
res.status(200).json({
149+
return res.json({
90150
containerId: containerId
91151
})
92152
})
93153

94-
expressWs.app.post('/stop', async (req: any, res: any) => {
154+
expressWs.app.post('/stop', async (req: express.Request, res: express.Response) => {
95155
// req: some JSON -> with container ID that will stop the container
156+
if (!doRequestValidation(req, res, ['environmentId', 'containerId'])) {
157+
return res.end()
158+
}
159+
160+
const env = new Environment(req.body.environmentId)
161+
const sessionParameters = new SessionParameters()
162+
sessionParameters.platform = Platform.DOCKER
163+
164+
const containerId = req.body.containerId
165+
166+
if (!await env.containerIsRunning(containerId)) {
167+
sendBadRequestResponse(res, `Container ${containerId} is not running.`)
168+
return res.end()
169+
}
170+
171+
if (await env.stopContainer(containerId)) {
172+
return res.json({
173+
message: `Container ${containerId} stopped.`
174+
})
175+
} else {
176+
return res.status(500).json({
177+
error: `Container ${containerId} was not stopped.`
178+
})
179+
}
180+
96181
})
97182

98183
app.listen(argv.port, argv.address)

0 commit comments

Comments
 (0)