Skip to content

chore: promote main to stable#629

Closed
github-actions[bot] wants to merge 64 commits intostablefrom
main
Closed

chore: promote main to stable#629
github-actions[bot] wants to merge 64 commits intostablefrom
main

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

Automated promotion of 64 commit(s) from main to stable.

572b783 revert: rm payload processing (#609)
2c748c3 fix: resolve RBAC namespace mismatch for RHOAI deployments (#625)
169213a feat: add salt encryption for API Key hashing (#619)
fb42db2 fix: standardize CRDs (#520)
17a81e2 docs: fix namespace inconsistency (#618)
461f66d feat: auto-create models-as-a-service namespace on controller startup (#591)
3e9d355 fix: resolve MaaSModelRef stuck state by handling HTTPRoute race condition (#600)
5d9bd72 docs: remove reliance on subscription header in inference and models endpoint (#614)
95b149d fix: address review feedback from ExternalModel CRD https://github.com/opendatahub-io/models-as-a-service/pull/586 (#612)
66781e6 test: add go coverage tests (#510)
2de4b7e feat: refactor MaasModelRef into ExternalModel CRs (#586)
a0419a0 feat: remove reliance on subscription header in inference and models endpoint (#584)
eef694a feat: replace admin check with SAR checks (#588)
ded9da5 fix: lock odh to ea1 (#604)
6090cea feat: add ExternalModel support with provider, endpoint, and credentialRef fields (#571)
1245a8d feat: complete the payload processing move (#602)
6df5036 chore(deps): bump google.golang.org/grpc from 1.71.1 to 1.79.3 in /maas-controller (#565)
362563d fix: move azureai api translator to the right place (#596)
a4ff436 feat(payload-processing): add model-provider-resolver and api-translation plugins (#595)
bda9e68 feat(payload-processing): add apikey-injection plugin (#594)
d87cb69 feat: add Vertex AI (Gemini) api-translation translator (#593)
a26787b [Feat]: Add Azure OpenAI API translation plugin (#592)
535a818 feat: move payload processing from a separate repo into a dir (#589)
2547fa1 feat(ci): add multi-arch image builds for maas (#570)
1cd39ca chore: sync security config files (#470)
e85a928 refactor: simplify TokenRateLimitPolicy by trusting AuthPolicy validation (#543)
4807f87 fix: include username in api key search query results (#580)
f23b0b9 style: update makefile help msg according to k8s best practices (#583)
17deedb feat: add subscriptions endpoint (#572)
9cdcd63 fix: use MaaS API key for validate-deployment.sh (#577)

jrhyness and others added 30 commits March 13, 2026 20:24
<!--- Provide a general summary of your changes in the Title above -->
   
## Description
  <!--- Describe your changes in detail -->

  This PR fixes two related bugs in the `/v1/models` endpoint:

### Bug 1: Empty Results
([RHOAIENG-52646](https://issues.redhat.com/browse/RHOAIENG-52646))

**Problem:** `/v1/models` returned empty list `{"data": []}` even when
MaaSModelRefs existed and users had proper authorization.

**Root Cause:** The maas-api searched for MaaSModelRefs only in its own
namespace (`opendatahub`), but after PR #483, all MaaSModelRefs are
deployed in the `llm` namespace (where
models live). The informer watches all namespaces, but the code filtered
results to only include models from a single namespace.

**Solution:** Remove namespace filtering entirely. Since models can be
deployed in any namespace, the `/v1/models` endpoint now returns models
from all namespaces that are accessible
   to the user.

  **Changes:**
  - Remove `maasModelNamespace` parameter from `ModelsHandler`
  - Remove `namespace` parameter from `MaaSModelRefLister` interface
  - Remove namespace filtering logic in `cluster_config.go`
  - Update unit tests to match new interface

### Bug 2: Null Data
([RHOAIENG-52505](https://issues.redhat.com/browse/RHOAIENG-52505))

**Problem:** `/v1/models` returned `{"data": null}` instead of `{"data":
[]}` when users had no accessible models.

**Root Cause:** Two locations initialized model slices using `var
modelList []Model` (nil slice) instead of `modelList := []Model{}`
(empty slice). In Go, nil slices marshal to JSON
  `null` while empty slices marshal to `[]`.

**Solution:** Initialize model slices to empty slices in both locations:
  - `handlers/models.go`: Initialize `modelList` at declaration
  - `models/discovery.go`: Initialize `out` in `FilterModelsByAccess`

  **Changes:**
  - `handlers/models.go:159`: `modelList := []models.Model{}`
  - `models/discovery.go:75`: `out := []Model{}`

  ## How Has This Been Tested?
  <!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
  <!--- see how your change affects other areas of the code, etc. -->

  **Unit Tests:**
  ```bash
  cd maas-api
  go test ./internal/handlers/... -v
  go test ./internal/models/... -v
  All existing tests pass with updated interfaces.

E2E Tests: Using new tests from
#509
  cd test/e2e
  ./scripts/prow_run_smoke_test.sh

  Results:
- test_single_subscription_auto_select: ✅ XPASS (now returns models from
all namespaces)
- test_explicit_subscription_header: ✅ XPASS (now returns models from
all namespaces)
  - test_empty_model_list: ✅ XPASS (now returns [] instead of null)
  - All other /v1/models tests: ✅ PASS

  Manual Testing:
  1. Deployed fix to test cluster
  2. Created test service account with no model access
3. Verified /v1/models returns {"object": "list", "data": []} (not null)
  4. Created service account with model access in llm namespace
  5. Verified /v1/models returns models from llm namespace (not empty)

  Environment:
  - OpenShift cluster with MaaS components deployed
  - maas-controller watching models-as-a-service namespace
  - Models deployed in llm namespace (post PR #483)
  - Kuadrant/Authorino for authorization
  - PostgreSQL backend for API key storage


## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Fixed JSON responses returning null for empty model lists; now
correctly returns empty arrays.

* **Refactor**
* Simplified model listing to retrieve models across all namespaces
instead of single namespace filtering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…controller (#454)

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from
0.39.0 to 0.45.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/crypto/commit/4e0068c0098be10d7025c99ab7c50ce454c1f0f9"><code>4e0068c</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/crypto/commit/e79546e28b85ea53dd37afe1c4102746ef553b9c"><code>e79546e</code></a>
ssh: curb GSSAPI DoS risk by limiting number of specified OIDs</li>
<li><a
href="https://github.com/golang/crypto/commit/f91f7a7c31bf90b39c1de895ad116a2bacc88748"><code>f91f7a7</code></a>
ssh/agent: prevent panic on malformed constraint</li>
<li><a
href="https://github.com/golang/crypto/commit/2df4153a0311bdfea44376e0eb6ef2faefb0275b"><code>2df4153</code></a>
acme/autocert: let automatic renewal work with short lifetime certs</li>
<li><a
href="https://github.com/golang/crypto/commit/bcf6a849efcf4702fa5172cb0998b46c3da1e989"><code>bcf6a84</code></a>
acme: pass context to request</li>
<li><a
href="https://github.com/golang/crypto/commit/b4f2b62076abeee4e43fb59544dac565715fbf1e"><code>b4f2b62</code></a>
ssh: fix error message on unsupported cipher</li>
<li><a
href="https://github.com/golang/crypto/commit/79ec3a51fcc7fbd2691d56155d578225ccc542e2"><code>79ec3a5</code></a>
ssh: allow to bind to a hostname in remote forwarding</li>
<li><a
href="https://github.com/golang/crypto/commit/122a78f140d9d3303ed3261bc374bbbca149140f"><code>122a78f</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/crypto/commit/c0531f9c34514ad5c5551e2d6ce569ca673a8afd"><code>c0531f9</code></a>
all: eliminate vet diagnostics</li>
<li><a
href="https://github.com/golang/crypto/commit/0997000b45e3a40598272081bcad03ffd21b8adb"><code>0997000</code></a>
all: fix some comments</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/crypto/compare/v0.39.0...v0.45.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/crypto&package-manager=go_modules&previous-version=0.39.0&new-version=0.45.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/opendatahub-io/models-as-a-service/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…in /maas-api (#461)

Bumps
[go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go)
from 1.37.0 to 1.40.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel/sdk's
changelog</a>.</em></p>
<blockquote>
<h2>[1.40.0/0.62.0/0.16.0] 2026-02-02</h2>
<h3>Added</h3>
<ul>
<li>Add <code>AlwaysRecord</code> sampler in
<code>go.opentelemetry.io/otel/sdk/trace</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7724">#7724</a>)</li>
<li>Add <code>Enabled</code> method to all synchronous instrument
interfaces (<code>Float64Counter</code>,
<code>Float64UpDownCounter</code>, <code>Float64Histogram</code>,
<code>Float64Gauge</code>, <code>Int64Counter</code>,
<code>Int64UpDownCounter</code>, <code>Int64Histogram</code>,
<code>Int64Gauge</code>,) in
<code>go.opentelemetry.io/otel/metric</code>.
This stabilizes the synchronous instrument enabled feature, allowing
users to check if an instrument will process measurements before
performing computationally expensive operations. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7763">#7763</a>)</li>
<li>Add <code>go.opentelemetry.io/otel/semconv/v1.39.0</code> package.
The package contains semantic conventions from the <code>v1.39.0</code>
version of the OpenTelemetry Semantic Conventions.
See the <a
href="https://github.com/open-telemetry/opentelemetry-go/blob/main/semconv/v1.39.0/MIGRATION.md">migration
documentation</a> for information on how to upgrade from
<code>go.opentelemetry.io/otel/semconv/v1.38.0.</code> (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7783">#7783</a>,
<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7789">#7789</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Improve the concurrent performance of
<code>HistogramReservoir</code> in
<code>go.opentelemetry.io/otel/sdk/metric/exemplar</code> by 4x. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7443">#7443</a>)</li>
<li>Improve the concurrent performance of
<code>FixedSizeReservoir</code> in
<code>go.opentelemetry.io/otel/sdk/metric/exemplar</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7447">#7447</a>)</li>
<li>Improve performance of concurrent histogram measurements in
<code>go.opentelemetry.io/otel/sdk/metric</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7474">#7474</a>)</li>
<li>Improve performance of concurrent synchronous gauge measurements in
<code>go.opentelemetry.io/otel/sdk/metric</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7478">#7478</a>)</li>
<li>Add experimental observability metrics in
<code>go.opentelemetry.io/otel/exporters/stdout/stdoutmetric</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7492">#7492</a>)</li>
<li><code>Exporter</code> in
<code>go.opentelemetry.io/otel/exporters/prometheus</code> ignores
metrics with the scope
<code>go.opentelemetry.io/contrib/bridges/prometheus</code>.
This prevents scrape failures when the Prometheus exporter is
misconfigured to get data from the Prometheus bridge. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7688">#7688</a>)</li>
<li>Improve performance of concurrent exponential histogram measurements
in <code>go.opentelemetry.io/otel/sdk/metric</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7702">#7702</a>)</li>
<li>The <code>rpc.grpc.status_code</code> attribute in the experimental
metrics emitted from
<code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code>
is replaced with the <code>rpc.response.status_code</code> attribute to
align with the semantic conventions. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7854">#7854</a>)</li>
<li>The <code>rpc.grpc.status_code</code> attribute in the experimental
metrics emitted from
<code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code>
is replaced with the <code>rpc.response.status_code</code> attribute to
align with the semantic conventions. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7854">#7854</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix bad log message when key-value pairs are dropped because of key
duplication in <code>go.opentelemetry.io/otel/sdk/log</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7662">#7662</a>)</li>
<li>Fix <code>DroppedAttributes</code> on <code>Record</code> in
<code>go.opentelemetry.io/otel/sdk/log</code> to not count the
non-attribute key-value pairs dropped because of key duplication. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7662">#7662</a>)</li>
<li>Fix <code>SetAttributes</code> on <code>Record</code> in
<code>go.opentelemetry.io/otel/sdk/log</code> to not log that attributes
are dropped when they are actually not dropped. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7662">#7662</a>)</li>
<li>Fix missing <code>request.GetBody</code> in
<code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code>
to correctly handle HTTP/2 <code>GOAWAY</code> frame. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7794">#7794</a>)</li>
<li><code>WithHostID</code> detector in
<code>go.opentelemetry.io/otel/sdk/resource</code> to use full path for
<code>ioreg</code> command on Darwin (macOS). (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7818">#7818</a>)</li>
</ul>
<h3>Deprecated</h3>
<ul>
<li>Deprecate <code>go.opentelemetry.io/otel/exporters/zipkin</code>.
For more information, see the <a
href="https://opentelemetry.io/blog/2025/deprecating-zipkin-exporters/">OTel
blog post deprecating the Zipkin exporter</a>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7670">#7670</a>)</li>
</ul>
<h2>[1.39.0/0.61.0/0.15.0/0.0.14] 2025-12-05</h2>
<h3>Added</h3>
<ul>
<li>Greatly reduce the cost of recording metrics in
<code>go.opentelemetry.io/otel/sdk/metric</code> using hashing for map
keys. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7175">#7175</a>)</li>
<li>Add <code>WithInstrumentationAttributeSet</code> option to
<code>go.opentelemetry.io/otel/log</code>,
<code>go.opentelemetry.io/otel/metric</code>, and
<code>go.opentelemetry.io/otel/trace</code> packages.
This provides a concurrent-safe and performant alternative to
<code>WithInstrumentationAttributes</code> by accepting a
pre-constructed <code>attribute.Set</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7287">#7287</a>)</li>
<li>Add experimental observability for the Prometheus exporter in
<code>go.opentelemetry.io/otel/exporters/prometheus</code>.
Check the
<code>go.opentelemetry.io/otel/exporters/prometheus/internal/x</code>
package documentation for more information. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7345">#7345</a>)</li>
<li>Add experimental observability metrics in
<code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7353">#7353</a>)</li>
<li>Add temporality selector functions
<code>DeltaTemporalitySelector</code>,
<code>CumulativeTemporalitySelector</code>,
<code>LowMemoryTemporalitySelector</code> to
<code>go.opentelemetry.io/otel/sdk/metric</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7434">#7434</a>)</li>
<li>Add experimental observability metrics for simple log processor in
<code>go.opentelemetry.io/otel/sdk/log</code>. (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7548">#7548</a>)</li>
<li>Add experimental observability metrics in
<code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc</code>.
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7459">#7459</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/a3a5317c5caed1656fb5b301b66dfeb3c4c944e0"><code>a3a5317</code></a>
Release v1.40.0 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7859">#7859</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/77785da545d67b38774891cbdd334368bfacdfd8"><code>77785da</code></a>
chore(deps): update github/codeql-action action to v4.32.1 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7858">#7858</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/56fa1c297bf71f0ada3dbf4574a45d0607812cc0"><code>56fa1c2</code></a>
chore(deps): update module github.com/clipperhouse/uax29/v2 to v2.5.0
(<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7857">#7857</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/298cbedf256b7a9ab3c21e41fc5e3e6d6e4e94aa"><code>298cbed</code></a>
Upgrade semconv use to v1.39.0 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7854">#7854</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/3264bf171b1e6cd70f6be4a483f2bcb84eda6ccf"><code>3264bf1</code></a>
refactor: modernize code (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7850">#7850</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/fd5d030c0aa8b5bfe786299047bc914b5714d642"><code>fd5d030</code></a>
chore(deps): update module github.com/grpc-ecosystem/grpc-gateway/v2 to
v2.27...</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/8d3b4cb2501dec9f1c5373123e425f109c43b8d2"><code>8d3b4cb</code></a>
chore(deps): update actions/cache action to v5.0.3 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7847">#7847</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/91f7cadfcac363d67030f6913687c6dbbe086823"><code>91f7cad</code></a>
chore(deps): update github.com/timakin/bodyclose digest to 73d1f95 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7845">#7845</a>)</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/fdad1eb7f350ee1f5fdb3d9a0c6855cc88ee9d75"><code>fdad1eb</code></a>
chore(deps): update module github.com/grpc-ecosystem/grpc-gateway/v2 to
v2.27...</li>
<li><a
href="https://github.com/open-telemetry/opentelemetry-go/commit/c46d3bac181ddaaa83286e9ccf2cd9f7705fd3d9"><code>c46d3ba</code></a>
chore(deps): update golang.org/x/telemetry digest to fcf36f6 (<a
href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7843">#7843</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.37.0...v1.40.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=go.opentelemetry.io/otel/sdk&package-manager=go_modules&previous-version=1.37.0&new-version=1.40.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/opendatahub-io/models-as-a-service/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->

Related to - https://issues.redhat.com/browse/RHOAIENG-49790

**User story**

As an operator currently using the tier-based setup (ConfigMap,
gateway-auth-policy),
I want a clear migration guide to move to the subscription model
(MaaSModel, MaaSAuthPolicy, MaaSSubscription),
So that I can adopt the new model without breaking production.

**Acceptance criteria**
Given the existing old-vs-new-flow doc,
When an operator follows the migration guide,
Then the guide provides steps to move from tier ConfigMap +
gateway-auth-policy to MaaSModel + MaaSAuthPolicy + MaaSSubscription.

Given ODH Model Controller cleanup (AuthPolicy removal) is done or
planned,
When the guide is read,
Then it covers that ODH no longer relies on gateway-auth-policy and
maas-controller install.
Given the guide does not assume API key minting or list-subscriptions
endpoints,
When an operator migrates,
Then auth for inference can remain OpenShift token (or existing
mechanism).

**Testability**: Doc review; optionally follow the guide in a test env
and verify subscription flow works.

**Documentation**: Migration guide (new or supplement to
old-vs-new-flow).

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

Tested on the cluster -
https://console-openshift-console.apps.ci-ln-ir3wk52-76ef8.aws-4.ci.openshift.org

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service %  ./scripts/migrate-tier-to-subscription.sh --tier premium \
       --models model-a,model-b,model-c \
       --groups premium-users \
       --rate-limit 50000 \
       --output migration-crs/premium/
✅ Created output directory: migration-crs/premium/
ℹ️  Migration Configuration:
ℹ️  Generating MaaSModelRef CRs...
✅ Generated: migration-crs/premium//maasmodelref-model-a.yaml
✅ Generated: migration-crs/premium//maasmodelref-model-b.yaml
✅ Generated: migration-crs/premium//maasmodelref-model-c.yaml
ℹ️  Generating MaaSAuthPolicy CR...
✅ Generated: migration-crs/premium//maasauthpolicy-premium.yaml
ℹ️  Generating MaaSSubscription CR...
✅ Generated: migration-crs/premium//maassubscription-premium.yaml

✅ Migration CRs generated successfully!

ℹ️  Summary:

ℹ️  Output directory: migration-crs/premium/
maasauthpolicy-premium.yaml
maasmodelref-model-a.yaml
maasmodelref-model-b.yaml
maasmodelref-model-c.yaml
maassubscription-premium.yaml

ℹ️  Next steps:

✅ Migration script completed!
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % 
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % ./scripts/migrate-tier-to-subscription.sh --tier free \
       --models simulator,qwen3 \
       --groups system:authenticated \
       --rate-limit 100 \
       --apply


✅ Created output directory: migration-crs
ℹ️  Migration Configuration:
ℹ️  Generating MaaSModelRef CRs...
✅ Generated: migration-crs/maasmodelref-simulator.yaml
✅ Generated: migration-crs/maasmodelref-qwen3.yaml
ℹ️  Generating MaaSAuthPolicy CR...
✅ Generated: migration-crs/maasauthpolicy-free.yaml
ℹ️  Generating MaaSSubscription CR...
✅ Generated: migration-crs/maassubscription-free.yaml

✅ Migration CRs generated successfully!

ℹ️  Summary:

ℹ️  Output directory: migration-crs
maasauthpolicy-enterprise.yaml
maasauthpolicy-free.yaml
maasmodelref-e2e-unconfigured-facebook-opt-125m-simulated.yaml
maasmodelref-facebook-opt-125m-simulated.yaml
maasmodelref-premium-simulated-simulated-premium.yaml
maasmodelref-qwen3.yaml
maasmodelref-simulator.yaml
maassubscription-enterprise.yaml
maassubscription-free.yaml
premium

ℹ️  Applying CRs to cluster...
maasauthpolicy.maas.opendatahub.io/enterprise-models-access unchanged
maasauthpolicy.maas.opendatahub.io/free-models-access configured
maasmodelref.maas.opendatahub.io/e2e-unconfigured-facebook-opt-125m-simulated unchanged
maasmodelref.maas.opendatahub.io/facebook-opt-125m-simulated unchanged
maasmodelref.maas.opendatahub.io/premium-simulated-simulated-premium unchanged
maasmodelref.maas.opendatahub.io/qwen3 configured
maasmodelref.maas.opendatahub.io/simulator configured
maassubscription.maas.opendatahub.io/enterprise-models-subscription unchanged
maassubscription.maas.opendatahub.io/free-models-subscription configured

✅ CRs applied successfully!

ℹ️  Validating deployment...
ℹ️  Checking MaaSModelRef status...
⚠️    simulator: Failed
⚠️    qwen3: Failed

ℹ️  Checking generated Kuadrant policies...
⚠️  Not all policies created yet. Controller may still be reconciling.
ℹ️  Check maas-controller logs: kubectl logs -n opendatahub -l app.kubernetes.io/name=maas-controller

ℹ️  Next steps:

✅ Migration script completed!
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % 
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % ./scripts/migrate-tier-to-subscription.sh --tier enterprise \
       --models $(kubectl get llminferenceservice -n llm -o name | cut -d/ -f2 | tr '\n' ',') \
       --groups enterprise-users \
       --rate-limit 100000 \
       --apply \
       --verbose
✅ Created output directory: migration-crs
ℹ️  Migration Configuration:
     Tier: enterprise
     Models: 3 (e2e-unconfigured-facebook-opt-125m-simulated,facebook-opt-125m-simulated,premium-simulated-simulated-premium,)
     Groups: 1 (enterprise-users)
     Rate Limit: 100000 tokens per 1m
     Output: migration-crs
     Namespaces: MaaS=opendatahub, Subscription=models-as-a-service, Model=llm
     Mode: GENERATE + APPLY
ℹ️  Generating MaaSModelRef CRs...
✅ Generated: migration-crs/maasmodelref-e2e-unconfigured-facebook-opt-125m-simulated.yaml
✅ Generated: migration-crs/maasmodelref-facebook-opt-125m-simulated.yaml
✅ Generated: migration-crs/maasmodelref-premium-simulated-simulated-premium.yaml
ℹ️  Generating MaaSAuthPolicy CR...
✅ Generated: migration-crs/maasauthpolicy-enterprise.yaml
ℹ️  Generating MaaSSubscription CR...
✅ Generated: migration-crs/maassubscription-enterprise.yaml

✅ Migration CRs generated successfully!

ℹ️  Summary:
     Tier: enterprise
     Models: 3
     Groups: 1
     Files generated:
       - 3 MaaSModelRef CRs
       - 1 MaaSAuthPolicy CR
       - 1 MaaSSubscription CR

ℹ️  Output directory: migration-crs
   Files:
maasauthpolicy-enterprise.yaml
maasauthpolicy-free.yaml
maasmodelref-e2e-unconfigured-facebook-opt-125m-simulated.yaml
maasmodelref-facebook-opt-125m-simulated.yaml
maasmodelref-premium-simulated-simulated-premium.yaml
maasmodelref-qwen3.yaml
maasmodelref-simulator.yaml
maassubscription-enterprise.yaml
maassubscription-free.yaml
premium

ℹ️  Applying CRs to cluster...
maasauthpolicy.maas.opendatahub.io/enterprise-models-access configured
maasauthpolicy.maas.opendatahub.io/free-models-access unchanged
maasmodelref.maas.opendatahub.io/e2e-unconfigured-facebook-opt-125m-simulated configured
maasmodelref.maas.opendatahub.io/facebook-opt-125m-simulated configured
maasmodelref.maas.opendatahub.io/premium-simulated-simulated-premium configured
maasmodelref.maas.opendatahub.io/qwen3 unchanged
maasmodelref.maas.opendatahub.io/simulator unchanged
maassubscription.maas.opendatahub.io/enterprise-models-subscription configured
maassubscription.maas.opendatahub.io/free-models-subscription unchanged

✅ CRs applied successfully!

ℹ️  Validating deployment...
ℹ️  Checking MaaSModelRef status...
✅   e2e-unconfigured-facebook-opt-125m-simulated: Ready
✅   facebook-opt-125m-simulated: Ready
✅   premium-simulated-simulated-premium: Ready

ℹ️  Checking generated Kuadrant policies...
     AuthPolicies created: 0 (expected: 3)
     TokenRateLimitPolicies created: 0 (expected: 3)
⚠️  Not all policies created yet. Controller may still be reconciling.
ℹ️  Check maas-controller logs: kubectl logs -n opendatahub -l app.kubernetes.io/name=maas-controller

ℹ️  Next steps:
     1. Test model access with users in tier 'enterprise' groups
     2. Validate rate limiting is working as expected
     3. Once validated, remove tier annotations from models
     4. Remove old gateway-auth-policy and tier-based TokenRateLimitPolicy

✅ Migration script completed!
```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added a comprehensive migration guide covering prerequisites,
zero-downtime and full-cutover strategies, phased workflows, per-model
transition examples, verification, rollback, troubleshooting, a
conversion worksheet, and updated navigation and cross-references.

* **New Features**
* Added a migration CLI to convert tier configurations into subscription
resources, with dry-run/apply modes, parameter options, and basic
post-apply validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->
Currently, if `deploy.sh --deployment-mode kustomize` is run multiple
times, it won't work because `kubectl apply --server-side=true`
conflicts with previous `configure_cluster_audience`. By adding env var
`KUSTOMIZE_FORCE_CONFLICTS` and setting it to true, `deploy.sh
--deployment-mode kustomize` can be idempotent.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
- `./scripts/deploy.sh --deployment-mode kustomize` -> Failure
- `KUSTOMIZE_FORCE_CONFLICTS=true ./scripts/deploy.sh --deployment-mode
kustomize` -> Success

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added `KUSTOMIZE_FORCE_CONFLICTS` environment variable for deployment
configurations (defaults to `false`).

* **Documentation**
* Updated environment variable documentation to include the new
configuration option.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!--- Provide a general summary of your changes in the Title above -->

## Description

Related to https://issues.redhat.com/browse/RHOAIENG-47840

In maas-api/cmd/main.go:59–70, debug mode enables an unrestricted CORS
policy. While this was mainly intended to support local development
(e.g., self-serving the OpenAPI schema), leaving these settings in place
creates a risk if debug mode is ever enabled in production.
If misconfigured, this could allow unintended cross-origin access,
increasing the potential for CSRF-style abuse or data exfiltration.

Debug mode is currently off by default and the API uses Bearer tokens
(not cookies), which lowers the immediate risk. However, to prevent
accidental exposure to dangerous behavior, the first step should be to
remove these permissive CORS settings (or strictly scope them to a safe
development-only mode).

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

- Unit tests are added.
- Tested on the cluster -
https://console-openshift-console.apps.ci-ln-idrxfgk-76ef8.aws-2.ci.openshift.org/dashboards

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % oc port-forward deployment/maas-api 8080:8080 -n maas-api 2>&1
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % echo "=== TEST 1: Localhost origin (should be ALLOWED) ===" && curl -si http://localhost:8080/health -H "Origin: http://localhost:3000" | grep -iE "HTTP/|Access-Control|^$" && echo "" && echo "=== TEST 2: External origin (should be REJECTED with 403) ===" && curl -si http://localhost:8080/health -H "Origin: https://evil.com" | grep -iE "HTTP/|Access-Control|^$" && echo "" && echo "=== TEST 3: Preflight from localhost (should be 204 with CORS headers) ===" && curl -si -X OPTIONS http://localhost:8080/health -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Authorization" | grep -iE "HTTP/|Access-Control|^$" && echo "" && echo "=== TEST 4: Preflight from external (should have NO CORS headers) ===" && curl -si -X OPTIONS http://localhost:8080/health -H "Origin: https://evil.com" -H "Access-Control-Request-Method: POST" | grep -iE "HTTP/|Access-Control|^$" && echo "" && echo "=== TEST 5: Credentials NOT allowed ===" && curl -si http://localhost:8080/health -H "Origin: http://localhost:3000" | grep -i "Access-Control-Allow-Credentials" && echo "(empty = PASS: no credentials header)" && echo ""                                     
=== TEST 1: Localhost origin (should be ALLOWED) ===
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Expose-Headers: Content-Type

=== TEST 2: External origin (should be REJECTED with 403) ===
HTTP/1.1 403 Forbidden

=== TEST 3: Preflight from localhost (should be 204 with CORS headers) ===
HTTP/1.1 204 No Content
Access-Control-Allow-Headers: Authorization,Content-Type,Accept
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Max-Age: 43200
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers

=== TEST 4: Preflight from external (should have NO CORS headers) ===
HTTP/1.1 403 Forbidden

=== TEST 5: Credentials NOT allowed ===
```


## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Debug-mode CORS now only allows localhost origins, blocking external
cross-origin requests while preserving local development access.

* **Tests**
* Added comprehensive tests covering localhost vs external origins,
preflight behavior, credential handling, same-origin requests, and
debug-mode toggles.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…516)

<!--- Provide a general summary of your changes in the Title above -->
https://issues.redhat.com/browse/RHOAIENG-48332

## Description
<!--- Describe your changes in detail -->
This PR:
- updates the way MaaSAuthPolicy generates Kuadrant AuthPolicy to
enforce "fail-close" logic for inference endpoint when subscription
endpoint is down.
- updates `cleanup-odh.sh` in accordance with new namespace structure.
- clean up some loose ends on comments from previous PRs

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
Before the change:
- Inference requests with subscription header get `200`, otherwise get
`403`.
- With subscription URL in AuthPolicy tampered, inference requests with
or without subscription header get `200` ("fail-open").
After the change:
- Inference requests with subscription header get `200`, otherwise get
`403`.
- With subscription URL in AuthPolicy tampered, inference requests with
or without subscription header get `403` ("fail-close").

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Stricter subscription validation in authentication policies: requests
now fail when subscription name is missing.

* **Tests**
* Updated end-to-end test expectations to reflect rebuilt policy
behavior after deletion operations.

* **Chores**
* Improved namespace cleanup: added an extra deletion step and enhanced
handling to remove finalizers from additional resource types before
namespace removal.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…#528)

## Description
<!--- Describe your changes in detail -->

Related to https://redhat.atlassian.net/browse/RHOAIENG-52941

The test test_multiple_subscriptions_different_namespaces_same_model
intermittently fails in Prow with a 429 (Too Many Requests) when calling
the model listing endpoint. The test expects 200 but receives 429.

**Observed failure:**
```

r = _list_models(api_key, MODEL_REF, MODEL_NAMESPACE, subscription=sub1_name)
assert r.status_code == 200, f"Model listing failed: {r.status_code}"
AssertionError: Model listing failed: 429
assert 429 == 200
Test location: test/e2e/tests/test_namespace_scoping.py — TestCrossNamespaceSubscription.test_multiple_subscriptions_different_namespaces_same_model
```

**Test flow:**

- Creates two MaaSSubscription}}s in different namespaces for the same
model (sub1: {{system:authenticated, 100/min; sub2: test_group, 200/min)
- Waits RECONCILE_WAIT (default 10s) for reconciliation
- Waits for AuthPolicy to be enforced
- Verifies TokenRateLimitPolicy exists
- Calls _list_models with subscription=sub1_name — fails here with 429

**Suspected Causes**
Rate limit exhaustion: Previous tests in the suite may have consumed the
rate limit for the same model/API key before this test runs

TRLP reconciliation timing: TokenRateLimitPolicy or Limitador may not be
fully synced when the test runs; RECONCILE_WAIT (10s) may be
insufficient under Prow load

Shared rate limit state: Multiple subscriptions aggregating into one
TRLP may have unexpected interaction with Limitador counters

Gateway default TRLP: Default deny or low default limits may apply
before subscription-specific limits are enforced

**Acceptance Criteria**
```
Given the Prow E2E suite runs test_multiple_subscriptions_different_namespaces_same_model,
When the test creates MaaSSubscriptions and calls the model listing endpoint with a valid subscription,
Then the response is 200, not 429.
```

```
Given the test runs in isolation (e.g. pytest test_namespace_scoping.py::TestCrossNamespaceSubscription::test_multiple_subscriptions_different_namespaces_same_model),
When run multiple times,
Then the test passes consistently (or root cause is identified if 429 persists).
```

**Verification**
Reproduce: Run full e2e suite in Prow or locally; observe intermittent
429
Isolate: Run test_multiple_subscriptions_different_namespaces_same_model
in isolation; check if 429 still occurs
Increase E2E_RECONCILE_WAIT (e.g. 15–20s) and re-run; check if failure
rate decreases
Inspect Limitador/TRLP state and gateway logs when 429 occurs

**Root cause:** 
MaaSSubscription resources were being created in random test namespaces,
but the maas-controller only watches the models-as-a-service namespace
(configured via MAAS_SUBSCRIPTION_NAMESPACE). This meant subscriptions
were never reconciled, TokenRateLimitPolicy was never updated, and
requests hit the default-deny policy resulting in 429 errors.

**Changes:**
- Fixed test_subscription_in_different_namespace to use
MAAS_SUBSCRIPTION_NAMESPACE
- Fixed test_multiple_subscriptions_different_namespaces_same_model to
use MAAS_SUBSCRIPTION_NAMESPACE
- Added _wait_for_trlp_enforced() to check TokenRateLimitPolicy
enforcement status
- Added _wait_for_trlp_enforced_with_retry() with 3 retry attempts and
5s backoff
- Added _get_trlp_generation() to track TRLP metadata.generation changes
- Added _get_trlp_metadata() to track uid/resourceVersion/generation for
verification
- Increased RECONCILE_WAIT from 10s to 15s for better Prow tolerance
      - Added TRLP enforcement waits before making requests
      - Added TRLP re-enforcement wait after subscription deletion
      - Updated test docstrings to clarify namespace requirements

      CodeRabbit fixes:
- Fixed race condition: moved TRLP enforcement wait before existence
checks
- Added comprehensive metadata verification to ensure TRLP was actually
updated
- Fixed type annotation: expected_generation parameter now uses
Optional[int]

**Test Results:**
- Before: ~0% success rate (intermittent 429 errors)
- After: 100% success rate (5/5 consecutive test runs passed)

The test is now production-ready and will pass reliably in Prow CI.

<!--- Provide a general summary of your changes in the Title above -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->


Tested on live cluster :

 ```
GATEWAY_HOST=maas.apps.ci-ln-3rxf0wb-76ef8.aws-4.ci.openshift.org
E2E_SKIP_TLS_VERIFY=true

MAAS_API_BASE_URL=https://maas.apps.ci-ln-3rxf0wb-76ef8.aws-4.ci.openshift.org/maas-api
E2E_RECONCILE_WAIT=15 (default)
```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful messages.
- [x] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Tests**
  * Improved end-to-end coverage for cross-namespace subscription and rate-limit policy enforcement.
  * Increased reconciliation timeout and added stronger wait/retry logic to reduce flakiness.
  * Added metadata and generation checks to verify policy presence, updates, and recreation after subscription changes.
  * Enhanced multi-subscription scenarios, cleanup, and logging to better validate cross-namespace interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
## Description
<!--- Describe your changes in detail -->
This PR adds comprehensive E2E test coverage for the `/v1/models`
endpoint that validates subscription-aware model filtering and access
control.
**Test Coverage:**
- **Single subscription auto-selection**: Users with one subscription
can list models without providing `x-maas-subscription` header
- **Explicit subscription header**: Users with multiple subscriptions
can select which subscription to use
- **Multiple subscriptions requiring header**: Validates proper 403
error when header is missing
- **Subscription filtering and validation**: Ensures models are filtered
by subscription access
- **Model deduplication scenarios**: Tests handling of multiple
modelRefs serving the same model
- **Empty model lists**: Validates response when user has no accessible
models
- **Response schema validation**: Ensures API responses match
OpenAI-compatible schema
- **Error cases**: Tests 401 (unauthenticated) and 403 (forbidden)
scenarios
**Sample Resources:**
- Added two distinct test models (`e2e-distinct-simulated`,
`e2e-distinct-2-simulated`) to enable multi-model subscription testing
- Added corresponding MaaSModelRef kustomize configurations for test
deployment
**Known Issues (marked as xfail):**
Three tests expose a known bug where `/v1/models` returns all accessible
models instead of filtering by the selected subscription. These are
marked with `@pytest.mark.xfail` and will
pass once the filtering bug is fixed:
- `test_single_subscription_auto_select`
  - `test_explicit_subscription_header`
  - `test_multiple_distinct_models_in_subscription`

  ## How Has This Been Tested?
  <!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
  <!--- see how your change affects other areas of the code, etc. -->

  **Test Execution:**
  ```bash
  cd test/e2e
  ./scripts/prow_run_smoke_test.sh

  Results:
  - 12 tests passed
  - 3 tests xfailed (expected failures due to known bug)
  - 0 tests failed
  - All tests run in isolated namespaces with proper cleanup

  Environment:
  - OpenShift cluster with MaaS components deployed
  - maas-controller watching models-as-a-service namespace
  - Models deployed in llm namespace
  - Kuadrant/Authorino for authorization
  - PostgreSQL backend for API key storage

  Validation:
  - Verified subscription-based access control works correctly
  - Verified API key authentication flow
  - Verified OpenAI-compatible response format
  - Verified proper error handling for unauthorized access


## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added sample Kustomize and model manifests (including MaaSModelRef) to
demonstrate distinct multi-model deployments and updated top-level
samples to include distinct and distinct-2 entries.
* **Tests**
* Added comprehensive end-to-end tests for the /v1/models endpoint
covering subscription-aware filtering, multi-model scenarios, auth/error
paths and deduplication checks; updated test runner to include them and
added env vars to support multi-model testing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…le instead of being hardcoded (#527)

During testing on the lates RHOAI operator build noticed that the
namespace for our auth policy is still opendatahub.

I updated it so it should be pulling from the `params.env` now. Also
changes the url to try to make it easier to identify this mistake
earlier (fail fast).

How did I test:

Can change the app_namespace in the `params.env` to whatever
`my_totally_correct_namespace` and validate the url looks correct
```
jland@fedora:~/Documents/RedHat/poc/models-as-a-service$ 
kustomize build deployment/overlays/odh | grep -E "url:|api-keys/validate"
          url: https://maas-api.my_totally_correct_namespace.svc.cluster.local:8443/internal/v1/api-keys/validate
```

And I checked the actual prams being installed with the operator and
that looks right.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated internal authentication policy configuration to use
overlay-based URL replacement pattern, improving deployment flexibility
across environments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!--- Provide a general summary of your changes in the Title above -->

## Description
Related to - https://redhat.atlassian.net/browse/RHOAIENG-53772

Background
PR #527 updated the maas-api auth policy to use a placeholder URL
(maas-api.placehold.svc) instead of hardcoded namespace (opendatahub).
The placeholder gets replaced via kustomize replacements in the ODH
overlay using the app-namespace parameter from params.env. This change
was intended to support flexible namespace deployment for different
operators (ODH vs RHOAI).

Problem
The TLS backend (tls-backend) and HTTP backend (http-backend) overlays
used in kustomize deployment mode do not have:
  - params.env file with namespace configuration
  - ConfigMap generator for parameters
  - Kustomize replacements to patch the placeholder URL

This caused the auth policy to be deployed with the literal placeholder
URL

https://maas-api.placehold.svc.cluster.local:8443/internal/v1/api-keys/validate,
breaking API key authentication.

  Steps to Reproduce
  1. Deploy MaaS using kustomize mode:
  ./scripts/deploy.sh --deployment-mode kustomize --enable-tls-backend
  2. Check the deployed AuthPolicy:
kubectl get authpolicy maas-api-auth-policy -n opendatahub -o yaml |
grep "api-keys/validate"

  Expected Behavior
The auth policy URL should be patched to use the actual deployment
namespace:
url:
https://maas-api.opendatahub.svc.cluster.local:8443/internal/v1/api-keys/validate

  Actual Behavior
  The auth policy URL contains the unpatched placeholder:
url:
https://maas-api.placehold.svc.cluster.local:8443/internal/v1/api-keys/validate

This causes API key validation requests to fail with DNS resolution
errors, breaking all API key-based authentication.

  Root Cause
The kustomize overlays (tls-backend, http-backend) lack the replacement
configuration that exists in the odh overlay:

  ODH overlay has:
  - params.env with app-namespace=opendatahub
  - ConfigMapGenerator creating maas-parameters
- Kustomize replacement targeting
spec.rules.metadata.apiKeyValidation.http.url

  TLS/HTTP backend overlays missing:
  - All of the above components

  Impact

- Severity: High - Breaks API key authentication completely in kustomize
deployment mode
- Scope: Affects all deployments using --deployment-mode kustomize (both
TLS and HTTP backends)
  - Workaround: None - deployment is broken for kustomize mode
- Operator mode: Not affected (ODH/RHOAI overlays have proper
replacement logic)

  Solution
Added sed-based patching in scripts/deploy.sh to replace the placeholder
URL with the actual namespace during kustomize build:
kustomize build "$overlay" | sed
"s/maas-api\.placehold\.svc/maas-api.$NAMESPACE.svc/g"

Design Decision: Chose script-based patching over duplicating params.env
files across three overlays to maintain DRY principles
  and avoid configuration drift.

  Files Changed
1. scripts/deploy.sh:577 - Added sed patching to replace placeholder
with $NAMESPACE
2. deployment/base/maas-api/policies/auth-policy.yaml:32-34 - Updated
comment to document both patching methods (ODH overlay
  replacement vs deploy.sh sed)

  ```
Testing/Verification

  ✅ Tested on cluster:
  ./scripts/deploy.sh --deployment-mode kustomize --enable-tls-backend

  ✅ Verified URL patching:
kubectl get authpolicy maas-api-auth-policy -n opendatahub -o yaml |
grep "api-keys/validate"
# Output: url:
https://maas-api.opendatahub.svc.cluster.local:8443/internal/v1/api-keys/validate

  ✅ Verified both overlays work:
  - tls-backend overlay: ✅ URL correctly patched
  - http-backend overlay: ✅ URL correctly patched
  - odh overlay: ✅ Still works with kustomize replacements

  ✅ CodeRabbit review: No findings
```

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

Tested on live cluster 
```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % oc login
https://api.ci-ln-hlkdrfk-76ef8.aws-4.ci.openshift.org:6443 --web
The server uses a certificate signed by an unknown authority.
You can bypass the certificate check, but any data you send to the
server could be intercepted by others.
Use insecure connections? (y/n): y

WARNING: Using insecure TLS client config. Setting this option is not
supported!

Opening login URL in the default browser:
https://oauth-openshift.apps.ci-ln-hlkdrfk-76ef8.aws-4.ci.openshift.org/oauth/authorize?client_id=openshift-cli-client&code_challenge=fvLt3bBVCbvYdSatndrZg_2MK5869FIZ7abjlUxLDsA&code_challenge_method=S256&redirect_uri=http%3A%2F%2F127.0.0.1%3A52210%2Fcallback&response_type=code
Login successful.

You have access to 74 projects, the list has been suppressed. You can
list all projects with 'oc projects'

Using project "default".
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service %
./scripts/deploy.sh --deployment-mode kustomize --enable-tls-backend
[INFO] ===================================================
[INFO]   Models-as-a-Service Deployment
[INFO] ===================================================
[INFO] Validating configuration...
[INFO] Configuration validated successfully
[INFO] Deployment configuration:
[INFO]   Mode: kustomize
[INFO]   Policy Engine: kuadrant
[INFO]   Namespace: opendatahub
[INFO]   TLS Backend: true
[INFO] Starting kustomize-based deployment...
[INFO] Installing policy engine: kuadrant
[INFO] Installing Kuadrant v1.3.1 (upstream community)
[INFO] Creating Kuadrant v1.3.1 catalog source...
namespace/kuadrant-system created
catalogsource.operators.coreos.com/kuadrant-operator-catalog created
[INFO] Waiting for Kuadrant catalog to be ready...
operatorgroup.operators.coreos.com/kuadrant-operator-group created
[INFO] Installing operator: kuadrant-operator in namespace:
kuadrant-system
namespace/kuadrant-system condition met
[INFO] Creating Subscription for kuadrant-operator from
kuadrant-operator-catalog (channel: stable)
subscription.operators.coreos.com/kuadrant-operator created
[INFO] Waiting for subscription to install...
* Waiting for Subscription kuadrant-system/kuadrant-operator to start
setup...
subscription.operators.coreos.com/kuadrant-operator condition met
* Waiting for Subscription setup to finish setup. CSV =
kuadrant-operator.v1.3.1 ...
clusterserviceversion.operators.coreos.com/kuadrant-operator.v1.3.1
condition met
[INFO] Operator kuadrant-operator installed successfully
[INFO] Patching kuadrant-operator CSV for OpenShift Gateway
controller...
clusterserviceversion.operators.coreos.com/kuadrant-operator.v1.3.1
patched
[INFO] CSV patched for OpenShift Gateway controller
[INFO] Forcing operator restart to apply new Gateway controller
configuration...
pod "kuadrant-operator-controller-manager-54b54c8744-smx2q" force
deleted from kuadrant-system namespace
pod "kuadrant-operator-controller-manager-68d7ff44d6-srsgr" force
deleted from kuadrant-system namespace
pod "limitador-operator-controller-manager-84d8fbb794-zp7mt" force
deleted from kuadrant-system namespace
[INFO] Waiting for operator pod to restart...
Waiting for deployment "kuadrant-operator-controller-manager" rollout to
finish: 1 old replicas are pending termination...
Waiting for deployment "kuadrant-operator-controller-manager" rollout to
finish: 1 old replicas are pending termination...
Waiting for deployment "kuadrant-operator-controller-manager" rollout to
finish: 1 old replicas are pending termination...
deployment "kuadrant-operator-controller-manager" successfully rolled
out
[WARN] Operator pod may not have correct env yet: 
[INFO] Waiting 15s for operator to fully initialize with Gateway
controller configuration...
[INFO] Initializing Gateway API and ModelsAsService gateway...
[INFO] Setting up Gateway API infrastructure...
gatewayclass.gateway.networking.k8s.io/openshift-default created
[INFO] Setting up ModelsAsService gateway...
[INFO]   Cluster domain: apps.ci-ln-hlkdrfk-76ef8.aws-4.ci.openshift.org
[INFO]   Detecting TLS certificate secret...
[INFO] * Found certificate from router deployment: router-certs-default
[INFO]   TLS certificate secret: router-certs-default
[INFO] Creating maas-default-gateway resource (allowing routes from all
namespaces)...
gateway.gateway.networking.k8s.io/maas-default-gateway
serverside-applied
[INFO] Waiting for Gateway to be Programmed (Service Mesh
initialization)...
gateway.gateway.networking.k8s.io/maas-default-gateway condition met
[INFO] Applying Kuadrant custom resource in kuadrant-system...
kuadrant.kuadrant.io/kuadrant created
[INFO] Waiting for Kuadrant to become ready (initial check)...
[INFO] Waiting for: Kuadrant ready in kuadrant-system (timeout: 60s)
[WARN] Kuadrant ready in kuadrant-system - Timeout after 60s
[INFO] Kuadrant shows MissingDependency - restarting operator to
re-register Gateway controller...
pod "authorino-76d7b84c9-8d5dj" force deleted from kuadrant-system
namespace
pod "kuadrant-operator-controller-manager-68d7ff44d6-lqn8z" force
deleted from kuadrant-system namespace
pod "limitador-operator-controller-manager-84d8fbb794-95tvc" force
deleted from kuadrant-system namespace
[INFO] Retrying Kuadrant readiness check after operator restart...
[INFO] Waiting for: Kuadrant ready in kuadrant-system (timeout: 120s)
[INFO] Kuadrant ready in kuadrant-system - Ready
[INFO] Kuadrant setup complete
[INFO] Using TLS backend overlay
[INFO] Creating namespace: opendatahub
namespace/opendatahub created
[INFO] Deploying PostgreSQL for API key storage...
[INFO] Generated random PostgreSQL password (stored in secret
postgres-creds)
[INFO]   Creating PostgreSQL deployment...
[INFO]   ⚠️  Using POC configuration (ephemeral storage)
secret/postgres-creds created
deployment.apps/postgres created
service/postgres created
secret/maas-db-config created
[INFO]   Waiting for PostgreSQL to be ready...
deployment.apps/postgres condition met
[INFO]   PostgreSQL deployed successfully
[INFO]   Database: maas
[INFO]   User: maas
[INFO]   Secret: maas-db-config (contains DB_CONNECTION_URL)
[INFO] 
[INFO] ⚠️ For production, use AWS RDS, Crunchy Operator, or Azure
Database
[INFO]   Note: Schema migrations run automatically when maas-api starts
[INFO] Applying kustomize manifests...

customresourcedefinition.apiextensions.k8s.io/maasauthpolicies.maas.opendatahub.io
serverside-applied

customresourcedefinition.apiextensions.k8s.io/maasmodelrefs.maas.opendatahub.io
serverside-applied

customresourcedefinition.apiextensions.k8s.io/maassubscriptions.maas.opendatahub.io
serverside-applied
serviceaccount/maas-api serverside-applied
serviceaccount/maas-controller serverside-applied
role.rbac.authorization.k8s.io/maas-controller-leader-election-role
serverside-applied
clusterrole.rbac.authorization.k8s.io/maas-api serverside-applied
clusterrole.rbac.authorization.k8s.io/maas-controller-role
serverside-applied

rolebinding.rbac.authorization.k8s.io/maas-controller-leader-election-rolebinding
serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/maas-api serverside-applied

clusterrolebinding.rbac.authorization.k8s.io/maas-controller-rolebinding
serverside-applied
configmap/tier-to-group-mapping serverside-applied
service/maas-api serverside-applied
deployment.apps/maas-api serverside-applied
deployment.apps/maas-controller serverside-applied
httproute.gateway.networking.k8s.io/maas-api-route serverside-applied
authpolicy.kuadrant.io/maas-api-auth-policy serverside-applied
destinationrule.networking.istio.io/maas-api-backend-tls
serverside-applied
networkpolicy.networking.k8s.io/maas-authorino-allow serverside-applied
[INFO] Applying gateway policies (openshift-ingress)...
authpolicy.kuadrant.io/gateway-default-auth serverside-applied
tokenratelimitpolicy.kuadrant.io/gateway-default-deny serverside-applied
[INFO] Configuring TLS backend for Authorino and MaaS API...
* Waiting for deployment/authorino in kuadrant-system...
  * Found deployment/authorino
[INFO] Running TLS configuration script...
[INFO] TLS configuration script completed successfully
[INFO] Restarting deployments to pick up TLS configuration...
deployment.apps/maas-api restarted
deployment.apps/authorino restarted
[INFO] Waiting for Authorino deployment to be ready...
Waiting for deployment "authorino" rollout to finish: 1 old replicas are
pending termination...
Waiting for deployment "authorino" rollout to finish: 1 old replicas are
pending termination...
deployment "authorino" successfully rolled out
[INFO] TLS backend configuration complete
[INFO] Checking cluster OIDC audience...
[INFO] Standard Kubernetes audience detected, no patching needed
[INFO] Kustomize deployment completed
[INFO] 
[INFO] MaaS Subscription Controller...
[INFO] Controller deployed via kustomize overlay
(deployment/base/maas-controller/default)
[INFO]   Waiting for maas-controller to be ready...
deployment "maas-controller" successfully rolled out
[INFO]   Subscription controller ready.
[INFO] Create MaaSModelRef, MaaSAuthPolicy, and MaaSSubscription to
enable per-model auth and rate limiting.
[INFO] ===================================================
[INFO]   Deployment completed successfully!
[INFO] ===================================================
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % kubectl get
authpolicy maas-api-auth-policy -n opendatahub -o yaml | grep -A2
"api-keys/validate"
url:
https://maas-api.opendatahub.svc.cluster.local:8443/internal/v1/api-keys/validate
        metrics: false
        priority: 0

```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful messages.
- [x] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved deployment configuration to automatically replace placeholder API URLs with actual namespace-specific addresses, ensuring correct API connectivity across different deployment environments and reducing manual configuration errors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Description    

For https://redhat.atlassian.net/browse/RHOAIENG-52971
This PR moves the subscription selection endpoint from
`/v1/subscriptions/select` to `/internal/v1/subscriptions/select`,
aligning it with the existing internal endpoint pattern used
by `/internal/v1/api-keys/validate`.
**Background:**
The subscription selection endpoint is exclusively used as an HTTP
callback by Authorino during the authentication/authorization flow. It
is not intended for direct user consumption
and should not be part of the public `/v1` API.
**Changes:**
- **maas-api/cmd/main.go**: Moved endpoint registration from `v1Routes`
to `internalRoutes` group
- **maas-controller/.../maasauthpolicy_controller.go**: Updated
AuthPolicy controller to call the new
`/internal/v1/subscriptions/select` URL
- **test/e2e/scripts/auth_utils.sh**: Updated test scripts to use new
endpoint path
- **test/e2e/tests/test_subscription.py**: Updated test documentation
comments
- **maas-api/architecture.md**: Added endpoint to API reference table
for consistency with other internal endpoints
**Impact:**
- ✅ No user-facing breaking changes (endpoint was never part of public
API)
- ✅ Only caller is Authorino via AuthPolicy (internal)
- ✅ Improves API architecture clarity by properly distinguishing public
vs internal endpoints
- ✅ Consistent with existing `/internal/v1/api-keys/validate` pattern
## How Has This Been Tested?

  ### Unit Tests
  Ran the subscription handler unit test suite:
  ```bash
  cd maas-api && go test -v ./internal/subscription/...
  Result: ✅ All 7 tests passed

  E2E Subscription Tests

  Ran the complete subscription E2E test suite:
=============================================================== test
session starts
================================================================
platform linux -- Python 3.13.12, pytest-8.4.2, pluggy-1.6.0 --
/home/jrhyness/src/models-as-a-service/test/e2e/.venv/bin/python
  cachedir: .pytest_cache
metadata: {'Python': '3.13.12', 'Platform':
'Linux-6.19.6-100.fc42.x86_64-x86_64-with-glibc2.41', 'Packages':
{'pytest': '8.4.2', 'pluggy': '1.6.0'}, 'Plugins': {'metadata': '3.1.1',
   'html': '4.2.0'}}
  rootdir: /home/jrhyness/src/models-as-a-service/test/e2e
  plugins: metadata-3.1.1, html-4.2.0
  collected 31 items


tests/test_subscription.py::TestAuthEnforcement::test_authorized_user_gets_200
PASSED
tests/test_subscription.py::TestAuthEnforcement::test_no_auth_gets_401
PASSED

tests/test_subscription.py::TestAuthEnforcement::test_invalid_token_gets_403
PASSED

tests/test_subscription.py::TestAuthEnforcement::test_wrong_group_gets_403
PASSED

tests/test_subscription.py::TestSubscriptionEnforcement::test_subscribed_user_gets_200
PASSED

tests/test_subscription.py::TestSubscriptionEnforcement::test_auth_pass_no_subscription_gets_403
PASSED

tests/test_subscription.py::TestSubscriptionEnforcement::test_invalid_subscription_header_gets_429
PASSED

tests/test_subscription.py::TestSubscriptionEnforcement::test_explicit_subscription_header_works
PASSED

tests/test_subscription.py::TestSubscriptionEnforcement::test_rate_limit_exhaustion_gets_429
PASSED

tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_user_in_one_of_two_subscriptions_gets_200
PASSED

tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_multi_tier_auto_select_highest
PASSED

tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_two_auth_policies_or_logic
PASSED

tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_delete_one_auth_policy_other_still_works
PASSED

tests/test_subscription.py::TestCascadeDeletion::test_delete_subscription_rebuilds_trlp
PASSED

tests/test_subscription.py::TestCascadeDeletion::test_delete_last_subscription_denies_access
PASSED

tests/test_subscription.py::TestOrderingEdgeCases::test_subscription_before_auth_policy
PASSED

tests/test_subscription.py::TestManagedAnnotation::test_authpolicy_managed_false_prevents_update
PASSED

tests/test_subscription.py::TestManagedAnnotation::test_trlp_managed_false_prevents_update
PASSED

tests/test_subscription.py::TestMaasSubscriptionNamespace::test_authpolicy_and_subscription_in_maas_subscription_namespace
PASSED

tests/test_subscription.py::TestMaasSubscriptionNamespace::test_authpolicy_and_subscription_in_another_namespace
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_with_both_access_and_subscription_gets_200
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_with_access_but_no_subscription_gets_403
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_with_subscription_but_no_access_gets_403
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_single_subscription_auto_selects
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_multiple_subscriptions_without_header_gets_403
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_multiple_subscriptions_with_valid_header_gets_200
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_multiple_subscriptions_with_invalid_header_gets_403
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_multiple_subscriptions_with_inaccessible_header_gets_403
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_group_based_access_gets_200
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_group_based_auth_but_no_subscription_gets_403
PASSED

tests/test_subscription.py::TestE2ESubscriptionFlow::test_e2e_group_based_subscription_but_no_auth_gets_403
PASSED

=================================================================
warnings summary
=================================================================
  tests/test_subscription.py: 29 warnings

/home/jrhyness/src/models-as-a-service/test/e2e/.venv/lib64/python3.13/site-packages/urllib3/connectionpool.py:1097:
InsecureRequestWarning: Unverified HTTPS request is being made
to host 'maas.apps.rosa.u22hm-zn2m5-w2g.1u73.p3.openshiftapps.com'.
Adding certificate verification is strongly advised. See:

https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
      warnings.warn(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=================================================== 31 passed, 29
warnings in 492.93s (0:08:12)
====================================================

Result: ✅ 31/31 tests passed - All subscription authentication,
authorization, multi-subscription, cascade deletion, and E2E flow tests
passed successfully.

  Manual Verification

- ✅ Verified AuthPolicy resources are correctly generated with new
internal URL
- ✅ Confirmed subscription selection callback works correctly during
Authorino flow
- ✅ Validated that old /v1/subscriptions/select endpoint no longer
exists

  Merge criteria:

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Reorganized internal API endpoint structure for subscription selection
to align with authentication workflow architecture.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
## Summary

- Add `git clone` and `cd` instructions before the first `deploy.sh`
command

Users need to clone the repository before running `./scripts/deploy.sh`.
The quickstart assumed users already had the repo cloned without stating
this.

---
*Created by document-review workflow `/create-prs` phase.*

Signed-off-by: Mynhardt Burger <mynhardt@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ore (#487)

<!--- Provide a general summary of your changes in the Title above -->

Related to https://issues.redhat.com/browse/RHOAIENG-51673

## Description
Summary
Remove dead or unused testing code and scripts (e.g. redundant
deploy/run wrappers, unused test helpers) and consolidate or remove
redundant .gitignore files under test/ so that the test layout is clear
and maintainable.

Story
As a maintainer, I want dead testing code and unused test scripts (e.g.
duplicate deploy/run scripts) removed and redundant {{.gitignore}}s
cleaned up, so that contributors and CI use a single clear test entry
point and we avoid duplicate or obsolete test paths.

Acceptance Criteria
Audit test/ (e.g. test/e2e/run-model-and-smoke.sh,
test/e2e/bootstrap.sh, test/maas_billing_tests_independent/, and other
test scripts): remove or consolidate scripts that are unused, duplicated
by prow_run_smoke_test.sh or smoke.sh, or obsolete. Keep only the
scripts that are part of the intended flow (e.g. smoke.sh, prow script,
auth_utils).
Remove redundant .gitignore files (e.g. under test/e2e/,
test/maas_billing_tests_independent/) where patterns are already covered
by the root .gitignore or can be merged into a single test .gitignore
without losing needed exclusions.
No broken references to removed scripts; e2e and any documented test
flows still run.

Testability / Verification
Run e2e (prow script or smoke.sh) and confirm tests still pass. Confirm
no references to deleted scripts remain (grep or docs). Verify
.gitignore behavior: ignored paths are still ignored and nothing
necessary is accidentally tracked.

## How Has This Been Tested?
Tested on cluster
https://console-openshift-console.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % cd /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service && SKIP_DEPLOYMENT=true SKIP_VALIDATION=true E2E_SKIP_TLS_VERIFY=true ./test/e2e/scripts/prow_run_smoke_test.sh

----------------------------------------
Deploying Maas on OpenShift
----------------------------------------

Checking prerequisites...
✅ Prerequisites met - logged in as: kube:admin on OpenShift
  Skipping deployment (SKIP_DEPLOYMENT=true)
  Assuming MaaS platform and models are already deployed

----------------------------------------
Setting up variables for tests
----------------------------------------

-- Setting up variables for tests --
K8S_CLUSTER_URL: https://api.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org:6443
HOST: maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org
MAAS_API_BASE_URL: https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/maas-api
CLUSTER_DOMAIN: apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org
✅ Variables for tests setup completed

----------------------------------------
Admin Setup (Premium Test Resources)
----------------------------------------

Setting up premium test token (SA-based, works when oc whoami -t is unavailable)...
Creating namespace: premium-users-namespace
namespace/premium-users-namespace created
Creating service account: premium-service-account
serviceaccount/premium-service-account created
Patching MaaSAuthPolicy premium-simulator-access to include system:serviceaccount:premium-users-namespace:premium-service-account...
maasauthpolicy.maas.opendatahub.io/premium-simulator-access patched
Patching MaaSSubscription premium-simulator-subscription to include system:serviceaccount:premium-users-namespace:premium-service-account...
maassubscription.maas.opendatahub.io/premium-simulator-subscription patched
✅ Premium test token setup complete (E2E_TEST_TOKEN_SA_* exported)

----------------------------------------
Setting up test tokens
----------------------------------------

Setting up test tokens (admin + regular user)...
Current admin session: kube:admin (will be preserved)
✅ Admin token for kube:admin (added to odh-admins)
✅ Regular user token for kube:admin (same as admin for local testing)
Token setup complete (main session unchanged: kube:admin)

----------------------------------------
Running E2E Tests
----------------------------------------

-- E2E Tests (API Keys + Subscription) --
Running e2e tests with:
  - TOKEN: sha256~viySFSVlUVxuR...
  - ADMIN_OC_TOKEN: sha256~viySFSVlUVxuR...
  - GATEWAY_HOST: maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org
============================= test session starts ==============================
platform darwin -- Python 3.13.3, pytest-8.4.2, pluggy-1.6.0 -- /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/.venv/bin/python
cachedir: .pytest_cache
metadata: {'Python': '3.13.3', 'Platform': 'macOS-15.7.4-arm64-arm-64bit-Mach-O', 'Packages': {'pytest': '8.4.2', 'pluggy': '1.6.0'}, 'Plugins': {'metadata': '3.1.1', 'html': '4.2.0'}}
rootdir: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service
plugins: metadata-3.1.1, html-4.2.0
collected 30 items                                                             

test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_create_api_key [token] using env TOKEN (masked): 50
[create] Created key id=811be98a-21ed-4d11-943a-8be772581549, key prefix=sk-oai-dwfx7AGZ...
PASSED [  3%]
test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_list_api_keys FAILED [  6%]
test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_revoke_api_key [revoke] Key eeffd5f5-f7e5-44f8-b0f2-a0c6550fd0e2 successfully revoked
PASSED [ 10%]
test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_admin_manage_other_users_keys [admin_token] using env ADMIN_OC_TOKEN (masked): 50
[admin] User 'kube:admin' created key ccedb884-8137-4c55-a17f-98679a78e541
[admin] Admin listed 8 keys for 'kube:admin'
[admin] Admin successfully revoked user's key ccedb884-8137-4c55-a17f-98679a78e541
PASSED [ 13%]
test/e2e/tests/test_api_keys.py::T            estAPIKeyAuthorization::test_non_a                        dmin_cannot_access_other_users_key                                   s FAILED [ 16%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_own_keys [bulk-revoke] Successfully revoked 11 keys for user kube:admin
PASSED [ 20%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_other_user_forbidden [bulk-revoke] Non-admin correctly got 403 when trying to bulk revoke other user's keys
PASSED [ 23%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_admin_can_revoke_any_user [bulk-revoke] Admin successfully revoked 1 keys for user kube:admin
PASSED [ 26%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_model_access_success [api_key] Creating API key for inference tests...
[api_key] Created API key id=ec224f1b-2e8f-424a-8559-1b174926df6c, key prefix=sk-oai-wqwZduWS...
[inference] Model access succeeded: facebook/opt-125m, tokens=12
PASSED [ 30%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_invalid_api_key_rejected [inference] Invalid API key correctly rejected with 403
PASSED [ 33%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_no_auth_header_rejected [inference] Missing auth header correctly rejected with 401
PASSED [ 36%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_revoked_api_key_rejected [inference] Key before revoke: HTTP 200
[inference] Revoked key correctly rejected with 403
PASSED [ 40%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_chat_completions [inference] Chat completions succeeded: facebook/opt-125m
PASSED [ 43%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_explicit_subscription_header [inference] API key with x-maas-subscription header succeeded
PASSED [ 46%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_invalid_subscription_header [inference] API key with invalid subscription correctly rejected with 403
PASSED [ 50%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_authorized_user_gets_200 PASSED [ 53%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_no_auth_gets_401 PASSED [ 56%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_invalid_token_gets_403 PASSED [ 60%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_wrong_group_gets_403 PASSED [ 63%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_subscribed_user_gets_200 PASSED [ 66%]
test/e2e/tests/test_subscription.py::TestSubscriptionEn        t_auth_pass_no_subscription_gets_403 PASSED [ 70%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_invalid_subscription_header_gets_429 PASSED [ 73%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_explicit_subscription_header_works PASSED [ 76%]
test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_user_in_one_of_two_subscriptions_gets_200 PASSED [ 80%]
test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_multi_tier_auto_select_highest PASSED [ 83%]
test/e2e/tests/test_subscription.py:odel::test_two_auth_policies_or_logPASSED [ 86%]
test/e2e/tests/test_subscription.py:                       :TestMultipleAuthPoliciesPerModel::t                                            est_delete_one_auth_policy_other_sti            ll_works PASSED [ 90%]
test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_subscription_rebuilds_trlp PASSED [ 93%]
test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_last_subscription_denies_access PASSED [ 96%]
test/e2e/tests/test_subscription.py::TestOrderingEdgeCases::test_subscription_before_auth_policy PASSED [100%]

=================================== FAILURES ===================================
______________________ TestAPIKeyCRUD.test_list_api_keys _______________________

self = <test_api_keys.TestAPIKeyCRUD object at 0x1044fb4d0>
api_keys_base_url = 'https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~viySFSVlUVxuRXmhBeLaoOIbus4VoJtEVowPviu6z24', 'Content-Type': 'application/json'}

    def test_list_api_keys(self, api_keys_base_url: str, headers: dict):
        """Test 2: List own keys - verify basic functionality."""
        # Create two keys
        r1 = requests.post(api_keys_base_url, headers=headers, json={"name": "test-key-list-1"}, timeout=30, verify=TLS_VERIFY)
        assert r1.status_code in (200, 201)
        key1_id = r1.json()["id"]
    
        r2 = requests.post(api_keys_base_url, headers=headers, json={"name": "test-key-list-2"}, timeout=30, verify=TLS_VERIFY)
        assert r2.status_code in (200, 201)
        key2_id = r2.json()["id"]
    
        # List keys using search endpoint
        r = requests.post(
            f"{api_keys_base_url}/search",
            headers=headers,
            json={
                "filters": {"status": ["active"]},
                "sort": {"by": "created_at", "order": "desc"},
                "pagination": {"limit": 50, "offset": 0}
            },
            timeout=30,
            verify=TLS_VERIFY
        )
>       assert r.status_code == 200
E       assert 500 == 200
E        +  where 500 = <Response [500]>.status_code

test/e2e/tests/test_api_keys.py:94: AssertionError
____ TestAPIKeyAuthorization.test_non_admin_cannot_access_other_users_keys _____

self = <test_api_keys.TestAPIKeyAuthorization object at 0x1044fb610>
api_keys_base_url = 'https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~viySFSVlUVxuRXmhBeLaoOIbus4VoJtEVowPviu6z24', 'Content-Type': 'application/json'}
admin_headers = {'Authorization': 'Bearer sha256~viySFSVlUVxuRXmhBeLaoOIbus4VoJtEVowPviu6z24', 'Content-Type': 'application/json'}

    def test_non_admin_cannot_access_other_users_keys(self, api_keys_base_url: str, headers: dict, admin_headers: dict):
        """Test 5: Non-admin cannot access other user's keys - verify denial.
    
        Note: API returns 404 instead of 403 for IDOR protection (prevents key enumeration).
        This is a security best practice - returning 403 would reveal the key exists.
        """
        if not admin_headers:
            pytest.skip("ADMIN_OC_TOKEN not set")
    
        # Admin creates a key
        r_admin = requests.post(api_keys_base_url, headers=admin_headers, json={"name": "admin-only-key"}, timeout=30, verify=TLS_VERIFY)
        assert r_admin.status_code in (200, 201)
        admin_key_id = r_admin.json()["id"]
    
        # Regular user tries to GET admin's key - returns 404 for IDOR protection
        r_get = requests.get(f"{api_keys_base_url}/{admin_key_id}", headers=headers, timeout=30, verify=TLS_VERIFY)
>       assert r_get.status_code == 404, f"Expected 404 (IDOR protection), got {r_get.status_code}"
E       AssertionError: Expected 404 (IDOR protection), got 200
E       assert 200 == 404
E        +  where 200 = <Response [200]>.status_code

test/e2e/tests/test_api_keys.py:205: AssertionError
- generated xml file: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/e2e-kube:admin.xml -
- Generated html report: file:///Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/e2e-kube%3Aadmin.html -
=========================== short test summary info ============================
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_list_api_keys - assert 500 == 200
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys - AssertionError: Expected 404 (IDOR protection), got 200
============ 2 failed, 28 passed, 32 warnings in 211.15s (0:03:31) =============
❌ ERROR: E2E tests failed

========== E2E Artifact Collection ==========
Artifact dir: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports
Collecting Authorino logs (token-redacted) to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/authorino-debug.log
  Saved to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/authorino-debug.log
Collecting cluster state to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports
  Saved to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/cluster-state.log
Collecting pod logs from namespace opendatahub to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/pod-logs
  Saved       16 pod log file(s) to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/pod-logs
==============================================

========== Auth Debug Report ==========

========================================
Cluster / Namespace Info
========================================

--- Current context ---
default/api-ci-ln-wxzb7yb-76ef8-aws-4-ci-openshift-org:6443/kube:admin

--- Logged-in user ---
kube:admin

--- Cluster domain ---
apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org

MAAS_NAMESPACE: opendatahub
AUTHORINO_NAMESPACE: kuadrant-system


========================================
MaaS API Deployment
========================================

--- maas-api pods ---
NAME                        READY   STATUS    RESTARTS   AGE     IP            NODE                                       NOMINATED NODE   READINESS GATES
maas-api-86b6d6f987-mcktb   1/1     Running   0          8m32s   10.128.2.44   ip-10-0-26-55.us-west-1.compute.internal   <none>           <none>

--- maas-api service ---
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE   SELECTOR
maas-api   ClusterIP   172.30.7.186   <none>        8443/TCP   20m   app.kubernetes.io/component=api,app.kubernetes.io/name=maas-api,app.kubernetes.io/part-of=models-as-a-service



========================================
maas-controller
========================================

--- maas-controller pods ---
NAME                               READY   STATUS    RESTARTS   AGE   IP            NODE                                        NOMINATED NODE   READINESS GATES
maas-controller-854bf8d4f8-cbrmc   1/1     Running   0          18m   10.129.2.25   ip-10-0-39-245.us-west-1.compute.internal   <none>           <none>

--- maas-controller MAAS_API_NAMESPACE ---
MAAS_API_NAMESPACE=metadata.namespace



========================================
Kuadrant AuthPolicies
========================================

--- AuthPolicies (all namespaces) ---
NAMESPACE           NAME                                            ACCEPTED   ENFORCED   TARGETKIND   TARGETNAME                                         TARGETSECTION   AGE
llm                 maas-auth-facebook-opt-125m-simulated           True       True       HTTPRoute    facebook-opt-125m-simulated-kserve-route                           2m2s
llm                 maas-auth-premium-simulated-simulated-premium   True       True       HTTPRoute    premium-simulated-simulated-premium-kserve-route                   57s
opendatahub         maas-api-auth-policy                            True       True       HTTPRoute    maas-api-route                                                     20m
openshift-ingress   gateway-auth-policy                             True       True       Gateway      maas-default-gateway                                               20m



========================================
MaaS CRs
========================================

--- MaaSAuthPolicies ---
NAME                       PHASE    AGE   AUTHPOLICIES
premium-simulator-access   Active   16m   maas-auth-premium-simulated-simulated-premium
simulator-access           Active   16m   maas-auth-facebook-opt-125m-simulated

--- MaaSSubscriptions ---
NAME                             PHASE    PRIORITY   AGE
premium-simulator-subscription   Active   0          16m
simulator-subscription           Active   0          86s

--- MaaSModelRefs ---
NAME                                           PHASE   ENDPOINT                                                                                                        HTTPROUTE                                                   GATEWAY                AGE
e2e-unconfigured-facebook-opt-125m-simulated   Ready   https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/llm/e2e-unconfigured-facebook-opt-125m-simulated   e2e-unconfigured-facebook-opt-125m-simulated-kserve-route   maas-default-gateway   16m
facebook-opt-125m-simulated                    Ready   https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated                    facebook-opt-125m-simulated-kserve-route                    maas-default-gateway   16m
premium-simulated-simulated-premium            Ready   https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/llm/premium-simulated-simulated-premium            premium-simulated-simulated-premium-kserve-route            maas-default-gateway   16m



========================================
Gateway / HTTPRoutes
========================================

--- Gateway ---
NAME                   CLASS               ADDRESS                                                                  PROGRAMMED   AGE
maas-default-gateway   openshift-default   a400e45cbfc0749d79007e636fa9fa9f-439495217.us-west-1.elb.amazonaws.com   True         23m

--- HTTPRoutes (maas-api) ---
NAME             HOSTNAMES   AGE
maas-api-route               20m



========================================
Authorino
========================================

--- Authorino pods ---
---



========================================
Subscription Selector Endpoint Validation
========================================

Expected URL (from maas-controller config): https://maas-api.opendatahub.svc.cluster.local:8443/v1/subscriptions/select

--- Connectivity test (from kuadrant-system, simulates Authorino) ---
curl -vsk -m 10 -X POST 'https://maas-api.opendatahub.svc.cluster.local:8443/v1/subscriptions/select' -H 'Content-Type: application/json' -d '{}'

kubectl run failed or timed out


========================================
DNS Resolution Check
========================================

Resolving: maas-api.opendatahub.svc.cluster.local
nslookup failed


======================================
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % cd /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service && E2E_SKIP_TLS_VERIFY=true MODEL_NAME=facebook-opt-125m-simulated ./test/e2e/smoke.sh
[smoke] Setting up Python virtual environment...
[smoke] Activating virtual environment
[smoke] Installing Python dependencies
[smoke] Virtual environment setup complete
[smoke] MAAS_API_BASE_URL=https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/maas-api
[smoke] Using MODEL_NAME=facebook-opt-125m-simulated
[smoke] Performing smoke test for user: kube:admin
[smoke] Setting up admin token for admin tests...
[smoke] Added kube:admin to odh-admins group
[smoke] ADMIN_OC_TOKEN configured - admin tests will run
[smoke] models catalog empty (attempt 1/10), retrying in 3s...
[smoke] models catalog empty (attempt 2/10), retrying in 3s...
[smoke] models catalog empty (attempt 3/10), retrying in 3s...
[smoke] models catalog empty (attempt 4/10), retrying in 3s...
[smoke] models catalog empty (attempt 5/10), retrying in 3s...
[smoke] models catalog empty (attempt 6/10), retrying in 3s...
[smoke] models catalog empty (attempt 7/10), retrying in 3s...
[smoke] models catalog empty (attempt 8/10), retrying in 3s...
[smoke] models catalog empty (attempt 9/10), retrying in 3s...
[smoke] models catalog empty (attempt 10/10), retrying in 3s...
[smoke] Using MODEL_URL=https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated/v1
[token] using env TOKEN (masked): 50
[create] Created key id=8631bd6e-3168-4d98-b220-d3be1043ce01, key prefix=sk-oai-tTvemsxv...
.[list] Found 3 keys
[list] Pagination works: limit=1 returned 1 items
.[revoke] Key 8ae795e9-cfe2-461a-bd15-52d97c9ffb18 successfully revoked
.[admin_token] using env ADMIN_OC_TOKEN (masked): 50
[admin] User 'kube:admin' created key 114afcd2-a1e3-48f7-b4a1-6047d094a3d1
[admin] Admin listed 4 keys for 'kube:admin'
[admin] Admin successfully revoked user's key 114afcd2-a1e3-48f7-b4a1-6047d094a3d1
.F
================================= FAILURES =================================
__ TestAPIKeyAuthorization.test_non_admin_cannot_access_other_users_keys ___

self = <test_api_keys.TestAPIKeyAuthorization object at 0x10318f4d0>
api_keys_base_url = 'https://maas.apps.ci-ln-wxzb7yb-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~viySFSVlUVxuRXmhBeLaoOIbus4VoJtEVowPviu6z24', 'Content-Type': 'application/json'}
admin_headers = {'Authorization': 'Bearer sha256~viySFSVlUVxuRXmhBeLaoOIbus4VoJtEVowPviu6z24', 'Content-Type': 'application/json'}

    def test_non_admin_cannot_access_other_users_keys(self, api_keys_base_url: str, headers: dict, admin_headers: dict):
        """Test 5: Non-admin cannot access other user's keys - verify denial.
    
        Note: API returns 404 instead of 403 for IDOR protection (prevents key enumeration).
        This is a security best practice - returning 403 would reveal the key exists.
        """
        if not admin_headers:
            pytest.skip("ADMIN_OC_TOKEN not set")
    
        # Admin creates a key
        r_admin = requests.post(api_keys_base_url, headers=admin_headers, json={"name": "admin-only-key"}, timeout=30, verify=TLS_VERIFY)
        assert r_admin.status_code in (200, 201)
        admin_key_id = r_admin.json()["id"]
    
        # Regular user tries to GET admin's key - returns 404 for IDOR protection
        r_get = requests.get(f"{api_keys_base_url}/{admin_key_id}", headers=headers, timeout=30, verify=TLS_VERIFY)
>       assert r_get.status_code == 404, f"Expected 404 (IDOR protection), got {r_get.status_code}"
E       AssertionError: Expected 404 (IDOR protection), got 200
E       assert 200 == 404
E        +  where 200 = <Response [200]>.status_code

test/e2e/tests/test_api_keys.py:205: AssertionError
- generated xml file: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/smoke-kube:admin.xml -
- Generated html report: file:///Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/smoke-kube%3Aadmin.html -
========================= short test summary info ==========================
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys - AssertionError: Expected 404 (IDOR protection), got 200
!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % 
```
Both smoke.sh and prow_run_smoke_test.sh are working on the live
cluster.


```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % git check-ignore -v \
  test/e2e/__pycache__/ \
  test/e2e/.pytest_cache/ \
  test/e2e/.venv/ \
  test/e2e/.env \
  test/e2e/reports/ \
  test/e2e/something.log \
  test/e2e/.vscode/ \
  test/e2e/.DS_Store
.gitignore:27:__pycache__/      test/e2e/__pycache__/
.gitignore:42:.pytest_cache/    test/e2e/.pytest_cache/
.gitignore:32:.venv/    test/e2e/.venv/
.gitignore:3:.env       test/e2e/.env
test/.gitignore:2:reports/      test/e2e/reports/
.gitignore:2:*.log      test/e2e/something.log

somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % git check-ignore -v \
  test/maas_billing_tests_independent/.venv/ \
  test/maas_billing_tests_independent/__pycache__/ \
  test/maas_billing_tests_independent/.pytest_cache/ \
  test/maas_billing_tests_independent/artifacts/somefile \
  test/maas_billing_tests_independent/reports/somefile \
  test/maas_billing_tests_independent/ingress-ca.crt
.gitignore:32:.venv/    test/maas_billing_tests_independent/.venv/
.gitignore:27:__pycache__/      test/maas_billing_tests_independent/__pycache__/
.gitignore:42:.pytest_cache/    test/maas_billing_tests_independent/.pytest_cache/
test/.gitignore:3:**/artifacts/*        test/maas_billing_tests_independent/artifacts/somefile
test/.gitignore:2:reports/      test/maas_billing_tests_independent/reports/somefile
test/.gitignore:7:ingress-ca.crt        test/maas_billing_tests_independent/ingress-ca.crt

somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % git check-ignore test/maas_billing_tests_independent/artifacts/.gitkeep
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % 
```

As per discussion with @SB159 , `maas_billing_tests_independent ` has
also being cleaned

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % find /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent -type f | head -30

/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/pytest.ini
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/requirements.txt
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_token_ratelimit.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/conftest.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_tool_calling.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_streaming_chat.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_limits_interplay.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_models_user.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_rbac.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/run-billing-tests.sh
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/README.MD
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_quota_global.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_maas_api_mint_and_models.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_gateway_endpoints.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_quota_per_user.py
/Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent/tests/test_tokens.py
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % ls -la /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/
total 8
drwxr-xr-x   5 somyabhatnagar  staff  160 Mar  6 23:55 .
drwxr-xr-x@ 17 somyabhatnagar  staff  544 Mar  6 04:50 ..
-rw-r--r--@  1 somyabhatnagar  staff  152 Mar  7 01:02 .gitignore
drwxr-xr-x   9 somyabhatnagar  staff  288 Mar  6 11:54 e2e
drwxr-xr-x@  5 somyabhatnagar  staff  160 Mar  6 23:55 maas_billing_tests_independent
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % rm -rf /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/maas_billing_tests_independent
```

After the cleanup, ran tests on cluster again

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % cd /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service && SKIP_DEPLOYMENT=true SKIP_VALIDATION=true E2E_SKIP_TLS_VERIFY=true ./test/e2e/scripts/prow_run_smoke_test.sh

----------------------------------------
Deploying Maas on OpenShift
----------------------------------------

Checking prerequisites...
✅ Prerequisites met - logged in as: kube:admin on OpenShift
  Skipping deployment (SKIP_DEPLOYMENT=true)
  Assuming MaaS platform and models are already deployed

----------------------------------------
Setting up variables for tests
----------------------------------------

-- Setting up variables for tests --
K8S_CLUSTER_URL: https://api.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org:6443
HOST: maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org
MAAS_API_BASE_URL: https://maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org/maas-api
CLUSTER_DOMAIN: apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org
✅ Variables for tests setup completed

----------------------------------------
Admin Setup (Premium Test Resources)
----------------------------------------

Setting up premium test token (SA-based, works when oc whoami -t is unavailable)...
Creating namespace: premium-users-namespace
namespace/premium-users-namespace created
Creating service account: premium-service-account
serviceaccount/premium-service-account created
Patching MaaSAuthPolicy premium-simulator-access to include system:serviceaccount:premium-users-namespace:premium-service-account...
maasauthpolicy.maas.opendatahub.io/premium-simulator-access patched
Patching MaaSSubscription premium-simulator-subscription to include system:serviceaccount:premium-users-namespace:premium-service-account...
maassubscription.maas.opendatahub.io/premium-simulator-subscription patched
✅ Premium test token setup complete (E2E_TEST_TOKEN_SA_* exported)

----------------------------------------
Setting up test tokens
----------------------------------------

Setting up test tokens (admin + regular user)...
Current admin session: kube:admin (will be preserved)
✅ Admin token for kube:admin (added to odh-admins)
✅ Regular user token for kube:admin (same as admin for local testing)
Token setup complete (main session unchanged: kube:admin)

----------------------------------------
Running E2E Tests
----------------------------------------

-- E2E Tests (API Keys + Subscription) --
Running e2e tests with:
  - TOKEN: sha256~K8ztKJZ_tkMXs...
  - ADMIN_OC_TOKEN: sha256~K8ztKJZ_tkMXs...
  - GATEWAY_HOST: maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org
================================== test session starts ==================================
platform darwin -- Python 3.13.3, pytest-8.4.2, pluggy-1.6.0 -- /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/.venv/bin/python
cachedir: .pytest_cache
metadata: {'Python': '3.13.3', 'Platform': 'macOS-15.7.4-arm64-arm-64bit-Mach-O', 'Packages': {'pytest': '8.4.2', 'pluggy': '1.6.0'}, 'Plugins': {'metadata': '3.1.1', 'html': '4.2.0'}}
rootdir: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service
plugins: metadata-3.1.1, html-4.2.0
collected 30 items                                                                      

test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_create_api_key [token] using env TOKEN (masked): 50
[create] Created key id=157847b9-ad26-4999-8cbc-7bf6302c0d85, key prefix=sk-oai-1IWDUUqa...
PASSED       [  3%]
test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_list_api_keys [list] Found 7 keys
[list] Pagination works: limit=1 returned 1 items
PASSED        [  6%]
test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_revoke_api_key [revoke] Key e3eaede5-c230-45ae-99f9-0d9f1e62caf5 successfully revoked
PASSED       [ 10%]
test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_admin_manage_other_users_keys [admin_token] using env ADMIN_OC_TOKEN (masked): 50
[admin] User 'kube:admin' created key 10c9e1eb-633f-4d9f-89be-2e495c04125f
[admin] Admin listed 8 keys for 'kube:admin'
[admin] Admin successfully revoked user's key 10c9e1eb-633f-4d9f-89be-2e495c04125f
PASSED [ 13%]
test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys FAILED [ 16%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_own_keys [bulk-revoke] Successfully revoked 11 keys for user kube:admin
PASSED [ 20%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_other_user_forbidden [bulk-revoke] Non-admin correctly got 403 when trying to bulk revoke other user's keys
PASSED [ 23%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_admin_can_revoke_any_user [bulk-revoke] Admin successfully revoked 1 keys for user kube:admin
PASSED [ 26%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_model_access_success [api_key] Creating API key for inference tests...
[api_key] Created API key id=ef238728-0091-4036-b55d-bb30778d3887, key prefix=sk-oai-Yc7THMz7...
[inference] Model access succeeded: facebook/opt-125m, tokens=7
PASSED [ 30%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_invalid_api_key_rejected [inference] Invalid API key correctly rejected with 403
PASSED [ 33%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_no_auth_header_rejected [inference] Missing auth header correctly rejected with 401
PASSED [ 36%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_revoked_api_key_rejected [inference] Key before revoke: HTTP 200
[inference] Revoked key correctly rejected with 403
PASSED [ 40%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_chat_completions [inference] Chat completions succeeded: facebook/opt-125m
PASSED [ 43%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_explicit_subscription_header [inference] API key with x-maas-subscription header succeeded
PASSED [ 46%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_invalid_subscription_header [inference] API key with invalid subscription correctly rejected with 403
PASSED [ 50%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_authorized_user_gets_200 PASSED [ 53%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_no_auth_gets_401 PASSED [ 56%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_invalid_token_gets_403 PASSED [ 60%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_wrong_group_gets_403 PASSED [ 63%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_subscribed_user_gets_200 PASSED [ 66%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_auth_pass_no_subscription_gets_403 PASSED [ 70%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_invalid_subscription_header_gets_429 PASSED [ 73%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_explicit_subscription_header_works PASSED [ 76%]
test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_user_in_one_of_two_subscriptions_gets_200 PASSED [ 80%]
test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_multi_tier_auto_select_highest FAILED [ 83%]
test/e2e/tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_two_auth_policies_or_logic FAILED [ 86%]
test/e2e/tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_delete_one_auth_policy_other_still_works PASSED [ 90%]
test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_subscription_rebuilds_trlp PASSED [ 93%]
test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_last_subscription_denies_access FAILED [ 96%]
test/e2e/tests/test_subscription.py::TestOrderingEdgeCases::test_subscription_before_auth_policy FAILED [100%]

======================================= FAILURES ========================================
_________ TestAPIKeyAuthorization.test_non_admin_cannot_access_other_users_keys _________

self = <test_api_keys.TestAPIKeyAuthorization object at 0x102313610>
api_keys_base_url = 'https://maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~K8ztKJZ_tkMXsjy2_W454fwmp4yRkif0fsMDOFphpUg', 'Content-Type': 'application/json'}
admin_headers = {'Authorization': 'Bearer sha256~K8ztKJZ_tkMXsjy2_W454fwmp4yRkif0fsMDOFphpUg', 'Content-Type': 'application/json'}

    def test_non_admin_cannot_access_other_users_keys(self, api_keys_base_url: str, headers: dict, admin_headers: dict):
        """Test 5: Non-admin cannot access other user's keys - verify denial.
    
        Note: API returns 404 instead of 403 for IDOR protection (prevents key enumeration).
        This is a security best practice - returning 403 would reveal the key exists.
        """
        if not admin_headers:
            pytest.skip("ADMIN_OC_TOKEN not set")
    
        # Admin creates a key
        r_admin = requests.post(api_keys_base_url, headers=admin_headers, json={"name": "admin-only-key"}, timeout=30, verify=TLS_VERIFY)
        assert r_admin.status_code in (200, 201)
        admin_key_id = r_admin.json()["id"]
    
        # Regular user tries to GET admin's key - returns 404 for IDOR protection
        r_get = requests.get(f"{api_keys_base_url}/{admin_key_id}", headers=headers, timeout=30, verify=TLS_VERIFY)
>       assert r_get.status_code == 404, f"Expected 404 (IDOR protection), got {r_get.status_code}"
E       AssertionError: Expected 404 (IDOR protection), got 200
E       assert 200 == 404
E        +  where 200 = <Response [200]>.status_code

test/e2e/tests/test_api_keys.py:205: AssertionError
_________ TestMultipleSubscriptionsPerModel.test_multi_tier_auto_select_highest _________

self = <test_subscription.TestMultipleSubscriptionsPerModel object at 0x102400690>

    def test_multi_tier_auto_select_highest(self):
        """With 2 tiers for the same model, API key in both should still get access.
        (Verifies multiple overlapping subscriptions don't break routing.)"""
        ns = _ns()
        try:
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": "e2e-high-tier", "namespace": ns},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [{"name": MODEL_REF, "tokenRateLimits": [{"limit": 9999, "window": "1m"}]}],
                },
            })
    
            api_key = _get_default_api_key()
>           _poll_status(api_key, 200, extra_headers={"x-maas-subscription": "e2e-high-tier"})

test/e2e/tests/test_subscription.py:521: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

token = 'sk-oai-1lXBtNeec4lILk7NKmL1VyvkSMdDGdt6YExSvzHKKyq', expected = 200, path = None
extra_headers = {'x-maas-subscription': 'e2e-high-tier'}, subscription = None
timeout = 60, poll_interval = 2

    def _poll_status(token, expected, path=None, extra_headers=None, subscription=None, timeout=None, poll_interval=2):
        """Poll inference endpoint until expected HTTP status or timeout."""
        timeout = timeout or max(RECONCILE_WAIT * 3, 60)
        deadline = time.time() + timeout
        last = None
        last_err = None
        while time.time() < deadline:
            try:
                r = _inference(token, path=path, extra_headers=extra_headers, subscription=subscription)
                last_err = None
                ok = r.status_code == expected if isinstance(expected, int) else r.status_code in expected
                if ok:
                    return r
                last = r
            except requests.RequestException as exc:
                last_err = exc
                log.debug(f"Transient request error while polling: {exc}")
            except Exception as exc:
                # Catch-all to surface non-RequestException (e.g. JSON decode, timeout config)
                last_err = exc
                log.warning(f"Unexpected error while polling: {exc}")
            time.sleep(poll_interval)
        # Build failure message with all available context
        exp_str = expected if isinstance(expected, int) else " or ".join(str(e) for e in expected)
        err_msg = f"Expected {exp_str} within {timeout}s"
        if last is not None:
            err_msg += f", last status: {last.status_code}"
        if last_err is not None:
            err_msg += f", last error: {last_err}"
        if last is None and last_err is None:
            err_msg += ", no response (all requests may have raised non-RequestException)"
>       raise AssertionError(err_msg)
E       AssertionError: Expected 200 within 60s, last status: 403

test/e2e/tests/test_subscription.py:338: AssertionError
___________ TestMultipleAuthPoliciesPerModel.test_two_auth_policies_or_logic ____________

self = <test_subscription.TestMultipleAuthPoliciesPerModel object at 0x1024007d0>

    def test_two_auth_policies_or_logic(self):
        """Two auth policies for the premium model with OR logic: user matching either gets access."""
        ns = _ns()
        try:
            # Create a 2nd auth policy that allows system:authenticated (user's actual group)
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": "e2e-premium-sa-auth", "namespace": ns},
                "spec": {
                    "modelRefs": [PREMIUM_MODEL_REF],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
            # Create a subscription for system:authenticated on premium model
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": "e2e-premium-sa-sub", "namespace": ns},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [{"name": PREMIUM_MODEL_REF, "tokenRateLimits": [{"limit": 100, "window": "1m"}]}],
                },
            })
            _wait_reconcile()
    
            # Default API key (inherits user's system:authenticated group) should now work
            api_key = _get_default_api_key()
>           r = _poll_status(api_key, 200, path=PREMIUM_MODEL_PATH, subscription="e2e-premium-sa-sub")
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

test/e2e/tests/test_subscription.py:561: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

token = 'sk-oai-1lXBtNeec4lILk7NKmL1VyvkSMdDGdt6YExSvzHKKyq', expected = 200
path = '/llm/premium-simulated-simulated-premium', extra_headers = None
subscription = 'e2e-premium-sa-sub', timeout = 60, poll_interval = 2

    def _poll_status(token, expected, path=None, extra_headers=None, subscription=None, timeout=None, poll_interval=2):
        """Poll inference endpoint until expected HTTP status or timeout."""
        timeout = timeout or max(RECONCILE_WAIT * 3, 60)
        deadline = time.time() + timeout
        last = None
        last_err = None
        while time.time() < deadline:
            try:
                r = _inference(token, path=path, extra_headers=extra_headers, subscription=subscription)
                last_err = None
                ok = r.status_code == expected if isinstance(expected, int) else r.status_code in expected
                if ok:
                    return r
                last = r
            except requests.RequestException as exc:
                last_err = exc
                log.debug(f"Transient request error while polling: {exc}")
            except Exception as exc:
                # Catch-all to surface non-RequestException (e.g. JSON decode, timeout config)
                last_err = exc
                log.warning(f"Unexpected error while polling: {exc}")
            time.sleep(poll_interval)
        # Build failure message with all available context
        exp_str = expected if isinstance(expected, int) else " or ".join(str(e) for e in expected)
        err_msg = f"Expected {exp_str} within {timeout}s"
        if last is not None:
            err_msg += f", last status: {last.status_code}"
        if last_err is not None:
            err_msg += f", last error: {last_err}"
        if last is None and last_err is None:
            err_msg += ", no response (all requests may have raised non-RequestException)"
>       raise AssertionError(err_msg)
E       AssertionError: Expected 200 within 60s, last status: 403

test/e2e/tests/test_subscription.py:338: AssertionError
____________ TestCascadeDeletion.test_delete_last_subscription_denies_access ____________

self = <test_subscription.TestCascadeDeletion object at 0x102400b90>

    def test_delete_last_subscription_denies_access(self):
        """Delete all subscriptions for a model -> access denied (403 or 429).
    
        When the last subscription is deleted, access is denied. The exact code
        depends on which policy evaluates first:
        - 403: AuthPolicy's subscription-error-check denies (no subscription found)
        - 429: Default-deny TRLP with 0 tokens kicks in
    
        Both indicate the intended behavior: no subscription = no access.
        """
        api_key = _get_default_api_key()
        original = _snapshot_cr("maassubscription", SIMULATOR_SUBSCRIPTION)
        assert original, f"Pre-existing {SIMULATOR_SUBSCRIPTION} not found"
        try:
            _delete_cr("maassubscription", SIMULATOR_SUBSCRIPTION)
            # With no subscription, expect either 403 or 429 (both = access denied)
>           r = _poll_status(api_key, [403, 429], subscription=False, timeout=30)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

test/e2e/tests/test_subscription.py:638: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

token = 'sk-oai-1lXBtNeec4lILk7NKmL1VyvkSMdDGdt6YExSvzHKKyq', expected = [403, 429]
path = None, extra_headers = None, subscription = False, timeout = 30, poll_interval = 2

    def _poll_status(token, expected, path=None, extra_headers=None, subscription=None, timeout=None, poll_interval=2):
        """Poll inference endpoint until expected HTTP status or timeout."""
        timeout = timeout or max(RECONCILE_WAIT * 3, 60)
        deadline = time.time() + timeout
        last = None
        last_err = None
        while time.time() < deadline:
            try:
                r = _inference(token, path=path, extra_headers=extra_headers, subscription=subscription)
                last_err = None
                ok = r.status_code == expected if isinstance(expected, int) else r.status_code in expected
                if ok:
                    return r
                last = r
            except requests.RequestException as exc:
                last_err = exc
                log.debug(f"Transient request error while polling: {exc}")
            except Exception as exc:
                # Catch-all to surface non-RequestException (e.g. JSON decode, timeout config)
                last_err = exc
                log.warning(f"Unexpected error while polling: {exc}")
            time.sleep(poll_interval)
        # Build failure message with all available context
        exp_str = expected if isinstance(expected, int) else " or ".join(str(e) for e in expected)
        err_msg = f"Expected {exp_str} within {timeout}s"
        if last is not None:
            err_msg += f", last status: {last.status_code}"
        if last_err is not None:
            err_msg += f", last error: {last_err}"
        if last is None and last_err is None:
            err_msg += ", no response (all requests may have raised non-RequestException)"
>       raise AssertionError(err_msg)
E       AssertionError: Expected 403 or 429 within 30s, last status: 200

test/e2e/tests/test_subscription.py:338: AssertionError
______________ TestOrderingEdgeCases.test_subscription_before_auth_policy _______________

self = <test_subscription.TestOrderingEdgeCases object at 0x102400cd0>

    def test_subscription_before_auth_policy(self):
        """Create subscription first, then auth policy -> should work once both exist."""
        ns = _ns()
        try:
            # Get the default API key (inherits user's groups including system:authenticated)
            api_key = _get_default_api_key()
    
            # Create subscription first (for system:authenticated group)
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSSubscription",
                "metadata": {"name": "e2e-ordering-sub", "namespace": ns},
                "spec": {
                    "owner": {"groups": [{"name": "system:authenticated"}]},
                    "modelRefs": [{"name": PREMIUM_MODEL_REF, "tokenRateLimits": [{"limit": 100, "window": "1m"}]}],
                },
            })
            _wait_reconcile()
    
            # Without auth policy for system:authenticated on premium model, request should fail with 403
            r1 = _inference(api_key, path=PREMIUM_MODEL_PATH, subscription="e2e-ordering-sub")
            log.info(f"Sub only (no auth policy) -> {r1.status_code}")
            assert r1.status_code == 403, f"Expected 403 (no auth policy yet), got {r1.status_code}"
    
            # Now add the auth policy
            _apply_cr({
                "apiVersion": "maas.opendatahub.io/v1alpha1",
                "kind": "MaaSAuthPolicy",
                "metadata": {"name": "e2e-ordering-auth", "namespace": ns},
                "spec": {
                    "modelRefs": [PREMIUM_MODEL_REF],
                    "subjects": {"groups": [{"name": "system:authenticated"}]},
                },
            })
    
            # Now it should work
>           r2 = _poll_status(api_key, 200, path=PREMIUM_MODEL_PATH, subscription="e2e-ordering-sub")
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

test/e2e/tests/test_subscription.py:692: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

token = 'sk-oai-1lXBtNeec4lILk7NKmL1VyvkSMdDGdt6YExSvzHKKyq', expected = 200
path = '/llm/premium-simulated-simulated-premium', extra_headers = None
subscription = 'e2e-ordering-sub', timeout = 60, poll_interval = 2

    def _poll_status(token, expected, path=None, extra_headers=None, subscription=None, timeout=None, poll_interval=2):
        """Poll inference endpoint until expected HTTP status or timeout."""
        timeout = timeout or max(RECONCILE_WAIT * 3, 60)
        deadline = time.time() + timeout
        last = None
        last_err = None
        while time.time() < deadline:
            try:
                r = _inference(token, path=path, extra_headers=extra_headers, subscription=subscription)
                last_err = None
                ok = r.status_code == expected if isinstance(expected, int) else r.status_code in expected
                if ok:
                    return r
                last = r
            except requests.RequestException as exc:
                last_err = exc
                log.debug(f"Transient request error while polling: {exc}")
            except Exception as exc:
                # Catch-all to surface non-RequestException (e.g. JSON decode, timeout config)
                last_err = exc
                log.warning(f"Unexpected error while polling: {exc}")
            time.sleep(poll_interval)
        # Build failure message with all available context
        exp_str = expected if isinstance(expected, int) else " or ".join(str(e) for e in expected)
        err_msg = f"Expected {exp_str} within {timeout}s"
        if last is not None:
            err_msg += f", last status: {last.status_code}"
        if last_err is not None:
            err_msg += f", last error: {last_err}"
        if last is None and last_err is None:
            err_msg += ", no response (all requests may have raised non-RequestException)"
>       raise AssertionError(err_msg)
E       AssertionError: Expected 200 within 60s, last status: 403

test/e2e/tests/test_subscription.py:338: AssertionError
----------------------------------- Captured log call -----------------------------------
INFO     test_subscription:test_subscription.py:677 Sub only (no auth policy) -> 403
- generated xml file: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/e2e-kube:admin.xml -
- Generated html report: file:///Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/e2e-kube%3Aadmin.html -
================================ short test summary info ================================
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys - AssertionError: Expected 404 (IDOR protection), got 200
FAILED test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_multi_tier_auto_select_highest - AssertionError: Expected 200 within 60s, last status: 403
FAILED test/e2e/tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_two_auth_policies_or_logic - AssertionError: Expected 200 within 60s, last status: 403
FAILED test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_last_subscription_denies_access - AssertionError: Expected 403 or 429 within 30s, last status: 200
FAILED test/e2e/tests/test_subscription.py::TestOrderingEdgeCases::test_subscription_before_auth_policy - AssertionError: Expected 200 within 60s, last status: 403
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 5 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
================= 5 failed, 25 passed, 32 warnings in 439.20s (0:07:19) =================
❌ ERROR: E2E tests failed

========== E2E Artifact Collection ==========
Artifact dir: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports
Collecting Authorino logs (token-redacted) to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/authorino-debug.log
  Saved to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/authorino-debug.log
Collecting cluster state to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports
  Saved to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/cluster-state.log
Collecting pod logs from namespace opendatahub to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/pod-logs
  Saved       24 pod log file(s) to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/pod-logs
==============================================

========== Auth Debug Report ==========

========================================
Cluster / Namespace Info
========================================

--- Current context ---
default/api-ci-ln-2xgfz8b-76ef8-aws-4-ci-openshift-org:6443/kube:admin

--- Logged-in user ---
kube:admin

--- Cluster domain ---
apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org

MAAS_NAMESPACE: opendatahub
AUTHORINO_NAMESPACE: kuadrant-system


========================================
MaaS API Deployment
========================================

--- maas-api pods ---
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE                           NOMINATED NODE   READINESS GATES
maas-api-86644774cc-2j7jf   1/1     Running   0          77m   10.129.2.46   ip-10-0-112-143.ec2.internal   <none>           <none>

--- maas-api service ---
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE    SELECTOR
maas-api   ClusterIP   172.30.51.20   <none>        8443/TCP   105m   app.kubernetes.io/component=api,app.kubernetes.io/name=maas-api,app.kubernetes.io/part-of=models-as-a-service



========================================
maas-controller
========================================

--- maas-controller pods ---
NAME                               READY   STATUS    RESTARTS   AGE    IP            NODE                           NOMINATED NODE   READINESS GATES
maas-controller-854bf8d4f8-9ngfr   1/1     Running   0          103m   10.129.2.37   ip-10-0-112-143.ec2.internal   <none>           <none>

--- maas-controller MAAS_API_NAMESPACE ---
MAAS_API_NAMESPACE=metadata.namespace



========================================
Kuadrant AuthPolicies
========================================

--- AuthPolicies (all namespaces) ---
NAMESPACE           NAME                                            ACCEPTED   ENFORCED   TARGETKIND   TARGETNAME                                         TARGETSECTION   AGE
llm                 maas-auth-facebook-opt-125m-simulated           True       True       HTTPRoute    facebook-opt-125m-simulated-kserve-route                           3m57s
llm                 maas-auth-premium-simulated-simulated-premium   True       True       HTTPRoute    premium-simulated-simulated-premium-kserve-route                   76s
opendatahub         maas-api-auth-policy                            True       True       HTTPRoute    maas-api-route                                                     105m
openshift-ingress   gateway-auth-policy                             True       True       Gateway      maas-default-gateway                                               105m



========================================
MaaS CRs
========================================

--- MaaSAuthPolicies ---
NAME                       PHASE    AGE   AUTHPOLICIES
premium-simulator-access   Active   90m   maas-auth-premium-simulated-simulated-premium
simulator-access           Active   90m   maas-auth-facebook-opt-125m-simulated

--- MaaSSubscriptions ---
NAME                             PHASE    PRIORITY   AGE
premium-simulator-subscription   Active   0          90m
simulator-subscription           Active   0          2m46s

--- MaaSModelRefs ---
NAME                                           PHASE   ENDPOINT                                                                                                        HTTPROUTE                                                   GATEWAY                AGE
e2e-unconfigured-facebook-opt-125m-simulated   Ready   https://maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org/llm/e2e-unconfigured-facebook-opt-125m-simulated   e2e-unconfigured-facebook-opt-125m-simulated-kserve-route   maas-default-gateway   90m
facebook-opt-125m-simulated                    Ready   https://maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated                    facebook-opt-125m-simulated-kserve-route                    maas-default-gateway   90m
premium-simulated-simulated-premium            Ready   https://maas.apps.ci-ln-2xgfz8b-76ef8.aws-4.ci.openshift.org/llm/premium-simulated-simulated-premium            premium-simulated-simulated-premium-kserve-route            maas-default-gateway   90m



========================================
Gateway / HTTPRoutes
========================================

--- Gateway ---
NAME                   CLASS               ADDRESS                                                                  PROGRAMMED   AGE
maas-default-gateway   openshift-default   a2d5d6bbaf9eb47d2b70203bc5863c5e-864156544.us-east-1.elb.amazonaws.com   True         109m

--- HTTPRoutes (maas-api) ---
NAME             HOSTNAMES   AGE
maas-api-route               106m



========================================
Authorino
========================================

--- Authorino pods ---
---



========================================
Subscription Selector Endpoint Validation
========================================

Expected URL (from maas-controller config): https://maas-api.opendatahub.svc.cluster.local:8443/v1/subscriptions/select

--- Connectivity test (from kuadrant-system, simulates Authorino) ---
curl -vsk -m 10 -X POST 'https://maas-api.opendatahub.svc.cluster.local:8443/v1/subscriptions/select' -H 'Content-Type: application/json' -d '{}'

kubectl run failed or timed out


========================================
DNS Resolution Check
========================================

Resolving: maas-api.opendatahub.svc.cluster.local
nslookup failed


======================================
```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated ignore rules across repo and test areas to exclude common
Python/tooling/editor artifacts.
* Removed numerous legacy end-to-end test scripts and supporting test
harness files.

* **Documentation**
* Simplified end-to-end test instructions to run pytest directly within
a virtual environment; bootstrap orchestration docs removed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Mynhardt Burger <mynhardt@gmail.com>
)

# Extract Authorino TLS and database setup into standalone scripts

## Description

Support work for the manual install documentation.

- **`scripts/setup-authorino-tls.sh`** — Moved from
`deployment/overlays/tls-backend/configure-authorino-tls.sh`; configures
Authorino for TLS with maas-api.
- **`scripts/setup-database.sh`** — New script for PostgreSQL deployment
for API key storage.
- **`scripts/deploy.sh`** — Calls these scripts instead of inlining
their logic.

## How Has This Been Tested?

- Ran `./scripts/deploy.sh` on OpenShift; deployment and validation
succeeded.
- Ran `setup-database.sh` and `setup-authorino-tls.sh` manually and
confirmed they work.

## Merge criteria

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Expanded scripts documentation with detailed usage examples,
prerequisites, and configuration options for TLS and database setup.

* **New Features**
* Added automated PostgreSQL database provisioning with built-in
credential generation and readiness validation.
* Enhanced deployment workflow with modular scripts and improved user
guidance for TLS configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- Replace stale `deploy-rhoai-stable.sh` reference with `deploy.sh` in
quickstart

The `deploy-rhoai-stable.sh` script does not exist. The unified
`deploy.sh` handles TLS configuration with TLS backend enabled by
default.

---
*Created by document-review workflow `/create-prs` phase.*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Updated quickstart documentation with corrected script references for
automated TLS configuration guidance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Mynhardt Burger <mynhardt@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary

- Remove `envsubst` from required tools list in maas-setup.md

The tool is listed as required but never used in any command in the
document.

---
*Created by document-review workflow `/create-prs` phase.*

Signed-off-by: Mynhardt Burger <mynhardt@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary

- Add links for installation guide, TLS configuration, token management,
Limitador persistence, and migration guide to the documentation landing
page

The index page only linked to 5 of 20+ documentation sections. Users
could not discover installation guides, TLS configuration, or the
migration guide from the landing page.

## Human Input Needed

- [ ] Confirm which access-control model is current vs legacy
(tier-based vs subscription-based) and add appropriate labels

---
*Created by document-review workflow `/create-prs` phase.*

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Expanded documentation with new advanced administration guides
covering Limitador Persistence, TLS Configuration, and Token Management
features.
* Added comprehensive Installation Guide with sections on Prerequisites,
Platform Setup, MaaS Setup, and Validation procedures.
* Introduced Migration guide specifically for Tier to Subscription
Migration scenarios.
* Updated navigation structure to reflect all new documentation
additions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Mynhardt Burger <mynhardt@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Add `tools.mk` to maas-controller for golangci-lint installation similar
to `maas-api`.

## Description
- Introduce `tools.mk` that installs golangci-lint v2.6.2 into a local
`bin/tools/` directory with version-based caching.
- Add a `lint` make target (`golangci-lint fmt` + `run`) with optional
`LINT_FIX=true` for auto-fixing.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
- Ran `make lint` — confirmed golangci-lint is downloaded, cached, and
runs against the codebase.
- Verified `bin/tools/` is covered by the root `.gitignore`.

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Added automated linting with an option to auto-fix formatting and lint
issues, and surfaced a new lint command in the project tooling.
* Introduced local tool installation and versioned binaries with symlink
management to ensure reproducible, project-local developer tools.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Chetan Kulkarni <ckulkarn@redhat.com>
Signed-off-by: Chaitanya Kulkarni <chkulkar@redhat.com>
…not provided (#544)

<!--- Provide a general summary of your changes in the Title above -->

## Description
This PR removes support for permanent API keys and introduces default
expiration for all API keys.

### Changes
**Behavior Change:**
* API keys now default to `API_KEY_MAX_EXPIRATION_DAYS (90 days)` when
expiresIn is not provided
* Permanent keys (keys without expiration) are no longer supported
* This improves security by ensuring all API keys have a bounded
lifetime
**Configuration:**
* Removed `API_KEY_EXPIRATION_POLICY` environment variable (no longer
needed since all keys must expire)
* Changed DefaultAPIKeyMaxExpirationDays from 30 to 90 days

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
* Unit tests
* Manual testing

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* API keys now expire by default (90 days) and the expiration is
configurable; creation responses always include an expiresAt timestamp
and the plaintext key on creation.

* **Documentation**
* Updated API docs and examples to reflect default expiration, new
example key names, and short-lived key example (30 days).

* **Chores**
  * Removed legacy API key expiration policy configuration field.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
)

The verify-models-and-limits.sh script was broken because it called the
removed /maas-api/v1/tokens endpoint. This migrates it to the current
/v1/api-keys flow. Model discovery stays on the /v1/models API (no
kubectl calls).

Changes:

Use POST /maas-api/v1/api-keys with named keys instead of removed
/v1/tokens
Parse .key and .id from API key response
Clean up temporary API key on exit via DELETE /v1/api-keys/{id}
Remove unused USER_NAME and stale JWT decoding logic
Prerequisite: The maas-api-auth-policy must support API key
authentication (see deployment/base/maas-api/policies/auth-policy.yaml).
The ODH operator currently deploys a stale version without API key
support.

How Has This Been Tested?
Ran the script end-to-end on a live cluster
(maas.apps.giteltal.dev.datahub.redhat.com)
API key creation, model discovery, inference, rate limiting, and cleanup
all passed
Merge criteria:

The commits are squashed in a cohesive manner and have meaningful
messages.

Testing instructions have been added in the PR body.

The developer has manually tested the changes and verified that the
changes work.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Switched verification scripts from token-based to API key-based
authentication and updated related messaging.
* Added creation and guaranteed cleanup (revocation) of temporary API
keys, with improved success/error reporting.
* Removed JWT decoding and related user-extraction steps; updated model
discovery and inference calls to use API key authorization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- Correct database reference from SQLite to PostgreSQL in maas-api
README

The code exclusively uses PostgreSQL (`NewPostgresStoreFromURL` in
cmd/main.go:138). The SQLite reference is incorrect and contradicts the
prerequisites doc.

---
*Created by document-review workflow `/create-prs` phase.*

Signed-off-by: Mynhardt Burger <mynhardt@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Description

Major rewrite of the MaaS documentation to align with the
subscription-based architecture and API key flow. Changes include:

- **Authentication**: Replaced token workflow with API key flow and
`X-MaaS-Subscription` header guidance
- **Install**: Split into Operator (production) and Kustomize
(development) paths; added troubleshooting, validation, DB and TLS setup
- **Subscription model**: New docs for overview, cardinality, known
issues, and quota/access configuration
- **Terminology**: Updated from "tier" to "subscription" across docs and
dashboards
- **API reference**: Interactive Swagger UI, OpenAPI spec, and CRD
reference pages
- **Samples**: New sample model deployments and release notes page

This is a large doc update. If you find issues, please open JIRAs so we
can track and fix them incrementally.

## How Has This Been Tested?

- Ran through the full install process on a local cluster
- Verified Quota and Access Configuration flow end-to-end
- Built docs with `mkdocs build` and confirmed navigation and links
- (Note I know some of the other docs around Token Rate Limit and Model
Listing probably need some updates)

**Preview**:
[https://jland-redhat.github.io/models-as-a-service/dev/](https://jland-redhat.github.io/models-as-a-service/dev/)

## Merge criteria:

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified the
changes work.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Replaced token-based with API key-based authentication for improved
security and lifecycle management.
* Introduced subscription-based access control replacing tier-based
system for flexible quota and access management.
* Added support for multiple sample model deployments (Facebook
OPT-125M, Qwen3).
  * Integrated interactive Swagger UI for API documentation.

* **Documentation**
* Significantly restructured guides to reflect subscription-based model
and API key workflows.
* Updated platform setup, configuration, troubleshooting, and migration
documentation.
  * Added comprehensive quota, access control, and model setup guides.
  * Removed legacy tier-based documentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…564)

## Summary
Enable FIPS compliance in `maas-controller/Dockerfile.konflux` by
aligning it with `maas-api/Dockerfile.konflux` (introduced in PR #255).

## Description
- Set `CGO_ENABLED=1` — required to link against system crypto libraries
(OpenSSL) instead of Go's built-in crypto
- Add `GOEXPERIMENT=strictfipsruntime` — enables startup validation that
a FIPS-compatible crypto backend is active at runtime

No behavioral changes to the controller logic or deployment manifests.

## How it was tested
- Built `Dockerfile.konflux` locally using `podman build` targeting
`linux/amd64`, image built successfully through both builder and runtime
stages

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated build configuration to enforce stricter compliance standards
and optimize native library integration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Chaitanya Kulkarni <chkulkar@redhat.com>
#493)

<!--- Provide a general summary of your changes in the Title above -->

## Description
Related to https://issues.redhat.com/browse/RHOAIENG-51672

Summary
Audit and remove unused or dead code and scripts from .github/hack/,
scripts/, scripts/installers/, and any other redundant paths.
Consolidate or delete scripts that are duplicated, obsolete, or no
longer referenced by CI or docs, so the codebase has a single clear path
for deployment and no leftover cruft.

Story
As a maintainer, I want unused and dead code and scripts removed from
the hack dir, scripts directory, installers, and elsewhere, so that we
reduce confusion, avoid duplicate code paths, and simplify the e2e
refactor (deploy in deploy.sh, tests in smoke.sh).

Acceptance Criteria
Audit .github/hack/ (e.g. install-cert-manager-and-lws.sh,
install-odh.sh, cleanup-odh.sh, uninstall-leader-worker-set.sh): remove
or consolidate into scripts/ if they are to be the single source of
truth; remove if truly dead and unreferenced.
Audit scripts/ and scripts/installers/: remove or merge scripts that are
unused, duplicated by deploy.sh, or superseded by a single deploy/test
flow. Document any removals and update references in prow script and
docs.
Remove any other redundant or dead code identified during the audit. CI
and documented flows (e.g. prow_run_smoke_test.sh, deploy.sh) continue
to work after cleanup.

Testability / Verification
After cleanup, run the full e2e flow (or current prow script) and
confirm it still passes. Grep/docs confirm no broken references to
removed scripts. List of removed files and rationale is documented (e.g.
in PR or ticket).

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

Tested on cluster
https://console-openshift-console.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org

```
omyabhatnagar@Somyas-MacBook-Pro models-as-a-service % SKIP_DEPLOYMENT=true ./test/e2e/scripts/prow_run_smoke_test.sh

----------------------------------------
Deploying Maas on OpenShift
----------------------------------------

Checking prerequisites...
✅ Prerequisites met - logged in as: kube:admin on OpenShift
  Skipping deployment (SKIP_DEPLOYMENT=true)
  Assuming MaaS platform and models are already deployed

----------------------------------------
Setting up variables for tests
----------------------------------------

-- Setting up variables for tests --
K8S_CLUSTER_URL: https://api.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org:6443
HOST: maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org
MAAS_API_BASE_URL: https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/maas-api
CLUSTER_DOMAIN: apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org
✅ Variables for tests setup completed

----------------------------------------
Admin Setup (Premium Test Resources)
----------------------------------------

Setting up premium test token (SA-based, works when oc whoami -t is unavailable)...
Patching MaaSAuthPolicy premium-simulator-access to include system:serviceaccount:premium-users-namespace:premium-service-account...
maasauthpolicy.maas.opendatahub.io/premium-simulator-access patched (no change)
Patching MaaSSubscription premium-simulator-subscription to include system:serviceaccount:premium-users-namespace:premium-service-account...
maassubscription.maas.opendatahub.io/premium-simulator-subscription patched (no change)
✅ Premium test token setup complete (E2E_TEST_TOKEN_SA_* exported)

----------------------------------------
Setting up test tokens
----------------------------------------

Setting up test tokens (admin + regular user)...
Current admin session: kube:admin (will be preserved)
✅ Admin token for kube:admin (added to odh-admins)
✅ Regular user token for kube:admin (same as admin for local testing)
Token setup complete (main session unchanged: kube:admin)

----------------------------------------
Running E2E Tests
----------------------------------------

-- E2E Tests (API Keys + Subscription) --
Running e2e tests with:
  - TOKEN: sha256~BqgcE8DH7Gjmk...
  - ADMIN_OC_TOKEN: sha256~BqgcE8DH7Gjmk...
  - GATEWAY_HOST: maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org
========================== test session starts ==========================
platform darwin -- Python 3.13.3, pytest-8.4.2, pluggy-1.6.0 -- /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/.venv/bin/python
cachedir: .pytest_cache
metadata: {'Python': '3.13.3', 'Platform': 'macOS-15.7.4-arm64-arm-64bit-Mach-O', 'Packages': {'pytest': '8.4.2', 'pluggy': '1.6.0'}, 'Plugins': {'metadata': '3.1.1', 'html': '4.2.0'}}
rootdir: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service
plugins: metadata-3.1.1, html-4.2.0
collected 30 items                                                      

test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_create_api_key [token] using env TOKEN (masked): 50
[create] Created key id=3f2124fa-9001-4425-b9bc-bcfad2c4bacd, key prefix=sk-oai-dtEwcImF...
PASSED [  3%]
test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_list_api_keys [list] Found 6 keys
[list] Pagination works: limit=1 returned 1 items
PASSED [  6%]
test/e2e/tests/test_api_keys.py::TestAPIKeyCRUD::test_revoke_api_key [revoke] Key bceb72fe-1742-423f-bb21-5924c6abf18f successfully revoked
PASSED [ 10%]
test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_admin_manage_other_users_keys [admin_token] using env ADMIN_OC_TOKEN (masked): 50
[admin] User 'kube:admin' created key 531a1190-87a8-4606-84a6-1aea8abbf9c8
[admin] Admin listed 8 keys for 'kube:admin'
[admin] Admin successfully revoked user's key 531a1190-87a8-4606-84a6-1aea8abbf9c8
PASSED [ 13%]
test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys FAILED [ 16%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_own_keys FAILED [ 20%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_other_user_forbidden [bulk-revoke] Non-admin correctly got 403 when trying to bulk revoke other user's keys
PASSED [ 23%]
test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_admin_can_revoke_any_user [bulk-revoke] Admin successfully revoked 1 keys for user kube:admin
PASSED [ 26%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_model_access_success [api_key] Creating API key for inference tests...
[api_key] Created API key id=5831d46d-f448-4fc3-b995-656558dda908, key prefix=sk-oai-fUHvXwro...
FAILED [ 30%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_invalid_api_key_rejected [inference] Invalid API key correctly rejected with 403
PASSED [ 33%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_no_auth_header_rejected [inference] Missing auth header correctly rejected with 401
PASSED [ 36%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_revoked_api_key_rejected [inference] Key before revoke: HTTP 200
[inference] Revoked key correctly rejected with 403
PASSED [ 40%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_chat_completions [inference] Chat completions returned 403: 
SKIPPED [ 43%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_explicit_subscription_header FAILED [ 46%]
test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_invalid_subscription_header [inference] API key with invalid subscription correctly rejected with 403
PASSED [ 50%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_authorized_user_gets_200 PASSED [ 53%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_no_auth_gets_401 PASSED [ 56%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_invalid_token_gets_403 PASSED [ 60%]
test/e2e/tests/test_subscription.py::TestAuthEnforcement::test_wrong_group_gets_403 PASSED [ 63%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_subscribed_user_gets_200 PASSED [ 66%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_auth_pass_no_subscription_gets_403 PASSED [ 70%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_invalid_subscription_header_gets_429 PASSED [ 73%]
test/e2e/tests/test_subscription.py::TestSubscriptionEnforcement::test_explicit_subscription_header_works PASSED [ 76%]
test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_user_in_one_of_two_subscriptions_gets_200 PASSED [ 80%]
test/e2e/tests/test_subscription.py::TestMultipleSubscriptionsPerModel::test_multi_tier_auto_select_highest PASSED [ 83%]
test/e2e/tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_two_auth_policies_or_logic PASSED [ 86%]
test/e2e/tests/test_subscription.py::TestMultipleAuthPoliciesPerModel::test_delete_one_auth_policy_other_still_works PASSED [ 90%]
test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_subscription_rebuilds_trlp PASSED [ 93%]
test/e2e/tests/test_subscription.py::TestCascadeDeletion::test_delete_last_subscription_denies_access PASSED [ 96%]
test/e2e/tests/test_subscription.py::TestOrderingEdgeCases::test_subscription_before_auth_policy PASSED [100%]

=============================== FAILURES ================================
_ TestAPIKeyAuthorization.test_non_admin_cannot_access_other_users_keys _

self = <test_api_keys.TestAPIKeyAuthorization object at 0x106fff610>
api_keys_base_url = 'https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~BqgcE8DH7GjmkR-pmt7TfMy1Q1f-DoN3ciJbRVnSPd8', 'Content-Type': 'application/json'}
admin_headers = {'Authorization': 'Bearer sha256~BqgcE8DH7GjmkR-pmt7TfMy1Q1f-DoN3ciJbRVnSPd8', 'Content-Type': 'application/json'}

    def test_non_admin_cannot_access_other_users_keys(self, api_keys_base_url: str, headers: dict, admin_headers: dict):
        """Test 5: Non-admin cannot access other user's keys - verify denial.
    
        Note: API returns 404 instead of 403 for IDOR protection (prevents key enumeration).
        This is a security best practice - returning 403 would reveal the key exists.
        """
        if not admin_headers:
            pytest.skip("ADMIN_OC_TOKEN not set")
    
        # Admin creates a key
        r_admin = requests.post(api_keys_base_url, headers=admin_headers, json={"name": "admin-only-key"}, timeout=30, verify=TLS_VERIFY)
        assert r_admin.status_code in (200, 201)
        admin_key_id = r_admin.json()["id"]
    
        # Regular user tries to GET admin's key - returns 404 for IDOR protection
        r_get = requests.get(f"{api_keys_base_url}/{admin_key_id}", headers=headers, timeout=30, verify=TLS_VERIFY)
>       assert r_get.status_code == 404, f"Expected 404 (IDOR protection), got {r_get.status_code}"
E       AssertionError: Expected 404 (IDOR protection), got 200
E       assert 200 == 404
E        +  where 200 = <Response [200]>.status_code

test/e2e/tests/test_api_keys.py:205: AssertionError
__________ TestAPIKeyBulkOperations.test_bulk_revoke_own_keys ___________

self = <test_api_keys.TestAPIKeyBulkOperations object at 0x106fff750>
api_keys_base_url = 'https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~BqgcE8DH7GjmkR-pmt7TfMy1Q1f-DoN3ciJbRVnSPd8', 'Content-Type': 'application/json'}

    def test_bulk_revoke_own_keys(self, api_keys_base_url: str, headers: dict):
        """Test 8: Bulk revoke - user can bulk revoke their own keys."""
        # Create multiple keys
        key_ids = []
        for i in range(3):
            r = requests.post(api_keys_base_url, headers=headers, json={"name": f"bulk-test-{i}"}, timeout=30, verify=TLS_VERIFY)
            assert r.status_code in (200, 201)
            key_ids.append(r.json()["id"])
    
        # Get username from one of the keys
        r_get = requests.get(f"{api_keys_base_url}/{key_ids[0]}", headers=headers, timeout=30, verify=TLS_VERIFY)
        username = r_get.json().get("username") or r_get.json().get("owner")
        assert username
    
        # Bulk revoke all keys for this user
        r_bulk = requests.post(
            f"{api_keys_base_url}/bulk-revoke",
            headers=headers,
            json={"username": username},
            timeout=30,
            verify=TLS_VERIFY
        )
        assert r_bulk.status_code == 200
        data = r_bulk.json()
>       assert data.get("revokedCount") >= 3, f"Expected at least 3 revoked, got {data.get('revokedCount')}"
E       AssertionError: Expected at least 3 revoked, got 0
E       assert 0 >= 3
E        +  where 0 = <built-in method get of dict object at 0x110833800>('revokedCount')
E        +    where <built-in method get of dict object at 0x110833800> = {'message': 'Successfully revoked 0 active API key(s) for user kube:admin', 'revokedCount': 0}.get

test/e2e/tests/test_api_keys.py:240: AssertionError
______ TestAPIKeyModelInference.test_api_key_model_access_success _______

self = <test_api_keys.TestAPIKeyModelInference object at 0x106fff9d0>
model_completions_url = 'https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated/v1/completions'
api_key_headers = {'Authorization': 'Bearer sk-oai-fUHvXwroiHvPfUQSFIwRlHrmhRuRWkJuRGru4e2U7Vz', 'Content-Type': 'application/json'}
inference_model_name = 'facebook/opt-125m'

    def test_api_key_model_access_success(
        self,
        model_completions_url: str,
        api_key_headers: dict,
        inference_model_name: str,
    ):
        """Test 11: Valid API key can access model endpoint - verify 200 response.
    
        Note: Users with access to multiple subscriptions must specify which one
        to use via X-MaaS-Subscription header.
        """
        # Add subscription header - required when user matches multiple subscriptions
        subscription_name = os.environ.get("E2E_SIMULATOR_SUBSCRIPTION", "simulator-subscription")
        headers = api_key_headers.copy()
        headers["X-MaaS-Subscription"] = subscription_name
    
        r = requests.post(
            model_completions_url,
            headers=headers,
            json={
                "model": inference_model_name,
                "prompt": "Hello world",
                "max_tokens": 10,
            },
            timeout=60,
            verify=TLS_VERIFY,
        )
    
>       assert r.status_code == 200, f"Expected 200, got {r.status_code}: {r.text}"
E       AssertionError: Expected 200, got 403: 
E       assert 403 == 200
E        +  where 403 = <Response [403]>.status_code

test/e2e/tests/test_api_keys.py:332: AssertionError
------------------------- Captured stdout setup -------------------------
[api_key] Creating API key for inference tests...
[api_key] Created API key id=5831d46d-f448-4fc3-b995-656558dda908, key prefix=sk-oai-fUHvXwro...
_ TestAPIKeyModelInference.test_api_key_with_explicit_subscription_header _

self = <test_api_keys.TestAPIKeyModelInference object at 0x107066360>
model_completions_url = 'https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated/v1/completions'
api_key_headers = {'Authorization': 'Bearer sk-oai-fUHvXwroiHvPfUQSFIwRlHrmhRuRWkJuRGru4e2U7Vz', 'Content-Type': 'application/json'}
inference_model_name = 'facebook/opt-125m'

    def test_api_key_with_explicit_subscription_header(
        self,
        model_completions_url: str,
        api_key_headers: dict,
        inference_model_name: str,
    ):
        """Test 16: API key with explicit x-maas-subscription header.
    
        When multiple subscriptions exist for the same model, the user can
        specify which subscription to use via the x-maas-subscription header.
        This works the same way for API keys as it does for OC tokens.
        """
        # Default subscription for free model
        subscription_name = os.environ.get("E2E_SIMULATOR_SUBSCRIPTION", "simulator-subscription")
    
        # Add x-maas-subscription header to API key headers
        headers_with_sub = api_key_headers.copy()
        headers_with_sub["x-maas-subscription"] = subscription_name
    
        r = requests.post(
            model_completions_url,
            headers=headers_with_sub,
            json={
                "model": inference_model_name,
                "prompt": "Test subscription header",
                "max_tokens": 5,
            },
            timeout=60,
            verify=TLS_VERIFY,
        )
    
>       assert r.status_code == 200, f"Expected 200 with explicit subscription, got {r.status_code}: {r.text}"
E       AssertionError: Expected 200 with explicit subscription, got 403: 
E       assert 403 == 200
E        +  where 403 = <Response [403]>.status_code

test/e2e/tests/test_api_keys.py:516: AssertionError
- generated xml file: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/e2e-kube:admin.xml -
- Generated html report: file:///Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/e2e-kube%3Aadmin.html -
======================== short test summary info ========================
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys - AssertionError: Expected 404 (IDOR protection), got 200
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyBulkOperations::test_bulk_revoke_own_keys - AssertionError: Expected at least 3 revoked, got 0
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_model_access_success - AssertionError: Expected 200, got 403: 
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyModelInference::test_api_key_with_explicit_subscription_header - AssertionError: Expected 200 with explicit subscription, got 403: 
=== 4 failed, 25 passed, 1 skipped, 32 warnings in 241.98s (0:04:01) ====
❌ ERROR: E2E tests failed

========== E2E Artifact Collection ==========
Artifact dir: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports
Collecting Authorino logs (token-redacted) to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/authorino-debug.log
  Saved to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/authorino-debug.log
Collecting cluster state to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports
  Saved to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/cluster-state.log
Collecting pod logs from namespace opendatahub to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/pod-logs
  Saved       44 pod log file(s) to /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/pod-logs
==============================================

========== Auth Debug Report ==========

========================================
Cluster / Namespace Info
========================================

--- Current context ---
default/api-ci-ln-c5br29k-76ef8-aws-4-ci-openshift-org:6443/kube:admin

--- Logged-in user ---
kube:admin

--- Cluster domain ---
apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org

MAAS_NAMESPACE: opendatahub
AUTHORINO_NAMESPACE: kuadrant-system


========================================
MaaS API Deployment
========================================

--- maas-api pods ---
NAME                        READY   STATUS    RESTARTS   AGE     IP            NODE                                        NOMINATED NODE   READINESS GATES
maas-api-5ddfd8d5f4-8rl7c   1/1     Running   0          5m42s   10.129.2.39   ip-10-0-75-222.us-west-1.compute.internal   <none>           <none>

--- maas-api service ---
NAME       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE   SELECTOR
maas-api   ClusterIP   172.30.34.232   <none>        8443/TCP   33m   app.kubernetes.io/component=api,app.kubernetes.io/name=maas-api,app.kubernetes.io/part-of=models-as-a-service



========================================
maas-controller
========================================

--- maas-controller pods ---
NAME                               READY   STATUS    RESTARTS   AGE   IP            NODE                                         NOMINATED NODE   READINESS GATES
maas-controller-854bf8d4f8-pmpwt   1/1     Running   0          31m   10.131.0.36   ip-10-0-105-207.us-west-1.compute.internal   <none>           <none>

--- maas-controller MAAS_API_NAMESPACE ---
MAAS_API_NAMESPACE=metadata.namespace



========================================
Kuadrant AuthPolicies
========================================

--- AuthPolicies (all namespaces) ---
NAMESPACE           NAME                                            ACCEPTED   ENFORCED   TARGETKIND   TARGETNAME                                         TARGETSECTION   AGE
llm                 maas-auth-facebook-opt-125m-simulated           True       True       HTTPRoute    facebook-opt-125m-simulated-kserve-route                           81s
llm                 maas-auth-premium-simulated-simulated-premium   True       True       HTTPRoute    premium-simulated-simulated-premium-kserve-route                   14s
opendatahub         maas-api-auth-policy                            True       True       HTTPRoute    maas-api-route                                                     33m
openshift-ingress   gateway-auth-policy                             True       True       Gateway      maas-default-gateway                                               33m



========================================
MaaS CRs
========================================

--- MaaSAuthPolicies ---
NAME                       PHASE    AGE   AUTHPOLICIES
premium-simulator-access   Active   21m   maas-auth-premium-simulated-simulated-premium
simulator-access           Active   21m   maas-auth-facebook-opt-125m-simulated

--- MaaSSubscriptions ---
NAME                             PHASE    PRIORITY   AGE
premium-simulator-subscription   Active   0          21m
simulator-subscription           Active   0          46s

--- MaaSModelRefs ---
NAME                                           PHASE   ENDPOINT                                                                                                        HTTPROUTE                                                   GATEWAY                AGE
e2e-unconfigured-facebook-opt-125m-simulated   Ready   https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/llm/e2e-unconfigured-facebook-opt-125m-simulated   e2e-unconfigured-facebook-opt-125m-simulated-kserve-route   maas-default-gateway   21m
facebook-opt-125m-simulated                    Ready   https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated                    facebook-opt-125m-simulated-kserve-route                    maas-default-gateway   21m
premium-simulated-simulated-premium            Ready   https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/llm/premium-simulated-simulated-premium            premium-simulated-simulated-premium-kserve-route            maas-default-gateway   21m



========================================
Gateway / HTTPRoutes
========================================

--- Gateway ---
NAME                   CLASS               ADDRESS                                                                   PROGRAMMED   AGE
maas-default-gateway   openshift-default   ac071c6e226d143ebad34b287ccc26a9-1378294721.us-west-1.elb.amazonaws.com   True         37m

--- HTTPRoutes (maas-api) ---
NAME             HOSTNAMES   AGE
maas-api-route               33m



========================================
Authorino
========================================

--- Authorino pods ---
---



========================================
Subscription Selector Endpoint Validation
========================================

Expected URL (from maas-controller config): https://maas-api.opendatahub.svc.cluster.local:8443/v1/subscriptions/select

--- Connectivity test (from kuadrant-system, simulates Authorino) ---
curl -vsk -m 10 -X POST 'https://maas-api.opendatahub.svc.cluster.local:8443/v1/subscriptions/select' -H 'Content-Type: application/json' -d '{}'

kubectl run failed or timed out


========================================
DNS Resolution Check
========================================

Resolving: maas-api.opendatahub.svc.cluster.local
nslookup failed


======================================
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % cd /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service && bash scripts/ci/validate-manifests.sh 2>&1
✅ Validating maas-api/deploy/overlays/odh/kustomization.yaml
✅ Validating maas-api/deploy/overlays/dev/kustomization.yaml
✅ Validating docs/samples/models/simulator-premium/kustomization.yaml
✅ Validating docs/samples/models/qwen3/kustomization.yaml
✅ Validating docs/samples/models/simulator/kustomization.yaml
✅ Validating docs/samples/models/facebook-opt-125m-cpu/kustomization.yaml
✅ Validating docs/samples/models/ibm-granite-2b-gpu/kustomization.yaml
✅ Validating docs/samples/maas-system/free/llm/kustomization.yaml
✅ Validating docs/samples/maas-system/free/maas/kustomization.yaml
✅ Validating docs/samples/maas-system/free/kustomization.yaml
✅ Validating docs/samples/maas-system/kustomization.yaml
✅ Validating docs/samples/maas-system/unconfigured/llm/kustomization.yaml
✅ Validating docs/samples/maas-system/unconfigured/maas/kustomization.yaml
✅ Validating docs/samples/maas-system/unconfigured/kustomization.yaml
✅ Validating docs/samples/maas-system/premium/llm/kustomization.yaml
✅ Validating docs/samples/maas-system/premium/maas/kustomization.yaml
✅ Validating docs/samples/maas-system/premium/kustomization.yaml
✅ Validating deployment/overlays/odh/kustomization.yaml
✅ Validating deployment/overlays/tls-backend-disk/kustomization.yaml
✅ Validating deployment/overlays/http-backend/kustomization.yaml
✅ Validating deployment/overlays/tls-backend/kustomization.yaml
✅ Validating deployment/overlays/openshift/kustomization.yaml
✅ Validating deployment/components/odh/kserve/kustomization.yaml
✅ Validating deployment/components/odh/operator/kustomization.yaml
✅ Validating deployment/components/observability/monitors/kustomization.yaml
✅ Validating deployment/components/observability/observability/kustomization.yaml
✅ Validating deployment/components/observability/dashboards/kustomization.yaml
✅ Validating deployment/components/observability/prometheus/kustomization.yaml
✅ Validating deployment/base/maas-controller/rbac/kustomization.yaml
✅ Validating deployment/base/maas-controller/crd/kustomization.yaml
✅ Validating deployment/base/maas-controller/default/kustomization.yaml
✅ Validating deployment/base/maas-controller/policies/kustomization.yaml
✅ Validating deployment/base/maas-controller/manager/kustomization.yaml
✅ Validating deployment/base/maas-api/rbac/kustomization.yaml
✅ Validating deployment/base/maas-api/core/kustomization.yaml
✅ Validating deployment/base/maas-api/overlays/tls/kustomization.yaml
✅ Validating deployment/base/maas-api/networking/kustomization.yaml
✅ Validating deployment/base/maas-api/kustomization.yaml
✅ Validating deployment/base/maas-api/resources/kustomization.yaml
✅ Validating deployment/base/maas-api/default/kustomization.yaml
✅ Validating deployment/base/maas-api/policies/kustomization.yaml
✅ Validating deployment/base/networking/odh/kustomization.yaml
✅ Validating deployment/base/networking/maas/kustomization.yaml
✅ Validating deployment/base/networking/kustomization.yaml
✅ Validating deployment/base/observability/kustomization.yaml
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % 
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % cd /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service && E2E_SKIP_TLS_VERIFY=true MODEL_NAME=facebook-opt-125m-simulated ./test/e2e/smoke.sh
[smoke] Setting up Python virtual environment...
[smoke] Activating virtual environment
[smoke] Installing Python dependencies
[smoke] Virtual environment setup complete
[smoke] MAAS_API_BASE_URL=https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/maas-api
[smoke] Using MODEL_NAME=facebook-opt-125m-simulated
[smoke] Performing smoke test for user: kube:admin
[smoke] Setting up admin token for admin tests...
[smoke] Added kube:admin to odh-admins group
[smoke] ADMIN_OC_TOKEN configured - admin tests will run
[smoke] models catalog empty (attempt 1/10), retrying in 3s...
[smoke] models catalog empty (attempt 2/10), retrying in 3s...
[smoke] models catalog empty (attempt 3/10), retrying in 3s...
[smoke] models catalog empty (attempt 4/10), retrying in 3s...
[smoke] models catalog empty (attempt 5/10), retrying in 3s...
[smoke] models catalog empty (attempt 6/10), retrying in 3s...
[smoke] models catalog empty (attempt 7/10), retrying in 3s...
[smoke] models catalog empty (attempt 8/10), retrying in 3s...
[smoke] models catalog empty (attempt 9/10), retrying in 3s...
[smoke] models catalog empty (attempt 10/10), retrying in 3s...
[smoke] Using MODEL_URL=https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/llm/facebook-opt-125m-simulated/v1
[token] using env TOKEN (masked): 50
[create] Created key id=4e57d078-e3d5-4a0d-af6c-91dc86e9ca2f, key prefix=sk-oai-aZdMBOUV...
.[list] Found 6 keys
[list] Pagination works: limit=1 returned 1 items
.[revoke] Key 17f2e9b3-21ba-496f-aff9-73f8c59f061d successfully revoked
.[admin_token] using env ADMIN_OC_TOKEN (masked): 50
[admin] User 'kube:admin' created key d5607437-b827-4b03-975e-4b010b018c28
[admin] Admin listed 7 keys for 'kube:admin'
[admin] Admin successfully revoked user's key d5607437-b827-4b03-975e-4b010b018c28
.F
====================================== FAILURES =======================================
________ TestAPIKeyAuthorization.test_non_admin_cannot_access_other_users_keys ________

self = <test_api_keys.TestAPIKeyAuthorization object at 0x10663b4d0>
api_keys_base_url = 'https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/maas-api/v1/api-keys'
headers = {'Authorization': 'Bearer sha256~BqgcE8DH7GjmkR-pmt7TfMy1Q1f-DoN3ciJbRVnSPd8', 'Content-Type': 'application/json'}
admin_headers = {'Authorization': 'Bearer sha256~BqgcE8DH7GjmkR-pmt7TfMy1Q1f-DoN3ciJbRVnSPd8', 'Content-Type': 'application/json'}

    def test_non_admin_cannot_access_other_users_keys(self, api_keys_base_url: str, headers: dict, admin_headers: dict):
        """Test 5: Non-admin cannot access other user's keys - verify denial.
    
        Note: API returns 404 instead of 403 for IDOR protection (prevents key enumeration).
        This is a security best practice - returning 403 would reveal the key exists.
        """
        if not admin_headers:
            pytest.skip("ADMIN_OC_TOKEN not set")
    
        # Admin creates a key
        r_admin = requests.post(api_keys_base_url, headers=admin_headers, json={"name": "admin-only-key"}, timeout=30, verify=TLS_VERIFY)
        assert r_admin.status_code in (200, 201)
        admin_key_id = r_admin.json()["id"]
    
        # Regular user tries to GET admin's key - returns 404 for IDOR protection
        r_get = requests.get(f"{api_keys_base_url}/{admin_key_id}", headers=headers, timeout=30, verify=TLS_VERIFY)
>       assert r_get.status_code == 404, f"Expected 404 (IDOR protection), got {r_get.status_code}"
E       AssertionError: Expected 404 (IDOR protection), got 200
E       assert 200 == 404
E        +  where 200 = <Response [200]>.status_code

test/e2e/tests/test_api_keys.py:205: AssertionError
- generated xml file: /Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/smoke-kube:admin.xml -
- Generated html report: file:///Users/somyabhatnagar/Documents/Projects/redhat/models-as-a-service/test/e2e/reports/smoke-kube%3Aadmin.html -
=============================== short test summary info ===============================
FAILED test/e2e/tests/test_api_keys.py::TestAPIKeyAuthorization::test_non_admin_cannot_access_other_users_keys - AssertionError: Expected 404 (IDOR protection), got 200
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 4 passed, 5 warnings in 17.03s
```

```
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % ./scripts/validate-deployment.sh

=========================================
🚀 MaaS Platform Deployment Validation
=========================================


=========================================
1️⃣ Component Status Checks
=========================================

🔍 Checking: MaaS API pods
✅ PASS: MaaS API has 1 running pod(s)
🔍 Checking: Kuadrant system pods
✅ PASS: Kuadrant has 8 running pod(s)
🔍 Checking: OpenDataHub/KServe pods
ℹ️    opendatahub namespace: 8 running pod(s)
✅ PASS: OpenDataHub/RHOAI has 8 total running pod(s)
🔍 Checking: LLM namespace and models
✅ PASS: Found        3 LLMInferenceService(s) with 3 running pod(s)

=========================================
2️⃣ Gateway Status
=========================================

🔍 Checking: Gateway resource
✅ PASS: Gateway is Accepted and Programmed
🔍 Checking: HTTPRoute for maas-api
✅ PASS: HTTPRoute maas-api-route is configured and accepted
🔍 Checking: Gateway hostname
✅ PASS: Gateway hostname: https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org

=========================================
3️⃣ Policy Status
=========================================

🔍 Checking: AuthPolicy
✅ PASS: AuthPolicy is configured and accepted
🔍 Checking: TokenRateLimitPolicy
⚠️  WARNING: TokenRateLimitPolicy found but status: NotFound
   Note: Policy may still be reconciling. Try deleting the kuadrant operator pod:
   kubectl delete pod -n kuadrant-system -l control-plane=controller-manager

=========================================
4️⃣ API Endpoint Tests
=========================================

ℹ️  Using gateway endpoint: https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org
🔍 Checking: Authentication token
✅ PASS: Authentication token available
🔍 Checking: Models endpoint
ℹ️  Testing: curl -sSk https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org/maas-api/v1/models -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN"
⚠️  WARNING: Models endpoint accessible but no models found
   Note: You may need to deploy a model a simulated model can be deployed with the following command:
   kustomize build docs/samples/models/simulator | kubectl apply --server-side=true --force-conflicts -f -
🔍 Checking: Authorization enforcement (401 without token)

=========================================
📊 Validation Summary
=========================================

Results:
  ✅ Passed: 9
  ❌ Failed: 0
  ⚠️  Warnings: 2

✅ PASS: All critical checks passed! 🎉

Next steps:
  1. Deploy a model: kustomize build docs/samples/models/simulator | kubectl apply -f -
  2. Access the API at: https://maas.apps.ci-ln-c5br29k-76ef8.aws-4.ci.openshift.org}
  3. Check documentation: docs/README.md
  4. Re-run validation with specific model: ./scripts/validate-deployment.sh MODEL_NAME
somyabhatnagar@Somyas-MacBook-Pro models-as-a-service % 
```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Removed several internal utility scripts including cleanup and
management tools for OpenDataHub and related components, deployment
helpers, and validation utilities.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Description

Implements
[RHOAIENG-52225](https://issues.redhat.com/browse/RHOAIENG-52225) and
[RHOAIENG-52226](https://issues.redhat.com/browse/RHOAIENG-52226) to
support returning models from all accessible subscriptions with
subscription metadata attached to each model.

### Changes

**New Header: `X-MaaS-Return-All-Models: true`**
- When set, `/v1/models` returns all models from all subscriptions the
user has access to
- Each model includes subscription metadata (`name`, `displayName`,
`description`)
- Deduplicates by `(model_id, URL)` key - same model endpoint aggregates
subscriptions into an array
- Different model endpoints (different URLs) appear as separate entries
even with the same model ID

**Subscription Metadata**
- Added `SubscriptionInfo` type with `name`, `displayName`, and
`description` fields
- Added `Subscriptions` array field to `Model` type (was singular
`Subscription` pointer)
- Parses `displayName` and `description` from MaaSSubscription CRD
`spec` (optional fields that don't exist in CRD yet)
- All models now include subscription metadata in responses (even with
existing single-subscription behavior)

**Deduplication Behavior**
- **Same model ID + same URL** → Single entry with subscriptions
aggregated in array
- **Same model ID + different URLs** → Separate entries (different
endpoints)
- **Multiple MaaSModelRefs pointing to same URL** → Deduplicated into
single entry

**API Behavior**
- **No header**: Existing behavior (auto-select single subscription or
return 403 for multiple)
- **`X-MaaS-Subscription: <name>`**: Filter to specific subscription
(existing behavior, now with subscription metadata)
- **`X-MaaS-Return-All-Models: true`**: Return all models from all
accessible subscriptions
- **Both headers**: Returns `400 Bad Request` (conflicting headers)
- **No subscriptions + return-all**: Returns empty array `[]` (not an
error)

**Implementation Details**
- Added `GetAllAccessible()` method to subscription selector
- Modified handlers/models.go to iterate over multiple subscriptions
when header is set
- Deduplication key is `(model_id, URL)` to aggregate subscriptions for
same endpoint
- Prevents duplicate subscription entries when same model is listed
multiple times
- Added comprehensive unit tests for selector and handlers

### Breaking Changes
None - all changes are additive. Existing API behavior is preserved.

### Notes
- `displayName` and `description` will be empty until MaaSSubscription
CRD is updated to include these optional fields
- Subscription metadata is now returned as an array for all `/v1/models`
responses

## How Has This Been Tested?

### Unit Tests
- Added `GetAllAccessible()` tests covering single/multiple/no
subscriptions scenarios
- Added handler tests for return-all-models behavior
- Added deduplication tests for subscription aggregation (same model in
different subscriptions)
- Added tests for different URLs with same model ID
- Added tests for same URL with different MaaSModelRefs
- Added error handling tests (conflicting headers, no subscriptions)
- All existing tests pass

Run tests:
```bash
go test ./internal/subscription -v -run TestGetAllAccessible
go test ./internal/handlers -v -run TestListModels
```

### Manual Testing

Tested on live cluster with service account that has access to multiple
subscriptions.

**Test Script:** `test_premium_sa.sh`
```bash
#!/bin/bash
set -e

GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

echo -e "${GREEN}=== Testing with Premium Service Account ===${NC}"
echo

MAAS_API_BASE_URL="https://maas.apps.rosa.nswzh-ayh46-h36.8ohi.p3.openshiftapps.com/maas-api"

# Use the existing premium SA
SA_NAME="premium-service-account"
SA_NAMESPACE="premium-users-namespace"

# Create the namespace if it doesn't exist
kubectl create namespace "$SA_NAMESPACE" 2>/dev/null || true

# Create the SA if it doesn't exist
kubectl get sa "$SA_NAME" -n "$SA_NAMESPACE" >/dev/null 2>&1 || kubectl create serviceaccount "$SA_NAME" -n "$SA_NAMESPACE"

# Get token
SA_TOKEN=$(kubectl create token "$SA_NAME" -n "$SA_NAMESPACE" --duration=10m)

# Create API key
API_KEY=$(curl -sk "${MAAS_API_BASE_URL}/v1/api-keys" \
    -H "Authorization: Bearer ${SA_TOKEN}" \
    -H "Content-Type: application/json" \
    -d '{"name": "premium-test-key"}' | jq -r '.key')

echo "Using SA: $SA_NAME in namespace: $SA_NAMESPACE"
echo "Got API key: ${API_KEY:0:20}..."
echo

# Add SA to simulator-subscription to give it access to both subscriptions
SA_USER="system:serviceaccount:${SA_NAMESPACE}:${SA_NAME}"
echo "Adding $SA_USER to simulator-subscription..."

kubectl patch maassubscription simulator-subscription -n models-as-a-service --type=json -p="[
  {\"op\": \"add\", \"path\": \"/spec/owner/users/-\", \"value\": \"$SA_USER\"}
]" 2>&1 | grep -v "already exists" || true

echo "Waiting for reconciliation..."
sleep 3
echo

# Test 1: Without header (should get 403 - multiple subscriptions)
echo -e "${GREEN}Test 1: No header (should fail - multiple subscriptions)${NC}"
RESPONSE1=$(curl -sk -w "\nHTTP_STATUS:%{http_code}" "${MAAS_API_BASE_URL}/v1/models" \
    -H "Authorization: Bearer ${API_KEY}")

HTTP_STATUS1=$(echo "$RESPONSE1" | grep HTTP_STATUS | cut -d: -f2)
BODY1=$(echo "$RESPONSE1" | sed '/HTTP_STATUS/d')

echo "HTTP Status: $HTTP_STATUS1"
echo "$BODY1" | jq '.'
echo

# Test 2: With X-MaaS-Return-All-Models: true
echo -e "${GREEN}Test 2: X-MaaS-Return-All-Models: true${NC}"
echo "Expected: 2 models (facebook/opt-125m from simulator, premium model from premium)"
RESPONSE2=$(curl -sk "${MAAS_API_BASE_URL}/v1/models" \
    -H "Authorization: Bearer ${API_KEY}" \
    -H "X-MaaS-Return-All-Models: true")

MODEL_COUNT=$(echo "$RESPONSE2" | jq '.data | length')
echo "Total models returned: $MODEL_COUNT"
echo

echo -e "${YELLOW}Full Response:${NC}"
echo "$RESPONSE2" | jq '.'
echo

echo "Models breakdown:"
echo "$RESPONSE2" | jq -r '.data[] | "  - Model ID: \(.id)\n    Subscriptions: \(.subscriptions | map(.name) | join(", "))\n    Owned By: \(.owned_by)\n    URL: \(.url)\n"'

# Test 3: Explicit simulator-subscription
echo -e "${GREEN}Test 3: X-MaaS-Subscription: simulator-subscription${NC}"
RESPONSE3=$(curl -sk "${MAAS_API_BASE_URL}/v1/models" \
    -H "Authorization: Bearer ${API_KEY}" \
    -H "X-MaaS-Subscription: simulator-subscription")

MODEL_COUNT3=$(echo "$RESPONSE3" | jq '.data | length')
echo "Models from simulator-subscription: $MODEL_COUNT3"
echo "$RESPONSE3" | jq -r '.data[] | "  - \(.id) from \(.subscriptions | map(.name) | join(", "))"'
echo

# Test 4: Explicit premium-simulator-subscription
echo -e "${GREEN}Test 4: X-MaaS-Subscription: premium-simulator-subscription${NC}"
RESPONSE4=$(curl -sk "${MAAS_API_BASE_URL}/v1/models" \
    -H "Authorization: Bearer ${API_KEY}" \
    -H "X-MaaS-Subscription: premium-simulator-subscription")

MODEL_COUNT4=$(echo "$RESPONSE4" | jq '.data | length')
echo "Models from premium-simulator-subscription: $MODEL_COUNT4"
echo "$RESPONSE4" | jq -r '.data[] | "  - \(.id) from \(.subscriptions | map(.name) | join(", "))"'
echo

# Cleanup - remove SA from simulator subscription
echo -e "${GREEN}Cleanup${NC}"
USERS_JSON=$(kubectl get maassubscription simulator-subscription -n models-as-a-service -o jsonpath='{.spec.owner.users}' 2>/dev/null || echo "[]")
if [ "$USERS_JSON" != "[]" ]; then
    UPDATED_USERS=$(echo "$USERS_JSON" | jq --arg user "$SA_USER" 'del(.[] | select(. == $user))')
    kubectl patch maassubscription simulator-subscription -n models-as-a-service --type=json -p="[
      {\"op\": \"replace\", \"path\": \"/spec/owner/users\", \"value\": $UPDATED_USERS}
    ]" 2>/dev/null || true
fi
echo "Removed SA from simulator subscription"

echo
echo -e "${GREEN}=== Summary ===${NC}"
echo "Test 1 (no header): HTTP $HTTP_STATUS1 (expected 403)"
echo "Test 2 (return-all): $MODEL_COUNT models (expected 2)"
echo "Test 3 (simulator only): $MODEL_COUNT3 model(s)"
echo "Test 4 (premium only): $MODEL_COUNT4 model(s)"
```

**Test Output:**
```
=== Testing with Premium Service Account ===

Using SA: premium-service-account in namespace: premium-users-namespace
Got API key: sk-oai-rrpGRkRKUAoz8...

Adding system:serviceaccount:premium-users-namespace:premium-service-account to simulator-subscription...
Waiting for reconciliation...

Test 1: No header (should fail - multiple subscriptions)
HTTP Status: 403
{
  "error": {
    "message": "user has access to multiple subscriptions, must specify subscription using X-MaaS-Subscription header",
    "type": "permission_error"
  }
}

Test 2: X-MaaS-Return-All-Models: true
Expected: 2 models (facebook/opt-125m from simulator, premium model from premium)
Total models returned: 2

Full Response:
{
  "data": [
    {
      "id": "facebook/opt-125m",
      "created": 1773331649,
      "object": "model",
      "owned_by": "vllm",
      "kind": "LLMInferenceService",
      "url": "https://maas.apps.rosa.nswzh-ayh46-h36.8ohi.p3.openshiftapps.com/llm/facebook-opt-125m-simulated",
      "ready": true,
      "subscriptions": [
        {
          "name": "simulator-subscription"
        },
        {
          "name": "premium-simulator-subscription"
        }
      ]
    },
    {
      "id": "facebook/opt-125m",
      "created": 1773331650,
      "object": "model",
      "owned_by": "vllm",
      "kind": "LLMInferenceService",
      "url": "https://maas.apps.rosa.nswzh-ayh46-h36.8ohi.p3.openshiftapps.com/llm/premium-simulated-simulated-premium",
      "ready": true,
      "subscriptions": [
        {
          "name": "premium-simulator-subscription"
        }
      ]
    }
  ],
  "object": "list"
}

Models breakdown:
  - Model ID: facebook/opt-125m
    Subscriptions: simulator-subscription, premium-simulator-subscription
    Owned By: vllm
    URL: https://maas.apps.rosa.nswzh-ayh46-h36.8ohi.p3.openshiftapps.com/llm/facebook-opt-125m-simulated

  - Model ID: facebook/opt-125m
    Subscriptions: premium-simulator-subscription
    Owned By: vllm
    URL: https://maas.apps.rosa.nswzh-ayh46-h36.8ohi.p3.openshiftapps.com/llm/premium-simulated-simulated-premium

Test 3: X-MaaS-Subscription: simulator-subscription
Models from simulator-subscription: 1
  - facebook/opt-125m from simulator-subscription

Test 4: X-MaaS-Subscription: premium-simulator-subscription
Models from premium-simulator-subscription: 2
  - facebook/opt-125m from premium-simulator-subscription
  - facebook/opt-125m from premium-simulator-subscription

=== Summary ===
Test 1 (no header): HTTP 403 (expected 403)
Test 2 (return-all): 2 models (expected 2)
Test 3 (simulator only): 1 model(s)
Test 4 (premium only): 2 model(s)
```

**Key Test Results:**
- ✅ Test 1: User with multiple subscriptions correctly gets 403 without
header
- ✅ Test 2: **X-MaaS-Return-All-Models: true** returns 2 models with
subscription aggregation
- Model at `facebook-opt-125m-simulated` URL shows **both
subscriptions** in the array
- Model at `premium-simulated-simulated-premium` URL shows **single
subscription**
- ✅ Test 3 & 4: Single subscription filtering still works correctly
- ✅ **Subscription Aggregation**: Same model endpoint aggregates
multiple subscriptions into array
- ✅ **Deduplication**: Different URLs create separate entries even with
same model ID
- ✅ **No duplicate subscriptions**: Same subscription doesn't appear
twice in the array

### Merge criteria:

- The commits are squashed in a cohesive manner and have meaningful
messages.
- Testing instructions have been added in the PR body (for PRs involving
changes that are not immediately obvious).
- The developer has manually tested the changes and verified that the
changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
- Models endpoint now supports filtering by subscription via
`X-MaaS-Subscription` header
- Added `X-MaaS-Return-All-Models` header to aggregate models across all
accessible subscriptions
- Model responses now include subscription metadata (name, display name,
description)
- Improved deduplication and aggregation of models across multiple
subscriptions

* **Documentation**
- Updated API specifications and guides for subscription filtering and
model aggregation
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
## Summary
Enable FIPS compliance in `maas-controller`.

## Description
- Set `CGO_ENABLED=1` and `GOEXPERIMENT=strictfipsruntime` in
Dockerfile.
- Add FIPS-aware build env and `GO_STRICTFIPS` toggle to Makefile.
- Extract container targets into `container.mk` with FIPS build-arg
passthrough.

## How it was tested
- Verified `make build`, `make build-image`, `make build-image-konflux`,
and `make build-push-image TAG=<my-repo>` locally.

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Refactored build system configuration and container image handling for
improved flexibility
* Reorganized Makefile structure with new automated build targets and
enhanced build configuration variables
* Updated Docker configurations to support configurable build parameters
and multiple build variants
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Chaitanya Kulkarni <chkulkar@redhat.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
List endpoint was replaced with Search endpoint. Some of the code for
List endpoint is still remaining but is a dead code. The PR removes the
dead code logic.

## How Has This Been Tested?
* `make build`
* E2E tests succeed
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **API Changes**
* Replaced API key retrieval endpoint with a new search-based approach.
The workflow now uses an authenticated POST request to
`/maas-api/v1/api-keys/search` instead of a GET list endpoint, enabling
more flexible API key discovery.

* **Documentation**
* Updated API documentation and test coverage to reflect the new
search-based workflow for managing API keys.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ce only (#553)

<!--- Provide a general summary of your changes in the Title above -->
https://redhat.atlassian.net/browse/RHOAIENG-52391

## Description
<!--- Describe your changes in detail -->
This PR:
- scopes MaaS API to get subscriptions from a configurable env var
`MAAS_SUBSCRIPTION_NAMESPACE` only.
- add corresponding test cases, and moves namespace scoping related test
to the separate file.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
Add a subscription to another namespace than default
`models-as-a-service`.
- Before the change, `/maas-api/v1/subscriptions/select` can obtain the
subscription.
- After the change, `/maas-api/v1/subscriptions/select` can't obtain the
subscription any more.

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added configurable subscription namespace (env/CLI) with a validated
default; runtime behavior now scopes subscription watching and
controller reconciliation to the configured namespace.

* **Tests**
* End-to-end and smoke tests revised to assert namespace-scoped
visibility and reconciliation, including new namespace-scoped test flows
and helpers.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!--- Provide a general summary of your changes in the Title above -->

## Description

This PR adds support for **ephemeral API keys** - short-lived
programmatic keys designed for temporary access scenarios like the GenAI
Playground or automated scripts.

### Key Features

- **Ephemeral flag**: New `ephemeral` boolean parameter when creating
API keys
- **Auto-generated names**: Name is optional for ephemeral keys
(auto-generated as `ephemeral-<timestamp>` if not provided)
- **1-hour default expiration**: Ephemeral keys default to 1hr instead
of 90 days
- **Excluded from search by default**: Ephemeral keys don't clutter
list/search results unless explicitly requested via `includeEphemeral:
true` filter
- **Ephemeral field in responses**: Both create and search responses
include the `ephemeral` boolean

### API Changes

**Create API Key** (`POST /v1/api-keys`):
```
// Minimal ephemeral key (1hr default expiration, auto-generated name)
{
  "ephemeral": true
}

// Ephemeral key with custom name and expiration
{
  "ephemeral": true,
  "name": "my-playground-key",
  "expiresIn": "2h"
}
```

**Search API Keys** (`POST /v1/api-keys/search`):
```
// Default search - excludes ephemeral keys
{
  "filters": {
    "status": ["active"]
  }
}

// Include ephemeral keys in results
{
  "filters": {
    "status": ["active"],
    "includeEphemeral": true
  }
}
```
https://redhat.atlassian.net/browse/RHOAIENG-54005 

## How Has This Been Tested?
* **Unit Tests**
* Added TestCreateEphemeralAPIKey - tests ephemeral key creation
with/without name
* Added TestSearchExcludesEphemeralByDefault - tests default search
exclusion and includeEphemeral filter
  * All tests pass: go test ./internal/api_keys/... -v
* **Build Verification**
  * make build passes (includes lint, fmt, and compile)
* **E2E Testing**
  * Deployed to OpenShift cluster
  * Verified ephemeral key creation (with/without name)
  * Verified 1-hour default expiration
  * Verified regular keys still require name (400 error without)
  * Verified ephemeral keys excluded from default search
  * Verified includeEphemeral: true filter works
  * Verified ephemeral keys work for model inference

## Merge criteria:
- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Ephemeral API Keys: short-lived programmatic tokens defaulting to 1
hour; name optional (auto-generated if omitted); creation response
includes an ephemeral flag. Ephemeral keys are excluded from
lists/search by default with an option to include them.

* **Documentation**
* API docs updated with examples for creating and searching ephemeral
keys and guidance on defaults.

* **Tests**
* Added tests covering ephemeral key creation, expiration, validation,
and search behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
abdallahsamabd and others added 22 commits March 23, 2026 21:45
## Description

Add Azure OpenAI API translation plugin under
`payload-processing/pkg/plugins/api-translation/`.

This introduces:
- The `Translator` interface (`translator/translator.go`) that defines
the contract for provider-specific request/response translation.
- The `azureopenai` translator implementation that handles path
rewriting and header adjustments for Azure OpenAI endpoints. Azure
OpenAI uses the same request/response schema as OpenAI, so no body
mutation is needed — translation is limited to rewriting the `:path`
header to `/openai/deployments/{model}/chat/completions?api-version=...`
and setting appropriate headers.
- Comprehensive test coverage including deployment ID validation, path
injection prevention, header verification, and response passthrough.

## How Has This Been Tested?

- Unit tests pass: `go test ./pkg/plugins/api-translation/...`
- Tests cover: basic chat translation, model-as-deployment-ID mapping,
missing/empty model validation, invalid character rejection (path
traversal, query injection), header mutation, body passthrough, and
response passthrough (including streaming chunks and error responses).

## Merge criteria:

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Azure OpenAI support: validates model/deployment identifiers,
rewrites requests to the Azure chat-completions endpoint using a default
API version, sets appropriate request headers, and leaves request bodies
unchanged.

* **Tests**
* Added tests covering identifier validation, endpoint/header rewriting,
API version behavior, error cases for invalid models, and response
passthrough behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Abdallah Samara <abdallahsamabd@gmail.com>
## Description

Move the Vertex AI translator from
[ai-gateway-payload-processing](https://github.com/opendatahub-io/ai-gateway-payload-processing)
to this repo as part of the repository migration
([#589](#589)).

This adds the Vertex AI (Gemini) translator under
`payload-processing/pkg/plugins/api-translation/translator/vertex/`.

### What's included

- **`translator/vertex/vertex.go`** — Full implementation of OpenAI Chat
Completions to Gemini GenerateContent API translation.
- **`translator/vertex/vertex_test.go`** — Comprehensive test suite with
**100% statement coverage** (59 tests).

### Translation capabilities

- **Request translation**: OpenAI Chat Completions → Gemini
`generateContent` format
- Message role mapping (`system`/`developer` → `systemInstruction`,
`user`/`assistant` → `contents`)
- Generation parameters (`temperature`, `top_p`, `max_tokens` →
`maxOutputTokens`, `stop` → `stopSequences`)
  - Path rewriting to `/v1beta/models/{model}:generateContent`
- Header mutation (`content-type`, `:path` set; `authorization`,
`content-length` removed)
- **Response translation**: Gemini → OpenAI Chat Completions format
  - Candidate → choices mapping with finish reason translation
  - Function calling (`functionCall` → `tool_calls`) support
  - Usage metadata translation
  - Vertex error structure → OpenAI error format conversion
- **Security**: Model name regex validation to prevent path injection
(CWE-74)

### Notes

- Uses `v1beta` of the Generative Language API for `systemInstruction`
support. If the project moves to the enterprise Vertex AI API, path
rewriting would be handled by Envoy routing rules, not by modifying this
translator.

## How Has This Been Tested?

- `go test ./pkg/plugins/api-translation/... -cover` — **100% statement
coverage**, 59 tests passing
- `go build ./...` — builds cleanly
- `golangci-lint run ./pkg/plugins/api-translation/...` — 0 issues
- `go vet ./pkg/plugins/api-translation/...` — clean
- Previously validated against real Google Gemini API (gemini-2.0-flash,
gemini-2.0-flash-lite, gemini-2.5-pro) with full pipeline testing in
ai-gateway-payload-processing PR
[#32](opendatahub-io/ai-gateway-payload-processing#32)

## Merge criteria:

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Google Vertex AI Gemini support: requests and responses are
translated to/from OpenAI-style chat completions, including role
mapping, generation settings (tokens, temperature, top-p, stop
sequences), function/tool call handling, error mapping, and usage
reporting.
* **Tests**
* Added comprehensive tests covering request/response translation,
validation, edge cases, and error scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Asaad Balum <abalum@redhat.com>
## Description

Add the `apikey-injection` ExtProc plugin to
`payload-processing/pkg/plugins/`. This plugin injects API keys into
outbound requests based on the resolved model provider. Includes the
plugin implementation, a credential store, a reconciler for syncing
Kubernetes secrets, and full unit tests.

## How Has This Been Tested?

Unit tests included for all components:
- `plugin_test.go` — request/response handling and header injection
- `store_test.go` — credential store operations
- `reconciler_test.go` — Kubernetes secret reconciliation

## Merge criteria:

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added API key injection plugin that automatically injects
authentication headers into AI provider requests.
* Supports OpenAI, Anthropic, Vertex AI, and Azure OpenAI providers with
proper header formatting for each.

* **Tests**
* Added comprehensive test coverage for plugin functionality, error
handling, and concurrent operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Yehudit Kerido <ykerido@ykerido-thinkpadp1gen7.raanaii.csb>
Co-authored-by: Yehudit Kerido <ykerido@ykerido-thinkpadp1gen7.raanaii.csb>
…tion plugins (#595)

## Description

Move the `model-provider-resolver` and `api-translation` plugins to
`payload-processing/pkg/plugins/`.

**model-provider-resolver** watches MaaSModelRef CRDs and resolves model
names to providers, writing the result (provider, credentialRef) to
CycleState for downstream plugins.

**api-translation** translates inference API requests and responses
between OpenAI Chat Completions format and provider-native formats.
Currently includes the Anthropic Messages API
translator. Vertex AI and Azure OpenAI translators are commented out
pending their own move PRs by their respective contributors.

Also uncomments the plugin registrations in `cmd/main.go` for these two
plugins.

  ## How Has This Been Tested?

  Unit tests included for all components:
- `model-provider-resolver/plugin_test.go` — request processing and
CycleState population
- `model-provider-resolver/model_store_test.go` — thread-safe model
store operations
- `api-translation/plugin_test.go` — request/response translation with
CycleState
- `api-translation/translator/anthropic/anthropic_test.go` — Anthropic
Messages API translation

  ## Merge criteria:

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Activated API translation plugin enabling automatic format conversion
between OpenAI and Anthropic APIs.
* Activated model provider resolver plugin for dynamic model-to-provider
mapping resolution from resource definitions.
  * Added Anthropic API request and response translation support.

* **Tests**
* Added comprehensive test coverage for API translation and model
provider resolver functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!--- Provide a general summary of your changes in the Title above -->

## Description
move azureai api translator to the right place in the plugins dir.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved error message clarity for AzureOpenAI model validation
failures – now displays the specific invalid model value instead of a
generic message, making troubleshooting easier.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Nir Rozenbaum <nrozenba@redhat.com>
…as-controller (#565)

Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.71.1 to 1.79.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.79.3</h2>
<h1>Security</h1>
<ul>
<li>server: fix an authorization bypass where malformed :path headers
(missing the leading slash) could bypass path-based restricted
&quot;deny&quot; rules in interceptors like <code>grpc/authz</code>. Any
request with a non-canonical path is now immediately rejected with an
<code>Unimplemented</code> error. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8981">#8981</a>)</li>
</ul>
<h2>Release 1.79.2</h2>
<h1>Bug Fixes</h1>
<ul>
<li>stats: Prevent redundant error logging in health/ORCA producers by
skipping stats/tracing processing when no stats handler is configured.
(<a
href="https://redirect.github.com/grpc/grpc-go/pull/8874">grpc/grpc-go#8874</a>)</li>
</ul>
<h2>Release 1.79.1</h2>
<h1>Bug Fixes</h1>
<ul>
<li>grpc: Remove the <code>-dev</code> suffix from the User-Agent
header. (<a
href="https://redirect.github.com/grpc/grpc-go/pull/8902">grpc/grpc-go#8902</a>)</li>
</ul>
<h2>Release 1.79.0</h2>
<h1>API Changes</h1>
<ul>
<li>mem: Add experimental API <code>SetDefaultBufferPool</code> to
change the default buffer pool. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8806">#8806</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/vanja-p"><code>@​vanja-p</code></a></li>
</ul>
</li>
<li>experimental/stats: Update <code>MetricsRecorder</code> to require
embedding the new <code>UnimplementedMetricsRecorder</code> (a no-op
struct) in all implementations for forward compatibility. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8780">#8780</a>)</li>
</ul>
<h1>Behavior Changes</h1>
<ul>
<li>balancer/weightedtarget: Remove handling of <code>Addresses</code>
and only handle <code>Endpoints</code> in resolver updates. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8841">#8841</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>experimental/stats: Add support for asynchronous gauge metrics
through the new <code>AsyncMetricReporter</code> and
<code>RegisterAsyncReporter</code> APIs. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8780">#8780</a>)</li>
<li>pickfirst: Add support for weighted random shuffling of endpoints,
as described in <a
href="https://redirect.github.com/grpc/proposal/pull/535">gRFC A113</a>.
<ul>
<li>This is enabled by default, and can be turned off using the
environment variable
<code>GRPC_EXPERIMENTAL_PF_WEIGHTED_SHUFFLING</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8864">#8864</a>)</li>
</ul>
</li>
<li>xds: Implement <code>:authority</code> rewriting, as specified in <a
href="https://github.com/grpc/proposal/blob/master/A81-xds-authority-rewriting.md">gRFC
A81</a>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8779">#8779</a>)</li>
<li>balancer/randomsubsetting: Implement the
<code>random_subsetting</code> LB policy, as specified in <a
href="https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md">gRFC
A68</a>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8650">#8650</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/marek-szews"><code>@​marek-szews</code></a></li>
</ul>
</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>credentials/tls: Fix a bug where the port was not stripped from the
authority override before validation. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8726">#8726</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/Atul1710"><code>@​Atul1710</code></a></li>
</ul>
</li>
<li>xds/priority: Fix a bug causing delayed failover to lower-priority
clusters when a higher-priority cluster is stuck in
<code>CONNECTING</code> state. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8813">#8813</a>)</li>
<li>health: Fix a bug where health checks failed for clients using
legacy compression options (<code>WithDecompressor</code> or
<code>RPCDecompressor</code>). (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8765">#8765</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/sanki92"><code>@​sanki92</code></a></li>
</ul>
</li>
<li>transport: Fix an issue where the HTTP/2 server could skip header
size checks when terminating a stream early. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8769">#8769</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/joybestourous"><code>@​joybestourous</code></a></li>
</ul>
</li>
<li>server: Propagate status detail headers, if available, when
terminating a stream during request header processing. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8754">#8754</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/joybestourous"><code>@​joybestourous</code></a></li>
</ul>
</li>
</ul>
<h1>Performance Improvements</h1>
<ul>
<li>credentials/alts: Optimize read buffer alignment to reduce copies.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8791">#8791</a>)</li>
<li>mem: Optimize pooling and creation of <code>buffer</code> objects.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8784">#8784</a>)</li>
<li>transport: Reduce slice re-allocations by reserving slice capacity.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8797">#8797</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/grpc/grpc-go/commit/dda86dbd9cecb8b35b58c73d507d81d67761205f"><code>dda86db</code></a>
Change version to 1.79.3 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8983">#8983</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/72186f163e75a065c39e6f7df9b6dea07fbdeff5"><code>72186f1</code></a>
grpc: enforce strict path checking for incoming requests on the server
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8981">#8981</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/97ca3522b239edf6813e2b1106924e9d55e89d43"><code>97ca352</code></a>
Changing version to 1.79.3-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8954">#8954</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/8902ab6efea590f5b3861126559eaa26fa9783b2"><code>8902ab6</code></a>
Change the version to release 1.79.2 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8947">#8947</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/a9286705aa689bee321ec674323b6896284f3e02"><code>a928670</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8874">#8874</a> to
v1.79.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8904">#8904</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/06df3638c0bcee88197b1033b3ba83e1eb8bc010"><code>06df363</code></a>
Change version to 1.79.2-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8903">#8903</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/782f2de44f597af18a120527e7682a6670d84289"><code>782f2de</code></a>
Change version to 1.79.1 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8902">#8902</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/850eccbb2257bd2de6ac28ee88a7172ab6175629"><code>850eccb</code></a>
Change version to 1.79.1-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8851">#8851</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/765ff056b6890f6c8341894df4e9668e9bfc18ef"><code>765ff05</code></a>
Change version to 1.79.0 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8850">#8850</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/68804be0e78ed0365bb5a576dedc12e2168ed63e"><code>68804be</code></a>
Cherry pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8864">#8864</a> to
v1.79.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8896">#8896</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/grpc/grpc-go/compare/v1.71.1...v1.79.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.71.1&new-version=1.79.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/opendatahub-io/models-as-a-service/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
This PR completes the payload processing move from its own repo to MaaS
repo.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Activated API key injection plugin for enhanced request security
  * Enabled support for Azure OpenAI translator
  * Enabled support for Vertex AI translator

* **Tests**
* Added comprehensive test coverage for Azure OpenAI request and
response handling
* Added comprehensive test coverage for Vertex request processing,
response transformation, error handling, and tool-call translation

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Nir Rozenbaum <nrozenba@redhat.com>
…alRef fields (#571)

## Summary

Adds external model provider support to the `MaaSModelRef` CRD, per the
[Egress Inference design

doc](https://docs.google.com/document/d/1poHd9g5XTa0-OwRQ7AxufB3_eq4LXlo1ycxhbdkNYUg/edit).
This enables users to register external AI providers (OpenAI, Anthropic)
alongside
   internal KServe models using a single CRD.

  ### CRD changes

  Added to `ModelReference`:
- `provider` — identifies API format and auth type (e.g., `openai`,
`anthropic`)
  - `endpoint` — external provider FQDN (e.g., `api.openai.com`)

  Added to `MaaSModelSpec`:
- `credentialRef` — references K8s Secret with provider API key (`name`
+ `namespace`)

All new fields are optional and only used when `kind: ExternalModel`.
Internal models (`kind: LLMInferenceService`) are unaffected.

  ### Controller changes

Implemented the `ExternalModel` provider handler (previously a stub
returning `ErrKindNotImplemented`):
- **ReconcileRoute**: validates user-supplied HTTPRoute
(`maas-model-<name>`) references the configured gateway
- **Status**: returns Ready once HTTPRoute is validated, derives
endpoint from gateway hostname
- **Provider validation**: ExternalModel without `provider` field →
phase=Failed with clear error message
  - **CleanupOnDelete**: no-op (user owns the HTTPRoute)

  ### Example

  ```yaml
  apiVersion: maas.opendatahub.io/v1alpha1
  kind: MaaSModelRef
  metadata:
    name: gpt-4o
    namespace: llm
  spec:
    modelRef:
      kind: ExternalModel
      name: gpt-4o
      provider: openai
      endpoint: "api.openai.com"
    credentialRef:
      name: openai-api-key
      namespace: llm
  ```

  ### Why this matters

The BBR `provider-resolver` plugin
([ai-gateway-payload-processing#14](opendatahub-io/ai-gateway-payload-processing#14))
will read these fields from
`MaaSModelRef` to resolve model→provider mappings and populate
CycleState for downstream plugins (api-translation, api-key-injection).
This makes `MaaSModelRef` the single
source of truth for both internal and external models — no separate
ConfigMaps or Secret annotations needed.

  ## Test plan

- [x] 11 unit tests (ReconcileRoute, Status, GetModelEndpoint,
validation, cleanup)
- [x] `make generate && make manifests && go build ./... && go test
./pkg/controller/maas/...` — all pass
  - [x] Deployed on RHOAI cluster (sandbox258):
- ExternalModel with provider + HTTPRoute → phase=Ready, endpoint
populated
    - ExternalModel without provider → phase=Failed, "missing provider"
- ExternalModel without HTTPRoute → phase=Failed, "create it to enable
routing"
- MaaSAuthPolicy → generates backing AuthPolicy (accepted + enforced)
    - MaaSSubscription → generates backing TokenRateLimitPolicy
    - Internal models unaffected (regression pass)
- External models route to simulator at 3.150.113.9:8000 — inference
works e2e
⎿  Tool use rejected with user message: before the commit in maas,
please validate that all local changes are production grade efficient
and also follow the repo guildlines
     nad ond optimize

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Reference credentials (Secret) for external models
  * Provider and endpoint fields for external model configuration
* Automatic HTTPRoute discovery and HTTPS endpoint derivation with
hostname resolution; status fields populated when route is accepted
* **Bug Fixes / Validation**
  * Require provider for ExternalModel
* Fail validation if credential namespace differs from resource
namespace
* **Tests**
* Added unit tests covering routing, endpoint resolution, readiness, and
credential ref handling
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This defaults our hack script to deploy ODH to EA1 through a manual
deploy to prevent auto upgrade to that latest version

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added environment variables to control operator installation version
and install-plan approval behavior.

* **Chores**
  * Default operator channel changed to fast-3.
* Operator subscription and install flow improved to support explicit
marketplace, starting version, and manual approval handling.
  * Minor test/deployment messaging hygiene updated.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Yuriy Teodorovych <Yuriy@ibm.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
Replaces Auth CR-based admin detection with Kubernetes
SubjectAccessReview (SAR) for RBAC-aligned authorization.

**Problem**: The existing admin checker enumerated groups from the
`services.opendatahub.io/v1alpha1/Auth` CR, which required maintaining a
separate admin group list and didn't align with Kubernetes RBAC.

**Solution**: Use SAR to check if a user can `create
maasauthpolicies.maas.opendatahub.io` in the MaaS namespace. This
leverages existing RBAC rules - anyone with this permission is
considered an admin.

https://redhat.atlassian.net/browse/RHOAIENG-54600

## How Has This Been Tested?
* Unit tests
* E2E tests
* Local testing

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Admin authorization now uses Kubernetes RBAC (SubjectAccessReview) for
platform-aligned checks.
* Authorization for API key actions (view, revoke, search, bulk revoke)
made context-aware and simplified.
* **Chores**
* Cluster RBAC updated to permit subject access reviews required for the
new admin checks.
* **Tests**
* Unit tests updated and added to validate the new RBAC-based admin
checks and behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…endpoint (#584)

<!--- Provide a general summary of your changes in the Title above -->
https://redhat.atlassian.net/browse/RHOAIENG-54596

## Description
<!--- Describe your changes in detail -->
### API key minting
- A column to API key storage and an optional field to API key creation
request for subscription are added.
- If the field is omitted, the subscription with the highest priority is
selected (with limit and name being the tie breaker).
### AuthPolicy flow
- Subscription name is now grabbed from `apiKeyValidation` result
instead of the header `X-MaaS-Subscription`.
- `subscription-info` is still needed for validation check, as well as
the header. It is injected after auth success.
### Controller feedback
- MaaSSubscriptions have a status condition `SpecPriorityDuplicate` to
indicate if they share the same priority with other peers.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
- Created API keys with different subscriptions, and see them in the
database.
- Verified the auto-select rule.
- Verified that each API key can only access the model ref in associated
subscription.
- Verified the status condition.

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* API keys are now bound to a specific subscription at creation time.
Optionally specify a `subscription` parameter when creating keys; if
omitted, the highest-priority accessible subscription is automatically
selected.
* Subscriptions can now include a `priority` field to control default
selection when multiple are available.
* The `X-MaaS-Subscription` header is no longer required for API
key-based inference requests; subscription is determined by the key's
binding.

* **Documentation**
* Updated guides to reflect subscription binding at key creation and
removed header-requirement documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Jim Rhyness <jrhyness@redhat.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
Implements typed reference pattern for ModelReference to replace
inlining variant-specific config, enabling extensible model kinds
without schema changes to ModelReference.

## How Has This Been Tested?
- Unit tests: All existing unit tests pass, new ExternalModel handler
tests added
- E2E tests: Enhanced test_namespace_scoping.py to create ExternalModel
CRs inline using existing _apply_cr pattern
- Manual testing: ExternalModel CRD generation and controller
reconciliation verified

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced external model provider support, enabling configuration of
external models as an alternative backend alongside inference services.

* **Documentation**
* Added comprehensive guide for external model configuration, including
credential and endpoint management.
* Updated model reference documentation to clarify support for both
inference services and external models.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
https://issues.redhat.com/browse/RHOAIENG-41301

## Description
Approach:
- For default values and env var parsing: Call Load() directly (it only
registers flags, doesn't parse them, so it's safe to test values before
flag.Parse()). Use t.Setenv() for env var isolation.
- For flag binding: Test bindFlags() directly with a fresh flag.FlagSet
per test case, which avoids the global state issue entirely.
Load() calls loadTLSConfig() internally, which reads TLS_CERT, TLS_KEY,
TLS_SELF_SIGNED.

## How Has This Been Tested?
Tested locally on my machine before creating the PR

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Expanded configuration test suite with end-to-end coverage for default
values, environment-variable overrides, flag reset handling, validation
rules (TLS consistency, certificate/key pairing, API key expiration
policy and bounds), secure/insecure address defaults, and
deprecated-flag behavior; added helper to reinitialize command-line
flags so Load() can be repeatedly registered in tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Yuriy Teodorovych <Yuriy@ibm.com>
This PR addresses the review comments from @nirrozenbaum on #586 that
were not resolved before merge.

##Changes

1. Remove namespace field from credentialRef
Comment: "isn't it always the same namespace? is there a use case for
other namespace? I would argue that it's the same as MaaSModelRef
expecting to reference ExternalModel from the same namespace.. IMO we
should be consistent."
  Remove optional namespace field from ExternalModel.spec.credentialRef
Credential Secret must now reside in the same namespace as the
ExternalModel (consistent with MaaSModelRef → ExternalModel reference
pattern)
2. Clarify rate-limiting model identity
Comment: "which field represents the model that MaaS is rate limiting
on? is it MaaSModelRef metadata.name? model.ref.Name?"
Update documentation to clarify that rate limiting is keyed on
MaaSModelRef.metadata.name
Add explanation of the relationship between MaaSModelRef and
ExternalModel
3. Document single credential limitation
Comment: "this works as long as there is a single api key per external
model. I'm assuming this won't be the case for long time. just as a fyi"
Add documentation note about current single-credential-per-ExternalModel
limitation
(Optional) Create tracking issue for multi-credential support


## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Breaking Changes**
* Credential references no longer support namespace override;
credentials must reside in the same namespace as the `ExternalModel`.

* **Validation**
  * Name fields now require a minimum length of 1 character.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…endpoint (#614)

<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Clarified API key authentication flow: keys now bind to a specific
subscription at creation time
* Updated guidance on when to use `X-MaaS-Subscription` header—required
only for user tokens with multiple subscriptions, not for API key-based
inference
* Revised model listing and inference examples to reflect the new
subscription binding behavior for API keys
* Updated authentication flow diagrams and troubleshooting guidance to
reflect current behavior

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Jim Rhyness <jrhyness@redhat.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…ition (#600)

## Summary

Related to - https://redhat.atlassian.net/browse/RHOAIENG-51551

Fixes the issue where MaaSModelRefs get stuck in non-Ready state when
created before KServe creates the corresponding HTTPRoute. This
eliminates the need for the controller bounce workaround in E2E tests.

## Problem

MaaSModelRefs could get stuck indefinitely when:
1. MaaSModelRef is created before HTTPRoute exists (common during
startup)
2. Controller treats "HTTPRoute not found" as permanent failure
3. Sets status to `Failed` and returns error
4. Triggers exponential backoff that can reach minutes
5. Requires controller restart to recover

**Current workaround:** E2E tests bounce the controller and retry (lines
262-283 in `prow_run_smoke_test.sh`)

## Solution

Changed HTTPRoute missing from permanent failure to temporary condition:

- **Added** `ErrHTTPRouteNotFound` sentinel error to distinguish
temporary from permanent failures
- **Changed** log level from ERROR → Warn with message `HTTPRoute not
found for LLMInferenceService, will retry when created`
- **Set** status to `Pending` (not Failed) when HTTPRoute doesn't exist
yet
- **Leverages** existing HTTPRoute watch for defense-in-depth when route
is created

## Changes

- `providers.go`: Added `ErrHTTPRouteNotFound` error
- `providers_llmisvc.go`: Return `ErrHTTPRouteNotFound` with INFO log
when HTTPRoute missing
- `maasmodelref_controller.go`: Handle `ErrHTTPRouteNotFound` with
Pending + RequeueAfter
- `maasmodelref_controller_test.go`: Added
`TestMaaSModelRefReconciler_HTTPRouteRaceCondition`
- `prow_run_smoke_test.sh`: Removed controller bounce workaround

## Testing

### Unit Tests
```bash
✅ make test - all tests pass
✅ TestMaaSModelRefReconciler_HTTPRouteRaceCondition - validates Pending → Ready transition
```

### Manual Cluster Testing
Deployed fix to live cluster and verified:
(https://console-openshift-console.apps.ci-ln-4jf5smt-76ef8.aws-4.ci.openshift.org/)
- ✅ MaaSModelRef enters Pending when HTTPRoute doesn't exist
- ✅ Controller logs INFO "will retry" (not ERROR)
- ✅ Transitions to Ready when HTTPRoute appears
- ✅ No controller bounce needed

### Expected E2E Impact
E2E tests will now:
- ✅ Wait for MaaSModelRefs to reach Ready naturally (no bounce)
- ✅ Fail fast if they don't (with diagnostics)
- ✅ Complete faster (no retry loop)

## Validation

**Before:**
```
HTTPRoute missing → ERROR → Failed status → Exponential backoff → Stuck
```

**After:**
```
HTTPRoute missing → INFO "will retry" → Pending status → RequeueAfter 5s → Ready
```

## Fixes

- Resolves issue described in
test/e2e/scripts/prow_run_smoke_test.sh:262-278
- Eliminates need for controller bounce workaround
- Makes MaaSModelRef reconciliation reliable during startup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Missing HTTP routes are now treated as transient: models move to
Pending and reconciliation continues instead of failing; logging for
expected route-not-found races is reduced and controllers skip
route-dependent work until routes appear.

* **Tests**
* Added a unit test covering out-of-order resource creation (HTTPRoute
appearing after model) and successful recovery.

* **Improvements**
* Deployment script uses a bounded timeout poll, clearer diagnostics on
failure, and a configurable model namespace for tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…#591)

## Summary

Related to https://redhat.atlassian.net/browse/RHOAIENG-53771

Automatically create the `models-as-a-service` namespace when the MaaS
controller starts, eliminating the need for manual `kubectl create
namespace` before deploying MaaSAuthPolicy and MaaSSubscription CRs.

## Changes

### Controller (`maas-controller/cmd/manager/main.go`)
- Added `ensureNamespace()` function that creates the subscription
namespace on startup
- Creates namespace with label `opendatahub.io/generated-namespace:
"true"`
- Handles existing namespace gracefully (idempotent)
- Exits with error only on actual failures (not AlreadyExists)
- Supports configurable namespace via `--maas-subscription-namespace`
flag

### RBAC (`deployment/base/maas-controller/rbac/clusterrole.yaml`)
- Added namespace permissions: `create`, `get`, `list`, `watch`
- Follows least-privilege principle (no delete/update)

### Deploy Script (`scripts/deploy.sh`)
- Creates subscription namespace in kustomize mode
- Respects `MAAS_SUBSCRIPTION_NAMESPACE` environment variable
- Idempotent - checks if namespace exists before creating

### Documentation
- Removed manual namespace creation steps from:
  - `docs/content/migration/tier-to-subscription.md`
  - `maas-controller/README.md`
  - `docs/samples/maas-system/README.md`
- Added notes explaining auto-creation behavior

## Testing

✅ **Verified on live OpenShift cluster:**
- Namespace auto-created with correct label on controller startup
- MaaS CRs created successfully without manual namespace creation
- Idempotent behavior confirmed - controller restarts without errors
when namespace exists
- Generated Kuadrant policies working correctly (AuthPolicy +
TokenRateLimitPolicy)

**Test evidence:**
```bash
# Namespace auto-created:
kubectl get namespace models-as-a-service -o jsonpath='{.metadata.labels.opendatahub\.io/generated-namespace}'
# Output: true

# Controller logs:
# {"msg":"subscription namespace ready","namespace":"models-as-a-service"}

# MaaS CRs created without manual namespace step:
kubectl get maasauthpolicy,maassubscription -n models-as-a-service
# All CRs in Active phase
```

## Acceptance Criteria

- [x] **AC1:** Namespace auto-created when operator runs, users can
create MaaS CRs without manual `kubectl create namespace`
- [x] **AC2:** Supports configurable subscription namespace via
`--maas-subscription-namespace` flag
- [x] **AC3:** Idempotent - no errors when namespace already exists

## Benefits

- 🎯 **Reduces installation friction** - one less manual step for users
- ✅ **Improves UX** - namespace creation happens automatically
- 🔧 **Flexible** - supports custom namespace names via configuration
- 🛡️ **Safe** - idempotent, minimal RBAC permissions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Controller now auto-creates exactly one subscription namespace on
startup (default: models-as-a-service) and supports creating a single
custom subscription namespace via configuration; deployment tooling
ensures the subscription namespace is applied.
* Controller granted permissions to create and read namespaces so it can
create and monitor them.

* **Documentation**
* Deployment guides, samples, and migration docs updated to remove
manual namespace creation and document automatic/custom-namespace
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
<!--- Provide a general summary of your changes in the Title above -->
https://redhat.atlassian.net/browse/RHOAIENG-54149

## Description
<!--- Describe your changes in detail -->
Fixing namespace inconsistency in documentation in accordance to the
following rules:
- MaaS API and controller are in `opendatahub` by default;
- MaaSAuthPolicy and MaaSSubscription are in `models-as-a-service` by
default;
- MaaSModelRef is in the same namespace as the service it refers to
(e.g., `llm`).

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
N/A

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Clarified that model references are discovered cluster-wide by the
MaaS API, with access filtering applied per referenced model.
* Specified that model-reference resources should be created in the same
namespace as their backend model; default namespaces for controller/API
and for auth/subscription resources are documented.
* Updated examples and verification commands to reflect the corrected
namespace guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
https://issues.redhat.com/browse/RHOAIENG-49788

## Description
**Context**
MaaS CRDs (MaaSModelRef, MaaSAuthPolicy, MaaSSubscription) have no
human-readable display metadata. The `GET /v1/models` API already has a
Details struct with `displayName`, `description`, and `genaiUseCase`
fields, and annotation constants already exist
(openshift.io/display-name, openshift.io/description,
opendatahub.io/genai-use-case), but `maasModelRefToModel()` never reads
annotations — so modelDetails is always `nil`.

**Approach**: mainly using Annotations
- [x] Use existing OpenShift-standard annotations for display
name/description on all 3 CRDs
- [x] Use opendatahub.io/* annotations for model-specific metadata
(contextWindow)
- [x] No CRD spec field changes — no regeneration needed
- [x] Enrich GET /v1/models API response with annotation data

## How Has This Been Tested?
Successfully ran the full test suite locally.

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added a CRD Annotations Reference and updated configuration/flow
guides to document supported annotations (display name, description,
GenAI use case, context window) and their appearance in model listings;
added tier-based access guidance.

* **New Features**
* Model listing/response now surfaces annotation-driven metadata
(modelDetails: displayName, description, genaiUseCase, contextWindow).
  * Sample manifests and examples updated with annotation usage.

* **Tests**
  * Tests updated to validate annotation-driven model details.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Yuriy Teodorovych <Yuriy@ibm.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
Implements per-key unique salt for API key hashing to meet FIPS 180-4
compliance requirements. The key ID embedded in each API key serves as
the salt, enabling deterministic O(1) hash lookups while providing
cryptographic uniqueness per key.

**Motivation**
Plain SHA-256 hashing of API keys doesn't meet FIPS compliance
requirements for production deployments. This change adds per-key
salting without requiring database schema changes or breaking the
existing O(1) lookup performance.

### Changes
**New API Key Format:**
```
Before: sk-oai-{secret}
After:  sk-oai-{key_id}_{secret}
```

**Hash Computation:**
```
Before: SHA-256(full_key)
After:  SHA-256(key_id + secret)
```
The `key_id` is a 96-bit random identifier (~16 base62 chars) that
serves as a unique per-key salt.

### Performance

Operation | Before | After | Overhead
-- | -- | -- | --
GenerateAPIKey | 2,322 ns | 3,312 ns | +43%
HashAPIKey | 101 ns | 157 ns | +55%
ValidateAPIKeyHash | N/A | 174 ns | New

### Security
- FIPS 180-4 compliant (salted SHA-256)
- Per-key unique salt prevents rainbow table attacks
- Constant-time hash comparison prevents timing attacks
- 256-bit entropy for secret portion
- 96-bit entropy for key_id (salt)

### Breaking Changes
⚠️ API keys created with the new format are not compatible with older
maas-api versions. Existing keys will fail validation after upgrade.
This is expected for a POC/feature branch.

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
- [x] Unit tests pass (`go test ./internal/api_keys/...`)
- [x] Linters pass (`make build`)
- [x] Manual testing
- [x] Benchmarks run
- [x] E2E tests

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * API key format redesigned with separate key ID and secret components
  * New methods for parsing API keys and validating key hashes

* **Bug Fixes**
  * Enhanced key hashing with improved salt-based computation

* **Tests**
* Added comprehensive test coverage for key parsing, validation, and
format verification
  * Extended uniqueness tests for all key components
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Description

**Summary:**

Related to - https://redhat.atlassian.net/browse/RHOAIENG-55555

When RHOAI operator deploys maas-controller to redhat-ods-applications
namespace, the ClusterRoleBinding was hardcoded to bind the
ServiceAccount in the 'opendatahub' namespace, causing RBAC permission
errors and CrashLoopBackOff on startup.

**Root cause:**
- ClusterRoleBinding hardcoded: namespace: opendatahub
- RHOAI deploys to: redhat-ods-applications
- ServiceAccount mismatch → Forbidden errors

**Changes:**
1. Parameterized RBAC binding namespaces in ODH overlay kustomization
   - ClusterRoleBinding now uses app-namespace parameter
   - RoleBinding now uses app-namespace parameter
   - Works for both opendatahub and redhat-ods-applications

2. Improved namespace creation logic in controller
   - Check namespace existence before attempting creation
   - Handle Forbidden errors without retry (operator may pre-create)
   - Clearer error messages for troubleshooting

Fixes maas-controller CrashLoopBackOff in RHOAI 3.4ea2+ deployments.

Tested on RHOAI 3.3.0 with DSC ModelsAsServiceReady: True.

## How Has This Been Tested?

### Test Environment
- **Platform**: OpenShift 4.x (AWS)
- **Cluster**: `api.ci-ln-3pwgqm2-76ef8.aws-4.ci.openshift.org`
- **Operator**: RHOAI v3.3.0 (rhods-operator.3.3.0)
- **Policy Engine**: RHCL v1.3.1 (Red Hat Connectivity Link)
- **Deployment Mode**: Operator (RHOAI)
- **Test Date**: 2026-03-26
### Test Results

#### ✅ 1. Controller Pod Status
```bash
$ oc get pods -n redhat-ods-applications -l app=maas-controller
NAME                               READY   STATUS    RESTARTS   AGE
maas-controller-68574bd4fc-wnb8n   1/1     Running   0          100s
```
**Result**: Pod running successfully (no CrashLoopBackOff)

#### ✅ 2. Namespace Creation
```bash
$ oc get namespace models-as-a-service
NAME                  STATUS   AGE
models-as-a-service   Active   92s
```
**Result**: Namespace auto-created by controller

#### ✅ 3. RBAC Bindings Verification
```bash
$ oc get clusterrolebinding maas-controller-rolebinding -o yaml | grep -A 5 "subjects:"
subjects:
- kind: ServiceAccount
  name: maas-controller
  namespace: redhat-ods-applications
```
**Result**: ClusterRoleBinding correctly references
`redhat-ods-applications` namespace

```bash
$ oc get rolebinding -n redhat-ods-applications maas-controller-leader-election-rolebinding -o yaml | grep -A 5 "subjects:"
subjects:
- kind: ServiceAccount
  name: maas-controller
  namespace: redhat-ods-applications
```
**Result**: RoleBinding correctly references `redhat-ods-applications`
namespace

#### ✅ 4. RBAC Permissions Validation
```bash
$ oc auth can-i get namespaces --as=system:serviceaccount:redhat-ods-applications:maas-controller
yes

$ oc auth can-i list namespaces --as=system:serviceaccount:redhat-ods-applications:maas-controller
yes

$ oc auth can-i create namespaces --as=system:serviceaccount:redhat-ods-applications:maas-controller
yes
```
**Result**: All required namespace permissions granted

#### ✅ 5. Controller Logs Verification
```bash
$ oc logs -n redhat-ods-applications deployment/maas-controller --tail=10
```

**Key log entries**:
```json
{"level":"info","msg":"subscription namespace not found, attempting to create it","namespace":"models-as-a-service"}
{"level":"info","msg":"subscription namespace ready","namespace":"models-as-a-service"}
{"level":"info","msg":"watching namespace for MaaS AuthPolicy and MaaSSubscription","namespace":"models-as-a-service"}
{"level":"info","msg":"starting manager"}
{"level":"info","msg":"Starting Controller","controller":"maasmodelref"}
{"level":"info","msg":"Starting Controller","controller":"maassubscription"}
{"level":"info","msg":"Starting Controller","controller":"maasauthpolicy"}
```
**Result**:
- Namespace creation logic executed successfully
- All controllers started without errors
- No Forbidden errors in logs

#### ✅ 6. DataScienceCluster Status
```bash
$ oc get datasciencecluster default-dsc -o jsonpath='{.status.conditions[?(@.type=="ModelsAsServiceReady")]}'
```
**Output**:
```json
{
  "lastTransitionTime": "2026-03-26T17:08:48Z",
  "status": "True",
  "type": "ModelsAsServiceReady"
}
```
**Result**: ModelsAsServiceReady condition = True

```bash
$ oc get datasciencecluster default-dsc -o jsonpath='{.status.conditions[?(@.type=="Ready")]}'
```
**Output**:
```json
{
  "lastTransitionTime": "2026-03-26T17:08:48Z",
  "status": "True",
  "type": "Ready"
}
```
**Result**: Overall DSC Ready condition = True

#### ✅ 7. Component Deployment Verification
```bash
$ oc get deployment -n redhat-ods-applications
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
maas-api          1/1     1            1           5m
maas-controller   1/1     1            1           5m
postgres          1/1     1            1           5m
```
**Result**: All MaaS components deployed and ready

#### ✅ 8. ClusterRole Permissions Inspection
```bash
$ oc get clusterrole maas-controller-role -o yaml
```
**Namespace permissions**:
```yaml
- apiGroups:
  - ""
  resources:
  - namespaces
  verbs:
  - create
  - get
  - list
  - watch
```
**Result**: All required verbs present for namespace operations

### Regression Testing

#### ✅ Standalone Deployment (opendatahub namespace)
The fix maintains backward compatibility with standalone deployments
using the `opendatahub` namespace:

**Kustomize validation**:
```bash
$ cd deployment/overlays/odh
$ cat params.env
app-namespace=opendatahub
...

$ kustomize build . | grep -A 5 "kind: ClusterRoleBinding"
kind: ClusterRoleBinding
metadata:
  name: maas-controller-rolebinding
subjects:
- kind: ServiceAccount
  name: maas-controller
  namespace: opendatahub  ✅
```
**Result**: Standalone deployments unaffected

### Code Quality Checks

#### ✅ Error Handling
The improved namespace creation logic includes:
- Pre-check: Verify namespace existence before attempting creation
- Permanent error detection: Forbidden errors are not retried
- Clear error messages: `"service account lacks permission to create
namespace %q — either pre-create the namespace or grant 'create' on
namespaces"`

#### ✅ Graceful Degradation
- If namespace exists (pre-created by operator): Controller proceeds
without creation attempt
- If namespace doesn't exist and SA has permissions: Controller creates
it
- If namespace doesn't exist and SA lacks permissions: Controller fails
with clear actionable error

### Performance Impact
- **Startup time**: No noticeable impact
- **Resource usage**: No change
- **Network calls**: +1 GET call to check namespace existence (before
create attempt)

### Summary

| Test Case | Expected | Actual | Status |
|-----------|----------|--------|--------|
| Controller pod status | Running 1/1 | Running 1/1 | ✅ PASS |
| Namespace auto-creation | Created | Created | ✅ PASS |
| ClusterRoleBinding namespace | redhat-ods-applications |
redhat-ods-applications | ✅ PASS |
| RoleBinding namespace | redhat-ods-applications |
redhat-ods-applications | ✅ PASS |
| RBAC permissions | get, list, create namespaces | get, list, create
namespaces | ✅ PASS |
| Controller logs | No Forbidden errors | No Forbidden errors | ✅ PASS |
| DSC ModelsAsServiceReady | True | True | ✅ PASS |
| DSC Overall Ready | True | True | ✅ PASS |
| Backward compatibility | opendatahub works | opendatahub works | ✅
PASS |

**Overall Result**: ✅ **ALL TESTS PASSED**

### Deployment Timeline
```
17:05:00 - Deployment started (RHOAI operator installation)
17:07:00 - RHCL operator ready
17:08:00 - RHOAI operator ready
17:08:48 - DSC applied, MaaS controller starting
17:09:05 - maas-controller pod running
17:09:05 - models-as-a-service namespace created
17:09:05 - All reconcilers started
17:10:00 - Full deployment validated

Total deployment time: ~5 minutes
```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [x] The commits are squashed in a cohesive manner and have meaningful
messages.
- [x] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [x] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved namespace existence checking with enhanced error handling for
permission failures.
* Refined namespace creation retry logic to properly distinguish between
recoverable and non-recoverable errors.

* **Configuration**
* Extended namespace configuration in deployment overlays to ensure
proper namespace settings for role bindings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
<!--- Provide a general summary of your changes in the Title above -->

## Description
revert payload processing

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets
all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] The commits are squashed in a cohesive manner and have meaningful
messages.
- [ ] Testing instructions have been added in the PR body (for PRs
involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that
the changes work


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Removals**
* Removed the entire payload-processing module including all plugin
framework and implementations
* Removed API translation capabilities and support for Anthropic, Azure
OpenAI, and Vertex AI providers
* Removed API key injection and Kubernetes secret credential management
features
* Removed model provider resolution with dynamic Kubernetes custom
resource integration
* Removed Docker containerization build configuration and development
tooling infrastructure

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Nir Rozenbaum <nrozenba@redhat.com>
@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Mar 27, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: github-actions[bot]
Once this PR has been reviewed and has the lgtm label, please assign sb159 for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Mar 27, 2026

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Mar 27, 2026

Hi @github-actions[bot]. Thanks for your PR.

I'm waiting for a opendatahub-io member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

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.