diff --git a/apps/alerting/notifications/Makefile b/apps/alerting/notifications/Makefile new file mode 100644 index 00000000000..4fcd5fddd83 --- /dev/null +++ b/apps/alerting/notifications/Makefile @@ -0,0 +1,4 @@ +.PHONY: generate +generate: + ## --crdencoding none is needed to avoid infinite loop while generating recursive models' + grafana-app-sdk generate -c . -g ./apis --crdencoding none diff --git a/apps/alerting/notifications/routingtree.cue b/apps/alerting/notifications/routingtree.cue new file mode 100644 index 00000000000..7fac2ab2165 --- /dev/null +++ b/apps/alerting/notifications/routingtree.cue @@ -0,0 +1,49 @@ +package core + +route: { + kind: "RoutingTree" + group: "notifications" + apiResource: { + groupOverride: "notifications.alerting.grafana.app" + } + codegen: { + frontend: false + backend: true + } + pluralName: "RoutingTrees" + current: "v0alpha1" + versions: { + "v0alpha1": { + schema: { + #RouteDefaults: { + receiver: string + group_by?: [...string] + group_wait?: string + group_interval?: string + repeat_interval?: string + } + #Matcher: { + type: "=" |"!="|"=~"|"!~" @cuetsy(kind="enum") + label: string + value: string + } + #Route: { + receiver?: string + matchers?: [...#Matcher] + continue: bool + + group_by?: [...string] + mute_time_intervals?: [...string] + routes?: [...#Route] + group_wait?: string + group_interval?: string + repeat_interval?: string + } + spec: { + defaults: #RouteDefaults + routes: [...#Route] + } + } + } + } +} \ No newline at end of file diff --git a/pkg/apis/alerting_notifications/v0alpha1/register.go b/pkg/apis/alerting_notifications/v0alpha1/register.go index 05eab2f7ba3..7bf9a679fab 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/register.go +++ b/pkg/apis/alerting_notifications/v0alpha1/register.go @@ -18,9 +18,10 @@ func init() { } const ( - GROUP = "notifications.alerting.grafana.app" - VERSION = "v0alpha1" - APIVERSION = GROUP + "/" + VERSION + GROUP = "notifications.alerting.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION + UserDefinedRoutingTreeName = "user-defined" ) var ( @@ -86,6 +87,27 @@ var ( }, }, ) + RouteResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "routingtrees", "routingtree", "RoutingTree", + func() runtime.Object { return &RoutingTree{} }, + func() runtime.Object { return &RoutingTreeList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + // {Name: "Intervals", Type: "string", Format: "string", Description: "The display name"}, + }, + Reader: func(obj any) ([]interface{}, error) { + r, ok := obj.(*RoutingTree) + if !ok { + return nil, fmt.Errorf("expected resource or info") + } + return []interface{}{ + r.Name, + // r.Spec, //TODO implement formatting for Spec, same as UI? + }, nil + }, + }, + ) // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} // SchemaBuilder is used by standard codegen @@ -108,6 +130,8 @@ func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error { &ReceiverList{}, &TemplateGroup{}, &TemplateGroupList{}, + &RoutingTree{}, + &RoutingTreeList{}, ) metav1.AddToGroupVersion(scheme, g) diff --git a/pkg/apis/alerting_notifications/v0alpha1/routingtree_spec.go b/pkg/apis/alerting_notifications/v0alpha1/routingtree_spec.go new file mode 100644 index 00000000000..e67cea00b05 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/routingtree_spec.go @@ -0,0 +1,52 @@ +package v0alpha1 + +// Defines values for MatcherType. +const ( + MatcherTypeNotEqual MatcherType = "!=" + MatcherTypeEqual MatcherType = "=" + MatcherTypeEqualRegex MatcherType = "=~" + MatcherTypeNotEqualRegex MatcherType = "!~" +) + +// Matcher defines model for Matcher. +// +k8s:openapi-gen=true +type Matcher struct { + Label string `json:"label"` + Type MatcherType `json:"type"` + Value string `json:"value"` +} + +// MatcherType defines model for Matcher.Type. +// +k8s:openapi-gen=true +type MatcherType string + +// Route defines model for Route. +// +k8s:openapi-gen=true +type Route struct { + Continue bool `json:"continue,omitempty"` + GroupBy []string `json:"group_by,omitempty"` + GroupInterval *string `json:"group_interval,omitempty"` + GroupWait *string `json:"group_wait,omitempty"` + Matchers []Matcher `json:"matchers,omitempty"` + MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"` + Receiver *string `json:"receiver,omitempty"` + RepeatInterval *string `json:"repeat_interval,omitempty"` + Routes []Route `json:"routes,omitempty"` +} + +// RouteDefaults defines model for RouteDefaults. +// +k8s:openapi-gen=true +type RouteDefaults struct { + GroupBy []string `json:"group_by,omitempty"` + GroupInterval *string `json:"group_interval,omitempty"` + GroupWait *string `json:"group_wait,omitempty"` + Receiver string `json:"receiver"` + RepeatInterval *string `json:"repeat_interval,omitempty"` +} + +// Spec defines model for Spec. +// +k8s:openapi-gen=true +type RoutingTreeSpec struct { + Defaults RouteDefaults `json:"defaults"` + Routes []Route `json:"routes"` +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/types.go b/pkg/apis/alerting_notifications/v0alpha1/types.go index e674f58ebd7..649ca7f565b 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/types.go +++ b/pkg/apis/alerting_notifications/v0alpha1/types.go @@ -255,3 +255,86 @@ type TemplateGroupList struct { } // endregion + +// region Routes + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:openapi-gen=true +type RoutingTree struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec RoutingTreeSpec `json:"spec"` +} + +func (o *RoutingTree) GetSpec() any { + return o.Spec +} + +func (o *RoutingTree) SetSpec(spec any) error { + cast, ok := spec.(RoutingTreeSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *RoutingTree) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *RoutingTree) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *RoutingTree) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *RoutingTree) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *RoutingTree) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *RoutingTree) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:openapi-gen=true +type RoutingTreeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []RoutingTree `json:"items"` +} + +// endregion Routes diff --git a/pkg/apis/alerting_notifications/v0alpha1/types_ext.go b/pkg/apis/alerting_notifications/v0alpha1/types_ext.go index d3b97edcd19..34a7cdbd77d 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/types_ext.go +++ b/pkg/apis/alerting_notifications/v0alpha1/types_ext.go @@ -97,3 +97,24 @@ func (o *TemplateGroup) SetProvenanceStatus(status string) { } o.Annotations[ProvenanceStatusAnnotationKey] = status } + +func (o *RoutingTree) GetProvenanceStatus() string { + if o == nil || o.Annotations == nil { + return "" + } + s, ok := o.Annotations[ProvenanceStatusAnnotationKey] + if !ok || s == "" { + return ProvenanceStatusNone + } + return s +} + +func (o *RoutingTree) SetProvenanceStatus(status string) { + if o.Annotations == nil { + o.Annotations = make(map[string]string, 1) + } + if status == "" { + status = ProvenanceStatusNone + } + o.Annotations[ProvenanceStatusAnnotationKey] = status +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go index b92ad6d360d..b69f9817c44 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go @@ -91,6 +91,22 @@ func (in *Interval) DeepCopy() *Interval { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Matcher) DeepCopyInto(out *Matcher) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Matcher. +func (in *Matcher) DeepCopy() *Matcher { + if in == nil { + return nil + } + out := new(Matcher) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Receiver) DeepCopyInto(out *Receiver) { *out = *in @@ -174,6 +190,184 @@ func (in *ReceiverSpec) DeepCopy() *ReceiverSpec { return out } +// 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) + } + if in.GroupInterval != nil { + in, out := &in.GroupInterval, &out.GroupInterval + *out = new(string) + **out = **in + } + if in.GroupWait != nil { + in, out := &in.GroupWait, &out.GroupWait + *out = new(string) + **out = **in + } + if in.Matchers != nil { + in, out := &in.Matchers, &out.Matchers + *out = make([]Matcher, len(*in)) + copy(*out, *in) + } + if in.MuteTimeIntervals != nil { + in, out := &in.MuteTimeIntervals, &out.MuteTimeIntervals + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Receiver != nil { + in, out := &in.Receiver, &out.Receiver + *out = new(string) + **out = **in + } + if in.RepeatInterval != nil { + in, out := &in.RepeatInterval, &out.RepeatInterval + *out = new(string) + **out = **in + } + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]Route, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route. +func (in *Route) DeepCopy() *Route { + if in == nil { + return nil + } + out := new(Route) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RouteDefaults) DeepCopyInto(out *RouteDefaults) { + *out = *in + if in.GroupBy != nil { + in, out := &in.GroupBy, &out.GroupBy + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.GroupInterval != nil { + in, out := &in.GroupInterval, &out.GroupInterval + *out = new(string) + **out = **in + } + if in.GroupWait != nil { + in, out := &in.GroupWait, &out.GroupWait + *out = new(string) + **out = **in + } + if in.RepeatInterval != nil { + in, out := &in.RepeatInterval, &out.RepeatInterval + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteDefaults. +func (in *RouteDefaults) DeepCopy() *RouteDefaults { + if in == nil { + return nil + } + out := new(RouteDefaults) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoutingTree) DeepCopyInto(out *RoutingTree) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoutingTree. +func (in *RoutingTree) DeepCopy() *RoutingTree { + if in == nil { + return nil + } + out := new(RoutingTree) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RoutingTree) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoutingTreeList) DeepCopyInto(out *RoutingTreeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RoutingTree, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoutingTreeList. +func (in *RoutingTreeList) DeepCopy() *RoutingTreeList { + if in == nil { + return nil + } + out := new(RoutingTreeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RoutingTreeList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoutingTreeSpec) DeepCopyInto(out *RoutingTreeSpec) { + *out = *in + in.Defaults.DeepCopyInto(&out.Defaults) + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]Route, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoutingTreeSpec. +func (in *RoutingTreeSpec) DeepCopy() *RoutingTreeSpec { + if in == nil { + return nil + } + out := new(RoutingTreeSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplateGroup) DeepCopyInto(out *TemplateGroup) { *out = *in diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go index 6703e64406c..f046c560b47 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go @@ -16,9 +16,15 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Integration": schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval": schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Matcher": schema_pkg_apis_alerting_notifications_v0alpha1_Matcher(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Receiver": schema_pkg_apis_alerting_notifications_v0alpha1_Receiver(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverList": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverList(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Route": schema_pkg_apis_alerting_notifications_v0alpha1_Route(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RouteDefaults": schema_pkg_apis_alerting_notifications_v0alpha1_RouteDefaults(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTree": schema_pkg_apis_alerting_notifications_v0alpha1_RoutingTree(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTreeList": schema_pkg_apis_alerting_notifications_v0alpha1_RoutingTreeList(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTreeSpec": schema_pkg_apis_alerting_notifications_v0alpha1_RoutingTreeSpec(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroup": schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroup(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroupList": schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroupList(ref), "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroupSpec(ref), @@ -209,6 +215,41 @@ func schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref common.Referen } } +func schema_pkg_apis_alerting_notifications_v0alpha1_Matcher(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Matcher defines model for Matcher.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "label": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"label", "type", "value"}, + }, + }, + } +} + func schema_pkg_apis_alerting_notifications_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -339,6 +380,282 @@ func schema_pkg_apis_alerting_notifications_v0alpha1_ReceiverSpec(ref common.Ref } } +func schema_pkg_apis_alerting_notifications_v0alpha1_Route(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Route defines model for Route.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "continue": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "group_by": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "group_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "group_wait": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "matchers": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Matcher"), + }, + }, + }, + }, + }, + "mute_time_intervals": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "receiver": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "repeat_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Route"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Matcher", "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Route"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_RouteDefaults(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RouteDefaults defines model for RouteDefaults.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group_by": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "group_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "group_wait": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "receiver": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "repeat_interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"receiver"}, + }, + }, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_RoutingTree(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTreeSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTreeSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_RoutingTreeList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTree"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RoutingTree", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Spec defines model for Spec.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "defaults": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RouteDefaults"), + }, + }, + "routes": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Route"), + }, + }, + }, + }, + }, + }, + Required: []string{"defaults", "routes"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Route", "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.RouteDefaults"}, + } +} + func schema_pkg_apis_alerting_notifications_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list index 7b756e64dfd..3b6889dbb81 100644 --- a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,4 +1,19 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,GroupBy +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,Matchers +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,MuteTimeIntervals +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,Routes +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,RouteDefaults,GroupBy +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,RoutingTreeSpec,Routes API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Interval,DaysOfMonth +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,GroupBy +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,GroupInterval +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,GroupWait +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,MuteTimeIntervals +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Route,RepeatInterval +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,RouteDefaults,GroupBy +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,RouteDefaults,GroupInterval +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,RouteDefaults,GroupWait +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,RouteDefaults,RepeatInterval API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeIntervalSpec,TimeIntervals API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,EndTime API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,StartTime diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/matcher.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/matcher.go new file mode 100644 index 00000000000..cbbe6a6c5be --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/matcher.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" +) + +// MatcherApplyConfiguration represents a declarative configuration of the Matcher type for use +// with apply. +type MatcherApplyConfiguration struct { + Label *string `json:"label,omitempty"` + Type *v0alpha1.MatcherType `json:"type,omitempty"` + Value *string `json:"value,omitempty"` +} + +// MatcherApplyConfiguration constructs a declarative configuration of the Matcher type for use with +// apply. +func Matcher() *MatcherApplyConfiguration { + return &MatcherApplyConfiguration{} +} + +// WithLabel sets the Label field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Label field is set to the value of the last call. +func (b *MatcherApplyConfiguration) WithLabel(value string) *MatcherApplyConfiguration { + b.Label = &value + return b +} + +// WithType sets the Type field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Type field is set to the value of the last call. +func (b *MatcherApplyConfiguration) WithType(value v0alpha1.MatcherType) *MatcherApplyConfiguration { + b.Type = &value + return b +} + +// WithValue sets the Value field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Value field is set to the value of the last call. +func (b *MatcherApplyConfiguration) WithValue(value string) *MatcherApplyConfiguration { + b.Value = &value + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/route.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/route.go new file mode 100644 index 00000000000..0a1b1e7bd18 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/route.go @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// RouteApplyConfiguration represents a declarative configuration of the Route type for use +// with apply. +type RouteApplyConfiguration struct { + Continue *bool `json:"continue,omitempty"` + GroupBy []string `json:"group_by,omitempty"` + GroupInterval *string `json:"group_interval,omitempty"` + GroupWait *string `json:"group_wait,omitempty"` + Matchers []MatcherApplyConfiguration `json:"matchers,omitempty"` + MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"` + Receiver *string `json:"receiver,omitempty"` + RepeatInterval *string `json:"repeat_interval,omitempty"` + Routes []RouteApplyConfiguration `json:"routes,omitempty"` +} + +// RouteApplyConfiguration constructs a declarative configuration of the Route type for use with +// apply. +func Route() *RouteApplyConfiguration { + return &RouteApplyConfiguration{} +} + +// WithContinue sets the Continue field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Continue field is set to the value of the last call. +func (b *RouteApplyConfiguration) WithContinue(value bool) *RouteApplyConfiguration { + b.Continue = &value + return b +} + +// WithGroupBy adds the given value to the GroupBy field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the GroupBy field. +func (b *RouteApplyConfiguration) WithGroupBy(values ...string) *RouteApplyConfiguration { + for i := range values { + b.GroupBy = append(b.GroupBy, values[i]) + } + return b +} + +// WithGroupInterval sets the GroupInterval field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupInterval field is set to the value of the last call. +func (b *RouteApplyConfiguration) WithGroupInterval(value string) *RouteApplyConfiguration { + b.GroupInterval = &value + return b +} + +// WithGroupWait sets the GroupWait field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupWait field is set to the value of the last call. +func (b *RouteApplyConfiguration) WithGroupWait(value string) *RouteApplyConfiguration { + b.GroupWait = &value + return b +} + +// WithMatchers adds the given value to the Matchers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Matchers field. +func (b *RouteApplyConfiguration) WithMatchers(values ...*MatcherApplyConfiguration) *RouteApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMatchers") + } + b.Matchers = append(b.Matchers, *values[i]) + } + return b +} + +// WithMuteTimeIntervals adds the given value to the MuteTimeIntervals field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the MuteTimeIntervals field. +func (b *RouteApplyConfiguration) WithMuteTimeIntervals(values ...string) *RouteApplyConfiguration { + for i := range values { + b.MuteTimeIntervals = append(b.MuteTimeIntervals, values[i]) + } + return b +} + +// WithReceiver sets the Receiver field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Receiver field is set to the value of the last call. +func (b *RouteApplyConfiguration) WithReceiver(value string) *RouteApplyConfiguration { + b.Receiver = &value + return b +} + +// WithRepeatInterval sets the RepeatInterval field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RepeatInterval field is set to the value of the last call. +func (b *RouteApplyConfiguration) WithRepeatInterval(value string) *RouteApplyConfiguration { + b.RepeatInterval = &value + return b +} + +// WithRoutes adds the given value to the Routes field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Routes field. +func (b *RouteApplyConfiguration) WithRoutes(values ...*RouteApplyConfiguration) *RouteApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithRoutes") + } + b.Routes = append(b.Routes, *values[i]) + } + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routedefaults.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routedefaults.go new file mode 100644 index 00000000000..bf685faa851 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routedefaults.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// RouteDefaultsApplyConfiguration represents a declarative configuration of the RouteDefaults type for use +// with apply. +type RouteDefaultsApplyConfiguration struct { + GroupBy []string `json:"group_by,omitempty"` + GroupInterval *string `json:"group_interval,omitempty"` + GroupWait *string `json:"group_wait,omitempty"` + Receiver *string `json:"receiver,omitempty"` + RepeatInterval *string `json:"repeat_interval,omitempty"` +} + +// RouteDefaultsApplyConfiguration constructs a declarative configuration of the RouteDefaults type for use with +// apply. +func RouteDefaults() *RouteDefaultsApplyConfiguration { + return &RouteDefaultsApplyConfiguration{} +} + +// WithGroupBy adds the given value to the GroupBy field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the GroupBy field. +func (b *RouteDefaultsApplyConfiguration) WithGroupBy(values ...string) *RouteDefaultsApplyConfiguration { + for i := range values { + b.GroupBy = append(b.GroupBy, values[i]) + } + return b +} + +// WithGroupInterval sets the GroupInterval field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupInterval field is set to the value of the last call. +func (b *RouteDefaultsApplyConfiguration) WithGroupInterval(value string) *RouteDefaultsApplyConfiguration { + b.GroupInterval = &value + return b +} + +// WithGroupWait sets the GroupWait field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupWait field is set to the value of the last call. +func (b *RouteDefaultsApplyConfiguration) WithGroupWait(value string) *RouteDefaultsApplyConfiguration { + b.GroupWait = &value + return b +} + +// WithReceiver sets the Receiver field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Receiver field is set to the value of the last call. +func (b *RouteDefaultsApplyConfiguration) WithReceiver(value string) *RouteDefaultsApplyConfiguration { + b.Receiver = &value + return b +} + +// WithRepeatInterval sets the RepeatInterval field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RepeatInterval field is set to the value of the last call. +func (b *RouteDefaultsApplyConfiguration) WithRepeatInterval(value string) *RouteDefaultsApplyConfiguration { + b.RepeatInterval = &value + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routingtree.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routingtree.go new file mode 100644 index 00000000000..f801d1815f5 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routingtree.go @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// RoutingTreeApplyConfiguration represents a declarative configuration of the RoutingTree type for use +// with apply. +type RoutingTreeApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *RoutingTreeSpecApplyConfiguration `json:"spec,omitempty"` +} + +// RoutingTree constructs a declarative configuration of the RoutingTree type for use with +// apply. +func RoutingTree(name, namespace string) *RoutingTreeApplyConfiguration { + b := &RoutingTreeApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("RoutingTree") + b.WithAPIVersion("notifications.alerting.grafana.app/v0alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithKind(value string) *RoutingTreeApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithAPIVersion(value string) *RoutingTreeApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithName(value string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithGenerateName(value string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithNamespace(value string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithUID(value types.UID) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithResourceVersion(value string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithGeneration(value int64) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithCreationTimestamp(value metav1.Time) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *RoutingTreeApplyConfiguration) WithLabels(entries map[string]string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *RoutingTreeApplyConfiguration) WithAnnotations(entries map[string]string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *RoutingTreeApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *RoutingTreeApplyConfiguration) WithFinalizers(values ...string) *RoutingTreeApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *RoutingTreeApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *RoutingTreeApplyConfiguration) WithSpec(value *RoutingTreeSpecApplyConfiguration) *RoutingTreeApplyConfiguration { + b.Spec = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *RoutingTreeApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.Name +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routingtreespec.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routingtreespec.go new file mode 100644 index 00000000000..1184a7d4244 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/routingtreespec.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// RoutingTreeSpecApplyConfiguration represents a declarative configuration of the RoutingTreeSpec type for use +// with apply. +type RoutingTreeSpecApplyConfiguration struct { + Defaults *RouteDefaultsApplyConfiguration `json:"defaults,omitempty"` + Routes []RouteApplyConfiguration `json:"routes,omitempty"` +} + +// RoutingTreeSpecApplyConfiguration constructs a declarative configuration of the RoutingTreeSpec type for use with +// apply. +func RoutingTreeSpec() *RoutingTreeSpecApplyConfiguration { + return &RoutingTreeSpecApplyConfiguration{} +} + +// WithDefaults sets the Defaults field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Defaults field is set to the value of the last call. +func (b *RoutingTreeSpecApplyConfiguration) WithDefaults(value *RouteDefaultsApplyConfiguration) *RoutingTreeSpecApplyConfiguration { + b.Defaults = value + return b +} + +// WithRoutes adds the given value to the Routes field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Routes field. +func (b *RoutingTreeSpecApplyConfiguration) WithRoutes(values ...*RouteApplyConfiguration) *RoutingTreeSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithRoutes") + } + b.Routes = append(b.Routes, *values[i]) + } + return b +} diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index 8221f95b1da..10aa8312bca 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -24,10 +24,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &alertingnotificationsv0alpha1.IntegrationApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("Interval"): return &alertingnotificationsv0alpha1.IntervalApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("Matcher"): + return &alertingnotificationsv0alpha1.MatcherApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("Receiver"): return &alertingnotificationsv0alpha1.ReceiverApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("ReceiverSpec"): return &alertingnotificationsv0alpha1.ReceiverSpecApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("Route"): + return &alertingnotificationsv0alpha1.RouteApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("RouteDefaults"): + return &alertingnotificationsv0alpha1.RouteDefaultsApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("RoutingTree"): + return &alertingnotificationsv0alpha1.RoutingTreeApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("RoutingTreeSpec"): + return &alertingnotificationsv0alpha1.RoutingTreeSpecApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("TemplateGroup"): return &alertingnotificationsv0alpha1.TemplateGroupApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("TemplateGroupSpec"): diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go index f77cbc0c51a..16770726462 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go @@ -15,6 +15,7 @@ import ( type NotificationsV0alpha1Interface interface { RESTClient() rest.Interface ReceiversGetter + RoutingTreesGetter TemplateGroupsGetter TimeIntervalsGetter } @@ -28,6 +29,10 @@ func (c *NotificationsV0alpha1Client) Receivers(namespace string) ReceiverInterf return newReceivers(c, namespace) } +func (c *NotificationsV0alpha1Client) RoutingTrees(namespace string) RoutingTreeInterface { + return newRoutingTrees(c, namespace) +} + func (c *NotificationsV0alpha1Client) TemplateGroups(namespace string) TemplateGroupInterface { return newTemplateGroups(c, namespace) } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go index f2695e6e8a3..fce832e1b2f 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go @@ -18,6 +18,10 @@ func (c *FakeNotificationsV0alpha1) Receivers(namespace string) v0alpha1.Receive return &FakeReceivers{c, namespace} } +func (c *FakeNotificationsV0alpha1) RoutingTrees(namespace string) v0alpha1.RoutingTreeInterface { + return &FakeRoutingTrees{c, namespace} +} + func (c *FakeNotificationsV0alpha1) TemplateGroups(namespace string) v0alpha1.TemplateGroupInterface { return &FakeTemplateGroups{c, namespace} } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_routingtree.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_routingtree.go new file mode 100644 index 00000000000..1d6f7badafe --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_routingtree.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeRoutingTrees implements RoutingTreeInterface +type FakeRoutingTrees struct { + Fake *FakeNotificationsV0alpha1 + ns string +} + +var routingtreesResource = v0alpha1.SchemeGroupVersion.WithResource("routingtrees") + +var routingtreesKind = v0alpha1.SchemeGroupVersion.WithKind("RoutingTree") + +// Get takes name of the routingTree, and returns the corresponding routingTree object, and an error if there is any. +func (c *FakeRoutingTrees) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.RoutingTree, err error) { + emptyResult := &v0alpha1.RoutingTree{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(routingtreesResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.RoutingTree), err +} + +// List takes label and field selectors, and returns the list of RoutingTrees that match those selectors. +func (c *FakeRoutingTrees) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.RoutingTreeList, err error) { + emptyResult := &v0alpha1.RoutingTreeList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(routingtreesResource, routingtreesKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v0alpha1.RoutingTreeList{ListMeta: obj.(*v0alpha1.RoutingTreeList).ListMeta} + for _, item := range obj.(*v0alpha1.RoutingTreeList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested routingTrees. +func (c *FakeRoutingTrees) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(routingtreesResource, c.ns, opts)) + +} + +// Create takes the representation of a routingTree and creates it. Returns the server's representation of the routingTree, and an error, if there is any. +func (c *FakeRoutingTrees) Create(ctx context.Context, routingTree *v0alpha1.RoutingTree, opts v1.CreateOptions) (result *v0alpha1.RoutingTree, err error) { + emptyResult := &v0alpha1.RoutingTree{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(routingtreesResource, c.ns, routingTree, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.RoutingTree), err +} + +// Update takes the representation of a routingTree and updates it. Returns the server's representation of the routingTree, and an error, if there is any. +func (c *FakeRoutingTrees) Update(ctx context.Context, routingTree *v0alpha1.RoutingTree, opts v1.UpdateOptions) (result *v0alpha1.RoutingTree, err error) { + emptyResult := &v0alpha1.RoutingTree{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(routingtreesResource, c.ns, routingTree, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.RoutingTree), err +} + +// Delete takes name of the routingTree and deletes it. Returns an error if one occurs. +func (c *FakeRoutingTrees) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(routingtreesResource, c.ns, name, opts), &v0alpha1.RoutingTree{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeRoutingTrees) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(routingtreesResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v0alpha1.RoutingTreeList{}) + return err +} + +// Patch applies the patch and returns the patched routingTree. +func (c *FakeRoutingTrees) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.RoutingTree, err error) { + emptyResult := &v0alpha1.RoutingTree{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(routingtreesResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.RoutingTree), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied routingTree. +func (c *FakeRoutingTrees) Apply(ctx context.Context, routingTree *alertingnotificationsv0alpha1.RoutingTreeApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.RoutingTree, err error) { + if routingTree == nil { + return nil, fmt.Errorf("routingTree provided to Apply must not be nil") + } + data, err := json.Marshal(routingTree) + if err != nil { + return nil, err + } + name := routingTree.Name + if name == nil { + return nil, fmt.Errorf("routingTree.Name must be provided to Apply") + } + emptyResult := &v0alpha1.RoutingTree{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(routingtreesResource, c.ns, *name, types.ApplyPatchType, data, opts.ToPatchOptions()), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v0alpha1.RoutingTree), err +} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go index f76e1bfa65d..5d92113ab80 100644 --- a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go @@ -6,6 +6,8 @@ package v0alpha1 type ReceiverExpansion interface{} +type RoutingTreeExpansion interface{} + type TemplateGroupExpansion interface{} type TimeIntervalExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/routingtree.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/routingtree.go new file mode 100644 index 00000000000..2996b3983c2 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/routingtree.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + scheme "github.com/grafana/grafana/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// RoutingTreesGetter has a method to return a RoutingTreeInterface. +// A group's client should implement this interface. +type RoutingTreesGetter interface { + RoutingTrees(namespace string) RoutingTreeInterface +} + +// RoutingTreeInterface has methods to work with RoutingTree resources. +type RoutingTreeInterface interface { + Create(ctx context.Context, routingTree *v0alpha1.RoutingTree, opts v1.CreateOptions) (*v0alpha1.RoutingTree, error) + Update(ctx context.Context, routingTree *v0alpha1.RoutingTree, opts v1.UpdateOptions) (*v0alpha1.RoutingTree, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v0alpha1.RoutingTree, error) + List(ctx context.Context, opts v1.ListOptions) (*v0alpha1.RoutingTreeList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.RoutingTree, err error) + Apply(ctx context.Context, routingTree *alertingnotificationsv0alpha1.RoutingTreeApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.RoutingTree, err error) + RoutingTreeExpansion +} + +// routingTrees implements RoutingTreeInterface +type routingTrees struct { + *gentype.ClientWithListAndApply[*v0alpha1.RoutingTree, *v0alpha1.RoutingTreeList, *alertingnotificationsv0alpha1.RoutingTreeApplyConfiguration] +} + +// newRoutingTrees returns a RoutingTrees +func newRoutingTrees(c *NotificationsV0alpha1Client, namespace string) *routingTrees { + return &routingTrees{ + gentype.NewClientWithListAndApply[*v0alpha1.RoutingTree, *v0alpha1.RoutingTreeList, *alertingnotificationsv0alpha1.RoutingTreeApplyConfiguration]( + "routingtrees", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v0alpha1.RoutingTree { return &v0alpha1.RoutingTree{} }, + func() *v0alpha1.RoutingTreeList { return &v0alpha1.RoutingTreeList{} }), + } +} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go index a84b5042189..641fef4aa7b 100644 --- a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go @@ -12,6 +12,8 @@ import ( type Interface interface { // Receivers returns a ReceiverInformer. Receivers() ReceiverInformer + // RoutingTrees returns a RoutingTreeInformer. + RoutingTrees() RoutingTreeInformer // TemplateGroups returns a TemplateGroupInformer. TemplateGroups() TemplateGroupInformer // TimeIntervals returns a TimeIntervalInformer. @@ -34,6 +36,11 @@ func (v *version) Receivers() ReceiverInformer { return &receiverInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// RoutingTrees returns a RoutingTreeInformer. +func (v *version) RoutingTrees() RoutingTreeInformer { + return &routingTreeInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // TemplateGroups returns a TemplateGroupInformer. func (v *version) TemplateGroups() TemplateGroupInformer { return &templateGroupInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/routingtree.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/routingtree.go new file mode 100644 index 00000000000..79238d3db65 --- /dev/null +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/routingtree.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + time "time" + + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + versioned "github.com/grafana/grafana/pkg/generated/clientset/versioned" + internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" + v0alpha1 "github.com/grafana/grafana/pkg/generated/listers/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// RoutingTreeInformer provides access to a shared informer and lister for +// RoutingTrees. +type RoutingTreeInformer interface { + Informer() cache.SharedIndexInformer + Lister() v0alpha1.RoutingTreeLister +} + +type routingTreeInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewRoutingTreeInformer constructs a new informer for RoutingTree type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewRoutingTreeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredRoutingTreeInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredRoutingTreeInformer constructs a new informer for RoutingTree type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredRoutingTreeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().RoutingTrees(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().RoutingTrees(namespace).Watch(context.TODO(), options) + }, + }, + &alertingnotificationsv0alpha1.RoutingTree{}, + resyncPeriod, + indexers, + ) +} + +func (f *routingTreeInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredRoutingTreeInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *routingTreeInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&alertingnotificationsv0alpha1.RoutingTree{}, f.defaultInformer) +} + +func (f *routingTreeInformer) Lister() v0alpha1.RoutingTreeLister { + return v0alpha1.NewRoutingTreeLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index 9c16b6eab97..a99bb909577 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -42,6 +42,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=notifications.alerting.grafana.app, Version=v0alpha1 case v0alpha1.SchemeGroupVersion.WithResource("receivers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().Receivers().Informer()}, nil + case v0alpha1.SchemeGroupVersion.WithResource("routingtrees"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().RoutingTrees().Informer()}, nil case v0alpha1.SchemeGroupVersion.WithResource("templategroups"): return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().TemplateGroups().Informer()}, nil case v0alpha1.SchemeGroupVersion.WithResource("timeintervals"): diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go index 8f4585658eb..79b5f5c82ba 100644 --- a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go @@ -12,6 +12,14 @@ type ReceiverListerExpansion interface{} // ReceiverNamespaceLister. type ReceiverNamespaceListerExpansion interface{} +// RoutingTreeListerExpansion allows custom methods to be added to +// RoutingTreeLister. +type RoutingTreeListerExpansion interface{} + +// RoutingTreeNamespaceListerExpansion allows custom methods to be added to +// RoutingTreeNamespaceLister. +type RoutingTreeNamespaceListerExpansion interface{} + // TemplateGroupListerExpansion allows custom methods to be added to // TemplateGroupLister. type TemplateGroupListerExpansion interface{} diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/routingtree.go b/pkg/generated/listers/alerting_notifications/v0alpha1/routingtree.go new file mode 100644 index 00000000000..539c50c24b2 --- /dev/null +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/routingtree.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by lister-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// RoutingTreeLister helps list RoutingTrees. +// All objects returned here must be treated as read-only. +type RoutingTreeLister interface { + // List lists all RoutingTrees in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.RoutingTree, err error) + // RoutingTrees returns an object that can list and get RoutingTrees. + RoutingTrees(namespace string) RoutingTreeNamespaceLister + RoutingTreeListerExpansion +} + +// routingTreeLister implements the RoutingTreeLister interface. +type routingTreeLister struct { + listers.ResourceIndexer[*v0alpha1.RoutingTree] +} + +// NewRoutingTreeLister returns a new RoutingTreeLister. +func NewRoutingTreeLister(indexer cache.Indexer) RoutingTreeLister { + return &routingTreeLister{listers.New[*v0alpha1.RoutingTree](indexer, v0alpha1.Resource("routingtree"))} +} + +// RoutingTrees returns an object that can list and get RoutingTrees. +func (s *routingTreeLister) RoutingTrees(namespace string) RoutingTreeNamespaceLister { + return routingTreeNamespaceLister{listers.NewNamespaced[*v0alpha1.RoutingTree](s.ResourceIndexer, namespace)} +} + +// RoutingTreeNamespaceLister helps list and get RoutingTrees. +// All objects returned here must be treated as read-only. +type RoutingTreeNamespaceLister interface { + // List lists all RoutingTrees in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.RoutingTree, err error) + // Get retrieves the RoutingTree from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v0alpha1.RoutingTree, error) + RoutingTreeNamespaceListerExpansion +} + +// routingTreeNamespaceLister implements the RoutingTreeNamespaceLister +// interface. +type routingTreeNamespaceLister struct { + listers.ResourceIndexer[*v0alpha1.RoutingTree] +} diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index 72a0b0f3c16..3b26328d684 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -14,6 +14,7 @@ import ( notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" receiver "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/receiver" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routing_tree" "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/template_group" timeInterval "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -89,10 +90,16 @@ func (t *NotificationsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiser return fmt.Errorf("failed to initialize templates group storage: %w", err) } + routeStorage, err := routing_tree.NewStorage(t.ng.Api.Policies, t.namespacer) + if err != nil { + return fmt.Errorf("failed to initialize route storage: %w", err) + } + apiGroupInfo.VersionedResourcesStorageMap[notificationsModels.VERSION] = map[string]rest.Storage{ notificationsModels.TimeIntervalResourceInfo.StoragePath(): intervals, notificationsModels.ReceiverResourceInfo.StoragePath(): recvStorage, notificationsModels.TemplateGroupResourceInfo.StoragePath(): templ, + notificationsModels.RouteResourceInfo.StoragePath(): routeStorage, } return nil } @@ -117,6 +124,7 @@ func (t *NotificationsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3 delete(oas.Paths.Paths, root+notificationsModels.ReceiverResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+notificationsModels.TemplateGroupResourceInfo.GroupResource().Resource) + delete(oas.Paths.Paths, root+notificationsModels.RouteResourceInfo.GroupResource().Resource) // The root API discovery list sub := oas.Paths.Paths[root] @@ -136,6 +144,8 @@ func (t *NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer { return timeInterval.Authorize(ctx, t.authz, a) case notificationsModels.ReceiverResourceInfo.GroupResource().Resource: return receiver.Authorize(ctx, t.receiverAuth, a) + case notificationsModels.RouteResourceInfo.GroupResource().Resource: + return routing_tree.Authorize(ctx, t.authz, a) } return authorizer.DecisionNoOpinion, "", nil }) diff --git a/pkg/registry/apis/alerting/notifications/routing_tree/authorize.go b/pkg/registry/apis/alerting/notifications/routing_tree/authorize.go new file mode 100644 index 00000000000..5394d728e91 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/routing_tree/authorize.go @@ -0,0 +1,51 @@ +package routing_tree + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if attr.GetResource() != resourceInfo.GroupResource().Resource { + return authorizer.DecisionNoOpinion, "", nil + } + user, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + var action accesscontrol.Evaluator + switch attr.GetVerb() { + case "patch": + fallthrough + case "create": + fallthrough + case "update": + fallthrough + case "deletecollection": + fallthrough + case "delete": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingRoutesWrite), + ) + } + + eval := accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingRoutesRead), + ) + if action != nil { + eval = accesscontrol.EvalAll(eval, action) + } + + ok, err := ac.Evaluate(ctx, user, eval) + if ok { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", err +} diff --git a/pkg/registry/apis/alerting/notifications/routing_tree/conversions.go b/pkg/registry/apis/alerting/notifications/routing_tree/conversions.go new file mode 100644 index 00000000000..70bea2dba7a --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/routing_tree/conversions.go @@ -0,0 +1,231 @@ +package routing_tree + +import ( + "errors" + "fmt" + "maps" + "slices" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + promModel "github.com/prometheus/common/model" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/util" +) + +func convertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) { + spec := model.RoutingTreeSpec{ + Defaults: model.RouteDefaults{ + GroupBy: r.GroupByStr, + GroupWait: optionalPrometheusDurationToString(r.GroupWait), + GroupInterval: optionalPrometheusDurationToString(r.GroupInterval), + RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval), + Receiver: r.Receiver, + }, + } + for _, route := range r.Routes { + if route == nil { + continue + } + spec.Routes = append(spec.Routes, convertRouteToK8sSubRoute(route)) + } + + var result = &model.RoutingTree{ + TypeMeta: metav1.TypeMeta{ + Kind: model.RouteResourceInfo.GroupVersionKind().Kind, + APIVersion: model.APIVERSION, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: model.UserDefinedRoutingTreeName, + Namespace: namespacer(orgID), + ResourceVersion: version, + }, + Spec: spec, + } + result.SetProvenanceStatus(string(r.Provenance)) + return result, nil +} + +func convertRouteToK8sSubRoute(r *definitions.Route) model.Route { + result := model.Route{ + GroupBy: r.GroupByStr, + MuteTimeIntervals: r.MuteTimeIntervals, + Continue: r.Continue, + GroupWait: optionalPrometheusDurationToString(r.GroupWait), + GroupInterval: optionalPrometheusDurationToString(r.GroupInterval), + RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval), + Routes: make([]model.Route, 0, len(r.Routes)), + } + if r.Receiver != "" { + result.Receiver = util.Pointer(r.Receiver) + } + + if r.Match != nil { + keys := slices.Collect(maps.Keys(r.Match)) + slices.Sort(keys) + for _, key := range keys { + result.Matchers = append(result.Matchers, model.Matcher{ + Label: key, + Type: model.MatcherTypeEqual, + Value: r.Match[key], + }) + } + } + + if r.MatchRE != nil { + keys := slices.Collect(maps.Keys(r.MatchRE)) + slices.Sort(keys) + for _, key := range keys { + m := model.Matcher{ + Label: key, + Type: model.MatcherTypeEqualRegex, + } + value, _ := r.MatchRE[key].MarshalYAML() + if s, ok := value.(string); ok { + m.Value = s + } + result.Matchers = append(result.Matchers, m) + } + } + + for _, m := range r.Matchers { + result.Matchers = append(result.Matchers, model.Matcher{ + Label: m.Name, + Type: model.MatcherType(m.Type.String()), + Value: m.Value, + }) + } + for _, m := range r.ObjectMatchers { + result.Matchers = append(result.Matchers, model.Matcher{ + Label: m.Name, + Type: model.MatcherType(m.Type.String()), + Value: m.Value, + }) + } + for _, route := range r.Routes { + if route == nil { + continue + } + result.Routes = append(result.Routes, convertRouteToK8sSubRoute(route)) + } + return result +} + +func convertToDomainModel(obj *model.RoutingTree) (definitions.Route, string, error) { + defaults := obj.Spec.Defaults + result := definitions.Route{ + Receiver: defaults.Receiver, + GroupByStr: defaults.GroupBy, + Routes: make([]*definitions.Route, 0, len(obj.Spec.Routes)), + } + path := "." + var errs []error + + result.GroupWait = parsePrometheusDuration(defaults.GroupWait, func(err error) { + errs = append(errs, fmt.Errorf("obj '%s' has invalid format of 'groupWait': %w", path, err)) + }) + result.GroupInterval = parsePrometheusDuration(defaults.GroupInterval, func(err error) { + errs = append(errs, fmt.Errorf("obj '%s' has invalid format of 'groupInterval': %w", path, err)) + }) + result.RepeatInterval = parsePrometheusDuration(defaults.RepeatInterval, func(err error) { + errs = append(errs, fmt.Errorf("obj '%s' has invalid format of 'repeatInterval': %w", path, err)) + }) + + for idx, route := range obj.Spec.Routes { + p := fmt.Sprintf("%s[%d]", path, idx) + s, err := convertK8sSubRouteToRoute(route, p) + if len(err) > 0 { + errs = append(errs, err...) + } else { + result.Routes = append(result.Routes, &s) + } + } + if len(errs) > 0 { + return definitions.Route{}, "", errors.Join(errs...) + } + result.Provenance = "" + return result, obj.ResourceVersion, nil +} + +func convertK8sSubRouteToRoute(r model.Route, path string) (definitions.Route, []error) { + result := definitions.Route{ + GroupByStr: r.GroupBy, + MuteTimeIntervals: r.MuteTimeIntervals, + Routes: make([]*definitions.Route, 0, len(r.Routes)), + Matchers: make(config.Matchers, 0, len(r.Matchers)), + Continue: r.Continue, + } + if r.Receiver != nil { + result.Receiver = *r.Receiver + } + var errs []error + result.GroupWait = parsePrometheusDuration(r.GroupWait, func(err error) { + errs = append(errs, fmt.Errorf("route '%s' has invalid format of 'groupWait': %w", path, err)) + }) + result.GroupInterval = parsePrometheusDuration(r.GroupInterval, func(err error) { + errs = append(errs, fmt.Errorf("route '%s' has invalid format of 'groupInterval': %w", path, err)) + }) + result.RepeatInterval = parsePrometheusDuration(r.RepeatInterval, func(err error) { + errs = append(errs, fmt.Errorf("route '%s' has invalid format of 'repeatInterval': %w", path, err)) + }) + + for _, matcher := range r.Matchers { + var mt labels.MatchType + switch matcher.Type { + case model.MatcherTypeEqual: + mt = labels.MatchEqual + case model.MatcherTypeNotEqual: + mt = labels.MatchNotEqual + case model.MatcherTypeEqualRegex: + mt = labels.MatchRegexp + case model.MatcherTypeNotEqualRegex: + mt = labels.MatchNotRegexp + default: + errs = append(errs, fmt.Errorf("route '%s' has unsupported matcher type: %s", path, matcher.Type)) + continue + } + + m, err := labels.NewMatcher(mt, matcher.Label, matcher.Value) + if err != nil { + errs = append(errs, fmt.Errorf("route '%s' has illegal matcher: %w", path, err)) + continue + } + result.ObjectMatchers = append(result.ObjectMatchers, m) + } + + for idx, route := range r.Routes { + p := fmt.Sprintf("%s[%d]", path, idx) + s, err := convertK8sSubRouteToRoute(route, p) + if len(err) > 0 { + errs = append(errs, err...) + } else { + result.Routes = append(result.Routes, &s) + } + } + return result, errs +} + +func optionalPrometheusDurationToString(d *promModel.Duration) *string { + if d != nil { + result := d.String() + return &result + } + return nil +} + +func parsePrometheusDuration(s *string, callback func(e error)) *promModel.Duration { + if s == nil || *s == "" { + return nil + } + d, err := promModel.ParseDuration(*s) + if err != nil { + callback(err) + return nil + } + return &d +} diff --git a/pkg/registry/apis/alerting/notifications/routing_tree/legacy_storage.go b/pkg/registry/apis/alerting/notifications/routing_tree/legacy_storage.go new file mode 100644 index 00000000000..d2b80ba2ffe --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/routing_tree/legacy_storage.go @@ -0,0 +1,166 @@ +package routing_tree + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + notifications "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +var ( + _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) +) + +var resourceInfo = notifications.RouteResourceInfo + +type RouteService interface { + GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) + UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) (definitions.Route, string, error) + ResetPolicyTree(ctx context.Context, orgID int64, p alerting_models.Provenance) (definitions.Route, error) +} + +type legacyStorage struct { + service RouteService + namespacer request.NamespaceMapper + tableConverter rest.TableConvertor +} + +func (s *legacyStorage) New() runtime.Object { + return resourceInfo.NewFunc() +} + +func (s *legacyStorage) Destroy() {} + +func (s *legacyStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyStorage) GetSingularName() string { + return resourceInfo.GetSingularName() +} + +func (s *legacyStorage) NewList() runtime.Object { + return resourceInfo.NewListFunc() +} + +func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyStorage) getUserDefinedRoutingTree(ctx context.Context) (*notifications.RoutingTree, error) { + orgId, err := request.OrgIDForList(ctx) + if err != nil { + return nil, err + } + + res, version, err := s.service.GetPolicyTree(ctx, orgId) + if err != nil { + return nil, err + } + return convertToK8sResource(orgId, res, version, s.namespacer) +} + +func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) { + user, err := s.getUserDefinedRoutingTree(ctx) + if err != nil { + return nil, err + } + return ¬ifications.RoutingTreeList{ + Items: []notifications.RoutingTree{ + *user, + }, + }, nil +} + +func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) { + if name != notifications.UserDefinedRoutingTreeName { + return nil, errors.NewNotFound(resourceInfo.GroupResource(), name) + } + return s.getUserDefinedRoutingTree(ctx) +} + +func (s *legacyStorage) Create(_ context.Context, + _ runtime.Object, + _ rest.ValidateObjectFunc, + _ *metav1.CreateOptions, +) (runtime.Object, error) { + return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "create") +} + +func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, _ rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + if name != notifications.UserDefinedRoutingTreeName { + return nil, false, errors.NewNotFound(resourceInfo.GroupResource(), name) + } + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + old, err := s.Get(ctx, notifications.UserDefinedRoutingTreeName, nil) + if err != nil { + return old, false, err + } + obj, err := objInfo.UpdatedObject(ctx, old) + if err != nil { + return old, false, err + } + if updateValidation != nil { + if err := updateValidation(ctx, obj, old); err != nil { + return nil, false, err + } + } + p, ok := obj.(*notifications.RoutingTree) + if !ok { + return nil, false, fmt.Errorf("expected %s but got %s", notifications.ReceiverResourceInfo.GroupVersionKind(), obj.GetObjectKind().GroupVersionKind()) + } + + model, version, err := convertToDomainModel(p) + if err != nil { + return nil, false, err + } + updated, updatedVersion, err := s.service.UpdatePolicyTree(ctx, info.OrgID, model, alerting_models.ProvenanceNone, version) + if err != nil { + return nil, false, err + } + + obj, err = convertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer) + return obj, false, err +} + +// Delete implements rest.GracefulDeleter. It is needed for API server to not crash when it registers DeleteCollection method +func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, opts *metav1.DeleteOptions) (runtime.Object, bool, error) { + if name != notifications.UserDefinedRoutingTreeName { + return nil, false, errors.NewNotFound(resourceInfo.GroupResource(), name) + } + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + old, err := s.Get(ctx, name, nil) + if err != nil { + return old, false, err + } + + if deleteValidation != nil { + if err = deleteValidation(ctx, old); err != nil { + return nil, false, err + } + } + _, err = s.service.ResetPolicyTree(ctx, info.OrgID, alerting_models.ProvenanceNone) // TODO add support for dry-run option + return old, false, err +} + +func (s *legacyStorage) DeleteCollection(_ context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *internalversion.ListOptions) (runtime.Object, error) { + return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "delete") +} diff --git a/pkg/registry/apis/alerting/notifications/routing_tree/storage.go b/pkg/registry/apis/alerting/notifications/routing_tree/storage.go new file mode 100644 index 00000000000..b637a2e691d --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/routing_tree/storage.go @@ -0,0 +1,17 @@ +package routing_tree + +import ( + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" +) + +func NewStorage(legacySvc RouteService, namespacer request.NamespaceMapper) (rest.Storage, error) { + legacyStore := &legacyStorage{ + service: legacySvc, + namespacer: namespacer, + tableConverter: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()), + } + // TODO implement dual write for routes. This API is a special beast - the resource is singleton. + return legacyStore, nil +} diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 9ee384540c1..fbd04342f6a 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -460,6 +460,10 @@ const ( ActionAlertingReceiversPermissionsRead = "receivers.permissions:read" ActionAlertingReceiversPermissionsWrite = "receivers.permissions:write" + // Alerting routes policies actions + ActionAlertingRoutesRead = "alert.notifications.routes:read" + ActionAlertingRoutesWrite = "alert.notifications.routes:write" + // External alerting rule actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system. ActionAlertingRuleExternalWrite = "alert.rules.external:write" ActionAlertingRuleExternalRead = "alert.rules.external:read" diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go index 55b08fb3cbc..7bbd138b7b4 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt_alerts_test.go @@ -258,6 +258,6 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se Routes: []*definition.Route{&child}, } - err := service.ngAlert.Api.Policies.UpdatePolicyTree(ctx, user.GetOrgID(), tree, "", "") + _, _, err := service.ngAlert.Api.Policies.UpdatePolicyTree(ctx, user.GetOrgID(), tree, "", "") require.NoError(t, err) } diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index 1dad2138a87..e675a47bc27 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -204,15 +204,39 @@ var ( }, } + routesReaderRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.routes:reader", + DisplayName: "Notification Policies Reader", + Description: "Read all notification policies in Grafana alerting", + Group: AlertRolesGroup, + Permissions: accesscontrol.ConcatPermissions([]accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingRoutesRead}, + }), + }, + } + + routesWriterRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.routes:writer", + DisplayName: "Notification Policies Writer", + Description: "Update and reset notification policies in Grafana alerting", + Group: AlertRolesGroup, + Permissions: accesscontrol.ConcatPermissions(routesReaderRole.Role.Permissions, []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingRoutesWrite}, + }), + }, + } + notificationsReaderRole = accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: accesscontrol.FixedRolePrefix + "alerting.notifications:reader", DisplayName: "Notifications Reader", Description: "Read notification policies and contact points in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, timeIntervalsReaderRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, timeIntervalsReaderRole.Role.Permissions, routesReaderRole.Role.Permissions, []accesscontrol.Permission{ { - Action: accesscontrol.ActionAlertingNotificationsRead, + Action: accesscontrol.ActionAlertingNotificationsRead, // TODO remove when we decide tò limit access to raw config API }, { Action: accesscontrol.ActionAlertingNotificationsExternalRead, @@ -228,9 +252,9 @@ var ( DisplayName: "Notifications Writer", Description: "Add, update, and delete contact points and notification policies in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, timeIntervalsWriterRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, timeIntervalsWriterRole.Role.Permissions, routesWriterRole.Role.Permissions, []accesscontrol.Permission{ { - Action: accesscontrol.ActionAlertingNotificationsWrite, + Action: accesscontrol.ActionAlertingNotificationsWrite, // TODO remove when we decide tò limit access to raw config API }, { Action: accesscontrol.ActionAlertingNotificationsExternalWrite, @@ -360,7 +384,7 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu } if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { - fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole) + fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole) } return service.DeclareFixedRoles(fixedRoles...) diff --git a/pkg/services/ngalert/api/api_alertmanager_guards.go b/pkg/services/ngalert/api/api_alertmanager_guards.go index be03baab717..21e41b60966 100644 --- a/pkg/services/ngalert/api/api_alertmanager_guards.go +++ b/pkg/services/ngalert/api/api_alertmanager_guards.go @@ -58,7 +58,10 @@ func (srv AlertmanagerSrv) k8sApiServiceGuard(currentConfig apimodels.GettableUs func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { reporter := cmputil.DiffReporter{} - options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{})} + options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{}), cmp.Transformer("", func(regexp amConfig.Regexp) any { + r, _ := regexp.MarshalYAML() + return r + })} routesEqual := cmp.Equal(currentConfig.AlertmanagerConfig.Route, newConfig.AlertmanagerConfig.Route, options...) if !routesEqual && currentConfig.AlertmanagerConfig.Route.Provenance != apimodels.Provenance(ngmodels.ProvenanceNone) { return fmt.Errorf("policies were provisioned and cannot be changed through the UI") diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index ccdfde76c75..36385e4e55d 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -52,7 +52,7 @@ type TemplateService interface { type NotificationPolicyService interface { GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) - UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) error + UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) (definitions.Route, string, error) ResetPolicyTree(ctx context.Context, orgID int64, provenance alerting_models.Provenance) (definitions.Route, error) } @@ -109,7 +109,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTreeExport(c *contextmodel.ReqContext) func (srv *ProvisioningSrv) RoutePutPolicyTree(c *contextmodel.ReqContext, tree definitions.Route) response.Response { provenance := determineProvenance(c) - err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.SignedInUser.GetOrgID(), tree, alerting_models.Provenance(provenance), "") + _, _, err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.SignedInUser.GetOrgID(), tree, alerting_models.Provenance(provenance), "") if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") } diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 16c13054b88..80c75b456dc 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -2035,13 +2035,13 @@ func (f *fakeNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID return result, "", nil } -func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, _ string) error { +func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) { if orgID != 1 { - return store.ErrNoAlertmanagerConfiguration + return definitions.Route{}, "", store.ErrNoAlertmanagerConfiguration } f.tree = tree f.prov = p - return nil + return tree, "some", nil } func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { @@ -2055,8 +2055,8 @@ func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context return definitions.Route{}, "", fmt.Errorf("something went wrong") } -func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, _ string) error { - return fmt.Errorf("something went wrong") +func (f *fakeFailingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) { + return definitions.Route{}, "", fmt.Errorf("something went wrong") } func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { @@ -2069,8 +2069,8 @@ func (f *fakeRejectingNotificationPolicyService) GetPolicyTree(ctx context.Conte return definitions.Route{}, "", nil } -func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, _ string) error { - return fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation) +func (f *fakeRejectingNotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) { + return definitions.Route{}, "", fmt.Errorf("%w: invalid policy tree", provisioning.ErrValidation) } func (f *fakeRejectingNotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { diff --git a/pkg/services/ngalert/provisioning/errors.go b/pkg/services/ngalert/provisioning/errors.go index febdbe49923..f13d2315df5 100644 --- a/pkg/services/ngalert/provisioning/errors.go +++ b/pkg/services/ngalert/provisioning/errors.go @@ -28,6 +28,11 @@ var ( ErrContactPointReferenced = errutil.Conflict("alerting.notifications.contact-points.referenced", errutil.WithPublicMessage("Contact point is currently referenced by a notification policy.")) ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules.")) + + ErrRouteInvalidFormat = errutil.BadRequest("alerting.notifications.routes.invalidFormat").MustTemplate( + "Invalid format of the submitted route.", + errutil.WithPublic("Invalid format of the submitted route: {{.Public.Error}}. Correct the payload and try again."), + ) ) // MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template @@ -90,3 +95,12 @@ func MakeErrTimeIntervalDependentResourcesProvenance(usedByRoutes bool, rules [] Public: data, }) } + +func MakeErrRouteInvalidFormat(err error) error { + return ErrRouteInvalidFormat.Build(errutil.TemplateData{ + Public: map[string]any{ + "Error": err.Error(), + }, + Error: err, + }) +} diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index afba5f0c0d9..1315f64fdd5 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -61,40 +61,40 @@ func (nps *NotificationPolicyService) GetPolicyTree(ctx context.Context, orgID i return result, version, nil } -func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) error { +func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p models.Provenance, version string) (definitions.Route, string, error) { err := tree.Validate() if err != nil { - return fmt.Errorf("%w: %s", ErrValidation, err.Error()) + return definitions.Route{}, "", MakeErrRouteInvalidFormat(err) } revision, err := nps.configStore.Get(ctx, orgID) if err != nil { - return err + return definitions.Route{}, "", err } err = nps.checkOptimisticConcurrency(*revision.Config.AlertmanagerConfig.Route, p, version, "update") if err != nil { - return err + return definitions.Route{}, "", err } // check that provenance is not changed in an invalid way storedProvenance, err := nps.provenanceStore.GetProvenance(ctx, &tree, orgID) if err != nil { - return err + return definitions.Route{}, "", err } if err := nps.validator(storedProvenance, p); err != nil { - return err - } - - receivers, err := nps.receiversToMap(revision.Config.AlertmanagerConfig.Receivers) - if err != nil { - return err + return definitions.Route{}, "", err } + receivers := map[string]struct{}{} receivers[""] = struct{}{} // Allow empty receiver (inheriting from parent) + for _, receiver := range revision.GetReceivers(nil) { + receivers[receiver.Name] = struct{}{} + } + err = tree.ValidateReceivers(receivers) if err != nil { - return fmt.Errorf("%w: %s", ErrValidation, err.Error()) + return definitions.Route{}, "", MakeErrRouteInvalidFormat(err) } timeIntervals := map[string]struct{}{} @@ -106,17 +106,21 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI } err = tree.ValidateMuteTimes(timeIntervals) if err != nil { - return fmt.Errorf("%w: %s", ErrValidation, err.Error()) + return definitions.Route{}, "", MakeErrRouteInvalidFormat(err) } revision.Config.AlertmanagerConfig.Config.Route = &tree - return nps.xact.InTransaction(ctx, func(ctx context.Context) error { + err = nps.xact.InTransaction(ctx, func(ctx context.Context) error { if err := nps.configStore.Save(ctx, revision, orgID); err != nil { return err } return nps.provenanceStore.SetProvenance(ctx, &tree, orgID, p) }) + if err != nil { + return definitions.Route{}, "", err + } + return tree, calculateRouteFingerprint(tree), nil } func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64, provenance models.Provenance) (definitions.Route, error) { @@ -159,14 +163,6 @@ func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID return *route, nil } -func (nps *NotificationPolicyService) receiversToMap(records []*definitions.PostableApiReceiver) (map[string]struct{}, error) { - receivers := map[string]struct{}{} - for _, receiver := range records { - receivers[receiver.Name] = struct{}{} - } - return receivers, nil -} - func (nps *NotificationPolicyService) ensureDefaultReceiverExists(cfg *definitions.PostableUserConfig, defaultCfg *definitions.PostableUserConfig) error { defaultRcv := cfg.AlertmanagerConfig.Route.Receiver diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index 9fa49c8f5f1..7723765a3b3 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -84,8 +84,8 @@ func TestUpdatePolicyTree(t *testing.T) { "not-existing", }, } - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) - require.ErrorIs(t, err, ErrValidation) + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) + require.ErrorIs(t, err, ErrRouteInvalidFormat) }) t.Run("ErrValidation if root route has no receiver", func(t *testing.T) { @@ -97,8 +97,8 @@ func TestUpdatePolicyTree(t *testing.T) { newRoute := definitions.Route{ Receiver: "", } - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) - require.ErrorIs(t, err, ErrValidation) + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) + require.ErrorIs(t, err, ErrRouteInvalidFormat) }) t.Run("ErrValidation if referenced receiver does not exist", func(t *testing.T) { @@ -113,8 +113,8 @@ func TestUpdatePolicyTree(t *testing.T) { newRoute := definitions.Route{ Receiver: "unknown", } - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) - require.ErrorIs(t, err, ErrValidation) + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) + require.ErrorIs(t, err, ErrRouteInvalidFormat) t.Run("including sub-routes", func(t *testing.T) { newRoute := definitions.Route{ @@ -123,8 +123,8 @@ func TestUpdatePolicyTree(t *testing.T) { {Receiver: "unknown"}, }, } - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) - require.ErrorIs(t, err, ErrValidation) + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) + require.ErrorIs(t, err, ErrRouteInvalidFormat) }) }) @@ -137,7 +137,7 @@ func TestUpdatePolicyTree(t *testing.T) { newRoute := definitions.Route{ Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name, } - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, "wrong-version") + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, "wrong-version") require.ErrorIs(t, err, ErrVersionConflict) }) @@ -161,7 +161,7 @@ func TestUpdatePolicyTree(t *testing.T) { return expectedErr } - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion) require.ErrorIs(t, err, expectedErr) assert.Len(t, prov.Calls, 1) @@ -180,8 +180,10 @@ func TestUpdatePolicyTree(t *testing.T) { expectedRev.ConcurrencyToken = rev.ConcurrencyToken expectedRev.Config.AlertmanagerConfig.Route = &route - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, defaultVersion) + result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, defaultVersion) require.NoError(t, err) + assert.Equal(t, newRoute, result) + assert.Equal(t, calculateRouteFingerprint(newRoute), version) assert.Len(t, store.Calls, 2) assert.Equal(t, "Save", store.Calls[1].Method) @@ -210,8 +212,10 @@ func TestUpdatePolicyTree(t *testing.T) { expectedRev.Config.AlertmanagerConfig.Route = &newRoute expectedRev.ConcurrencyToken = rev.ConcurrencyToken - err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, "") + result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceAPI, "") require.NoError(t, err) + assert.Equal(t, newRoute, result) + assert.Equal(t, calculateRouteFingerprint(newRoute), version) assert.Len(t, store.Calls, 2) assert.Equal(t, "Save", store.Calls[1].Method) diff --git a/pkg/services/provisioning/alerting/notification_policy_provisioner.go b/pkg/services/provisioning/alerting/notification_policy_provisioner.go index 63f6cbe9ce3..cc55cab86f7 100644 --- a/pkg/services/provisioning/alerting/notification_policy_provisioner.go +++ b/pkg/services/provisioning/alerting/notification_policy_provisioner.go @@ -31,7 +31,7 @@ func (c *defaultNotificationPolicyProvisioner) Provision(ctx context.Context, files []*AlertingFile) error { for _, file := range files { for _, np := range file.Policies { - err := c.notificationPolicyService.UpdatePolicyTree(ctx, np.OrgID, + _, _, err := c.notificationPolicyService.UpdatePolicyTree(ctx, np.OrgID, np.Policy, models.ProvenanceFile, "") if err != nil { return fmt.Errorf("%s: %w", file.Filename, err) diff --git a/pkg/tests/apis/alerting/notifications/routing_tree/routing_tree_test.go b/pkg/tests/apis/alerting/notifications/routing_tree/routing_tree_test.go new file mode 100644 index 00000000000..e71114bc23f --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/routing_tree/routing_tree_test.go @@ -0,0 +1,643 @@ +package routing_tree + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/grafana/alerting/definition" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/generated/clientset/versioned" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/tests/api/alerting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/grafana/grafana/pkg/util" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func getTestHelper(t *testing.T) *apis.K8sTestHelper { + return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{ + featuremgmt.FlagAlertingApiServer, + }, + }) +} + +func TestIntegrationNotAllowedMethods(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + client := adminK8sClient.NotificationsV0alpha1().RoutingTrees("default") + + route := &v0alpha1.RoutingTree{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.RoutingTreeSpec{}, + } + _, err = client.Create(ctx, route, v1.CreateOptions{}) + assert.Error(t, err) + require.Truef(t, errors.IsMethodNotSupported(err), "Expected MethodNotSupported but got %s", err) + + err = client.DeleteCollection(ctx, v1.DeleteOptions{}, v1.ListOptions{}) + assert.Error(t, err) + require.Truef(t, errors.IsMethodNotSupported(err), "Expected MethodNotSupported but got %s", err) +} + +func TestIntegrationAccessControl(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + org1 := helper.Org1 + + type testCase struct { + user apis.User + canRead bool + canUpdate bool + } + + reader := helper.CreateUser("RoutesReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingRoutesRead, + }, + }, + }) + writer := helper.CreateUser("RoutesWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingRoutesRead, + accesscontrol.ActionAlertingRoutesWrite, + }, + }, + }) + none := helper.CreateUser("RoutesNone", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{}) + legacyReader := helper.CreateUser("LegacyRoutesReader", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsRead, + }, + }, + }) + legacyWriter := helper.CreateUser("LegacyRoutesWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsRead, + accesscontrol.ActionAlertingNotificationsWrite, + }, + }, + }) + + testCases := []testCase{ + { + user: none, + }, + { + user: org1.Admin, + canRead: true, + canUpdate: true, + }, + { + user: org1.Editor, + canRead: true, + canUpdate: true, + }, + { + user: org1.Viewer, + canRead: true, + }, + { + user: reader, + canRead: true, + }, + { + user: writer, + canRead: true, + canUpdate: true, + }, + { + user: legacyReader, + canRead: true, + }, + { + user: legacyWriter, + canRead: true, + canUpdate: true, + }, + } + + admin := org1.Admin + adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().RoutingTrees("default") + + for _, tc := range testCases { + t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { + k8sClient, err := versioned.NewForConfig(tc.user.NewRestConfig()) + require.NoError(t, err) + client := k8sClient.NotificationsV0alpha1().RoutingTrees("default") + + if tc.canRead { + t.Run("should be able to list routing trees", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 1) + require.Equal(t, v0alpha1.UserDefinedRoutingTreeName, list.Items[0].Name) + }) + + t.Run("should be able to read routing trees by resource identifier", func(t *testing.T) { + _, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + + t.Run("should get NotFound if resource does not exist", func(t *testing.T) { + _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to list routing trees", func(t *testing.T) { + _, err := client.List(ctx, v1.ListOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + + t.Run("should be forbidden to read routing tree by name", func(t *testing.T) { + _, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should get forbidden even if name does not exist", func(t *testing.T) { + _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + } + + current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + expected := current.DeepCopy() + expected.Spec.Routes = []v0alpha1.Route{ + { + Matchers: []v0alpha1.Matcher{ + { + Label: "test", + Type: v0alpha1.MatcherTypeEqual, + Value: "test", + }, + }, + }, + } + + d, err := json.Marshal(expected) + require.NoError(t, err) + + if tc.canUpdate { + t.Run("should be able to update routing tree", func(t *testing.T) { + updated, err := client.Update(ctx, expected, v1.UpdateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + + expected = updated + + t.Run("should get NotFound if name does not exist", func(t *testing.T) { + up := expected.DeepCopy() + up.Name = "notFound" + _, err := client.Update(ctx, up, v1.UpdateOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to update routing tree", func(t *testing.T) { + _, err := client.Update(ctx, expected, v1.UpdateOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should get forbidden even if resource does not exist", func(t *testing.T) { + up := expected.DeepCopy() + up.Name = "notFound" + _, err := client.Update(ctx, up, v1.UpdateOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + } + + if tc.canUpdate { + t.Run("should be able to reset routing tree", func(t *testing.T) { + err := client.Delete(ctx, expected.Name, v1.DeleteOptions{}) + require.NoError(t, err) + + t.Run("should get NotFound if name does not exist", func(t *testing.T) { + err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to reset routing tree", func(t *testing.T) { + err := client.Delete(ctx, expected.Name, v1.DeleteOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should be forbidden even if resource does not exist", func(t *testing.T) { + err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{})) + } + }) + + err = adminClient.Delete(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.DeleteOptions{}) + require.NoError(t, err) + } +} + +func TestIntegrationProvisioning(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + org := helper.Org1 + + admin := org.Admin + adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().RoutingTrees("default") + + env := helper.GetEnv() + ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient()) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) + require.NoError(t, err) + + current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "none", current.GetProvenanceStatus()) + + t.Run("should provide provenance status", func(t *testing.T) { + require.NoError(t, db.SetProvenance(ctx, &definitions.Route{}, admin.Identity.GetOrgID(), "API")) + + got, err := adminClient.Get(ctx, current.Name, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "API", got.GetProvenanceStatus()) + }) + t.Run("should not let update if provisioned", func(t *testing.T) { + updated := current.DeepCopy() + updated.Spec.Routes = []v0alpha1.Route{ + { + Matchers: []v0alpha1.Matcher{ + { + Label: "test", + Type: v0alpha1.MatcherTypeNotEqual, + Value: "123", + }, + }, + }, + } + + _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + + t.Run("should not let delete if provisioned", func(t *testing.T) { + err := adminClient.Delete(ctx, current.Name, v1.DeleteOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) +} + +func TestIntegrationOptimisticConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().RoutingTrees("default") + + current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + require.NotEmpty(t, current.ResourceVersion) + + t.Run("should forbid if version does not match", func(t *testing.T) { + updated := current.DeepCopy() + updated.ResourceVersion = "test" + _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) + }) + t.Run("should update if version matches", func(t *testing.T) { + updated := current.DeepCopy() + updated.Spec.Defaults.GroupBy = append(updated.Spec.Defaults.GroupBy, "data") + actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.EqualValues(t, updated.Spec, actualUpdated.Spec) + require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion) + }) + t.Run("should update if version is empty", func(t *testing.T) { + current, err = adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + updated := current.DeepCopy() + updated.ResourceVersion = "" + updated.Spec.Routes = append(updated.Spec.Routes, v0alpha1.Route{Continue: true}) + + actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.EqualValues(t, updated.Spec, actualUpdated.Spec) + require.NotEqual(t, current.ResourceVersion, actualUpdated.ResourceVersion) + }) +} + +func TestIntegrationDataConsistency(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + cliCfg := helper.Org1.Admin.NewRestConfig() + legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) + + adminK8sClient, err := versioned.NewForConfig(cliCfg) + require.NoError(t, err) + client := adminK8sClient.NotificationsV0alpha1().RoutingTrees("default") + + receiver := "grafana-default-email" + timeInterval := "test-time-interval" + createRoute := func(t *testing.T, route definitions.Route) { + t.Helper() + cfg, _, _ := legacyCli.GetAlertmanagerConfigWithStatus(t) + var receivers []*definitions.PostableApiReceiver + for _, apiReceiver := range cfg.AlertmanagerConfig.Receivers { + var recv []*definitions.PostableGrafanaReceiver + for _, r := range apiReceiver.GettableGrafanaReceivers.GrafanaManagedReceivers { + recv = append(recv, &definitions.PostableGrafanaReceiver{ + UID: r.UID, + Name: r.Name, + Type: r.Type, + DisableResolveMessage: r.DisableResolveMessage, + Settings: r.Settings, + }) + } + receivers = append(receivers, &definitions.PostableApiReceiver{ + Receiver: config.Receiver{Name: apiReceiver.Name}, + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{GrafanaManagedReceivers: recv}, + }) + } + _, err := legacyCli.PostConfiguration(t, definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definition.Config{ + Route: &route, + TimeIntervals: []config.TimeInterval{ + { + Name: timeInterval, + }, + }, + }, + Receivers: receivers, + }, + }) + require.NoError(t, err) + } + + var regex config.Regexp + require.NoError(t, json.Unmarshal([]byte(`".*"`), ®ex)) + + ensureMatcher := func(t *testing.T, mt labels.MatchType, lbl, val string) *labels.Matcher { + m, err := labels.NewMatcher(mt, lbl, val) + require.NoError(t, err) + return m + } + + t.Run("all matchers are handled", func(t *testing.T) { + t.Run("can read all legacy matchers", func(t *testing.T) { + route := definitions.Route{ + Receiver: receiver, + Routes: []*definitions.Route{ + { + Match: map[string]string{ + "label_match": "test-123", + }, + MatchRE: map[string]config.Regexp{ + "label_re": regex, + }, + Matchers: config.Matchers{ + ensureMatcher(t, labels.MatchRegexp, "label_matchers", "test-321"), + }, + ObjectMatchers: definitions.ObjectMatchers{ + ensureMatcher(t, labels.MatchNotRegexp, "object-label-matchers", "test-456"), + }, + }, + }, + } + createRoute(t, route) + tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, []v0alpha1.Matcher{ + { + Label: "label_match", + Type: v0alpha1.MatcherTypeEqual, + Value: "test-123", + }, + { + Label: "label_re", + Type: v0alpha1.MatcherTypeEqualRegex, + Value: ".*", + }, + { + Label: "label_matchers", + Type: v0alpha1.MatcherTypeEqualRegex, + Value: "test-321", + }, + { + Label: "object-label-matchers", + Type: v0alpha1.MatcherTypeNotEqualRegex, + Value: "test-456", + }, + }, tree.Spec.Routes[0].Matchers) + }) + t.Run("should save into ObjectMatchers", func(t *testing.T) { + route := definitions.Route{ + Receiver: receiver, + Routes: []*definitions.Route{ + { + Match: map[string]string{ + "oldmatch": "123", + }, + }, + { + MatchRE: map[string]config.Regexp{ + "oldmatchre": regex, + }, + }, + { + Matchers: config.Matchers{ + ensureMatcher(t, labels.MatchNotEqual, "matchers", "v"), + }, + }, + { + ObjectMatchers: definitions.ObjectMatchers{ + ensureMatcher(t, labels.MatchEqual, "t2", "v2"), + }, + }, + }, + } + createRoute(t, route) + cfg, _, _ := legacyCli.GetAlertmanagerConfigWithStatus(t) + expectedRoutes := cfg.AlertmanagerConfig.Route.Routes // autogenerated route is the first one + expectedRoutes[1].Match = nil + expectedRoutes[1].ObjectMatchers = definitions.ObjectMatchers{ + ensureMatcher(t, labels.MatchEqual, "oldmatch", "123"), + } + expectedRoutes[2].MatchRE = nil + expectedRoutes[2].ObjectMatchers = definitions.ObjectMatchers{ + ensureMatcher(t, labels.MatchRegexp, "oldmatchre", ".*"), + } + expectedRoutes[3].Matchers = nil + expectedRoutes[3].ObjectMatchers = definitions.ObjectMatchers{ + ensureMatcher(t, labels.MatchNotEqual, "matchers", "v"), + } + + tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + _, err = client.Update(ctx, tree, v1.UpdateOptions{}) + require.NoError(t, err) + + cfg, _, _ = legacyCli.GetAlertmanagerConfigWithStatus(t) + routes := cfg.AlertmanagerConfig.Route.Routes + require.EqualValues(t, expectedRoutes, routes) + }) + }) + + route := definitions.Route{ + Receiver: receiver, + GroupByStr: []string{"test-123", "test-456"}, + GroupWait: util.Pointer(model.Duration(30 * time.Second)), + GroupInterval: util.Pointer(model.Duration(1 * time.Minute)), + RepeatInterval: util.Pointer(model.Duration(24 * time.Hour)), + Routes: []*definitions.Route{ + { + ObjectMatchers: definitions.ObjectMatchers{ + ensureMatcher(t, labels.MatchNotEqual, "m", "1"), + ensureMatcher(t, labels.MatchEqual, "n", "1"), + ensureMatcher(t, labels.MatchRegexp, "o", "1"), + ensureMatcher(t, labels.MatchNotRegexp, "p", "1"), + }, + Receiver: receiver, + GroupByStr: []string{"test-789"}, + GroupWait: util.Pointer(model.Duration(2 * time.Minute)), + GroupInterval: util.Pointer(model.Duration(5 * time.Minute)), + RepeatInterval: util.Pointer(model.Duration(30 * time.Hour)), + MuteTimeIntervals: []string{timeInterval}, + Continue: true, + }, + }, + } + createRoute(t, route) + + t.Run("correctly reads all fields", func(t *testing.T) { + tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, v0alpha1.RouteDefaults{ + Receiver: receiver, + GroupBy: []string{"test-123", "test-456"}, + GroupWait: util.Pointer("30s"), + GroupInterval: util.Pointer("1m"), + RepeatInterval: util.Pointer("1d"), + }, tree.Spec.Defaults) + assert.Len(t, tree.Spec.Routes, 1) + assert.Equal(t, v0alpha1.Route{ + Continue: true, + Receiver: util.Pointer(receiver), + GroupBy: []string{"test-789"}, + GroupWait: util.Pointer("2m"), + GroupInterval: util.Pointer("5m"), + RepeatInterval: util.Pointer("1d6h"), + MuteTimeIntervals: []string{timeInterval}, + Matchers: []v0alpha1.Matcher{ + { + Label: "m", + Type: v0alpha1.MatcherTypeNotEqual, + Value: "1", + }, + { + Label: "n", + Type: v0alpha1.MatcherTypeEqual, + Value: "1", + }, + { + Label: "o", + Type: v0alpha1.MatcherTypeEqualRegex, + Value: "1", + }, + { + Label: "p", + Type: v0alpha1.MatcherTypeNotEqualRegex, + Value: "1", + }, + }, + }, tree.Spec.Routes[0]) + }) + + t.Run("correctly save all fields", func(t *testing.T) { + before, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t) + require.Equalf(t, http.StatusOK, status, body) + tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + tree.Spec.Defaults.GroupBy = []string{"test-123", "test-456", "test-789"} + require.NoError(t, err) + _, err = client.Update(ctx, tree, v1.UpdateOptions{}) + require.NoError(t, err) + + before.AlertmanagerConfig.Route.GroupByStr = []string{"test-123", "test-456", "test-789"} + before.AlertmanagerConfig.Route.GroupBy = []model.LabelName{"test-123", "test-456", "test-789"} + + after, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t) + require.Equalf(t, http.StatusOK, status, body) + require.Equal(t, before, after) + }) +}