-
Notifications
You must be signed in to change notification settings - Fork 71
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
RHOAIENG-17695: chore(ci): create a test for calling oc version
in the test, which can be run with ci testing
#829
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import pathlib | ||
|
||
PROJECT_ROOT = pathlib.Path(__file__).parent.parent | ||
|
||
__all__ = [ | ||
PROJECT_ROOT, | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
import pathlib | ||
import tempfile | ||
from typing import TYPE_CHECKING | ||
|
||
import testcontainers.core.container | ||
import testcontainers.core.waiting_utils | ||
|
||
from tests.containers import docker_utils | ||
|
||
logging.basicConfig(level=logging.DEBUG) | ||
LOGGER = logging.getLogger(__name__) | ||
|
||
if TYPE_CHECKING: | ||
import pytest_subtests | ||
|
||
|
||
class TestBaseImage: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jstourac wondered how to structure the tests when more are to be added, like when the version tests are rewritten from makefile to here |
||
"""Tests that are applicable for all images we have in this repository.""" | ||
|
||
def test_oc_command_runs(self, image: str): | ||
container = testcontainers.core.container.DockerContainer(image=image, user=123456, group_add=[0]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jstourac suggested abstracting this object away, so the same test can run against local docker as well as remote kubernetes, depending on some cli switch or some other configuration mechanism There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
container.with_command("/bin/sh -c 'sleep infinity'") | ||
try: | ||
container.start() | ||
ecode, output = container.exec(["/bin/sh", "-c", "oc version"]) | ||
finally: | ||
docker_utils.NotebookContainer(container).stop(timeout=0) | ||
|
||
logging.debug(output.decode()) | ||
assert ecode == 0 | ||
|
||
def test_oc_command_runs_fake_fips(self, image: str, subtests: pytest_subtests.SubTests): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jstourac suggested skipping this test when running suite on openshift, openshift will test real configurations, not these fake ones There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
"""Establishes a best-effort fake FIPS environment and attempts to execute `oc` binary in it. | ||
|
||
Related issue: RHOAIENG-4350 In workbench the oc CLI tool cannot be used on FIPS enabled cluster""" | ||
with tempfile.TemporaryDirectory() as tmp_crypto: | ||
# Ubuntu does not even have /proc/sys/crypto directory, unless FIPS is activated and machine | ||
# is rebooted, see https://ubuntu.com/security/certifications/docs/fips-enablement | ||
# NOTE: mounting a temp file as `/proc/sys/crypto/fips_enabled` is further discussed in | ||
# * https://issues.redhat.com/browse/RHOAIENG-4350 | ||
# * https://github.com/junaruga/fips-mode-user-space/blob/main/fips-mode-user-space-setup | ||
tmp_crypto = pathlib.Path(tmp_crypto) | ||
(tmp_crypto / 'crypto').mkdir() | ||
(tmp_crypto / 'crypto' / 'fips_enabled').write_text("1\n") | ||
(tmp_crypto / 'crypto' / 'fips_name').write_text("Linux Kernel Cryptographic API\n") | ||
(tmp_crypto / 'crypto' / 'fips_version').write_text("6.10.10-200.fc40.aarch64\n") | ||
# tmpdir is by-default created with perms restricting access to user only | ||
tmp_crypto.chmod(0o777) | ||
|
||
container = testcontainers.core.container.DockerContainer(image=image, user=654321, group_add=[0]) | ||
container.with_volume_mapping(str(tmp_crypto), "/proc/sys") | ||
container.with_command("/bin/sh -c 'sleep infinity'") | ||
|
||
try: | ||
container.start() | ||
|
||
with subtests.test("/proc/sys/crypto/fips_enabled is 1"): | ||
ecode, output = container.exec(["/bin/sh", "-c", "sysctl crypto.fips_enabled"]) | ||
assert ecode == 0, output.decode() | ||
assert "crypto.fips_enabled = 1\n" == output.decode(), output.decode() | ||
|
||
# 0: enabled, 1: partial success, 2: not enabled | ||
with subtests.test("/fips-mode-setup --is-enabled reports 1"): | ||
ecode, output = container.exec(["/bin/sh", "-c", "fips-mode-setup --is-enabled"]) | ||
assert ecode == 1, output.decode() | ||
|
||
with subtests.test("/fips-mode-setup --check reports partial success"): | ||
ecode, output = container.exec(["/bin/sh", "-c", "fips-mode-setup --check"]) | ||
assert ecode == 1, output.decode() | ||
assert "FIPS mode is enabled.\n" in output.decode(), output.decode() | ||
assert "Inconsistent state detected.\n" in output.decode(), output.decode() | ||
|
||
with subtests.test("oc version command runs"): | ||
ecode, output = container.exec(["/bin/sh", "-c", "oc version"]) | ||
assert ecode == 0, output.decode() | ||
finally: | ||
docker_utils.NotebookContainer(container).stop(timeout=0) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING | ||
|
||
import testcontainers.core.config | ||
import testcontainers.core.container | ||
import testcontainers.core.docker_client | ||
|
||
import pytest | ||
|
||
if TYPE_CHECKING: | ||
from pytest import ExitCode, Session, Parser, Metafunc | ||
|
||
SHUTDOWN_RYUK = False | ||
|
||
# NOTE: Configure Testcontainers through `testcontainers.core.config` and not through env variables. | ||
# Importing `testcontainers` above has already read out env variables, and so at this point, setting | ||
# * DOCKER_HOST | ||
# * TESTCONTAINERS_RYUK_DISABLED | ||
# * TESTCONTAINERS_RYUK_PRIVILEGED | ||
# * TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE | ||
# would have no effect. | ||
|
||
# We'd get selinux violations with podman otherwise, so either ryuk must be privileged, or we need to disable selinux. | ||
# https://github.com/testcontainers/testcontainers-java/issues/2088#issuecomment-1169830358 | ||
testcontainers.core.config.testcontainers_config.ryuk_privileged = True | ||
|
||
|
||
def pytest_addoption(parser: Parser) -> None: | ||
parser.addoption("--image", action="append", default=[], | ||
help="Image to use, can be specified multiple times") | ||
|
||
|
||
def pytest_generate_tests(metafunc: Metafunc) -> None: | ||
if image.__name__ in metafunc.fixturenames: | ||
metafunc.parametrize(image.__name__, metafunc.config.getoption("--image")) | ||
|
||
|
||
# https://docs.pytest.org/en/stable/how-to/fixtures.html#parametrizing-fixtures | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jstourac was discouraged by this long doc page and left the office for today shortly after opening it |
||
# indirect parametrization https://stackoverflow.com/questions/18011902/how-to-pass-a-parameter-to-a-fixture-function-in-pytest | ||
@pytest.fixture(scope="session") | ||
def image(request): | ||
yield request.param | ||
|
||
|
||
def pytest_sessionstart(session: Session) -> None: | ||
# first preflight check: ping the Docker API | ||
client = testcontainers.core.docker_client.DockerClient() | ||
assert client.client.ping(), "Failed to connect to Docker" | ||
|
||
# second preflight check: start the Reaper container | ||
assert testcontainers.core.container.Reaper.get_instance() is not None, "Failed to start Reaper container" | ||
|
||
|
||
# https://docs.pytest.org/en/latest/reference/reference.html#pytest.hookspec.pytest_sessionfinish | ||
def pytest_sessionfinish(session: Session, exitstatus: int | ExitCode) -> None: | ||
# resolves a shutdown resource leak warning that would be otherwise reported | ||
if SHUTDOWN_RYUK: | ||
testcontainers.core.container.Reaper.delete_instance() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
from __future__ import annotations | ||
|
||
import io | ||
import logging | ||
import os.path | ||
import sys | ||
import tarfile | ||
import time | ||
from typing import TYPE_CHECKING | ||
|
||
import testcontainers.core.container | ||
|
||
if TYPE_CHECKING: | ||
from docker.models.containers import Container | ||
|
||
|
||
class NotebookContainer: | ||
@classmethod | ||
def wrap(cls, container: testcontainers.core.container.DockerContainer): | ||
return NotebookContainer(container) | ||
|
||
def __init__(self, container: testcontainers.core.container.DockerContainer) -> None: | ||
self.testcontainer = container | ||
|
||
def stop(self, timeout: int = 10): | ||
"""Stop container with customizable timeout. | ||
|
||
DockerContainer.stop() has unchangeable 10s timeout between SIGSTOP and SIGKILL.""" | ||
self.testcontainer.get_wrapped_container().stop(timeout=timeout) | ||
self.testcontainer.stop() | ||
|
||
def wait_for_exit(self) -> int: | ||
container = self.testcontainer.get_wrapped_container() | ||
container.reload() | ||
while container.status != "exited": | ||
time.sleep(0.2) | ||
container.reload() | ||
return container.attrs["State"]["ExitCode"] | ||
|
||
|
||
def container_cp(container: Container, src: str, dst: str, | ||
user: int | None = None, group: int | None = None) -> None: | ||
""" | ||
Copies a directory into a container | ||
From https://stackoverflow.com/questions/46390309/how-to-copy-a-file-from-host-to-container-using-docker-py-docker-sdk | ||
""" | ||
fh = io.BytesIO() | ||
tar = tarfile.open(fileobj=fh, mode="w:gz") | ||
|
||
tar_filter = None | ||
if user or group: | ||
def tar_filter(f: tarfile.TarInfo) -> tarfile.TarInfo: | ||
if user: | ||
f.uid = user | ||
if group: | ||
f.gid = group | ||
return f | ||
|
||
logging.debug(f"Adding {src=} to archive {dst=}") | ||
try: | ||
tar.add(src, arcname=os.path.basename(src), filter=tar_filter) | ||
finally: | ||
tar.close() | ||
|
||
fh.seek(0) | ||
container.put_archive(dst, fh) | ||
|
||
|
||
def container_exec( | ||
container: Container, | ||
cmd: str | list[str], | ||
stdout: bool = True, | ||
stderr: bool = True, | ||
stdin: bool = False, | ||
tty: bool = False, | ||
privileged: bool = False, | ||
user: str = "", | ||
detach: bool = False, | ||
stream: bool = False, | ||
socket: bool = False, | ||
environment: dict[str, str] | None = None, | ||
workdir: str | None = None, | ||
) -> ContainerExec: | ||
""" | ||
An enhanced version of #docker.Container.exec_run() which returns an object | ||
that can be properly inspected for the status of the executed commands. | ||
Usage example: | ||
result = tools.container_exec(container, cmd, stream=True, **kwargs) | ||
res = result.communicate(line_prefix=b'--> ') | ||
if res != 0: | ||
error('exit code {!r}'.format(res)) | ||
From https://github.com/docker/docker-py/issues/1989 | ||
""" | ||
|
||
exec_id = container.client.api.exec_create( | ||
container.id, | ||
cmd, | ||
stdout=stdout, | ||
stderr=stderr, | ||
stdin=stdin, | ||
tty=tty, | ||
privileged=privileged, | ||
user=user, | ||
environment=environment, | ||
workdir=workdir, | ||
)["Id"] | ||
|
||
output = container.client.api.exec_start(exec_id, detach=detach, tty=tty, stream=stream, socket=socket) | ||
|
||
return ContainerExec(container.client, exec_id, output) | ||
|
||
|
||
class ContainerExec: | ||
def __init__(self, client, id, output: list[int] | list[str]): | ||
self.client = client | ||
self.id = id | ||
self.output = output | ||
|
||
def inspect(self): | ||
return self.client.api.exec_inspect(self.id) | ||
|
||
def poll(self): | ||
return self.inspect()["ExitCode"] | ||
|
||
def communicate(self, line_prefix=b""): | ||
for data in self.output: | ||
if not data: | ||
continue | ||
offset = 0 | ||
while offset < len(data): | ||
sys.stdout.buffer.write(line_prefix) | ||
nl = data.find(b"\n", offset) | ||
if nl >= 0: | ||
slice = data[offset: nl + 1] | ||
offset = nl + 1 | ||
else: | ||
slice = data[offset:] | ||
offset += len(slice) | ||
sys.stdout.buffer.write(slice) | ||
sys.stdout.flush() | ||
while self.poll() is None: | ||
raise RuntimeError("Hm could that really happen?") | ||
return self.poll() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jstourac suggested disabling ryuk for gha runs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.