host-admin.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. const fs = require('fs')
  2. const Path = require('path')
  3. const { execFile } = require('child_process')
  4. const express = require('express')
  5. const bodyParser = require('body-parser')
  6. const {
  7. celebrate: validate,
  8. Joi,
  9. errors: handleValidationErrors,
  10. } = require('celebrate')
  11. const YAML = require('js-yaml')
  12. const PATHS = {
  13. DOCKER_COMPOSE_OVERRIDE: 'docker-compose.override.yml',
  14. SANDBOXED_COMPILES_HOST_DIR: Path.join(__dirname, 'cypress/compiles'),
  15. }
  16. const IMAGES = {
  17. CE: process.env.IMAGE_TAG_CE.replace(/:.+/, ''),
  18. PRO: process.env.IMAGE_TAG_PRO.replace(/:.+/, ''),
  19. }
  20. let mongoIsInitialized = false
  21. function readDockerComposeOverride() {
  22. try {
  23. return YAML.load(fs.readFileSync(PATHS.DOCKER_COMPOSE_OVERRIDE, 'utf-8'))
  24. } catch (error) {
  25. if (error.code !== 'ENOENT') {
  26. throw error
  27. }
  28. return {
  29. services: {
  30. sharelatex: {
  31. environment: {},
  32. },
  33. 'git-bridge': {},
  34. },
  35. }
  36. }
  37. }
  38. function writeDockerComposeOverride(cfg) {
  39. fs.writeFileSync(PATHS.DOCKER_COMPOSE_OVERRIDE, YAML.dump(cfg))
  40. }
  41. const app = express()
  42. app.get('/status', (req, res) => {
  43. res.send('host-admin is up')
  44. })
  45. app.use(bodyParser.json())
  46. app.use((req, res, next) => {
  47. // Basic access logs
  48. console.log(req.method, req.url, req.body)
  49. // Add CORS headers
  50. res.setHeader('Access-Control-Allow-Origin', 'http://sharelatex')
  51. res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
  52. next()
  53. })
  54. app.post(
  55. '/run/script',
  56. validate(
  57. {
  58. body: {
  59. cwd: Joi.string().required(),
  60. script: Joi.string().required(),
  61. args: Joi.array().items(Joi.string()),
  62. },
  63. },
  64. { allowUnknown: false }
  65. ),
  66. (req, res) => {
  67. const { cwd, script, args } = req.body
  68. execFile(
  69. 'docker',
  70. [
  71. 'compose',
  72. 'exec',
  73. 'sharelatex',
  74. 'bash',
  75. '-c',
  76. `source /etc/container_environment.sh && source /etc/overleaf/env.sh && cd ${JSON.stringify(cwd)} && node ${JSON.stringify(script)} ${args.map(a => JSON.stringify(a)).join(' ')}`,
  77. ],
  78. (error, stdout, stderr) => {
  79. res.json({
  80. error,
  81. stdout,
  82. stderr,
  83. })
  84. }
  85. )
  86. }
  87. )
  88. function setVersionDockerCompose({ pro, version }) {
  89. const cfg = readDockerComposeOverride()
  90. cfg.services.sharelatex.image = `${pro ? IMAGES.PRO : IMAGES.CE}:${version}`
  91. cfg.services['git-bridge'].image = `quay.io/sharelatex/git-bridge:${version}`
  92. writeDockerComposeOverride(cfg)
  93. }
  94. app.post(
  95. '/set/version',
  96. validate(
  97. {
  98. body: {
  99. pro: Joi.boolean(),
  100. version: Joi.string().required(),
  101. path: Joi.allow(
  102. 'docker-compose.yml'
  103. // When extending testing for Toolkit:
  104. // 'config/version'
  105. ),
  106. },
  107. },
  108. { allowUnknown: false }
  109. ),
  110. (req, res) => {
  111. const { pro, version } = req.body
  112. if (req.body.path === 'docker-compose.yml') {
  113. try {
  114. setVersionDockerCompose({ pro, version })
  115. } catch (error) {
  116. return res.json({ error })
  117. }
  118. }
  119. res.json({})
  120. }
  121. )
  122. const allowedVars = Joi.object(
  123. Object.fromEntries(
  124. [
  125. 'OVERLEAF_APP_NAME',
  126. 'OVERLEAF_LEFT_FOOTER',
  127. 'OVERLEAF_RIGHT_FOOTER',
  128. 'OVERLEAF_PROXY_LEARN',
  129. 'GIT_BRIDGE_ENABLED',
  130. 'GIT_BRIDGE_HOST',
  131. 'GIT_BRIDGE_PORT',
  132. 'V1_HISTORY_URL',
  133. 'DOCKER_RUNNER',
  134. 'SANDBOXED_COMPILES',
  135. 'SANDBOXED_COMPILES_SIBLING_CONTAINERS',
  136. 'ALL_TEX_LIVE_DOCKER_IMAGE_NAMES',
  137. ].map(name => [name, Joi.string()])
  138. )
  139. )
  140. function setVarsDockerCompose({ vars }) {
  141. const cfg = readDockerComposeOverride()
  142. cfg.services.sharelatex.environment = vars
  143. if (cfg.services.sharelatex.environment.GIT_BRIDGE_ENABLED === 'true') {
  144. cfg.services.sharelatex.depends_on = ['git-bridge']
  145. }
  146. if (
  147. cfg.services.sharelatex.environment
  148. .SANDBOXED_COMPILES_SIBLING_CONTAINERS === 'true'
  149. ) {
  150. cfg.services.sharelatex.environment.SANDBOXED_COMPILES_HOST_DIR =
  151. PATHS.SANDBOXED_COMPILES_HOST_DIR
  152. cfg.services.sharelatex.environment.TEX_LIVE_DOCKER_IMAGE =
  153. process.env.TEX_LIVE_DOCKER_IMAGE
  154. cfg.services.sharelatex.environment.ALL_TEX_LIVE_DOCKER_IMAGES =
  155. process.env.ALL_TEX_LIVE_DOCKER_IMAGES
  156. cfg.services.sharelatex.volumes = [
  157. '/var/run/docker.sock:/var/run/docker.sock',
  158. `${PATHS.SANDBOXED_COMPILES_HOST_DIR}:/var/lib/overleaf/data/compiles`,
  159. ]
  160. } else {
  161. cfg.services.sharelatex.volumes = []
  162. }
  163. writeDockerComposeOverride(cfg)
  164. }
  165. app.post(
  166. '/set/vars',
  167. validate(
  168. {
  169. body: {
  170. vars: allowedVars,
  171. path: Joi.allow(
  172. 'docker-compose.yml'
  173. // When extending the testing for Toolkit:
  174. // 'overleaf.rc', 'variables.env'
  175. ),
  176. },
  177. },
  178. { allowUnknown: false }
  179. ),
  180. (req, res) => {
  181. if (req.body.path === 'docker-compose.yml') {
  182. const { vars } = req.body
  183. try {
  184. setVarsDockerCompose({ vars })
  185. } catch (error) {
  186. return res.json({ error })
  187. }
  188. }
  189. res.json({})
  190. }
  191. )
  192. app.post(
  193. '/docker/compose/:cmd',
  194. validate(
  195. {
  196. body: {
  197. args: Joi.array().allow(
  198. '--detach',
  199. '--wait',
  200. '--volumes',
  201. '--timeout',
  202. '0',
  203. 'sharelatex',
  204. 'git-bridge',
  205. 'mongo',
  206. 'redis'
  207. ),
  208. },
  209. params: {
  210. cmd: Joi.allow('up', 'stop', 'down', 'ps', 'logs'),
  211. },
  212. },
  213. { allowUnknown: false }
  214. ),
  215. (req, res) => {
  216. const { cmd } = req.params
  217. const { args } = req.body
  218. if (['stop', 'down'].includes(cmd)) {
  219. mongoIsInitialized = false
  220. }
  221. execFile('docker', ['compose', cmd, ...args], (error, stdout, stderr) => {
  222. res.json({ error, stdout, stderr })
  223. })
  224. }
  225. )
  226. function mongoInit(callback) {
  227. execFile(
  228. 'docker',
  229. [
  230. 'compose',
  231. 'exec',
  232. 'mongo',
  233. 'mongo',
  234. '--eval',
  235. 'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })',
  236. ],
  237. (error, stdout, stderr) => {
  238. if (!error) {
  239. mongoIsInitialized = true
  240. }
  241. callback(error, stdout, stderr)
  242. }
  243. )
  244. }
  245. app.post('/mongo/init', (req, res) => {
  246. mongoInit((error, stdout, stderr) => {
  247. res.json({ error, stdout, stderr })
  248. })
  249. })
  250. app.post(
  251. '/reconfigure',
  252. validate(
  253. {
  254. body: {
  255. pro: Joi.boolean().required(),
  256. version: Joi.string().required(),
  257. vars: allowedVars,
  258. },
  259. },
  260. { allowUnknown: false }
  261. ),
  262. (req, res) => {
  263. const doMongoInit = mongoIsInitialized ? cb => cb() : mongoInit
  264. doMongoInit((error, stdout, stderr) => {
  265. if (error) return res.json({ error, stdout, stderr })
  266. const { pro, version, vars } = req.body
  267. try {
  268. setVersionDockerCompose({ pro, version })
  269. setVarsDockerCompose({ vars })
  270. } catch (error) {
  271. return res.json({ error })
  272. }
  273. execFile(
  274. 'docker',
  275. ['compose', 'up', '--detach', '--wait', 'sharelatex'],
  276. (error, stdout, stderr) => {
  277. res.json({ error, stdout, stderr })
  278. }
  279. )
  280. })
  281. }
  282. )
  283. app.use(handleValidationErrors())
  284. // Init on startup
  285. mongoInit(err => {
  286. if (err) {
  287. console.error('mongo init failed', err)
  288. process.exit(1)
  289. }
  290. app.listen(80)
  291. })