Skip to content

Commit bd99110

Browse files
authored
Merge branch 'opendatahub-io:main' into main
2 parents 9d8e668 + de11de9 commit bd99110

46 files changed

Lines changed: 2424 additions & 760 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ fcn_exclude_functions =
1919
re,
2020
logging,
2121
LOGGER,
22+
BASIC_LOGGER,
2223
os,
2324
json,
2425
pytest,

.github/workflows/scripts/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* Run [pre-commit](https://pre-commit.ci/)
2828
* Run [tox](https://tox.wiki/)
2929
* Add PR author as the PR assignee
30+
* Build image based on the PR
3031
3132
Available user actions:
3233
* To mark a PR as `WIP`, add `/wip` in a comment. To remove it from the PR comment `/wip cancel` to the PR.
@@ -37,6 +38,8 @@
3738
`verified` label removed on each new commit push.
3839
* To Cherry-pick a merged PR `/cherry-pick <target_branch_name>` to the PR. If <target_branch_name> is valid,
3940
and the current PR is merged, a cherry-picked PR would be created and linked to the current PR.
41+
* To build and push image to quay, add /build-push-pr-image in a comment. This would create an image with tag
42+
pr-<pr_number> to quay repository. This image tag, however would be deleted on PR merge or close action.
4043
4144
<details>
4245
<summary>Supported labels</summary>

.github/workflows/verify_build_container.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ jobs:
99
runs-on: ubuntu-latest
1010

1111
steps:
12-
- uses: actions/checkout@v4
13-
1412
- name: Checkout Pull Request Branch
1513
uses: actions/checkout@v4
1614
with:

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ repos:
3535
- id: detect-secrets
3636

3737
- repo: https://github.com/astral-sh/ruff-pre-commit
38-
rev: v0.11.9
38+
rev: v0.11.11
3939
hooks:
4040
- id: ruff
4141
- id: ruff-format
@@ -49,7 +49,7 @@ repos:
4949
# - id: renovate-config-validator
5050

5151
- repo: https://github.com/gitleaks/gitleaks
52-
rev: v8.25.1
52+
rev: v8.26.0
5353
hooks:
5454
- id: gitleaks
5555

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ RUN curl -sSL "https://github.com/fullstorydev/grpcurl/releases/download/v1.9.2/
3535
&& tar xvf /tmp/grpcurl_1.2.tar.gz --no-same-owner \
3636
&& mv grpcurl /usr/bin/grpcurl
3737

38+
# Install htpasswd
39+
RUN apt-get update && \
40+
apt-get install -y apache2-utils && \
41+
apt-get clean && \
42+
rm -rf /var/lib/apt/lists/*
43+
3844
RUN useradd -ms /bin/bash $USER
3945
USER $USER
4046
WORKDIR $HOME_DIR

conftest.py

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
import os
33
import pathlib
44
import shutil
5+
import datetime
6+
import traceback
57

68
import shortuuid
9+
from _pytest.runner import CallInfo
10+
from _pytest.reports import TestReport
711
from pytest import (
812
Parser,
913
Session,
1014
FixtureRequest,
1115
FixtureDef,
1216
Item,
17+
Collector,
1318
Config,
1419
CollectReport,
1520
)
@@ -18,8 +23,15 @@
1823
from pytest_testconfig import config as py_config
1924

2025
from utilities.constants import KServeDeploymentType
26+
from utilities.database import Database
2127
from utilities.logger import separator, setup_logging
22-
28+
from utilities.must_gather_collector import (
29+
set_must_gather_collector_directory,
30+
set_must_gather_collector_values,
31+
get_must_gather_collector_dir,
32+
collect_rhoai_must_gather,
33+
get_base_dir,
34+
)
2335

2436
LOGGER = logging.getLogger(name=__name__)
2537
BASIC_LOGGER = logging.getLogger(name="basic")
@@ -30,7 +42,7 @@ def pytest_addoption(parser: Parser) -> None:
3042
buckets_group = parser.getgroup(name="Buckets")
3143
runtime_group = parser.getgroup(name="Runtime details")
3244
upgrade_group = parser.getgroup(name="Upgrade options")
33-
platform_group = parser.getgroup(name="Platform")
45+
must_gather_group = parser.getgroup(name="MustGather")
3446
cluster_sanity_group = parser.getgroup(name="ClusterSanity")
3547

3648
# AWS config and credentials options
@@ -112,11 +124,11 @@ def pytest_addoption(parser: Parser) -> None:
112124
help="Coma-separated str; specify inference service deployment modes tests to run in upgrade tests. "
113125
"If not set, all will be tested.",
114126
)
115-
116-
# Platform options
117-
platform_group.addoption(
118-
"--applications-namespace",
119-
help="RHOAI/ODH applications namespace",
127+
must_gather_group.addoption(
128+
"--collect-must-gather",
129+
help="Indicate if must-gather should be collected on failure.",
130+
action="store_true",
131+
default=False,
120132
)
121133

122134
# Cluster sanity options
@@ -205,14 +217,22 @@ def _add_upgrade_test(_item: Item, _upgrade_deployment_modes: list[str]) -> bool
205217

206218

207219
def pytest_sessionstart(session: Session) -> None:
208-
tests_log_file = session.config.getoption("log_file") or "pytest-tests.log"
220+
log_file = session.config.getoption("log_file") or "pytest-tests.log"
221+
tests_log_file = os.path.join(get_base_dir(), log_file)
222+
LOGGER.info(f"Writing tests log to {tests_log_file}")
209223
if os.path.exists(tests_log_file):
210224
pathlib.Path(tests_log_file).unlink()
211-
225+
if session.config.getoption("--collect-must-gather"):
226+
session.config.option.must_gather_db = Database()
212227
session.config.option.log_listener = setup_logging(
213228
log_file=tests_log_file,
214229
log_level=session.config.getoption("log_cli_level") or logging.INFO,
215230
)
231+
must_gather_dict = set_must_gather_collector_values()
232+
shutil.rmtree(
233+
path=must_gather_dict["must_gather_base_directory"],
234+
ignore_errors=True,
235+
)
216236

217237

218238
def pytest_fixture_setup(fixturedef: FixtureDef[Any], request: FixtureRequest) -> None:
@@ -226,9 +246,23 @@ def pytest_runtest_setup(item: Item) -> None:
226246
2. Adds `fail_if_missing_dependent_operators` fixture for Serverless tests.
227247
3. Adds fixtures to enable KServe/model mesh in DSC for model server tests.
228248
"""
229-
230249
BASIC_LOGGER.info(f"\n{separator(symbol_='-', val=item.name)}")
231250
BASIC_LOGGER.info(f"{separator(symbol_='-', val='SETUP')}")
251+
if item.config.getoption("--collect-must-gather"):
252+
# set must-gather collection directory:
253+
set_must_gather_collector_directory(item=item, directory_path=get_must_gather_collector_dir())
254+
255+
# At the begining of setup work, insert current epoch time into the database to indicate test
256+
# start time
257+
258+
try:
259+
db = item.config.option.must_gather_db
260+
db.insert_test_start_time(
261+
test_name=f"{item.fspath}::{item.name}",
262+
start_time=int(datetime.datetime.now().timestamp()),
263+
)
264+
except Exception as db_exception:
265+
LOGGER.error(f"Database error: {db_exception}. Must-gather collection may not be accurate")
232266

233267
if KServeDeploymentType.SERVERLESS.lower() in item.keywords:
234268
item.fixturenames.insert(0, "fail_if_missing_dependent_operators")
@@ -252,6 +286,10 @@ def pytest_runtest_call(item: Item) -> None:
252286

253287
def pytest_runtest_teardown(item: Item) -> None:
254288
BASIC_LOGGER.info(f"{separator(symbol_='-', val='TEARDOWN')}")
289+
# reset must-gather collector after each tests
290+
py_config["must_gather_collector"]["collector_directory"] = py_config["must_gather_collector"][
291+
"must_gather_base_directory"
292+
]
255293

256294

257295
def pytest_report_teststatus(report: CollectReport, config: Config) -> None:
@@ -276,10 +314,56 @@ def pytest_sessionfinish(session: Session, exitstatus: int) -> None:
276314
session.config.option.log_listener.stop()
277315
if session.config.option.setupplan or session.config.option.collectonly:
278316
return
279-
base_dir = py_config["tmp_base_dir"]
280-
LOGGER.info(f"Deleting pytest base dir {base_dir}")
281-
shutil.rmtree(path=base_dir, ignore_errors=True)
317+
if session.config.getoption("--collect-must-gather"):
318+
db = session.config.option.must_gather_db
319+
file_path = db.database_file_path
320+
LOGGER.info(f"Removing database file path {file_path}")
321+
if os.path.exists(file_path):
322+
os.remove(file_path)
323+
# clean up the empty folders
324+
collector_directory = py_config["must_gather_collector"]["must_gather_base_directory"]
325+
if os.path.exists(collector_directory):
326+
for root, dirs, files in os.walk(collector_directory, topdown=False):
327+
for _dir in dirs:
328+
dir_path = os.path.join(root, _dir)
329+
if not os.listdir(dir_path):
330+
shutil.rmtree(path=dir_path, ignore_errors=True)
331+
LOGGER.info(f"Deleting pytest base dir {session.config.option.basetemp}")
332+
shutil.rmtree(path=session.config.option.basetemp, ignore_errors=True)
282333

283334
reporter: Optional[TerminalReporter] = session.config.pluginmanager.get_plugin("terminalreporter")
284335
if reporter:
285336
reporter.summary_stats()
337+
338+
339+
def calculate_must_gather_timer(test_start_time: int) -> int:
340+
default_duration = 300
341+
if test_start_time > 0:
342+
duration = int(datetime.datetime.now().timestamp()) - test_start_time
343+
return duration if duration > 60 else default_duration
344+
else:
345+
LOGGER.warning(f"Could not get start time of test. Collecting must-gather for last {default_duration}s")
346+
return default_duration
347+
348+
349+
def pytest_exception_interact(node: Item | Collector, call: CallInfo[Any], report: TestReport | CollectReport) -> None:
350+
LOGGER.error(report.longreprtext)
351+
if node.config.getoption("--collect-must-gather"):
352+
test_name = f"{node.fspath}::{node.name}"
353+
LOGGER.info(f"Must-gather collection is enabled for {test_name}.")
354+
355+
try:
356+
db = node.config.option.must_gather_db
357+
test_start_time = db.get_test_start_time(test_name=test_name)
358+
except Exception as db_exception:
359+
test_start_time = 0
360+
LOGGER.warning(f"Error: {db_exception} in accessing database.")
361+
362+
try:
363+
collect_rhoai_must_gather(
364+
since=calculate_must_gather_timer(test_start_time=test_start_time),
365+
target_dir=os.path.join(get_must_gather_collector_dir(), "pytest_exception_interact"),
366+
)
367+
368+
except Exception as current_exception:
369+
LOGGER.warning(f"Failed to collect logs: {test_name}: {current_exception} {traceback.format_exc()}")

docs/DEVELOPER_GUIDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ secret = Secret(name=request.param["name"], model_dir=request.param["model-dir"]
141141
Check [pytest.ini](../pytest.ini) for available markers; additional markers can always be added when needed.
142142
- Classes are good to group related tests together, for example when they share a fixture.
143143
You should NOT group unrelated tests in one class (because it is misleading the reader).
144+
- All the tests should be properly documented. Every test (or test class), should have a docstring explaning what the test does so that anyone (engineers from other components, managers, PMs, or non-technical users) can have a basic understanding of what the code is trying to test without having to dive into the technical details of related functions or fixtures.
144145

145146

146147
## Check the code

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ dependencies = [
6565
"jira>=3.8.0",
6666
"openshift-python-wrapper>=11.0.50",
6767
"semver>=3.0.4",
68+
"sqlalchemy>=2.0.40",
6869
"pytest-order>=1.3.0",
6970
"marshmallow==3.26.1,<4", # this version is needed for pytest-jira
71+
"pytest-html>=4.1.1",
7072
]
7173

7274
[project.urls]

tests/conftest.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
get_operator_distribution,
3535
login_with_user_password,
3636
get_openshift_token,
37+
get_data_science_cluster,
3738
)
3839
from utilities.constants import (
3940
AcceleratorType,
@@ -66,17 +67,19 @@ def tests_tmp_dir(request: FixtureRequest, tmp_path_factory: TempPathFactory) ->
6667

6768
@pytest.fixture(scope="session")
6869
def updated_global_config(request: FixtureRequest, admin_client: DynamicClient) -> None:
69-
if get_operator_distribution(client=admin_client) == "Open Data Hub":
70+
distribution = get_operator_distribution(client=admin_client)
71+
if distribution == "Open Data Hub":
7072
py_config["distribution"] = "upstream"
7173

72-
else:
74+
elif distribution.startswith("OpenShift AI"):
7375
py_config["distribution"] = "downstream"
74-
75-
if applications_namespace := request.config.getoption("applications_namespace"):
76-
py_config["applications_namespace"] = applications_namespace
77-
7876
else:
79-
py_config["applications_namespace"] = get_dsci_applications_namespace(client=admin_client)
77+
pytest.exit(f"Unknown distribution: {distribution}")
78+
79+
py_config["applications_namespace"] = get_dsci_applications_namespace(client=admin_client)
80+
py_config["model_registry_namespace"] = get_data_science_cluster(
81+
client=admin_client
82+
).instance.spec.components.modelregistry.registriesNamespace
8083

8184

8285
@pytest.fixture(scope="session")

tests/global_config.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
global config # type:ignore[unused-ignore]
22

3-
distribution: str = "downstream"
4-
applications_namespace: str = "redhat-ods-applications" # overwritten in conftest.py if distribution is upstream
53
dsc_name: str = "default-dsc"
4+
must_gather_base_dir: str = "must-gather-base-dir"
65
dsci_name: str = "default-dsci"
76
dependent_operators: str = "servicemeshoperator,authorino-operator,serverless-operator"
8-
97
use_unprivileged_client: bool = True
8+
# overwrite the followings in conftest.py, in updated_global_config() if distribution is upstream
9+
distribution: str = "downstream"
10+
applications_namespace: str = "redhat-ods-applications"
11+
model_registry_namespace: str = "rhoai-model-registries"
1012

1113
for _dir in dir():
1214
val = locals()[_dir]

0 commit comments

Comments
 (0)