DockerRunnerTests.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS206: Consider reworking classes to avoid initClass
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. const SandboxedModule = require('sandboxed-module')
  16. const sinon = require('sinon')
  17. const { expect } = require('chai')
  18. const modulePath = require('path').join(
  19. __dirname,
  20. '../../../app/js/DockerRunner'
  21. )
  22. const Path = require('path')
  23. describe('DockerRunner', function () {
  24. beforeEach(function () {
  25. let container, Docker, Timer
  26. this.container = container = {}
  27. this.DockerRunner = SandboxedModule.require(modulePath, {
  28. requires: {
  29. '@overleaf/settings': (this.Settings = {
  30. clsi: { docker: {} },
  31. path: {},
  32. }),
  33. dockerode: (Docker = (function () {
  34. Docker = class Docker {
  35. static initClass() {
  36. this.prototype.getContainer = sinon.stub().returns(container)
  37. this.prototype.createContainer = sinon
  38. .stub()
  39. .yields(null, container)
  40. this.prototype.listContainers = sinon.stub()
  41. }
  42. }
  43. Docker.initClass()
  44. return Docker
  45. })()),
  46. fs: (this.fs = {
  47. stat: sinon.stub().yields(null, {
  48. isDirectory() {
  49. return true
  50. },
  51. }),
  52. }),
  53. './Metrics': {
  54. Timer: (Timer = class Timer {
  55. done() {}
  56. }),
  57. },
  58. './LockManager': {
  59. runWithLock(key, runner, callback) {
  60. return runner(callback)
  61. },
  62. },
  63. },
  64. globals: { Math }, // used by lodash
  65. })
  66. this.Docker = Docker
  67. this.getContainer = Docker.prototype.getContainer
  68. this.createContainer = Docker.prototype.createContainer
  69. this.listContainers = Docker.prototype.listContainers
  70. this.directory = '/local/compile/directory'
  71. this.mainFile = 'main-file.tex'
  72. this.compiler = 'pdflatex'
  73. this.image = 'example.com/sharelatex/image:2016.2'
  74. this.env = {}
  75. this.callback = sinon.stub()
  76. this.project_id = 'project-id-123'
  77. this.volumes = { '/local/compile/directory': '/compile' }
  78. this.Settings.clsi.docker.image = this.defaultImage = 'default-image'
  79. this.compileGroup = 'compile-group'
  80. return (this.Settings.clsi.docker.env = { PATH: 'mock-path' })
  81. })
  82. afterEach(function () {
  83. this.DockerRunner.stopContainerMonitor()
  84. })
  85. describe('run', function () {
  86. beforeEach(function (done) {
  87. this.DockerRunner._getContainerOptions = sinon
  88. .stub()
  89. .returns((this.options = { mockoptions: 'foo' }))
  90. this.DockerRunner._fingerprintContainer = sinon
  91. .stub()
  92. .returns((this.fingerprint = 'fingerprint'))
  93. this.name = `project-${this.project_id}-${this.fingerprint}`
  94. this.command = ['mock', 'command', '--outdir=$COMPILE_DIR']
  95. this.command_with_dir = ['mock', 'command', '--outdir=/compile']
  96. this.timeout = 42000
  97. return done()
  98. })
  99. describe('successfully', function () {
  100. beforeEach(function (done) {
  101. this.DockerRunner._runAndWaitForContainer = sinon
  102. .stub()
  103. .callsArgWith(3, null, (this.output = 'mock-output'))
  104. return this.DockerRunner.run(
  105. this.project_id,
  106. this.command,
  107. this.directory,
  108. this.image,
  109. this.timeout,
  110. this.env,
  111. this.compileGroup,
  112. (err, output) => {
  113. this.callback(err, output)
  114. return done()
  115. }
  116. )
  117. })
  118. it('should generate the options for the container', function () {
  119. return this.DockerRunner._getContainerOptions
  120. .calledWith(
  121. this.command_with_dir,
  122. this.image,
  123. this.volumes,
  124. this.timeout
  125. )
  126. .should.equal(true)
  127. })
  128. it('should generate the fingerprint from the returned options', function () {
  129. return this.DockerRunner._fingerprintContainer
  130. .calledWith(this.options)
  131. .should.equal(true)
  132. })
  133. it('should do the run', function () {
  134. return this.DockerRunner._runAndWaitForContainer
  135. .calledWith(this.options, this.volumes, this.timeout)
  136. .should.equal(true)
  137. })
  138. return it('should call the callback', function () {
  139. return this.callback.calledWith(null, this.output).should.equal(true)
  140. })
  141. })
  142. describe('when path.sandboxedCompilesHostDir is set', function () {
  143. beforeEach(function () {
  144. this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles'
  145. this.directory = '/var/lib/sharelatex/data/compiles/xyz'
  146. this.DockerRunner._runAndWaitForContainer = sinon
  147. .stub()
  148. .callsArgWith(3, null, (this.output = 'mock-output'))
  149. return this.DockerRunner.run(
  150. this.project_id,
  151. this.command,
  152. this.directory,
  153. this.image,
  154. this.timeout,
  155. this.env,
  156. this.compileGroup,
  157. this.callback
  158. )
  159. })
  160. it('should re-write the bind directory', function () {
  161. const volumes =
  162. this.DockerRunner._runAndWaitForContainer.lastCall.args[1]
  163. return expect(volumes).to.deep.equal({
  164. '/some/host/dir/compiles/xyz': '/compile',
  165. })
  166. })
  167. return it('should call the callback', function () {
  168. return this.callback.calledWith(null, this.output).should.equal(true)
  169. })
  170. })
  171. describe('when the run throws an error', function () {
  172. beforeEach(function () {
  173. let firstTime = true
  174. this.output = 'mock-output'
  175. this.DockerRunner._runAndWaitForContainer = (
  176. options,
  177. volumes,
  178. timeout,
  179. callback
  180. ) => {
  181. if (callback == null) {
  182. callback = function () {}
  183. }
  184. if (firstTime) {
  185. firstTime = false
  186. const error = new Error('(HTTP code 500) server error - ...')
  187. error.statusCode = 500
  188. return callback(error)
  189. } else {
  190. return callback(null, this.output)
  191. }
  192. }
  193. sinon.spy(this.DockerRunner, '_runAndWaitForContainer')
  194. this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
  195. return this.DockerRunner.run(
  196. this.project_id,
  197. this.command,
  198. this.directory,
  199. this.image,
  200. this.timeout,
  201. this.env,
  202. this.compileGroup,
  203. this.callback
  204. )
  205. })
  206. it('should do the run twice', function () {
  207. return this.DockerRunner._runAndWaitForContainer.calledTwice.should.equal(
  208. true
  209. )
  210. })
  211. it('should destroy the container in between', function () {
  212. return this.DockerRunner.destroyContainer
  213. .calledWith(this.name, null)
  214. .should.equal(true)
  215. })
  216. return it('should call the callback', function () {
  217. return this.callback.calledWith(null, this.output).should.equal(true)
  218. })
  219. })
  220. describe('with no image', function () {
  221. beforeEach(function () {
  222. this.DockerRunner._runAndWaitForContainer = sinon
  223. .stub()
  224. .callsArgWith(3, null, (this.output = 'mock-output'))
  225. return this.DockerRunner.run(
  226. this.project_id,
  227. this.command,
  228. this.directory,
  229. null,
  230. this.timeout,
  231. this.env,
  232. this.compileGroup,
  233. this.callback
  234. )
  235. })
  236. return it('should use the default image', function () {
  237. return this.DockerRunner._getContainerOptions
  238. .calledWith(
  239. this.command_with_dir,
  240. this.defaultImage,
  241. this.volumes,
  242. this.timeout
  243. )
  244. .should.equal(true)
  245. })
  246. })
  247. describe('with image override', function () {
  248. beforeEach(function () {
  249. this.Settings.texliveImageNameOveride = 'overrideimage.com/something'
  250. this.DockerRunner._runAndWaitForContainer = sinon
  251. .stub()
  252. .callsArgWith(3, null, (this.output = 'mock-output'))
  253. return this.DockerRunner.run(
  254. this.project_id,
  255. this.command,
  256. this.directory,
  257. this.image,
  258. this.timeout,
  259. this.env,
  260. this.compileGroup,
  261. this.callback
  262. )
  263. })
  264. return it('should use the override and keep the tag', function () {
  265. const image = this.DockerRunner._getContainerOptions.args[0][1]
  266. return image.should.equal('overrideimage.com/something/image:2016.2')
  267. })
  268. })
  269. describe('with image restriction', function () {
  270. beforeEach(function () {
  271. this.Settings.clsi.docker.allowedImages = [
  272. 'repo/image:tag1',
  273. 'repo/image:tag2',
  274. ]
  275. this.DockerRunner._runAndWaitForContainer = sinon
  276. .stub()
  277. .callsArgWith(3, null, (this.output = 'mock-output'))
  278. })
  279. describe('with a valid image', function () {
  280. beforeEach(function () {
  281. this.DockerRunner.run(
  282. this.project_id,
  283. this.command,
  284. this.directory,
  285. 'repo/image:tag1',
  286. this.timeout,
  287. this.env,
  288. this.compileGroup,
  289. this.callback
  290. )
  291. })
  292. it('should setup the container', function () {
  293. this.DockerRunner._getContainerOptions.called.should.equal(true)
  294. })
  295. })
  296. describe('with a invalid image', function () {
  297. beforeEach(function () {
  298. this.DockerRunner.run(
  299. this.project_id,
  300. this.command,
  301. this.directory,
  302. 'something/different:evil',
  303. this.timeout,
  304. this.env,
  305. this.compileGroup,
  306. this.callback
  307. )
  308. })
  309. it('should call the callback with an error', function () {
  310. const err = new Error('image not allowed')
  311. this.callback.called.should.equal(true)
  312. this.callback.args[0][0].message.should.equal(err.message)
  313. })
  314. it('should not setup the container', function () {
  315. this.DockerRunner._getContainerOptions.called.should.equal(false)
  316. })
  317. })
  318. })
  319. })
  320. describe('run with _getOptions', function () {
  321. beforeEach(function (done) {
  322. // this.DockerRunner._getContainerOptions = sinon
  323. // .stub()
  324. // .returns((this.options = { mockoptions: 'foo' }))
  325. this.DockerRunner._fingerprintContainer = sinon
  326. .stub()
  327. .returns((this.fingerprint = 'fingerprint'))
  328. this.name = `project-${this.project_id}-${this.fingerprint}`
  329. this.command = ['mock', 'command', '--outdir=$COMPILE_DIR']
  330. this.command_with_dir = ['mock', 'command', '--outdir=/compile']
  331. this.timeout = 42000
  332. return done()
  333. })
  334. describe('when a compile group config is set', function () {
  335. beforeEach(function () {
  336. this.Settings.clsi.docker.compileGroupConfig = {
  337. 'compile-group': {
  338. 'HostConfig.newProperty': 'new-property',
  339. },
  340. 'other-group': { otherProperty: 'other-property' },
  341. }
  342. this.DockerRunner._runAndWaitForContainer = sinon
  343. .stub()
  344. .callsArgWith(3, null, (this.output = 'mock-output'))
  345. return this.DockerRunner.run(
  346. this.project_id,
  347. this.command,
  348. this.directory,
  349. this.image,
  350. this.timeout,
  351. this.env,
  352. this.compileGroup,
  353. this.callback
  354. )
  355. })
  356. it('should set the docker options for the compile group', function () {
  357. const options =
  358. this.DockerRunner._runAndWaitForContainer.lastCall.args[0]
  359. return expect(options.HostConfig).to.deep.include({
  360. Binds: ['/local/compile/directory:/compile:rw'],
  361. LogConfig: { Type: 'none', Config: {} },
  362. CapDrop: 'ALL',
  363. SecurityOpt: ['no-new-privileges'],
  364. newProperty: 'new-property',
  365. })
  366. })
  367. return it('should call the callback', function () {
  368. return this.callback.calledWith(null, this.output).should.equal(true)
  369. })
  370. })
  371. })
  372. describe('_runAndWaitForContainer', function () {
  373. beforeEach(function () {
  374. this.options = { mockoptions: 'foo', name: (this.name = 'mock-name') }
  375. this.DockerRunner.startContainer = (
  376. options,
  377. volumes,
  378. attachStreamHandler,
  379. callback
  380. ) => {
  381. attachStreamHandler(null, (this.output = 'mock-output'))
  382. return callback(null, (this.containerId = 'container-id'))
  383. }
  384. sinon.spy(this.DockerRunner, 'startContainer')
  385. this.DockerRunner.waitForContainer = sinon
  386. .stub()
  387. .callsArgWith(2, null, (this.exitCode = 42))
  388. return this.DockerRunner._runAndWaitForContainer(
  389. this.options,
  390. this.volumes,
  391. this.timeout,
  392. this.callback
  393. )
  394. })
  395. it('should create/start the container', function () {
  396. return this.DockerRunner.startContainer
  397. .calledWith(this.options, this.volumes)
  398. .should.equal(true)
  399. })
  400. it('should wait for the container to finish', function () {
  401. return this.DockerRunner.waitForContainer
  402. .calledWith(this.name, this.timeout)
  403. .should.equal(true)
  404. })
  405. return it('should call the callback with the output', function () {
  406. return this.callback.calledWith(null, this.output).should.equal(true)
  407. })
  408. })
  409. describe('startContainer', function () {
  410. beforeEach(function () {
  411. this.attachStreamHandler = sinon.stub()
  412. this.attachStreamHandler.cock = true
  413. this.options = { mockoptions: 'foo', name: 'mock-name' }
  414. this.container.inspect = sinon.stub().callsArgWith(0)
  415. this.DockerRunner.attachToContainer = (
  416. containerId,
  417. attachStreamHandler,
  418. cb
  419. ) => {
  420. attachStreamHandler()
  421. return cb()
  422. }
  423. return sinon.spy(this.DockerRunner, 'attachToContainer')
  424. })
  425. describe('when the container exists', function () {
  426. beforeEach(function () {
  427. this.container.inspect = sinon.stub().callsArgWith(0)
  428. this.container.start = sinon.stub().yields()
  429. return this.DockerRunner.startContainer(
  430. this.options,
  431. this.volumes,
  432. () => {},
  433. this.callback
  434. )
  435. })
  436. it('should start the container with the given name', function () {
  437. this.getContainer.calledWith(this.options.name).should.equal(true)
  438. return this.container.start.called.should.equal(true)
  439. })
  440. it('should not try to create the container', function () {
  441. return this.createContainer.called.should.equal(false)
  442. })
  443. it('should attach to the container', function () {
  444. return this.DockerRunner.attachToContainer.called.should.equal(true)
  445. })
  446. it('should call the callback', function () {
  447. return this.callback.called.should.equal(true)
  448. })
  449. return it('should attach before the container starts', function () {
  450. return sinon.assert.callOrder(
  451. this.DockerRunner.attachToContainer,
  452. this.container.start
  453. )
  454. })
  455. })
  456. describe('when the container does not exist', function () {
  457. beforeEach(function () {
  458. const exists = false
  459. this.container.start = sinon.stub().yields()
  460. this.container.inspect = sinon
  461. .stub()
  462. .callsArgWith(0, { statusCode: 404 })
  463. return this.DockerRunner.startContainer(
  464. this.options,
  465. this.volumes,
  466. this.attachStreamHandler,
  467. this.callback
  468. )
  469. })
  470. it('should create the container', function () {
  471. return this.createContainer.calledWith(this.options).should.equal(true)
  472. })
  473. it('should call the callback and stream handler', function () {
  474. this.attachStreamHandler.called.should.equal(true)
  475. return this.callback.called.should.equal(true)
  476. })
  477. it('should attach to the container', function () {
  478. return this.DockerRunner.attachToContainer.called.should.equal(true)
  479. })
  480. return it('should attach before the container starts', function () {
  481. return sinon.assert.callOrder(
  482. this.DockerRunner.attachToContainer,
  483. this.container.start
  484. )
  485. })
  486. })
  487. describe('when the container is already running', function () {
  488. beforeEach(function () {
  489. const error = new Error(
  490. `HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`
  491. )
  492. error.statusCode = 304
  493. this.container.start = sinon.stub().yields(error)
  494. this.container.inspect = sinon.stub().callsArgWith(0)
  495. return this.DockerRunner.startContainer(
  496. this.options,
  497. this.volumes,
  498. this.attachStreamHandler,
  499. this.callback
  500. )
  501. })
  502. it('should not try to create the container', function () {
  503. return this.createContainer.called.should.equal(false)
  504. })
  505. return it('should call the callback and stream handler without an error', function () {
  506. this.attachStreamHandler.called.should.equal(true)
  507. return this.callback.called.should.equal(true)
  508. })
  509. })
  510. describe('when a volume does not exist', function () {
  511. beforeEach(function () {
  512. this.fs.stat = sinon.stub().yields(new Error('no such path'))
  513. return this.DockerRunner.startContainer(
  514. this.options,
  515. this.volumes,
  516. this.attachStreamHandler,
  517. this.callback
  518. )
  519. })
  520. it('should not try to create the container', function () {
  521. return this.createContainer.called.should.equal(false)
  522. })
  523. it('should call the callback with an error', function () {
  524. this.callback.calledWith(sinon.match(Error)).should.equal(true)
  525. })
  526. })
  527. describe('when a volume exists but is not a directory', function () {
  528. beforeEach(function () {
  529. this.fs.stat = sinon.stub().yields(null, {
  530. isDirectory() {
  531. return false
  532. },
  533. })
  534. return this.DockerRunner.startContainer(
  535. this.options,
  536. this.volumes,
  537. this.attachStreamHandler,
  538. this.callback
  539. )
  540. })
  541. it('should not try to create the container', function () {
  542. return this.createContainer.called.should.equal(false)
  543. })
  544. it('should call the callback with an error', function () {
  545. this.callback.calledWith(sinon.match(Error)).should.equal(true)
  546. })
  547. })
  548. describe('when a volume does not exist, but sibling-containers are used', function () {
  549. beforeEach(function () {
  550. this.fs.stat = sinon.stub().yields(new Error('no such path'))
  551. this.Settings.path.sandboxedCompilesHostDir = '/some/path'
  552. this.container.start = sinon.stub().yields()
  553. return this.DockerRunner.startContainer(
  554. this.options,
  555. this.volumes,
  556. () => {},
  557. this.callback
  558. )
  559. })
  560. afterEach(function () {
  561. return delete this.Settings.path.sandboxedCompilesHostDir
  562. })
  563. it('should start the container with the given name', function () {
  564. this.getContainer.calledWith(this.options.name).should.equal(true)
  565. return this.container.start.called.should.equal(true)
  566. })
  567. it('should not try to create the container', function () {
  568. return this.createContainer.called.should.equal(false)
  569. })
  570. return it('should call the callback', function () {
  571. this.callback.called.should.equal(true)
  572. return this.callback.calledWith(new Error()).should.equal(false)
  573. })
  574. })
  575. return describe('when the container tries to be created, but already has been (race condition)', function () {})
  576. })
  577. describe('waitForContainer', function () {
  578. beforeEach(function () {
  579. this.containerId = 'container-id'
  580. this.timeout = 5000
  581. this.container.wait = sinon
  582. .stub()
  583. .yields(null, { StatusCode: (this.statusCode = 42) })
  584. return (this.container.kill = sinon.stub().yields())
  585. })
  586. describe('when the container returns in time', function () {
  587. beforeEach(function () {
  588. return this.DockerRunner.waitForContainer(
  589. this.containerId,
  590. this.timeout,
  591. this.callback
  592. )
  593. })
  594. it('should wait for the container', function () {
  595. this.getContainer.calledWith(this.containerId).should.equal(true)
  596. return this.container.wait.called.should.equal(true)
  597. })
  598. return it('should call the callback with the exit', function () {
  599. return this.callback
  600. .calledWith(null, this.statusCode)
  601. .should.equal(true)
  602. })
  603. })
  604. return describe('when the container does not return before the timeout', function () {
  605. beforeEach(function (done) {
  606. this.container.wait = function (callback) {
  607. if (callback == null) {
  608. callback = function () {}
  609. }
  610. return setTimeout(() => callback(null, { StatusCode: 42 }), 100)
  611. }
  612. this.timeout = 5
  613. return this.DockerRunner.waitForContainer(
  614. this.containerId,
  615. this.timeout,
  616. (...args) => {
  617. this.callback(...Array.from(args || []))
  618. return done()
  619. }
  620. )
  621. })
  622. it('should call kill on the container', function () {
  623. this.getContainer.calledWith(this.containerId).should.equal(true)
  624. return this.container.kill.called.should.equal(true)
  625. })
  626. it('should call the callback with an error', function () {
  627. this.callback.calledWith(sinon.match(Error)).should.equal(true)
  628. const errorObj = this.callback.args[0][0]
  629. expect(errorObj.message).to.include('container timed out')
  630. expect(errorObj.timedout).equal(true)
  631. })
  632. })
  633. })
  634. describe('destroyOldContainers', function () {
  635. beforeEach(function (done) {
  636. const oneHourInSeconds = 60 * 60
  637. const oneHourInMilliseconds = oneHourInSeconds * 1000
  638. const nowInSeconds = Date.now() / 1000
  639. this.containers = [
  640. {
  641. Name: '/project-old-container-name',
  642. Id: 'old-container-id',
  643. Created: nowInSeconds - oneHourInSeconds - 100,
  644. },
  645. {
  646. Name: '/project-new-container-name',
  647. Id: 'new-container-id',
  648. Created: nowInSeconds - oneHourInSeconds + 100,
  649. },
  650. {
  651. Name: '/totally-not-a-project-container',
  652. Id: 'some-random-id',
  653. Created: nowInSeconds - 2 * oneHourInSeconds,
  654. },
  655. ]
  656. this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds
  657. this.listContainers.callsArgWith(1, null, this.containers)
  658. this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
  659. return this.DockerRunner.destroyOldContainers(error => {
  660. this.callback(error)
  661. return done()
  662. })
  663. })
  664. it('should list all containers', function () {
  665. return this.listContainers.calledWith({ all: true }).should.equal(true)
  666. })
  667. it('should destroy old containers', function () {
  668. this.DockerRunner.destroyContainer.callCount.should.equal(1)
  669. return this.DockerRunner.destroyContainer
  670. .calledWith('project-old-container-name', 'old-container-id')
  671. .should.equal(true)
  672. })
  673. it('should not destroy new containers', function () {
  674. return this.DockerRunner.destroyContainer
  675. .calledWith('project-new-container-name', 'new-container-id')
  676. .should.equal(false)
  677. })
  678. it('should not destroy non-project containers', function () {
  679. return this.DockerRunner.destroyContainer
  680. .calledWith('totally-not-a-project-container', 'some-random-id')
  681. .should.equal(false)
  682. })
  683. return it('should callback the callback', function () {
  684. return this.callback.called.should.equal(true)
  685. })
  686. })
  687. describe('_destroyContainer', function () {
  688. beforeEach(function () {
  689. this.containerId = 'some_id'
  690. this.fakeContainer = { remove: sinon.stub().callsArgWith(1, null) }
  691. return (this.Docker.prototype.getContainer = sinon
  692. .stub()
  693. .returns(this.fakeContainer))
  694. })
  695. it('should get the container', function (done) {
  696. return this.DockerRunner._destroyContainer(
  697. this.containerId,
  698. false,
  699. err => {
  700. if (err) return done(err)
  701. this.Docker.prototype.getContainer.callCount.should.equal(1)
  702. this.Docker.prototype.getContainer
  703. .calledWith(this.containerId)
  704. .should.equal(true)
  705. return done()
  706. }
  707. )
  708. })
  709. it('should try to force-destroy the container when shouldForce=true', function (done) {
  710. return this.DockerRunner._destroyContainer(
  711. this.containerId,
  712. true,
  713. err => {
  714. if (err) return done(err)
  715. this.fakeContainer.remove.callCount.should.equal(1)
  716. this.fakeContainer.remove
  717. .calledWith({ force: true, v: true })
  718. .should.equal(true)
  719. return done()
  720. }
  721. )
  722. })
  723. it('should not try to force-destroy the container when shouldForce=false', function (done) {
  724. return this.DockerRunner._destroyContainer(
  725. this.containerId,
  726. false,
  727. err => {
  728. if (err) return done(err)
  729. this.fakeContainer.remove.callCount.should.equal(1)
  730. this.fakeContainer.remove
  731. .calledWith({ force: false, v: true })
  732. .should.equal(true)
  733. return done()
  734. }
  735. )
  736. })
  737. it('should not produce an error', function (done) {
  738. return this.DockerRunner._destroyContainer(
  739. this.containerId,
  740. false,
  741. err => {
  742. expect(err).to.equal(null)
  743. return done()
  744. }
  745. )
  746. })
  747. describe('when the container is already gone', function () {
  748. beforeEach(function () {
  749. this.fakeError = new Error('woops')
  750. this.fakeError.statusCode = 404
  751. this.fakeContainer = {
  752. remove: sinon.stub().callsArgWith(1, this.fakeError),
  753. }
  754. return (this.Docker.prototype.getContainer = sinon
  755. .stub()
  756. .returns(this.fakeContainer))
  757. })
  758. return it('should not produce an error', function (done) {
  759. return this.DockerRunner._destroyContainer(
  760. this.containerId,
  761. false,
  762. err => {
  763. expect(err).to.equal(null)
  764. return done()
  765. }
  766. )
  767. })
  768. })
  769. return describe('when container.destroy produces an error', function (done) {
  770. beforeEach(function () {
  771. this.fakeError = new Error('woops')
  772. this.fakeError.statusCode = 500
  773. this.fakeContainer = {
  774. remove: sinon.stub().callsArgWith(1, this.fakeError),
  775. }
  776. return (this.Docker.prototype.getContainer = sinon
  777. .stub()
  778. .returns(this.fakeContainer))
  779. })
  780. return it('should produce an error', function (done) {
  781. return this.DockerRunner._destroyContainer(
  782. this.containerId,
  783. false,
  784. err => {
  785. expect(err).to.not.equal(null)
  786. expect(err).to.equal(this.fakeError)
  787. return done()
  788. }
  789. )
  790. })
  791. })
  792. })
  793. return describe('kill', function () {
  794. beforeEach(function () {
  795. this.containerId = 'some_id'
  796. this.fakeContainer = { kill: sinon.stub().callsArgWith(0, null) }
  797. return (this.Docker.prototype.getContainer = sinon
  798. .stub()
  799. .returns(this.fakeContainer))
  800. })
  801. it('should get the container', function (done) {
  802. return this.DockerRunner.kill(this.containerId, err => {
  803. if (err) return done(err)
  804. this.Docker.prototype.getContainer.callCount.should.equal(1)
  805. this.Docker.prototype.getContainer
  806. .calledWith(this.containerId)
  807. .should.equal(true)
  808. return done()
  809. })
  810. })
  811. it('should try to force-destroy the container', function (done) {
  812. return this.DockerRunner.kill(this.containerId, err => {
  813. if (err) return done(err)
  814. this.fakeContainer.kill.callCount.should.equal(1)
  815. return done()
  816. })
  817. })
  818. it('should not produce an error', function (done) {
  819. return this.DockerRunner.kill(this.containerId, err => {
  820. expect(err).to.equal(undefined)
  821. return done()
  822. })
  823. })
  824. describe('when the container is not actually running', function () {
  825. beforeEach(function () {
  826. this.fakeError = new Error('woops')
  827. this.fakeError.statusCode = 500
  828. this.fakeError.message =
  829. 'Cannot kill container <whatever> is not running'
  830. this.fakeContainer = {
  831. kill: sinon.stub().callsArgWith(0, this.fakeError),
  832. }
  833. return (this.Docker.prototype.getContainer = sinon
  834. .stub()
  835. .returns(this.fakeContainer))
  836. })
  837. return it('should not produce an error', function (done) {
  838. return this.DockerRunner.kill(this.containerId, err => {
  839. expect(err).to.equal(undefined)
  840. return done()
  841. })
  842. })
  843. })
  844. return describe('when container.kill produces a legitimate error', function (done) {
  845. beforeEach(function () {
  846. this.fakeError = new Error('woops')
  847. this.fakeError.statusCode = 500
  848. this.fakeError.message = 'Totally legitimate reason to throw an error'
  849. this.fakeContainer = {
  850. kill: sinon.stub().callsArgWith(0, this.fakeError),
  851. }
  852. return (this.Docker.prototype.getContainer = sinon
  853. .stub()
  854. .returns(this.fakeContainer))
  855. })
  856. return it('should produce an error', function (done) {
  857. return this.DockerRunner.kill(this.containerId, err => {
  858. expect(err).to.not.equal(undefined)
  859. expect(err).to.equal(this.fakeError)
  860. return done()
  861. })
  862. })
  863. })
  864. })
  865. })