dockerNode.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /**
  2. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: AGPL-3.0-or-later
  4. */
  5. /* eslint-disable no-console */
  6. /* eslint-disable n/no-unpublished-import */
  7. /* eslint-disable n/no-extraneous-import */
  8. import Docker from 'dockerode'
  9. import waitOn from 'wait-on'
  10. import tar from 'tar'
  11. import path from 'path'
  12. import { execSync } from 'child_process'
  13. import { existsSync } from 'fs'
  14. export const docker = new Docker()
  15. const CONTAINER_NAME = 'nextcloud-cypress-tests-server'
  16. const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
  17. /**
  18. * Start the testing container
  19. *
  20. * @param {string} branch the branch of your current work
  21. */
  22. export const startNextcloud = async function(branch: string = getCurrentGitBranch()): Promise<any> {
  23. try {
  24. try {
  25. // Pulling images
  26. console.log('\nPulling images... ⏳')
  27. await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => {
  28. if (err) {
  29. reject(err)
  30. }
  31. if (stream === null) {
  32. reject(new Error('Could not connect to docker, ensure docker is running.'))
  33. return
  34. }
  35. // https://github.com/apocas/dockerode/issues/357
  36. docker.modem.followProgress(stream, onFinished)
  37. /**
  38. *
  39. * @param err
  40. */
  41. function onFinished(err) {
  42. if (!err) {
  43. resolve(true)
  44. return
  45. }
  46. reject(err)
  47. }
  48. }))
  49. console.log('└─ Done')
  50. } catch (e) {
  51. console.log('└─ Failed to pull images')
  52. throw e
  53. }
  54. // Remove old container if exists
  55. console.log('\nChecking running containers... 🔍')
  56. try {
  57. const oldContainer = docker.getContainer(CONTAINER_NAME)
  58. const oldContainerData = await oldContainer.inspect()
  59. if (oldContainerData) {
  60. console.log('├─ Existing running container found')
  61. console.log('├─ Removing... ⏳')
  62. // Forcing any remnants to be removed just in case
  63. await oldContainer.remove({ force: true })
  64. console.log('└─ Done')
  65. }
  66. } catch (error) {
  67. console.log('└─ None found!')
  68. }
  69. // Starting container
  70. console.log('\nStarting Nextcloud container... 🚀')
  71. console.log(`├─ Using branch '${branch}'`)
  72. const container = await docker.createContainer({
  73. Image: SERVER_IMAGE,
  74. name: CONTAINER_NAME,
  75. HostConfig: {
  76. Binds: [],
  77. },
  78. Env: [
  79. `BRANCH=${branch}`,
  80. ],
  81. })
  82. await container.start()
  83. // Get container's IP
  84. const ip = await getContainerIP(container)
  85. console.log(`├─ Nextcloud container's IP is ${ip} 🌏`)
  86. return ip
  87. } catch (err) {
  88. console.log('└─ Unable to start the container 🛑')
  89. console.log('\n', err, '\n')
  90. stopNextcloud()
  91. throw new Error('Unable to start the container')
  92. }
  93. }
  94. /**
  95. * Configure Nextcloud
  96. */
  97. export const configureNextcloud = async function() {
  98. console.log('\nConfiguring nextcloud...')
  99. const container = docker.getContainer(CONTAINER_NAME)
  100. await runExec(container, ['php', 'occ', '--version'], true)
  101. // Be consistent for screenshots
  102. await runExec(container, ['php', 'occ', 'config:system:set', 'default_language', '--value', 'en'], true)
  103. await runExec(container, ['php', 'occ', 'config:system:set', 'force_language', '--value', 'en'], true)
  104. await runExec(container, ['php', 'occ', 'config:system:set', 'default_locale', '--value', 'en_US'], true)
  105. await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
  106. await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
  107. // Speed up test and make them less flaky. If a cron execution is needed, it can be triggered manually.
  108. await runExec(container, ['php', 'occ', 'background:cron'], true)
  109. console.log('└─ Nextcloud is now ready to use 🎉')
  110. }
  111. /**
  112. * Applying local changes to the container
  113. * Only triggered if we're not in CI. Otherwise the
  114. * continuous-integration-shallow-server image will
  115. * already fetch the proper branch.
  116. */
  117. export const applyChangesToNextcloud = async function() {
  118. console.log('\nApply local changes to nextcloud...')
  119. const htmlPath = '/var/www/html'
  120. const folderPaths = [
  121. './3rdparty',
  122. './apps',
  123. './core',
  124. './dist',
  125. './lib',
  126. './ocs',
  127. './ocs-provider',
  128. './resources',
  129. './console.php',
  130. './cron.php',
  131. './index.php',
  132. './occ',
  133. './public.php',
  134. './remote.php',
  135. './status.php',
  136. './version.php',
  137. ]
  138. let needToApplyChanges = false
  139. folderPaths.forEach((folderPath) => {
  140. const fullPath = path.join(htmlPath, folderPath)
  141. if (existsSync(fullPath)) {
  142. needToApplyChanges = true
  143. console.log(`├─ Copying ${folderPath}`)
  144. }
  145. })
  146. // Don't try to apply changes, when there are none. Otherwise we
  147. // still execute the 'chown' command, which is not needed.
  148. if (!needToApplyChanges) {
  149. console.log('└─ No local changes found to apply')
  150. return
  151. }
  152. const container = docker.getContainer(CONTAINER_NAME)
  153. // Tar-streaming the above folders into the container
  154. const serverTar = tar.c({ gzip: false }, folderPaths)
  155. await container.putArchive(serverTar, {
  156. path: htmlPath,
  157. })
  158. // Making sure we have the proper permissions
  159. await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
  160. console.log('└─ Changes applied successfully 🎉')
  161. }
  162. /**
  163. * Force stop the testing container
  164. */
  165. export const stopNextcloud = async function() {
  166. try {
  167. const container = docker.getContainer(CONTAINER_NAME)
  168. console.log('Stopping Nextcloud container...')
  169. container.remove({ force: true })
  170. console.log('└─ Nextcloud container removed 🥀')
  171. } catch (err) {
  172. console.log(err)
  173. }
  174. }
  175. /**
  176. * Get the testing container's IP
  177. *
  178. * @param {Docker.Container} container the container to get the IP from
  179. */
  180. export const getContainerIP = async function(
  181. container = docker.getContainer(CONTAINER_NAME),
  182. ): Promise<string> {
  183. let ip = ''
  184. let tries = 0
  185. while (ip === '' && tries < 10) {
  186. tries++
  187. await container.inspect(function(err, data) {
  188. if (err) {
  189. throw err
  190. }
  191. ip = data?.NetworkSettings?.IPAddress || ''
  192. })
  193. if (ip !== '') {
  194. break
  195. }
  196. await sleep(1000 * tries)
  197. }
  198. return ip
  199. }
  200. // Would be simpler to start the container from cypress.config.ts,
  201. // but when checking out different branches, it can take a few seconds
  202. // Until we can properly configure the baseUrl retry intervals,
  203. // We need to make sure the server is already running before cypress
  204. // https://github.com/cypress-io/cypress/issues/22676
  205. export const waitOnNextcloud = async function(ip: string) {
  206. console.log('├─ Waiting for Nextcloud to be ready... ⏳')
  207. await waitOn({
  208. resources: [`http://${ip}/index.php`],
  209. // wait for nextcloud to be up and return any non error status
  210. validateStatus: (status) => status >= 200 && status < 400,
  211. // timout in ms
  212. timeout: 5 * 60 * 1000,
  213. // timeout for a single HTTP request
  214. httpTimeout: 60 * 1000,
  215. })
  216. console.log('└─ Done')
  217. }
  218. const runExec = async function(
  219. container: Docker.Container,
  220. command: string[],
  221. verbose = false,
  222. user = 'www-data',
  223. ) {
  224. const exec = await container.exec({
  225. Cmd: command,
  226. AttachStdout: true,
  227. AttachStderr: true,
  228. User: user,
  229. })
  230. return new Promise((resolve, reject) => {
  231. exec.start({}, (err, stream) => {
  232. if (err) {
  233. reject(err)
  234. }
  235. if (stream) {
  236. stream.setEncoding('utf-8')
  237. stream.on('data', str => {
  238. if (verbose && str.trim() !== '') {
  239. console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`)
  240. }
  241. })
  242. stream.on('end', resolve)
  243. }
  244. })
  245. })
  246. }
  247. const sleep = function(milliseconds: number) {
  248. return new Promise((resolve) => setTimeout(resolve, milliseconds))
  249. }
  250. const getCurrentGitBranch = function() {
  251. return execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'master'
  252. }