mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
[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
This commit is contained in:
parent
b4ad1ed35d
commit
0346ba2698
7 changed files with 400 additions and 3 deletions
6
server-ce/init_scripts/910_check_texlive_images
Executable file
6
server-ce/init_scripts/910_check_texlive_images
Executable file
|
@ -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
|
|
@ -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])
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
|
@ -1 +1,14 @@
|
||||||
require('../../../../../test/acceptance/src/helpers/InitApp')
|
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)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const Settings = require('@overleaf/settings')
|
|
||||||
const { execSync } = require('child_process')
|
const { execSync } = require('child_process')
|
||||||
|
const fs = require('fs')
|
||||||
|
const Settings = require('@overleaf/settings')
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const { db } = require('../../../../../app/src/infrastructure/mongodb')
|
const { db } = require('../../../../../app/src/infrastructure/mongodb')
|
||||||
const User = require('../../../../../test/acceptance/src/helpers/User').promises
|
const User = require('../../../../../test/acceptance/src/helpers/User').promises
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} cmd
|
* @param {string} cmd
|
||||||
|
@ -20,6 +20,21 @@ function run(cmd) {
|
||||||
}).toString()
|
}).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) {
|
async function getUser(email) {
|
||||||
return db.users.findOne({ email }, { projection: { _id: 0, isAdmin: 1 } })
|
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.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,44 @@ const { batchedUpdateWithResultHandling } = require('./helpers/batchedUpdate')
|
||||||
|
|
||||||
const argv = minimist(process.argv.slice(2))
|
const argv = minimist(process.argv.slice(2))
|
||||||
const commit = argv.commit !== undefined
|
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) {
|
if (!commit) {
|
||||||
console.error('DOING DRY RUN. TO SAVE CHANGES PASS --commit')
|
console.error('DOING DRY RUN. TO SAVE CHANGES PASS --commit')
|
||||||
|
@ -12,5 +50,5 @@ if (!commit) {
|
||||||
batchedUpdateWithResultHandling(
|
batchedUpdateWithResultHandling(
|
||||||
'projects',
|
'projects',
|
||||||
{ imageName: null },
|
{ imageName: null },
|
||||||
{ $set: { imageName: 'quay.io/sharelatex/texlive-full:2014.2' } }
|
{ $set: { imageName } }
|
||||||
)
|
)
|
||||||
|
|
46
services/web/scripts/update_project_image_name.js
Normal file
46
services/web/scripts/update_project_image_name.js
Normal file
|
@ -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)
|
||||||
|
})
|
Loading…
Reference in a new issue