From 595e5ae704d63a801029176c8fdedf0869c5257b Mon Sep 17 00:00:00 2001 From: Maxim Samoilov Date: Wed, 13 Dec 2023 15:58:18 +0400 Subject: [PATCH] Add support to limit applied policies in automation by specifying a selector Signed-off-by: Maxim Samoilov --- api/v1beta2/condition_types.go | 3 + api/v1beta2/imageupdateautomation_types.go | 5 ++ api/v1beta2/zz_generated.deepcopy.go | 5 ++ ...lkit.fluxcd.io_imageupdateautomations.yaml | 46 ++++++++++++++ docs/api/v1beta2/image-automation.md | 30 +++++++++ docs/spec/v1beta2/imageupdateautomations.md | 36 ++++++++++- .../imageupdateautomation_controller.go | 23 +++++-- internal/controller/update_test.go | 62 ++++++++++++++++++- 8 files changed, 204 insertions(+), 6 deletions(-) diff --git a/api/v1beta2/condition_types.go b/api/v1beta2/condition_types.go index 96bde947..910b706f 100644 --- a/api/v1beta2/condition_types.go +++ b/api/v1beta2/condition_types.go @@ -33,4 +33,7 @@ const ( // UpdateFailedReason represents a failure during source update. UpdateFailedReason string = "UpdateFailed" + + // InvalidPolicySelectorReason represents an invalid policy selector. + InvalidPolicySelectorReason string = "InvalidPolicySelector" ) diff --git a/api/v1beta2/imageupdateautomation_types.go b/api/v1beta2/imageupdateautomation_types.go index 70200b56..4eb31e7a 100644 --- a/api/v1beta2/imageupdateautomation_types.go +++ b/api/v1beta2/imageupdateautomation_types.go @@ -48,6 +48,11 @@ type ImageUpdateAutomationSpec struct { // +required Interval metav1.Duration `json:"interval"` + // PolicySelector allows to filter applied policies based on labels. + // By default includes all policies in namespace. + // +optional + PolicySelector *metav1.LabelSelector `json:"policySelector,omitempty"` + // Update gives the specification for how to update the files in // the repository. This can be left empty, to use the default // value. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 9ebc16fa..2400f0a6 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -202,6 +202,11 @@ func (in *ImageUpdateAutomationSpec) DeepCopyInto(out *ImageUpdateAutomationSpec (*in).DeepCopyInto(*out) } out.Interval = in.Interval + if in.PolicySelector != nil { + in, out := &in.PolicySelector, &out.PolicySelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } if in.Update != nil { in, out := &in.Update, &out.Update *out = new(UpdateStrategy) diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml index 12e6b781..d6e7c8ad 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml @@ -499,6 +499,52 @@ spec: run should be attempted. pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ type: string + policySelector: + description: |- + PolicySelector allows to filter applied policies based on labels. + By default includes all policies in namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic sourceRef: description: |- SourceRef refers to the resource giving access details diff --git a/docs/api/v1beta2/image-automation.md b/docs/api/v1beta2/image-automation.md index 3ef1ab9d..107ebb96 100644 --- a/docs/api/v1beta2/image-automation.md +++ b/docs/api/v1beta2/image-automation.md @@ -409,6 +409,21 @@ run should be attempted.

+policySelector
+ + +Kubernetes meta/v1.LabelSelector + + + + +(Optional) +

PolicySelector allows to filter applied policies based on labels. +By default includes all policies in namespace.

+ + + + update
@@ -517,6 +532,21 @@ run should be attempted.

+policySelector
+ +
+Kubernetes meta/v1.LabelSelector + + + + +(Optional) +

PolicySelector allows to filter applied policies based on labels. +By default includes all policies in namespace.

+ + + + update
diff --git a/docs/spec/v1beta2/imageupdateautomations.md b/docs/spec/v1beta2/imageupdateautomations.md index 9d46d512..e21d6e35 100644 --- a/docs/spec/v1beta2/imageupdateautomations.md +++ b/docs/spec/v1beta2/imageupdateautomations.md @@ -535,6 +535,39 @@ the ImageUpdateAutomation, and changes to the resource or image policies or Git repository will not result in any update. When the field is set to `false` or removed, it will resume. +### PolicySelector + +`.spec.policySelector` is an optional field to limit policies that an +ImageUpdateAutomation takes into account. It supports the same selectors as +`Deployment.spec.selector` (`matchLabels` and `matchExpressions` fields). If +not specified, it defaults to `matchLabels: {}` which means all policies in +namespace. + +```yaml +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImageUpdateAutomation +metadata: + name: +spec: + policySelector: + matchLabels: + app.kubernetes.io/instance: my-app +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImageUpdateAutomation +metadata: + name: +spec: + policySelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - my-component + - my-other-component +``` + ## Working with ImageUpdateAutomation ### Triggering a reconciliation @@ -901,11 +934,12 @@ completing. This can occur due to some of the following factors: - The source configuration is invalid for the current state of the source, for example, the specified branch does not exists in the remote source repository. - The remote source repository prevents push or creation of new push branch. +- The policy selector is invalid, for example, label is too long When this happens, the controller sets the `Ready` Condition status to `False` with the following reasons: -- `reason: AccessDenied` | `reason: InvalidSourceConfiguration` | `reason: GitOperationFailed` | `reason: UpdateFailed` +- `reason: AccessDenied` | `reason: InvalidSourceConfiguration` | `reason: GitOperationFailed` | `reason: UpdateFailed` | `reason: InvalidPolicySelector` While the ImageUpdateAutomation is in failing state, the controller will continue to attempt to update the source with an exponential backoff, until it diff --git a/internal/controller/imageupdateautomation_controller.go b/internal/controller/imageupdateautomation_controller.go index 0b22b56a..a5cf4905 100644 --- a/internal/controller/imageupdateautomation_controller.go +++ b/internal/controller/imageupdateautomation_controller.go @@ -26,6 +26,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" kerrors "k8s.io/apimachinery/pkg/util/errors" kuberecorder "k8s.io/client-go/tools/record" @@ -78,6 +79,8 @@ var imageUpdateAutomationNegativeConditions = []string{ meta.ReconcilingCondition, } +var errParsePolicySelector = errors.New("failed to parse policy selector") + // getPatchOptions composes patch options based on the given parameters. // It is used as the options used when patching an object. func getPatchOptions(ownedConditions []string, controllerName string) []patch.Option { @@ -303,9 +306,13 @@ func (r *ImageUpdateAutomationReconciler) reconcile(ctx context.Context, sp *pat } // List the policies and construct observed policies. - // TODO: Add support for filtering policies. - policies, err := getPolicies(ctx, r.Client, obj.Namespace) + policies, err := getPolicies(ctx, r.Client, obj.Namespace, obj.Spec.PolicySelector) if err != nil { + if errors.Is(err, errParsePolicySelector) { + conditions.MarkStalled(obj, imagev1.InvalidPolicySelectorReason, err.Error()) + result, retErr = ctrl.Result{}, nil + return + } result, retErr = ctrl.Result{}, err return } @@ -501,9 +508,17 @@ func (r *ImageUpdateAutomationReconciler) reconcileDelete(obj *imagev1.ImageUpda // getPolicies returns list of policies in the given namespace that have latest // image. -func getPolicies(ctx context.Context, kclient client.Client, namespace string) ([]imagev1_reflect.ImagePolicy, error) { +func getPolicies(ctx context.Context, kclient client.Client, namespace string, selector *metav1.LabelSelector) ([]imagev1_reflect.ImagePolicy, error) { + policySelector := labels.Everything() + var err error + if selector != nil { + if policySelector, err = metav1.LabelSelectorAsSelector(selector); err != nil { + return nil, fmt.Errorf("%w: %w", errParsePolicySelector, err) + } + } + var policies imagev1_reflect.ImagePolicyList - if err := kclient.List(ctx, &policies, &client.ListOptions{Namespace: namespace}); err != nil { + if err := kclient.List(ctx, &policies, &client.ListOptions{Namespace: namespace, LabelSelector: policySelector}); err != nil { return nil, fmt.Errorf("failed to list policies: %w", err) } diff --git a/internal/controller/update_test.go b/internal/controller/update_test.go index 81a6acfa..b335045d 100644 --- a/internal/controller/update_test.go +++ b/internal/controller/update_test.go @@ -38,6 +38,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -314,6 +315,47 @@ func TestImageUpdateAutomationReconciler_Reconcile(t *testing.T) { checker.WithT(g).CheckErr(ctx, obj) }) + t.Run("invalid policy selector results in stalled", func(t *testing.T) { + g := NewWithT(t) + + namespace, err := testEnv.CreateNamespace(ctx, "test-update") + g.Expect(err).ToNot(HaveOccurred()) + defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }() + + obj := &imagev1.ImageUpdateAutomation{} + obj.Name = updateName + obj.Namespace = namespace.Name + obj.Spec = imagev1.ImageUpdateAutomationSpec{ + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: "foo", + }, + PolicySelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label-too-long-" + strings.Repeat("0", validation.LabelValueMaxLength): "", + }, + }, + } + g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) + defer func() { + g.Expect(deleteImageUpdateAutomation(ctx, testEnv, obj.Name, obj.Namespace)).To(Succeed()) + }() + + expectedConditions := []metav1.Condition{ + *conditions.TrueCondition(meta.StalledCondition, imagev1.InvalidPolicySelectorReason, "failed to parse policy selector"), + *conditions.FalseCondition(meta.ReadyCondition, imagev1.InvalidPolicySelectorReason, "failed to parse policy selector"), + } + g.Eventually(func(g Gomega) { + g.Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions)) + }).Should(Succeed()) + + // Check if the object status is valid. + condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions} + checker := conditionscheck.NewChecker(testEnv.Client, condns) + checker.WithT(g).CheckErr(ctx, obj) + }) + t.Run("non-existing gitrepo results in failure", func(t *testing.T) { g := NewWithT(t) @@ -1434,11 +1476,13 @@ func Test_getPolicies(t *testing.T) { name string namespace string latestImage string + labels map[string]string } tests := []struct { name string listNamespace string + selector *metav1.LabelSelector policies []policyArgs wantPolicies []string }{ @@ -1453,6 +1497,21 @@ func Test_getPolicies(t *testing.T) { }, wantPolicies: []string{"p1", "p2"}, }, + { + name: "lists policies with label selector in same namespace", + listNamespace: testNS1, + selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label": "one", + }, + }, + policies: []policyArgs{ + {name: "p1", namespace: testNS1, latestImage: "aaa:bbb", labels: map[string]string{"label": "one"}}, + {name: "p2", namespace: testNS1, latestImage: "ccc:ddd", labels: map[string]string{"label": "false"}}, + {name: "p3", namespace: testNS2, latestImage: "eee:fff", labels: map[string]string{"label": "one"}}, + }, + wantPolicies: []string{"p1"}, + }, { name: "no policies in empty namespace", listNamespace: testNS2, @@ -1475,13 +1534,14 @@ func Test_getPolicies(t *testing.T) { aPolicy.Status = imagev1_reflect.ImagePolicyStatus{ LatestImage: p.latestImage, } + aPolicy.Labels = p.labels testObjects = append(testObjects, aPolicy) } kClient := fakeclient.NewClientBuilder(). WithScheme(testEnv.GetScheme()). WithObjects(testObjects...).Build() - result, err := getPolicies(context.TODO(), kClient, tt.listNamespace) + result, err := getPolicies(context.TODO(), kClient, tt.listNamespace, tt.selector) g.Expect(err).ToNot(HaveOccurred()) // Extract policy name from the result and compare with the expected