From 155a39a2534500b383b03157dd6b9d74105dbf3d Mon Sep 17 00:00:00 2001 From: Steffen Baarsgaard Date: Sun, 2 Nov 2025 17:27:46 +0100 Subject: [PATCH 1/2] fix: Split Route into PartialRoute containing top-level fields Removes a fields from .spec.route preventing users from defining them and ending up with an invalid API request/DTO Additionally, .spec.provenance is deprecated as it seemingly does nothing --- .../grafananotificationpolicy_types.go | 72 ++++--- .../grafananotificationpolicy_types_test.go | 43 +++-- api/v1beta1/zz_generated.deepcopy.go | 76 +++++--- controllers/notificationpolicy_controller.go | 10 +- .../notificationpolicy_controller_test.go | 182 +++++++++++------- 5 files changed, 233 insertions(+), 150 deletions(-) diff --git a/api/v1beta1/grafananotificationpolicy_types.go b/api/v1beta1/grafananotificationpolicy_types.go index f1ec57f9f..6a16c515e 100644 --- a/api/v1beta1/grafananotificationpolicy_types.go +++ b/api/v1beta1/grafananotificationpolicy_types.go @@ -29,7 +29,7 @@ type GrafanaNotificationPolicySpec struct { GrafanaCommonSpec `json:",inline"` // Routes for alerts to match against - Route *Route `json:"route"` + Route *PartialRoute `json:"route"` // Whether to enable or disable editing of the notification policy in Grafana UI // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" @@ -37,10 +37,7 @@ type GrafanaNotificationPolicySpec struct { Editable *bool `json:"editable,omitempty"` } -type Route struct { - // continue - Continue bool `json:"continue,omitempty"` - +type PartialRoute struct { // group by GroupBy []string `json:"group_by,omitempty"` @@ -50,23 +47,6 @@ type Route struct { // group wait GroupWait string `json:"group_wait,omitempty"` - // match re - MatchRe models.MatchRegexps `json:"match_re,omitempty"` - - // matchers - Matchers Matchers `json:"matchers,omitempty"` - - // mute time intervals - MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"` - - ActiveTimeIntervals []string `json:"active_time_intervals,omitempty"` - - // object matchers - ObjectMatchers models.ObjectMatchers `json:"object_matchers,omitempty"` - - // provenance - Provenance models.Provenance `json:"provenance,omitempty"` - // receiver // +kubebuilder:validation:MinLength=1 Receiver string `json:"receiver"` @@ -82,6 +62,31 @@ type Route struct { // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless Routes []*Route `json:"routes,omitempty"` + + // Deprecated: Does nothing + Provenance models.Provenance `json:"provenance,omitempty"` +} + +type Route struct { + PartialRoute `json:",inline"` + + // continue + Continue bool `json:"continue,omitempty"` + + // match re + MatchRe models.MatchRegexps `json:"match_re,omitempty"` + + // matchers + Matchers Matchers `json:"matchers,omitempty"` + + // object matchers + ObjectMatchers models.ObjectMatchers `json:"object_matchers,omitempty"` + + // mute time intervals + MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"` + + // active time intervals + ActiveTimeIntervals []string `json:"active_time_intervals,omitempty"` } type Matcher struct { @@ -124,7 +129,6 @@ func (r *Route) ToModelRoute() *models.Route { MuteTimeIntervals: r.MuteTimeIntervals, ActiveTimeIntervals: r.ActiveTimeIntervals, ObjectMatchers: r.ObjectMatchers, - Provenance: r.Provenance, Receiver: r.Receiver, RepeatInterval: r.RepeatInterval, Routes: make([]*models.Route, len(r.Routes)), @@ -136,15 +140,31 @@ func (r *Route) ToModelRoute() *models.Route { return out } +func (r *PartialRoute) ToModelRoute() *models.Route { + out := &models.Route{ + GroupBy: r.GroupBy, + GroupInterval: r.GroupInterval, + GroupWait: r.GroupWait, + Receiver: r.Receiver, + RepeatInterval: r.RepeatInterval, + Routes: make([]*models.Route, len(r.Routes)), + } + for i, v := range r.Routes { + out.Routes[i] = v.ToModelRoute() + } + + return out +} + // selectorMutuallyExclusive checks if a single route satisfies the mutual exclusivity constraint // for checking the entire route including nested routes, use IsRouteSelectorMutuallyExclusive -func (r *Route) selectorMutuallyExclusive() bool { +func (r *PartialRoute) selectorMutuallyExclusive() bool { return !(r.RouteSelector != nil && len(r.Routes) > 0) // nolint:staticcheck } // IsRouteSelectorMutuallyExclusive returns true when the route and all its sub-routes // satisfy the constraint of routes and routeSelector being mutually exclusive -func (r *Route) IsRouteSelectorMutuallyExclusive() bool { +func (r *PartialRoute) IsRouteSelectorMutuallyExclusive() bool { if !r.selectorMutuallyExclusive() { return false } @@ -160,7 +180,7 @@ func (r *Route) IsRouteSelectorMutuallyExclusive() bool { } // HasRouteSelector checks if the given Route or any of its nested Routes has a RouteSelector -func (r *Route) HasRouteSelector() bool { +func (r *PartialRoute) HasRouteSelector() bool { if r.RouteSelector != nil { return true } diff --git a/api/v1beta1/grafananotificationpolicy_types_test.go b/api/v1beta1/grafananotificationpolicy_types_test.go index 01abf1ef8..b08272097 100644 --- a/api/v1beta1/grafananotificationpolicy_types_test.go +++ b/api/v1beta1/grafananotificationpolicy_types_test.go @@ -38,13 +38,10 @@ func newNotificationPolicy(name string, editable *bool) *GrafanaNotificationPoli }, }, }, - Route: &Route{ - Continue: false, - Receiver: "grafana-default-email", - GroupBy: []string{"group_name", "alert_name"}, - MuteTimeIntervals: []string{}, - ActiveTimeIntervals: []string{}, - Routes: []*Route{}, + Route: &PartialRoute{ + Receiver: "grafana-default-email", + GroupBy: []string{"group_name", "alert_name"}, + Routes: []*Route{}, }, }, } @@ -91,24 +88,24 @@ var _ = Describe("NotificationPolicy type", func() { func TestIsRouteSelectorMutuallyExclusive(t *testing.T) { tests := []struct { name string - route *Route + route *PartialRoute expected bool }{ { name: "Empty route", - route: &Route{}, + route: &PartialRoute{}, expected: true, }, { name: "Route with only RouteSelector", - route: &Route{ + route: &PartialRoute{ RouteSelector: &metav1.LabelSelector{}, }, expected: true, }, { name: "Route with only sub-routes", - route: &Route{ + route: &PartialRoute{ Routes: []*Route{ {}, {}, @@ -118,7 +115,7 @@ func TestIsRouteSelectorMutuallyExclusive(t *testing.T) { }, { name: "Route with both RouteSelector and sub-routes", - route: &Route{ + route: &PartialRoute{ RouteSelector: &metav1.LabelSelector{}, Routes: []*Route{ {}, @@ -128,14 +125,18 @@ func TestIsRouteSelectorMutuallyExclusive(t *testing.T) { }, { name: "Nested routes with mutual exclusivity", - route: &Route{ + route: &PartialRoute{ Routes: []*Route{ { - RouteSelector: &metav1.LabelSelector{}, + PartialRoute: PartialRoute{ + RouteSelector: &metav1.LabelSelector{}, + }, }, { - Routes: []*Route{ - {}, + PartialRoute: PartialRoute{ + Routes: []*Route{ + {}, + }, }, }, }, @@ -144,12 +145,14 @@ func TestIsRouteSelectorMutuallyExclusive(t *testing.T) { }, { name: "Nested routes without mutual exclusivity", - route: &Route{ + route: &PartialRoute{ Routes: []*Route{ { - RouteSelector: &metav1.LabelSelector{}, - Routes: []*Route{ - {}, + PartialRoute: PartialRoute{ + RouteSelector: &metav1.LabelSelector{}, + Routes: []*Route{ + {}, + }, }, }, }, diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 93c461b8b..0615da086 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1585,7 +1585,7 @@ func (in *GrafanaNotificationPolicySpec) DeepCopyInto(out *GrafanaNotificationPo in.GrafanaCommonSpec.DeepCopyInto(&out.GrafanaCommonSpec) if in.Route != nil { in, out := &in.Route, &out.Route - *out = new(Route) + *out = new(PartialRoute) (*in).DeepCopyInto(*out) } if in.Editable != nil { @@ -2307,6 +2307,42 @@ func (in *OperatorReconcileVars) DeepCopy() *OperatorReconcileVars { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PartialRoute) DeepCopyInto(out *PartialRoute) { + *out = *in + if in.GroupBy != nil { + in, out := &in.GroupBy, &out.GroupBy + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RouteSelector != nil { + in, out := &in.RouteSelector, &out.RouteSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]*Route, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Route) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PartialRoute. +func (in *PartialRoute) DeepCopy() *PartialRoute { + if in == nil { + return nil + } + out := new(PartialRoute) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PersistentVolumeClaimV1) DeepCopyInto(out *PersistentVolumeClaimV1) { *out = *in @@ -2436,11 +2472,7 @@ func (in *Record) DeepCopy() *Record { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Route) DeepCopyInto(out *Route) { *out = *in - if in.GroupBy != nil { - in, out := &in.GroupBy, &out.GroupBy - *out = make([]string, len(*in)) - copy(*out, *in) - } + in.PartialRoute.DeepCopyInto(&out.PartialRoute) if in.MatchRe != nil { in, out := &in.MatchRe, &out.MatchRe *out = make(models.MatchRegexps, len(*in)) @@ -2459,16 +2491,6 @@ func (in *Route) DeepCopyInto(out *Route) { } } } - if in.MuteTimeIntervals != nil { - in, out := &in.MuteTimeIntervals, &out.MuteTimeIntervals - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.ActiveTimeIntervals != nil { - in, out := &in.ActiveTimeIntervals, &out.ActiveTimeIntervals - *out = make([]string, len(*in)) - copy(*out, *in) - } if in.ObjectMatchers != nil { in, out := &in.ObjectMatchers, &out.ObjectMatchers *out = make(models.ObjectMatchers, len(*in)) @@ -2480,21 +2502,15 @@ func (in *Route) DeepCopyInto(out *Route) { } } } - if in.RouteSelector != nil { - in, out := &in.RouteSelector, &out.RouteSelector - *out = new(metav1.LabelSelector) - (*in).DeepCopyInto(*out) + if in.MuteTimeIntervals != nil { + in, out := &in.MuteTimeIntervals, &out.MuteTimeIntervals + *out = make([]string, len(*in)) + copy(*out, *in) } - if in.Routes != nil { - in, out := &in.Routes, &out.Routes - *out = make([]*Route, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(Route) - (*in).DeepCopyInto(*out) - } - } + if in.ActiveTimeIntervals != nil { + in, out := &in.ActiveTimeIntervals, &out.ActiveTimeIntervals + *out = make([]string, len(*in)) + copy(*out, *in) } } diff --git a/controllers/notificationpolicy_controller.go b/controllers/notificationpolicy_controller.go index 37b873e1b..04dc8474e 100644 --- a/controllers/notificationpolicy_controller.go +++ b/controllers/notificationpolicy_controller.go @@ -215,9 +215,9 @@ func assembleNotificationPolicyRoutes(ctx context.Context, k8sClient client.Clie // so we can detect loops visitedChilds := make(map[string]bool) - var assembleRoute func(*v1beta1.Route) error + var assembleRoute func(*v1beta1.PartialRoute) error - assembleRoute = func(route *v1beta1.Route) error { + assembleRoute = func(route *v1beta1.PartialRoute) error { if route.RouteSelector != nil { routes, err := getMatchingNotificationPolicyRoutes(ctx, k8sClient, route.RouteSelector, namespace) if err != nil { @@ -243,7 +243,7 @@ func assembleNotificationPolicyRoutes(ctx context.Context, k8sClient client.Clie visitedChilds[key] = true // Recursively assemble the matched route - if err := assembleRoute(&matchedRoute.Spec.Route); err != nil { + if err := assembleRoute(&matchedRoute.Spec.PartialRoute); err != nil { return err } @@ -254,7 +254,7 @@ func assembleNotificationPolicyRoutes(ctx context.Context, k8sClient client.Clie } else { // if no RouteSelector is specified, process inline routes, as they are mutually exclusive for i, inlineRoute := range route.Routes { - if err := assembleRoute(inlineRoute); err != nil { + if err := assembleRoute(&inlineRoute.PartialRoute); err != nil { return err } @@ -379,7 +379,7 @@ func (r *GrafanaNotificationPolicyReconciler) SetupWithManager(mgr ctrl.Manager) }() // check if notification policy route is valid - if !npr.Spec.Route.IsRouteSelectorMutuallyExclusive() { + if !npr.Spec.PartialRoute.IsRouteSelectorMutuallyExclusive() { setInvalidSpecMutuallyExclusive(&npr.Status.Conditions, npr.Generation) return nil } diff --git a/controllers/notificationpolicy_controller_test.go b/controllers/notificationpolicy_controller_test.go index 07ef30df6..e386b37bf 100644 --- a/controllers/notificationpolicy_controller_test.go +++ b/controllers/notificationpolicy_controller_test.go @@ -56,7 +56,7 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { name: "Simple assembly with one level of routes", notificationPolicy: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", RouteSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"tier": "first"}, @@ -73,7 +73,9 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-A-receiver", + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + }, Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, }, }, @@ -81,14 +83,14 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, want: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", - Routes: []*v1beta1.Route{ - { + Routes: []*v1beta1.Route{{ + PartialRoute: v1beta1.PartialRoute{ Receiver: "team-A-receiver", - Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, }, - }, + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, + }}, }, }, }, @@ -104,7 +106,7 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { GrafanaCommonSpec: v1beta1.GrafanaCommonSpec{ AllowCrossNamespaceImport: false, }, - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", RouteSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"tier": "first"}, @@ -121,8 +123,10 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + }, }, }, }, @@ -134,8 +138,10 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-A-receiver-other-namespace", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver-other-namespace", + }, }, }, }, @@ -145,12 +151,14 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { Namespace: "default", }, Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", Routes: []*v1beta1.Route{ { - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + }, }, }, }, @@ -162,7 +170,7 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { name: "Assembly with nested routes", notificationPolicy: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", RouteSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"tier": "first"}, @@ -179,10 +187,12 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tier": "second"}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "second"}, + }, }, }, }, @@ -195,7 +205,9 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-B-receiver", + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-B-receiver", + }, Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "B", IsEqual: true}}, }, }, @@ -203,16 +215,20 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, want: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", Routes: []*v1beta1.Route{ { - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, - Routes: []*v1beta1.Route{ - { - Receiver: "team-B-receiver", - Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "B", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + Routes: []*v1beta1.Route{ + { + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "B", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-B-receiver", + }, + }, }, }, }, @@ -226,21 +242,25 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { name: "Assembly with nested routes and multiple RouteSelectors inside Routes", notificationPolicy: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", Routes: []*v1beta1.Route{ { - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tier": "second", "team": "A"}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "second", "team": "A"}, + }, }, }, { - Receiver: "team-B-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "B", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tier": "second", "team": "B"}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-B-receiver", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "second", "team": "B"}, + }, }, }, }, @@ -256,8 +276,10 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "project-X-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("project"), Value: "X", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "project-X-receiver", + }, }, }, }, @@ -269,34 +291,44 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "project-Y-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("project"), Value: "Y", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "project-Y-receiver", + }, }, }, }, }, want: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", Routes: []*v1beta1.Route{ { - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, - Routes: []*v1beta1.Route{ - { - Receiver: "project-X-receiver", - Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("project"), Value: "X", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + Routes: []*v1beta1.Route{ + { + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("project"), Value: "X", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "project-X-receiver", + }, + }, }, }, }, { - Receiver: "team-B-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "B", IsEqual: true}}, - Routes: []*v1beta1.Route{ - { - Receiver: "project-Y-receiver", - Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("project"), Value: "Y", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-B-receiver", + Routes: []*v1beta1.Route{ + { + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("project"), Value: "Y", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "project-Y-receiver", + }, + }, }, }, }, @@ -310,7 +342,7 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { name: "Detect loop in routes", notificationPolicy: &v1beta1.GrafanaNotificationPolicy{ Spec: v1beta1.GrafanaNotificationPolicySpec{ - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", RouteSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"tier": "first"}, @@ -327,10 +359,12 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-A-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "A", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tier": "second"}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-A-receiver", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "second"}, + }, }, }, }, @@ -343,10 +377,12 @@ func TestAssembleNotificationPolicyRoutes(t *testing.T) { }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "team-B-receiver", Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "B", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tier": "first"}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "team-B-receiver", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "first"}, + }, }, }, }, @@ -394,7 +430,7 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke Conditions", func() { meta: objectMetaSuspended, spec: v1beta1.GrafanaNotificationPolicySpec{ GrafanaCommonSpec: commonSpecSuspended, - Route: &v1beta1.Route{Receiver: "default-receiver"}, + Route: &v1beta1.PartialRoute{Receiver: "default-receiver"}, }, want: metav1.Condition{ Type: conditionSuspended, @@ -406,7 +442,7 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke Conditions", func() { meta: objectMetaNoMatchingInstances, spec: v1beta1.GrafanaNotificationPolicySpec{ GrafanaCommonSpec: commonSpecNoMatchingInstances, - Route: &v1beta1.Route{Receiver: "default-receiver"}, + Route: &v1beta1.PartialRoute{Receiver: "default-receiver"}, }, want: metav1.Condition{ Type: conditionNoMatchingInstance, @@ -419,7 +455,7 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke Conditions", func() { meta: objectMetaApplyFailed, spec: v1beta1.GrafanaNotificationPolicySpec{ GrafanaCommonSpec: commonSpecApplyFailed, - Route: &v1beta1.Route{Receiver: "default-receiver"}, + Route: &v1beta1.PartialRoute{Receiver: "default-receiver"}, }, want: metav1.Condition{ Type: conditionNotificationPolicySynchronized, @@ -432,10 +468,12 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke Conditions", func() { meta: objectMetaInvalidSpec, spec: v1beta1.GrafanaNotificationPolicySpec{ GrafanaCommonSpec: commonSpecInvalidSpec, - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "default-receiver", Routes: []*v1beta1.Route{{ - Receiver: "default-receiver", + PartialRoute: v1beta1.PartialRoute{ + Receiver: "default-receiver", + }, }}, RouteSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{}, @@ -453,7 +491,7 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke Conditions", func() { meta: objectMetaSynchronized, spec: v1beta1.GrafanaNotificationPolicySpec{ GrafanaCommonSpec: commonSpecSynchronized, - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "grafana-default-email", }, }, @@ -492,14 +530,16 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke LoopDetected Condition" MatchLabels: map[string]string{"loop-detected": "test"}, }, }, - Route: &v1beta1.Route{ + Route: &v1beta1.PartialRoute{ Receiver: "grafana-default-email", Routes: []*v1beta1.Route{{ - Receiver: "grafana-default-email", - Matchers: v1beta1.Matchers{{Name: stringP("team"), Value: "a", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"team-a": "child"}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "grafana-default-email", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"team-a": "child"}, + }, }, + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "a", IsEqual: true}}, }}, }, }, @@ -512,10 +552,12 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke LoopDetected Condition" }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "grafana-default-email", - Matchers: v1beta1.Matchers{{Name: stringP("team"), Value: "b", IsEqual: true}}, - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"team-b": "child"}, + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "b", IsEqual: true}}, + PartialRoute: v1beta1.PartialRoute{ + Receiver: "grafana-default-email", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"team-b": "child"}, + }, }, }, }, @@ -528,10 +570,12 @@ var _ = Describe("NotificationPolicy Reconciler: Provoke LoopDetected Condition" }, Spec: v1beta1.GrafanaNotificationPolicyRouteSpec{ Route: v1beta1.Route{ - Receiver: "grafana-default-email", - Matchers: v1beta1.Matchers{{Name: stringP("team"), Value: "b", IsEqual: true}}, // Also matches team b - RouteSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"team-b": "child"}, + Matchers: v1beta1.Matchers{&v1beta1.Matcher{Name: stringP("team"), Value: "b", IsEqual: true}}, // Also matches team b + PartialRoute: v1beta1.PartialRoute{ + Receiver: "grafana-default-email", + RouteSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"team-b": "child"}, + }, }, }, }, From 5eaf360430aa5bb514be19e221369c881534095d Mon Sep 17 00:00:00 2001 From: Steffen Baarsgaard Date: Sun, 2 Nov 2025 17:28:05 +0100 Subject: [PATCH 2/2] chore: Generate crds/docs --- ...eatly.org_grafananotificationpolicies.yaml | 51 +--------- ...y.org_grafananotificationpolicyroutes.yaml | 3 +- ...eatly.org_grafananotificationpolicies.yaml | 51 +--------- ...y.org_grafananotificationpolicyroutes.yaml | 3 +- deploy/kustomize/base/crds.yaml | 54 +---------- docs/docs/api.md | 96 +------------------ 6 files changed, 12 insertions(+), 246 deletions(-) diff --git a/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml b/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml index fe6208cfb..0daa917b0 100644 --- a/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafananotificationpolicies.yaml @@ -120,13 +120,6 @@ spec: route: description: Routes for alerts to match against properties: - active_time_intervals: - items: - type: string - type: array - continue: - description: continue - type: boolean group_by: description: group by items: @@ -138,50 +131,8 @@ spec: group_wait: description: group wait type: string - match_re: - additionalProperties: - type: string - description: match re - type: object - matchers: - description: matchers - items: - properties: - isEqual: - description: is equal - type: boolean - isRegex: - description: is regex - type: boolean - name: - description: name - type: string - value: - description: value - type: string - required: - - isRegex - - value - type: object - type: array - mute_time_intervals: - description: mute time intervals - items: - type: string - type: array - object_matchers: - description: object matchers - items: - description: |- - ObjectMatcher ObjectMatcher is a matcher that can be used to filter alerts. - - swagger:model ObjectMatcher - items: - type: string - type: array - type: array provenance: - description: provenance + description: 'Deprecated: Does nothing' type: string receiver: description: receiver diff --git a/config/crd/bases/grafana.integreatly.org_grafananotificationpolicyroutes.yaml b/config/crd/bases/grafana.integreatly.org_grafananotificationpolicyroutes.yaml index 1f11c0921..708c2fa3e 100644 --- a/config/crd/bases/grafana.integreatly.org_grafananotificationpolicyroutes.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafananotificationpolicyroutes.yaml @@ -44,6 +44,7 @@ spec: of GrafanaNotificationPolicyRoute properties: active_time_intervals: + description: active time intervals items: type: string type: array @@ -104,7 +105,7 @@ spec: type: array type: array provenance: - description: provenance + description: 'Deprecated: Does nothing' type: string receiver: description: receiver diff --git a/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicies.yaml b/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicies.yaml index fe6208cfb..0daa917b0 100644 --- a/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicies.yaml +++ b/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicies.yaml @@ -120,13 +120,6 @@ spec: route: description: Routes for alerts to match against properties: - active_time_intervals: - items: - type: string - type: array - continue: - description: continue - type: boolean group_by: description: group by items: @@ -138,50 +131,8 @@ spec: group_wait: description: group wait type: string - match_re: - additionalProperties: - type: string - description: match re - type: object - matchers: - description: matchers - items: - properties: - isEqual: - description: is equal - type: boolean - isRegex: - description: is regex - type: boolean - name: - description: name - type: string - value: - description: value - type: string - required: - - isRegex - - value - type: object - type: array - mute_time_intervals: - description: mute time intervals - items: - type: string - type: array - object_matchers: - description: object matchers - items: - description: |- - ObjectMatcher ObjectMatcher is a matcher that can be used to filter alerts. - - swagger:model ObjectMatcher - items: - type: string - type: array - type: array provenance: - description: provenance + description: 'Deprecated: Does nothing' type: string receiver: description: receiver diff --git a/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicyroutes.yaml b/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicyroutes.yaml index 1f11c0921..708c2fa3e 100644 --- a/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicyroutes.yaml +++ b/deploy/helm/grafana-operator/files/crds/grafana.integreatly.org_grafananotificationpolicyroutes.yaml @@ -44,6 +44,7 @@ spec: of GrafanaNotificationPolicyRoute properties: active_time_intervals: + description: active time intervals items: type: string type: array @@ -104,7 +105,7 @@ spec: type: array type: array provenance: - description: provenance + description: 'Deprecated: Does nothing' type: string receiver: description: receiver diff --git a/deploy/kustomize/base/crds.yaml b/deploy/kustomize/base/crds.yaml index d5660b3ce..9bb16c9ff 100644 --- a/deploy/kustomize/base/crds.yaml +++ b/deploy/kustomize/base/crds.yaml @@ -2637,13 +2637,6 @@ spec: route: description: Routes for alerts to match against properties: - active_time_intervals: - items: - type: string - type: array - continue: - description: continue - type: boolean group_by: description: group by items: @@ -2655,50 +2648,8 @@ spec: group_wait: description: group wait type: string - match_re: - additionalProperties: - type: string - description: match re - type: object - matchers: - description: matchers - items: - properties: - isEqual: - description: is equal - type: boolean - isRegex: - description: is regex - type: boolean - name: - description: name - type: string - value: - description: value - type: string - required: - - isRegex - - value - type: object - type: array - mute_time_intervals: - description: mute time intervals - items: - type: string - type: array - object_matchers: - description: object matchers - items: - description: |- - ObjectMatcher ObjectMatcher is a matcher that can be used to filter alerts. - - swagger:model ObjectMatcher - items: - type: string - type: array - type: array provenance: - description: provenance + description: 'Deprecated: Does nothing' type: string receiver: description: receiver @@ -2901,6 +2852,7 @@ spec: of GrafanaNotificationPolicyRoute properties: active_time_intervals: + description: active time intervals items: type: string type: array @@ -2961,7 +2913,7 @@ spec: type: array type: array provenance: - description: provenance + description: 'Deprecated: Does nothing' type: string receiver: description: receiver diff --git a/docs/docs/api.md b/docs/docs/api.md index b802047b7..1f63a2fe2 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -5128,20 +5128,6 @@ Routes for alerts to match against receiver
true - - active_time_intervals - []string - -
- - false - - continue - boolean - - continue
- - false group_by []string @@ -5163,39 +5149,11 @@ Routes for alerts to match against group wait
false - - match_re - map[string]string - - match re
- - false - - matchers - []object - - matchers
- - false - - mute_time_intervals - []string - - mute time intervals
- - false - - object_matchers - [][]string - - object matchers
- - false provenance string - provenance
+ Deprecated: Does nothing
false @@ -5224,54 +5182,6 @@ mutually exclusive with Routes
-### GrafanaNotificationPolicy.spec.route.matchers[index] -[↩ Parent](#grafananotificationpolicyspecroute) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
isRegexboolean - is regex
-
true
valuestring - value
-
true
isEqualboolean - is equal
-
false
namestring - name
-
false
- - ### GrafanaNotificationPolicy.spec.route.routeSelector [↩ Parent](#grafananotificationpolicyspecroute) @@ -5555,7 +5465,7 @@ GrafanaNotificationPolicyRouteSpec defines the desired state of GrafanaNotificat active_time_intervals []string -
+ active time intervals
false @@ -5618,7 +5528,7 @@ GrafanaNotificationPolicyRouteSpec defines the desired state of GrafanaNotificat provenance string - provenance
+ Deprecated: Does nothing
false