Merge pull request #482 from raffis/feat-receiver-by-labels

feat: support multi receiver by matchLabels
This commit is contained in:
Max Jonas Werner 2023-03-23 13:26:51 +01:00 committed by GitHub
commit f8ab99e080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 508 additions and 48 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

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

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,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}},
},
},
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"),
},
},
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)
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,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