mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-20 12:36:26 +00:00
feat: scoped multi-namespace mode (Role per namespace, no ClusterRole)
Add a third RBAC posture between watch-globally (ClusterRole) and single namespace: give Reloader an explicit list of namespaces to watch. The chart creates a namespace-scoped Role + RoleBinding in each listed namespace (no ClusterRole), and one install covers them all. Go: - new --namespaces flag / options.Namespaces - resolveWatchNamespaces() picks list -> KUBERNETES_NAMESPACE -> all - controller creation loops over the watched namespaces - namespaces-to-ignore is now only honored in global mode (watchGlobally=true); in single-namespace and scoped modes the watched set is already explicit Helm: - new reloader.namespaces value (active when watchGlobally=false); accepts either a YAML list or a comma-separated string for consistency with the sibling namespace options - reloader-watchNamespaces helper (release ns always auto-included, deduped) - shared reloader-namespaced-rules template reused per namespace - role.yaml/rolebinding.yaml range over the list; deployment passes --namespaces - --namespaces-to-ignore only rendered when watchGlobally=true - fail guard for watchGlobally=true + namespaces set Tests: unit test for resolveWatchNamespaces; scoped-namespaces e2e case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2cbb7715de
commit
50153d05ea
@@ -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
|
||||
}
|
||||
|
||||
func startReloader(cmd *cobra.Command, args []string) {
|
||||
common.GetCommandLineOptions()
|
||||
err := configureLogging(options.LogFormat, options.LogLevel)
|
||||
@@ -110,12 +125,11 @@ 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 {
|
||||
logrus.Warnf("KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces.")
|
||||
} else if len(options.Namespaces) > 0 {
|
||||
logrus.Infof("Watching scoped namespaces: %s", strings.Join(watchNamespaces, ", "))
|
||||
}
|
||||
|
||||
// create the clientset
|
||||
@@ -129,14 +143,20 @@ func startReloader(cmd *cobra.Command, args []string) {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
ignoredNamespacesList := options.NamespacesToIgnore
|
||||
// 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
|
||||
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)
|
||||
@@ -159,31 +179,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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cmd
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user