DockerRunnerTests.js 30 KB

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