dockerNode.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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 node/no-unpublished-import */
  24. import Docker from 'dockerode'
  25. import waitOn from 'wait-on'
  26. import tar from 'tar'
  27. export const docker = new Docker()
  28. const CONTAINER_NAME = 'nextcloud-cypress-tests-server'
  29. const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
  30. /**
  31. * Start the testing container
  32. *
  33. * @param {string} branch the branch of your current work
  34. */
  35. export const startNextcloud = async function(branch: string = 'master'): Promise<any> {
  36. try {
  37. // Pulling images
  38. console.log('\nPulling images... ⏳')
  39. await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => {
  40. if (err) {
  41. reject(err)
  42. }
  43. // https://github.com/apocas/dockerode/issues/357
  44. docker.modem.followProgress(stream, onFinished)
  45. function onFinished(err) {
  46. if (!err) {
  47. resolve(true)
  48. return
  49. }
  50. reject(err)
  51. }
  52. }))
  53. console.log('└─ Done')
  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. })
  79. await container.start()
  80. // Get container's IP
  81. const ip = await getContainerIP(container)
  82. console.log(`├─ Nextcloud container's IP is ${ip} 🌏`)
  83. return ip
  84. } catch (err) {
  85. console.log('└─ Unable to start the container 🛑')
  86. console.log(err)
  87. stopNextcloud()
  88. throw new Error('Unable to start the container')
  89. }
  90. }
  91. /**
  92. * Configure Nextcloud
  93. */
  94. export const configureNextcloud = async function() {
  95. console.log('\nConfiguring nextcloud...')
  96. const container = docker.getContainer(CONTAINER_NAME)
  97. await runExec(container, ['php', 'occ', '--version'], true)
  98. // Be consistent for screenshots
  99. await runExec(container, ['php', 'occ', 'config:system:set', 'default_language', '--value', 'en'], true)
  100. await runExec(container, ['php', 'occ', 'config:system:set', 'force_language', '--value', 'en'], true)
  101. await runExec(container, ['php', 'occ', 'config:system:set', 'default_locale', '--value', 'en_US'], true)
  102. await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
  103. await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
  104. // Enable the app and give status
  105. await runExec(container, ['php', 'occ', 'app:enable', '--force', 'viewer'], true)
  106. // await runExec(container, ['php', 'occ', 'app:list'], 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. './apps',
  121. './core',
  122. './dist',
  123. './lib',
  124. './ocs',
  125. ]
  126. // Tar-streaming the above folder sinto the container
  127. const serverTar = tar.c({ gzip: false }, folderPaths)
  128. await container.putArchive(serverTar, {
  129. path: htmlPath,
  130. })
  131. // Making sure we have the proper permissions
  132. await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
  133. console.log('└─ Changes applied successfully 🎉')
  134. }
  135. /**
  136. * Force stop the testing container
  137. */
  138. export const stopNextcloud = async function() {
  139. try {
  140. const container = docker.getContainer(CONTAINER_NAME)
  141. console.log('Stopping Nextcloud container...')
  142. container.remove({ force: true })
  143. console.log('└─ Nextcloud container removed 🥀')
  144. } catch (err) {
  145. console.log(err)
  146. }
  147. }
  148. /**
  149. * Get the testing container's IP
  150. *
  151. * @param {Docker.Container} container the container to get the IP from
  152. */
  153. export const getContainerIP = async function(
  154. container = docker.getContainer(CONTAINER_NAME)
  155. ): Promise<string> {
  156. let ip = ''
  157. let tries = 0
  158. while (ip === '' && tries < 10) {
  159. tries++
  160. await container.inspect(function(err, data) {
  161. if (err) {
  162. throw err
  163. }
  164. ip = data?.NetworkSettings?.IPAddress || ''
  165. })
  166. if (ip !== '') {
  167. break
  168. }
  169. await sleep(1000 * tries)
  170. }
  171. return ip
  172. }
  173. // Would be simpler to start the container from cypress.config.ts,
  174. // but when checking out different branches, it can take a few seconds
  175. // Until we can properly configure the baseUrl retry intervals,
  176. // We need to make sure the server is already running before cypress
  177. // https://github.com/cypress-io/cypress/issues/22676
  178. export const waitOnNextcloud = async function(ip: string) {
  179. console.log('├─ Waiting for Nextcloud to be ready... ⏳')
  180. await waitOn({ resources: [`http://${ip}/index.php`] })
  181. console.log('└─ Done')
  182. }
  183. const runExec = async function(
  184. container: Docker.Container,
  185. command: string[],
  186. verbose = false,
  187. user = 'www-data'
  188. ) {
  189. const exec = await container.exec({
  190. Cmd: command,
  191. AttachStdout: true,
  192. AttachStderr: true,
  193. User: user,
  194. })
  195. return new Promise((resolve, reject) => {
  196. exec.start({}, (err, stream) => {
  197. if (err) {
  198. reject(err)
  199. }
  200. if (stream) {
  201. stream.setEncoding('utf-8')
  202. stream.on('data', str => {
  203. if (verbose && str.trim() !== '') {
  204. console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`)
  205. }
  206. })
  207. stream.on('end', resolve)
  208. }
  209. })
  210. })
  211. }
  212. const sleep = function(milliseconds: number) {
  213. return new Promise((resolve) => setTimeout(resolve, milliseconds))
  214. }