dockerNode.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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. ].filter((folderPath) => {
  138. const fullPath = path.resolve(__dirname, '..', folderPath)
  139. if (existsSync(fullPath)) {
  140. console.log(`├─ Copying ${folderPath}`)
  141. return true
  142. }
  143. return false
  144. })
  145. // Don't try to apply changes, when there are none. Otherwise we
  146. // still execute the 'chown' command, which is not needed.
  147. if (folderPaths.length === 0) {
  148. console.log('└─ No local changes found to apply')
  149. return
  150. }
  151. const container = docker.getContainer(CONTAINER_NAME)
  152. // Tar-streaming the above folders into the container
  153. const serverTar = tar.c({ gzip: false }, folderPaths)
  154. await container.putArchive(serverTar, {
  155. path: htmlPath,
  156. })
  157. // Making sure we have the proper permissions
  158. await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
  159. console.log('└─ Changes applied successfully 🎉')
  160. }
  161. /**
  162. * Force stop the testing container
  163. */
  164. export const stopNextcloud = async function() {
  165. try {
  166. const container = docker.getContainer(CONTAINER_NAME)
  167. console.log('Stopping Nextcloud container...')
  168. container.remove({ force: true })
  169. console.log('└─ Nextcloud container removed 🥀')
  170. } catch (err) {
  171. console.log(err)
  172. }
  173. }
  174. /**
  175. * Get the testing container's IP
  176. *
  177. * @param {Docker.Container} container the container to get the IP from
  178. */
  179. export const getContainerIP = async function(
  180. container = docker.getContainer(CONTAINER_NAME),
  181. ): Promise<string> {
  182. let ip = ''
  183. let tries = 0
  184. while (ip === '' && tries < 10) {
  185. tries++
  186. await container.inspect(function(err, data) {
  187. if (err) {
  188. throw err
  189. }
  190. ip = data?.NetworkSettings?.IPAddress || ''
  191. })
  192. if (ip !== '') {
  193. break
  194. }
  195. await sleep(1000 * tries)
  196. }
  197. return ip
  198. }
  199. // Would be simpler to start the container from cypress.config.ts,
  200. // but when checking out different branches, it can take a few seconds
  201. // Until we can properly configure the baseUrl retry intervals,
  202. // We need to make sure the server is already running before cypress
  203. // https://github.com/cypress-io/cypress/issues/22676
  204. export const waitOnNextcloud = async function(ip: string) {
  205. console.log('├─ Waiting for Nextcloud to be ready... ⏳')
  206. await waitOn({
  207. resources: [`http://${ip}/index.php`],
  208. // wait for nextcloud to be up and return any non error status
  209. validateStatus: (status) => status >= 200 && status < 400,
  210. // timout in ms
  211. timeout: 5 * 60 * 1000,
  212. // timeout for a single HTTP request
  213. httpTimeout: 60 * 1000,
  214. })
  215. console.log('└─ Done')
  216. }
  217. const runExec = async function(
  218. container: Docker.Container,
  219. command: string[],
  220. verbose = false,
  221. user = 'www-data',
  222. ) {
  223. const exec = await container.exec({
  224. Cmd: command,
  225. AttachStdout: true,
  226. AttachStderr: true,
  227. User: user,
  228. })
  229. return new Promise((resolve, reject) => {
  230. exec.start({}, (err, stream) => {
  231. if (err) {
  232. reject(err)
  233. }
  234. if (stream) {
  235. stream.setEncoding('utf-8')
  236. stream.on('data', str => {
  237. if (verbose && str.trim() !== '') {
  238. console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`)
  239. }
  240. })
  241. stream.on('end', resolve)
  242. }
  243. })
  244. })
  245. }
  246. const sleep = function(milliseconds: number) {
  247. return new Promise((resolve) => setTimeout(resolve, milliseconds))
  248. }
  249. const getCurrentGitBranch = function() {
  250. return execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'master'
  251. }