mirror of https://github.com/grafana/grafana
Alerting: Notifications Routes API (#91550)
* Introduce new models RoutingTree, RouteDefaults and Route and api-server to serve them that is backed by provisioning notification policy service. * update method UpdatePolicyTree of notification policy service to return route and new version * declare new actions alert.notifications.routes:read and alert.notifications.routes:write and two corresponding fixed roles. --------- Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com> Co-authored-by: Matthew Jacobson <matthew.jacobson@grafana.com>pull/95368/head
parent
fb0221d561
commit
2deced7d40
@ -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
|
||||||
@ -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] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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"` |
||||||
|
} |
||||||
@ -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,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,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,EndTime |
||||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,StartTime |
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,StartTime |
||||||
|
|||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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{} }), |
||||||
|
} |
||||||
|
} |
||||||
@ -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()) |
||||||
|
} |
||||||
@ -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] |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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") |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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) |
||||||
|
}) |
||||||
|
} |
||||||
Loading…
Reference in new issue