Skip to content

Runner: support per-object cache namespace scoping (ByObject) #300

Description

@liangwen12year

Problem

The Runner creates the controller-runtime manager with a cache.Options that supports only coarse-grained namespace scoping via the NAMESPACE env var (DefaultNamespaces). This scopes all informers to one namespace. There is no way for downstream consumers to scope individual resource types to specific namespaces.

This matters for RBAC hardening. The apikey-injection plugin registers a Secret reconciler (For(&corev1.Secret{})) that creates a cluster-scoped Secret informer. The informer does LIST /api/v1/secrets at cluster scope, which requires a ClusterRoleBinding with secrets access. We want to restrict secrets access to only the namespaces where model credentials exist (principle of least privilege), using per-namespace RoleBindings instead of a cluster-wide ClusterRoleBinding.

Per-namespace RoleBindings grant list secrets within a namespace, but they do not satisfy a cluster-scoped LIST /api/v1/secrets request. When cluster-wide secrets access is removed from the ClusterRole, every payload-processing pod fails at startup:

secrets is forbidden: User "system:serviceaccount:openshift-ingress:payload-processing-..."
cannot list resource "secrets" in API group "" at the cluster scope

Current Runner behavior

// runner.go L171-180
cacheOptions := cache.Options{}
namespace := os.Getenv("NAMESPACE")
if namespace != "" {
    cacheOptions.DefaultNamespaces = map[string]cache.Config{
        namespace: {},
    }
}
mgr, err := ctrl.NewManager(cfg, ctrl.Options{Cache: cacheOptions, Metrics: metricsServerOptions})

DefaultNamespaces scopes all informers (Secrets, ExternalModels, ExternalProviders, Services, HTTPRoutes, etc.) to one namespace. This is too coarse — we need Secrets scoped to model namespaces while keeping other resources cluster-wide (or multi-namespace).

Proposed solution

Add a WithCacheByObject method to Runner that lets downstream consumers configure cache.Options.ByObject for specific resource types:

func (r *Runner) WithCacheByObject(byObject map[client.Object]cache.ByObject) *Runner {
    r.cacheByObject = byObject
    return r
}

Then in Run():

cacheOptions := cache.Options{}
if r.cacheByObject != nil {
    cacheOptions.ByObject = r.cacheByObject
}
// existing NAMESPACE logic stays as fallback for DefaultNamespaces

Downstream usage (ai-gateway-payload-processing)

runner.NewRunner().
    WithCacheByObject(map[client.Object]cache.ByObject{
        &corev1.Secret{}: {
            Namespaces: map[string]cache.Config{
                "llm": {},  // only watch secrets in model namespace
            },
        },
    }).
    WithCustomControllers(...).
    Run(ctx)

This generates namespace-scoped LIST /api/v1/namespaces/llm/secrets requests that are satisfied by a RoleBinding in the llm namespace — no cluster-wide secrets access needed.

Context

  • controller-runtime version: v0.23.3
  • cache.Options.ByObject overrides DefaultNamespaces for specific types (docs)
  • The apikey-injection plugin's Secret reconciler already filters events via a label predicate (inference.networking.k8s.io/bbr-managed), but the underlying informer still needs list/watch permission at whatever scope it's configured for
  • Upstream consumer: opendatahub-io/ai-gateway-payload-processing
  • Tracking issue: RHOAIENG-56795

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions