Repository: operator-framework/operator-sdk (go)

Issue:
Helm Operator for A/B deployment 
## Feature Request

#### Describe the problem you need a feature to resolve.

I’m using the Helm Operator and I want to use it for an A/B deployment (or blue green/deployment) in the same namespace (it must be in the same namespace).

Obviously I can’t deploy two Helm Operators with the same watches.yaml but different Helm chart versions (they will step on each other).

I also don’t want to bump the CRD version for each new Helm chart release.

#### Describe the solution you'd like.

Each Helm Operator could watch the CR with a specific label defined in their watches.yaml e.g: release: A, release B.

Sorry, I think this feature was requested before but I can’t find the answer.

Thanks

JF


Pull request:
Closes #4949: Label-based filtering for Helm via predicates
**Description of the change:**
Added a predicate to filter events based on labels.

**Motivation for the change:**
Allow Helm operators to only watch CRs with specific labels defined in `watches.yaml`, as mentioned in #4949.


Patch:
diff --git a/changelog/fragments/helm-labelselector.yaml b/changelog/fragments/helm-labelselector.yaml
new file mode 100644
index 00000000..1e2d1049
--- /dev/null
+++ b/changelog/fragments/helm-labelselector.yaml
@@ -0,0 +1,7 @@
+entries:
+  - description: >
+      Added a predicate for helm operators that filters resources based on selectors specified in watches.yaml.
+      Only the resources that contain the labels specified by selectors will be reconciled.
+
+    kind: addition
+    breaking: false
diff --git a/internal/cmd/helm-operator/run/cmd.go b/internal/cmd/helm-operator/run/cmd.go
index ef367713..e909d27c 100644
--- a/internal/cmd/helm-operator/run/cmd.go
+++ b/internal/cmd/helm-operator/run/cmd.go
@@ -202,6 +202,7 @@ func run(cmd *cobra.Command, f *flags.Flags) {
 			WatchDependentResources: *w.WatchDependentResources,
 			OverrideValues:          w.OverrideValues,
 			MaxConcurrentReconciles: f.MaxConcurrentReconciles,
+			Selector:                w.Selector,
 		})
 		if err != nil {
 			log.Error(err, "Failed to add manager factory to controller.")
diff --git a/internal/helm/controller/controller.go b/internal/helm/controller/controller.go
index 3870fa78..7b4e44ba 100644
--- a/internal/helm/controller/controller.go
+++ b/internal/helm/controller/controller.go
@@ -16,6 +16,7 @@ package controller
 
 import (
 	"fmt"
+	"reflect"
 	"strings"
 	"sync"
 	"time"
@@ -30,6 +31,7 @@ import (
 	crthandler "sigs.k8s.io/controller-runtime/pkg/handler"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
+	ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 	"sigs.k8s.io/yaml"
 
@@ -51,6 +53,7 @@ type WatchOptions struct {
 	WatchDependentResources bool
 	OverrideValues          map[string]string
 	MaxConcurrentReconciles int
+	Selector                metav1.LabelSelector
 }
 
 // Add creates a new helm operator controller and adds it to the manager
@@ -80,7 +83,19 @@ func Add(mgr manager.Manager, options WatchOptions) error {
 
 	o := &unstructured.Unstructured{}
 	o.SetGroupVersionKind(options.GVK)
-	if err := c.Watch(&source.Kind{Type: o}, &libhandler.InstrumentedEnqueueRequestForObject{}); err != nil {
+
+	var preds []ctrlpredicate.Predicate
+	p, err := parsePredicateSelector(options.Selector)
+
+	if err != nil {
+		return err
+	}
+
+	if p != nil {
+		preds = append(preds, p)
+	}
+
+	if err := c.Watch(&source.Kind{Type: o}, &libhandler.InstrumentedEnqueueRequestForObject{}, preds...); err != nil {
 		return err
 	}
 
@@ -93,6 +108,20 @@ func Add(mgr manager.Manager, options WatchOptions) error {
 	return nil
 }
 
+// parsePredicateSelector parses the selector in the WatchOptions and creates a predicate
+// that is used to filter resources based on the specified selector
+func parsePredicateSelector(selector metav1.LabelSelector) (ctrlpredicate.Predicate, error) {
+	// If a selector has been specified in watches.yaml, add it to the watch's predicates.
+	if !reflect.ValueOf(selector).IsZero() {
+		p, err := ctrlpredicate.LabelSelectorPredicate(selector)
+		if err != nil {
+			return nil, fmt.Errorf("error constructing predicate from watches selector: %v", err)
+		}
+		return p, nil
+	}
+	return nil, nil
+}
+
 // watchDependentResources adds a release hook function to the HelmOperatorReconciler
 // that adds watches for resources in released Helm charts.
 func watchDependentResources(mgr manager.Manager, r *HelmOperatorReconciler, c controller.Controller) {
diff --git a/internal/helm/watches/watches.go b/internal/helm/watches/watches.go
index c02413ab..8aa0e650 100644
--- a/internal/helm/watches/watches.go
+++ b/internal/helm/watches/watches.go
@@ -22,6 +22,7 @@ import (
 	"os"
 
 	"helm.sh/helm/v3/pkg/chartutil"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/yaml"
 )
@@ -32,9 +33,10 @@ const WatchesFile = "watches.yaml"
 // custom resource.
 type Watch struct {
 	schema.GroupVersionKind `json:",inline"`
-	ChartDir                string            `json:"chart"`
-	WatchDependentResources *bool             `json:"watchDependentResources,omitempty"`
-	OverrideValues          map[string]string `json:"overrideValues,omitempty"`
+	ChartDir                string               `json:"chart"`
+	WatchDependentResources *bool                `json:"watchDependentResources,omitempty"`
+	OverrideValues          map[string]string    `json:"overrideValues,omitempty"`
+	Selector                metav1.LabelSelector `json:"selector"`
 }
 
 // UnmarshalYAML unmarshals an individual watch from the Helm watches.yaml file
diff --git a/website/content/en/docs/building-operators/helm/reference/watches.md b/website/content/en/docs/building-operators/helm/reference/watches.md
index de423dfd..f8447b68 100644
--- a/website/content/en/docs/building-operators/helm/reference/watches.md
+++ b/website/content/en/docs/building-operators/helm/reference/watches.md
@@ -19,6 +19,7 @@ The follow tables describes the fields in an entry in `watches.yaml`:
 | chart                   | The path to the helm chart to use when reconciling this GVK.  |
 | watchDependentResources | Enable watching resources that are created by helm (default: `true`). |
 | overrideValues          | Values to be used for overriding Helm chart's defaults. For additional information see the [reference doc][override-values]. |
+| selector                | The conditions that a resource's labels must satsify in order to get reconciled. For additional information see [labels and selectors documentation][label-selector-doc]. |
 
 
 For reference, here is an example of a simple `watches.yaml` file:
@@ -31,7 +32,11 @@ For reference, here is an example of a simple `watches.yaml` file:
   chart: helm-charts/foo
   overrideValues:
     image.repository: quay.io/mycustomrepo
-  watchDependentResources: false   
+  watchDependentResources: false
+  selector:
+    matchExpressions:
+     - {key: testLabel, operator: Exists, values: []}
 ```
 
 [override-values]: /docs/building-operators/helm/reference/advanced_features/override_values/
+[label-selector-doc]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/


Test patch:
diff --git a/internal/helm/controller/controller_test.go b/internal/helm/controller/controller_test.go
new file mode 100644
index 00000000..968fdef1
--- /dev/null
+++ b/internal/helm/controller/controller_test.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Operator-SDK Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package controller
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestFilterPredicate(t *testing.T) {
+	matchLabelPass := make(map[string]string)
+	matchLabelPass["testKey"] = "testValue"
+	selectorPass := metav1.LabelSelector{
+		MatchLabels: matchLabelPass,
+	}
+	noSelector := metav1.LabelSelector{}
+
+	passPredicate, err := parsePredicateSelector(selectorPass)
+	assert.Equal(t, nil, err, "Verify that no error is thrown on a valid populated selector")
+	assert.NotEqual(t, nil, passPredicate, "Verify that a predicate is constructed using a valid selector")
+
+	nilPredicate, err := parsePredicateSelector(noSelector)
+	assert.Equal(t, nil, err, "Verify that no error is thrown on a valid unpopulated selector")
+	assert.Equal(t, nil, nilPredicate, "Verify correct parsing of an unpopulated selector")
+}