diff --git a/deploy/helm/grafana-operator/README.md b/deploy/helm/grafana-operator/README.md index f4f044555..8883fe7f1 100644 --- a/deploy/helm/grafana-operator/README.md +++ b/deploy/helm/grafana-operator/README.md @@ -104,5 +104,6 @@ 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 | +| watchLabelSelectors | string | `""` | Sets the `WATCH_LABEL_SELECTORS` environment variable, if defines which CRs are watched according to thei labels. By default, the operator watches all CRs. To make it watch only a subset of CRs, define the variable as a *Set-based requirement* See also: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ | | watchNamespaceSelector | string | `""` | Sets the `WATCH_NAMESPACE_SELECTOR` environment variable, it defines which namespaces the operator should be listening for based on a namespace label (e.g. `"environment: dev"`). By default, the operator watches all namespaces. To make it watch only its own namespace, check out `namespaceScope` option instead. | | watchNamespaces | string | `""` | Sets the `WATCH_NAMESPACE` environment variable, it defines which namespaces the operator should be listening for (e.g. `"grafana, foo"`). By default, the operator watches all namespaces. To make it watch only its own namespace, check out `namespaceScope` option instead. | diff --git a/deploy/helm/grafana-operator/templates/deployment.yaml b/deploy/helm/grafana-operator/templates/deployment.yaml index 0ea27aeb5..459087a8f 100644 --- a/deploy/helm/grafana-operator/templates/deployment.yaml +++ b/deploy/helm/grafana-operator/templates/deployment.yaml @@ -51,6 +51,12 @@ spec: {{ else }} value: {{quote .Values.watchNamespaceSelector }} {{- end }} + - name: WATCH_LABEL_SELECTORS + {{- if and .Values.watchLabelSelectors (eq .Values.watchLabelSelectors "") }} + value: "" + {{ else }} + value: {{quote .Values.watchLabelSelectors }} + {{- 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 489fb9c6b..d0c704c1a 100644 --- a/deploy/helm/grafana-operator/values.yaml +++ b/deploy/helm/grafana-operator/values.yaml @@ -15,6 +15,13 @@ watchNamespaces: "" # By default, the operator watches all namespaces. To make it watch only its own namespace, check out `namespaceScope` option instead. watchNamespaceSelector: "" +# -- Sets the `WATCH_LABEL_SELECTORS` environment variable, +# it defines which CRs are watched according to their labels. +# By default, the operator watches all CRs. To make it watch only a subset of CRs, define the variable as a *stringified label selector*. +# See also: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +watchLabelSelectors: "" +# watchLabelSelectors: "partition in (customerA, customerB),environment!=qa" + # -- Determines if the target cluster is OpenShift. Additional rbac permissions for routes will be added on OpenShift isOpenShift: false diff --git a/main.go b/main.go index fde33afc6..5a62967b8 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ package main import ( "context" "flag" + "fmt" "log" "log/slog" "os" @@ -48,6 +49,7 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + "github.com/grafana/grafana-operator/v5/controllers/model" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -71,6 +73,10 @@ const ( // eg: "environment: dev" // If empty or undefined, the operator will run in cluster scope. watchNamespaceEnvSelector = "WATCH_NAMESPACE_SELECTOR" + // watchLabelSelectorsEnvVar is the constant for env variable WATCH_LABEL_SELECTORS which specifies the resources to watch according to their labels. + // eg: 'partition in (customerA, customerB),environment!=qa' + // If empty of undefined, the operator will watch all CRs. + watchLabelSelectorsEnvVar = "WATCH_LABEL_SELECTORS" ) var ( @@ -123,6 +129,7 @@ func main() { watchNamespace, _ := os.LookupEnv(watchNamespaceEnvVar) watchNamespaceSelector, _ := os.LookupEnv(watchNamespaceEnvSelector) + watchLabelSelectors, _ := os.LookupEnv(watchLabelSelectorsEnvVar) // Fetch k8s api credentials and detect platform restConfig := ctrl.GetConfigOrDie() @@ -151,22 +158,28 @@ func main() { PprofBindAddress: pprofAddr, } + labelSelectors, err := getLabelSelectors(watchLabelSelectors) + if err != nil { + setupLog.Error(err, fmt.Sprintf("unable to parse %s", watchLabelSelectorsEnvVar)) + os.Exit(1) //nolint + } switch { case strings.Contains(watchNamespace, ","): // multi namespace scoped - controllerOptions.Cache.DefaultNamespaces = getNamespaceConfig(watchNamespace) + controllerOptions.Cache.DefaultNamespaces = getNamespaceConfig(watchNamespace, labelSelectors) setupLog.Info("operator running in namespace scoped mode for multiple namespaces", "namespaces", watchNamespace) case watchNamespace != "": // namespace scoped - controllerOptions.Cache.DefaultNamespaces = getNamespaceConfig(watchNamespace) + controllerOptions.Cache.DefaultNamespaces = getNamespaceConfig(watchNamespace, labelSelectors) setupLog.Info("operator running in namespace scoped mode", "namespace", watchNamespace) case strings.Contains(watchNamespaceSelector, ":"): // namespace scoped - controllerOptions.Cache.DefaultNamespaces = getNamespaceConfigSelector(restConfig, watchNamespaceSelector) + controllerOptions.Cache.DefaultNamespaces = getNamespaceConfigSelector(restConfig, watchNamespaceSelector, labelSelectors) setupLog.Info("operator running in namespace scoped mode using namespace selector", "namespace", watchNamespace) case watchNamespace == "" && watchNamespaceSelector == "": // cluster scoped + controllerOptions.Cache.DefaultLabelSelector = labelSelectors setupLog.Info("operator running in cluster scoped mode") } @@ -266,7 +279,7 @@ func main() { setupLog.Info("SIGTERM request gotten, shutting down operator") } -func getNamespaceConfig(namespaces string) map[string]cache.Config { +func getNamespaceConfig(namespaces string, labelSelectors labels.Selector) map[string]cache.Config { defaultNamespaces := map[string]cache.Config{} for _, v := range strings.Split(namespaces, ",") { // Generate a mapping of namespaces to label/field selectors, set to Everything() to enable matching all @@ -274,7 +287,7 @@ func getNamespaceConfig(namespaces string) map[string]cache.Config { // 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] = cache.Config{ - LabelSelector: labels.Everything(), // Match any labels + LabelSelector: labelSelectors, FieldSelector: fields.Everything(), // Match any fields Transform: nil, UnsafeDisableDeepCopy: nil, @@ -283,7 +296,7 @@ func getNamespaceConfig(namespaces string) map[string]cache.Config { return defaultNamespaces } -func getNamespaceConfigSelector(restConfig *rest.Config, selector string) map[string]cache.Config { +func getNamespaceConfigSelector(restConfig *rest.Config, selector string, labelSelectors labels.Selector) map[string]cache.Config { cl, err := client.New(restConfig, client.Options{}) if err != nil { setupLog.Error(err, "Failed to get watch namespaces") @@ -305,7 +318,7 @@ func getNamespaceConfigSelector(restConfig *rest.Config, selector string) map[st // 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 + LabelSelector: labelSelectors, FieldSelector: fields.Everything(), // Match any fields Transform: nil, UnsafeDisableDeepCopy: nil, @@ -313,3 +326,21 @@ func getNamespaceConfigSelector(restConfig *rest.Config, selector string) map[st } return defaultNamespaces } + +func getLabelSelectors(watchLabelSelectors string) (labels.Selector, error) { + var ( + labelSelectors labels.Selector + err error + ) + if watchLabelSelectors != "" { + labelSelectors, err = labels.Parse(watchLabelSelectors) + if err != nil { + return labelSelectors, fmt.Errorf("unable to parse %s: %w", watchLabelSelectorsEnvVar, err) + } + } else { + labelSelectors = labels.Everything() // Match any labels + } + managedByLabelSelector, _ := labels.SelectorFromSet(model.CommonLabels).Requirements() + labelSelectors.Add(managedByLabelSelector...) + return labelSelectors, nil +}