diff options
Diffstat (limited to 'cypress/dockerNode.ts')
-rw-r--r-- | cypress/dockerNode.ts | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/cypress/dockerNode.ts b/cypress/dockerNode.ts new file mode 100644 index 00000000000..6e21b33101c --- /dev/null +++ b/cypress/dockerNode.ts @@ -0,0 +1,357 @@ +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +/* eslint-disable no-console */ +/* eslint-disable n/no-unpublished-import */ +/* eslint-disable n/no-extraneous-import */ + +import Docker from 'dockerode' +import waitOn from 'wait-on' +import { c as createTar } from 'tar' +import path, { basename } from 'path' +import { execSync } from 'child_process' +import { existsSync } from 'fs' + +export const docker = new Docker() + +const CONTAINER_NAME = `nextcloud-cypress-tests_${basename(process.cwd()).replace(' ', '')}` +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 = getCurrentGitBranch()): Promise<any> { + + try { + try { + // Pulling images + console.log('\nPulling images... ⏳') + await new Promise((resolve, reject): any => docker.pull(SERVER_IMAGE, (err, stream) => { + if (err) { + reject(err) + } + if (stream === null) { + reject(new Error('Could not connect to docker, ensure docker is running.')) + return + } + + // https://github.com/apocas/dockerode/issues/357 + docker.modem.followProgress(stream, onFinished) + + function onFinished(err) { + if (!err) { + resolve(true) + return + } + reject(err) + } + })) + + const digest = await (await docker.getImage(SERVER_IMAGE).inspect()).RepoDigests.at(0) + const sha = digest?.split('@').at(1) + console.log('├─ Using image ' + sha) + console.log('└─ Done') + } catch (e) { + console.log('└─ Failed to pull images') + throw e + } + + // 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: { + Mounts: [{ + Target: '/var/www/html/data', + Source: '', + Type: 'tmpfs', + ReadOnly: false, + }], + PortBindings: { + '80/tcp': [{ + HostIP: '0.0.0.0', + HostPort: '8083', + }], + }, + // If running the setup tests, let's bind to host + // to communicate with the github actions DB services + NetworkMode: process.env.SETUP_TESTING === 'true' ? await getGithubNetwork() : undefined, + }, + Env: [ + `BRANCH=${branch}`, + 'APCU=1', + ], + }) + await container.start() + + // Set proper permissions for the data folder + await runExec(container, ['chown', '-R', 'www-data:www-data', '/var/www/html/data'], false, 'root') + await runExec(container, ['chmod', '0770', '/var/www/html/data'], false, 'root') + + // 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('\n', err, '\n') + 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) + + // Speed up test and make them less flaky. If a cron execution is needed, it can be triggered manually. + await runExec(container, ['php', 'occ', 'background:cron'], true) + + // Checking apcu + const distributed = await runExec(container, ['php', 'occ', 'config:system:get', 'memcache.distributed']) + const local = await runExec(container, ['php', 'occ', 'config:system:get', 'memcache.local']) + const hashing = await runExec(container, ['php', 'occ', 'config:system:get', 'hashing_default_password']) + + console.log('├─ Checking APCu configuration... 👀') + if (!distributed.trim().includes('Memcache\\APCu') + || !local.trim().includes('Memcache\\APCu') + || !hashing.trim().includes('true')) { + console.log('└─ APCu is not properly configured 🛑') + throw new Error('APCu is not properly configured') + } + console.log('│ └─ OK !') + + // Saving DB state + console.log('├─ Creating init DB snapshot...') + await runExec(container, ['cp', '/var/www/html/data/owncloud.db', '/var/www/html/data/owncloud.db-init'], true) + console.log('├─ Creating init data backup...') + await runExec(container, ['tar', 'cf', 'data-init.tar', 'admin'], true, undefined, '/var/www/html/data') + + 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 htmlPath = '/var/www/html' + const folderPaths = [ + './3rdparty', + './apps', + './core', + './dist', + './lib', + './ocs', + './ocs-provider', + './resources', + './tests', + './console.php', + './cron.php', + './index.php', + './occ', + './public.php', + './remote.php', + './status.php', + './version.php', + ].filter((folderPath) => { + const fullPath = path.resolve(__dirname, '..', folderPath) + + if (existsSync(fullPath)) { + console.log(`├─ Copying ${folderPath}`) + return true + } + return false + }) + + // Don't try to apply changes, when there are none. Otherwise we + // still execute the 'chown' command, which is not needed. + if (folderPaths.length === 0) { + console.log('└─ No local changes found to apply') + return + } + + const container = docker.getContainer(CONTAINER_NAME) + + // Tar-streaming the above folders into the container + const serverTar = createTar({ 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++ + + container.inspect(function(err, data) { + if (err) { + throw err + } + + if (data?.HostConfig.PortBindings?.['80/tcp']?.[0]?.HostPort) { + ip = `localhost:${data.HostConfig.PortBindings['80/tcp'][0].HostPort}` + } else { + 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`], + // wait for nextcloud to be up and return any non error status + validateStatus: (status) => status >= 200 && status < 400, + // timout in ms + timeout: 5 * 60 * 1000, + // timeout for a single HTTP request + httpTimeout: 60 * 1000, + }) + console.log('└─ Done') +} + +const runExec = async function( + container: Docker.Container, + command: string[], + verbose = false, + user = 'www-data', + workdir?: string, +): Promise<string> { + const exec = await container.exec({ + Cmd: command, + WorkingDir: workdir, + AttachStdout: true, + AttachStderr: true, + User: user, + }) + + return new Promise((resolve, reject) => { + let output = '' + exec.start({}, (err, stream) => { + if (err) { + reject(err) + } + if (stream) { + stream.setEncoding('utf-8') + stream.on('data', str => { + str = str.trim() + // Remove non printable characters + .replace(/[^\x0A\x0D\x20-\x7E]+/g, '') + // Remove non alphanumeric leading characters + .replace(/^[^a-z]/gi, '') + output += str + if (verbose && str !== '') { + console.log(`├─ ${str.replace(/\n/gi, '\n├─ ')}`) + } + }) + stream.on('end', () => resolve(output)) + } + }) + }) +} + +const sleep = function(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)) +} + +const getCurrentGitBranch = function() { + return execSync('git rev-parse --abbrev-ref HEAD').toString().trim() || 'master' +} + +/** + * Get the network name of the github actions network + * This is used to connect to the database services + * started by github actions + */ +const getGithubNetwork = async function(): Promise<string|undefined> { + console.log('├─ Looking for github actions network... 🔍') + const networks = await docker.listNetworks() + const network = networks.find((network) => network.Name.startsWith('github_network')) + if (network) { + console.log('│ └─ Found github actions network: ' + network.Name) + return network.Name + } + + console.log('│ └─ No github actions network found') + return undefined +} |