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"` Kind string `json:"kind,omitempty"`
// Name of the referent. // Name of the referent.
// If multiple resources are targeted `*` may be set.
// +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=53 // +kubebuilder:validation:MaxLength=53
// +required // +required
@ -44,6 +45,7 @@ type CrossNamespaceObjectReference struct {
// MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels // 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 // 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 `*`.
// +optional // +optional
MatchLabels map[string]string `json:"matchLabels,omitempty"` 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 {key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements 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 type: object
name: name:
description: Name of the referent. description: Name of the referent. If multiple resources are
targeted `*` may be set.
maxLength: 53 maxLength: 53
minLength: 1 minLength: 1
type: string type: string

View File

@ -288,10 +288,11 @@ spec:
{key,value} in the matchLabels map is equivalent to an element {key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements 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 type: object
name: name:
description: Name of the referent. description: Name of the referent. If multiple resources are
targeted `*` may be set.
maxLength: 53 maxLength: 53
minLength: 1 minLength: 1
type: string type: string

View File

@ -741,7 +741,8 @@ string
</em> </em>
</td> </td>
<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> </td>
</tr> </tr>
<tr> <tr>
@ -767,7 +768,8 @@ map[string]string
<em>(Optional)</em> <em>(Optional)</em>
<p>MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels <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 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> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -635,9 +635,35 @@ A resource entry contains the following fields:
`GitRepository`, `Kustomization`, `HelmRelease`, `HelmChart`, `GitRepository`, `Kustomization`, `HelmRelease`, `HelmChart`,
`HelmRepository`, `ImageRepository`, `ImagePolicy`, `ImageUpdateAutomation` `HelmRepository`, `ImageRepository`, `ImagePolicy`, `ImageUpdateAutomation`
and `OCIRepository`. 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`. - `namespace` (Optional): The Flux Custom Resource `.metadata.namespace`.
When not specified, the Receiver's `.metadata.namespace` is used instead. 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 **Note:** Cross-namespace references [can be disabled for security

View File

@ -23,13 +23,16 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/google/go-github/v41/github" "github.com/google/go-github/v41/github"
"github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
@ -38,21 +41,23 @@ import (
apiv1 "github.com/fluxcd/notification-controller/api/v1beta2" apiv1 "github.com/fluxcd/notification-controller/api/v1beta2"
) )
func Test_validate(t *testing.T) { func Test_handlePayload(t *testing.T) {
type hashOpts struct { type hashOpts struct {
calculate bool calculate bool
header string header string
} }
tests := []struct { tests := []struct {
name string name string
hashOpts hashOpts hashOpts hashOpts
headers map[string]string headers map[string]string
payload map[string]interface{} payload map[string]interface{}
receiver *apiv1.Receiver receiver *apiv1.Receiver
receiverType string receiverType string
secret *corev1.Secret secret *corev1.Secret
expectedErr bool resources []client.Object
expectedResourcesAnnotated int
expectedResponseCode int
}{ }{
{ {
name: "Generic receiver", name: "Generic receiver",
@ -66,6 +71,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
secret: &corev1.Secret{ secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -75,7 +84,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"), "token": []byte("token"),
}, },
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "gitlab receiver", name: "gitlab receiver",
@ -89,6 +98,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
headers: map[string]string{ headers: map[string]string{
"X-Gitlab-Token": "token", "X-Gitlab-Token": "token",
@ -101,7 +114,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"), "token": []byte("token"),
}, },
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "github receiver", name: "github receiver",
@ -115,6 +128,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
hashOpts: hashOpts{ hashOpts: hashOpts{
calculate: true, calculate: true,
@ -134,7 +151,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"), "token": []byte("token"),
}, },
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "generic hmac receiver", name: "generic hmac receiver",
@ -148,6 +165,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
hashOpts: hashOpts{ hashOpts: hashOpts{
calculate: true, calculate: true,
@ -164,7 +185,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"), "token": []byte("token"),
}, },
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "bitbucket receiver", name: "bitbucket receiver",
@ -179,6 +200,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
hashOpts: hashOpts{ hashOpts: hashOpts{
calculate: true, calculate: true,
@ -196,7 +221,7 @@ func Test_validate(t *testing.T) {
"token": []byte("token"), "token": []byte("token"),
}, },
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "quay receiver", name: "quay receiver",
@ -210,6 +235,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
secret: &corev1.Secret{ secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -225,7 +254,7 @@ func Test_validate(t *testing.T) {
"v0.0.1", "v0.0.1",
}, },
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "harbor receiver", name: "harbor receiver",
@ -239,6 +268,10 @@ func Test_validate(t *testing.T) {
Name: "token", Name: "token",
}, },
}, },
Status: apiv1.ReceiverStatus{
WebhookPath: apiv1.ReceiverWebhookPath,
Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
},
}, },
secret: &corev1.Secret{ secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -251,7 +284,7 @@ func Test_validate(t *testing.T) {
headers: map[string]string{ headers: map[string]string{
"Authorization": "token", "Authorization": "token",
}, },
expectedErr: false, expectedResponseCode: http.StatusOK,
}, },
{ {
name: "missing secret", name: "missing secret",
@ -265,8 +298,330 @@ func Test_validate(t *testing.T) {
Name: "non-existing", 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
g := gomega.NewGomegaWithT(t)
builder := fake.NewClientBuilder() builder := fake.NewClientBuilder()
builder.WithScheme(scheme) builder.WithScheme(scheme)
builder.WithObjects(tt.receiver)
if tt.receiver != nil {
builder.WithObjects(tt.receiver)
}
builder.WithObjects(tt.resources...)
if tt.secret != nil { if tt.secret != nil {
builder.WithObjects(tt.secret) builder.WithObjects(tt.secret)
} }
@ -295,7 +657,7 @@ func Test_validate(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("error marshalling test payload: '%s'", err) 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 { for key, val := range tt.headers {
req.Header.Set(key, val) 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))) req.Header.Set(tt.hashOpts.header, "sha256="+hex.EncodeToString(mac.Sum(nil)))
} }
err = s.validate(context.Background(), *tt.receiver, req) rr := httptest.NewRecorder()
if tt.expectedErr && err == nil { handler := s.handlePayload()
t.Errorf("expected error but got %s", err) 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 { g.Expect(annotatedResources).To(gomega.Equal(tt.expectedResourcesAnnotated))
t.Errorf("unexpected error: '%s'", err)
}
}) })
} }
} }

View File

@ -32,6 +32,7 @@ import (
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/conditions"
"github.com/go-logr/logr"
"github.com/google/go-github/v41/github" "github.com/google/go-github/v41/github"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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. // defaultFluxAPIVersions is a map of Flux API kinds to their API versions.
var defaultFluxAPIVersions = map[string]string{ var defaultFluxAPIVersions = map[string]string{
"Bucket": "source.toolkit.fluxcd.io/v1beta2", "Bucket": "source.toolkit.fluxcd.io/v1beta2",
"HelmChart": "source.toolkit.fluxcd.io/v1beta2",
"HelmRepository": "source.toolkit.fluxcd.io/v1beta2", "HelmRepository": "source.toolkit.fluxcd.io/v1beta2",
"GitRepository": "source.toolkit.fluxcd.io/v1beta2", "GitRepository": "source.toolkit.fluxcd.io/v1beta2",
"OCIRepository": "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 { for _, resource := range receiver.Spec.Resources {
if err := s.annotate(ctx, resource, receiver.Namespace); err != nil { if err := s.requestReconciliation(ctx, logger, resource, receiver.Namespace); err != nil {
logger.Error(err, fmt.Sprintf("unable to annotate resource '%s/%s.%s'", logger.Error(err, "unable to process resource")
resource.Kind, resource.Name, resource.Namespace))
withErrors = true 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 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 namespace := defaultNamespace
if resource.Namespace != "" { if resource.Namespace != "" {
namespace = resource.Namespace namespace = resource.Namespace
} }
objectKey := client.ObjectKey{
Namespace: namespace,
Name: resource.Name,
}
apiVersion := resource.APIVersion apiVersion := resource.APIVersion
if apiVersion == "" { if apiVersion == "" {
@ -367,6 +362,46 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNames
group, version := getGroupVersion(apiVersion) 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 := &metav1.PartialObjectMetadata{}
u.SetGroupVersionKind(schema.GroupVersionKind{ u.SetGroupVersionKind(schema.GroupVersionKind{
Group: group, Group: group,
@ -374,19 +409,42 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource apiv1.CrossNames
Version: version, Version: version,
}) })
objectKey := client.ObjectKey{
Namespace: namespace,
Name: resource.Name,
}
if err := s.kubeClient.Get(ctx, objectKey, u); err != nil { if err := s.kubeClient.Get(ctx, objectKey, u); err != nil {
return fmt.Errorf("unable to read %s '%s' error: %w", resource.Kind, objectKey, err) return fmt.Errorf("unable to read %s '%s' error: %w", resource.Kind, objectKey, err)
} }
patch := client.MergeFrom(u.DeepCopy()) err := s.annotate(ctx, u)
sourceAnnotations := u.GetAnnotations() 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 { if sourceAnnotations == nil {
sourceAnnotations = make(map[string]string) sourceAnnotations = make(map[string]string)
} }
sourceAnnotations[meta.ReconcileRequestAnnotation] = metav1.Now().String() sourceAnnotations[meta.ReconcileRequestAnnotation] = metav1.Now().String()
u.SetAnnotations(sourceAnnotations) resource.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) 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 return nil