dockerNode.ts 7.7 KB

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