Skip to content

Commit

Permalink
feat(test-tooling): containers#logDiagnostics() utility method
Browse files Browse the repository at this point in the history
This helper is designed to be used by test cases to dump (hopefully)
useful information about the docker daemon of the CI runner in
the event of a test failure.

The idea is that we could potentially nail down the source of the the
flake we have with the Fabric end to end tests where the AIO container
fails to start due to unavailable ports.
Using this to dump information in the tests should at least get us
closer to the solution to stomping that flake at last.

Also fixed spelling errors that were introduced by a previous commit.
In the future these kind of issues will be made impossible by the
new PR merge constraint that we had just introduced (see the mailing
list post at the time of this writing)

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed May 27, 2021
1 parent 33fdd50 commit ed9e125
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 14 deletions.
5 changes: 5 additions & 0 deletions packages/cactus-test-tooling/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cactus-test-tooling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"keycloak-admin": "1.14.11",
"node-ssh": "11.1.1",
"p-retry": "4.4.0",
"run-time-error": "1.4.0",
"tar-stream": "2.1.2",
"typescript-optional": "2.0.1",
"web3": "1.2.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Dockerode from "dockerode";
import tar from "tar-stream";
import fs from "fs-extra";
import pRetry from "p-retry";
import { RuntimeError } from "run-time-error";
import { Streams } from "../common/streams";
import {
Checks,
Expand Down Expand Up @@ -50,7 +51,76 @@ export interface IPushFileFromFsOptions {
dstFileDir: string;
}

export interface IGetDiagnosticsRequest {
logLevel: LogLevelDesc;
dockerodeOptions?: Dockerode.DockerOptions;
}

export interface IGetDiagnosticsResponse {
readonly images: Dockerode.ImageInfo[];
readonly containers: Dockerode.ContainerInfo[];
readonly volumes: {
Volumes: Dockerode.VolumeInspectInfo[];
Warnings: string[];
};
readonly networks: unknown[];
readonly info: unknown;
readonly version: Dockerode.DockerVersion;
}

export class Containers {
/**
* Obtains container diagnostic information that is mainly meant to be useful
* in the event of a hard-to-debug test failure.
*/
static async getDiagnostics(
req: IGetDiagnosticsRequest,
): Promise<IGetDiagnosticsResponse> {
const log = LoggerProvider.getOrCreate({
label: "containers#get-diagnostics",
level: req.logLevel,
});

try {
const dockerode = new Dockerode(req.dockerodeOptions);
const images = await dockerode.listImages();
const containers = await dockerode.listContainers();
const volumes = await dockerode.listVolumes();
const networks = await dockerode.listNetworks();
const info = await dockerode.info();
const version = await dockerode.version();

const response: IGetDiagnosticsResponse = {
images,
containers,
volumes,
networks,
info,
version,
};
return response;
} catch (ex) {
log.error("Failed to get diagnostics of Docker daemon", ex);
throw new RuntimeError("Failed to get diagnostics of Docker daemon", ex);
}
}
/**
* Obtains container diagnostic information that is mainly meant to be useful
* in the event of a hard-to-debug test failure.
*/
static async logDiagnostics(
req: IGetDiagnosticsRequest,
): Promise<IGetDiagnosticsResponse> {
const log = LoggerProvider.getOrCreate({
label: "containers#log-diagnostics",
level: req.logLevel,
});

const response = await Containers.getDiagnostics(req);
log.info("ContainerDiagnostics=%o", JSON.stringify(response, null, 4));
return response;
}

/**
* Uploads a file from the local (host) file system to a container's file-system.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os from "os";
import path from "path";
import { IncomingMessage } from "http";
const tap = require("tap");
import test, { Test } from "tape-promise/tape";
import type { IncomingMessage } from "http";
import { v4 as uuidV4 } from "uuid";
import fs from "fs-extra";
import { Logger, LoggerProvider } from "@hyperledger/cactus-common";
Expand All @@ -13,13 +13,16 @@ import {
LoggerProvider.setLogLevel("DEBUG");
const log: Logger = LoggerProvider.getOrCreate({ label: "containers-test" });

tap.test("pushes file to container unharmed", async (assert: any) => {
test("pushes file to container unharmed", async (t: Test) => {
const anHttpEchoContainer = new HttpEchoContainer();
log.debug("Starting HttpEchoContainer...");
const container = await anHttpEchoContainer.start();
log.debug("Container started OK.");
assert.tearDown(() => anHttpEchoContainer.stop());
assert.tearDown(() => anHttpEchoContainer.destroy());

test.onFinish(async () => {
await anHttpEchoContainer.stop();
await anHttpEchoContainer.destroy();
});

const srcFileName = uuidV4();
const srcFileDir = os.tmpdir();
Expand All @@ -44,22 +47,79 @@ tap.test("pushes file to container unharmed", async (assert: any) => {
dstFileName,
});

assert.ok(res, "putArchive() Docker API response OK");
assert.ok(typeof res.statusCode === "number", "API response.statusCode OK");
t.ok(res, "putArchive() Docker API response OK");
t.ok(typeof res.statusCode === "number", "API response.statusCode OK");
const statusCode: number = res.statusCode as number;

assert.ok(statusCode > 199, "putArchive() API res.statusCode > 199");
assert.ok(statusCode < 300, "putArchive() API res.statusCode < 300");
assert.equal(res.statusMessage, "OK", "putArchive() res.statusMessage OK");
t.ok(statusCode > 199, "putArchive() API res.statusCode > 199");
t.ok(statusCode < 300, "putArchive() API res.statusCode < 300");
t.equal(res.statusMessage, "OK", "putArchive() res.statusMessage OK");

log.debug("Put file result: %o %o", res.statusCode, res.statusMessage);

const fileAsString2 = await Containers.pullFile(container, dstFilePath);
assert.ok(fileAsString2, "Read back file contents truthy");
t.ok(fileAsString2, "Read back file contents truthy");

const fileContents2 = JSON.parse(fileAsString2);
assert.ok(fileContents2, "Read back file JSON.parse() OK");
assert.equal(fileContents2.id, fileContents.id, "File UUIDs OK");
t.ok(fileContents2, "Read back file JSON.parse() OK");
t.equal(fileContents2.id, fileContents.id, "File UUIDs OK");

t.end();
});

test("Can obtain docker diagnostics info", async (t: Test) => {
const httpEchoContainer = new HttpEchoContainer();
test.onFinish(async () => {
await httpEchoContainer.stop();
await httpEchoContainer.destroy();
});
t.ok(httpEchoContainer, "httpEchoContainer truthy OK");
const container = await httpEchoContainer.start();
t.ok(container, "container truthy OK");

const diag = await Containers.getDiagnostics({ logLevel: "TRACE" });
t.ok(diag, "diag truthy OK");

t.ok(diag.containers, "diag.containers truthy OK");
t.ok(Array.isArray(diag.containers), "diag.containers is Array OK");
t.ok(diag.containers.length > 0, "diag.containers not empty array OK");

t.ok(diag.images, "diag.images truthy OK");
t.ok(diag.images.length > 0, "diag.images not empty array OK");
t.ok(Array.isArray(diag.images), "diag.images is Array OK");

t.ok(diag.info, "diag.info truthy OK");

t.ok(diag.networks, "diag.networks truthy OK");
t.ok(diag.networks.length > 0, "diag.networks not empty array OK");
t.ok(Array.isArray(diag.networks), "diag.networks is Array OK");

t.ok(diag.version, "diag.version truthy OK");

t.ok(diag.volumes, "diag.volumes truthy OK");
t.ok(diag.volumes.Volumes, "diag.volumes.Volumes truthy OK");
t.ok(Array.isArray(diag.volumes.Volumes), "diag.volumes.Volumes is Array OK");
t.end();
});

assert.end();
test("Can report error if docker daemon is not accessable", async (t: Test) => {
const badSocketPath = "/some-non-existent-path/to-make-it-trip-up/";
try {
await Containers.getDiagnostics({
logLevel: "TRACE",
// pass in an incorrect value for the port so that it fails for sure
dockerodeOptions: {
port: 9999,
socketPath: badSocketPath,
},
});
t.fail("Containers.getDiagnostics was supposed to fail but did not.");
} catch (ex) {
t.ok(ex, "exception thrown is truthy OK");
t.ok(ex.cause, "ex.cause truthy OK");
t.ok(ex.cause.message, "ex.cause.message truthy OK");
const causeMsgIsInformative = ex.cause.message.includes(badSocketPath);
t.true(causeMsgIsInformative, "causeMsgIsInformative");
}
t.end();
});

0 comments on commit ed9e125

Please sign in to comment.