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 { performance } from "./performance";
|
||||||
import { synapseDocker } from "./synapsedocker";
|
import { synapseDocker } from "./synapsedocker";
|
||||||
import { webserver } from "./webserver";
|
import { webserver } from "./webserver";
|
||||||
|
import { docker } from "./docker";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Cypress.PluginConfig}
|
* @type {Cypress.PluginConfig}
|
||||||
*/
|
*/
|
||||||
export default function(on: PluginEvents, config: PluginConfigOptions) {
|
export default function(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
|
docker(on, config);
|
||||||
performance(on, config);
|
performance(on, config);
|
||||||
synapseDocker(on, config);
|
synapseDocker(on, config);
|
||||||
webserver(on, config);
|
webserver(on, config);
|
||||||
|
|
|
@ -19,12 +19,12 @@ limitations under the License.
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as childProcess from "child_process";
|
|
||||||
import * as fse from "fs-extra";
|
import * as fse from "fs-extra";
|
||||||
import * as net from "net";
|
|
||||||
|
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
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
|
// A cypress plugins to add command to start & stop synapses in
|
||||||
// docker with preset templates.
|
// docker with preset templates.
|
||||||
|
@ -47,16 +47,6 @@ function randB64Bytes(numBytes: number): string {
|
||||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
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> {
|
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||||
const templateDir = path.join(__dirname, "templates", template);
|
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}...`);
|
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||||
|
|
||||||
const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`;
|
const synapseId = await dockerRun({
|
||||||
const userInfo = os.userInfo();
|
image: "matrixdotorg/synapse:develop",
|
||||||
|
containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`,
|
||||||
let userParams: string[] = [];
|
params: [
|
||||||
if (userInfo.uid >= 0) {
|
"--rm",
|
||||||
// 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",
|
|
||||||
"-v", `${synCfg.configDir}:/data`,
|
"-v", `${synCfg.configDir}:/data`,
|
||||||
"-p", `${synCfg.port}:8008/tcp`,
|
"-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}.`);
|
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||||
|
|
||||||
// Await Synapse healthcheck
|
// Await Synapse healthcheck
|
||||||
await new Promise<void>((resolve, reject) => {
|
await dockerExec({
|
||||||
childProcess.execFile("docker", [
|
containerId: synapseId,
|
||||||
"exec", synapseId,
|
params: [
|
||||||
"curl",
|
"curl",
|
||||||
"--connect-timeout", "30",
|
"--connect-timeout", "30",
|
||||||
"--retry", "30",
|
"--retry", "30",
|
||||||
|
@ -147,10 +122,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
"--retry-all-errors",
|
"--retry-all-errors",
|
||||||
"--silent",
|
"--silent",
|
||||||
"http://localhost:8008/health",
|
"http://localhost:8008/health",
|
||||||
], { encoding: 'utf8' }, (err, stdout) => {
|
],
|
||||||
if (err) reject(err);
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const synapse: SynapseInstance = { synapseId, ...synCfg };
|
const synapse: SynapseInstance = { synapseId, ...synCfg };
|
||||||
|
@ -163,43 +135,18 @@ async function synapseStop(id: string): Promise<void> {
|
||||||
|
|
||||||
if (!synCfg) throw new Error("Unknown synapse ID");
|
if (!synCfg) throw new Error("Unknown synapse ID");
|
||||||
|
|
||||||
try {
|
const synapseLogsPath = path.join("cypress", "synapselogs", id);
|
||||||
const synapseLogsPath = path.join("cypress", "synapselogs", id);
|
await fse.ensureDir(synapseLogsPath);
|
||||||
await fse.ensureDir(synapseLogsPath);
|
|
||||||
|
|
||||||
const stdoutFile = await fse.open(path.join(synapseLogsPath, "stdout.log"), "w");
|
await dockerLogs({
|
||||||
const stderrFile = await fse.open(path.join(synapseLogsPath, "stderr.log"), "w");
|
containerId: id,
|
||||||
await new Promise<void>((resolve, reject) => {
|
stdoutFile: path.join(synapseLogsPath, "stdout.log"),
|
||||||
childProcess.spawn('docker', [
|
stderrFile: path.join(synapseLogsPath, "stderr.log"),
|
||||||
"logs",
|
});
|
||||||
id,
|
|
||||||
], {
|
|
||||||
stdio: ["ignore", stdoutFile, stderrFile],
|
|
||||||
}).once('close', resolve);
|
|
||||||
});
|
|
||||||
await fse.close(stdoutFile);
|
|
||||||
await fse.close(stderrFile);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await dockerStop({
|
||||||
childProcess.execFile('docker', [
|
containerId: id,
|
||||||
"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 fse.remove(synCfg.configDir);
|
await fse.remove(synCfg.configDir);
|
||||||
|
|
||||||
|
@ -207,7 +154,7 @@ async function synapseStop(id: string): Promise<void> {
|
||||||
|
|
||||||
console.log(`Stopped synapse id ${id}.`);
|
console.log(`Stopped synapse id ${id}.`);
|
||||||
// cypress deliberately fails if you return 'undefined', so
|
// 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;
|
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