Skip to content

Commit

Permalink
feat: Improve usability of dockerode wrapper
Browse files Browse the repository at this point in the history
* Export `getContainer` and `getContainerName` functions
  This can be used to e.g. create the `runOCC` cypress command
* Make the container name dependent on the current app to prevent issues when reusing containers
* Allow to pass options for container creation to the `startNextcloud` function
  * `forceRecreate` to not reuse any container but force creating a new one
  * `mounts` to allow binding other directories to the container (e.g. server config)
* Allow to expose a port

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Apr 12, 2024
1 parent ee2e462 commit 65f3597
Showing 1 changed file with 89 additions and 24 deletions.
113 changes: 89 additions & 24 deletions lib/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,84 @@
*
*/

import type { Container } from 'dockerode'
import type { Stream } from 'stream'

import Docker from 'dockerode'
import waitOn from 'wait-on'

import { type Stream, PassThrough } from 'stream'
import { join, resolve, sep } from 'path'
import { PassThrough } from 'stream'
import { basename, 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 VENDOR_APPS = {
text: 'https://github.com/nextcloud/text.git',
viewer: 'https://github.com/nextcloud/viewer.git',
notifications: 'https://github.com/nextcloud/notifications.git',
activity: 'https://github.com/nextcloud/activity.git',
}

export const docker = new Docker()

// Store the container name, different names are used to prevent conflicts when testing multiple apps locally
let _containerName: string|null = null
// Store latest server branch used, will be used for vendored apps
let _serverBranch = 'master'

/**
* Get the container name that is currently created and/or used by dockerode
*/
export const getContainerName = function(): string {
if (_containerName === null) {
const app = basename(process.cwd()).replace(' ', '')
_containerName = `nextcloud-cypress-tests_${app}`
}
return _containerName
}

/**
* Get the current container used
* Throws if not found
*/
export const getContainer = function(): Container {
return docker.getContainer(getContainerName())
}

interface StartOptions {
/**
* Force recreate the container even if an old one is found
* @default false
*/
forceRecreate?: boolean

/**
* Additional mounts to create on the container
* You can pass a mapping from server path (relative to Nextcloud root) to your local file system
* @example ```js
* { config: '/path/to/local/config' }
* ```
*/
mounts?: Record<string, string>

/**
* Optional port binding
* The default port (TCP 80) will be exposed to this host port
*/
exposePort?: number
}

/**
* 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)
* @param {string|undefined} branch server branch to use (default 'master')
* @param {boolean|string|undefined} mountApp bind mount app within server (`true` for autodetect, `false` to disable, or a string to force a path) (default true)
* @param {StartOptions|undefined} options Optional parameters to configre the container creation
* @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 = 'master', mountApp: boolean|string = true): Promise<string> {
export async function startNextcloud(branch = 'master', mountApp: boolean|string = true, options: StartOptions = {}): Promise<string> {
let appPath = mountApp === true ? process.cwd() : mountApp
let appId: string|undefined
let appVersion: string|undefined
Expand Down Expand Up @@ -81,7 +127,7 @@ export const startNextcloud = async function(branch = 'master', mountApp: boolea

try {
// Pulling images
console.log('Pulling images... ⏳')
console.log('Pulling images ⏳')
await new Promise((resolve, reject) => docker.pull(SERVER_IMAGE, (_err, stream: Stream) => {
const onFinished = function(err: Error | null) {
if (!err) {
Expand All @@ -95,17 +141,19 @@ export const startNextcloud = async function(branch = 'master', mountApp: boolea
console.log('└─ Done')

// Getting latest image
console.log('\nChecking running containers... 🔍')
console.log('\nChecking running containers 🔍')
const localImage = await docker.listImages({ filters: `{"reference": ["${SERVER_IMAGE}"]}` })

// Remove old container if exists and not initialized by us
try {
const oldContainer = docker.getContainer(CONTAINER_NAME)
const oldContainer = getContainer()
const oldContainerData = await oldContainer.inspect()
if (oldContainerData.State.Running) {
console.log('├─ Existing running container found')
if (localImage[0].Id !== oldContainerData.Image) {
console.log('└─ But running container is outdated, replacing...')
if (options.forceRecreate === true) {
console.log('└─ Forced recreation of container was enabled, removing…')
} else if (localImage[0].Id !== oldContainerData.Image) {
console.log('└─ But running container is outdated, replacing…')
} else {
// Get container's IP
console.log('├─ Reusing that container')
Expand All @@ -122,14 +170,30 @@ export const startNextcloud = async function(branch = 'master', mountApp: boolea
}

// Starting container
console.log('\nStarting Nextcloud container... 🚀')
console.log('\nStarting Nextcloud container 🚀')
console.log(`├─ Using branch '${branch}'`)

const mounts: string[] = []
if (appPath !== false) {
mounts.push(`${appPath}:/var/www/html/apps/${appId}:ro`)
}
Object.entries(options.mounts ?? {})
.forEach(([server, local]) => mounts.push(`${local}:/var/www/html/${server}:ro`))

const PortBindings = !options.exposePort ? undefined : {
'80/tcp': [{
HostIP: '0.0.0.0',
HostPort: options.exposePort.toString(),
}],
}

const container = await docker.createContainer({
Image: SERVER_IMAGE,
name: CONTAINER_NAME,
name: getContainerName(),
Env: [`BRANCH=${branch}`],
HostConfig: {
Binds: appPath !== false ? [`${appPath}:/var/www/html/apps/${appId}`] : undefined,
Binds: mounts.length > 0 ? mounts : undefined,
PortBindings,
},
})
await container.start()
Expand All @@ -154,12 +218,13 @@ export const startNextcloud = async function(branch = 'master', mountApp: boolea
*
* @param {string[]} apps List of default apps to install (default is ['viewer'])
* @param {string|undefined} vendoredBranch The branch used for vendored apps, should match server (defaults to latest branch used for `startNextcloud` or fallsback to `master`)
* @param {Container|undefined} container Optional server container to use (defaults to current container)
*/
export const configureNextcloud = async function(apps = ['viewer'], vendoredBranch?: string) {
export const configureNextcloud = async function(apps = ['viewer'], vendoredBranch?: string, container?: Container) {
vendoredBranch = vendoredBranch || _serverBranch

console.log('\nConfiguring nextcloud...')
const container = docker.getContainer(CONTAINER_NAME)
console.log('\nConfiguring Nextcloud…')
container = container ?? getContainer()
await runExec(container, ['php', 'occ', '--version'], true)

// Be consistent for screenshots
Expand Down Expand Up @@ -200,8 +265,8 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran
*/
export const stopNextcloud = async function() {
try {
const container = docker.getContainer(CONTAINER_NAME)
console.log('Stopping Nextcloud container...')
const container = getContainer()
console.log('Stopping Nextcloud container')
container.remove({ force: true })
console.log('└─ Nextcloud container removed 🥀')
} catch (err) {
Expand All @@ -215,7 +280,7 @@ export const stopNextcloud = async function() {
* @param container name of the container
*/
export const getContainerIP = async function(
container = docker.getContainer(CONTAINER_NAME)
container = getContainer()
): Promise<string> {
let ip = ''
let tries = 0
Expand All @@ -242,7 +307,7 @@ export const getContainerIP = async function(
// 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... ⏳')
console.log('├─ Waiting for Nextcloud to be ready ⏳')
await waitOn({ resources: [`http://${ip}/index.php`] })
console.log('└─ Done')
}
Expand Down

0 comments on commit 65f3597

Please sign in to comment.