Add support to limit applied policies in automation by specifying a selector
Signed-off-by: Maxim Samoilov <nitive@icloud.com>
This commit is contained in:
parent
fd4a40d941
commit
d0a24940d6
|
|
@ -33,4 +33,7 @@ const (
|
|||
|
||||
// UpdateFailedReason represents a failure during source update.
|
||||
UpdateFailedReason string = "UpdateFailed"
|
||||
|
||||
// InvalidPolicySelectorReason represents an invalid policy selector.
|
||||
InvalidPolicySelectorReason string = "InvalidPolicySelector"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -409,6 +409,21 @@ run should be attempted.</p>
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>policySelector</code><br>
|
||||
<em>
|
||||
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
|
||||
Kubernetes meta/v1.LabelSelector
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>PolicySelector allows to filter applied policies based on labels.
|
||||
By default includes all policies in namespace.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>update</code><br>
|
||||
<em>
|
||||
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">
|
||||
|
|
@ -517,6 +532,21 @@ run should be attempted.</p>
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>policySelector</code><br>
|
||||
<em>
|
||||
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
|
||||
Kubernetes meta/v1.LabelSelector
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>PolicySelector allows to filter applied policies based on labels.
|
||||
By default includes all policies in namespace.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>update</code><br>
|
||||
<em>
|
||||
<a href="#image.toolkit.fluxcd.io/v1beta2.UpdateStrategy">
|
||||
|
|
|
|||
|
|
@ -542,6 +542,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: <automation-name>
|
||||
spec:
|
||||
policySelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: my-app
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: <automation-name>
|
||||
spec:
|
||||
policySelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/component
|
||||
operator: In
|
||||
values:
|
||||
- my-component
|
||||
- my-other-component
|
||||
```
|
||||
|
||||
## Working with ImageUpdateAutomation
|
||||
|
||||
### Triggering a reconciliation
|
||||
|
|
@ -908,11 +941,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
|
||||
|
|
|
|||
|
|
@ -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,12 +306,21 @@ 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
|
||||
}
|
||||
// Update any stale Ready=False condition from policies config failure.
|
||||
if conditions.HasAnyReason(obj, meta.ReadyCondition, imagev1.InvalidPolicySelectorReason) {
|
||||
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
|
||||
}
|
||||
|
||||
observedPolicies, err := observedPolicies(policies)
|
||||
if err != nil {
|
||||
result, retErr = ctrl.Result{}, err
|
||||
|
|
@ -501,9 +513,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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue