diff --git a/README.md b/README.md index b2d9ab1f..05b2d81a 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ spec: - you may override the configmap annotation with the `--configmap-annotation` flag - you may override the secret annotation with the `--secret-annotation` flag - you may want to prevent watching certain namespaces with the `--namespaces-to-ignore` flag +- you may want to watch only a set of namespaces with certain labels by using the `--namespace-selector` flag - you may want to prevent watching certain resources with the `--resources-to-ignore` flag - you can configure logging in JSON format with the `--log-format=json` option - you can configure the "reload strategy" with the `--reload-strategy=` option (details below) @@ -182,6 +183,25 @@ Reloader can be configured to ignore the resources `secrets` and `configmaps` by `Note`: At one time only one of these resource can be ignored, trying to do it will cause error in Reloader. Workaround for ignoring both resources is by scaling down the reloader pods to `0`. +Reloader can be configured to watch only namespaces labeled with (one or more) labels of your choosing by using the `--namespace-selector` parameter, for example: +``` +--namespace-selector=reloder:enabled,test:true +``` + +Only namespaces labeled like the following namespace YAML will be watched: +```yaml +kind: Namespace +apiVersion: v1 +metadata: + ... + labels: + reloder: enabled + test: true + ... +``` +If you want to select namespace only by the key of the label use ```*``` as the value. +For example, for ```--namespace-selector=select-this:*``` all namespaces with label-key "select-this" will be selected regardless of the labels value + ### Vanilla kustomize You can also apply the vanilla manifests by running the following command @@ -233,6 +253,12 @@ Reloader can be configured to ignore the resources `secrets` and `configmaps` by `Note`: At one time only one of these resource can be ignored, trying to do it will cause error in helm template compilation. +Reloader can be configured to watch only namespaces labeled with (one or more) labels of your choosing by using the `namespaceSelector` parameter + +| Parameter | Description | Type | +| ---------------- | -------------------------------------------------------------- | ------- | +| namespaceSelector | list of comma separated key:value namespace | string | + You can also set the log format of Reloader to json by setting `logFormat` to `json` in values.yaml and apply the chart You can enable to scrape Reloader's Prometheus metrics by setting `serviceMonitor.enabled` or `podMonitor.enabled` to `true` in values.yaml file. Service monitor will be removed in future releases of reloader in favour of Pod monitor. diff --git a/deployments/kubernetes/chart/reloader/templates/clusterrole.yaml b/deployments/kubernetes/chart/reloader/templates/clusterrole.yaml index 86b86daf..0ce01712 100644 --- a/deployments/kubernetes/chart/reloader/templates/clusterrole.yaml +++ b/deployments/kubernetes/chart/reloader/templates/clusterrole.yaml @@ -32,6 +32,14 @@ rules: - list - get - watch +{{- if .Values.reloader.namespaceSelector }} + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get +{{- end }} {{- if and (.Capabilities.APIVersions.Has "apps.openshift.io/v1") (.Values.reloader.isOpenshift) }} - apiGroups: - "apps.openshift.io" diff --git a/deployments/kubernetes/chart/reloader/templates/deployment.yaml b/deployments/kubernetes/chart/reloader/templates/deployment.yaml index f69cf551..3f99c8d9 100644 --- a/deployments/kubernetes/chart/reloader/templates/deployment.yaml +++ b/deployments/kubernetes/chart/reloader/templates/deployment.yaml @@ -157,7 +157,7 @@ spec: - mountPath: /tmp/ name: tmp-volume {{- end }} - {{- if or (.Values.reloader.logFormat) (.Values.reloader.ignoreSecrets) (.Values.reloader.ignoreNamespaces) (.Values.reloader.ignoreConfigMaps) (.Values.reloader.custom_annotations) (eq .Values.reloader.isArgoRollouts true) (eq .Values.reloader.reloadOnCreate true) (ne .Values.reloader.reloadStrategy "default") (.Values.reloader.enableHA)}} + {{- if or (.Values.reloader.logFormat) (.Values.reloader.ignoreSecrets) (.Values.reloader.ignoreNamespaces) (.Values.reloader.namespaceSelector) (.Values.reloader.ignoreConfigMaps) (.Values.reloader.custom_annotations) (eq .Values.reloader.isArgoRollouts true) (eq .Values.reloader.reloadOnCreate true) (ne .Values.reloader.reloadStrategy "default") (.Values.reloader.enableHA)}} args: {{- if .Values.reloader.logFormat }} - "--log-format={{ .Values.reloader.logFormat }}" @@ -171,7 +171,9 @@ spec: {{- if .Values.reloader.ignoreNamespaces }} - "--namespaces-to-ignore={{ .Values.reloader.ignoreNamespaces }}" {{- end }} - + {{- if .Values.reloader.namespaceSelector }} + - "--namespace-selector={{ .Values.reloader.namespaceSelector }}" + {{- end }} {{- if .Values.reloader.custom_annotations }} {{- if .Values.reloader.custom_annotations.configmap }} - "--configmap-annotation" diff --git a/deployments/kubernetes/chart/reloader/values.yaml b/deployments/kubernetes/chart/reloader/values.yaml index abc0a779..2005e5b0 100644 --- a/deployments/kubernetes/chart/reloader/values.yaml +++ b/deployments/kubernetes/chart/reloader/values.yaml @@ -16,6 +16,7 @@ reloader: reloadOnCreate: false reloadStrategy: default # Set to default, env-vars or annotations ignoreNamespaces: "" # Comma separated list of namespaces to ignore + namespaceSelector: "" # Comma separated list of 'key:value' labels for namespaces selection logFormat: "" #json watchGlobally: true # Set to true to enable leadership election allowing you to run multiple replicas diff --git a/internal/pkg/cmd/reloader.go b/internal/pkg/cmd/reloader.go index f81fa3a9..26c115bf 100644 --- a/internal/pkg/cmd/reloader.go +++ b/internal/pkg/cmd/reloader.go @@ -38,6 +38,7 @@ func NewReloaderCommand() *cobra.Command { cmd.PersistentFlags().StringVar(&options.LogFormat, "log-format", "", "Log format to use (empty string for text, or JSON") cmd.PersistentFlags().StringSlice("resources-to-ignore", []string{}, "list of resources to ignore (valid options 'configMaps' or 'secrets')") cmd.PersistentFlags().StringSlice("namespaces-to-ignore", []string{}, "list of namespaces to ignore") + cmd.PersistentFlags().StringSlice("namespace-selector", []string{}, "list of key:vaule namespace labels to include") cmd.PersistentFlags().StringVar(&options.IsArgoRollouts, "is-Argo-Rollouts", "false", "Add support for argo rollouts") cmd.PersistentFlags().StringVar(&options.ReloadStrategy, constants.ReloadStrategyFlag, constants.EnvVarsReloadStrategy, "Specifies the desired reload strategy") cmd.PersistentFlags().StringVar(&options.ReloadOnCreate, "reload-on-create", "false", "Add support to watch create events") @@ -132,6 +133,15 @@ func startReloader(cmd *cobra.Command, args []string) { logrus.Fatal(err) } + namespaceLabelSelector, err := getNamespaceLabelSelector(cmd) + if err != nil { + logrus.Fatal(err) + } + + if len(namespaceLabelSelector) > 0 { + logrus.Warnf("namespace-selector is set, will detect changes in namespaces with these labels: %s.", namespaceLabelSelector) + } + collectors := metrics.SetupPrometheusEndpoint() var controllers []*controller.Controller @@ -140,7 +150,7 @@ func startReloader(cmd *cobra.Command, args []string) { continue } - c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, collectors) + c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, namespaceLabelSelector, collectors) if err != nil { logrus.Fatalf("%s", err) } @@ -174,6 +184,21 @@ func getIgnoredNamespacesList(cmd *cobra.Command) (util.List, error) { return getStringSliceFromFlags(cmd, "namespaces-to-ignore") } +func getNamespaceLabelSelector(cmd *cobra.Command) (util.Map, error) { + slice, err := getStringSliceFromFlags(cmd, "namespace-selector") + if err != nil { + logrus.Fatal(err) + } + + var namespaceSelectorMap util.Map = make(util.Map) + for _, kv := range slice { + split := strings.Split(kv, ":") + namespaceSelectorMap[split[0]] = split[1] + } + + return namespaceSelectorMap, nil +} + func getStringSliceFromFlags(cmd *cobra.Command, flag string) ([]string, error) { slice, err := cmd.Flags().GetStringSlice(flag) if err != nil { diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go index 4efab9f7..49c3350f 100644 --- a/internal/pkg/controller/controller.go +++ b/internal/pkg/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "context" "fmt" "time" @@ -11,6 +12,7 @@ import ( "github.com/stakater/Reloader/internal/pkg/util" "github.com/stakater/Reloader/pkg/kube" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -32,6 +34,7 @@ type Controller struct { ignoredNamespaces util.List collectors metrics.Collectors recorder record.EventRecorder + namespaceSelector map[string]string } // controllerInitialized flag determines whether controlled is being initialized @@ -39,12 +42,13 @@ var controllerInitialized bool = false // NewController for initializing a Controller func NewController( - client kubernetes.Interface, resource string, namespace string, ignoredNamespaces []string, collectors metrics.Collectors) (*Controller, error) { + client kubernetes.Interface, resource string, namespace string, ignoredNamespaces []string, namespaceLabelSelector map[string]string, collectors metrics.Collectors) (*Controller, error) { c := Controller{ client: client, namespace: namespace, ignoredNamespaces: ignoredNamespaces, + namespaceSelector: namespaceLabelSelector, } eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{ @@ -73,7 +77,7 @@ func NewController( // Add function to add a new object to the queue in case of creating a resource func (c *Controller) Add(obj interface{}) { if options.ReloadOnCreate == "true" { - if !c.resourceInIgnoredNamespace(obj) && controllerInitialized { + if !c.resourceInIgnoredNamespace(obj) && c.resourceInNamespaceSelector(obj) && controllerInitialized { c.queue.Add(handler.ResourceCreatedHandler{ Resource: obj, Collectors: c.collectors, @@ -93,9 +97,45 @@ func (c *Controller) resourceInIgnoredNamespace(raw interface{}) bool { return false } +func (c *Controller) resourceInNamespaceSelector(raw interface{}) bool { + if len(c.namespaceSelector) == 0 { + return true + } + + switch object := raw.(type) { + case *v1.ConfigMap: + return c.matchLabels(object.ObjectMeta.Namespace) + case *v1.Secret: + return c.matchLabels(object.ObjectMeta.Namespace) + } + return true +} + +func (c *Controller) matchLabels(resourceNamespace string) bool { + namespace, err := c.client.CoreV1().Namespaces().Get(context.Background(), resourceNamespace, metav1.GetOptions{}) + if err != nil { + logrus.Warn(err) + return false + } + + for selectorKey, selectorVal := range c.namespaceSelector { + + namespaceLabelVal, namespaceLabelKeyExists := namespace.ObjectMeta.Labels[selectorKey] + + if namespaceLabelKeyExists && selectorVal == "*" { + continue + } + + if !namespaceLabelKeyExists || selectorVal != namespaceLabelVal { + return false + } + } + return true +} + // Update function to add an old object and a new object to the queue in case of updating a resource func (c *Controller) Update(old interface{}, new interface{}) { - if !c.resourceInIgnoredNamespace(new) { + if !c.resourceInIgnoredNamespace(new) && c.resourceInNamespaceSelector(new) { c.queue.Add(handler.ResourceUpdatedHandler{ Resource: new, OldResource: old, diff --git a/internal/pkg/controller/controller_test.go b/internal/pkg/controller/controller_test.go index c266ceff..6b9179bc 100644 --- a/internal/pkg/controller/controller_test.go +++ b/internal/pkg/controller/controller_test.go @@ -1,11 +1,13 @@ package controller import ( - "github.com/stakater/Reloader/internal/pkg/constants" + "context" "os" "testing" "time" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/sirupsen/logrus" @@ -14,7 +16,10 @@ import ( "github.com/stakater/Reloader/internal/pkg/testutil" "github.com/stakater/Reloader/internal/pkg/util" "github.com/stakater/Reloader/pkg/kube" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" ) @@ -40,7 +45,7 @@ func TestMain(m *testing.M) { logrus.Infof("Creating controller") for k := range kube.ResourceMap { - c, err := NewController(clients.KubernetesClient, k, namespace, []string{}, collectors) + c, err := NewController(clients.KubernetesClient, k, namespace, []string{}, map[string]string{}, collectors) if err != nil { logrus.Fatalf("%s", err) } @@ -2279,3 +2284,143 @@ func TestController_resourceInIgnoredNamespace(t *testing.T) { }) } } + +func TestController_resourceInNamespaceSelector(t *testing.T) { + type fields struct { + indexer cache.Indexer + queue workqueue.RateLimitingInterface + informer cache.Controller + namespace v1.Namespace + namespaceSelector util.Map + } + type args struct { + raw interface{} + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "TestConfigMapResourceInNamespaceSelector", + fields: fields{ + namespaceSelector: util.Map{ + "select": "this", + "select2": "this2", + }, + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "selected-namespace", + Labels: map[string]string{ + "select": "this", + "select2": "this2", + }, + }, + }, + }, + args: args{ + raw: testutil.GetConfigmap("selected-namespace", "testcm", "test"), + }, + want: true, + }, { + name: "TestConfigMapResourceNotInNamespaceSelector", + fields: fields{ + namespaceSelector: util.Map{ + "select": "this", + "select2": "this2", + }, + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-selected-namespace", + Labels: map[string]string{}, + }, + }, + }, + args: args{ + raw: testutil.GetConfigmap("not-selected-namespace", "testcm", "test"), + }, + want: false, + }, + { + name: "TestSecretResourceInNamespaceSelector", + fields: fields{ + namespaceSelector: util.Map{ + "select": "this", + "select2": "this2", + }, + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "selected-namespace", + Labels: map[string]string{ + "select": "this", + "select2": "this2", + }, + }, + }, + }, + args: args{ + raw: testutil.GetSecret("selected-namespace", "testsecret", "test"), + }, + want: true, + }, { + name: "TestSecretResourceNotInNamespaceSelector", + fields: fields{ + namespaceSelector: util.Map{ + "select": "this", + "select2": "this2", + }, + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-selected-namespace", + Labels: map[string]string{}, + }, + }, + }, + args: args{ + raw: testutil.GetSecret("not-selected-namespace", "secret", "test"), + }, + want: false, + }, { + name: "TestSecretResourceInNamespaceSelectorWiledcardValue", + fields: fields{ + namespaceSelector: util.Map{ + "select": "*", + }, + namespace: v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "selected-namespace", + Labels: map[string]string{ + "select": "this", + }, + }, + }, + }, + args: args{ + raw: testutil.GetSecret("selected-namespace", "secret", "test"), + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := fake.NewSimpleClientset() + namespace, _ := fakeClient.CoreV1().Namespaces().Create(context.Background(), &tt.fields.namespace, metav1.CreateOptions{}) + logrus.Infof("created fakeClient namesapce for testing = %s", namespace.Name) + + c := &Controller{ + client: fakeClient, + indexer: tt.fields.indexer, + queue: tt.fields.queue, + informer: tt.fields.informer, + namespace: tt.fields.namespace.ObjectMeta.Name, + namespaceSelector: tt.fields.namespaceSelector, + } + + if got := c.resourceInNamespaceSelector(tt.args.raw); got != tt.want { + t.Errorf("Controller.resourceInNamespaceSelector() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/leadership/leadership_test.go b/internal/pkg/leadership/leadership_test.go index 2d64d0cb..99f638c2 100644 --- a/internal/pkg/leadership/leadership_test.go +++ b/internal/pkg/leadership/leadership_test.go @@ -119,7 +119,7 @@ func TestRunLeaderElectionWithControllers(t *testing.T) { t.Logf("Creating controller") var controllers []*controller.Controller for k := range kube.ResourceMap { - c, err := controller.NewController(testutil.Clients.KubernetesClient, k, testutil.Namespace, []string{}, metrics.NewCollectors()) + c, err := controller.NewController(testutil.Clients.KubernetesClient, k, testutil.Namespace, []string{}, map[string]string{}, metrics.NewCollectors()) if err != nil { logrus.Fatalf("%s", err) } diff --git a/internal/pkg/util/util.go b/internal/pkg/util/util.go index 993084ab..1a2696d8 100644 --- a/internal/pkg/util/util.go +++ b/internal/pkg/util/util.go @@ -54,6 +54,8 @@ func GetSHAfromSecret(data map[string][]byte) string { type List []string +type Map map[string]string + func (l *List) Contains(s string) bool { for _, v := range *l { if v == s {