Merge pull request #1162 from michal-marszalek-h2oai/951-namespace-scope-rbac

feat: scoped multi-namespace RBAC mode (Role per namespace, no ClusterRole)
This commit is contained in:
Muhammad Safwan Karim
2026-07-10 10:44:45 +05:00
committed by GitHub
25 changed files with 643 additions and 168 deletions
+48 -29
View File
@@ -102,6 +102,21 @@ func getHAEnvs() (string, string) {
return podName, podNamespace
}
// resolveWatchNamespaces determines the set of namespaces to watch and whether
// Reloader runs in global (all-namespaces) mode. Precedence:
// 1. an explicit --namespaces list (scoped mode) — watch exactly those namespaces;
// 2. the KUBERNETES_NAMESPACE env var (single-namespace mode);
// 3. otherwise watch all namespaces (global mode).
func resolveWatchNamespaces(namespaces []string, kubernetesNamespace string) ([]string, bool) {
if len(namespaces) > 0 {
return namespaces, false
}
if len(kubernetesNamespace) > 0 {
return []string{kubernetesNamespace}, false
}
return []string{v1.NamespaceAll}, true
}
// namespaceWatchScopeMessage returns the startup log message describing the
// namespace scope Reloader will watch when KUBERNETES_NAMESPACE is unset
// (global mode). It reflects --namespaces-to-ignore so the log is not
@@ -124,11 +139,9 @@ func startReloader(cmd *cobra.Command, args []string) {
}
logrus.Info("Starting Reloader")
isGlobal := false
currentNamespace := os.Getenv("KUBERNETES_NAMESPACE")
if len(currentNamespace) == 0 {
currentNamespace = v1.NamespaceAll
isGlobal = true
watchNamespaces, isGlobal := resolveWatchNamespaces(options.Namespaces, os.Getenv("KUBERNETES_NAMESPACE"))
if !isGlobal && len(options.Namespaces) > 0 {
logrus.Infof("Watching scoped namespaces: %s", strings.Join(watchNamespaces, ", "))
}
// create the clientset
@@ -142,17 +155,21 @@ func startReloader(cmd *cobra.Command, args []string) {
logrus.Fatal(err)
}
ignoredNamespacesList := options.NamespacesToIgnore
if isGlobal {
logrus.Warn(namespaceWatchScopeMessage(ignoredNamespacesList))
}
// namespaces-to-ignore and namespace-selector only make sense when watching all
// namespaces. In single-namespace and scoped modes the watched set is already
// explicit, so both are intentionally left empty.
ignoredNamespacesList := []string{}
namespaceLabelSelector := ""
if isGlobal {
ignoredNamespacesList = options.NamespacesToIgnore
logrus.Warn(namespaceWatchScopeMessage(ignoredNamespacesList))
namespaceLabelSelector, err = common.GetNamespaceLabelSelector(options.NamespaceSelectors)
if err != nil {
logrus.Fatal(err)
}
} else if len(options.NamespacesToIgnore) > 0 {
logrus.Warnf("namespaces-to-ignore is set but is only honored in global mode (watchGlobally=true); ignoring it.")
}
resourceLabelSelector, err := common.GetResourceLabelSelector(options.ResourceSelectors)
@@ -175,31 +192,33 @@ func startReloader(cmd *cobra.Command, args []string) {
collectors := metrics.SetupPrometheusEndpoint()
var controllers []*controller.Controller
for k := range kube.ResourceMap {
if k == constants.SecretProviderClassController && !shouldRunCSIController() {
continue
}
for _, currentNamespace := range watchNamespaces {
for k := range kube.ResourceMap {
if k == constants.SecretProviderClassController && !shouldRunCSIController() {
continue
}
if ignoredResourcesList.Contains(k) || (len(namespaceLabelSelector) == 0 && k == "namespaces") {
continue
}
if ignoredResourcesList.Contains(k) || (len(namespaceLabelSelector) == 0 && k == "namespaces") {
continue
}
c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, namespaceLabelSelector, resourceLabelSelector, collectors)
if err != nil {
logrus.Fatalf("%s", err)
}
c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, namespaceLabelSelector, resourceLabelSelector, collectors)
if err != nil {
logrus.Fatalf("%s", err)
}
controllers = append(controllers, c)
controllers = append(controllers, c)
// If HA is enabled we only run the controller when
if options.EnableHA {
continue
// If HA is enabled we only run the controller when
if options.EnableHA {
continue
}
// Now let's start the controller
stop := make(chan struct{})
defer close(stop)
logrus.Infof("Starting Controller to watch resource type: %s in namespace: %s", k, currentNamespace)
go c.Run(1, stop)
}
// Now let's start the controller
stop := make(chan struct{})
defer close(stop)
logrus.Infof("Starting Controller to watch resource type: %s", k)
go c.Run(1, stop)
}
// Run leadership election
+60 -1
View File
@@ -1,6 +1,65 @@
package cmd
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestResolveWatchNamespaces(t *testing.T) {
tests := []struct {
name string
namespaces []string
kubernetesNamespace string
wantNamespaces []string
wantGlobal bool
}{
{
name: "scoped mode takes precedence over env",
namespaces: []string{"team-a", "team-b"},
kubernetesNamespace: "reloader-system",
wantNamespaces: []string{"team-a", "team-b"},
wantGlobal: false,
},
{
name: "scoped mode with single namespace",
namespaces: []string{"team-a"},
kubernetesNamespace: "",
wantNamespaces: []string{"team-a"},
wantGlobal: false,
},
{
name: "single namespace mode from env",
namespaces: nil,
kubernetesNamespace: "reloader-system",
wantNamespaces: []string{"reloader-system"},
wantGlobal: false,
},
{
name: "global mode when nothing set",
namespaces: nil,
kubernetesNamespace: "",
wantNamespaces: []string{v1.NamespaceAll},
wantGlobal: true,
},
{
name: "empty list falls back to env",
namespaces: []string{},
kubernetesNamespace: "reloader-system",
wantNamespaces: []string{"reloader-system"},
wantGlobal: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNamespaces, gotGlobal := resolveWatchNamespaces(tt.namespaces, tt.kubernetesNamespace)
assert.Equal(t, tt.wantNamespaces, gotNamespaces)
assert.Equal(t, tt.wantGlobal, gotGlobal)
})
}
}
func TestNamespaceWatchScopeMessage(t *testing.T) {
tests := []struct {
+3
View File
@@ -76,6 +76,9 @@ var (
ResourcesToIgnore = []string{}
// WorkloadTypesToIgnore is a list of workload types to ignore when watching for changes
WorkloadTypesToIgnore = []string{}
// Namespaces is an explicit list of namespaces to watch (scoped mode). When non-empty,
// Reloader watches exactly these namespaces and requires no ClusterRole.
Namespaces = []string{}
// NamespacesToIgnore is a list of namespace names to ignore when watching for changes
NamespacesToIgnore = []string{}
// NamespaceSelectors is a list of namespace selectors to watch for changes
+1
View File
@@ -97,6 +97,7 @@ func ConfigureReloaderFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&options.WebhookUrl, "webhook-url", "", "webhook to trigger instead of performing a reload")
cmd.PersistentFlags().StringSliceVar(&options.ResourcesToIgnore, "resources-to-ignore", options.ResourcesToIgnore, "list of resources to ignore (valid options 'configmaps' or 'secrets')")
cmd.PersistentFlags().StringSliceVar(&options.WorkloadTypesToIgnore, "ignored-workload-types", options.WorkloadTypesToIgnore, "list of workload types to ignore (valid options: 'jobs', 'cronjobs', or both)")
cmd.PersistentFlags().StringSliceVar(&options.Namespaces, "namespaces", options.Namespaces, "explicit list of namespaces to watch (scoped mode; creates no ClusterRole)")
cmd.PersistentFlags().StringSliceVar(&options.NamespacesToIgnore, "namespaces-to-ignore", options.NamespacesToIgnore, "list of namespaces to ignore")
cmd.PersistentFlags().StringSliceVar(&options.NamespaceSelectors, "namespace-selector", options.NamespaceSelectors, "list of key:value labels to filter on for namespaces")
cmd.PersistentFlags().StringSliceVar(&options.ResourceSelectors, "resource-label-selector", options.ResourceSelectors, "list of key:value labels to filter on for configmaps and secrets")