Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/v1/reference_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/api/v1/notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,9 @@ string
</td>
<td>
<em>(Optional)</em>
<p>Namespace of the referent</p>
<p>Namespace of the referent
If multiple resources across all namespaces are targeted <code>*</code> may be set,
which requires the name to also be set to <code>*</code>.</p>
</td>
</tr>
<tr>
Expand Down
135 changes: 134 additions & 1 deletion internal/server/receiver_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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")
}
Expand Down
19 changes: 13 additions & 6 deletions internal/server/receiver_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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)
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
Expand Down