Skip to content

Commit 65f3597

Browse files
committed
feat: Improve usability of dockerode wrapper
* 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 <[email protected]>
1 parent ee2e462 commit 65f3597

File tree

1 file changed

+89
-24
lines changed

1 file changed

+89
-24
lines changed

lib/docker.ts

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,84 @@
2121
*
2222
*/
2323

24+
import type { Container } from 'dockerode'
25+
import type { Stream } from 'stream'
26+
2427
import Docker from 'dockerode'
2528
import waitOn from 'wait-on'
2629

27-
import { type Stream, PassThrough } from 'stream'
28-
import { join, resolve, sep } from 'path'
30+
import { PassThrough } from 'stream'
31+
import { basename, join, resolve, sep } from 'path'
2932
import { existsSync, readFileSync } from 'fs'
3033
import { XMLParser } from 'fast-xml-parser'
3134

32-
export const docker = new Docker()
33-
34-
const CONTAINER_NAME = 'nextcloud-cypress-tests'
3535
const SERVER_IMAGE = 'ghcr.io/nextcloud/continuous-integration-shallow-server'
36-
3736
const VENDOR_APPS = {
3837
text: 'https://github.com/nextcloud/text.git',
3938
viewer: 'https://github.com/nextcloud/viewer.git',
4039
notifications: 'https://github.com/nextcloud/notifications.git',
4140
activity: 'https://github.com/nextcloud/activity.git',
4241
}
4342

43+
export const docker = new Docker()
44+
45+
// Store the container name, different names are used to prevent conflicts when testing multiple apps locally
46+
let _containerName: string|null = null
4447
// Store latest server branch used, will be used for vendored apps
4548
let _serverBranch = 'master'
4649

50+
/**
51+
* Get the container name that is currently created and/or used by dockerode
52+
*/
53+
export const getContainerName = function(): string {
54+
if (_containerName === null) {
55+
const app = basename(process.cwd()).replace(' ', '')
56+
_containerName = `nextcloud-cypress-tests_${app}`
57+
}
58+
return _containerName
59+
}
60+
61+
/**
62+
* Get the current container used
63+
* Throws if not found
64+
*/
65+
export const getContainer = function(): Container {
66+
return docker.getContainer(getContainerName())
67+
}
68+
69+
interface StartOptions {
70+
/**
71+
* Force recreate the container even if an old one is found
72+
* @default false
73+
*/
74+
forceRecreate?: boolean
75+
76+
/**
77+
* Additional mounts to create on the container
78+
* You can pass a mapping from server path (relative to Nextcloud root) to your local file system
79+
* @example ```js
80+
* { config: '/path/to/local/config' }
81+
* ```
82+
*/
83+
mounts?: Record<string, string>
84+
85+
/**
86+
* Optional port binding
87+
* The default port (TCP 80) will be exposed to this host port
88+
*/
89+
exposePort?: number
90+
}
91+
4792
/**
4893
* Start the testing container
4994
*
50-
* @param branch server branch to use
51-
* @param mountApp bind mount app within server (`true` for autodetect, `false` to disable, or a string to force a path)
95+
* @param {string|undefined} branch server branch to use (default 'master')
96+
* @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)
97+
* @param {StartOptions|undefined} options Optional parameters to configre the container creation
5298
* @return Promise resolving to the IP address of the server
5399
* @throws {Error} If Nextcloud container could not be started
54100
*/
55-
export const startNextcloud = async function(branch = 'master', mountApp: boolean|string = true): Promise<string> {
101+
export async function startNextcloud(branch = 'master', mountApp: boolean|string = true, options: StartOptions = {}): Promise<string> {
56102
let appPath = mountApp === true ? process.cwd() : mountApp
57103
let appId: string|undefined
58104
let appVersion: string|undefined
@@ -81,7 +127,7 @@ export const startNextcloud = async function(branch = 'master', mountApp: boolea
81127

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

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

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

124172
// Starting container
125-
console.log('\nStarting Nextcloud container... 🚀')
173+
console.log('\nStarting Nextcloud container 🚀')
126174
console.log(`├─ Using branch '${branch}'`)
175+
176+
const mounts: string[] = []
177+
if (appPath !== false) {
178+
mounts.push(`${appPath}:/var/www/html/apps/${appId}:ro`)
179+
}
180+
Object.entries(options.mounts ?? {})
181+
.forEach(([server, local]) => mounts.push(`${local}:/var/www/html/${server}:ro`))
182+
183+
const PortBindings = !options.exposePort ? undefined : {
184+
'80/tcp': [{
185+
HostIP: '0.0.0.0',
186+
HostPort: options.exposePort.toString(),
187+
}],
188+
}
189+
127190
const container = await docker.createContainer({
128191
Image: SERVER_IMAGE,
129-
name: CONTAINER_NAME,
192+
name: getContainerName(),
130193
Env: [`BRANCH=${branch}`],
131194
HostConfig: {
132-
Binds: appPath !== false ? [`${appPath}:/var/www/html/apps/${appId}`] : undefined,
195+
Binds: mounts.length > 0 ? mounts : undefined,
196+
PortBindings,
133197
},
134198
})
135199
await container.start()
@@ -154,12 +218,13 @@ export const startNextcloud = async function(branch = 'master', mountApp: boolea
154218
*
155219
* @param {string[]} apps List of default apps to install (default is ['viewer'])
156220
* @param {string|undefined} vendoredBranch The branch used for vendored apps, should match server (defaults to latest branch used for `startNextcloud` or fallsback to `master`)
221+
* @param {Container|undefined} container Optional server container to use (defaults to current container)
157222
*/
158-
export const configureNextcloud = async function(apps = ['viewer'], vendoredBranch?: string) {
223+
export const configureNextcloud = async function(apps = ['viewer'], vendoredBranch?: string, container?: Container) {
159224
vendoredBranch = vendoredBranch || _serverBranch
160225

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

165230
// Be consistent for screenshots
@@ -200,8 +265,8 @@ export const configureNextcloud = async function(apps = ['viewer'], vendoredBran
200265
*/
201266
export const stopNextcloud = async function() {
202267
try {
203-
const container = docker.getContainer(CONTAINER_NAME)
204-
console.log('Stopping Nextcloud container...')
268+
const container = getContainer()
269+
console.log('Stopping Nextcloud container')
205270
container.remove({ force: true })
206271
console.log('└─ Nextcloud container removed 🥀')
207272
} catch (err) {
@@ -215,7 +280,7 @@ export const stopNextcloud = async function() {
215280
* @param container name of the container
216281
*/
217282
export const getContainerIP = async function(
218-
container = docker.getContainer(CONTAINER_NAME)
283+
container = getContainer()
219284
): Promise<string> {
220285
let ip = ''
221286
let tries = 0
@@ -242,7 +307,7 @@ export const getContainerIP = async function(
242307
// We need to make sure the server is already running before cypress
243308
// https://github.com/cypress-io/cypress/issues/22676
244309
export const waitOnNextcloud = async function(ip: string) {
245-
console.log('├─ Waiting for Nextcloud to be ready... ⏳')
310+
console.log('├─ Waiting for Nextcloud to be ready ⏳')
246311
await waitOn({ resources: [`http://${ip}/index.php`] })
247312
console.log('└─ Done')
248313
}

0 commit comments

Comments
 (0)