You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dockerNode.ts 6.7KB

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