@@ -21,6 +21,9 @@ export enum Platform {
21
21
UNIX , WIN , DOCKER
22
22
}
23
23
24
+ const DOCKER_DEFAULT_COMMAND = 'sh'
25
+ const DOCKER_CONTAINER_ID_SHORT_LENGTH = 12
26
+
24
27
/**
25
28
* Parameters of a user session inside an environment
26
29
*/
@@ -49,6 +52,25 @@ export class SessionParameters {
49
52
* Standard output stream
50
53
*/
51
54
stdout : stream . Writable = process . stdout
55
+
56
+ /**
57
+ * CPU shares (only applies when using Docker platform). Priority of the container relative to other processes.
58
+ * The default is 1024, a higher number means higher priority for execution (when CPU contention exists).
59
+ */
60
+ cpuShares : Number = 1024
61
+
62
+ /**
63
+ * Memory limit (only applies when using Docker platform). Maximum amount of memory a container can use.
64
+ * Should be set in the format <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g.
65
+ * Minimum is 4M.
66
+ */
67
+ memoryLimit : string = '0'
68
+
69
+ /**
70
+ * The ID of the container to attempt to use for an execution. It may be an empty string in which case the executor
71
+ * will start a new container.
72
+ */
73
+ containerId : string = ''
52
74
}
53
75
54
76
/**
@@ -350,11 +372,11 @@ export default class Environment {
350
372
/**
351
373
* Create variables for an environment.
352
374
*
353
- * This method is used in several other metho
375
+ * This method is used in several other methods
354
376
* e.g. `within`, `enter`
355
377
*
356
378
* A 'pure' environment will only have available the executables that
357
- * were exlicitly installed into the environment
379
+ * were explicitly installed into the environment
358
380
*
359
381
* @param pure Should the shell that this command is executed in be 'pure'?
360
382
*/
@@ -389,14 +411,42 @@ export default class Environment {
389
411
} )
390
412
}
391
413
414
+ private async getDockerShellArgs ( dockerCommand : string , sessionParameters : SessionParameters , daemonize : boolean = false ) : Promise < Array < string > > {
415
+ const { command, cpuShares, memoryLimit } = sessionParameters
416
+ const nixLocation = await nix . location ( this . name )
417
+ const shellArgs = [
418
+ dockerCommand , '--interactive' , '--tty' , '--rm' ,
419
+ // Prepend the environment path to the PATH variable
420
+ '--env' , `PATH=${ nixLocation } /bin:${ nixLocation } /sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` ,
421
+ // We also need to tell R where to find libraries
422
+ '--env' , `R_LIBS_SITE=${ nixLocation } /library` ,
423
+ // Read-only bind mount of the Nix store
424
+ '--volume' , '/nix/store:/nix/store:ro' ,
425
+ // Apply CPU shares
426
+ `--cpu-shares=${ cpuShares } ` ,
427
+ // Apply memory limit
428
+ `--memory=${ memoryLimit } ` ,
429
+ // We use Alpine Linux as a base image because it is very small but has some basic
430
+ // shell utilities (lkike ls and uname) that are good for debugging but also sometimes
431
+ // required for things like R
432
+ 'alpine'
433
+ ] . concat (
434
+ // Command to execute in the container
435
+ command ? command . split ( ' ' ) : DOCKER_DEFAULT_COMMAND
436
+ )
437
+
438
+ if ( daemonize ) shellArgs . splice ( 1 , 0 , '-d' )
439
+
440
+ return shellArgs
441
+ }
442
+
392
443
/**
393
444
* Enter the a shell within the environment
394
445
*
395
446
* @param sessionParameters Parameters of the session
396
447
*/
397
448
async enter ( sessionParameters : SessionParameters ) {
398
449
let { command, platform, pure, stdin, stdout } = sessionParameters
399
- const location = await nix . location ( this . name )
400
450
401
451
if ( platform === undefined ) {
402
452
switch ( os . platform ( ) ) {
@@ -417,22 +467,7 @@ export default class Environment {
417
467
break
418
468
case Platform . DOCKER :
419
469
shellName = 'docker'
420
- shellArgs = [
421
- 'run' , '--interactive' , '--tty' , '--rm' ,
422
- // Prepend the environment path to the PATH variable
423
- '--env' , `PATH=${ location } /bin:${ location } /sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` ,
424
- // We also need to tell R where to find libraries
425
- '--env' , `R_LIBS_SITE=${ location } /library` ,
426
- // Read-only bind mount of the Nix store
427
- '--volume' , '/nix/store:/nix/store:ro' ,
428
- // We use Alpine Linux as a base image because it is very small but has some basic
429
- // shell utilities (lkike ls and uname) that are good for debugging but also sometimes
430
- // required for things like R
431
- 'alpine'
432
- ] . concat (
433
- // Command to execute in the container
434
- command ? command . split ( ' ' ) : 'sh'
435
- )
470
+ shellArgs = await this . getDockerShellArgs ( 'run' , sessionParameters , false )
436
471
break
437
472
default :
438
473
shellName = 'bash'
@@ -452,10 +487,14 @@ export default class Environment {
452
487
// During development you'll need to use ---pure=false so that
453
488
// node is available to run Nixster. In production, when a user
454
489
// has installed a binary, this shouldn't be necessary
455
- let nixsterPath = await spawn ( 'which' , [ 'nixster' ] )
456
- const tempRcFile = tmp . fileSync ( )
457
- fs . writeFileSync ( tempRcFile . name , `alias nixster="${ nixsterPath . toString ( ) . trim ( ) } "\n` )
458
- shellArgs . push ( '--rcfile' , tempRcFile . name )
490
+ try {
491
+ let nixsterPath = await spawn ( 'which' , [ 'nixster' ] )
492
+ const tempRcFile = tmp . fileSync ( )
493
+ fs . writeFileSync ( tempRcFile . name , `alias nixster="${ nixsterPath . toString ( ) . trim ( ) } "\n` )
494
+ shellArgs . push ( '--rcfile' , tempRcFile . name )
495
+ } catch ( e ) {
496
+ // ignore
497
+ }
459
498
}
460
499
461
500
// Environment variables
@@ -512,4 +551,53 @@ export default class Environment {
512
551
513
552
if ( platform === Platform . UNIX && command ) shellProcess . write ( command + '\r' )
514
553
}
554
+
555
+ private async checkContainerRunning ( containerId : string ) {
556
+ const containerRegex = new RegExp ( / ^ [ ^ _ \W ] { 12 } $ / )
557
+ if ( containerRegex . exec ( containerId ) === null ) {
558
+ throw new Error ( `'${ containerId } ' is not a valid docker container ID.` )
559
+ }
560
+
561
+ // List running containers that match the containerId we are looking for. There should be only one or zero.
562
+ const dockerPsProcess = await spawn ( 'docker' , [ 'ps' , '-q' , '--filter' , `id=${ containerId } ` ] )
563
+ const foundContainerId = dockerPsProcess . toString ( ) . trim ( )
564
+ return foundContainerId === containerId // foundContainerId should be either containerId or an empty string
565
+ }
566
+
567
+ /**
568
+ * Start a new Docker container and execute a command within it. The container daemonizes and keeps running
569
+ * (until the process it is running stops).
570
+ *
571
+ * Returns the short ID of the container that is running.
572
+ */
573
+ async execute ( sessionParameters : SessionParameters ) : Promise < string > {
574
+ if ( sessionParameters . platform !== Platform . DOCKER ) {
575
+ throw new Error ( 'Execute is only valid with the Docker platform.' )
576
+ }
577
+
578
+ const shellArgs = await this . getDockerShellArgs ( 'run' , sessionParameters , true )
579
+
580
+ const dockerProcess = await spawn ( 'docker' , shellArgs )
581
+ return dockerProcess . toString ( ) . trim ( ) . substr ( 0 , DOCKER_CONTAINER_ID_SHORT_LENGTH )
582
+ }
583
+
584
+ /**
585
+ * Build a Docker container for this environment
586
+ */
587
+ async dockerBuild ( ) {
588
+ const requisites = await nix . requisites ( this . name )
589
+ const dockerignore = `*\n${ requisites . map ( req => '!' + req ) . join ( '\n' ) } `
590
+ console . log ( dockerignore )
591
+
592
+ // The Dockerfile does essentially the same as the `docker run` command
593
+ // generated above in `dockerRun`...
594
+ const location = await nix . location ( this . name )
595
+ const dockerfile = `
596
+ FROM alpine
597
+ ENV PATH ${ location } /bin:${ location } /sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
598
+ ENV R_LIBS_SITE=${ location } /library
599
+ COPY /nix/store /nix/store
600
+ `
601
+ console . log ( dockerfile )
602
+ }
515
603
}
0 commit comments