From f66b1fac5cceb8c738fac6a3a7d1890ec22cb3eb Mon Sep 17 00:00:00 2001 From: Wen Liang Date: Wed, 26 Aug 2026 10:19:49 -0400 Subject: [PATCH] feat(runner): add WithCacheByObject for per-object cache namespace scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a WithCacheByObject method to Runner that lets downstream consumers configure cache.Options.ByObject for specific resource types. This enables per-object namespace scoping — e.g. restricting the Secret informer to model namespaces — while keeping other informers cluster-wide. Without this, consumers that need namespace-scoped RBAC for secrets (RoleBindings instead of ClusterRoleBindings) cannot use the Runner because the default cache does a cluster-scoped LIST that requires cluster-wide access. Closes #300 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Wen Liang --- cmd/runner/runner.go | 29 ++++++++++++++++++++++ cmd/runner/runner_test.go | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 cmd/runner/runner_test.go diff --git a/cmd/runner/runner.go b/cmd/runner/runner.go index 782627f6..01d84977 100644 --- a/cmd/runner/runner.go +++ b/cmd/runner/runner.go @@ -94,6 +94,7 @@ type Runner struct { customCollectors []prometheus.Collector customControllers []func(client.Client, *ctrlbuilder.Builder) error + cacheByObject map[client.Object]cache.ByObject processor datasource.DatalayerProcessor } @@ -119,6 +120,31 @@ func (r *Runner) WithCustomControllers(setupFuncs ...func(client.Client, *ctrlbu return r } +// WithCacheByObject configures per-object cache namespace scoping on the +// controller-runtime manager. Entries override DefaultNamespaces (set via +// the NAMESPACE env var) for the specified object types, allowing some +// informers to be scoped to specific namespaces while others remain +// cluster-wide. +// +// This is useful for RBAC hardening: scope a Secret informer to model +// namespaces so the ServiceAccount only needs namespace-scoped +// RoleBindings instead of cluster-wide secrets access. +// +// Example: +// +// runner.NewRunner(). +// WithCacheByObject(map[client.Object]cache.ByObject{ +// &corev1.Secret{}: { +// Namespaces: map[string]cache.Config{ +// "model-namespace": {}, +// }, +// }, +// }) +func (r *Runner) WithCacheByObject(byObject map[client.Object]cache.ByObject) *Runner { + r.cacheByObject = byObject + return r +} + func (r *Runner) Run(ctx context.Context) error { // Setup a basic logger in case command-line argument parsing fails. logutil.InitSetupLogging() @@ -188,6 +214,9 @@ func (r *Runner) Run(ctx context.Context) error { namespace: {}, } } + if r.cacheByObject != nil { + cacheOptions.ByObject = r.cacheByObject + } mgr, err := ctrl.NewManager(cfg, ctrl.Options{Cache: cacheOptions, Metrics: metricsServerOptions}) if err != nil { diff --git a/cmd/runner/runner_test.go b/cmd/runner/runner_test.go new file mode 100644 index 00000000..9a0f66e4 --- /dev/null +++ b/cmd/runner/runner_test.go @@ -0,0 +1,52 @@ +/* +Copyright 2026 The llm-d Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package runner + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestWithCacheByObject(t *testing.T) { + byObject := map[client.Object]cache.ByObject{ + &corev1.Secret{}: { + Namespaces: map[string]cache.Config{ + "model-ns": {}, + }, + }, + } + + r := NewRunner().WithCacheByObject(byObject) + + if r.cacheByObject == nil { + t.Fatal("expected cacheByObject to be set") + } + if len(r.cacheByObject) != 1 { + t.Fatalf("expected 1 entry in cacheByObject, got %d", len(r.cacheByObject)) + } +} + +func TestWithCacheByObjectNil(t *testing.T) { + r := NewRunner() + + if r.cacheByObject != nil { + t.Fatal("expected cacheByObject to be nil by default") + } +}