feat: support multi receiver by matchLabels
Signed-off-by: Raffael Sahli <raffael.sahli@doodle.com>
This commit is contained in:
parent
b841b2d02a
commit
57f62f400c
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 “key”, the
|
||||
operator is “In”, and the values array contains only “value”. The requirements are ANDed.</p>
|
||||
operator is “In”, and the values array contains only “value”. The requirements are ANDed.
|
||||
MatchLabels requires the name to be set to <code>*</code>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue