DockerRunnerTests.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  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(3, 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. {},
  613. this.callback
  614. )
  615. })
  616. it('should wait for the container', function () {
  617. this.getContainer.calledWith(this.containerId).should.equal(true)
  618. return this.container.wait.called.should.equal(true)
  619. })
  620. return it('should call the callback with the exit', function () {
  621. return this.callback
  622. .calledWith(null, this.statusCode)
  623. .should.equal(true)
  624. })
  625. })
  626. describe('when the container is removed before waiting', function () {
  627. const err = new Error('not found')
  628. err.statusCode = 404
  629. beforeEach(function () {
  630. this.container.wait = sinon.stub().yields(err)
  631. })
  632. describe('AutoRemove not set', function () {
  633. beforeEach(function () {
  634. this.DockerRunner.waitForContainer(
  635. this.containerId,
  636. this.timeout,
  637. { HostConfig: {} },
  638. this.callback
  639. )
  640. })
  641. it('should wait for the container', function () {
  642. this.getContainer.calledWith(this.containerId).should.equal(true)
  643. this.container.wait.called.should.equal(true)
  644. })
  645. it('should call the callback with the error', function () {
  646. this.callback.calledWith(err).should.equal(true)
  647. })
  648. })
  649. describe('AutoRemove=true', function () {
  650. beforeEach(function () {
  651. this.DockerRunner.waitForContainer(
  652. this.containerId,
  653. this.timeout,
  654. { HostConfig: { AutoRemove: true } },
  655. this.callback
  656. )
  657. })
  658. it('should wait for the container', function () {
  659. this.getContainer.calledWith(this.containerId).should.equal(true)
  660. this.container.wait.called.should.equal(true)
  661. })
  662. it('should call the callback with exit code 0', function () {
  663. this.callback.calledWith(null, 0).should.equal(true)
  664. })
  665. })
  666. })
  667. return describe('when the container does not return before the timeout', function () {
  668. beforeEach(function (done) {
  669. this.container.wait = function (callback) {
  670. if (callback == null) {
  671. callback = function () {}
  672. }
  673. return setTimeout(() => callback(null, { StatusCode: 42 }), 100)
  674. }
  675. this.timeout = 5
  676. return this.DockerRunner.waitForContainer(
  677. this.containerId,
  678. this.timeout,
  679. {},
  680. (...args) => {
  681. this.callback(...Array.from(args || []))
  682. return done()
  683. }
  684. )
  685. })
  686. it('should call kill on the container', function () {
  687. this.getContainer.calledWith(this.containerId).should.equal(true)
  688. return this.container.kill.called.should.equal(true)
  689. })
  690. it('should call the callback with an error', function () {
  691. this.callback.calledWith(sinon.match(Error)).should.equal(true)
  692. const errorObj = this.callback.args[0][0]
  693. expect(errorObj.message).to.include('container timed out')
  694. expect(errorObj.timedout).equal(true)
  695. })
  696. })
  697. })
  698. describe('destroyOldContainers', function () {
  699. beforeEach(function (done) {
  700. const oneHourInSeconds = 60 * 60
  701. const oneHourInMilliseconds = oneHourInSeconds * 1000
  702. const nowInSeconds = Date.now() / 1000
  703. this.containers = [
  704. {
  705. Name: '/project-old-container-name',
  706. Id: 'old-container-id',
  707. Created: nowInSeconds - oneHourInSeconds - 100,
  708. },
  709. {
  710. Name: '/project-new-container-name',
  711. Id: 'new-container-id',
  712. Created: nowInSeconds - oneHourInSeconds + 100,
  713. },
  714. {
  715. Name: '/totally-not-a-project-container',
  716. Id: 'some-random-id',
  717. Created: nowInSeconds - 2 * oneHourInSeconds,
  718. },
  719. ]
  720. this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds
  721. this.listContainers.callsArgWith(1, null, this.containers)
  722. this.DockerRunner.destroyContainer = sinon.stub().callsArg(3)
  723. return this.DockerRunner.destroyOldContainers(error => {
  724. this.callback(error)
  725. return done()
  726. })
  727. })
  728. it('should list all containers', function () {
  729. return this.listContainers.calledWith({ all: true }).should.equal(true)
  730. })
  731. it('should destroy old containers', function () {
  732. this.DockerRunner.destroyContainer.callCount.should.equal(1)
  733. return this.DockerRunner.destroyContainer
  734. .calledWith('project-old-container-name', 'old-container-id')
  735. .should.equal(true)
  736. })
  737. it('should not destroy new containers', function () {
  738. return this.DockerRunner.destroyContainer
  739. .calledWith('project-new-container-name', 'new-container-id')
  740. .should.equal(false)
  741. })
  742. it('should not destroy non-project containers', function () {
  743. return this.DockerRunner.destroyContainer
  744. .calledWith('totally-not-a-project-container', 'some-random-id')
  745. .should.equal(false)
  746. })
  747. return it('should callback the callback', function () {
  748. return this.callback.called.should.equal(true)
  749. })
  750. })
  751. describe('_destroyContainer', function () {
  752. beforeEach(function () {
  753. this.containerId = 'some_id'
  754. this.fakeContainer = { remove: sinon.stub().callsArgWith(1, null) }
  755. return (this.Docker.prototype.getContainer = sinon
  756. .stub()
  757. .returns(this.fakeContainer))
  758. })
  759. it('should get the container', function (done) {
  760. return this.DockerRunner._destroyContainer(
  761. this.containerId,
  762. false,
  763. err => {
  764. if (err) return done(err)
  765. this.Docker.prototype.getContainer.callCount.should.equal(1)
  766. this.Docker.prototype.getContainer
  767. .calledWith(this.containerId)
  768. .should.equal(true)
  769. return done()
  770. }
  771. )
  772. })
  773. it('should try to force-destroy the container when shouldForce=true', function (done) {
  774. return this.DockerRunner._destroyContainer(
  775. this.containerId,
  776. true,
  777. err => {
  778. if (err) return done(err)
  779. this.fakeContainer.remove.callCount.should.equal(1)
  780. this.fakeContainer.remove
  781. .calledWith({ force: true, v: true })
  782. .should.equal(true)
  783. return done()
  784. }
  785. )
  786. })
  787. it('should not try to force-destroy the container when shouldForce=false', function (done) {
  788. return this.DockerRunner._destroyContainer(
  789. this.containerId,
  790. false,
  791. err => {
  792. if (err) return done(err)
  793. this.fakeContainer.remove.callCount.should.equal(1)
  794. this.fakeContainer.remove
  795. .calledWith({ force: false, v: true })
  796. .should.equal(true)
  797. return done()
  798. }
  799. )
  800. })
  801. it('should not produce an error', function (done) {
  802. return this.DockerRunner._destroyContainer(
  803. this.containerId,
  804. false,
  805. err => {
  806. expect(err).to.equal(null)
  807. return done()
  808. }
  809. )
  810. })
  811. describe('when the container is already gone', function () {
  812. beforeEach(function () {
  813. this.fakeError = new Error('woops')
  814. this.fakeError.statusCode = 404
  815. this.fakeContainer = {
  816. remove: sinon.stub().callsArgWith(1, this.fakeError),
  817. }
  818. return (this.Docker.prototype.getContainer = sinon
  819. .stub()
  820. .returns(this.fakeContainer))
  821. })
  822. return it('should not produce an error', function (done) {
  823. return this.DockerRunner._destroyContainer(
  824. this.containerId,
  825. false,
  826. err => {
  827. expect(err).to.equal(null)
  828. return done()
  829. }
  830. )
  831. })
  832. })
  833. return describe('when container.destroy produces an error', function (done) {
  834. beforeEach(function () {
  835. this.fakeError = new Error('woops')
  836. this.fakeError.statusCode = 500
  837. this.fakeContainer = {
  838. remove: sinon.stub().callsArgWith(1, this.fakeError),
  839. }
  840. return (this.Docker.prototype.getContainer = sinon
  841. .stub()
  842. .returns(this.fakeContainer))
  843. })
  844. return it('should produce an error', function (done) {
  845. return this.DockerRunner._destroyContainer(
  846. this.containerId,
  847. false,
  848. err => {
  849. expect(err).to.not.equal(null)
  850. expect(err).to.equal(this.fakeError)
  851. return done()
  852. }
  853. )
  854. })
  855. })
  856. })
  857. return describe('kill', function () {
  858. beforeEach(function () {
  859. this.containerId = 'some_id'
  860. this.fakeContainer = { kill: sinon.stub().callsArgWith(0, null) }
  861. return (this.Docker.prototype.getContainer = sinon
  862. .stub()
  863. .returns(this.fakeContainer))
  864. })
  865. it('should get the container', function (done) {
  866. return this.DockerRunner.kill(this.containerId, err => {
  867. if (err) return done(err)
  868. this.Docker.prototype.getContainer.callCount.should.equal(1)
  869. this.Docker.prototype.getContainer
  870. .calledWith(this.containerId)
  871. .should.equal(true)
  872. return done()
  873. })
  874. })
  875. it('should try to force-destroy the container', function (done) {
  876. return this.DockerRunner.kill(this.containerId, err => {
  877. if (err) return done(err)
  878. this.fakeContainer.kill.callCount.should.equal(1)
  879. return done()
  880. })
  881. })
  882. it('should not produce an error', function (done) {
  883. return this.DockerRunner.kill(this.containerId, err => {
  884. expect(err).to.equal(undefined)
  885. return done()
  886. })
  887. })
  888. describe('when the container is not actually running', function () {
  889. beforeEach(function () {
  890. this.fakeError = new Error('woops')
  891. this.fakeError.statusCode = 500
  892. this.fakeError.message =
  893. 'Cannot kill container <whatever> is not running'
  894. this.fakeContainer = {
  895. kill: sinon.stub().callsArgWith(0, this.fakeError),
  896. }
  897. return (this.Docker.prototype.getContainer = sinon
  898. .stub()
  899. .returns(this.fakeContainer))
  900. })
  901. return it('should not produce an error', function (done) {
  902. return this.DockerRunner.kill(this.containerId, err => {
  903. expect(err).to.equal(undefined)
  904. return done()
  905. })
  906. })
  907. })
  908. return describe('when container.kill produces a legitimate error', function (done) {
  909. beforeEach(function () {
  910. this.fakeError = new Error('woops')
  911. this.fakeError.statusCode = 500
  912. this.fakeError.message = 'Totally legitimate reason to throw an error'
  913. this.fakeContainer = {
  914. kill: sinon.stub().callsArgWith(0, this.fakeError),
  915. }
  916. return (this.Docker.prototype.getContainer = sinon
  917. .stub()
  918. .returns(this.fakeContainer))
  919. })
  920. return it('should produce an error', function (done) {
  921. return this.DockerRunner.kill(this.containerId, err => {
  922. expect(err).to.not.equal(undefined)
  923. expect(err).to.equal(this.fakeError)
  924. return done()
  925. })
  926. })
  927. })
  928. })
  929. })