Skip to content

Commit df4bde7

Browse files
Nitivedarkowlzz
authored andcommitted
Add support to limit applied policies in automation by specifying a selector
Signed-off-by: Maxim Samoilov <[email protected]>
1 parent fa09050 commit df4bde7

8 files changed

+209
-6
lines changed

api/v1beta2/condition_types.go

+3
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ const (
3333

3434
// UpdateFailedReason represents a failure during source update.
3535
UpdateFailedReason string = "UpdateFailed"
36+
37+
// InvalidPolicySelectorReason represents an invalid policy selector.
38+
InvalidPolicySelectorReason string = "InvalidPolicySelector"
3639
)

api/v1beta2/imageupdateautomation_types.go

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ type ImageUpdateAutomationSpec struct {
4848
// +required
4949
Interval metav1.Duration `json:"interval"`
5050

51+
// PolicySelector allows to filter applied policies based on labels.
52+
// By default includes all policies in namespace.
53+
// +optional
54+
PolicySelector *metav1.LabelSelector `json:"policySelector,omitempty"`
55+
5156
// Update gives the specification for how to update the files in
5257
// the repository. This can be left empty, to use the default
5358
// value.

api/v1beta2/zz_generated.deepcopy.go

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml

+46
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,52 @@ spec:
499499
run should be attempted.
500500
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
501501
type: string
502+
policySelector:
503+
description: |-
504+
PolicySelector allows to filter applied policies based on labels.
505+
By default includes all policies in namespace.
506+
properties:
507+
matchExpressions:
508+
description: matchExpressions is a list of label selector requirements.
509+
The requirements are ANDed.
510+
items:
511+
description: |-
512+
A label selector requirement is a selector that contains values, a key, and an operator that
513+
relates the key and values.
514+
properties:
515+
key:
516+
description: key is the label key that the selector applies
517+
to.
518+
type: string
519+
operator:
520+
description: |-
521+
operator represents a key's relationship to a set of values.
522+
Valid operators are In, NotIn, Exists and DoesNotExist.
523+
type: string
524+
values:
525+
description: |-
526+
values is an array of string values. If the operator is In or NotIn,
527+
the values array must be non-empty. If the operator is Exists or DoesNotExist,
528+
the values array must be empty. This array is replaced during a strategic
529+
merge patch.
530+
items:
531+
type: string
532+
type: array
533+
required:
534+
- key
535+
- operator
536+
type: object
537+
type: array
538+
matchLabels:
539+
additionalProperties:
540+
type: string
541+
description: |-
542+
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
543+
map is equivalent to an element of matchExpressions, whose key field is "key", the
544+
operator is "In", and the values array contains only "value". The requirements are ANDed.
545+
type: object
546+
type: object
547+
x-kubernetes-map-type: atomic
502548
sourceRef:
503549
description: |-
504550
SourceRef refers to the resource giving access details

docs/api/v1beta2/image-automation.md

+30
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,21 @@ run should be attempted.</p>
409409
</tr>
410410
<tr>
411411
<td>
412+
<code>policySelector</code><br>
413+
<em>
414+
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
415+
Kubernetes meta/v1.LabelSelector
416+
</a>
417+
</em>
418+
</td>
419+
<td>
420+
<em>(Optional)</em>
421+
<p>PolicySelector allows to filter applied policies based on labels.
422+
By default includes all policies in namespace.</p>
423+
</td>
424+
</tr>
425+
<tr>
426+
<td>
412427
<code>update</code><br>
413428
<em>
414429
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">
@@ -517,6 +532,21 @@ run should be attempted.</p>
517532
</tr>
518533
<tr>
519534
<td>
535+
<code>policySelector</code><br>
536+
<em>
537+
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
538+
Kubernetes meta/v1.LabelSelector
539+
</a>
540+
</em>
541+
</td>
542+
<td>
543+
<em>(Optional)</em>
544+
<p>PolicySelector allows to filter applied policies based on labels.
545+
By default includes all policies in namespace.</p>
546+
</td>
547+
</tr>
548+
<tr>
549+
<td>
520550
<code>update</code><br>
521551
<em>
522552
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">

docs/spec/v1beta2/imageupdateautomations.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,39 @@ the ImageUpdateAutomation, and changes to the resource or image policies or Git
535535
repository will not result in any update. When the field is set to `false` or
536536
removed, it will resume.
537537

538+
### PolicySelector
539+
540+
`.spec.policySelector` is an optional field to limit policies that an
541+
ImageUpdateAutomation takes into account. It supports the same selectors as
542+
`Deployment.spec.selector` (`matchLabels` and `matchExpressions` fields). If
543+
not specified, it defaults to `matchLabels: {}` which means all policies in
544+
namespace.
545+
546+
```yaml
547+
---
548+
apiVersion: image.toolkit.fluxcd.io/v1beta2
549+
kind: ImageUpdateAutomation
550+
metadata:
551+
name: <automation-name>
552+
spec:
553+
policySelector:
554+
matchLabels:
555+
app.kubernetes.io/instance: my-app
556+
---
557+
apiVersion: image.toolkit.fluxcd.io/v1beta2
558+
kind: ImageUpdateAutomation
559+
metadata:
560+
name: <automation-name>
561+
spec:
562+
policySelector:
563+
matchExpressions:
564+
- key: app.kubernetes.io/component
565+
operator: In
566+
values:
567+
- my-component
568+
- my-other-component
569+
```
570+
538571
## Working with ImageUpdateAutomation
539572

540573
### Triggering a reconciliation
@@ -901,11 +934,12 @@ completing. This can occur due to some of the following factors:
901934
- The source configuration is invalid for the current state of the source, for
902935
example, the specified branch does not exists in the remote source repository.
903936
- The remote source repository prevents push or creation of new push branch.
937+
- The policy selector is invalid, for example, label is too long.
904938

905939
When this happens, the controller sets the `Ready` Condition status to `False`
906940
with the following reasons:
907941

908-
- `reason: AccessDenied` | `reason: InvalidSourceConfiguration` | `reason: GitOperationFailed` | `reason: UpdateFailed`
942+
- `reason: AccessDenied` | `reason: InvalidSourceConfiguration` | `reason: GitOperationFailed` | `reason: UpdateFailed` | `reason: InvalidPolicySelector`
909943

910944
While the ImageUpdateAutomation is in failing state, the controller will
911945
continue to attempt to update the source with an exponential backoff, until it

internal/controller/imageupdateautomation_controller.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
corev1 "k8s.io/api/core/v1"
2727
apierrors "k8s.io/apimachinery/pkg/api/errors"
2828
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/labels"
2930
"k8s.io/apimachinery/pkg/runtime"
3031
kerrors "k8s.io/apimachinery/pkg/util/errors"
3132
kuberecorder "k8s.io/client-go/tools/record"
@@ -78,6 +79,8 @@ var imageUpdateAutomationNegativeConditions = []string{
7879
meta.ReconcilingCondition,
7980
}
8081

82+
var errParsePolicySelector = errors.New("failed to parse policy selector")
83+
8184
// getPatchOptions composes patch options based on the given parameters.
8285
// It is used as the options used when patching an object.
8386
func getPatchOptions(ownedConditions []string, controllerName string) []patch.Option {
@@ -303,12 +306,21 @@ func (r *ImageUpdateAutomationReconciler) reconcile(ctx context.Context, sp *pat
303306
}
304307

305308
// List the policies and construct observed policies.
306-
// TODO: Add support for filtering policies.
307-
policies, err := getPolicies(ctx, r.Client, obj.Namespace)
309+
policies, err := getPolicies(ctx, r.Client, obj.Namespace, obj.Spec.PolicySelector)
308310
if err != nil {
311+
if errors.Is(err, errParsePolicySelector) {
312+
conditions.MarkStalled(obj, imagev1.InvalidPolicySelectorReason, err.Error())
313+
result, retErr = ctrl.Result{}, nil
314+
return
315+
}
309316
result, retErr = ctrl.Result{}, err
310317
return
311318
}
319+
// Update any stale Ready=False condition from policies config failure.
320+
if conditions.HasAnyReason(obj, meta.ReadyCondition, imagev1.InvalidPolicySelectorReason) {
321+
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
322+
}
323+
312324
observedPolicies, err := observedPolicies(policies)
313325
if err != nil {
314326
result, retErr = ctrl.Result{}, err
@@ -501,9 +513,17 @@ func (r *ImageUpdateAutomationReconciler) reconcileDelete(obj *imagev1.ImageUpda
501513

502514
// getPolicies returns list of policies in the given namespace that have latest
503515
// image.
504-
func getPolicies(ctx context.Context, kclient client.Client, namespace string) ([]imagev1_reflect.ImagePolicy, error) {
516+
func getPolicies(ctx context.Context, kclient client.Client, namespace string, selector *metav1.LabelSelector) ([]imagev1_reflect.ImagePolicy, error) {
517+
policySelector := labels.Everything()
518+
var err error
519+
if selector != nil {
520+
if policySelector, err = metav1.LabelSelectorAsSelector(selector); err != nil {
521+
return nil, fmt.Errorf("%w: %w", errParsePolicySelector, err)
522+
}
523+
}
524+
505525
var policies imagev1_reflect.ImagePolicyList
506-
if err := kclient.List(ctx, &policies, &client.ListOptions{Namespace: namespace}); err != nil {
526+
if err := kclient.List(ctx, &policies, &client.ListOptions{Namespace: namespace, LabelSelector: policySelector}); err != nil {
507527
return nil, fmt.Errorf("failed to list policies: %w", err)
508528
}
509529

internal/controller/update_test.go

+61-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3939
"k8s.io/apimachinery/pkg/types"
4040
"k8s.io/apimachinery/pkg/util/rand"
41+
"k8s.io/apimachinery/pkg/util/validation"
4142
"k8s.io/client-go/tools/record"
4243
ctrl "sigs.k8s.io/controller-runtime"
4344
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -314,6 +315,47 @@ func TestImageUpdateAutomationReconciler_Reconcile(t *testing.T) {
314315
checker.WithT(g).CheckErr(ctx, obj)
315316
})
316317

318+
t.Run("invalid policy selector results in stalled", func(t *testing.T) {
319+
g := NewWithT(t)
320+
321+
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
322+
g.Expect(err).ToNot(HaveOccurred())
323+
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
324+
325+
obj := &imagev1.ImageUpdateAutomation{}
326+
obj.Name = updateName
327+
obj.Namespace = namespace.Name
328+
obj.Spec = imagev1.ImageUpdateAutomationSpec{
329+
SourceRef: imagev1.CrossNamespaceSourceReference{
330+
Kind: "GitRepository",
331+
Name: "foo",
332+
},
333+
PolicySelector: &metav1.LabelSelector{
334+
MatchLabels: map[string]string{
335+
"label-too-long-" + strings.Repeat("0", validation.LabelValueMaxLength): "",
336+
},
337+
},
338+
}
339+
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
340+
defer func() {
341+
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, obj.Name, obj.Namespace)).To(Succeed())
342+
}()
343+
344+
expectedConditions := []metav1.Condition{
345+
*conditions.TrueCondition(meta.StalledCondition, imagev1.InvalidPolicySelectorReason, "failed to parse policy selector"),
346+
*conditions.FalseCondition(meta.ReadyCondition, imagev1.InvalidPolicySelectorReason, "failed to parse policy selector"),
347+
}
348+
g.Eventually(func(g Gomega) {
349+
g.Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
350+
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
351+
}).Should(Succeed())
352+
353+
// Check if the object status is valid.
354+
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
355+
checker := conditionscheck.NewChecker(testEnv.Client, condns)
356+
checker.WithT(g).CheckErr(ctx, obj)
357+
})
358+
317359
t.Run("non-existing gitrepo results in failure", func(t *testing.T) {
318360
g := NewWithT(t)
319361

@@ -1434,11 +1476,13 @@ func Test_getPolicies(t *testing.T) {
14341476
name string
14351477
namespace string
14361478
latestImage string
1479+
labels map[string]string
14371480
}
14381481

14391482
tests := []struct {
14401483
name string
14411484
listNamespace string
1485+
selector *metav1.LabelSelector
14421486
policies []policyArgs
14431487
wantPolicies []string
14441488
}{
@@ -1453,6 +1497,21 @@ func Test_getPolicies(t *testing.T) {
14531497
},
14541498
wantPolicies: []string{"p1", "p2"},
14551499
},
1500+
{
1501+
name: "lists policies with label selector in same namespace",
1502+
listNamespace: testNS1,
1503+
selector: &metav1.LabelSelector{
1504+
MatchLabels: map[string]string{
1505+
"label": "one",
1506+
},
1507+
},
1508+
policies: []policyArgs{
1509+
{name: "p1", namespace: testNS1, latestImage: "aaa:bbb", labels: map[string]string{"label": "one"}},
1510+
{name: "p2", namespace: testNS1, latestImage: "ccc:ddd", labels: map[string]string{"label": "false"}},
1511+
{name: "p3", namespace: testNS2, latestImage: "eee:fff", labels: map[string]string{"label": "one"}},
1512+
},
1513+
wantPolicies: []string{"p1"},
1514+
},
14561515
{
14571516
name: "no policies in empty namespace",
14581517
listNamespace: testNS2,
@@ -1475,13 +1534,14 @@ func Test_getPolicies(t *testing.T) {
14751534
aPolicy.Status = imagev1_reflect.ImagePolicyStatus{
14761535
LatestImage: p.latestImage,
14771536
}
1537+
aPolicy.Labels = p.labels
14781538
testObjects = append(testObjects, aPolicy)
14791539
}
14801540
kClient := fakeclient.NewClientBuilder().
14811541
WithScheme(testEnv.GetScheme()).
14821542
WithObjects(testObjects...).Build()
14831543

1484-
result, err := getPolicies(context.TODO(), kClient, tt.listNamespace)
1544+
result, err := getPolicies(context.TODO(), kClient, tt.listNamespace, tt.selector)
14851545
g.Expect(err).ToNot(HaveOccurred())
14861546

14871547
// Extract policy name from the result and compare with the expected

0 commit comments

Comments
 (0)