server/cypress/dockerNode.ts

244 lines
6.8 KiB
TypeScript

/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* eslint-disable no-console */
/* eslint-disable node/no-unpublished-import */
import Docker from 'dockerode'
import waitOn from 'wait-on'
import tar from 'tar'
export const docker = new Docker()
const CONTAINER_NAME = 'nextcloud-cypress-tests-server'
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
/**
* Start the testing container
*
* @param {string} branch the branch of your current work
*/
export const startNextcloud = async function(branch: string = 'master'): Promise<any> {
try {
// Pulling images
console.log('\nPulling images... ⏳')
await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => {
if (err) {
reject(err)
}
// https://github.com/apocas/dockerode/issues/357
docker.modem.followProgress(stream, onFinished)
function onFinished(err) {
if (!err) {
resolve(true)
return
}
reject(err)
}
}))
console.log('└─ Done')
// Remove old container if exists
console.log('\nChecking running containers... 🔍')
try {
const oldContainer = docker.getContainer(CONTAINER_NAME)
const oldContainerData = await oldContainer.inspect()
if (oldContainerData) {
console.log('├─ Existing running container found')
console.log('├─ Removing... ⏳')
// Forcing any remnants to be removed just in case
await oldContainer.remove({ force: true })
console.log('└─ Done')
}
} catch (error) {
console.log('└─ None found!')
}
// Starting container
console.log('\nStarting Nextcloud container... 🚀')
console.log(`├─ Using branch '${branch}'`)
const container = await docker.createContainer({
Image: SERVER_IMAGE,
name: CONTAINER_NAME,
HostConfig: {
Binds: [],
},
})
await container.start()
// Get container's IP
const ip = await getContainerIP(container)
console.log(`├─ Nextcloud container's IP is ${ip} 🌏`)
return ip
} catch (err) {
console.log('└─ Unable to start the container 🛑')
console.log(err)
stopNextcloud()
throw new Error('Unable to start the container')
}
}
/**
* Configure Nextcloud
*/
export const configureNextcloud = async function() {
console.log('\nConfiguring nextcloud...')
const container = docker.getContainer(CONTAINER_NAME)
await runExec(container, ['php', 'occ', '--version'], true)
// Be consistent for screenshots
await runExec(container, ['php', 'occ', 'config:system:set', 'default_language', '--value', 'en'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'force_language', '--value', 'en'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'default_locale', '--value', 'en_US'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
// Enable the app and give status
await runExec(container, ['php', 'occ', 'app:enable', '--force', 'viewer'], true)
// await runExec(container, ['php', 'occ', 'app:list'], true)
console.log('└─ Nextcloud is now ready to use 🎉')
}
/**
* Applying local changes to the container
* Only triggered if we're not in CI. Otherwise the
* continuous-integration-shallow-server image will
* already fetch the proper branch.
*/
export const applyChangesToNextcloud = async function() {
console.log('\nApply local changes to nextcloud...')
const container = docker.getContainer(CONTAINER_NAME)
const htmlPath = '/var/www/html'
const folderPaths = [
'./apps',
'./core',
'./dist',
'./lib',
'./ocs',
]
// Tar-streaming the above folder sinto the container
const serverTar = tar.c({ gzip: false }, folderPaths)
await container.putArchive(serverTar, {
path: htmlPath,
})
// Making sure we have the proper permissions
await runExec(container, ['chown', '-R', 'www-data:www-data', htmlPath], false, 'root')
console.log('└─ Changes applied successfully 🎉')
}
/**
* Force stop the testing container
*/
export const stopNextcloud = async function() {
try {
const container = docker.getContainer(CONTAINER_NAME)
console.log('Stopping Nextcloud container...')
container.remove({ force: true })
console.log('└─ Nextcloud container removed 🥀')
} catch (err) {
console.log(err)
}
}
/**
* Get the testing container's IP
*
* @param {Docker.Container} container the container to get the IP from
*/
export const getContainerIP = async function(
container = docker.getContainer(CONTAINER_NAME)
): Promise<string> {
let ip = ''
let tries = 0
while (ip === '' && tries < 10) {
tries++
await container.inspect(function(err, data) {
if (err) {
throw err
}
ip = data?.NetworkSettings?.IPAddress || ''
})
if (ip !== '') {
break
}
await sleep(1000 * tries)
}
return ip
}
// Would be simpler to start the container from cypress.config.ts,
// but when checking out different branches, it can take a few seconds
// Until we can properly configure the baseUrl retry intervals,
// We need to make sure the server is already running before cypress
// https://github.com/cypress-io/cypress/issues/22676
export const waitOnNextcloud = async function(ip: string) {
console.log('├─ Waiting for Nextcloud to be ready... ⏳')
await waitOn({ resources: [`http://${ip}/index.php`] })
console.log('└─ Done')
}
const runExec = async function(
container: Docker.Container,
command: string[],
verbose = false,
user = 'www-data'
) {
const exec = await container.exec({
Cmd: command,
AttachStdout: true,
AttachStderr: true,
User: user,
})
return new Promise((resolve, reject) => {
exec.start({}, (err, stream) => {
if (err) {
reject(err)
}
if (stream) {
stream.setEncoding('utf-8')
stream.on('data', str => {
if (verbose && str.trim() !== '') {
console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`)
}
})
stream.on('end', resolve)
}
})
})
}
const sleep = function(milliseconds: number) {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}