feat: support multi receiver by matchLabels

Signed-off-by: Raffael Sahli <raffael.sahli@doodle.com>
This commit is contained in:
Raffael Sahli 2023-03-20 08:11:30 +00:00
parent b841b2d02a
commit 57f62f400c
No known key found for this signature in database
GPG Key ID: 5E0BF46A67AD81C4
6 changed files with 492 additions and 47 deletions

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View File

@ -741,7 +741,8 @@ string
</em>
</td>
<td>
<p>Name of the referent.</p>
<p>Name of the referent.
If multiple resources are targeted <code>*</code> may be set.</p>
</td>
</tr>
<tr>
@ -767,7 +768,8 @@ map[string]string
<em>(Optional)</em>
<p>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 &ldquo;key&rdquo;, the
operator is &ldquo;In&rdquo;, and the values array contains only &ldquo;value&rdquo;. The requirements are ANDed.</p>
operator is &ldquo;In&rdquo;, and the values array contains only &ldquo;value&rdquo;. The requirements are ANDed.
MatchLabels requires the name to be set to <code>*</code>.</p>
</td>
</tr>
</tbody>

View File

@ -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,351 @@ func Test_validate(t *testing.T) {
Name: "non-existing",
},
},
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
},
expectedErr: true,
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"),
},
},
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: 3, // it is 3 because we target receivers itself in this test and the receiver here is included with *
expectedResponseCode: http.StatusOK,
},
{
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 +652,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 +678,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 +691,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)
var annotatedResources int
for _, obj := range allReceivers.Items {
if _, ok := obj.GetAnnotations()[meta.ReconcileRequestAnnotation]; ok {
annotatedResources++
}
}
if !tt.expectedErr && err != nil {
t.Errorf("unexpected error: '%s'", err)
}
g.Expect(annotatedResources).To(gomega.Equal(tt.expectedResourcesAnnotated))
})
}
}

View File

@ -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,36 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNames
group, version := getGroupVersion(apiVersion)
if resource.Name == "*" {
logger.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)
}
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.V(1).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 +399,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