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
Problem
The
Runnercreates the controller-runtime manager with acache.Optionsthat supports only coarse-grained namespace scoping via theNAMESPACEenv 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-injectionplugin registers a Secret reconciler (For(&corev1.Secret{})) that creates a cluster-scoped Secret informer. The informer doesLIST /api/v1/secretsat cluster scope, which requires a ClusterRoleBinding withsecretsaccess. 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 secretswithin a namespace, but they do not satisfy a cluster-scopedLIST /api/v1/secretsrequest. When cluster-wide secrets access is removed from the ClusterRole, every payload-processing pod fails at startup:Current Runner behavior
DefaultNamespacesscopes 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
WithCacheByObjectmethod toRunnerthat lets downstream consumers configurecache.Options.ByObjectfor specific resource types:Then in
Run():Downstream usage (ai-gateway-payload-processing)
This generates namespace-scoped
LIST /api/v1/namespaces/llm/secretsrequests that are satisfied by a RoleBinding in thellmnamespace — no cluster-wide secrets access needed.Context
cache.Options.ByObjectoverridesDefaultNamespacesfor specific types (docs)apikey-injectionplugin'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