diff --git a/api/v1beta2/reference_types.go b/api/v1beta2/reference_types.go
index 50594f3fd..b3e484b3e 100644
--- a/api/v1beta2/reference_types.go
+++ b/api/v1beta2/reference_types.go
@@ -29,6 +29,7 @@ type CrossNamespaceObjectReference struct {
Kind string `json:"kind,omitempty"`
// Name of the referent.
+ // If multiple resources are targeted `*` may be set.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=53
// +required
@@ -44,6 +45,7 @@ type CrossNamespaceObjectReference struct {
// 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.
+ // MatchLabels requires the name to be set to `*`.
// +optional
MatchLabels map[string]string `json:"matchLabels,omitempty"`
}
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
index 1502aaddb..fbf395bca 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
@@ -279,10 +279,11 @@ spec:
{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.
+ are ANDed. MatchLabels requires the name to be set to `*`.
type: object
name:
- description: Name of the referent.
+ description: Name of the referent. If multiple resources are
+ targeted `*` may be set.
maxLength: 53
minLength: 1
type: string
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
index 4291168e0..021d1882b 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
@@ -288,10 +288,11 @@ spec:
{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.
+ are ANDed. MatchLabels requires the name to be set to `*`.
type: object
name:
- description: Name of the referent.
+ description: Name of the referent. If multiple resources are
+ targeted `*` may be set.
maxLength: 53
minLength: 1
type: string
diff --git a/docs/api/notification.md b/docs/api/notification.md
index cc2837b5e..f61bd7616 100644
--- a/docs/api/notification.md
+++ b/docs/api/notification.md
@@ -741,7 +741,8 @@ string
- Name of the referent.
+Name of the referent.
+If multiple resources are targeted * may be set.
|
@@ -767,7 +768,8 @@ map[string]string
(Optional)
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.
+operator is “In”, and the values array contains only “value”. The requirements are ANDed.
+MatchLabels requires the name to be set to *
.
diff --git a/docs/spec/v1beta2/receivers.md b/docs/spec/v1beta2/receivers.md
index bbd192979..6e4285642 100644
--- a/docs/spec/v1beta2/receivers.md
+++ b/docs/spec/v1beta2/receivers.md
@@ -635,9 +635,35 @@ A resource entry contains the following fields:
`GitRepository`, `Kustomization`, `HelmRelease`, `HelmChart`,
`HelmRepository`, `ImageRepository`, `ImagePolicy`, `ImageUpdateAutomation`
and `OCIRepository`.
-- `name`: The Flux Custom Resource `.metadata.name`.
+- `name`: The Flux Custom Resource `.metadata.name` or it can be set to '*' wildcard(when `matchLabels` is specified)
- `namespace` (Optional): The Flux Custom Resource `.metadata.namespace`.
When not specified, the Receiver's `.metadata.namespace` is used instead.
+- `matchLabels` (Optional): Annotate Flux Custom Resources with specific labels.
+ The `name` field must be set to '*' when using `matchLabels`
+
+#### Annotate objects by name
+
+To annotate single Flux object, set the `kind`, `name` and `namespace`:
+
+```yaml
+resources:
+ - apiVersion: image.toolkit.fluxcd.io/v1beta1
+ kind: ImageRepository
+ name: podinfo
+```
+
+#### Annotate objects by label
+
+To annotate Flux objects of a particular kind with specific labels:
+
+```yaml
+resources:
+ - apiVersion: image.toolkit.fluxcd.io/v1beta1
+ kind: ImageRepository
+ name: "*"
+ matchLabels:
+ app: podinfo
+```
**Note:** Cross-namespace references [can be disabled for security
diff --git a/internal/server/receiver_handler_test.go b/internal/server/receiver_handler_test.go
index c28436e40..c134b04d6 100644
--- a/internal/server/receiver_handler_test.go
+++ b/internal/server/receiver_handler_test.go
@@ -23,13 +23,16 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
+ "net/http"
"net/http/httptest"
"testing"
"github.com/google/go-github/v41/github"
+ "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/fluxcd/pkg/apis/meta"
@@ -38,21 +41,23 @@ import (
apiv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
-func Test_validate(t *testing.T) {
+func Test_handlePayload(t *testing.T) {
type hashOpts struct {
calculate bool
header string
}
tests := []struct {
- name string
- hashOpts hashOpts
- headers map[string]string
- payload map[string]interface{}
- receiver *apiv1.Receiver
- receiverType string
- secret *corev1.Secret
- expectedErr bool
+ name string
+ hashOpts hashOpts
+ headers map[string]string
+ payload map[string]interface{}
+ receiver *apiv1.Receiver
+ receiverType string
+ secret *corev1.Secret
+ resources []client.Object
+ expectedResourcesAnnotated int
+ expectedResponseCode int
}{
{
name: "Generic receiver",
@@ -66,6 +71,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -75,7 +84,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"),
},
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "gitlab receiver",
@@ -89,6 +98,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
headers: map[string]string{
"X-Gitlab-Token": "token",
@@ -101,7 +114,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"),
},
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "github receiver",
@@ -115,6 +128,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
hashOpts: hashOpts{
calculate: true,
@@ -134,7 +151,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"),
},
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "generic hmac receiver",
@@ -148,6 +165,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
hashOpts: hashOpts{
calculate: true,
@@ -164,7 +185,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"),
},
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "bitbucket receiver",
@@ -179,6 +200,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
hashOpts: hashOpts{
calculate: true,
@@ -196,7 +221,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"),
},
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "quay receiver",
@@ -210,6 +235,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -225,7 +254,7 @@ func Test_validate(t *testing.T) {
"v0.0.1",
},
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "harbor receiver",
@@ -239,6 +268,10 @@ func Test_validate(t *testing.T) {
Name: "token",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -251,7 +284,7 @@ func Test_validate(t *testing.T) {
headers: map[string]string{
"Authorization": "token",
},
- expectedErr: false,
+ expectedResponseCode: http.StatusOK,
},
{
name: "missing secret",
@@ -265,8 +298,330 @@ func Test_validate(t *testing.T) {
Name: "non-existing",
},
},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ expectedResponseCode: http.StatusBadRequest,
+ },
+ {
+ name: "no receiver configured",
+ expectedResponseCode: http.StatusNotFound,
+ },
+ {
+ name: "not ready receiver is ignored",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "notready-receiver",
+ },
+ Spec: apiv1.ReceiverSpec{},
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.StalledCondition, Status: metav1.ConditionFalse}},
+ },
+ },
+ expectedResponseCode: http.StatusNotFound,
+ },
+ {
+ name: "suspended receiver ignored",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "suspended-receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Suspend: true,
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ expectedResponseCode: http.StatusNotFound,
+ },
+ {
+ name: "missing apiVersion in resource",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.CrossNamespaceObjectReference{
+ {
+ Kind: apiv1.ReceiverKind,
+ MatchLabels: map[string]string{
+ "label": "match",
+ },
+ },
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ expectedResponseCode: http.StatusBadRequest,
+ },
+ {
+ name: "resource by name not found",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.CrossNamespaceObjectReference{
+ {
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "does-not-exists",
+ },
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ expectedResponseCode: http.StatusBadRequest,
+ },
+ {
+ name: "annotating resources by label match",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.CrossNamespaceObjectReference{
+ {
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "*",
+ MatchLabels: map[string]string{
+ "label": "match",
+ },
+ },
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ resources: []client.Object{
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource-2",
+ Labels: map[string]string{
+ "label": "does-not-match",
+ },
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource",
+ Labels: map[string]string{
+ "label": "match",
+ },
+ },
+ },
+ },
+ expectedResourcesAnnotated: 1,
+ expectedResponseCode: http.StatusOK,
+ },
+ {
+ name: "annotating resource by name",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.CrossNamespaceObjectReference{
+ {
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "dummy-resource",
+ },
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ resources: []client.Object{
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource-2",
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource",
+ },
+ },
+ },
+ expectedResourcesAnnotated: 1,
+ expectedResponseCode: http.StatusOK,
+ },
+ {
+ name: "annotating all resources if name is *",
+ receiver: &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.CrossNamespaceObjectReference{
+ {
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "*",
+ },
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
},
- expectedErr: true,
+ expectedResponseCode: http.StatusBadRequest,
+ },
+ {
+ name: "resource matchLabels is ignored if name is not *",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.CrossNamespaceObjectReference{
+ {
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "dummy-resource",
+ MatchLabels: map[string]string{
+ "label": "match",
+ },
+ },
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ resources: []client.Object{
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource-2",
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource",
+ },
+ },
+ },
+ expectedResourcesAnnotated: 1,
+ expectedResponseCode: http.StatusOK,
},
}
@@ -276,10 +631,17 @@ func Test_validate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ g := gomega.NewGomegaWithT(t)
builder := fake.NewClientBuilder()
builder.WithScheme(scheme)
- builder.WithObjects(tt.receiver)
+
+ if tt.receiver != nil {
+ builder.WithObjects(tt.receiver)
+ }
+
+ builder.WithObjects(tt.resources...)
+
if tt.secret != nil {
builder.WithObjects(tt.secret)
}
@@ -295,7 +657,7 @@ func Test_validate(t *testing.T) {
if err != nil {
t.Errorf("error marshalling test payload: '%s'", err)
}
- req := httptest.NewRequest("POST", "/", bytes.NewBuffer(data))
+ req := httptest.NewRequest("POST", "/hook/", bytes.NewBuffer(data))
for key, val := range tt.headers {
req.Header.Set(key, val)
}
@@ -308,14 +670,22 @@ func Test_validate(t *testing.T) {
req.Header.Set(tt.hashOpts.header, "sha256="+hex.EncodeToString(mac.Sum(nil)))
}
- err = s.validate(context.Background(), *tt.receiver, req)
- if tt.expectedErr && err == nil {
- t.Errorf("expected error but got %s", err)
- }
+ rr := httptest.NewRecorder()
+ handler := s.handlePayload()
+ handler(rr, req)
+ g.Expect(rr.Result().StatusCode).To(gomega.Equal(tt.expectedResponseCode))
+
+ var allReceivers apiv1.ReceiverList
+ err = client.List(context.TODO(), &allReceivers)
- if !tt.expectedErr && err != nil {
- t.Errorf("unexpected error: '%s'", err)
+ var annotatedResources int
+ for _, obj := range allReceivers.Items {
+ if _, ok := obj.GetAnnotations()[meta.ReconcileRequestAnnotation]; ok {
+ annotatedResources++
+ }
}
+
+ g.Expect(annotatedResources).To(gomega.Equal(tt.expectedResourcesAnnotated))
})
}
}
diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go
index 28ca40d70..7ebc2cc50 100644
--- a/internal/server/receiver_handlers.go
+++ b/internal/server/receiver_handlers.go
@@ -32,6 +32,7 @@ import (
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/go-logr/logr"
"github.com/google/go-github/v41/github"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -45,6 +46,7 @@ import (
// defaultFluxAPIVersions is a map of Flux API kinds to their API versions.
var defaultFluxAPIVersions = map[string]string{
"Bucket": "source.toolkit.fluxcd.io/v1beta2",
+ "HelmChart": "source.toolkit.fluxcd.io/v1beta2",
"HelmRepository": "source.toolkit.fluxcd.io/v1beta2",
"GitRepository": "source.toolkit.fluxcd.io/v1beta2",
"OCIRepository": "source.toolkit.fluxcd.io/v1beta2",
@@ -94,13 +96,9 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req
}
for _, resource := range receiver.Spec.Resources {
- if err := s.annotate(ctx, resource, receiver.Namespace); err != nil {
- logger.Error(err, fmt.Sprintf("unable to annotate resource '%s/%s.%s'",
- resource.Kind, resource.Name, resource.Namespace))
+ if err := s.requestReconciliation(ctx, logger, resource, receiver.Namespace); err != nil {
+ logger.Error(err, "unable to process resource")
withErrors = true
- } else {
- logger.Info(fmt.Sprintf("resource '%s/%s.%s' annotated",
- resource.Kind, resource.Name, resource.Namespace))
}
}
}
@@ -347,15 +345,12 @@ func (s *ReceiverServer) token(ctx context.Context, receiver apiv1.Receiver) (st
return token, nil
}
-func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNamespaceObjectReference, defaultNamespace string) error {
+// requestReconciliation requests reconciliation of all the resources matching the given CrossNamespaceObjectReference by annotating them accordingly.
+func (s *ReceiverServer) requestReconciliation(ctx context.Context, logger logr.Logger, resource apiv1.CrossNamespaceObjectReference, defaultNamespace string) error {
namespace := defaultNamespace
if resource.Namespace != "" {
namespace = resource.Namespace
}
- objectKey := client.ObjectKey{
- Namespace: namespace,
- Name: resource.Name,
- }
apiVersion := resource.APIVersion
if apiVersion == "" {
@@ -367,6 +362,46 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNames
group, version := getGroupVersion(apiVersion)
+ if resource.Name == "*" {
+ if resource.MatchLabels == nil {
+ return fmt.Errorf("matchLabels field not set when using wildcard '*' as name")
+ }
+
+ logger.V(1).Info(fmt.Sprintf("annotate resources by matchLabel for kind '%s' in '%s'",
+ resource.Kind, namespace), "matchLabels", resource.MatchLabels)
+
+ var resources metav1.PartialObjectMetadataList
+ resources.SetGroupVersionKind(schema.GroupVersionKind{
+ Group: group,
+ Kind: resource.Kind,
+ Version: version,
+ })
+
+ if err := s.kubeClient.List(ctx, &resources,
+ client.InNamespace(namespace),
+ client.MatchingLabels(resource.MatchLabels),
+ ); err != nil {
+ return fmt.Errorf("failed listing resources in namespace %q by matching labels %q: %w", namespace, resource.MatchLabels, err)
+ }
+
+ if len(resources.Items) == 0 {
+ noObjectsFoundErr := fmt.Errorf("no '%s' resources found with matching labels '%s' in '%s' namespace", resource.Kind, resource.MatchLabels, namespace)
+ logger.Error(noObjectsFoundErr, "error annotating resources")
+ return nil
+ }
+
+ for _, resource := range resources.Items {
+ if err := s.annotate(ctx, &resource); err != nil {
+ return fmt.Errorf("failed to annotate resource: '%s/%s.%s': %w", resource.Kind, resource.Name, namespace, err)
+ } else {
+ logger.Info(fmt.Sprintf("resource '%s/%s.%s' annotated",
+ resource.Kind, resource.Name, namespace))
+ }
+ }
+
+ return nil
+ }
+
u := &metav1.PartialObjectMetadata{}
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: group,
@@ -374,19 +409,42 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNames
Version: version,
})
+ objectKey := client.ObjectKey{
+ Namespace: namespace,
+ Name: resource.Name,
+ }
+
if err := s.kubeClient.Get(ctx, objectKey, u); err != nil {
return fmt.Errorf("unable to read %s '%s' error: %w", resource.Kind, objectKey, err)
}
- patch := client.MergeFrom(u.DeepCopy())
- sourceAnnotations := u.GetAnnotations()
+ err := s.annotate(ctx, u)
+ if err != nil {
+ return fmt.Errorf("failed to annotate resource: '%s/%s.%s': %w", resource.Kind, resource.Name, namespace, err)
+ } else {
+ logger.Info(fmt.Sprintf("resource '%s/%s.%s' annotated",
+ resource.Kind, resource.Name, namespace))
+ }
+
+ return nil
+}
+
+func (s *ReceiverServer) annotate(ctx context.Context, resource *metav1.PartialObjectMetadata) error {
+ patch := client.MergeFrom(resource.DeepCopy())
+ sourceAnnotations := resource.GetAnnotations()
+
if sourceAnnotations == nil {
sourceAnnotations = make(map[string]string)
}
+
sourceAnnotations[meta.ReconcileRequestAnnotation] = metav1.Now().String()
- u.SetAnnotations(sourceAnnotations)
- if err := s.kubeClient.Patch(ctx, u, patch); err != nil {
- return fmt.Errorf("unable to annotate %s '%s' error: %w", resource.Kind, objectKey, err)
+ resource.SetAnnotations(sourceAnnotations)
+
+ if err := s.kubeClient.Patch(ctx, resource, patch); err != nil {
+ return fmt.Errorf("unable to annotate %s '%s' error: %w", resource.Kind, client.ObjectKey{
+ Namespace: resource.Namespace,
+ Name: resource.Name,
+ }, err)
}
return nil