DockerRunnerTests.js 30 KB

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