-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Make docker integration available for other projects
* Provide the docker helpers as `@nextcloud/cypress/docker` import. * Allow installing custom required apps for testing * Handle `text` app which is not in the appstore but also not bundled * Allow to autodetect current app, or set one, and bind mount the directory for testing the app Signed-off-by: Ferdinand Thiessen <[email protected]>
- Loading branch information
1 parent
9d1a7a0
commit b8540a5
Showing
6 changed files
with
200 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
/* eslint-disable no-console */ | ||
/** | ||
* @copyright Copyright (c) 2022 John Molakvoæ <[email protected]> | ||
* | ||
|
@@ -23,15 +24,53 @@ | |
import Docker from 'dockerode' | ||
import waitOn from 'wait-on' | ||
|
||
import type { Stream } from 'node:stream' | ||
import { join, resolve, sep } from 'path' | ||
import { existsSync, readFileSync } from 'fs' | ||
import { XMLParser } from 'fast-xml-parser' | ||
|
||
export const docker = new Docker() | ||
|
||
const CONTAINER_NAME = 'nextcloud-cypress-tests' | ||
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server' | ||
|
||
const TEXT_APP_GIT = 'https://github.com/nextcloud/text.git' | ||
|
||
/** | ||
* Start the testing container | ||
* | ||
* @param branch server branch to use | ||
* @param mountApp bind mount app within server (`true` for autodetect, `false` to disable, or a string to force a path) | ||
* @return Promise resolving to the IP address of the server | ||
* @throws {Error} If Nextcloud container could not be started | ||
*/ | ||
export const startNextcloud = async function (branch: string = 'master'): Promise<any> { | ||
export const startNextcloud = async function(branch = 'master', mountApp: boolean|string = true): Promise<any> { | ||
let appPath = mountApp === true ? process.cwd() : mountApp | ||
let appId: string|undefined | ||
let appVersion: string|undefined | ||
if (appPath) { | ||
console.log('Mounting app directory') | ||
while (appPath) { | ||
const appInfoPath = resolve(join(appPath, 'appinfo', 'info.xml')) | ||
if (existsSync(appInfoPath)) { | ||
const parser = new XMLParser() | ||
const xmlDoc = parser.parse(readFileSync(appInfoPath)) | ||
appId = xmlDoc.info.id | ||
appVersion = xmlDoc.info.version | ||
console.log(`└─ Found ${appId} version ${appVersion}`) | ||
break | ||
} else { | ||
// skip if root is reached or manual directory was set | ||
if (appPath === sep || typeof mountApp === 'string') { | ||
console.log('└─ No appinfo found') | ||
appPath = false | ||
break | ||
} | ||
appPath = join(appPath, '..') | ||
} | ||
} | ||
} | ||
|
||
try { | ||
// Pulling images | ||
console.log('Pulling images... ⏳') | ||
|
@@ -56,22 +95,22 @@ export const startNextcloud = async function (branch: string = 'master'): Promis | |
const oldContainer = docker.getContainer(CONTAINER_NAME) | ||
const oldContainerData = await oldContainer.inspect() | ||
if (oldContainerData.State.Running) { | ||
console.log(`├─ Existing running container found`) | ||
console.log('├─ Existing running container found') | ||
if (localImage[0].Id !== oldContainerData.Image) { | ||
console.log(`└─ But running container is outdated, replacing...`) | ||
console.log('└─ But running container is outdated, replacing...') | ||
} else { | ||
// Get container's IP | ||
console.log(`├─ Reusing that container`) | ||
let ip = await getContainerIP(oldContainer) | ||
console.log('├─ Reusing that container') | ||
const ip = await getContainerIP(oldContainer) | ||
return ip | ||
} | ||
} else { | ||
console.log(`└─ None found!`) | ||
console.log('└─ None found!') | ||
} | ||
// Forcing any remnants to be removed just in case | ||
await oldContainer.remove({ force: true }) | ||
} catch (error) { | ||
console.log(`└─ None found!`) | ||
console.log('└─ None found!') | ||
} | ||
|
||
// Starting container | ||
|
@@ -81,16 +120,19 @@ export const startNextcloud = async function (branch: string = 'master'): Promis | |
Image: SERVER_IMAGE, | ||
name: CONTAINER_NAME, | ||
Env: [`BRANCH=${branch}`], | ||
HostConfig: { | ||
Binds: appPath !== false ? [`${appPath}:/var/www/html/apps/${appId}`] : undefined, | ||
}, | ||
}) | ||
await container.start() | ||
|
||
// Get container's IP | ||
let ip = await getContainerIP(container) | ||
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('└─ Unable to start the container 🛑') | ||
console.log(err) | ||
stopNextcloud() | ||
throw new Error('Unable to start the container') | ||
|
@@ -99,8 +141,10 @@ export const startNextcloud = async function (branch: string = 'master'): Promis | |
|
||
/** | ||
* Configure Nextcloud | ||
* | ||
* @param {string[]} apps List of default apps to install (default is ['viewer']) | ||
*/ | ||
export const configureNextcloud = async function () { | ||
export const configureNextcloud = async function(apps = ['viewer']) { | ||
console.log('\nConfiguring nextcloud...') | ||
const container = docker.getContainer(CONTAINER_NAME) | ||
await runExec(container, ['php', 'occ', '--version'], true) | ||
|
@@ -112,8 +156,31 @@ export const configureNextcloud = async function () { | |
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) | ||
// Build app list | ||
const json = await runExec(container, ['php', 'occ', 'app:list', '--output', 'json'], false) | ||
// fix dockerode bug returning invalid leading characters | ||
const applist = JSON.parse(json.substring(json.indexOf('{'))) | ||
|
||
// Enable apps and give status | ||
for (const app of apps) { | ||
if (app in applist.enabled) { | ||
console.log(`├─ ${app} version ${applist.enabled[app]} already installed and enabled`) | ||
} else if (app in applist.disabled) { | ||
// built in | ||
await runExec(container, ['php', 'occ', 'app:enable', '--force', app], true) | ||
} else { | ||
if (app === 'text') { | ||
// text is vendored but not within the server package | ||
await runExec(container, ['apt', 'update'], false, 'root') | ||
await runExec(container, ['apt-get', '-y', 'install', 'git'], false, 'root') | ||
await runExec(container, ['git', 'clone', '--depth=1', TEXT_APP_GIT, 'apps/text'], true) | ||
await runExec(container, ['php', 'occ', 'app:enable', '--force', app], true) | ||
} else { | ||
// try appstore | ||
await runExec(container, ['php', 'occ', 'app:install', '--force', app], true) | ||
} | ||
} | ||
} | ||
// await runExec(container, ['php', 'occ', 'app:list'], true) | ||
|
||
console.log('└─ Nextcloud is now ready to use 🎉') | ||
|
@@ -122,7 +189,7 @@ export const configureNextcloud = async function () { | |
/** | ||
* Force stop the testing container | ||
*/ | ||
export const stopNextcloud = async function () { | ||
export const stopNextcloud = async function() { | ||
try { | ||
const container = docker.getContainer(CONTAINER_NAME) | ||
console.log('Stopping Nextcloud container...') | ||
|
@@ -135,16 +202,18 @@ export const stopNextcloud = async function () { | |
|
||
/** | ||
* Get the testing container's IP | ||
* | ||
* @param container name of the container | ||
*/ | ||
export const getContainerIP = async function ( | ||
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) { | ||
await container.inspect((_err, data) => { | ||
ip = data?.NetworkSettings?.IPAddress || '' | ||
}) | ||
|
||
|
@@ -163,40 +232,45 @@ export const getContainerIP = async function ( | |
// 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) { | ||
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 ( | ||
const runExec = async function( | ||
container: Docker.Container, | ||
command: string[], | ||
verbose: boolean = false | ||
verbose = false, | ||
user = 'www-data' | ||
) { | ||
const exec = await container.exec({ | ||
Cmd: command, | ||
AttachStdout: true, | ||
AttachStderr: true, | ||
User: 'www-data', | ||
User: user, | ||
}) | ||
|
||
return new Promise((resolve, reject) => { | ||
return new Promise<string>((resolve, reject) => { | ||
exec.start({}, (err, stream) => { | ||
if (stream) { | ||
stream.setEncoding('utf-8') | ||
stream.on('data', str => { | ||
if (verbose && str.trim() !== '') { | ||
console.log(`├─ ${str.trim().replace(/\n/gi, '\n├─ ')}`) | ||
const data = [] as string[] | ||
stream.setEncoding('utf8') | ||
stream.on('data', (str) => { | ||
data.push(str) | ||
const printable = str.replace(/\p{C}/gu, '').trim() | ||
if (verbose && printable !== '') { | ||
console.log(`├─ ${printable.replace(/\n/gi, '\n├─ ')}`) | ||
} | ||
}) | ||
stream.on('end', resolve) | ||
stream.on('end', () => resolve(data.join(''))) | ||
} else { | ||
reject(err) | ||
} | ||
}) | ||
}) | ||
} | ||
|
||
const sleep = function (milliseconds: number) { | ||
const sleep = function(milliseconds: number) { | ||
return new Promise((resolve) => setTimeout(resolve, milliseconds)) | ||
} | ||
|
Oops, something went wrong.