Extract dockery bits from Cypress' synapsedocker plugin (#9103)
* Extract dockery bits from Cypress' synapsedocker plugin * Update cypress/plugins/docker/index.ts Co-authored-by: David Baker <dbkr@users.noreply.github.com> Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
parent
8383148373
commit
ca1d9729fd
4 changed files with 183 additions and 76 deletions
131
cypress/plugins/docker/index.ts
Normal file
131
cypress/plugins/docker/index.ts
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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<string> {
|
||||
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<string>((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<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
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<void>((resolve) => {
|
||||
childProcess.spawn("docker", [
|
||||
"logs",
|
||||
args.containerId,
|
||||
], {
|
||||
stdio: ["ignore", stdoutFile, stderrFile],
|
||||
}).once('close', resolve);
|
||||
});
|
||||
|
||||
if (args.stdoutFile) await fse.close(<number>stdoutFile);
|
||||
if (args.stderrFile) await fse.close(<number>stderrFile);
|
||||
}
|
||||
|
||||
export function dockerStop(args: {
|
||||
containerId: string;
|
||||
}): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"stop",
|
||||
args.containerId,
|
||||
], err => {
|
||||
if (err) reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dockerRm(args: {
|
||||
containerId: string;
|
||||
}): Promise<void> {
|
||||
return new Promise<void>((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,
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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<number> {
|
||||
return new Promise<number>(resolve => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = (<net.AddressInfo>srv.address()).port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||
const templateDir = path.join(__dirname, "templates", template);
|
||||
|
||||
|
@ -109,37 +99,22 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
|||
|
||||
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<string>((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<void>((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<SynapseInstance> {
|
|||
"--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<void> {
|
|||
|
||||
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<void>((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<void>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"stop",
|
||||
id,
|
||||
], err => {
|
||||
if (err) reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
await new Promise<void>((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<void> {
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
27
cypress/plugins/utils/port.ts
Normal file
27
cypress/plugins/utils/port.ts
Normal file
|
@ -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<number> {
|
||||
return new Promise<number>(resolve => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = (<net.AddressInfo>srv.address()).port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue