-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ISV-5274] Prepare the integration test framework scaffolding (1/2) (#…
…749)
- Loading branch information
Showing
17 changed files
with
1,389 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -149,3 +149,4 @@ ansible/vault-password* | |
|
||
.pdm-python | ||
|
||
integration-tests-config.yaml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--- | ||
|
||
operator_repository: | ||
# The GitHub repository hosting the operators for integration tests | ||
url: https://github.com/foo/operators-integration-tests | ||
token: secret123 | ||
contributor_repository: | ||
# The GitHub repository hosting where to fork the integration tests repo and submit the PR from | ||
url: https://github.com/bar/operators-integration-tests-fork | ||
token: secret456 | ||
ssh_key: ~/.ssh/id_rsa_alt | ||
bundle_registry: &quay | ||
# The container registry where the bundle and index images will be pushed to | ||
base_ref: quay.io/foo | ||
username: foo | ||
password: secret789 | ||
test_registry: *quay | ||
# The container registry where to push the operator-pipeline image | ||
iib: | ||
# The iib instance to use to manipulate indices | ||
url: https://iib.stage.engineering.redhat.com | ||
keytab: /tmp/keytab |
64 changes: 64 additions & 0 deletions
64
operator-pipeline-images/operatorcert/entrypoints/integration_tests.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
""" | ||
Integration tests for the operator-pipelines project | ||
""" | ||
|
||
import argparse | ||
import logging | ||
import sys | ||
from pathlib import Path | ||
|
||
from operatorcert.integration.runner import run_integration_tests | ||
|
||
LOGGER = logging.getLogger("operator-cert") | ||
|
||
|
||
def parse_args() -> argparse.Namespace: | ||
""" | ||
Parse command line arguments | ||
Returns: | ||
Parsed arguments | ||
""" | ||
parser = argparse.ArgumentParser(description="Run integration tests") | ||
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") | ||
parser.add_argument( | ||
"--image", "-i", help="Skip image build and use alternate container image" | ||
) | ||
parser.add_argument( | ||
"directory", type=Path, help="operator-pipelines project directory" | ||
) | ||
parser.add_argument("config_file", type=Path, help="Path to the yaml config file") | ||
|
||
return parser.parse_args() | ||
|
||
|
||
def setup_logging(verbose: bool) -> None: | ||
""" | ||
Set up the logging configuration for the application. | ||
Args: | ||
verbose (bool): If True, set the logging level to DEBUG; otherwise, set it to INFO. | ||
This function configures the logging format and level for the application, allowing for | ||
detailed debug messages when verbose mode is enabled. | ||
""" | ||
|
||
logging.basicConfig( | ||
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", | ||
level=logging.DEBUG if verbose else logging.INFO, | ||
) | ||
|
||
|
||
def main() -> int: | ||
""" | ||
Main function for integration tests runner | ||
""" | ||
args = parse_args() | ||
setup_logging(args.verbose) | ||
|
||
# Logic | ||
return run_integration_tests(args.directory, args.config_file, args.image) | ||
|
||
|
||
if __name__ == "__main__": # pragma: no cover | ||
sys.exit(main()) |
Empty file.
66 changes: 66 additions & 0 deletions
66
operator-pipeline-images/operatorcert/integration/config.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
""" | ||
Schema of the integration tests configuration file | ||
""" | ||
|
||
from pathlib import Path | ||
from typing import Optional, Type, TypeVar | ||
|
||
from pydantic import BaseModel | ||
from yaml import safe_load | ||
|
||
|
||
class GitHubRepoConfig(BaseModel): | ||
""" | ||
A GitHub repository | ||
""" | ||
|
||
url: str | ||
token: Optional[str] = None | ||
ssh_key: Optional[Path] = None | ||
|
||
|
||
class ContainerRegistryConfig(BaseModel): | ||
""" | ||
A container registry | ||
""" | ||
|
||
base_ref: str | ||
username: Optional[str] = None | ||
password: Optional[str] = None | ||
|
||
|
||
class IIBConfig(BaseModel): | ||
""" | ||
An IIB API endpoint | ||
""" | ||
|
||
url: str | ||
keytab: Path | ||
|
||
|
||
C = TypeVar("C", bound="Config") | ||
|
||
|
||
class Config(BaseModel): | ||
""" | ||
Root configuration object | ||
""" | ||
|
||
operator_repository: GitHubRepoConfig | ||
contributor_repository: GitHubRepoConfig | ||
bundle_registry: ContainerRegistryConfig | ||
test_registry: ContainerRegistryConfig | ||
iib: IIBConfig | ||
|
||
@classmethod | ||
def from_yaml(cls: Type[C], path: Path) -> C: | ||
""" | ||
Parse a yaml configuration file | ||
Args: | ||
path: path to the configuration file | ||
Returns: | ||
the parsed configuration object | ||
""" | ||
return cls(**safe_load(path.read_text())) |
197 changes: 197 additions & 0 deletions
197
operator-pipeline-images/operatorcert/integration/external_tools.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
""" | ||
Utility classes to run external tools | ||
""" | ||
|
||
import base64 | ||
import json | ||
import logging | ||
import subprocess | ||
import tempfile | ||
from os import PathLike | ||
from pathlib import Path | ||
from typing import Mapping, Optional, Sequence, TypeAlias | ||
|
||
LOGGER = logging.getLogger("operator-cert") | ||
|
||
|
||
CommandArg: TypeAlias = str | PathLike[str] | ||
|
||
|
||
class Secret(str): | ||
""" | ||
A string with sensitive content that should not be logged | ||
""" | ||
|
||
|
||
def run( | ||
*cmd: CommandArg, | ||
cwd: Optional[CommandArg] = None, | ||
env: Optional[Mapping[str, str]] = None, | ||
) -> None: | ||
""" | ||
Execute an external command | ||
Args: | ||
*cmd: The command and its arguments | ||
cwd: Directory to run the command in, by default current working directory | ||
env: Environment variables, if None the current environment is used | ||
Raises: | ||
subprocess.CalledProcessError when the called process exits with a | ||
non-zero status; the process' stdout and stderr can be obtained | ||
from the exception object | ||
""" | ||
LOGGER.debug("Running %s from %s", cmd, cwd or Path.cwd()) | ||
subprocess.run( | ||
cmd, | ||
stdout=subprocess.PIPE, | ||
stderr=subprocess.STDOUT, | ||
check=True, | ||
cwd=cwd, | ||
env=env, | ||
) | ||
|
||
|
||
class Ansible: | ||
""" | ||
Utility class to interact with Ansible | ||
""" | ||
|
||
def __init__(self, path: Optional[Path]) -> None: | ||
""" | ||
Initialize the Ansible instance | ||
Args: | ||
path: The working directory for the ansible commands; | ||
It must contain an ansible.cfg file | ||
""" | ||
self.path = (path or Path.cwd()).absolute() | ||
# Simple sanity check to ensure the directory actually contains | ||
# a copy of the operator-pipelines project | ||
ansible_cfg_file = self.path / "ansible.cfg" | ||
if not ansible_cfg_file.exists(): | ||
raise FileNotFoundError(f"ansible.cfg not found in {self.path}") | ||
|
||
def playbook_path(self, playbook_name: str) -> Path: | ||
""" | ||
Return the path to the playbook file with the given name; this is specific | ||
to the operator-pipelines project | ||
""" | ||
playbook_dir = self.path / "ansible" / "playbooks" | ||
for ext in ("yml", "yaml"): | ||
playbook_path = playbook_dir / f"{playbook_name}.{ext}" | ||
if playbook_path.exists(): | ||
return playbook_path | ||
raise FileNotFoundError(f"Playbook {playbook_name} not found in {playbook_dir}") | ||
|
||
def run_playbook( | ||
self, playbook: str, *extra_args: CommandArg, **extra_vars: str | Secret | ||
) -> None: | ||
""" | ||
Run an ansible playbook | ||
Args: | ||
playbook: The name of the playbook to execute | ||
*extra_args: Additional arguments for the ansible playbook | ||
**extra_vars: Extra variables to pass to the playbook | ||
""" | ||
command: list[CommandArg] = ["ansible-playbook", self.playbook_path(playbook)] | ||
command.extend(extra_args) | ||
secrets = {} | ||
for k, v in extra_vars.items(): | ||
if isinstance(v, Secret): | ||
# Avoid adding secrets to the command line | ||
secrets[k] = str(v) | ||
else: | ||
command.extend(["-e", f"{k}={v}"]) | ||
with tempfile.NamedTemporaryFile( | ||
mode="w", | ||
encoding="utf-8", | ||
suffix=".json", | ||
delete=True, | ||
delete_on_close=False, | ||
) as tmp: | ||
if secrets: | ||
json.dump(secrets, tmp) | ||
command.extend(["-e", f"@{tmp.name}"]) | ||
tmp.close() | ||
run(*command, cwd=self.path) | ||
|
||
|
||
class Podman: | ||
""" | ||
Utility class to interact with Podman. | ||
""" | ||
|
||
def __init__(self, auth: Optional[Mapping[str, tuple[str, str]]] = None): | ||
""" | ||
Initialize the Podman instance | ||
Args: | ||
auth: The authentication credentials for registries | ||
""" | ||
self._auth = { | ||
"auths": { | ||
registry: { | ||
"auth": base64.b64encode( | ||
f"{username}:{password}".encode("utf-8") | ||
).decode("ascii") | ||
} | ||
for registry, (username, password) in (auth or {}).items() | ||
if username and password | ||
} | ||
} | ||
|
||
def _run(self, *args: CommandArg) -> None: | ||
""" | ||
Run a podman subcommand | ||
Args: | ||
*args: The podman subcommand and its arguments | ||
""" | ||
command: list[CommandArg] = ["podman"] | ||
command.extend(args) | ||
with tempfile.NamedTemporaryFile( | ||
mode="w", | ||
encoding="utf-8", | ||
suffix=".json", | ||
delete=True, | ||
delete_on_close=False, | ||
) as tmp: | ||
json.dump(self._auth, tmp) | ||
tmp.close() | ||
LOGGER.debug("Using podman auth file: %s", tmp.name) | ||
run(*command, env={"REGISTRY_AUTH_FILE": tmp.name}) | ||
|
||
def build( | ||
self, | ||
context: CommandArg, | ||
image: str, | ||
containerfile: Optional[CommandArg] = None, | ||
extra_args: Optional[Sequence[CommandArg]] = None, | ||
) -> None: | ||
""" | ||
Build an image | ||
Args: | ||
context: Directory to build the image from | ||
image: The name of the image to build | ||
containerfile: The path to the container configuration file, | ||
if not specified it will be inferred by podman | ||
extra_args: Additional arguments for the podman build command | ||
""" | ||
command: list[CommandArg] = ["build", "-t", image, context] | ||
if containerfile: | ||
command.extend(["-f", containerfile]) | ||
if extra_args: | ||
command.extend(extra_args) | ||
self._run(*command) | ||
|
||
def push(self, image: str) -> None: | ||
""" | ||
Push an image to a registry. | ||
Args: | ||
image: The name of the image to push. | ||
""" | ||
self._run("push", image) |
Oops, something went wrong.