Skip to content

fix: restore the default Endpoint Picker image - #186

Merged
dcmcand merged 5 commits into
nebari-dev:mainfrom
Keerthirajan58:fix/epp-default-image
Nov 11, 2026
Merged

dcmcand merged 5 commits into
nebari-dev:mainfrom
Keerthirajan58:fix/epp-default-image

Conversation

@Keerthirajan58

@Keerthirajan58 Keerthirajan58 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • replace the removed llm-d-inference-scheduler:v0.8.0 default with llm-d-router-endpoint-picker:v0.9.0 in Helm and the operator fallback
  • move the built-in EndpointPickerConfig to the non-deprecated llm-d.ai/v1alpha1 API
  • add an anonymous OCI manifest check for the image defaults in values.yaml
  • let the EPP Role read inferenceobjectives and inferencemodelrewrites in llm-d.ai, with the same rule in the operator ClusterRole
  • add an Upgrading note for installs that mirror the old v0.8.0 image

Why

The old GHCR package no longer grants an anonymous pull token, so new and rescheduled EPP pods fail with ImagePullBackOff. Updating the default restores the Endpoint Picker deployment. The CI check exercises the public Registry v2 Bearer flow and fails if any checked default becomes unavailable.

The availability check discovers both scalar image values and repository/tag mappings. It uses no Docker or GitHub package credentials, and reports missing manifests, access denial, rate limits, and registry failures separately. A reference with both a tag and a digest is checked by digest. Mappings with an empty tag or no repository, such as openmeter.setup.image and openmeter.console.image, cannot be checked from values.yaml alone (the release workflow pins those tags), so the check prints a SKIP line for them.

llm-d-router v0.9.0 reads InferenceObjective and InferenceModelRewrite from llm-d.ai when those CRDs are installed, so the EPP Role grants read access there as well as in the legacy group. The operator ClusterRole carries the same rule because Kubernetes rejects a Role that grants permissions its creator does not hold (#121).

llm-d-inference-scheduler:v0.8.0 cannot load the llm-d.ai/v1alpha1 config, so an install that sets defaults.epp.image to a mirror of the old image needs to mirror the new one. The Upgrading page now says so.

Verification

  • cd operator && make test with the released AI Gateway v1.1.0 CRDs, as in CI
  • cd operator && make lint
  • cd operator && go test -race ./internal/imagecheck -count=1
  • helm lint charts/nebari-llm-serving/ with the chart's required values
  • helm template confirms LLM_DEFAULT_EPP_IMAGE=ghcr.io/llm-d/llm-d-router-endpoint-picker:v0.9.0 and the llm-d.ai rule in the operator ClusterRole
  • actionlint .github/workflows/test.yaml
  • docs: npm test, npm run build, and scripts/check-links.sh
  • current values: 7 anonymous manifest checks pass and the 2 openmeter images are skipped
  • untouched upstream values: 6 checks pass and the removed EPP package fails with HTTP 403
  • envtest API server: the operator, bound to the updated ClusterRole, can create the EPP Role; bound to main's ClusterRole, it is refused for the two llm-d.ai resources

Kubernetes testing used envtest and rendered manifests rather than a live cluster.

Fixes #174

@dcmcand dcmcand left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for picking this up. The image fix is right and we need it: main still ships the removed llm-d-inference-scheduler:v0.8.0, so every new EPP pod fails to pull. I checked the compatibility claims against llm-d-router v0.9.0 and they hold. The default-image check needs two parser fixes before it can run against current main, and the branch needs a rebase.

🛑 Merge conflict in .github/workflows/test.yaml. main has gained jobs since this branch was cut. Please rebase onto main.

💡 Let the EPP's Role read the llm-d.ai inference group too.

  • In llm-d-router v0.9.0, InferenceObjective and InferenceModelRewrite moved to llm-d.ai, and the EPP prefers that group when its CRDs are installed (pkg/epp/server/controller_config.go:66-77 at v0.9.0).
  • The Role in inferencepool.go (around line 323) only grants inference.networking.x-k8s.io.
  • On a cluster with the new CRDs installed, the informers would likely fail to sync again, the crash loop the comment above that rule describes. That's inferred from the source, not observed.
  • Fix: add a rule for llm-d.ai with the same resources and verbs, and update that comment, which still says "v0.8.x".

💡 v0.9.0 is a fine pin for this fix. Moving to v0.10.0, v0.11.0 or v1.0.0 belongs in a separate PR.

What I verified (static reading at 8d4a2fc4, plus curl; the checker itself wasn't run):

  • llm-d.ai/v1alpha1 is registered at v0.9.0 (apix/config/v1alpha1/zz_generated.register.go).
  • max-score-picker and single-profile-handler are registered (cmd/epp/runner/runner.go:504,507).
  • Every flag the operator passes exists (pkg/epp/server/options.go).
  • llm-d-router-endpoint-picker:v0.9.0 returns 200 to an anonymous manifest HEAD, and the old image returns 403.

}
return strings.TrimSpace(image), nil
case map[string]any:
repository, err := requiredString(image, "repository", pathText)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 On current main this errors before any image is checked, so the new CI job fails on every run.

main's values.yaml has openmeter.console.image: {tag: ""} and openmeter.setup.image: {tag: ""}, with no repository. The release workflow fills those tags in at release time. requiredString returns openmeter.console.image.repository is required, and main.go exits.

Please skip mappings that have no repository or have an empty tag (those are pinned at release time), and add a defaults_test.go case for that shape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9ca78cc. A mapping with an empty tag or no repository is now skipped and shows up as a SKIP line (SKIP openmeter.console.image: empty tag). A repository with no tag key at all is still an error, so an entry that leaves the key out still fails the check. TestParseDefaultImagesSkipsReleasePinnedMappings covers the {tag: ""} shape from main, and run locally against this branch's values.yaml the checker now passes 7 images and skips these two.


name := image
manifestReference := ""
if at := strings.LastIndex(image, "@"); at > lastSlash {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 A reference that has both a tag and a digest keeps the :tag in the repository path.

main has usage.collector.image.tag: "v1.0.0-beta.234@sha256:cdc3...". That builds ghcr.io/openmeterio/benthos-collector:v1.0.0-beta.234@sha256:..., which requests https://ghcr.io/v2/openmeterio/benthos-collector:v1.0.0-beta.234/manifests/sha256:.... That URL returns 404, while the same URL without :v1.0.0-beta.234 returns 200.

After splitting off the digest, please also strip a :tag that follows the last / from name, and add a test case for repo:tag@sha256:....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9ca78cc. When a digest is present, a :tag after the last / is now dropped from the name, and a registry port such as localhost:5000 is left alone. I added tag and digest, registry port with tag and digest and registry port with digest cases to TestParseImageReference, and the real usage.collector.image reference went from HTTP 404 to OK.

if parsed["kind"] != "EndpointPickerConfig" {
t.Errorf("expected parsed kind=EndpointPickerConfig, got %v", parsed["kind"])
}
if parsed["apiVersion"] != "llm-d.ai/v1alpha1" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❤️ Nice touch. This assertion pins the move to the non-deprecated config API, so a later revert to the deprecated inference.networking.x-k8s.io group fails a test instead of only logging a deprecation warning.

main's values.yaml now has openmeter.setup.image and
openmeter.console.image with an empty tag and no repository. The release
workflow pins those tags and the subchart supplies the repository, so the
check cannot build a reference for them. The parser now returns such
mappings with a SkipReason, and the CLI prints a SKIP line for each. A
mapping that has a repository but no tag key is still rejected.

A reference with both a tag and a digest kept the tag in the repository
path, so usage.collector.image requested a manifest URL that returns
404. The tag before the digest is now dropped; a registry port stays.
llm-d-router v0.9.0 reads InferenceObjective and InferenceModelRewrite
from llm-d.ai when those CRDs are installed, and from
inference.networking.x-k8s.io otherwise
(pkg/epp/server/controller_config.go). The EPP Role only granted the
legacy group. It now also grants read access to the two llm-d.ai CRDs
that v0.9.0 ships, matching the router's own Helm chart.

The operator must already hold any permission it grants (nebari-dev#121), so the
chart ClusterRole and dev/manifests/operator.yaml get the same rule.
Without it the API server rejects the EPP Role as an escalation.
The operator now writes the built-in EPP config as llm-d.ai/v1alpha1.
llm-d-inference-scheduler v0.8.0 loads its config with the
gateway-api-inference-extension v1.5.0 loader, which only registers
inference.networking.x-k8s.io, so an install that pins a mirror of the
old image needs to move to the new one.
llm-d-router-endpoint-picker is the image name. The EPP deployment
template the specs follow is in llm-d-router's routerlib chart
(config/charts/routerlib/templates/_deployment.yaml at v0.9.0).
@Keerthirajan58

Copy link
Copy Markdown
Contributor Author

I rebased onto main and kept both test-provider-tools and check-default-images in test.yaml. The original commit is otherwise unchanged, and the review changes are separate commits on top. With the parser fixes in 9ca78cc, the checker passes locally on this branch's values.yaml: 7 images OK and the two openmeter images skipped. Against main's values.yaml it fails only on llm-d-inference-scheduler:v0.8.0, with HTTP 403.

c70fe68 lets the EPP Role read inferenceobjectives and inferencemodelrewrites in llm-d.ai. At v0.9.0 those are the only two llm-d.ai CRDs (config/crd/bases), and the router's own chart grants the same pair (config/charts/llm-d-router-gateway/templates/rbac.yaml), so I left inferencepools and inferencepoolimports out of the new rule. The chart ClusterRole and dev/manifests/operator.yaml got the same rule, since the operator can only grant what it holds (#121). I checked that with a local envtest test that I kept out of the PR: bound to main's ClusterRole, the operator was refused the new Role ("attempting to grant RBAC permissions not currently held" for those two resources), and the updated ClusterRole let it through.

fc9ce13 adds a short entry to the Upgrading page. The gateway-api-inference-extension v1.5.0 loader that llm-d-inference-scheduler:v0.8.0 uses rejects the llm-d.ai/v1alpha1 config, so an install that mirrors the old image has to mirror the new one too. fe9c609 is a one-line comment fix: the inferencepool.go header named the image instead of the llm-d-router chart the specs follow.

make test with the released AI Gateway CRDs, make lint, helm lint, actionlint, and the docs tests and build all pass locally.

@dcmcand dcmcand left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: Approve. Every item from my last review is fixed, and one optional suggestion is inline.

💡 Suggestion: No test checks that the operator ClusterRoles hold every rule in buildEPPRole, so a new EPP rule can break the Role again. Please open a follow-up issue for a test that compares them. This predates the PR and does not block it.

Previous item Status Evidence
🛑 Merge conflict in test.yaml Fixed Both jobs present, merges cleanly into main
🛑 {tag: ""} mappings crash the checker Fixed in 9ca78cc Checker passes at fe9c609: 7 OK, 2 SKIP
🛑 Tag kept in the path when a digest is present Fixed in 9ca78cc usage.collector.image now returns OK
💡 llm-d.ai rule for the EPP Role Fixed in c70fe68 Rule in the Role and both ClusterRoles, test fails without it
💡 Keep the v0.9.0 pin Followed Pin unchanged

Key: 🛑 Blocker (must fix before merge) · 💡 Suggestion (improvement, not blocking) · ❓ Question (needs author input) · ❤️ Kudos

if tag, ok := image["tag"]; ok && isBlank(tag) {
return "empty tag"
}
if isBlank(image["repository"]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Suggestion: skipReason skips any mapping without a usable repository, so a mistyped key passes CI as a SKIP line.
Drop the no repository branch and the sidecar.image case in TestParseDefaultImagesSkipsReleasePinnedMappings.
My last review asked for this branch, but it was never needed: both openmeter images have tag: "", so the empty tag branch already skips them. With it, repositry: x plus a real tag prints SKIP instead of failing.

go test ./internal/controller/reconcilers -run TestGatewayAdmission -v
```

### Upgrading from 0.1.4 or earlier: a mirrored Endpoint Picker image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❤️ Kudos: This section tells anyone running a mirror of the old image exactly which image to mirror before upgrading. It is accurate: v0.8.0 loads its config with the GIE v1.5.0 loader, which registers only inference.networking.x-k8s.io.

@dcmcand

dcmcand commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Thanks for this, @Keerthirajan58. Every new model on main was failing to pull its Endpoint Picker image, and this gets them running again. You turned the rebase and both parser fixes around quickly. You also ran the envtest check against the old ClusterRole to prove the RBAC rule was needed, which nobody asked for. The upgrade note will save anyone running a mirror of the v0.8.0 image from a confusing failure. Merging now.

@dcmcand
dcmcand merged commit 76e5680 into nebari-dev:main Nov 11, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Default EPP image ghcr.io/llm-d/llm-d-inference-scheduler:v0.8.0 no longer exists

2 participants