Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HMS-5523: Add Container & sub-man/rhc client support #5

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/stageTestAction.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ jobs:
echo "PROXY=$PROXY" >> .env
echo "TOKEN=apple" >> .env
echo "CI=true" >> .env

echo "ORG_ID_1=$STAGE_ORG_ID" >> .env
echo "ACTIVATION_KEY_1=$STAGE_ACTIVATION_KEY" >> .env
echo "DOCKER_SOCKET=/var/run/docker.sock" >> .env
- name: Set up Node.js
uses: actions/setup-node@v2
with:
Expand Down
2 changes: 2 additions & 0 deletions buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ phases:
install:
commands:
- echo Entered the install phase...
- nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 &
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
finally:
- echo This always runs even if the update or install command fails
pre_build:
Expand Down
6 changes: 6 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@

USER1USERNAME="contentPlaywrightUserAdmin" # Required
USER1PASSWORD="" # Required (Ask Andrew if needed)
ORG_ID_1="1234" #org id to register for registration tests
ACTIVATION_KEY_1="MyKey" #activation Key used for testing

PROD=""
BASE_URL="https://stage.foo.redhat.com:1337" # Required
PROXY="https://something.foo.redhat.com:5432" # Required in CI only or if locally running against stage
CI="" # This is set to true for CI jobs, if checking for CI do !!process.env.CI
TOKEN="" # This is handled programmatically.

#DOCKER_SOCKET="/tmp/podman.sock"
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"dayjs": "^1.11.13"
},
"dependencies": {
"@types/dockerode": "^3.3.34",
"dockerode": "^4.0.4",
"dotenv": "^16.4.7",
"github-actions-ctrf": "^0.0.58",
"playwright-ctrf-json-reporter": "^0.0.18"
Expand Down
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ This stores your local credentials

yarn get-tests

# Setup container api for testing with clients

## Podman

As your user, run podman to serve the api:
```
podman system service -t 0 /tmp/podman.sock
```

Uncomment the DOCKER_SOCKET option in the .env file:
```
DOCKER_SOCKET="/tmp/podman.sock"
```

## Docker

* ensure the docker service is running
* ensure your user is part of the 'docker' user group


# Option 1 Run local:

For local testing, make sure your front-end/backend servers are running and accessible, then:
Expand Down
29 changes: 29 additions & 0 deletions tests/helpers/containers.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
killContainer,
runCommand,
startNewContainer,
} from "../helpers/containers";
import { test, expect } from "@playwright/test";

test("Test container", async ({}) => {
await startNewContainer(
"my_container",
"quay.io/jlsherri/client-rhel9:latest"
);

const stream = await runCommand("my_container", ["ls", "-l"]);
if (stream != undefined) {
console.log(stream.stdout);
console.log(stream.stderr);
console.log(stream.exitCode);
}

const stream2 = await runCommand("my_container", ["ls", "-z"]);
if (stream2 != undefined) {
console.log(stream2.stdout);
console.log(stream2.stderr);
console.log(stream2.exitCode);
}

await killContainer("my_container");
});
214 changes: 214 additions & 0 deletions tests/helpers/containers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { time } from "console";
import Dockerode, { Container } from "dockerode";
import { PassThrough } from "stream";
import { finished } from "stream/promises";

var Docker = require("dockerode");

const util = require("util");
const exec = util.promisify(require("child_process").exec);

const docker = (): Dockerode => {
return new Docker({ socketPath: process.env.DOCKER_SOCKET! });
};

/**
* starts a container, killing the existing one if its present
*
* @param containerName customizable name ("my_container)" to give to your container. Could be the test name, or something linking it to the test.
* @param imageName full image name and tag: "localhost/my_image:latest"
*/
export const startNewContainer = async (
containerName: string,
imageName: string
) => {
await killContainer(containerName);
await startContainer(containerName, imageName);
};

async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
* Starts a container, should not already be running, image should already be pulled
* @param containerName customizable name ("my_container)" to give to your container. Could be the test name, or something linking it to the test.
* @param imageName full image name and tag: "localhost/my_image:latest"
* @returns
*/
const startContainer = async (containerName: string, imageName: string) => {
await pullImage(imageName);
console.log("starting container " + containerName);
const container = await docker().createContainer({
Image: imageName,
name: containerName,
HostConfig: {
Privileged: true,
},
});
return container?.start();
};

/**
* Pulls an image and waits for it to finish, up to 5 seconds
*
* @param imageName the full image name (localhost/my-image:latest)
* @param retryCount number of times to retry the pull until its successful (defaults to 3)
* @param waitTime amount of time to wait for each pull
*/
const pullImage = async (
imageName: string,
retryCount?: number,
waitTime?: number
) => {
var sleepTime = waitTime || 10000;
if (retryCount == 0) {
return;
}
await docker().pull(imageName);
while (sleepTime > 0) {
if (await imagePresent(imageName)) {
return true;
}
await sleep(1000);
sleepTime -= 1000;
}
await pullImage(imageName, (retryCount || 3) - 1, waitTime);
};

const imagePresent = async (imageName: string): Promise<boolean> => {
const images = await docker().listImages();
for (var image of images) {
if ((image.RepoTags ? image.RepoTags : []).includes(imageName)) {
return true;
}
}
return false;
};

const waitForContainer = async (name: string): Promise<Container | void> => {
var container = await getContainer(name);
var waited = 10;
while (container == undefined && waited > 0) {
waited -= 1;
await sleep(500);
container = await getContainer(name);
}
return container;
};

const waitForContainerRunning = async (
name: string
): Promise<Container | void> => {
var container = await getContainerInfo(name);
var waited = 10;
while (container?.State !== "running" && waited > 0) {
waited -= 1;
await sleep(500);
container = await getContainerInfo(name);
}
};

const getContainerInfo = async (name: string) => {
const containers = await docker().listContainers({ all: true });

for (var contInfo of containers) {
if (contInfo.Names.includes("/" + name)) {
return contInfo;
}
}
return undefined;
};

const getContainer = async (name: string): Promise<Container | void> => {
const cInfo = await getContainerInfo(name);
if (cInfo !== undefined) {
return docker().getContainer(cInfo.Id);
}
};

/**
* Kills the running container and deletes it
* @param containerName the user provided container to kill
* @returns
*/
export const killContainer = async (containerName: string) => {
const info = await getContainerInfo(containerName);
const c = await getContainer(containerName);
if (info?.State == "running") {
await c?.kill();
}
await c?.remove();
};

export interface ExecReturn {
stdout?: string;
stderr?: string;
exitCode?: number | null;
}

//
// Defaults to a 500 ms timeout unless timeout is specified

/**
* Runs a non-interactive command and returns stdout, stderr, and the exit code
*
* @param containerName the human readable container name to execute the command
* @param command the command to execute
* @param timeout_ms timeout (in milliseconds) the command should execute in (defaults to 500ms)
* @returns ExecReturn containing stdout, stderr, exitCode
*/
export const runCommand = async (
containerName: string,
command: string[],
timeout_ms?: number
): Promise<ExecReturn | void> => {
console.log("Running " + command + " on " + containerName);

const controller = new AbortController();
const signal = controller.signal;

const timeout = setTimeout(() => {
console.error("Timeout reached for command (" + command + ")");
controller.abort();
}, timeout_ms || 500);

const c = await getContainer(containerName);
const exec = await c?.exec({
Cmd: command,
AttachStdout: true,
AttachStderr: true,
Privileged: true,
abortSignal: signal,
});
if (exec == undefined) {
return undefined;
}

const execStream = await exec?.start({
abortSignal: signal,
});

clearTimeout(timeout);
if (execStream == undefined) {
return undefined;
}

const stdoutStream = new PassThrough();
const stderrStream = new PassThrough();

docker().modem.demuxStream(execStream, stdoutStream, stderrStream);

execStream.resume();
await finished(execStream);

const stderr = stderrStream.read() as Buffer | undefined;
const stdout = stdoutStream.read() as Buffer | undefined;
const execInfo = await exec.inspect();

return {
exitCode: execInfo.ExitCode,
stderr: stderr?.toString(),
stdout: stdout?.toString(),
};
};
3 changes: 2 additions & 1 deletion tests/helpers/loginHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export const throwIfMissingEnvVariables = () => {
"USER1USERNAME",
"USER1PASSWORD",
"BASE_URL",
"PROXY",
"ORG_ID_1",
"ACTIVATION_KEY_1",
];

if (!process.env.PROD) ManditoryEnvVariables.push("PROXY");
Expand Down
38 changes: 38 additions & 0 deletions tests/helpers/rhsmClient.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from "@playwright/test";
import { RHSMClient } from "./rhsmClient";

test("RHSM client", async ({}, testInfo) => {
// change the test timeout as registering a client can be slow
testInfo.setTimeout(5 * 60 * 1000); // Five minutes

// Create a client with a test-specific name
const client = new RHSMClient("RHSMClientTest");

// Start the rhel9 container
await client.Boot("rhel9");

// Register, overriding the default key and org
const reg = await client.RegisterSubMan("my_activation_key", "my_org_id");
if (reg?.exitCode != 0) {
console.log(reg?.stdout);
console.log(reg?.stderr);
}
expect(reg?.exitCode).toBe(0);

// vim-enhanced shouldn't be installed
const notExist = await client.Exec(["rpm", "-q", "vim-enhanced"]);
expect(notExist?.exitCode).not.toBe(0);

// Install vim-enhanced, expect it to finish in 60 seconds
const yumInstall = await client.Exec(
["yum", "install", "-y", "vim-enhanced"],
60000
);
expect(yumInstall?.exitCode).toBe(0);

// Now vim-enhanced should be installed
const exist = await client.Exec(["rpm", "-q", "vim-enhanced"]);
expect(exist?.exitCode).toBe(0);

await client.Destroy();
});
Loading