dockerNode.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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 { c as createTar } from 'tar'
  11. import path, { basename } 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_${basename(process.cwd()).replace(' ', '')}`
  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. function onFinished(err) {
  38. if (!err) {
  39. resolve(true)
  40. return
  41. }
  42. reject(err)
  43. }
  44. }))
  45. console.log('└─ Done')
  46. } catch (e) {
  47. console.log('└─ Failed to pull images')
  48. throw e
  49. }
  50. // Remove old container if exists
  51. console.log('\nChecking running containers... 🔍')
  52. try {
  53. const oldContainer = docker.getContainer(CONTAINER_NAME)
  54. const oldContainerData = await oldContainer.inspect()
  55. if (oldContainerData) {
  56. console.log('├─ Existing running container found')
  57. console.log('├─ Removing... ⏳')
  58. // Forcing any remnants to be removed just in case
  59. await oldContainer.remove({ force: true })
  60. console.log('└─ Done')
  61. }
  62. } catch (error) {
  63. console.log('└─ None found!')
  64. }
  65. // Starting container
  66. console.log('\nStarting Nextcloud container... 🚀')
  67. console.log(`├─ Using branch '${branch}'`)
  68. const container = await docker.createContainer({
  69. Image: SERVER_IMAGE,
  70. name: CONTAINER_NAME,
  71. HostConfig: {
  72. Mounts: [{
  73. Target: '/var/www/html/data',
  74. Source: '',
  75. Type: 'tmpfs',
  76. ReadOnly: false,
  77. }],
  78. },
  79. Env: [
  80. `BRANCH=${branch}`,
  81. 'APCU=1',
  82. ],
  83. })
  84. await container.start()
  85. // Set proper permissions for the data folder
  86. await runExec(container, ['chown', '-R', 'www-data:www-data', '/var/www/html/data'], false, 'root')
  87. await runExec(container, ['chmod', '0770', '/var/www/html/data'], false, 'root')
  88. // Init Nextcloud
  89. // await runExec(container, ['initnc.sh'], true, 'root')
  90. // Get container's IP
  91. const ip = await getContainerIP(container)
  92. console.log(`├─ Nextcloud container's IP is ${ip} 🌏`)
  93. return ip
  94. } catch (err) {
  95. console.log('└─ Unable to start the container 🛑')
  96. console.log('\n', err, '\n')
  97. stopNextcloud()
  98. throw new Error('Unable to start the container')
  99. }
  100. }
  101. /**
  102. * Configure Nextcloud
  103. */
  104. export const configureNextcloud = async function() {
  105. console.log('\nConfiguring nextcloud...')
  106. const container = docker.getContainer(CONTAINER_NAME)
  107. await runExec(container, ['php', 'occ', '--version'], true)
  108. // Be consistent for screenshots
  109. await runExec(container, ['php', 'occ', 'config:system:set', 'default_language', '--value', 'en'], true)
  110. await runExec(container, ['php', 'occ', 'config:system:set', 'force_language', '--value', 'en'], true)
  111. await runExec(container, ['php', 'occ', 'config:system:set', 'default_locale', '--value', 'en_US'], true)
  112. await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
  113. await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
  114. // Speed up test and make them less flaky. If a cron execution is needed, it can be triggered manually.
  115. await runExec(container, ['php', 'occ', 'background:cron'], true)
  116. // Checking apcu
  117. const distributed = await runExec(container, ['php', 'occ', 'config:system:get', 'memcache.distributed'])
  118. const local = await runExec(container, ['php', 'occ', 'config:system:get', 'memcache.local'])
  119. const hashing = await runExec(container, ['php', 'occ', 'config:system:get', 'hashing_default_password'])
  120. console.log('├─ Checking APCu configuration... 👀')
  121. if (!distributed.trim().includes('Memcache\\APCu')
  122. || !local.trim().includes('Memcache\\APCu')
  123. || !hashing.trim().includes('true')) {
  124. console.log('└─ APCu is not properly configured 🛑')
  125. throw new Error('APCu is not properly configured')
  126. }
  127. console.log('│ └─ OK !')
  128. // Saving DB state
  129. console.log('├─ Creating init DB snapshot...')
  130. await runExec(container, ['cp', '/var/www/html/data/owncloud.db', '/var/www/html/data/owncloud.db-init'], true)
  131. console.log('├─ Creating init data backup...')
  132. await runExec(container, ['tar', 'cf', 'data-init.tar', 'admin'], true, undefined, '/var/www/html/data')
  133. console.log('└─ Nextcloud is now ready to use 🎉')
  134. }
  135. /**
  136. * Applying local changes to the container
  137. * Only triggered if we're not in CI. Otherwise the
  138. * continuous-integration-shallow-server image will
  139. * already fetch the proper branch.
  140. */
  141. export const applyChangesToNextcloud = async function() {
  142. console.log('\nApply local changes to nextcloud...')
  143. const htmlPath = '/var/www/html'
  144. const folderPaths = [
  145. './3rdparty',
  146. './apps',
  147. './core',
  148. './dist',
  149. './lib',
  150. './ocs',
  151. './ocs-provider',
  152. './resources',
  153. './console.php',
  154. './cron.php',
  155. './index.php',
  156. './occ',
  157. './public.php',
  158. './remote.php',
  159. './status.php',
  160. './version.php',
  161. ].filter((folderPath) => {
  162. const fullPath = path.resolve(__dirname, '..', folderPath)
  163. if (existsSync(fullPath)) {
  164. console.log(`├─ Copying ${folderPath}`)
  165. return true
  166. }
  167. return false
  168. })
  169. // Don't try to apply changes, when there are none. Otherwise we
  170. // still execute the 'chown' command, which is not needed.
  171. if (folderPaths.length === 0) {
  172. console.log('└─ No local changes found to apply')
  173. return
  174. }
  175. const container = docker.getContainer(CONTAINER_NAME)
  176. // Tar-streaming the above folders into the container
  177. const serverTar = createTar({ gzip: false }, folderPaths)
  178. await container.putArchive(serverTar, {
  179. path: htmlPath,
  180. })
  181. // Making sure we have the proper permissions
  182. await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
  183. console.log('└─ Changes applied successfully 🎉')
  184. }
  185. /**
  186. * Force stop the testing container
  187. */
  188. export const stopNextcloud = async function() {
  189. try {
  190. const container = docker.getContainer(CONTAINER_NAME)
  191. console.log('Stopping Nextcloud container...')
  192. container.remove({ force: true })
  193. console.log('└─ Nextcloud container removed 🥀')
  194. } catch (err) {
  195. console.log(err)
  196. }
  197. }
  198. /**
  199. * Get the testing container's IP
  200. *
  201. * @param {Docker.Container} container the container to get the IP from
  202. */
  203. export const getContainerIP = async function(
  204. container = docker.getContainer(CONTAINER_NAME),
  205. ): Promise<string> {
  206. let ip = ''
  207. let tries = 0
  208. while (ip === '' && tries < 10) {
  209. tries++
  210. await container.inspect(function(err, data) {
  211. if (err) {
  212. throw err
  213. }
  214. ip = data?.NetworkSettings?.IPAddress || ''
  215. })
  216. if (ip !== '') {
  217. break
  218. }
  219. await sleep(1000 * tries)
  220. }
  221. return ip
  222. }
  223. // Would be simpler to start the container from cypress.config.ts,
  224. // but when checking out different branches, it can take a few seconds
  225. // Until we can properly configure the baseUrl retry intervals,
  226. // We need to make sure the server is already running before cypress
  227. // https://github.com/cypress-io/cypress/issues/22676
  228. export const waitOnNextcloud = async function(ip: string) {
  229. console.log('├─ Waiting for Nextcloud to be ready... ⏳')
  230. await waitOn({
  231. resources: [`http://${ip}/index.php`],
  232. // wait for nextcloud to be up and return any non error status
  233. validateStatus: (status) => status >= 200 && status < 400,
  234. // timout in ms
  235. timeout: 5 * 60 * 1000,
  236. // timeout for a single HTTP request
  237. httpTimeout: 60 * 1000,
  238. })
  239. console.log('└─ Done')
  240. }
  241. const runExec = async function(
  242. container: Docker.Container,
  243. command: string[],
  244. verbose = false,
  245. user = 'www-data',
  246. workdir?: string,
  247. ): Promise<string> {
  248. const exec = await container.exec({
  249. Cmd: command,
  250. WorkingDir: workdir,
  251. AttachStdout: true,
  252. AttachStderr: true,
  253. User: user,
  254. })
  255. return new Promise((resolve, reject) => {
  256. let output = ''
  257. exec.start({}, (err, stream) => {
  258. if (err) {
  259. reject(err)
  260. }
  261. if (stream) {
  262. stream.setEncoding('utf-8')
  263. stream.on('data', str => {
  264. str = str.trim()
  265. // Remove non printable characters
  266. .replace(/[^\x0A\x0D\x20-\x7E]+/g, '')
  267. // Remove non alphanumeric leading characters
  268. .replace(/^[^a-z]/gi, '')
  269. output += str
  270. if (verbose && str !== '') {
  271. console.log(`├─ ${str.replace(/\n/gi, '\n├─ ')}`)
  272. }
  273. })
  274. stream.on('end', () => resolve(output))
  275. }
  276. })
  277. })
  278. }
  279. const sleep = function(milliseconds: number) {
  280. return new Promise((resolve) => setTimeout(resolve, milliseconds))
  281. }
  282. const getCurrentGitBranch = function() {
  283. return execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'master'
  284. }