Skip to content

feat(evalhub): Update EvalHub provider tests and add RBAC fixtures#1178

Closed
ruivieira wants to merge 24 commits intoopendatahub-io:mainfrom
ruivieira:evalhub-collections
Closed

feat(evalhub): Update EvalHub provider tests and add RBAC fixtures#1178
ruivieira wants to merge 24 commits intoopendatahub-io:mainfrom
ruivieira:evalhub-collections

Conversation

@ruivieira
Copy link
Copy Markdown
Member

@ruivieira ruivieira commented Mar 6, 2026

Pull Request

Summary

  • Introduced new fixtures for ServiceAccounts and RoleBindings to facilitate testing of EvalHub multi-tenancy.
  • Added tests to verify the listing and retrieval of providers, ensuring proper response structure and required fields.
  • Implemented health check and utility functions for interacting with the EvalHub API.
  • Defined constants for RBAC roles for access management in tests.

How it has been tested

  • Locally
  • Jenkins

Additional Requirements

  • If this PR introduces a new test image, did you create a PR to mirror it in disconnected environment?
  • If this PR introduces new marker(s)/adds a new component, was relevant ticket created to update relevant Jenkins job?

Summary by CodeRabbit

  • New Features

    • Token-based provider API: list and retrieve EvalHub providers using short-lived tokens and optional tenant header; health checks accept tokens.
    • Certificate utilities now auto-discover router certificate configuration when building CA bundles.
  • Tests

    • End-to-end tests for EvalHub providers covering listing, details, pagination, data validation, and auth error cases.
    • New test fixtures to create authorized and unauthorized service accounts and issue short-lived tokens.

- Introduced new fixtures for ServiceAccounts and RoleBindings to facilitate testing of EvalHub multi-tenancy.
- Added tests to verify the listing and retrieval of providers, ensuring proper response structure and required fields.
- Implemented health check and utility functions for interacting with the EvalHub API.
- Defined constants for RBAC roles for access management in tests.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 6, 2026

The following are automatically added/executed:

  • PR size label.
  • Run pre-commit
  • Run tox
  • Add PR author as the PR assignee
  • Build image based on the PR

Available user actions:

  • To mark a PR as WIP, add /wip in a comment. To remove it from the PR comment /wip cancel to the PR.
  • To block merging of a PR, add /hold in a comment. To un-block merging of PR comment /hold cancel.
  • To mark a PR as approved, add /lgtm in a comment. To remove, add /lgtm cancel.
    lgtm label removed on each new commit push.
  • To mark PR as verified comment /verified to the PR, to un-verify comment /verified cancel to the PR.
    verified label removed on each new commit push.
  • To Cherry-pick a merged PR /cherry-pick <target_branch_name> to the PR. If <target_branch_name> is valid,
    and the current PR is merged, a cherry-picked PR would be created and linked to the current PR.
  • To build and push image to quay, add /build-push-pr-image in a comment. This would create an image with tag
    pr-<pr_number> to quay repository. This image tag, however would be deleted on PR merge or close action.
Supported labels

{'/build-push-pr-image', '/wip', '/cherry-pick', '/lgtm', '/hold', '/verified'}

# Conflicts:
#	tests/model_explainability/evalhub/conftest.py
…troller for secret retrieval

- Updated `create_ca_bundle_file` to fetch the router certificate secret name from the default IngressController.
- Removed the hardcoded secret name.
- Changed the `evalhub_ca_bundle_file` function to work with the new CA bundle creation logic.
@ruivieira ruivieira requested a review from a team as a code owner March 6, 2026 10:08
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds EvalHub provider API end-to-end tests, RBAC ServiceAccount fixtures with short-lived tokens, tenant-aware EvalHub client helpers, and ensures IngressController exists when resolving router TLS secret. Token creation via shell introduces command‑injection and secret‑leak risks (CWE-78, CWE-532).

Changes

Cohort / File(s) Summary
EvalHub Test Fixtures & Constants
tests/model_explainability/evalhub/conftest.py, tests/model_explainability/evalhub/constants.py
Added class-scoped fixtures: authorized ServiceAccount (evalhub_scoped_sa), RoleBinding (evalhub_providers_role_binding), scoped token (evalhub_scoped_token), unauthorized ServiceAccount (evalhub_unauthorised_sa), and its token (evalhub_unauthorised_token). Introduced EVALHUB_PROVIDERS_ACCESS_CLUSTER_ROLE constant. Tokens are created via oc create token --duration=30m.
EvalHub Provider Tests
tests/model_explainability/evalhub/test_evalhub_providers.py
New end-to-end tests: TestEvalHubProviders (list/pagination, provider schema, benchmarks, single-provider retrieval, not-found handling) and TestEvalHubProvidersUnauthorised (403 checks). Tests use tokens, CA bundle, route, and RBAC fixtures.
EvalHub Utility Functions
tests/model_explainability/evalhub/utils.py
Added TENANT_HEADER and private _build_headers(token, tenant) for Bearer + optional tenant headers. New public helpers: list_evalhub_providers(...) and get_evalhub_provider(...). validate_evalhub_health now accepts token.
Certificate Secret Resolution
utilities/certificates_utils.py
IngressController lookup now uses IngressController(..., ensure_exists=True) and reads spec.defaultCertificate to determine router TLS secret name instead of relying on a hard-coded value.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding RBAC fixtures and updating provider tests for EvalHub, which aligns with the changeset's focus on test infrastructure and fixtures.
Description check ✅ Passed The description covers the main objectives (fixtures, tests, utility functions, constants) and includes testing status, but lacks specific issue/ticket references mentioned in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/model_explainability/evalhub/utils.py (1)

129-129: Consider URL-encoding provider_id for defense in depth.

Direct string interpolation of provider_id into the URL path could allow path traversal if input is not sanitized. While test code typically uses controlled inputs, URL-encoding provides defense in depth.

Proposed fix
+from urllib.parse import quote
+
 ...
-    url = f"https://{host}{EVALHUB_PROVIDERS_PATH}/{provider_id}"
+    url = f"https://{host}{EVALHUB_PROVIDERS_PATH}/{quote(provider_id, safe='')}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_explainability/evalhub/utils.py` at line 129, The URL currently
interpolates provider_id directly into url (url =
f"https://{host}{EVALHUB_PROVIDERS_PATH}/{provider_id}"); to harden this,
URL-encode provider_id before composing the URL (use urllib.parse.quote with
safe='' or similar) and then build the url using the encoded value so
path-traversal characters are percent-encoded; update the code that sets url to
reference the encoded_provider_id variable instead of raw provider_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_explainability/evalhub/test_evalhub_providers.py`:
- Line 162: The test accesses providers["items"][0"] without checking for an
empty list, which can raise IndexError; update the test around the
first_provider_id assignment to first verify providers["items"] is non-empty
(e.g., assert providers.get("items") and len(providers["items"]) > 0) or call
pytest.skip when no providers are present, then only set first_provider_id =
providers["items"][0]["resource"]["id"]; reference the variables providers,
providers["items"], and first_provider_id when making the guard.

In `@utilities/certificates_utils.py`:
- Around line 27-33: The code assumes IngressController("default") exists and
accesses ingress_controller.instance.spec which can raise AttributeError; update
the creation or access to guard existence by either instantiating
IngressController with ensure_exists=True (or the equivalent constructor flag)
or check ingress_controller.instance is not None before accessing .spec, and
handle the missing resource by logging or raising a clear error; modify the
lines that create ingress_controller and the subsequent access to default_cert
(variables: IngressController, ingress_controller, ingress_controller.instance,
default_cert) to include this existence check or ensure_exists parameter and an
explicit fallback/error path.

---

Nitpick comments:
In `@tests/model_explainability/evalhub/utils.py`:
- Line 129: The URL currently interpolates provider_id directly into url (url =
f"https://{host}{EVALHUB_PROVIDERS_PATH}/{provider_id}"); to harden this,
URL-encode provider_id before composing the URL (use urllib.parse.quote with
safe='' or similar) and then build the url using the encoded value so
path-traversal characters are percent-encoded; update the code that sets url to
reference the encoded_provider_id variable instead of raw provider_id.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 0e04a8f0-65e7-456b-9204-82414e8f0be6

📥 Commits

Reviewing files that changed from the base of the PR and between c214dd7 and 783e4f6.

📒 Files selected for processing (5)
  • tests/model_explainability/evalhub/conftest.py
  • tests/model_explainability/evalhub/constants.py
  • tests/model_explainability/evalhub/test_evalhub_providers.py
  • tests/model_explainability/evalhub/utils.py
  • utilities/certificates_utils.py

Copy link
Copy Markdown

@mariusdanciu mariusdanciu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/model_explainability/evalhub/test_evalhub_providers.py (1)

148-155: ⚠️ Potential issue | 🟡 Minor

Guard items before Line 154.

If the environment returns no providers, Line 154 raises IndexError and the test never exercises get_evalhub_provider. Add a local precondition assertion or pytest.skip instead of relying on another test to establish it.

Suggested fix
         providers = list_evalhub_providers(
             host=evalhub_route.host,
             token=evalhub_scoped_token,
             ca_bundle_file=evalhub_ca_bundle_file,
             tenant=model_namespace.name,
         )
+        assert providers.get("items"), "No providers available to test get_single_provider"
         first_provider_id = providers["items"][0]["resource"]["id"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_explainability/evalhub/test_evalhub_providers.py` around lines
148 - 155, The test currently assumes list_evalhub_providers() returns items and
then indexes providers["items"][0] which will IndexError if no providers exist;
modify the test around the call to list_evalhub_providers (and before computing
first_provider_id) to guard providers["items"] by asserting its truthiness or
calling pytest.skip() when empty so the test short-circuits instead of
failing—apply this check right after list_evalhub_providers(...) and before
using first_provider_id and get_evalhub_provider to ensure the rest of the test
only runs when providers are present.
🧹 Nitpick comments (1)
tests/model_explainability/evalhub/test_evalhub_providers.py (1)

22-24: Apply the RBAC fixture once at class scope.

evalhub_providers_role_binding is never read inside these tests; it is only a setup side effect. Put @pytest.mark.usefixtures("evalhub_providers_role_binding") on TestEvalHubProviders and drop the repeated parameters to remove the ARG002 noise and make the dependency explicit.

Refactor sketch
 import pytest
 import requests
 from ocp_resources.namespace import Namespace
-from ocp_resources.role_binding import RoleBinding
 from ocp_resources.route import Route
@@
 `@pytest.mark.sanity`
 `@pytest.mark.model_explainability`
+@pytest.mark.usefixtures("evalhub_providers_role_binding")
 class TestEvalHubProviders:
@@
-        evalhub_providers_role_binding: RoleBinding,
         evalhub_ca_bundle_file: str,
         evalhub_route: Route,

Also applies to: 31-31, 52-52, 71-71, 94-94, 117-117, 143-143, 172-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_explainability/evalhub/test_evalhub_providers.py` around lines 22
- 24, The tests currently accept the evalhub_providers_role_binding fixture as a
parameter but never use it; instead, add
`@pytest.mark.usefixtures`("evalhub_providers_role_binding") to the
TestEvalHubProviders class to apply the RBAC setup once at class scope and
remove the repeated evalhub_providers_role_binding parameters from the
individual test functions (this removes ARG002 noise); apply the same change for
other test classes/functions that currently accept
evalhub_providers_role_binding as an unused parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_explainability/evalhub/test_evalhub_providers.py`:
- Around line 177-184: Update the test that calls get_evalhub_provider to
explicitly assert a 404 response: wrap the call in a try/except (or use
pytest.raises as excinfo) capturing requests.exceptions.HTTPError raised by
utils.py:138, then inspect excinfo.value.response.status_code (or the caught
exception.response.status_code) and assert it equals 404; keep the existing
parameters and provider_id="nonexistent-provider-id" and
tenant=model_namespace.name so the test verifies the 404 contract instead of any
non-2xx error.

---

Duplicate comments:
In `@tests/model_explainability/evalhub/test_evalhub_providers.py`:
- Around line 148-155: The test currently assumes list_evalhub_providers()
returns items and then indexes providers["items"][0] which will IndexError if no
providers exist; modify the test around the call to list_evalhub_providers (and
before computing first_provider_id) to guard providers["items"] by asserting its
truthiness or calling pytest.skip() when empty so the test short-circuits
instead of failing—apply this check right after list_evalhub_providers(...) and
before using first_provider_id and get_evalhub_provider to ensure the rest of
the test only runs when providers are present.

---

Nitpick comments:
In `@tests/model_explainability/evalhub/test_evalhub_providers.py`:
- Around line 22-24: The tests currently accept the
evalhub_providers_role_binding fixture as a parameter but never use it; instead,
add `@pytest.mark.usefixtures`("evalhub_providers_role_binding") to the
TestEvalHubProviders class to apply the RBAC setup once at class scope and
remove the repeated evalhub_providers_role_binding parameters from the
individual test functions (this removes ARG002 noise); apply the same change for
other test classes/functions that currently accept
evalhub_providers_role_binding as an unused parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: fdff9954-ba41-4a20-b156-dd9584955232

📥 Commits

Reviewing files that changed from the base of the PR and between 783e4f6 and 55b31df.

📒 Files selected for processing (1)
  • tests/model_explainability/evalhub/test_evalhub_providers.py

kpunwatk
kpunwatk previously approved these changes Mar 6, 2026
Copy link
Copy Markdown
Contributor

@kpunwatk kpunwatk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@ruivieira
Copy link
Copy Markdown
Member Author

@kpunwatk this PR includes #1181. Should that one be merged first and then I can rebase this one?

@kpunwatk
Copy link
Copy Markdown
Contributor

kpunwatk commented Mar 6, 2026

@kpunwatk this PR includes #1181. Should that one be merged first and then I can rebase this one?

@ruivieira Yes, #1181 got merged, We can rebase this one and get it merge, Thanks!

…b-collections

# Conflicts:
#	tests/model_explainability/evalhub/test_evalhub_providers.py
Copy link
Copy Markdown

@mariusdanciu mariusdanciu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm.

A few suggestions if I may (for the future):

  • Have a separate test file for authn and authz. Including trying to tamper with the /events endpoint while protected by status-events access.
  • Have a separate test files for evaluation jobs
  • Have a separate test files for providers
  • Have a separate test files for collections

provider
for provider in evalhub_providers_response["items"]
if provider["resource"]["id"] == "lm_evaluation_harness"
)
Copy link
Copy Markdown
Collaborator

@dbasunag dbasunag Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any next() call should be used with an explicit default. We are trying to move to python 3.14 and they tightened this in 3.14

Copy link
Copy Markdown
Collaborator

@dbasunag dbasunag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment on next(). Won't hold the PR for that, but please be aware, #1160 is about pinning to 3.14

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/model_explainability/evalhub/test_evalhub_providers.py (1)

153-159: ⚠️ Potential issue | 🟡 Minor

Assert the 403 contract on response.status_code, not the exception text.

match="403" only checks the rendered message. These tests should capture the HTTPError and assert excinfo.value.response.status_code == 403, the same way the 404 path is already pinned.

Suggested change
-        with pytest.raises(requests.exceptions.HTTPError, match="403"):
+        with pytest.raises(requests.exceptions.HTTPError) as excinfo:
             list_evalhub_providers(
                 host=evalhub_route.host,
                 token=evalhub_unauthorised_token,
                 ca_bundle_file=evalhub_ca_bundle_file,
                 tenant=model_namespace.name,
             )
+        assert excinfo.value.response is not None
+        assert excinfo.value.response.status_code == 403
@@
-        with pytest.raises(requests.exceptions.HTTPError, match="403"):
+        with pytest.raises(requests.exceptions.HTTPError) as excinfo:
             get_evalhub_provider(
                 host=evalhub_route.host,
                 token=evalhub_unauthorised_token,
                 ca_bundle_file=evalhub_ca_bundle_file,
                 provider_id=provider_id,
                 tenant=model_namespace.name,
             )
+        assert excinfo.value.response is not None
+        assert excinfo.value.response.status_code == 403
In pytest 9, does `pytest.raises(..., match="403")` only match the exception message, and when `requests.Response.raise_for_status()` raises `requests.exceptions.HTTPError`, is the originating `response.status_code` available on `exception.response`?

Also applies to: 171-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_explainability/evalhub/test_evalhub_providers.py` around lines
153 - 159, The test should assert the HTTP status via the exception's response,
not by matching the exception text: replace the pytest.raises(..., match="403")
usage around the call to list_evalhub_providers (and the similar block at lines
171-178) with a context manager that captures the exception (e.g., using "with
pytest.raises(requests.exceptions.HTTPError) as excinfo:") and then assert
excinfo.value.response.status_code == 403; keep the same call parameters (host,
token, ca_bundle_file, tenant) so the test verifies the HTTP contract rather
than the rendered message.
🧹 Nitpick comments (1)
tests/model_explainability/evalhub/conftest.py (1)

78-90: Drop the unused evalhub_deployment dependency from these ServiceAccount fixtures.

These fixtures do not read evalhub_deployment, so they force SA/token setup to wait on EvalHub rollout for no effect. That adds avoidable setup latency and couples RBAC fixtures to deployment readiness.

Suggested change
 def evalhub_scoped_sa(
     admin_client: DynamicClient,
     model_namespace: Namespace,
-    evalhub_deployment: Deployment,
 ) -> Generator[ServiceAccount, Any, Any]:
@@
 def evalhub_unauthorised_sa(
     admin_client: DynamicClient,
     model_namespace: Namespace,
-    evalhub_deployment: Deployment,
 ) -> Generator[ServiceAccount, Any, Any]:

Also applies to: 140-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_explainability/evalhub/conftest.py` around lines 78 - 90, The
fixture evalhub_scoped_sa currently depends on evalhub_deployment but never uses
it, causing unnecessary test setup latency; remove the unused parameter from the
fixture signature (delete evalhub_deployment from the argument list of the
evalhub_scoped_sa function) and update any other ServiceAccount fixtures that
similarly list evalhub_deployment (e.g., the other fixture around lines 140-152)
to drop that dependency so SA/token setup no longer waits on EvalHub rollout;
keep the ServiceAccount context manager code (ServiceAccount(...)) and yield
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_explainability/evalhub/test_evalhub_providers.py`:
- Around line 167-170: Guard the provider lookup in the test by verifying
evalhub_providers_response["items"] is non-empty before indexing; if empty, skip
the test (or assert a meaningful failure) so an IndexError cannot occur.
Specifically update the section that computes provider_id from
evalhub_providers_response (the line using
evalhub_providers_response["items"][0]["resource"]["id"]) to first check
len(evalhub_providers_response.get("items", [])) > 0 (or use a test skip) and
only then extract provider_id and continue to the 403 assertion.

---

Duplicate comments:
In `@tests/model_explainability/evalhub/test_evalhub_providers.py`:
- Around line 153-159: The test should assert the HTTP status via the
exception's response, not by matching the exception text: replace the
pytest.raises(..., match="403") usage around the call to list_evalhub_providers
(and the similar block at lines 171-178) with a context manager that captures
the exception (e.g., using "with pytest.raises(requests.exceptions.HTTPError) as
excinfo:") and then assert excinfo.value.response.status_code == 403; keep the
same call parameters (host, token, ca_bundle_file, tenant) so the test verifies
the HTTP contract rather than the rendered message.

---

Nitpick comments:
In `@tests/model_explainability/evalhub/conftest.py`:
- Around line 78-90: The fixture evalhub_scoped_sa currently depends on
evalhub_deployment but never uses it, causing unnecessary test setup latency;
remove the unused parameter from the fixture signature (delete
evalhub_deployment from the argument list of the evalhub_scoped_sa function) and
update any other ServiceAccount fixtures that similarly list evalhub_deployment
(e.g., the other fixture around lines 140-152) to drop that dependency so
SA/token setup no longer waits on EvalHub rollout; keep the ServiceAccount
context manager code (ServiceAccount(...)) and yield behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 0dbee5e2-c14e-48f2-9d8a-978a3650306f

📥 Commits

Reviewing files that changed from the base of the PR and between 663ce8d and 06d8faf.

📒 Files selected for processing (2)
  • tests/model_explainability/evalhub/conftest.py
  • tests/model_explainability/evalhub/test_evalhub_providers.py

@ruivieira
Copy link
Copy Markdown
Member Author

Closing this as there is a new set of tests for multi-tenant EvalHub that will replace the ones in this PR.

@ruivieira ruivieira closed this Mar 20, 2026
auto-merge was automatically disabled March 20, 2026 15:38

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants