Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve usability of dockerode wrapper #486

Merged
merged 1 commit into from
Apr 12, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading