From 81aafcac80cb4326f556db4da2eaa7f0334ac8e7 Mon Sep 17 00:00:00 2001 From: Cosmin Gavagiuc <75359737+atos-cosmin-gavagiuc@users.noreply.github.com> Date: Thu, 14 Mar 2024 18:15:34 +0200 Subject: [PATCH] feat: add namespace selector using env variable (#1434) --- deploy/helm/grafana-operator/README.md | 1 + .../templates/deployment.yaml | 6 +++ deploy/helm/grafana-operator/values.yaml | 5 +++ docs/docs/grafana.md | 4 ++ main.go | 42 ++++++++++++++++++- 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/deploy/helm/grafana-operator/README.md b/deploy/helm/grafana-operator/README.md index d0f758da9..b1a47f73d 100644 --- a/deploy/helm/grafana-operator/README.md +++ b/deploy/helm/grafana-operator/README.md @@ -69,4 +69,5 @@ It's easier to just manage this configuration outside of the operator. | serviceMonitor.targetLabels | list | `[]` | Set of labels to transfer from the Kubernetes Service onto the target | | serviceMonitor.telemetryPath | string | `"/metrics"` | Set path to metrics path | | tolerations | list | `[]` | pod tolerations | +| watchNamespaceSelector | string | `""` | Sets the WATCH_NAMESPACE_SELECTOR environment variable, it defines which namespaces the operator should be listening for based on label and key value pair added on namespace kind. By default it's all namespaces. | | watchNamespaces | string | `""` | Sets the WATCH_NAMESPACE environment variable, it defines which namespaces the operator should be listening for. By default it's all namespaces, if you only want to listen for the same namespace as the operator is deployed to look at namespaceScope. | diff --git a/deploy/helm/grafana-operator/templates/deployment.yaml b/deploy/helm/grafana-operator/templates/deployment.yaml index 3e817e2ca..1cdd10724 100644 --- a/deploy/helm/grafana-operator/templates/deployment.yaml +++ b/deploy/helm/grafana-operator/templates/deployment.yaml @@ -49,6 +49,12 @@ spec: {{ else }} value: {{ .Values.watchNamespaces }} {{- end }} + - name: WATCH_NAMESPACE_SELECTOR + {{- if and .Values.namespaceScope (eq .Values.watchNamespaceSelector "") }} + value: "" + {{ else }} + value: {{quote .Values.watchNamespaceSelector }} + {{- end }} {{- with .Values.env }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/deploy/helm/grafana-operator/values.yaml b/deploy/helm/grafana-operator/values.yaml index 6bdd428b5..3462e712b 100644 --- a/deploy/helm/grafana-operator/values.yaml +++ b/deploy/helm/grafana-operator/values.yaml @@ -10,6 +10,11 @@ leaderElect: false # By default it's all namespaces, if you only want to listen for the same namespace as the operator is deployed to look at namespaceScope. watchNamespaces: "" +# -- Sets the WATCH_NAMESPACE_SELECTOR environment variable, +# it defines which namespaces the operator should be listening for based on label and key value pair added on namespace kind. +# By default it's all namespaces. +watchNamespaceSelector: "" + # -- Determines if the target cluster is OpenShift. Additional rbac permissions for routes will be added on OpenShift isOpenShift: false diff --git a/docs/docs/grafana.md b/docs/docs/grafana.md index 18789c7d7..18357dfa9 100644 --- a/docs/docs/grafana.md +++ b/docs/docs/grafana.md @@ -25,6 +25,10 @@ To support that, we offer 3 operational modes (you can switch between those thro - Cluster-wide permissions are still required; - single namespace (`WATCH_NAMESPACE: "grafana"`): - With this mode, it's possible to use `Role` + `RoleBinding` to grant the operator the required access. +- mutliple namespaces using label selector (`WATCH_NAMESPACE_SELECTOR: "environment: dev"`): + - With this mode, it is possible detect and load all namespaces that match the label selector automatically. + New namespaces won't be automatically included until the Grafana operator is restarted. + - Cluster-wide permissions are still required; ## External Grafana instances diff --git a/main.go b/main.go index 4f23f02a3..509e3e122 100644 --- a/main.go +++ b/main.go @@ -24,11 +24,14 @@ import ( "strings" "syscall" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/webhook" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/cache" routev1 "github.com/openshift/api/route/v1" @@ -55,6 +58,10 @@ const ( // watchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE which specifies the Namespace to watch. // If empty or undefined, the operator will run in cluster scope. watchNamespaceEnvVar = "WATCH_NAMESPACE" + // watchNamespaceEnvSelector is the constant for env variable WATCH_NAMESPACE_SELECTOR which specifies the Namespace label and key to watch. + // eg: "environment: dev" + // If empty or undefined, the operator will run in cluster scope. + watchNamespaceEnvSelector = "WATCH_NAMESPACE_SELECTOR" ) var ( @@ -89,6 +96,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) watchNamespace, _ := os.LookupEnv(watchNamespaceEnvVar) + watchNamespaceSelector, _ := os.LookupEnv(watchNamespaceEnvSelector) controllerOptions := ctrl.Options{ Scheme: scheme, @@ -119,6 +127,34 @@ func main() { } return defaultNamespaces } + getNamespaceConfigSelector := func(selector string) map[string]cache.Config { + cl, err := client.New(config.GetConfigOrDie(), client.Options{}) + if err != nil { + setupLog.Error(err, "Failed to get watch namespaces") + } + nsList := &corev1.NamespaceList{} + listOpts := []client.ListOption{ + client.MatchingLabels(map[string]string{strings.Split(selector, ":")[0]: strings.Split(selector, ":")[1]}), + } + err = cl.List(context.Background(), nsList, listOpts...) + if err != nil { + setupLog.Error(err, "Failed to get watch namespaces") + } + defaultNamespaces := map[string]cache.Config{} + for _, v := range nsList.Items { + // Generate a mapping of namespaces to label/field selectors, set to Everything() to enable matching all + // instances in all namespaces from watchNamespace to be controlled by the operator + // this is the default behavior of the operator on v5, if you require finer grained control over this + // please file an issue in the grafana-operator/grafana-operator GH project + defaultNamespaces[v.Name] = cache.Config{ + LabelSelector: labels.Everything(), // Match any labels + FieldSelector: fields.Everything(), // Match any fields + Transform: nil, + UnsafeDisableDeepCopy: nil, + } + } + return defaultNamespaces + } switch { case strings.Contains(watchNamespace, ","): // multi namespace scoped @@ -128,8 +164,12 @@ func main() { // namespace scoped controllerOptions.Cache.DefaultNamespaces = getNamespaceConfig(watchNamespace) setupLog.Info("operator running in namespace scoped mode", "namespace", watchNamespace) + case strings.Contains(watchNamespaceSelector, ":"): + // namespace scoped + controllerOptions.Cache.DefaultNamespaces = getNamespaceConfigSelector(watchNamespaceSelector) + setupLog.Info("operator running in namespace scoped mode using namespace selector", "namespace", watchNamespace) - case watchNamespace == "": + case watchNamespace == "" && watchNamespaceSelector == "": // cluster scoped setupLog.Info("operator running in cluster scoped mode") }