dockerNode.ts 7.1 KB

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