dockerNode.ts 7.8 KB

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