2018-03-05 06:02:31 -05:00
SandboxedModule = require ( 'sandboxed-module' )
sinon = require ( 'sinon' )
require ( 'chai' ) . should ( )
expect = require ( 'chai' ) . expect
require "coffee-script"
modulePath = require ( 'path' ) . join _ _dirname , '../../../app/coffee/DockerRunner'
Path = require "path"
describe "DockerRunner" , - >
beforeEach - >
@ container = container = { }
@ DockerRunner = SandboxedModule . require modulePath , requires :
"settings-sharelatex" : @ Settings =
clsi : docker : { }
path : { }
"logger-sharelatex" : @ logger = {
log : sinon . stub ( ) ,
error : sinon . stub ( ) ,
info : sinon . stub ( ) ,
warn : sinon . stub ( )
}
"dockerode" : class Docker
getContainer : sinon . stub ( ) . returns ( container )
createContainer : sinon . stub ( ) . yields ( null , container )
listContainers : sinon . stub ( )
"fs" : @ fs = { stat : sinon . stub ( ) . yields ( null , { isDirectory : ( ) - > true } ) }
"./Metrics" :
Timer : class Timer
done : ( ) - >
"./LockManager" :
runWithLock : ( key , runner , callback ) - > runner ( callback )
@ Docker = Docker
@ getContainer = Docker : : getContainer
@ createContainer = Docker : : createContainer
@ listContainers = Docker : : listContainers
@ directory = "/local/compile/directory"
@ mainFile = "main-file.tex"
@ compiler = "pdflatex"
@ image = "example.com/sharelatex/image:2016.2"
@ env = { }
@ callback = sinon . stub ( )
@ project _id = "project-id-123"
@ volumes =
"/local/compile/directory" : "/compile"
@ Settings . clsi . docker . image = @ defaultImage = "default-image"
@ Settings . clsi . docker . env = PATH : "mock-path"
describe "run" , - >
beforeEach ( done ) - >
@ DockerRunner . _getContainerOptions = sinon . stub ( ) . returns ( @ options = { mockoptions : "foo" } )
@ DockerRunner . _fingerprintContainer = sinon . stub ( ) . returns ( @ fingerprint = "fingerprint" )
@ name = "project-#{@project_id}-#{@fingerprint}"
@ command = [ "mock" , "command" , "--outdir=$COMPILE_DIR" ]
@ command _with _dir = [ "mock" , "command" , "--outdir=/compile" ]
@ timeout = 42000
done ( )
describe "successfully" , - >
beforeEach ( done ) - >
@ DockerRunner . _runAndWaitForContainer = sinon . stub ( ) . callsArgWith ( 3 , null , @ output = "mock-output" )
@ DockerRunner . run @ project _id , @ command , @ directory , @ image , @ timeout , @ env , ( err , output ) =>
@ callback ( err , output )
done ( )
it "should generate the options for the container" , - >
@ DockerRunner . _getContainerOptions
. calledWith ( @ command _with _dir , @ image , @ volumes , @ timeout )
. should . equal true
it "should generate the fingerprint from the returned options" , - >
@ DockerRunner . _fingerprintContainer
. calledWith ( @ options )
. should . equal true
it "should do the run" , - >
@ DockerRunner . _runAndWaitForContainer
. calledWith ( @ options , @ volumes , @ timeout )
. should . equal true
it "should call the callback" , - >
@ callback . calledWith ( null , @ output ) . should . equal true
describe 'when path.sandboxedCompilesHostDir is set' , - >
beforeEach - >
@ Settings . path . sandboxedCompilesHostDir = '/some/host/dir/compiles'
@ directory = '/var/lib/sharelatex/data/compiles/xyz'
@ DockerRunner . _runAndWaitForContainer = sinon . stub ( ) . callsArgWith ( 3 , null , @ output = "mock-output" )
@ DockerRunner . run @ project _id , @ command , @ directory , @ image , @ timeout , @ env , @ callback
it 'should re-write the bind directory' , - >
volumes = @ DockerRunner . _runAndWaitForContainer . lastCall . args [ 1 ]
expect ( volumes ) . to . deep . equal {
'/some/host/dir/compiles/xyz' : '/compile'
}
it "should call the callback" , - >
@ callback . calledWith ( null , @ output ) . should . equal true
describe "when the run throws an error" , - >
beforeEach - >
firstTime = true
@ output = "mock-output"
@ DockerRunner . _runAndWaitForContainer = ( options , volumes , timeout , callback = ( error , output ) - > ) =>
if firstTime
firstTime = false
callback new Error ( "HTTP code is 500 which indicates error: server error" )
else
callback ( null , @ output )
sinon . spy @ DockerRunner , "_runAndWaitForContainer"
@ DockerRunner . destroyContainer = sinon . stub ( ) . callsArg ( 3 )
@ DockerRunner . run @ project _id , @ command , @ directory , @ image , @ timeout , @ env , @ callback
it "should do the run twice" , - >
@ DockerRunner . _runAndWaitForContainer
. calledTwice . should . equal true
it "should destroy the container in between" , - >
@ DockerRunner . destroyContainer
. calledWith ( @ name , null )
. should . equal true
it "should call the callback" , - >
@ callback . calledWith ( null , @ output ) . should . equal true
describe "with no image" , - >
beforeEach - >
@ DockerRunner . _runAndWaitForContainer = sinon . stub ( ) . callsArgWith ( 3 , null , @ output = "mock-output" )
@ DockerRunner . run @ project _id , @ command , @ directory , null , @ timeout , @ env , @ callback
it "should use the default image" , - >
@ DockerRunner . _getContainerOptions
. calledWith ( @ command _with _dir , @ defaultImage , @ volumes , @ timeout )
. should . equal true
2018-07-13 06:52:49 -04:00
describe "with image override" , - >
beforeEach - >
2018-07-16 12:25:14 -04:00
@ Settings . texliveImageNameOveride = "overrideimage.com/something"
2018-07-13 06:52:49 -04:00
@ DockerRunner . _runAndWaitForContainer = sinon . stub ( ) . callsArgWith ( 3 , null , @ output = "mock-output" )
@ DockerRunner . run @ project _id , @ command , @ directory , @ image , @ timeout , @ env , @ callback
it "should use the override and keep the tag" , - >
image = @ DockerRunner . _getContainerOptions . args [ 0 ] [ 1 ]
2018-07-16 12:25:14 -04:00
image . should . equal "overrideimage.com/something/image:2016.2"
2018-03-05 06:02:31 -05:00
describe "_runAndWaitForContainer" , - >
beforeEach - >
@ options = { mockoptions : "foo" , name : @ name = "mock-name" }
@ DockerRunner . startContainer = ( options , volumes , attachStreamHandler , callback ) =>
attachStreamHandler ( null , @ output = "mock-output" )
callback ( null , @ containerId = "container-id" )
sinon . spy @ DockerRunner , "startContainer"
@ DockerRunner . waitForContainer = sinon . stub ( ) . callsArgWith ( 2 , null , @ exitCode = 42 )
@ DockerRunner . _runAndWaitForContainer @ options , @ volumes , @ timeout , @ callback
it "should create/start the container" , - >
@ DockerRunner . startContainer
. calledWith ( @ options , @ volumes )
. should . equal true
it "should wait for the container to finish" , - >
@ DockerRunner . waitForContainer
. calledWith ( @ name , @ timeout )
. should . equal true
it "should call the callback with the output" , - >
@ callback . calledWith ( null , @ output ) . should . equal true
describe "startContainer" , - >
beforeEach - >
@ attachStreamHandler = sinon . stub ( )
@ attachStreamHandler . cock = true
@ options = { mockoptions : "foo" , name : "mock-name" }
@ container . inspect = sinon . stub ( ) . callsArgWith ( 0 )
@ DockerRunner . attachToContainer = ( containerId , attachStreamHandler , cb ) =>
attachStreamHandler ( )
cb ( )
sinon . spy @ DockerRunner , "attachToContainer"
describe "when the container exists" , - >
beforeEach - >
@ container . inspect = sinon . stub ( ) . callsArgWith ( 0 )
@ container . start = sinon . stub ( ) . yields ( )
@ DockerRunner . startContainer @ options , @ volumes , @ callback , - >
it "should start the container with the given name" , - >
@ getContainer
. calledWith ( @ options . name )
. should . equal true
@ container . start
. called
. should . equal true
it "should not try to create the container" , - >
@ createContainer . called . should . equal false
it "should attach to the container" , - >
@ DockerRunner . attachToContainer . called . should . equal true
it "should call the callback" , - >
@ callback . called . should . equal true
it "should attach before the container starts" , - >
sinon . assert . callOrder ( @ DockerRunner . attachToContainer , @ container . start )
describe "when the container does not exist" , - >
beforeEach ( ) - >
exists = false
@ container . start = sinon . stub ( ) . yields ( )
@ container . inspect = sinon . stub ( ) . callsArgWith ( 0 , { statusCode : 404 } )
@ DockerRunner . startContainer @ options , @ volumes , @ attachStreamHandler , @ callback
it "should create the container" , - >
@ createContainer
. calledWith ( @ options )
. should . equal true
it "should call the callback and stream handler" , - >
@ attachStreamHandler . called . should . equal true
@ callback . called . should . equal true
it "should attach to the container" , - >
@ DockerRunner . attachToContainer . called . should . equal true
it "should attach before the container starts" , - >
sinon . assert . callOrder ( @ DockerRunner . attachToContainer , @ container . start )
describe "when the container is already running" , - >
beforeEach - >
error = new Error ( "HTTP code is 304 which indicates error: server error - start: Cannot start container #{@name}: The container MOCKID is already running." )
error . statusCode = 304
@ container . start = sinon . stub ( ) . yields ( error )
@ container . inspect = sinon . stub ( ) . callsArgWith ( 0 )
@ DockerRunner . startContainer @ options , @ volumes , @ attachStreamHandler , @ callback
it "should not try to create the container" , - >
@ createContainer . called . should . equal false
it "should call the callback and stream handler without an error" , - >
@ attachStreamHandler . called . should . equal true
@ callback . called . should . equal true
describe "when a volume does not exist" , - >
beforeEach ( ) - >
@ fs . stat = sinon . stub ( ) . yields ( new Error ( "no such path" ) )
@ DockerRunner . startContainer @ options , @ volumes , @ attachStreamHandler , @ callback
it "should not try to create the container" , - >
@ createContainer . called . should . equal false
it "should call the callback with an error" , - >
@ callback . calledWith ( new Error ( ) ) . should . equal true
describe "when a volume exists but is not a directory" , - >
beforeEach - >
@ fs . stat = sinon . stub ( ) . yields ( null , { isDirectory : ( ) - > return false } )
@ DockerRunner . startContainer @ options , @ volumes , @ attachStreamHandler , @ callback
it "should not try to create the container" , - >
@ createContainer . called . should . equal false
it "should call the callback with an error" , - >
@ callback . calledWith ( new Error ( ) ) . should . equal true
describe "when a volume does not exist, but sibling-containers are used" , - >
beforeEach - >
@ fs . stat = sinon . stub ( ) . yields ( new Error ( "no such path" ) )
@ Settings . path . sandboxedCompilesHostDir = '/some/path'
@ container . start = sinon . stub ( ) . yields ( )
@ DockerRunner . startContainer @ options , @ volumes , @ callback
afterEach - >
delete @ Settings . path . sandboxedCompilesHostDir
it "should start the container with the given name" , - >
@ getContainer
. calledWith ( @ options . name )
. should . equal true
@ container . start
. called
. should . equal true
it "should not try to create the container" , - >
@ createContainer . called . should . equal false
it "should call the callback" , - >
@ callback . called . should . equal true
@ callback . calledWith ( new Error ( ) ) . should . equal false
describe "when the container tries to be created, but already has been (race condition)" , - >
describe "waitForContainer" , - >
beforeEach - >
@ containerId = "container-id"
@ timeout = 5000
@ container . wait = sinon . stub ( ) . yields ( null , StatusCode : @ statusCode = 42 )
@ container . kill = sinon . stub ( ) . yields ( )
describe "when the container returns in time" , - >
beforeEach - >
@ DockerRunner . waitForContainer @ containerId , @ timeout , @ callback
it "should wait for the container" , - >
@ getContainer
. calledWith ( @ containerId )
. should . equal true
@ container . wait
. called
. should . equal true
it "should call the callback with the exit" , - >
@ callback
. calledWith ( null , @ statusCode )
. should . equal true
describe "when the container does not return before the timeout" , - >
beforeEach ( done ) - >
@ container . wait = ( callback = ( error , exitCode ) - > ) - >
setTimeout ( ) - >
callback ( null , StatusCode : 42 )
, 100
@ timeout = 5
@ DockerRunner . waitForContainer @ containerId , @ timeout , ( args ... ) =>
@ callback ( args ... )
done ( )
it "should call kill on the container" , - >
@ getContainer
. calledWith ( @ containerId )
. should . equal true
@ container . kill
. called
. should . equal true
it "should call the callback with an error" , - >
error = new Error ( "container timed out" )
error . timedout = true
@ callback
. calledWith ( error )
. should . equal true
describe "destroyOldContainers" , - >
beforeEach ( done ) - >
oneHourInSeconds = 60 * 60
oneHourInMilliseconds = oneHourInSeconds * 1000
nowInSeconds = Date . now ( ) / 1000
@ containers = [ {
Name : "/project-old-container-name"
Id : "old-container-id"
Created : nowInSeconds - oneHourInSeconds - 100
} , {
Name : "/project-new-container-name"
Id : "new-container-id"
Created : nowInSeconds - oneHourInSeconds + 100
} , {
Name : "/totally-not-a-project-container"
Id : "some-random-id"
Created : nowInSeconds - ( 2 * oneHourInSeconds )
} ]
@ DockerRunner . MAX _CONTAINER _AGE = oneHourInMilliseconds
@ listContainers . callsArgWith ( 1 , null , @ containers )
@ DockerRunner . destroyContainer = sinon . stub ( ) . callsArg ( 3 )
@ DockerRunner . destroyOldContainers ( error ) =>
@ callback ( error )
done ( )
it "should list all containers" , - >
@ listContainers
. calledWith ( all : true )
. should . equal true
it "should destroy old containers" , - >
@ DockerRunner . destroyContainer
. callCount
. should . equal 1
@ DockerRunner . destroyContainer
. calledWith ( "/project-old-container-name" , "old-container-id" )
. should . equal true
it "should not destroy new containers" , - >
@ DockerRunner . destroyContainer
. calledWith ( "/project-new-container-name" , "new-container-id" )
. should . equal false
it "should not destroy non-project containers" , - >
@ DockerRunner . destroyContainer
. calledWith ( "/totally-not-a-project-container" , "some-random-id" )
. should . equal false
it "should callback the callback" , - >
@ callback . called . should . equal true
describe '_destroyContainer' , - >
beforeEach - >
@ containerId = 'some_id'
@ fakeContainer =
remove : sinon . stub ( ) . callsArgWith ( 1 , null )
@ Docker : : getContainer = sinon . stub ( ) . returns ( @ fakeContainer )
it 'should get the container' , ( done ) - >
@ DockerRunner . _destroyContainer @ containerId , false , ( err ) =>
@ Docker : : getContainer . callCount . should . equal 1
@ Docker : : getContainer . calledWith ( @ containerId ) . should . equal true
done ( )
it 'should try to force-destroy the container when shouldForce=true' , ( done ) - >
@ DockerRunner . _destroyContainer @ containerId , true , ( err ) =>
@ fakeContainer . remove . callCount . should . equal 1
@ fakeContainer . remove . calledWith ( { force : true } ) . should . equal true
done ( )
it 'should not try to force-destroy the container when shouldForce=false' , ( done ) - >
@ DockerRunner . _destroyContainer @ containerId , false , ( err ) =>
@ fakeContainer . remove . callCount . should . equal 1
@ fakeContainer . remove . calledWith ( { force : false } ) . should . equal true
done ( )
it 'should not produce an error' , ( done ) - >
@ DockerRunner . _destroyContainer @ containerId , false , ( err ) =>
expect ( err ) . to . equal null
done ( )
describe 'when the container is already gone' , - >
beforeEach - >
@ fakeError = new Error ( 'woops' )
@ fakeError . statusCode = 404
@ fakeContainer =
remove : sinon . stub ( ) . callsArgWith ( 1 , @ fakeError )
@ Docker : : getContainer = sinon . stub ( ) . returns ( @ fakeContainer )
it 'should not produce an error' , ( done ) - >
@ DockerRunner . _destroyContainer @ containerId , false , ( err ) =>
expect ( err ) . to . equal null
done ( )
describe 'when container.destroy produces an error' , ( done ) - >
beforeEach - >
@ fakeError = new Error ( 'woops' )
@ fakeError . statusCode = 500
@ fakeContainer =
remove : sinon . stub ( ) . callsArgWith ( 1 , @ fakeError )
@ Docker : : getContainer = sinon . stub ( ) . returns ( @ fakeContainer )
it 'should produce an error' , ( done ) - >
@ DockerRunner . _destroyContainer @ containerId , false , ( err ) =>
expect ( err ) . to . not . equal null
expect ( err ) . to . equal @ fakeError
done ( )
describe 'kill' , - >
beforeEach - >
@ containerId = 'some_id'
@ fakeContainer =
kill : sinon . stub ( ) . callsArgWith ( 0 , null )
@ Docker : : getContainer = sinon . stub ( ) . returns ( @ fakeContainer )
it 'should get the container' , ( done ) - >
@ DockerRunner . kill @ containerId , ( err ) =>
@ Docker : : getContainer . callCount . should . equal 1
@ Docker : : getContainer . calledWith ( @ containerId ) . should . equal true
done ( )
it 'should try to force-destroy the container' , ( done ) - >
@ DockerRunner . kill @ containerId , ( err ) =>
@ fakeContainer . kill . callCount . should . equal 1
done ( )
it 'should not produce an error' , ( done ) - >
@ DockerRunner . kill @ containerId , ( err ) =>
expect ( err ) . to . equal undefined
done ( )
describe 'when the container is not actually running' , - >
beforeEach - >
@ fakeError = new Error ( 'woops' )
@ fakeError . statusCode = 500
@ fakeError . message = 'Cannot kill container <whatever> is not running'
@ fakeContainer =
kill : sinon . stub ( ) . callsArgWith ( 0 , @ fakeError )
@ Docker : : getContainer = sinon . stub ( ) . returns ( @ fakeContainer )
it 'should not produce an error' , ( done ) - >
@ DockerRunner . kill @ containerId , ( err ) =>
expect ( err ) . to . equal undefined
done ( )
describe 'when container.kill produces a legitimate error' , ( done ) - >
beforeEach - >
@ fakeError = new Error ( 'woops' )
@ fakeError . statusCode = 500
@ fakeError . message = 'Totally legitimate reason to throw an error'
@ fakeContainer =
kill : sinon . stub ( ) . callsArgWith ( 0 , @ fakeError )
@ Docker : : getContainer = sinon . stub ( ) . returns ( @ fakeContainer )
it 'should produce an error' , ( done ) - >
@ DockerRunner . kill @ containerId , ( err ) =>
expect ( err ) . to . not . equal undefined
expect ( err ) . to . equal @ fakeError
done ( )