diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts new file mode 100644 index 0000000000..2f3c646408 --- /dev/null +++ b/cypress/plugins/docker/index.ts @@ -0,0 +1,131 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import * as os from "os"; +import * as childProcess from "child_process"; +import * as fse from "fs-extra"; + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; + +// A cypress plugin to run docker commands + +export function dockerRun(args: { + image: string; + containerName: string; + params?: string[]; +}): Promise { + const userInfo = os.userInfo(); + const params = args.params ?? []; + + if (userInfo.uid >= 0) { + // On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult + params.push("-u", `${userInfo.uid}:${userInfo.gid}`); + } + + return new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "run", + "--name", args.containerName, + "-d", + ...params, + args.image, + "run", + ], (err, stdout) => { + if (err) reject(err); + resolve(stdout.trim()); + }); + }); +} + +export function dockerExec(args: { + containerId: string; + params: string[]; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile("docker", [ + "exec", args.containerId, + ...args.params, + ], { encoding: 'utf8' }, err => { + if (err) reject(err); + else resolve(); + }); + }); +} + +export async function dockerLogs(args: { + containerId: string; + stdoutFile?: string; + stderrFile?: string; +}): Promise { + const stdoutFile = args.stdoutFile ? await fse.open(args.stdoutFile, "w") : "ignore"; + const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore"; + + await new Promise((resolve) => { + childProcess.spawn("docker", [ + "logs", + args.containerId, + ], { + stdio: ["ignore", stdoutFile, stderrFile], + }).once('close', resolve); + }); + + if (args.stdoutFile) await fse.close(stdoutFile); + if (args.stderrFile) await fse.close(stderrFile); +} + +export function dockerStop(args: { + containerId: string; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "stop", + args.containerId, + ], err => { + if (err) reject(err); + resolve(); + }); + }); +} + +export function dockerRm(args: { + containerId: string; +}): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "rm", + args.containerId, + ], err => { + if (err) reject(err); + resolve(); + }); + }); +} + +/** + * @type {Cypress.PluginConfig} + */ +export function docker(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + dockerRun, + dockerExec, + dockerLogs, + dockerStop, + dockerRm, + }); +} diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index bc62efb03f..8a22b5cb55 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -21,11 +21,13 @@ import PluginConfigOptions = Cypress.PluginConfigOptions; import { performance } from "./performance"; import { synapseDocker } from "./synapsedocker"; import { webserver } from "./webserver"; +import { docker } from "./docker"; /** * @type {Cypress.PluginConfig} */ export default function(on: PluginEvents, config: PluginConfigOptions) { + docker(on, config); performance(on, config); synapseDocker(on, config); webserver(on, config); diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 42241ecc7d..5227b5e4ac 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -19,12 +19,12 @@ limitations under the License. import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; -import * as childProcess from "child_process"; import * as fse from "fs-extra"; -import * as net from "net"; import PluginEvents = Cypress.PluginEvents; import PluginConfigOptions = Cypress.PluginConfigOptions; +import { getFreePort } from "../utils/port"; +import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; // A cypress plugins to add command to start & stop synapses in // docker with preset templates. @@ -47,16 +47,6 @@ function randB64Bytes(numBytes: number): string { return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); } -async function getFreePort(): Promise { - return new Promise(resolve => { - const srv = net.createServer(); - srv.listen(0, () => { - const port = (srv.address()).port; - srv.close(() => resolve(port)); - }); - }); -} - async function cfgDirFromTemplate(template: string): Promise { const templateDir = path.join(__dirname, "templates", template); @@ -109,37 +99,22 @@ async function synapseStart(template: string): Promise { console.log(`Starting synapse with config dir ${synCfg.configDir}...`); - const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`; - const userInfo = os.userInfo(); - - let userParams: string[] = []; - if (userInfo.uid >= 0) { - // On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult - userParams = ["-u", `${userInfo.uid}:${userInfo.gid}`]; - } - - const synapseId = await new Promise((resolve, reject) => { - childProcess.execFile('docker', [ - "run", - "--name", containerName, - "-d", + const synapseId = await dockerRun({ + image: "matrixdotorg/synapse:develop", + containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`, + params: [ + "--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`, - ...userParams, - "matrixdotorg/synapse:develop", - "run", - ], (err, stdout) => { - if (err) reject(err); - resolve(stdout.trim()); - }); + ], }); console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`); // Await Synapse healthcheck - await new Promise((resolve, reject) => { - childProcess.execFile("docker", [ - "exec", synapseId, + await dockerExec({ + containerId: synapseId, + params: [ "curl", "--connect-timeout", "30", "--retry", "30", @@ -147,10 +122,7 @@ async function synapseStart(template: string): Promise { "--retry-all-errors", "--silent", "http://localhost:8008/health", - ], { encoding: 'utf8' }, (err, stdout) => { - if (err) reject(err); - else resolve(); - }); + ], }); const synapse: SynapseInstance = { synapseId, ...synCfg }; @@ -163,43 +135,18 @@ async function synapseStop(id: string): Promise { if (!synCfg) throw new Error("Unknown synapse ID"); - try { - const synapseLogsPath = path.join("cypress", "synapselogs", id); - await fse.ensureDir(synapseLogsPath); + const synapseLogsPath = path.join("cypress", "synapselogs", id); + await fse.ensureDir(synapseLogsPath); - const stdoutFile = await fse.open(path.join(synapseLogsPath, "stdout.log"), "w"); - const stderrFile = await fse.open(path.join(synapseLogsPath, "stderr.log"), "w"); - await new Promise((resolve, reject) => { - childProcess.spawn('docker', [ - "logs", - id, - ], { - stdio: ["ignore", stdoutFile, stderrFile], - }).once('close', resolve); - }); - await fse.close(stdoutFile); - await fse.close(stderrFile); + await dockerLogs({ + containerId: id, + stdoutFile: path.join(synapseLogsPath, "stdout.log"), + stderrFile: path.join(synapseLogsPath, "stderr.log"), + }); - await new Promise((resolve, reject) => { - childProcess.execFile('docker', [ - "stop", - id, - ], err => { - if (err) reject(err); - resolve(); - }); - }); - } finally { - await new Promise((resolve, reject) => { - childProcess.execFile('docker', [ - "rm", - id, - ], err => { - if (err) reject(err); - resolve(); - }); - }); - } + await dockerStop({ + containerId: id, + }); await fse.remove(synCfg.configDir); @@ -207,7 +154,7 @@ async function synapseStop(id: string): Promise { console.log(`Stopped synapse id ${id}.`); // cypress deliberately fails if you return 'undefined', so - // return null to signal all is well and we've handled the task. + // return null to signal all is well, and we've handled the task. return null; } diff --git a/cypress/plugins/utils/port.ts b/cypress/plugins/utils/port.ts new file mode 100644 index 0000000000..064ccc7cf9 --- /dev/null +++ b/cypress/plugins/utils/port.ts @@ -0,0 +1,27 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as net from "net"; + +export async function getFreePort(): Promise { + return new Promise(resolve => { + const srv = net.createServer(); + srv.listen(0, () => { + const port = (srv.address()).port; + srv.close(() => resolve(port)); + }); + }); +}