Sfoglia il codice sorgente

[web] CE script to verify TexLive versions on startup (#19653)

* [web] CE script to verify TexLive versions on startup

---------

Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: b99001d38468a775991a7284611aa333e956b919
Miguel Serrano 2 anni fa
parent
commit
0346ba2698

+ 6 - 0
server-ce/init_scripts/910_check_texlive_images

@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e
+
+echo "Checking texlive images"
+cd /overleaf/services/web
+node modules/server-ce-scripts/scripts/check-texlive-images

+ 18 - 0
services/web/migrations/20240730155209_create_project_imageName_index.js

@@ -0,0 +1,18 @@
+const Helpers = require('./lib/helpers')
+
+exports.tags = ['server-ce', 'server-pro']
+
+const index = {
+  key: { imageName: 1 },
+  name: 'imageName_1',
+}
+
+exports.migrate = async client => {
+  const { db } = client
+  await Helpers.addIndexesToCollection(db.projects, [index])
+}
+
+exports.rollback = async client => {
+  const { db } = client
+  await Helpers.dropIndexesFromCollection(db.projects, [index])
+}

+ 94 - 0
services/web/modules/server-ce-scripts/scripts/check-texlive-images.js

@@ -0,0 +1,94 @@
+const { waitForDb, db } = require('../../../app/src/infrastructure/mongodb')
+
+async function readImagesInUse() {
+  await waitForDb()
+  const projectCount = await db.projects.countDocuments()
+  if (projectCount === 0) {
+    return []
+  }
+  const images = await db.projects.distinct('imageName')
+
+  if (!images || images.length === 0 || images.includes(null)) {
+    console.error(`'project.imageName' is not set for some projects`)
+    console.error(
+      `Set SKIP_TEX_LIVE_CHECK=true in config/variables.env, restart the instance and run 'bin/run-script scripts/backfill_project_image_name.js' to initialise TexLive image in existing projects.`
+    )
+    console.error(
+      `After running the script, remove SKIP_TEX_LIVE_CHECK from config/variables.env and restart the instance.`
+    )
+    process.exit(1)
+  }
+  return images
+}
+
+function checkSandboxedCompilesAreEnabled() {
+  if (process.env.SANDBOXED_COMPILES !== 'true') {
+    console.log('Sandboxed compiles disabled, skipping TexLive checks')
+    process.exit(0)
+  }
+}
+
+function checkTexLiveEnvVariablesAreProvided() {
+  if (
+    !process.env.TEX_LIVE_DOCKER_IMAGE ||
+    !process.env.ALL_TEX_LIVE_DOCKER_IMAGES
+  ) {
+    console.error(
+      'Sandboxed compiles require TEX_LIVE_DOCKER_IMAGE and ALL_TEX_LIVE_DOCKER_IMAGES being set.'
+    )
+    process.exit(1)
+  }
+}
+
+async function main() {
+  if (process.env.SKIP_TEX_LIVE_CHECK === 'true') {
+    console.log(`SKIP_TEX_LIVE_CHECK=true, skipping TexLive images check`)
+    process.exit(0)
+  }
+
+  checkSandboxedCompilesAreEnabled()
+  checkTexLiveEnvVariablesAreProvided()
+
+  const allTexLiveImages = process.env.ALL_TEX_LIVE_DOCKER_IMAGES.split(',')
+
+  if (!allTexLiveImages.includes(process.env.TEX_LIVE_DOCKER_IMAGE)) {
+    console.error(
+      `TEX_LIVE_DOCKER_IMAGE must be included in ALL_TEX_LIVE_DOCKER_IMAGES`
+    )
+    process.exit(1)
+  }
+
+  const currentImages = await readImagesInUse()
+
+  const danglingImages = []
+  for (const image of currentImages) {
+    if (!allTexLiveImages.includes(image)) {
+      danglingImages.push(image)
+    }
+  }
+  if (danglingImages.length > 0) {
+    danglingImages.forEach(image =>
+      console.error(
+        `${image} is currently in use but it's not included in ALL_TEX_LIVE_DOCKER_IMAGES`
+      )
+    )
+    console.error(
+      `Set SKIP_TEX_LIVE_CHECK=true in config/variables.env, restart the instance and run 'bin/run-script scripts/update_project_image_name.js <dangling_image> <new_image>' to update projects to a new image.`
+    )
+    console.error(
+      `After running the script, remove SKIP_TEX_LIVE_CHECK from config/variables.env and restart the instance.`
+    )
+    process.exit(1)
+  }
+
+  console.log('Done.')
+}
+
+main()
+  .then(() => {
+    process.exit(0)
+  })
+  .catch(err => {
+    console.error(err)
+    process.exit(1)
+  })

+ 13 - 0
services/web/modules/server-ce-scripts/test/acceptance/src/Init.js

@@ -1 +1,14 @@
 require('../../../../../test/acceptance/src/helpers/InitApp')
+const MockProjectHistoryApi = require('../../../../../test/acceptance/src/mocks/MockProjectHistoryApi')
+const MockDocstoreApi = require('../../../../../test/acceptance/src/mocks/MockDocstoreApi')
+const MockDocUpdaterApi = require('../../../../../test/acceptance/src/mocks/MockDocUpdaterApi')
+const MockV1Api = require('../../../../admin-panel/test/acceptance/src/mocks/MockV1Api')
+
+const mockOpts = {
+  debug: ['1', 'true', 'TRUE'].includes(process.env.DEBUG_MOCKS),
+}
+
+MockDocstoreApi.initialize(23016, mockOpts)
+MockDocUpdaterApi.initialize(23003, mockOpts)
+MockProjectHistoryApi.initialize(23054, mockOpts)
+MockV1Api.initialize(25000, mockOpts)

+ 184 - 2
services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js

@@ -1,9 +1,9 @@
-const Settings = require('@overleaf/settings')
 const { execSync } = require('child_process')
+const fs = require('fs')
+const Settings = require('@overleaf/settings')
 const { expect } = require('chai')
 const { db } = require('../../../../../app/src/infrastructure/mongodb')
 const User = require('../../../../../test/acceptance/src/helpers/User').promises
-const fs = require('fs')
 
 /**
  * @param {string} cmd
@@ -20,6 +20,21 @@ function run(cmd) {
   }).toString()
 }
 
+function runAndExpectError(cmd, errorMessages) {
+  try {
+    run(cmd)
+  } catch (error) {
+    expect(error.status).to.equal(1)
+    if (errorMessages) {
+      errorMessages.forEach(errorMessage =>
+        expect(error.stderr.toString()).to.include(errorMessage)
+      )
+    }
+    return
+  }
+  expect.fail('command should have failed')
+}
+
 async function getUser(email) {
   return db.users.findOne({ email }, { projection: { _id: 0, isAdmin: 1 } })
 }
@@ -497,4 +512,171 @@ describe('ServerCEScripts', function () {
       })
     })
   })
+
+  describe('check-texlive-images', function () {
+    const TEST_TL_IMAGE = 'sharelatex/texlive:2023'
+    const TEST_TL_IMAGE_LIST =
+      'sharelatex/texlive:2021,sharelatex/texlive:2022,sharelatex/texlive:2023'
+
+    let output
+
+    function buildCheckTexLiveCmd({
+      SANDBOXED_COMPILES,
+      TEX_LIVE_DOCKER_IMAGE,
+      ALL_TEX_LIVE_DOCKER_IMAGES,
+    }) {
+      let cmd = `SANDBOXED_COMPILES=${SANDBOXED_COMPILES ? 'true' : 'false'}`
+      if (TEX_LIVE_DOCKER_IMAGE) {
+        cmd += ` TEX_LIVE_DOCKER_IMAGE='${TEX_LIVE_DOCKER_IMAGE}'`
+      }
+      if (ALL_TEX_LIVE_DOCKER_IMAGES) {
+        cmd += ` ALL_TEX_LIVE_DOCKER_IMAGES='${ALL_TEX_LIVE_DOCKER_IMAGES}'`
+      }
+      return (
+        cmd + ' node modules/server-ce-scripts/scripts/check-texlive-images'
+      )
+    }
+
+    beforeEach(async function () {
+      const user = new User()
+      await user.ensureUserExists()
+      await user.login()
+      await user.createProject('test-project')
+    })
+
+    describe('when sandboxed compiles are disabled', function () {
+      beforeEach('run script', function () {
+        output = run(buildCheckTexLiveCmd({ SANDBOXED_COMPILES: false }))
+      })
+
+      it('should skip checks', function () {
+        expect(output).to.include(
+          'Sandboxed compiles disabled, skipping TexLive checks'
+        )
+      })
+    })
+
+    describe('when texlive configuration is incorrect', function () {
+      it('should fail when TEX_LIVE_DOCKER_IMAGE is not set', function () {
+        runAndExpectError(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            ALL_TEX_LIVE_DOCKER_IMAGES: TEST_TL_IMAGE_LIST,
+          }),
+          [
+            'Sandboxed compiles require TEX_LIVE_DOCKER_IMAGE and ALL_TEX_LIVE_DOCKER_IMAGES being set',
+          ]
+        )
+      })
+
+      it('should fail when ALL_TEX_LIVE_DOCKER_IMAGES is not set', function () {
+        runAndExpectError(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
+          }),
+          [
+            'Sandboxed compiles require TEX_LIVE_DOCKER_IMAGE and ALL_TEX_LIVE_DOCKER_IMAGES being set',
+          ]
+        )
+      })
+
+      it('should fail when TEX_LIVE_DOCKER_IMAGE is not defined in ALL_TEX_LIVE_DOCKER_IMAGES', function () {
+        runAndExpectError(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: 'tl-1',
+            ALL_TEX_LIVE_DOCKER_IMAGES: 'tl-2,tl-3',
+          }),
+          [
+            'TEX_LIVE_DOCKER_IMAGE must be included in ALL_TEX_LIVE_DOCKER_IMAGES',
+          ]
+        )
+      })
+    })
+
+    describe(`when projects don't have 'imageName' set`, function () {
+      beforeEach(async function () {
+        await db.projects.updateMany({}, { $unset: { imageName: 1 } })
+      })
+
+      it('should fail and suggest running backfilling scripts', function () {
+        runAndExpectError(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
+            ALL_TEX_LIVE_DOCKER_IMAGES: TEST_TL_IMAGE_LIST,
+          }),
+          [
+            `'project.imageName' is not set for some projects`,
+            `Set SKIP_TEX_LIVE_CHECK=true in config/variables.env, restart the instance and run 'bin/run-script scripts/backfill_project_image_name.js' to initialise TexLive image in existing projects`,
+          ]
+        )
+      })
+    })
+
+    describe(`when projects have a null 'imageName'`, function () {
+      beforeEach(async function () {
+        await db.projects.updateMany({}, { $set: { imageName: null } })
+      })
+
+      it('should fail and suggest running backfilling scripts', function () {
+        runAndExpectError(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
+            ALL_TEX_LIVE_DOCKER_IMAGES: TEST_TL_IMAGE_LIST,
+          }),
+          [
+            `'project.imageName' is not set for some projects`,
+            `Set SKIP_TEX_LIVE_CHECK=true in config/variables.env, restart the instance and run 'bin/run-script scripts/backfill_project_image_name.js' to initialise TexLive image in existing projects`,
+          ]
+        )
+      })
+    })
+
+    describe('when TexLive ALL_TEX_LIVE_DOCKER_IMAGES are upgraded and used images are no longer available', function () {
+      it('should suggest running a fixing script', async function () {
+        await db.projects.updateMany({}, { $set: { imageName: TEST_TL_IMAGE } })
+        runAndExpectError(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: 'tl-1',
+            ALL_TEX_LIVE_DOCKER_IMAGES: 'tl-1,tl-2',
+          }),
+          [
+            `Set SKIP_TEX_LIVE_CHECK=true in config/variables.env, restart the instance and run 'bin/run-script scripts/update_project_image_name.js <dangling_image> <new_image>' to update projects to a new image`,
+          ]
+        )
+      })
+    })
+
+    describe('success scenarios', function () {
+      beforeEach(async function () {
+        await db.projects.updateMany({}, { $set: { imageName: TEST_TL_IMAGE } })
+      })
+
+      it('should succeed when there are no changes to the TexLive images', function () {
+        const output = run(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: TEST_TL_IMAGE,
+            ALL_TEX_LIVE_DOCKER_IMAGES: TEST_TL_IMAGE_LIST,
+          })
+        )
+        expect(output).to.include('Done.')
+      })
+
+      it('should succeed when there are valid changes to the TexLive images', function () {
+        const output = run(
+          buildCheckTexLiveCmd({
+            SANDBOXED_COMPILES: true,
+            TEX_LIVE_DOCKER_IMAGE: 'new-image',
+            ALL_TEX_LIVE_DOCKER_IMAGES: TEST_TL_IMAGE_LIST + ',new-image',
+          })
+        )
+        expect(output).to.include('Done.')
+      })
+    })
+  })
 })

+ 39 - 1
services/web/scripts/backfill_project_image_name.js

@@ -3,6 +3,44 @@ const { batchedUpdateWithResultHandling } = require('./helpers/batchedUpdate')
 
 const argv = minimist(process.argv.slice(2))
 const commit = argv.commit !== undefined
+let imageName = argv._[0]
+
+function usage() {
+  console.log(
+    'Usage: node backfill_project_image_name.js --commit <texlive_docker_image>'
+  )
+  console.log(
+    'Argument <texlive_docker_image> is not required when TEX_LIVE_DOCKER_IMAGE is set.'
+  )
+  console.log(
+    'Environment variable ALL_TEX_LIVE_DOCKER_IMAGES must contain <texlive_docker_image>.'
+  )
+}
+
+if (!imageName && process.env.TEX_LIVE_DOCKER_IMAGE) {
+  imageName = process.env.TEX_LIVE_DOCKER_IMAGE
+}
+
+if (!imageName) {
+  usage()
+  process.exit(1)
+}
+
+if (!process.env.ALL_TEX_LIVE_DOCKER_IMAGES) {
+  console.error(
+    'Error: environment variable ALL_TEX_LIVE_DOCKER_IMAGES is not defined.'
+  )
+  usage()
+  process.exit(1)
+}
+
+if (!process.env.ALL_TEX_LIVE_DOCKER_IMAGES.split(',').includes(imageName)) {
+  console.error(
+    `Error: ALL_TEX_LIVE_DOCKER_IMAGES doesn't contain ${imageName}`
+  )
+  usage()
+  process.exit(1)
+}
 
 if (!commit) {
   console.error('DOING DRY RUN. TO SAVE CHANGES PASS --commit')
@@ -12,5 +50,5 @@ if (!commit) {
 batchedUpdateWithResultHandling(
   'projects',
   { imageName: null },
-  { $set: { imageName: 'quay.io/sharelatex/texlive-full:2014.2' } }
+  { $set: { imageName } }
 )

+ 46 - 0
services/web/scripts/update_project_image_name.js

@@ -0,0 +1,46 @@
+const { batchedUpdate } = require('./helpers/batchedUpdate')
+
+const oldImage = process.argv[2]
+const newImage = process.argv[3]
+
+function usage() {
+  console.log(
+    `Usage: update_project_image_name.js <old_texlive_image> <new_texlive_image>`
+  )
+  console.log(
+    'Environment variable ALL_TEX_LIVE_DOCKER_IMAGES must contain <new_texlive_image>.'
+  )
+}
+
+if (!oldImage || !newImage) {
+  usage()
+  process.exit(1)
+}
+
+if (!process.env.ALL_TEX_LIVE_DOCKER_IMAGES) {
+  console.error(
+    'Error: environment variable ALL_TEX_LIVE_DOCKER_IMAGES is not defined.'
+  )
+  usage()
+  process.exit(1)
+}
+
+if (!process.env.ALL_TEX_LIVE_DOCKER_IMAGES.split(',').includes(newImage)) {
+  console.error(`Error: ALL_TEX_LIVE_DOCKER_IMAGES doesn't contain ${newImage}`)
+  usage()
+  process.exit(1)
+}
+
+batchedUpdate(
+  'projects',
+  { imageName: oldImage },
+  { $set: { imageName: newImage } }
+)
+  .then(() => {
+    console.log('Done')
+    process.exit(0)
+  })
+  .catch(error => {
+    console.error(error)
+    process.exit(1)
+  })