diff --git a/api/v1/reference_types.go b/api/v1/reference_types.go
index 2dcb3c116..8b0dbfda6 100644
--- a/api/v1/reference_types.go
+++ b/api/v1/reference_types.go
@@ -36,6 +36,8 @@ type CrossNamespaceObjectReference struct {
Name string `json:"name"`
// Namespace of the referent
+ // If multiple resources across all namespaces are targeted `*` may be set,
+ // which requires the name to also be set to `*`.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:Optional
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
index 89f03d2f1..3108daf41 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
@@ -116,7 +116,10 @@ spec:
minLength: 1
type: string
namespace:
- description: Namespace of the referent
+ description: |-
+ Namespace of the referent
+ If multiple resources across all namespaces are targeted `*` may be set,
+ which requires the name to also be set to `*`.
maxLength: 253
minLength: 1
type: string
@@ -335,7 +338,10 @@ spec:
minLength: 1
type: string
namespace:
- description: Namespace of the referent
+ description: |-
+ Namespace of the referent
+ If multiple resources across all namespaces are targeted `*` may be set,
+ which requires the name to also be set to `*`.
maxLength: 253
minLength: 1
type: string
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
index 6a544de10..1d92c274b 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
@@ -216,7 +216,10 @@ spec:
minLength: 1
type: string
namespace:
- description: Namespace of the referent
+ description: |-
+ Namespace of the referent
+ If multiple resources across all namespaces are targeted `*` may be set,
+ which requires the name to also be set to `*`.
maxLength: 253
minLength: 1
type: string
@@ -457,7 +460,10 @@ spec:
minLength: 1
type: string
namespace:
- description: Namespace of the referent
+ description: |-
+ Namespace of the referent
+ If multiple resources across all namespaces are targeted `*` may be set,
+ which requires the name to also be set to `*`.
maxLength: 253
minLength: 1
type: string
diff --git a/docs/api/v1/notification.md b/docs/api/v1/notification.md
index 344e6a28f..231d14c57 100644
--- a/docs/api/v1/notification.md
+++ b/docs/api/v1/notification.md
@@ -273,7 +273,9 @@ string
(Optional)
- Namespace of the referent
+Namespace of the referent
+If multiple resources across all namespaces are targeted * may be set,
+which requires the name to also be set to *.
|
diff --git a/internal/server/receiver_handler_test.go b/internal/server/receiver_handler_test.go
index afdcd8d30..cf8d60cba 100644
--- a/internal/server/receiver_handler_test.go
+++ b/internal/server/receiver_handler_test.go
@@ -552,6 +552,139 @@ func Test_handlePayload(t *testing.T) {
expectedResourcesAnnotated: 1,
expectedResponseCode: http.StatusOK,
},
+ {
+ name: "annotating resources by label match across all namespaces",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: &meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.ReceiverResource{
+ {CrossNamespaceObjectReference: apiv1.CrossNamespaceObjectReference{
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "*",
+ Namespace: "*",
+ MatchLabels: map[string]string{
+ "label": "match",
+ },
+ }},
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: testSecretWithToken,
+ resources: []client.Object{
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource-ns-a",
+ Namespace: "namespace-a",
+ Labels: map[string]string{
+ "label": "match",
+ },
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource-ns-b",
+ Namespace: "namespace-b",
+ Labels: map[string]string{
+ "label": "match",
+ },
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dummy-resource-no-match",
+ Namespace: "namespace-c",
+ Labels: map[string]string{
+ "label": "does-not-match",
+ },
+ },
+ },
+ },
+ expectedResourcesAnnotated: 2,
+ expectedResponseCode: http.StatusOK,
+ },
+ {
+ name: "cannot annotate across all namespaces if namespace is * but name is not *",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: &meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.ReceiverResource{
+ {CrossNamespaceObjectReference: apiv1.CrossNamespaceObjectReference{
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "dummy-resource",
+ Namespace: "*",
+ }},
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: testSecretWithToken,
+ expectedResponseCode: http.StatusInternalServerError,
+ },
+ {
+ name: "cannot annotate across all namespaces if cross-namespace refs are disabled",
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: &meta.LocalObjectReference{
+ Name: "token",
+ },
+ Resources: []apiv1.ReceiverResource{
+ {CrossNamespaceObjectReference: apiv1.CrossNamespaceObjectReference{
+ APIVersion: apiv1.GroupVersion.String(),
+ Kind: apiv1.ReceiverKind,
+ Name: "*",
+ Namespace: "*",
+ MatchLabels: map[string]string{
+ "label": "match",
+ },
+ }},
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: testSecretWithToken,
+ noCrossNamespaceRefs: true,
+ expectedResponseCode: http.StatusInternalServerError,
+ },
{
name: "annotating resource by name",
receiver: &apiv1.Receiver{
@@ -1511,7 +1644,7 @@ func Test_handlePayload(t *testing.T) {
logger: logger.NewLogger(logger.Options{}),
kubeClient: client,
noCrossNamespaceRefs: tt.noCrossNamespaceRefs,
- gcrTokenValidator: func(_ context.Context, bearer string, expectedEmail string, expectedAudience string) error {
+ gcrTokenValidator: func(_ context.Context, bearer, expectedEmail, expectedAudience string) error {
if bearer == "" {
return fmt.Errorf("missing authorization header")
}
diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go
index 323716efc..bb1c3c56f 100644
--- a/internal/server/receiver_handlers.go
+++ b/internal/server/receiver_handlers.go
@@ -186,7 +186,7 @@ func (s *ReceiverServer) notifyDynamicResources(ctx context.Context, logger logr
}
logger.V(1).Info(fmt.Sprintf("annotate resources by matchLabel for kind %q in %q",
- resource.Kind, namespace), "matchLabels", resource.MatchLabels)
+ resource.Kind, resource.Namespace), "matchLabels", resource.MatchLabels)
var resources metav1.PartialObjectMetadataList
resources.SetGroupVersionKind(schema.GroupVersionKind{
@@ -195,10 +195,14 @@ func (s *ReceiverServer) notifyDynamicResources(ctx context.Context, logger logr
Version: version,
})
- if err := s.kubeClient.List(ctx, &resources,
- client.InNamespace(namespace),
- client.MatchingLabels(resource.MatchLabels),
- ); err != nil {
+ // List resources matching the labels in the specified namespace
+ // or across all namespaces if namespace is '*' (kube client list all resources when namespace is not specified).
+ listOpts := []client.ListOption{client.MatchingLabels(resource.MatchLabels)}
+ if namespace != "*" {
+ listOpts = append(listOpts, client.InNamespace(namespace))
+ }
+
+ if err := s.kubeClient.List(ctx, &resources, listOpts...); err != nil {
return fmt.Errorf("failed listing resources in namespace %q by matching labels %q: %w", namespace, resource.MatchLabels, err)
}
@@ -571,6 +575,9 @@ func (s *ReceiverServer) requestReconciliation(ctx context.Context, logger logr.
if s.noCrossNamespaceRefs && resource.Namespace != defaultNamespace {
return fmt.Errorf("cross-namespace references are not allowed")
}
+ if resource.Namespace == "*" && resource.Name != "*" {
+ return fmt.Errorf("name must be set to '*' when namespace is set to '*'")
+ }
namespace = resource.Namespace
}
@@ -623,7 +630,7 @@ func (s *ReceiverServer) annotate(ctx context.Context, resource *metav1.PartialO
// authenticateGCRRequest validates the OIDC ID token according to
// https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions#go.
-func authenticateGCRRequest(ctx context.Context, bearer string, expectedEmail string, expectedAudience string) error {
+func authenticateGCRRequest(ctx context.Context, bearer, expectedEmail, expectedAudience string) error {
const bearerPrefix = "Bearer "
if !strings.HasPrefix(bearer, bearerPrefix) {
return fmt.Errorf("the Authorization header is missing or malformed")