Merge remote-tracking branch 'origin/main' into resource-store

This commit is contained in:
Ryan McKinley 2024-06-21 00:52:10 +03:00
commit b07c4e4210
77 changed files with 3631 additions and 165 deletions

1
.github/CODEOWNERS vendored
View File

@ -71,6 +71,7 @@
/scripts/modowners/ @grafana/grafana-backend-services-squad
/hack/ @grafana/grafana-app-platform-squad
/apps/alerting/ @grafana/alerting-backend
/pkg/api/ @grafana/grafana-backend-group
/pkg/apis/ @grafana/grafana-app-platform-squad
/pkg/bus/ @grafana/grafana-search-and-storage

View File

@ -0,0 +1 @@
module:"notifications"

View File

@ -0,0 +1,37 @@
package core
timeInterval: {
kind: "TimeInterval"
group: "notifications"
apiResource: {
groupOverride: "notifications.alerting.grafana.app"
}
codegen: {
frontend: false
backend: true
}
pluralName: "TimeIntervals"
current: "v0alpha1"
versions: {
"v0alpha1": {
schema: {
#TimeRange: {
start_time: string
end_time: string
}
#Interval: {
times?: [...#TimeRange]
weekdays?: [...string]
days_of_month?: [...string]
months?: [...string]
years?: [...string]
location?: string
}
spec: {
name: string
time_intervals: [...#Interval]
}
}
}
}
}

View File

@ -1304,6 +1304,9 @@ disable_jitter = false
# Retention period for Alertmanager notification log entries.
notification_log_retention = 5d
# Duration for which a resolved alert state transition will continue to be sent to the Alertmanager.
resolved_alert_retention = 15m
[unified_alerting.screenshots]
# Enable screenshots in notifications. You must have either installed the Grafana image rendering
# plugin, or set up Grafana to use a remote rendering service.

View File

@ -792,7 +792,7 @@
;role_attribute_strict = false
;groups_attribute_path =
;id_token_attribute_name =
;team_ids_attribute_path
;team_ids_attribute_path
;auth_url = https://foo.bar/login/oauth/authorize
;token_url = https://foo.bar/login/oauth/access_token
;api_url = https://foo.bar/user
@ -1290,6 +1290,9 @@
# Retention period for Alertmanager notification log entries.
;notification_log_retention = 5d
# Duration for which a resolved alert state transition will continue to be sent to the Alertmanager.
;resolved_alert_retention = 15m
[unified_alerting.screenshots]
# Enable screenshots in notifications. You must have either installed the Grafana image rendering
# plugin, or set up Grafana to use a remote rendering service.
@ -1837,4 +1840,4 @@ timeout = 30s
#################################### Public Dashboards #####################################
[public_dashboards]
# Set to false to disable public dashboards
;enabled = true
;enabled = true

View File

@ -191,6 +191,7 @@ Experimental features might be changed or removed without prior notice.
| `pinNavItems` | Enables pinning of nav items |
| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs |
| `databaseReadReplica` | Use a read replica for some database queries. |
| `alertingApiServer` | Register Alerting APIs with the K8s API server |
## Development feature toggles

View File

@ -22,8 +22,13 @@ go mod download
# the happy path
./hack/update-codegen.sh
```
Note that the script deletes existing openapi go code and regenerates in place so that you will temporarily see
deleted files in your `git status`. After a successful run, you should see them restored.
```
If resource client is not generated for your resource make sure that it follows the k8s guidelines for structuring the resource definition
- the directory is named after resource version, i.e. `<resource_name>/v<version>` (e.g. service/v0alpha1)
- the resource directory contains file `types.go` that includes resource definitions
- the resource definitions are annotated with comment `// +genclient`

View File

@ -198,4 +198,5 @@ export interface FeatureToggles {
databaseReadReplica?: boolean;
zanzana?: boolean;
passScopeToDashboardApi?: boolean;
alertingApiServer?: boolean;
}

View File

@ -0,0 +1,6 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=notifications.alerting.grafana.app
package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"

View File

@ -0,0 +1,53 @@
package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
func init() {
localSchemeBuilder.Register(AddKnownTypes)
}
const (
GROUP = "notifications.alerting.grafana.app"
VERSION = "v0alpha1"
APIVERSION = GROUP + "/" + VERSION
)
var (
TimeIntervalResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"timeintervals", "timeinterval", "TimeIntervals",
func() runtime.Object { return &TimeInterval{} },
func() runtime.Object { return &TimeIntervalList{} },
)
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
// SchemaBuilder is used by standard codegen
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
// Adds the list of known types to the given scheme.
func AddKnownTypes(scheme *runtime.Scheme) error {
return AddKnownTypesGroup(scheme, SchemeGroupVersion)
}
// Adds the list of known types to the given scheme and group version.
func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error {
scheme.AddKnownTypes(g,
&TimeInterval{},
&TimeIntervalList{},
)
metav1.AddToGroupVersion(scheme, g)
return nil
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}

View File

@ -0,0 +1,115 @@
package v0alpha1
import (
"fmt"
"math/rand"
"slices"
"strings"
"time"
"github.com/grafana/grafana/pkg/util"
)
// +k8s:openapi-gen=false
// +k8s:deepcopy-gen=false
type IntervalMutator func(spec *Interval)
// +k8s:openapi-gen=false
// +k8s:deepcopy-gen=false
type IntervalGenerator struct {
mutators []IntervalMutator
}
func (t IntervalGenerator) With(mutators ...IntervalMutator) IntervalGenerator {
return IntervalGenerator{
mutators: append(t.mutators, mutators...),
}
}
func (t IntervalGenerator) generateDaysOfMonth() string {
isRange := rand.Int()%2 == 0
if !isRange {
return fmt.Sprintf("%d", rand.Intn(30)+1)
}
from := rand.Intn(15) + 1
to := rand.Intn(31-from) + from + 1
return fmt.Sprintf("%d:%d", from, to)
}
func (t IntervalGenerator) generateTimeRange() TimeRange {
from := rand.Int63n(1440 / 2) // [0, 719]
to := from + rand.Int63n(1440/2) + 1 // from < ([0,719] + [1,720]) < 1440
return TimeRange{
StartTime: time.Unix(from*60, 0).UTC().Format("15:04"),
EndTime: time.Unix(to*60, 0).UTC().Format("15:04"),
}
}
func (t IntervalGenerator) generateWeekday() string {
day := rand.Intn(7)
return strings.ToLower(time.Weekday(day).String())
}
func (t IntervalGenerator) generateYear() string {
from := 1970 + rand.Intn(100)
if rand.Int()%3 == 0 {
to := 1970 + from + rand.Intn(10) + 1
return fmt.Sprintf("%d:%d", from, to)
}
return fmt.Sprintf("%d", from)
}
func (t IntervalGenerator) generateLocation() *string {
if rand.Int()%3 == 0 {
return nil
}
return util.Pointer("UTC")
}
func (t IntervalGenerator) generateMonth() string {
return fmt.Sprintf("%d", rand.Intn(12)+1)
}
func (t IntervalGenerator) GenerateMany(count int) []Interval {
result := make([]Interval, 0, count)
for i := 0; i < count; i++ {
result = append(result, t.Generate())
}
return result
}
func (t IntervalGenerator) Generate() Interval {
i := Interval{
DaysOfMonth: generateMany(rand.Intn(6), true, t.generateDaysOfMonth),
Location: t.generateLocation(),
Months: generateMany(rand.Intn(3), true, t.generateMonth),
Times: generateMany(rand.Intn(6), true, t.generateTimeRange),
Weekdays: generateMany(rand.Intn(3), true, t.generateWeekday),
Years: generateMany(rand.Intn(3), true, t.generateYear),
}
for _, mutator := range t.mutators {
mutator(&i)
}
return i
}
func generateMany[T comparable](repeatTimes int, unique bool, f func() T) []T {
qty := repeatTimes + 1
result := make([]T, 0, qty)
for i := 0; i < qty; i++ {
r := f()
if unique && slices.Contains(result, r) {
continue
}
result = append(result, f())
}
return result
}
func CopyWith(in Interval, mutators ...IntervalMutator) Interval {
r := *in.DeepCopy()
for _, mut := range mutators {
mut(&r)
}
return r
}

View File

@ -0,0 +1,38 @@
package v0alpha1
// Interval defines model for Interval.
// +k8s:openapi-gen=true
type Interval struct {
// +listType=atomic
DaysOfMonth []string `json:"days_of_month,omitempty"`
// +listType=atomic
Location *string `json:"location,omitempty"`
// +listType=atomic
Months []string `json:"months,omitempty"`
// +listType=atomic
Times []TimeRange `json:"times,omitempty"`
// +listType=atomic
Weekdays []string `json:"weekdays,omitempty"`
// +listType=atomic
Years []string `json:"years,omitempty"`
}
// Spec defines model for Spec.
// +k8s:openapi-gen=true
type TimeIntervalSpec struct {
Name string `json:"name"`
// +listType=atomic
TimeIntervals []Interval `json:"time_intervals"`
}
// TimeRange defines model for TimeRange.
// +k8s:openapi-gen=true
type TimeRange struct {
EndTime string `json:"end_time"`
StartTime string `json:"start_time"`
}

View File

@ -0,0 +1,87 @@
package v0alpha1
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +genclient
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TimeInterval struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Spec TimeIntervalSpec `json:"spec"`
}
func (o *TimeInterval) GetSpec() any {
return o.Spec
}
func (o *TimeInterval) SetSpec(spec any) error {
cast, ok := spec.(TimeIntervalSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *TimeInterval) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *TimeInterval) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *TimeInterval) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *TimeInterval) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *TimeInterval) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *TimeInterval) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:openapi-gen=true
type TimeIntervalList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []TimeInterval `json:"items"`
}

View File

@ -0,0 +1,157 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Interval) DeepCopyInto(out *Interval) {
*out = *in
if in.DaysOfMonth != nil {
in, out := &in.DaysOfMonth, &out.DaysOfMonth
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Location != nil {
in, out := &in.Location, &out.Location
*out = new(string)
**out = **in
}
if in.Months != nil {
in, out := &in.Months, &out.Months
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Times != nil {
in, out := &in.Times, &out.Times
*out = make([]TimeRange, len(*in))
copy(*out, *in)
}
if in.Weekdays != nil {
in, out := &in.Weekdays, &out.Weekdays
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Years != nil {
in, out := &in.Years, &out.Years
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Interval.
func (in *Interval) DeepCopy() *Interval {
if in == nil {
return nil
}
out := new(Interval)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeInterval) DeepCopyInto(out *TimeInterval) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeInterval.
func (in *TimeInterval) DeepCopy() *TimeInterval {
if in == nil {
return nil
}
out := new(TimeInterval)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TimeInterval) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeIntervalList) DeepCopyInto(out *TimeIntervalList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TimeInterval, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeIntervalList.
func (in *TimeIntervalList) DeepCopy() *TimeIntervalList {
if in == nil {
return nil
}
out := new(TimeIntervalList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TimeIntervalList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeIntervalSpec) DeepCopyInto(out *TimeIntervalSpec) {
*out = *in
if in.TimeIntervals != nil {
in, out := &in.TimeIntervals, &out.TimeIntervals
*out = make([]Interval, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeIntervalSpec.
func (in *TimeIntervalSpec) DeepCopy() *TimeIntervalSpec {
if in == nil {
return nil
}
out := new(TimeIntervalSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeRange) DeepCopyInto(out *TimeRange) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeRange.
func (in *TimeRange) DeepCopy() *TimeRange {
if in == nil {
return nil
}
out := new(TimeRange)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,19 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}

View File

@ -0,0 +1,303 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
// This file was autogenerated by openapi-gen. Do not edit it manually!
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval": schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref),
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval": schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref),
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref),
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref),
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange": schema_pkg_apis_alerting_notifications_v0alpha1_TimeRange(ref),
}
}
func schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Interval defines model for Interval.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"days_of_month": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"location": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"months": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"times": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange"),
},
},
},
},
},
"weekdays": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"years": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange"},
}
}
func schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec"),
},
},
},
Required: []string{"metadata", "spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval"),
},
},
},
},
},
},
Required: []string{"metadata", "items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Spec defines model for Spec.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"time_intervals": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval"),
},
},
},
},
},
},
Required: []string{"name", "time_intervals"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval"},
}
}
func schema_pkg_apis_alerting_notifications_v0alpha1_TimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TimeRange defines model for TimeRange.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"end_time": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"start_time": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"end_time", "start_time"},
},
},
}
}

View File

@ -0,0 +1,4 @@
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,TimeIntervalSpec,TimeIntervals
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,EndTime
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,StartTime

View File

@ -0,0 +1,83 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v0alpha1
// IntervalApplyConfiguration represents an declarative configuration of the Interval type for use
// with apply.
type IntervalApplyConfiguration struct {
DaysOfMonth []string `json:"days_of_month,omitempty"`
Location *string `json:"location,omitempty"`
Months []string `json:"months,omitempty"`
Times []TimeRangeApplyConfiguration `json:"times,omitempty"`
Weekdays []string `json:"weekdays,omitempty"`
Years []string `json:"years,omitempty"`
}
// IntervalApplyConfiguration constructs an declarative configuration of the Interval type for use with
// apply.
func Interval() *IntervalApplyConfiguration {
return &IntervalApplyConfiguration{}
}
// WithDaysOfMonth adds the given value to the DaysOfMonth 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 DaysOfMonth field.
func (b *IntervalApplyConfiguration) WithDaysOfMonth(values ...string) *IntervalApplyConfiguration {
for i := range values {
b.DaysOfMonth = append(b.DaysOfMonth, values[i])
}
return b
}
// WithLocation sets the Location 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 Location field is set to the value of the last call.
func (b *IntervalApplyConfiguration) WithLocation(value string) *IntervalApplyConfiguration {
b.Location = &value
return b
}
// WithMonths adds the given value to the Months 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 Months field.
func (b *IntervalApplyConfiguration) WithMonths(values ...string) *IntervalApplyConfiguration {
for i := range values {
b.Months = append(b.Months, values[i])
}
return b
}
// WithTimes adds the given value to the Times 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 Times field.
func (b *IntervalApplyConfiguration) WithTimes(values ...*TimeRangeApplyConfiguration) *IntervalApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithTimes")
}
b.Times = append(b.Times, *values[i])
}
return b
}
// WithWeekdays adds the given value to the Weekdays 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 Weekdays field.
func (b *IntervalApplyConfiguration) WithWeekdays(values ...string) *IntervalApplyConfiguration {
for i := range values {
b.Weekdays = append(b.Weekdays, values[i])
}
return b
}
// WithYears adds the given value to the Years 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 Years field.
func (b *IntervalApplyConfiguration) WithYears(values ...string) *IntervalApplyConfiguration {
for i := range values {
b.Years = append(b.Years, values[i])
}
return b
}

View File

@ -0,0 +1,196 @@
// 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"
)
// TimeIntervalApplyConfiguration represents an declarative configuration of the TimeInterval type for use
// with apply.
type TimeIntervalApplyConfiguration struct {
v1.TypeMetaApplyConfiguration `json:",inline"`
*v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
Spec *TimeIntervalSpecApplyConfiguration `json:"spec,omitempty"`
}
// TimeInterval constructs an declarative configuration of the TimeInterval type for use with
// apply.
func TimeInterval(name, namespace string) *TimeIntervalApplyConfiguration {
b := &TimeIntervalApplyConfiguration{}
b.WithName(name)
b.WithNamespace(namespace)
b.WithKind("TimeInterval")
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 *TimeIntervalApplyConfiguration) WithKind(value string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithAPIVersion(value string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithName(value string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithGenerateName(value string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithNamespace(value string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithUID(value types.UID) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithResourceVersion(value string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithGeneration(value int64) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithCreationTimestamp(value metav1.Time) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithLabels(entries map[string]string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithAnnotations(entries map[string]string) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *TimeIntervalApplyConfiguration {
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 *TimeIntervalApplyConfiguration) WithFinalizers(values ...string) *TimeIntervalApplyConfiguration {
b.ensureObjectMetaApplyConfigurationExists()
for i := range values {
b.Finalizers = append(b.Finalizers, values[i])
}
return b
}
func (b *TimeIntervalApplyConfiguration) 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 *TimeIntervalApplyConfiguration) WithSpec(value *TimeIntervalSpecApplyConfiguration) *TimeIntervalApplyConfiguration {
b.Spec = value
return b
}

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v0alpha1
// TimeIntervalSpecApplyConfiguration represents an declarative configuration of the TimeIntervalSpec type for use
// with apply.
type TimeIntervalSpecApplyConfiguration struct {
Name *string `json:"name,omitempty"`
TimeIntervals []IntervalApplyConfiguration `json:"time_intervals,omitempty"`
}
// TimeIntervalSpecApplyConfiguration constructs an declarative configuration of the TimeIntervalSpec type for use with
// apply.
func TimeIntervalSpec() *TimeIntervalSpecApplyConfiguration {
return &TimeIntervalSpecApplyConfiguration{}
}
// 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 *TimeIntervalSpecApplyConfiguration) WithName(value string) *TimeIntervalSpecApplyConfiguration {
b.Name = &value
return b
}
// WithTimeIntervals adds the given value to the TimeIntervals 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 TimeIntervals field.
func (b *TimeIntervalSpecApplyConfiguration) WithTimeIntervals(values ...*IntervalApplyConfiguration) *TimeIntervalSpecApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithTimeIntervals")
}
b.TimeIntervals = append(b.TimeIntervals, *values[i])
}
return b
}

View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v0alpha1
// TimeRangeApplyConfiguration represents an declarative configuration of the TimeRange type for use
// with apply.
type TimeRangeApplyConfiguration struct {
EndTime *string `json:"end_time,omitempty"`
StartTime *string `json:"start_time,omitempty"`
}
// TimeRangeApplyConfiguration constructs an declarative configuration of the TimeRange type for use with
// apply.
func TimeRange() *TimeRangeApplyConfiguration {
return &TimeRangeApplyConfiguration{}
}
// WithEndTime sets the EndTime 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 EndTime field is set to the value of the last call.
func (b *TimeRangeApplyConfiguration) WithEndTime(value string) *TimeRangeApplyConfiguration {
b.EndTime = &value
return b
}
// WithStartTime sets the StartTime 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 StartTime field is set to the value of the last call.
func (b *TimeRangeApplyConfiguration) WithStartTime(value string) *TimeRangeApplyConfiguration {
b.StartTime = &value
return b
}

View File

@ -5,8 +5,10 @@
package applyconfiguration
import (
v0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
servicev0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/service/v0alpha1"
v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1"
applyconfigurationservicev0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/service/v0alpha1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
)
@ -14,11 +16,21 @@ import (
// apply configuration type exists for the given GroupVersionKind.
func ForKind(kind schema.GroupVersionKind) interface{} {
switch kind {
// Group=service.grafana.app, Version=v0alpha1
case v0alpha1.SchemeGroupVersion.WithKind("ExternalName"):
return &servicev0alpha1.ExternalNameApplyConfiguration{}
case v0alpha1.SchemeGroupVersion.WithKind("ExternalNameSpec"):
return &servicev0alpha1.ExternalNameSpecApplyConfiguration{}
// Group=notifications.alerting.grafana.app, Version=v0alpha1
case v0alpha1.SchemeGroupVersion.WithKind("Interval"):
return &alertingnotificationsv0alpha1.IntervalApplyConfiguration{}
case v0alpha1.SchemeGroupVersion.WithKind("TimeInterval"):
return &alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration{}
case v0alpha1.SchemeGroupVersion.WithKind("TimeIntervalSpec"):
return &alertingnotificationsv0alpha1.TimeIntervalSpecApplyConfiguration{}
case v0alpha1.SchemeGroupVersion.WithKind("TimeRange"):
return &alertingnotificationsv0alpha1.TimeRangeApplyConfiguration{}
// Group=service.grafana.app, Version=v0alpha1
case servicev0alpha1.SchemeGroupVersion.WithKind("ExternalName"):
return &applyconfigurationservicev0alpha1.ExternalNameApplyConfiguration{}
case servicev0alpha1.SchemeGroupVersion.WithKind("ExternalNameSpec"):
return &applyconfigurationservicev0alpha1.ExternalNameSpecApplyConfiguration{}
}
return nil

View File

@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
notificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1"
servicev0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/service/v0alpha1"
discovery "k8s.io/client-go/discovery"
rest "k8s.io/client-go/rest"
@ -16,13 +17,20 @@ import (
type Interface interface {
Discovery() discovery.DiscoveryInterface
NotificationsV0alpha1() notificationsv0alpha1.NotificationsV0alpha1Interface
ServiceV0alpha1() servicev0alpha1.ServiceV0alpha1Interface
}
// Clientset contains the clients for groups.
type Clientset struct {
*discovery.DiscoveryClient
serviceV0alpha1 *servicev0alpha1.ServiceV0alpha1Client
notificationsV0alpha1 *notificationsv0alpha1.NotificationsV0alpha1Client
serviceV0alpha1 *servicev0alpha1.ServiceV0alpha1Client
}
// NotificationsV0alpha1 retrieves the NotificationsV0alpha1Client
func (c *Clientset) NotificationsV0alpha1() notificationsv0alpha1.NotificationsV0alpha1Interface {
return c.notificationsV0alpha1
}
// ServiceV0alpha1 retrieves the ServiceV0alpha1Client
@ -74,6 +82,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
var cs Clientset
var err error
cs.notificationsV0alpha1, err = notificationsv0alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
if err != nil {
return nil, err
}
cs.serviceV0alpha1, err = servicev0alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
if err != nil {
return nil, err
@ -99,6 +111,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
// New creates a new Clientset for the given RESTClient.
func New(c rest.Interface) *Clientset {
var cs Clientset
cs.notificationsV0alpha1 = notificationsv0alpha1.New(c)
cs.serviceV0alpha1 = servicev0alpha1.New(c)
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)

View File

@ -6,6 +6,8 @@ package fake
import (
clientset "github.com/grafana/grafana/pkg/generated/clientset/versioned"
notificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1"
fakenotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake"
servicev0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/service/v0alpha1"
fakeservicev0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/service/v0alpha1/fake"
"k8s.io/apimachinery/pkg/runtime"
@ -65,6 +67,11 @@ var (
_ testing.FakeClient = &Clientset{}
)
// NotificationsV0alpha1 retrieves the NotificationsV0alpha1Client
func (c *Clientset) NotificationsV0alpha1() notificationsv0alpha1.NotificationsV0alpha1Interface {
return &fakenotificationsv0alpha1.FakeNotificationsV0alpha1{Fake: &c.Fake}
}
// ServiceV0alpha1 retrieves the ServiceV0alpha1Client
func (c *Clientset) ServiceV0alpha1() servicev0alpha1.ServiceV0alpha1Interface {
return &fakeservicev0alpha1.FakeServiceV0alpha1{Fake: &c.Fake}

View File

@ -5,6 +5,7 @@
package fake
import (
notificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
@ -17,6 +18,7 @@ var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
notificationsv0alpha1.AddToScheme,
servicev0alpha1.AddToScheme,
}

View File

@ -5,6 +5,7 @@
package scheme
import (
notificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
@ -17,6 +18,7 @@ var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
notificationsv0alpha1.AddToScheme,
servicev0alpha1.AddToScheme,
}

View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by client-gen. DO NOT EDIT.
package v0alpha1
import (
"net/http"
v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/generated/clientset/versioned/scheme"
rest "k8s.io/client-go/rest"
)
type NotificationsV0alpha1Interface interface {
RESTClient() rest.Interface
TimeIntervalsGetter
}
// NotificationsV0alpha1Client is used to interact with features provided by the notifications.alerting.grafana.app group.
type NotificationsV0alpha1Client struct {
restClient rest.Interface
}
func (c *NotificationsV0alpha1Client) TimeIntervals(namespace string) TimeIntervalInterface {
return newTimeIntervals(c, namespace)
}
// NewForConfig creates a new NotificationsV0alpha1Client for the given config.
// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
// where httpClient was generated with rest.HTTPClientFor(c).
func NewForConfig(c *rest.Config) (*NotificationsV0alpha1Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
httpClient, err := rest.HTTPClientFor(&config)
if err != nil {
return nil, err
}
return NewForConfigAndClient(&config, httpClient)
}
// NewForConfigAndClient creates a new NotificationsV0alpha1Client for the given config and http client.
// Note the http client provided takes precedence over the configured transport values.
func NewForConfigAndClient(c *rest.Config, h *http.Client) (*NotificationsV0alpha1Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientForConfigAndClient(&config, h)
if err != nil {
return nil, err
}
return &NotificationsV0alpha1Client{client}, nil
}
// NewForConfigOrDie creates a new NotificationsV0alpha1Client for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *NotificationsV0alpha1Client {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new NotificationsV0alpha1Client for the given RESTClient.
func New(c rest.Interface) *NotificationsV0alpha1Client {
return &NotificationsV0alpha1Client{c}
}
func setConfigDefaults(config *rest.Config) error {
gv := v0alpha1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *NotificationsV0alpha1Client) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}

View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated typed clients.
package v0alpha1

View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeNotificationsV0alpha1 struct {
*testing.Fake
}
func (c *FakeNotificationsV0alpha1) TimeIntervals(namespace string) v0alpha1.TimeIntervalInterface {
return &FakeTimeIntervals{c, namespace}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeNotificationsV0alpha1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@ -0,0 +1,140 @@
// 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"
)
// FakeTimeIntervals implements TimeIntervalInterface
type FakeTimeIntervals struct {
Fake *FakeNotificationsV0alpha1
ns string
}
var timeintervalsResource = v0alpha1.SchemeGroupVersion.WithResource("timeintervals")
var timeintervalsKind = v0alpha1.SchemeGroupVersion.WithKind("TimeInterval")
// Get takes name of the timeInterval, and returns the corresponding timeInterval object, and an error if there is any.
func (c *FakeTimeIntervals) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.TimeInterval, err error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(timeintervalsResource, c.ns, name), &v0alpha1.TimeInterval{})
if obj == nil {
return nil, err
}
return obj.(*v0alpha1.TimeInterval), err
}
// List takes label and field selectors, and returns the list of TimeIntervals that match those selectors.
func (c *FakeTimeIntervals) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.TimeIntervalList, err error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(timeintervalsResource, timeintervalsKind, c.ns, opts), &v0alpha1.TimeIntervalList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v0alpha1.TimeIntervalList{ListMeta: obj.(*v0alpha1.TimeIntervalList).ListMeta}
for _, item := range obj.(*v0alpha1.TimeIntervalList).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 timeIntervals.
func (c *FakeTimeIntervals) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(timeintervalsResource, c.ns, opts))
}
// Create takes the representation of a timeInterval and creates it. Returns the server's representation of the timeInterval, and an error, if there is any.
func (c *FakeTimeIntervals) Create(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.CreateOptions) (result *v0alpha1.TimeInterval, err error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(timeintervalsResource, c.ns, timeInterval), &v0alpha1.TimeInterval{})
if obj == nil {
return nil, err
}
return obj.(*v0alpha1.TimeInterval), err
}
// Update takes the representation of a timeInterval and updates it. Returns the server's representation of the timeInterval, and an error, if there is any.
func (c *FakeTimeIntervals) Update(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.UpdateOptions) (result *v0alpha1.TimeInterval, err error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(timeintervalsResource, c.ns, timeInterval), &v0alpha1.TimeInterval{})
if obj == nil {
return nil, err
}
return obj.(*v0alpha1.TimeInterval), err
}
// Delete takes name of the timeInterval and deletes it. Returns an error if one occurs.
func (c *FakeTimeIntervals) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteActionWithOptions(timeintervalsResource, c.ns, name, opts), &v0alpha1.TimeInterval{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeTimeIntervals) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
action := testing.NewDeleteCollectionAction(timeintervalsResource, c.ns, listOpts)
_, err := c.Fake.Invokes(action, &v0alpha1.TimeIntervalList{})
return err
}
// Patch applies the patch and returns the patched timeInterval.
func (c *FakeTimeIntervals) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TimeInterval, err error) {
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(timeintervalsResource, c.ns, name, pt, data, subresources...), &v0alpha1.TimeInterval{})
if obj == nil {
return nil, err
}
return obj.(*v0alpha1.TimeInterval), err
}
// Apply takes the given apply declarative configuration, applies it and returns the applied timeInterval.
func (c *FakeTimeIntervals) Apply(ctx context.Context, timeInterval *alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TimeInterval, err error) {
if timeInterval == nil {
return nil, fmt.Errorf("timeInterval provided to Apply must not be nil")
}
data, err := json.Marshal(timeInterval)
if err != nil {
return nil, err
}
name := timeInterval.Name
if name == nil {
return nil, fmt.Errorf("timeInterval.Name must be provided to Apply")
}
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(timeintervalsResource, c.ns, *name, types.ApplyPatchType, data), &v0alpha1.TimeInterval{})
if obj == nil {
return nil, err
}
return obj.(*v0alpha1.TimeInterval), err
}

View File

@ -0,0 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by client-gen. DO NOT EDIT.
package v0alpha1
type TimeIntervalExpansion interface{}

View File

@ -0,0 +1,194 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by client-gen. DO NOT EDIT.
package v0alpha1
import (
"context"
json "encoding/json"
"fmt"
"time"
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"
rest "k8s.io/client-go/rest"
)
// TimeIntervalsGetter has a method to return a TimeIntervalInterface.
// A group's client should implement this interface.
type TimeIntervalsGetter interface {
TimeIntervals(namespace string) TimeIntervalInterface
}
// TimeIntervalInterface has methods to work with TimeInterval resources.
type TimeIntervalInterface interface {
Create(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.CreateOptions) (*v0alpha1.TimeInterval, error)
Update(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.UpdateOptions) (*v0alpha1.TimeInterval, 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.TimeInterval, error)
List(ctx context.Context, opts v1.ListOptions) (*v0alpha1.TimeIntervalList, 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.TimeInterval, err error)
Apply(ctx context.Context, timeInterval *alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TimeInterval, err error)
TimeIntervalExpansion
}
// timeIntervals implements TimeIntervalInterface
type timeIntervals struct {
client rest.Interface
ns string
}
// newTimeIntervals returns a TimeIntervals
func newTimeIntervals(c *NotificationsV0alpha1Client, namespace string) *timeIntervals {
return &timeIntervals{
client: c.RESTClient(),
ns: namespace,
}
}
// Get takes name of the timeInterval, and returns the corresponding timeInterval object, and an error if there is any.
func (c *timeIntervals) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.TimeInterval, err error) {
result = &v0alpha1.TimeInterval{}
err = c.client.Get().
Namespace(c.ns).
Resource("timeintervals").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do(ctx).
Into(result)
return
}
// List takes label and field selectors, and returns the list of TimeIntervals that match those selectors.
func (c *timeIntervals) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.TimeIntervalList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v0alpha1.TimeIntervalList{}
err = c.client.Get().
Namespace(c.ns).
Resource("timeintervals").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do(ctx).
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested timeIntervals.
func (c *timeIntervals) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("timeintervals").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch(ctx)
}
// Create takes the representation of a timeInterval and creates it. Returns the server's representation of the timeInterval, and an error, if there is any.
func (c *timeIntervals) Create(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.CreateOptions) (result *v0alpha1.TimeInterval, err error) {
result = &v0alpha1.TimeInterval{}
err = c.client.Post().
Namespace(c.ns).
Resource("timeintervals").
VersionedParams(&opts, scheme.ParameterCodec).
Body(timeInterval).
Do(ctx).
Into(result)
return
}
// Update takes the representation of a timeInterval and updates it. Returns the server's representation of the timeInterval, and an error, if there is any.
func (c *timeIntervals) Update(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.UpdateOptions) (result *v0alpha1.TimeInterval, err error) {
result = &v0alpha1.TimeInterval{}
err = c.client.Put().
Namespace(c.ns).
Resource("timeintervals").
Name(timeInterval.Name).
VersionedParams(&opts, scheme.ParameterCodec).
Body(timeInterval).
Do(ctx).
Into(result)
return
}
// Delete takes name of the timeInterval and deletes it. Returns an error if one occurs.
func (c *timeIntervals) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("timeintervals").
Name(name).
Body(&opts).
Do(ctx).
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *timeIntervals) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
var timeout time.Duration
if listOpts.TimeoutSeconds != nil {
timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
}
return c.client.Delete().
Namespace(c.ns).
Resource("timeintervals").
VersionedParams(&listOpts, scheme.ParameterCodec).
Timeout(timeout).
Body(&opts).
Do(ctx).
Error()
}
// Patch applies the patch and returns the patched timeInterval.
func (c *timeIntervals) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TimeInterval, err error) {
result = &v0alpha1.TimeInterval{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("timeintervals").
Name(name).
SubResource(subresources...).
VersionedParams(&opts, scheme.ParameterCodec).
Body(data).
Do(ctx).
Into(result)
return
}
// Apply takes the given apply declarative configuration, applies it and returns the applied timeInterval.
func (c *timeIntervals) Apply(ctx context.Context, timeInterval *alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TimeInterval, err error) {
if timeInterval == nil {
return nil, fmt.Errorf("timeInterval provided to Apply must not be nil")
}
patchOpts := opts.ToPatchOptions()
data, err := json.Marshal(timeInterval)
if err != nil {
return nil, err
}
name := timeInterval.Name
if name == nil {
return nil, fmt.Errorf("timeInterval.Name must be provided to Apply")
}
result = &v0alpha1.TimeInterval{}
err = c.client.Patch(types.ApplyPatchType).
Namespace(c.ns).
Resource("timeintervals").
Name(*name).
VersionedParams(&patchOpts, scheme.ParameterCodec).
Body(data).
Do(ctx).
Into(result)
return
}

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by informer-gen. DO NOT EDIT.
package alerting_notifications
import (
v0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1"
internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// V0alpha1 provides access to shared informers for resources in V0alpha1.
V0alpha1() v0alpha1.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// V0alpha1 returns a new v0alpha1.Interface.
func (g *group) V0alpha1() v0alpha1.Interface {
return v0alpha1.New(g.factory, g.namespace, g.tweakListOptions)
}

View File

@ -0,0 +1,31 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by informer-gen. DO NOT EDIT.
package v0alpha1
import (
internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// TimeIntervals returns a TimeIntervalInformer.
TimeIntervals() TimeIntervalInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// TimeIntervals returns a TimeIntervalInformer.
func (v *version) TimeIntervals() TimeIntervalInformer {
return &timeIntervalInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}

View File

@ -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"
)
// TimeIntervalInformer provides access to a shared informer and lister for
// TimeIntervals.
type TimeIntervalInformer interface {
Informer() cache.SharedIndexInformer
Lister() v0alpha1.TimeIntervalLister
}
type timeIntervalInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewTimeIntervalInformer constructs a new informer for TimeInterval 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 NewTimeIntervalInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredTimeIntervalInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredTimeIntervalInformer constructs a new informer for TimeInterval 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 NewFilteredTimeIntervalInformer(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().TimeIntervals(namespace).List(context.TODO(), options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.NotificationsV0alpha1().TimeIntervals(namespace).Watch(context.TODO(), options)
},
},
&alertingnotificationsv0alpha1.TimeInterval{},
resyncPeriod,
indexers,
)
}
func (f *timeIntervalInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredTimeIntervalInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *timeIntervalInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&alertingnotificationsv0alpha1.TimeInterval{}, f.defaultInformer)
}
func (f *timeIntervalInformer) Lister() v0alpha1.TimeIntervalLister {
return v0alpha1.NewTimeIntervalLister(f.Informer().GetIndexer())
}

View File

@ -10,6 +10,7 @@ import (
time "time"
versioned "github.com/grafana/grafana/pkg/generated/clientset/versioned"
alertingnotifications "github.com/grafana/grafana/pkg/generated/informers/externalversions/alerting_notifications"
internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces"
service "github.com/grafana/grafana/pkg/generated/informers/externalversions/service"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -239,9 +240,14 @@ type SharedInformerFactory interface {
// client.
InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer
Notifications() alertingnotifications.Interface
Service() service.Interface
}
func (f *sharedInformerFactory) Notifications() alertingnotifications.Interface {
return alertingnotifications.New(f, f.namespace, f.tweakListOptions)
}
func (f *sharedInformerFactory) Service() service.Interface {
return service.New(f, f.namespace, f.tweakListOptions)
}

View File

@ -7,7 +7,8 @@ package externalversions
import (
"fmt"
v0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
@ -38,8 +39,12 @@ func (f *genericInformer) Lister() cache.GenericLister {
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=service.grafana.app, Version=v0alpha1
case v0alpha1.SchemeGroupVersion.WithResource("externalnames"):
// Group=notifications.alerting.grafana.app, Version=v0alpha1
case v0alpha1.SchemeGroupVersion.WithResource("timeintervals"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().TimeIntervals().Informer()}, nil
// Group=service.grafana.app, Version=v0alpha1
case servicev0alpha1.SchemeGroupVersion.WithResource("externalnames"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Service().V0alpha1().ExternalNames().Informer()}, nil
}

View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by lister-gen. DO NOT EDIT.
package v0alpha1
// TimeIntervalListerExpansion allows custom methods to be added to
// TimeIntervalLister.
type TimeIntervalListerExpansion interface{}
// TimeIntervalNamespaceListerExpansion allows custom methods to be added to
// TimeIntervalNamespaceLister.
type TimeIntervalNamespaceListerExpansion interface{}

View File

@ -0,0 +1,85 @@
// 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/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// TimeIntervalLister helps list TimeIntervals.
// All objects returned here must be treated as read-only.
type TimeIntervalLister interface {
// List lists all TimeIntervals in the indexer.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error)
// TimeIntervals returns an object that can list and get TimeIntervals.
TimeIntervals(namespace string) TimeIntervalNamespaceLister
TimeIntervalListerExpansion
}
// timeIntervalLister implements the TimeIntervalLister interface.
type timeIntervalLister struct {
indexer cache.Indexer
}
// NewTimeIntervalLister returns a new TimeIntervalLister.
func NewTimeIntervalLister(indexer cache.Indexer) TimeIntervalLister {
return &timeIntervalLister{indexer: indexer}
}
// List lists all TimeIntervals in the indexer.
func (s *timeIntervalLister) List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v0alpha1.TimeInterval))
})
return ret, err
}
// TimeIntervals returns an object that can list and get TimeIntervals.
func (s *timeIntervalLister) TimeIntervals(namespace string) TimeIntervalNamespaceLister {
return timeIntervalNamespaceLister{indexer: s.indexer, namespace: namespace}
}
// TimeIntervalNamespaceLister helps list and get TimeIntervals.
// All objects returned here must be treated as read-only.
type TimeIntervalNamespaceLister interface {
// List lists all TimeIntervals in the indexer for a given namespace.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error)
// Get retrieves the TimeInterval from the indexer for a given namespace and name.
// Objects returned here must be treated as read-only.
Get(name string) (*v0alpha1.TimeInterval, error)
TimeIntervalNamespaceListerExpansion
}
// timeIntervalNamespaceLister implements the TimeIntervalNamespaceLister
// interface.
type timeIntervalNamespaceLister struct {
indexer cache.Indexer
namespace string
}
// List lists all TimeIntervals in the indexer for a given namespace.
func (s timeIntervalNamespaceLister) List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error) {
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*v0alpha1.TimeInterval))
})
return ret, err
}
// Get retrieves the TimeInterval from the indexer for a given namespace and name.
func (s timeIntervalNamespaceLister) Get(name string) (*v0alpha1.TimeInterval, error) {
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v0alpha1.Resource("timeinterval"), name)
}
return obj.(*v0alpha1.TimeInterval), nil
}

View File

@ -0,0 +1,112 @@
package notifications
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/apiserver/builder"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
timeInterval "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/setting"
)
var _ builder.APIGroupBuilder = (*NotificationsAPIBuilder)(nil)
// This is used just so wire has something unique to return
type NotificationsAPIBuilder struct {
authz accesscontrol.AccessControl
ng *ngalert.AlertNG
namespacer request.NamespaceMapper
gv schema.GroupVersion
}
func (t NotificationsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, toMode map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
// Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go
return grafanarest.Mode0
}
func RegisterAPIService(
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
cfg *setting.Cfg,
ng *ngalert.AlertNG,
) *NotificationsAPIBuilder {
if ng.IsDisabled() || !features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
return nil
}
builder := &NotificationsAPIBuilder{
ng: ng,
namespacer: request.GetNamespaceMapper(cfg),
gv: notificationsModels.SchemeGroupVersion,
authz: ng.Api.AccessControl,
}
apiregistration.RegisterAPI(builder)
return builder
}
func (t NotificationsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return t.gv
}
func (t NotificationsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
err := notificationsModels.AddToScheme(scheme)
if err != nil {
return err
}
return scheme.SetVersionPriority(notificationsModels.SchemeGroupVersion)
}
func (t NotificationsAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory,
optsGetter generic.RESTOptionsGetter,
desiredMode grafanarest.DualWriterMode,
reg prometheus.Registerer,
) (*genericapiserver.APIGroupInfo, error) {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(notificationsModels.GROUP, scheme, metav1.ParameterCodec, codecs)
intervals, err := timeInterval.NewStorage(t.ng.Api.MuteTimings, t.namespacer, scheme, desiredMode, optsGetter, reg)
if err != nil {
return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err)
}
apiGroupInfo.VersionedResourcesStorageMap[notificationsModels.VERSION] = map[string]rest.Storage{
notificationsModels.TimeIntervalResourceInfo.StoragePath(): intervals,
}
return &apiGroupInfo, nil
}
func (t NotificationsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return notificationsModels.GetOpenAPIDefinitions
}
func (t NotificationsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
return nil
}
func (t NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
switch a.GetResource() {
case notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource:
return timeInterval.Authorize(ctx, t.authz, a)
}
return authorizer.DecisionNoOpinion, "", nil
})
}

View File

@ -0,0 +1,54 @@
package timeinterval
import (
"context"
"k8s.io/apiserver/pkg/authorization/authorizer"
"github.com/grafana/grafana/pkg/infra/appcontext"
"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 := appcontext.User(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":
action = accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite),
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite),
)
case "deletecollection":
fallthrough
case "delete":
action = accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete),
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite),
)
}
eval := accesscontrol.EvalAny(
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTimeIntervalsRead),
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead),
)
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
}

View File

@ -0,0 +1,97 @@
package timeinterval
import (
"encoding/json"
"fmt"
"hash/fnv"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
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"
)
func getIntervalUID(t definitions.MuteTimeInterval) string {
sum := fnv.New64()
_, _ = sum.Write([]byte(t.Name))
return fmt.Sprintf("%016x", sum.Sum64())
}
func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeIntervalList, error) {
data, err := json.Marshal(intervals)
if err != nil {
return nil, err
}
var specs []model.TimeIntervalSpec
err = json.Unmarshal(data, &specs)
if err != nil {
return nil, err
}
result := &model.TimeIntervalList{}
for idx := range specs {
interval := intervals[idx]
spec := specs[idx]
uid := getIntervalUID(interval) // TODO replace to stable UID when we switch to normal storage
result.Items = append(result.Items, model.TimeInterval{
TypeMeta: resourceInfo.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(uid), // TODO This is needed to make PATCH work
Name: uid, // TODO replace to stable UID when we switch to normal storage
Namespace: namespacer(orgID),
Annotations: map[string]string{ // TODO find a better place for provenance?
"grafana.com/provenance": string(interval.Provenance),
},
ResourceVersion: interval.Version,
},
Spec: spec,
})
}
return result, nil
}
func convertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeInterval, error) {
data, err := json.Marshal(interval)
if err != nil {
return nil, err
}
spec := model.TimeIntervalSpec{}
err = json.Unmarshal(data, &spec)
if err != nil {
return nil, err
}
uid := getIntervalUID(interval) // TODO replace to stable UID when we switch to normal storage
return &model.TimeInterval{
TypeMeta: resourceInfo.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(uid), // TODO This is needed to make PATCH work
Name: uid, // TODO replace to stable UID when we switch to normal storage
Namespace: namespacer(orgID),
Annotations: map[string]string{ // TODO find a better place for provenance?
"grafana.com/provenance": string(interval.Provenance),
},
ResourceVersion: interval.Version,
},
Spec: spec,
}, nil
}
func convertToDomainModel(interval *model.TimeInterval) (definitions.MuteTimeInterval, error) {
b, err := json.Marshal(interval.Spec)
if err != nil {
return definitions.MuteTimeInterval{}, err
}
result := definitions.MuteTimeInterval{}
err = json.Unmarshal(b, &result)
if err != nil {
return definitions.MuteTimeInterval{}, err
}
result.Version = interval.ResourceVersion
err = result.Validate()
if err != nil {
return definitions.MuteTimeInterval{}, err
}
return result, nil
}

View File

@ -0,0 +1,205 @@
package timeinterval
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"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
var (
_ grafanaRest.LegacyStorage = (*legacyStorage)(nil)
)
var resourceInfo = notifications.TimeIntervalResourceInfo
type TimeIntervalService interface {
GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTimeInterval, error)
GetMuteTiming(ctx context.Context, name string, orgID int64) (definitions.MuteTimeInterval, error)
CreateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (definitions.MuteTimeInterval, error)
UpdateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (definitions.MuteTimeInterval, error)
DeleteMuteTiming(ctx context.Context, name string, orgID int64, provenance definitions.Provenance, version string) error
}
type legacyStorage struct {
service TimeIntervalService
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) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
orgId, err := request.OrgIDForList(ctx)
if err != nil {
return nil, err
}
res, err := s.service.GetMuteTimings(ctx, orgId)
if err != nil {
return nil, err
}
return convertToK8sResources(orgId, res, s.namespacer)
}
func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
timings, err := s.service.GetMuteTimings(ctx, info.OrgID)
if err != nil {
return nil, err
}
for _, mt := range timings {
if getIntervalUID(mt) == uid {
return convertToK8sResource(info.OrgID, mt, s.namespacer)
}
}
return nil, errors.NewNotFound(resourceInfo.GroupResource(), uid)
}
func (s *legacyStorage) Create(ctx context.Context,
obj runtime.Object,
createValidation rest.ValidateObjectFunc,
_ *metav1.CreateOptions,
) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}
p, ok := obj.(*notifications.TimeInterval)
if !ok {
return nil, fmt.Errorf("expected time-interval but got %s", obj.GetObjectKind().GroupVersionKind())
}
if p.ObjectMeta.Name != "" { // TODO remove when metadata.name can be defined by user
return nil, errors.NewBadRequest("object's metadata.name should be empty")
}
model, err := convertToDomainModel(p)
if err != nil {
return nil, err
}
out, err := s.service.CreateMuteTiming(ctx, model, info.OrgID)
if err != nil {
return nil, err
}
return convertToK8sResource(info.OrgID, out, s.namespacer)
}
func (s *legacyStorage) Update(ctx context.Context,
uid string,
objInfo rest.UpdatedObjectInfo,
createValidation rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc,
_ bool,
_ *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
old, err := s.Get(ctx, uid, 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.TimeInterval)
if !ok {
return nil, false, fmt.Errorf("expected time-interval but got %s", obj.GetObjectKind().GroupVersionKind())
}
interval, err := convertToDomainModel(p)
if err != nil {
return old, false, err
}
if p.ObjectMeta.Name != getIntervalUID(interval) {
return nil, false, errors.NewBadRequest("title of cannot be changed. Consider creating a new resource.")
}
updated, err := s.service.UpdateMuteTiming(ctx, interval, info.OrgID)
if err != nil {
return nil, false, err
}
r, err := convertToK8sResource(info.OrgID, updated, s.namespacer)
return r, false, err
}
// GracefulDeleter
func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
old, err := s.Get(ctx, uid, nil)
if err != nil {
return old, false, err
}
if deleteValidation != nil {
if err = deleteValidation(ctx, old); err != nil {
return nil, false, err
}
}
version := ""
if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
version = *options.Preconditions.ResourceVersion
}
p, ok := old.(*notifications.TimeInterval)
if !ok {
return nil, false, fmt.Errorf("expected time-interval but got %s", old.GetObjectKind().GroupVersionKind())
}
err = s.service.DeleteMuteTiming(ctx, p.Spec.Name, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option
return old, false, err // false - will be deleted async
}
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "deleteCollection")
}

View File

@ -0,0 +1,79 @@
package timeinterval
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/prometheus/client_golang/prometheus"
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
)
var _ grafanarest.Storage = (*storage)(nil)
type storage struct {
*genericregistry.Store
}
func (s storage) Compare(storageObj, legacyObj runtime.Object) bool {
// TODO implement when supported dual write mode is not Mode0
return false
}
func NewStorage(
legacySvc TimeIntervalService,
namespacer request.NamespaceMapper,
scheme *runtime.Scheme,
desiredMode grafanarest.DualWriterMode,
optsGetter generic.RESTOptionsGetter,
reg prometheus.Registerer) (rest.Storage, error) {
legacyStore := &legacyStorage{
service: legacySvc,
namespacer: namespacer,
tableConverter: utils.NewTableConverter(
resourceInfo.GroupResource(),
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
// {Name: "Intervals", Type: "string", Format: "string", Description: "The display name"},
},
func(obj any) ([]interface{}, error) {
r, ok := obj.(*model.TimeInterval)
if ok {
return []interface{}{
r.Name,
// r.Spec, //TODO implement formatting for Spec, same as UI?
}, nil
}
return nil, fmt.Errorf("expected resource or info")
}),
}
if optsGetter != nil && desiredMode != grafanarest.Mode0 {
strategy := grafanaregistry.NewStrategy(scheme)
s := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: legacyStore.tableConverter,
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
if err := s.CompleteWithOptions(options); err != nil {
return nil, err
}
return grafanarest.NewDualWriter(desiredMode, legacyStore, storage{Store: s}, reg), nil
}
return legacyStore, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
@ -35,6 +36,7 @@ func ProvideRegistryServiceSink(
_ *peakq.PeakQAPIBuilder,
_ *scope.ScopeAPIBuilder,
_ *query.QueryAPIBuilder,
_ *notifications.NotificationsAPIBuilder,
) *Service {
return &Service{}
}

View File

@ -3,6 +3,7 @@ package apiregistry
import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
@ -37,4 +38,5 @@ var WireSet = wire.NewSet(
service.RegisterAPIService,
query.RegisterAPIService,
scope.RegisterAPIService,
notifications.RegisterAPIService,
)

View File

@ -450,8 +450,9 @@ const (
ActionAlertingNotificationsWrite = "alert.notifications:write"
// Alerting notifications time interval actions
ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read"
ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write"
ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read"
ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write"
ActionAlertingNotificationsTimeIntervalsDelete = "alert.notifications.time-intervals:delete"
// Alerting receiver actions
ActionAlertingReceiversList = "alert.notifications.receivers:list"

View File

@ -1355,6 +1355,13 @@ var (
HideFromDocs: true,
HideFromAdminPage: true,
},
{
Name: "alertingApiServer",
Description: "Register Alerting APIs with the K8s API server",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
RequiresRestart: true,
},
}
)

View File

@ -179,3 +179,4 @@ failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false
databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false
zanzana,experimental,@grafana/identity-access-team,false,false,false
passScopeToDashboardApi,experimental,@grafana/dashboards-squad,false,false,false
alertingApiServer,experimental,@grafana/alerting-squad,false,true,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
179 databaseReadReplica experimental @grafana/grafana-backend-services-squad false false false
180 zanzana experimental @grafana/identity-access-team false false false
181 passScopeToDashboardApi experimental @grafana/dashboards-squad false false false
182 alertingApiServer experimental @grafana/alerting-squad false true false

View File

@ -726,4 +726,8 @@ const (
// FlagPassScopeToDashboardApi
// Enables the passing of scopes to dashboards fetching in Grafana
FlagPassScopeToDashboardApi = "passScopeToDashboardApi"
// FlagAlertingApiServer
// Register Alerting APIs with the K8s API server
FlagAlertingApiServer = "alertingApiServer"
)

View File

@ -90,6 +90,19 @@
"codeowner": "@grafana/alerting-squad"
}
},
{
"metadata": {
"name": "alertingApiServer",
"resourceVersion": "1718908755156",
"creationTimestamp": "2024-06-20T18:39:15Z"
},
"spec": {
"description": "Register Alerting APIs with the K8s API server",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"requiresRestart": true
}
},
{
"metadata": {
"name": "alertingBacktesting",
@ -2349,4 +2362,4 @@
}
}
]
}
}

View File

@ -135,7 +135,7 @@ type AlertNG struct {
stateManager *state.Manager
folderService folder.Service
dashboardService dashboards.DashboardService
api *api.API
Api *api.API
// Alerting notification services
MultiOrgAlertmanager *notifier.MultiOrgAlertmanager
@ -377,6 +377,7 @@ func (ng *AlertNG) init() error {
RulesPerRuleGroupLimit: ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit,
Tracer: ng.tracer,
Log: log.New("ngalert.state.manager"),
ResolvedRetention: ng.Cfg.UnifiedAlerting.ResolvedAlertRetention,
}
logger := log.New("ngalert.state.manager.persist")
statePersister := state.NewSyncStatePersisiter(logger, cfg)
@ -408,7 +409,7 @@ func (ng *AlertNG) init() error {
ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, ng.Log, notifier.NewNotificationSettingsValidationService(ng.store),
ac.NewRuleService(ng.accesscontrol))
ng.api = &api.API{
ng.Api = &api.API{
Cfg: ng.Cfg,
DatasourceCache: ng.DataSourceCache,
DatasourceService: ng.DataSourceService,
@ -437,7 +438,7 @@ func (ng *AlertNG) init() error {
Hooks: api.NewHooks(ng.Log),
Tracer: ng.tracer,
}
ng.api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())
ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())
if err := RegisterQuotas(ng.Cfg, ng.QuotaService, ng.store); err != nil {
return err
@ -515,7 +516,7 @@ func (ng *AlertNG) IsDisabled() bool {
// GetHooks returns a facility for replacing handlers for paths. The handler hook for a path
// is invoked after all other middleware is invoked (authentication, instrumentation).
func (ng *AlertNG) GetHooks() *api.Hooks {
return ng.api.Hooks
return ng.Api.Hooks
}
type Historian interface {

View File

@ -8,6 +8,10 @@ import (
"time"
"github.com/benbjohnson/clock"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/datasources"
@ -19,9 +23,6 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// Rule represents a single piece of work that is executed periodically by the ruler.
@ -418,7 +419,7 @@ func (a *alertRule) evaluate(ctx context.Context, key ngmodels.AlertRuleKey, f f
processDuration.Observe(a.clock.Now().Sub(start).Seconds())
start = a.clock.Now()
alerts := state.FromStateTransitionToPostableAlerts(processedStates, a.stateManager, a.appURL)
alerts := state.FromStateTransitionToPostableAlerts(e.scheduledAt, processedStates, a.stateManager, a.appURL)
span.AddEvent("results processed", trace.WithAttributes(
attribute.Int64("state_transitions", int64(len(processedStates))),
attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))),

View File

@ -2,7 +2,7 @@ package schedule
import (
"bytes"
context "context"
"context"
"fmt"
"math"
"math/rand"
@ -11,19 +11,21 @@ import (
"testing"
"time"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
prometheusModel "github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana/pkg/infra/log"
definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/util"
)
@ -762,8 +764,94 @@ func TestRuleRoutine(t *testing.T) {
require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID))
})
t.Run("when there are resolved alerts they should keep sending until retention period is over", func(t *testing.T) {
rule := gen.With(withQueryForState(t, eval.Normal), models.RuleMuts.WithInterval(time.Second)).GenerateRef()
evalAppliedChan := make(chan time.Time)
sender := NewSyncAlertsSenderMock()
sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return()
sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender)
sch.stateManager.ResolvedRetention = 4 * time.Second
sch.stateManager.ResendDelay = 2 * time.Second
sch.stateManager.Put([]*state.State{
stateForRule(rule, sch.clock.Now(), eval.Alerting), // Add existing Alerting state so evals will resolve.
})
ruleStore.PutRule(context.Background(), rule)
factory := ruleFactoryFromScheduler(sch)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
ruleInfo := factory.new(ctx, rule)
go func() {
_ = ruleInfo.Run(rule.GetKey())
}()
// Evaluate 10 times:
// 1. Send resolve #1.
// 2. 2s resend delay.
// 3. Send resolve #2.
// 4. 2s resend delay.
// 5. Send resolve #3.
// 6. No more sends, 4s retention period is over.
expectedResolves := map[time.Time]struct{}{
sch.clock.Now().Add(1 * time.Second): {},
sch.clock.Now().Add(3 * time.Second): {},
sch.clock.Now().Add(5 * time.Second): {},
}
calls := 0
for i := 1; i < 10; i++ {
ts := sch.clock.Now().Add(time.Duration(int64(i)*rule.IntervalSeconds) * time.Second)
ruleInfo.Eval(&Evaluation{
scheduledAt: ts,
rule: rule,
})
waitForTimeChannel(t, evalAppliedChan)
if _, ok := expectedResolves[ts]; ok {
calls++
prevCallAlerts, ok := sender.Calls()[calls-1].Arguments[2].(definitions.PostableAlerts)
assert.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[calls-1].Arguments[2]))
assert.Len(t, prevCallAlerts.PostableAlerts, 1)
}
sender.AssertNumberOfCalls(t, "Send", calls)
}
})
}
func ruleFactoryFromScheduler(sch *schedule) ruleFactory {
return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, &sch.schedulableAlertRules, sch.clock, sch.featureToggles, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc)
}
func stateForRule(rule *models.AlertRule, ts time.Time, evalState eval.State) *state.State {
s := &state.State{
OrgID: rule.OrgID,
AlertRuleUID: rule.UID,
CacheID: 0,
State: evalState,
Annotations: make(map[string]string),
Labels: make(map[string]string),
StartsAt: ts,
EndsAt: ts,
ResolvedAt: &ts,
LastSentAt: &ts,
LastEvaluationTime: ts,
}
for k, v := range rule.Labels {
s.Labels[k] = v
}
for k, v := range state.GetRuleExtraLabels(&logtest.Fake{}, rule, "", true) {
if _, ok := s.Labels[k]; !ok {
s.Labels[k] = v
}
}
il := models.InstanceLabels(s.Labels)
s.Labels = data.Labels(il)
id := il.Fingerprint()
s.CacheID = id
return s
}

View File

@ -7,15 +7,17 @@ import (
"testing"
"time"
definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
mock "github.com/stretchr/testify/mock"
)
// waitForTimeChannel blocks the execution until either the channel ch has some data or a timeout of 10 second expires.
// Timeout will cause the test to fail.
// Returns the data from the channel.
func waitForTimeChannel(t *testing.T, ch chan time.Time) time.Time {
t.Helper()
select {
case result := <-ch:
return result

View File

@ -10,11 +10,12 @@ import (
"github.com/benbjohnson/clock"
"github.com/go-openapi/strfmt"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/common/model"
alertingModels "github.com/grafana/alerting/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -73,7 +74,7 @@ func StateToPostableAlert(transition StateTransition, appURL *url.URL) *models.P
}
state := alertState.State
if alertState.Resolved {
if alertState.ResolvedAt != nil {
// If this is a resolved alert, we need to send an alert with the correct labels such that they will expire the previous alert.
// In most cases the labels on the state will be correct, however when the previous alert was a NoData or Error alert, we need to
// ensure to modify it appropriately.
@ -139,13 +140,12 @@ func errorAlert(labels, annotations data.Labels, alertState *State, urlStr strin
}
}
func FromStateTransitionToPostableAlerts(firingStates []StateTransition, stateManager *Manager, appURL *url.URL) apimodels.PostableAlerts {
func FromStateTransitionToPostableAlerts(evaluatedAt time.Time, firingStates []StateTransition, stateManager *Manager, appURL *url.URL) apimodels.PostableAlerts {
alerts := apimodels.PostableAlerts{PostableAlerts: make([]models.PostableAlert, 0, len(firingStates))}
ts := time.Now()
sentAlerts := make([]*State, 0, len(firingStates))
for _, alertState := range firingStates {
if !alertState.NeedsSending(stateManager.ResendDelay) {
if !alertState.NeedsSending(stateManager.ResendDelay, stateManager.ResolvedRetention) {
continue
}
alert := StateToPostableAlert(alertState, appURL)
@ -153,7 +153,7 @@ func FromStateTransitionToPostableAlerts(firingStates []StateTransition, stateMa
if alertState.StateReason == ngModels.StateReasonMissingSeries { // do not put stale state back to state manager
continue
}
alertState.LastSentAt = ts
alertState.LastSentAt = &evaluatedAt
sentAlerts = append(sentAlerts, alertState.State)
}
stateManager.Put(sentAlerts)

View File

@ -9,12 +9,13 @@ import (
"github.com/benbjohnson/clock"
"github.com/go-openapi/strfmt"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
@ -267,7 +268,9 @@ func TestStateToPostableAlertFromNodataError(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
alertState := randomTransition(tc.from, tc.to)
alertState.Resolved = tc.resolved
if tc.resolved {
alertState.ResolvedAt = &alertState.LastEvaluationTime
}
alertState.Labels = data.Labels(standardLabels)
result := StateToPostableAlert(alertState, appURL)
require.Equal(t, tc.expectedLabels, result.Labels)
@ -339,7 +342,7 @@ func randomTransition(from, to eval.State) StateTransition {
EndsAt: randomTimeInFuture(),
LastEvaluationTime: randomTimeInPast(),
EvaluationDuration: randomDuration(),
LastSentAt: randomTimeInPast(),
LastSentAt: util.Pointer(randomTimeInPast()),
Annotations: make(map[string]string),
Labels: make(map[string]string),
Values: make(map[string]float64),

View File

@ -39,9 +39,10 @@ type Manager struct {
metrics *metrics.State
tracer tracing.Tracer
clock clock.Clock
cache *cache
ResendDelay time.Duration
clock clock.Clock
cache *cache
ResendDelay time.Duration
ResolvedRetention time.Duration
instanceStore InstanceStore
images ImageCapturer
@ -73,6 +74,9 @@ type ManagerCfg struct {
DisableExecution bool
// Duration for which a resolved alert state transition will continue to be sent to the Alertmanager.
ResolvedRetention time.Duration
Tracer tracing.Tracer
Log log.Logger
}
@ -88,6 +92,7 @@ func NewManager(cfg ManagerCfg, statePersister StatePersister) *Manager {
m := &Manager{
cache: c,
ResendDelay: ResendDelay, // TODO: make this configurable
ResolvedRetention: cfg.ResolvedRetention,
log: cfg.Log,
metrics: cfg.Metrics,
instanceStore: cfg.InstanceStore,
@ -245,7 +250,11 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al
s.SetNormal(reason, startsAt, now)
// Set Resolved property so the scheduler knows to send a postable alert
// to Alertmanager.
s.Resolved = oldState == eval.Alerting || oldState == eval.Error || oldState == eval.NoData
if oldState == eval.Alerting || oldState == eval.Error || oldState == eval.NoData {
s.ResolvedAt = &now
} else {
s.ResolvedAt = nil
}
s.LastEvaluationTime = now
s.Values = map[string]float64{}
transitions = append(transitions, StateTransition{
@ -418,9 +427,15 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu
// Set Resolved property so the scheduler knows to send a postable alert
// to Alertmanager.
currentState.Resolved = oldState == eval.Alerting && currentState.State == eval.Normal
newlyResolved := false
if oldState == eval.Alerting && currentState.State == eval.Normal {
currentState.ResolvedAt = &result.EvaluatedAt
newlyResolved = true
} else if currentState.State != eval.Normal && currentState.State != eval.Pending { // Retain the last resolved time for Normal->Normal and Normal->Pending.
currentState.ResolvedAt = nil
}
if shouldTakeImage(currentState.State, oldState, currentState.Image, currentState.Resolved) {
if shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved) {
image, err := takeImage(ctx, st.images, alertRule)
if err != nil {
logger.Warn("Failed to take an image",
@ -505,7 +520,7 @@ func (st *Manager) deleteStaleStatesFromCache(ctx context.Context, logger log.Lo
s.LastEvaluationTime = evaluatedAt
if oldState == eval.Alerting {
s.Resolved = true
s.ResolvedAt = &evaluatedAt
image, err := takeImage(ctx, st.images, alertRule)
if err != nil {
logger.Warn("Failed to take an image",

View File

@ -563,7 +563,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t2,
Resolved: true,
ResolvedAt: &t2,
},
},
},
@ -622,7 +622,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -1051,7 +1051,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -1091,7 +1091,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -1133,7 +1133,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -1175,7 +1175,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -1275,7 +1275,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t2,
Resolved: true,
ResolvedAt: &t2,
},
},
},
@ -1304,6 +1304,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t3,
ResolvedAt: &t2,
},
},
},
@ -1935,7 +1936,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
},
@ -2163,7 +2164,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -2191,7 +2192,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -2221,7 +2222,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -2251,7 +2252,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
{
@ -2314,7 +2315,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t2,
Resolved: true,
ResolvedAt: &t2,
},
},
},
@ -2330,6 +2331,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t3,
ResolvedAt: &t2,
},
},
},
@ -3060,7 +3062,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t1,
EndsAt: t3,
LastEvaluationTime: t3,
Resolved: true,
ResolvedAt: &t3,
},
},
},
@ -3480,7 +3482,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) {
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t2,
Resolved: true,
ResolvedAt: &t2,
},
},
},

View File

@ -7,6 +7,7 @@ import (
"fmt"
"math"
"math/rand"
"net/url"
"sort"
"strings"
"testing"
@ -294,7 +295,7 @@ func TestProcessEvalResults(t *testing.T) {
evaluationDuration := 10 * time.Millisecond
evaluationInterval := 10 * time.Second
t1 := time.Time{}.Add(evaluationInterval)
t1 := time.Unix(0, 0).Add(evaluationInterval)
tn := func(n int) time.Time {
return t1.Add(time.Duration(n-1) * evaluationInterval)
@ -424,6 +425,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t1,
EndsAt: t1.Add(state.ResendDelay * 4),
LastEvaluationTime: t1,
LastSentAt: &t1,
},
},
},
@ -471,6 +473,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t2,
EndsAt: t2.Add(state.ResendDelay * 4),
LastEvaluationTime: t2,
LastSentAt: &t2,
},
},
},
@ -501,6 +504,94 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(4),
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
LastSentAt: util.Pointer(tn(4)),
},
},
},
{
desc: "alerting -> normal resolves and sets ResolvedAt",
alertRule: baseRule,
evalResults: map[time.Time]eval.Results{
t1: {
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
},
t2: {
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
},
},
expectedAnnotations: 2,
expectedStates: []*state.State{
{
Labels: labels["system + rule + labels1"],
ResultFingerprint: labels1.Fingerprint(),
State: eval.Normal,
LatestResult: newEvaluation(t2, eval.Normal),
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t2,
ResolvedAt: &t2,
LastSentAt: &t2,
},
},
},
{
desc: "alerting -> normal -> normal resolves and maintains ResolvedAt",
alertRule: baseRule,
evalResults: map[time.Time]eval.Results{
t1: {
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
},
t2: {
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
},
t3: {
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
},
},
expectedAnnotations: 2,
expectedStates: []*state.State{
{
Labels: labels["system + rule + labels1"],
ResultFingerprint: labels1.Fingerprint(),
State: eval.Normal,
LatestResult: newEvaluation(t3, eval.Normal),
StartsAt: t2,
EndsAt: t2,
LastEvaluationTime: t3,
ResolvedAt: &t2,
LastSentAt: &t2,
},
},
},
{
desc: "pending -> alerting -> normal -> pending resolves and resets ResolvedAt at t4",
alertRule: baseRuleWith(m.WithForNTimes(1)),
evalResults: map[time.Time]eval.Results{
t1: {
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)),
},
t2: {
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), // Alerting.
},
t3: {
newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)),
},
tn(4): {
newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), // Pending.
},
},
expectedAnnotations: 4,
expectedStates: []*state.State{
{
Labels: labels["system + rule + labels1"],
ResultFingerprint: labels1.Fingerprint(),
State: eval.Pending,
LatestResult: newEvaluation(tn(4), eval.Alerting),
StartsAt: tn(4),
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
ResolvedAt: &t3,
LastSentAt: &t3,
},
},
},
@ -534,6 +625,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(4),
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(5),
LastSentAt: util.Pointer(tn(3)), // 30s resend delay causing the last sent at to be t3.
},
},
},
@ -564,6 +656,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(4),
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
LastSentAt: &t3, // Resend delay is 30s, so last sent at is t3.
},
},
},
@ -672,6 +765,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(5),
EndsAt: tn(5).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(5),
LastSentAt: util.Pointer(tn(5)),
},
},
},
@ -696,6 +790,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t2,
EndsAt: t2.Add(state.ResendDelay * 4),
LastEvaluationTime: t2,
LastSentAt: &t2,
},
},
},
@ -729,6 +824,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t2,
EndsAt: t2.Add(state.ResendDelay * 4),
LastEvaluationTime: t2,
LastSentAt: &t2,
},
},
},
@ -772,6 +868,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t2,
EndsAt: t2.Add(state.ResendDelay * 4),
LastEvaluationTime: t2,
LastSentAt: &t2,
},
},
},
@ -808,6 +905,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t2,
EndsAt: t2.Add(state.ResendDelay * 4),
LastEvaluationTime: t2,
LastSentAt: &t2,
},
},
},
@ -839,6 +937,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t3,
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
LastSentAt: &t3, // Resend delay is 30s, so last sent at is t3.
},
},
},
@ -870,6 +969,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(4),
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
LastSentAt: util.Pointer(tn(4)),
},
},
},
@ -956,6 +1056,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(5),
EndsAt: tn(5).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(5),
LastSentAt: util.Pointer(tn(5)),
},
},
},
@ -988,6 +1089,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t2,
EndsAt: t2.Add(state.ResendDelay * 4),
LastEvaluationTime: t2,
LastSentAt: &t2,
EvaluationDuration: evaluationDuration,
Annotations: map[string]string{"annotation": "test", "Error": "[sse.dataQueryError] failed to execute query [A]: this is an error"},
},
@ -1021,6 +1123,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t3,
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
LastSentAt: &t3, // Resend delay is 30s, so last sent at is t3.
},
},
},
@ -1052,6 +1155,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(4),
EndsAt: tn(4).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(4),
LastSentAt: util.Pointer(tn(4)),
},
},
},
@ -1139,6 +1243,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(4),
EndsAt: tn(6).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(6),
LastSentAt: util.Pointer(tn(6)), // After 30s resend delay, last sent at is t6.
},
},
},
@ -1169,6 +1274,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(8),
EndsAt: tn(8).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(8),
LastSentAt: util.Pointer(tn(5)),
},
},
},
@ -1199,6 +1305,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: tn(6),
EndsAt: tn(6).Add(state.ResendDelay * 4),
LastEvaluationTime: tn(6),
LastSentAt: util.Pointer(tn(5)),
},
},
},
@ -1265,6 +1372,7 @@ func TestProcessEvalResults(t *testing.T) {
StartsAt: t3,
EndsAt: t3.Add(state.ResendDelay * 4),
LastEvaluationTime: t3,
LastSentAt: &t1, // Resend delay is 30s, so last sent at is t1.
},
},
},
@ -1306,8 +1414,9 @@ func TestProcessEvalResults(t *testing.T) {
res[i].EvaluatedAt = evalTime
}
clk.Set(evalTime)
_ = st.ProcessEvalResults(context.Background(), evalTime, tc.alertRule, res, systemLabels)
processedStates := st.ProcessEvalResults(context.Background(), evalTime, tc.alertRule, res, systemLabels)
results += len(res)
_ = state.FromStateTransitionToPostableAlerts(evalTime, processedStates, st, &url.URL{}) // Set LastSentAt.
}
states := st.GetStatesForRuleUID(tc.alertRule.OrgID, tc.alertRule.UID)
@ -1670,7 +1779,7 @@ func TestStaleResults(t *testing.T) {
assert.Equal(t, models.StateReasonMissingSeries, s.StateReason)
assert.Equal(t, clk.Now(), s.EndsAt)
if s.CacheID == state2 {
assert.Truef(t, s.Resolved, "Returned stale state should have Resolved set to true")
assert.Equalf(t, clk.Now(), *s.ResolvedAt, "Returned stale state should have ResolvedAt set")
}
key, err := s.GetAlertInstanceKey()
require.NoError(t, err)
@ -1819,11 +1928,11 @@ func TestDeleteStateByRuleUID(t *testing.T) {
assert.Equal(t, expectedReason, s.StateReason)
if oldState.State == eval.Normal {
assert.Equal(t, oldState.StartsAt, s.StartsAt)
assert.False(t, s.Resolved)
assert.Zero(t, s.ResolvedAt)
} else {
assert.Equal(t, clk.Now(), s.StartsAt)
if oldState.State == eval.Alerting {
assert.True(t, s.Resolved)
assert.Equal(t, clk.Now(), *s.ResolvedAt)
}
}
assert.Equal(t, clk.Now(), s.EndsAt)
@ -1959,11 +2068,11 @@ func TestResetStateByRuleUID(t *testing.T) {
assert.Equal(t, models.StateReasonPaused, s.StateReason)
if oldState.State == eval.Normal {
assert.Equal(t, oldState.StartsAt, s.StartsAt)
assert.False(t, s.Resolved)
assert.Zero(t, s.ResolvedAt)
} else {
assert.Equal(t, clk.Now(), s.StartsAt)
if oldState.State == eval.Alerting {
assert.True(t, s.Resolved)
assert.Equal(t, clk.Now(), *s.ResolvedAt)
}
}
assert.Equal(t, clk.Now(), s.EndsAt)

View File

@ -45,10 +45,6 @@ type State struct {
// can still contain the results of previous evaluations.
Error error
// Resolved is set to true if this state is the transitional state between Firing and Normal.
// All subsequent states will be false until the next transition from Firing to Normal.
Resolved bool
// Image contains an optional image for the state. It tends to be included in notifications
// as a visualization to show why the alert fired.
Image *models.Image
@ -65,9 +61,15 @@ type State struct {
// conditions.
Values map[string]float64
StartsAt time.Time
EndsAt time.Time
LastSentAt time.Time
StartsAt time.Time
// EndsAt is different from the Prometheus EndsAt as EndsAt is updated for both Normal states
// and states that have been resolved. It cannot be used to determine when a state was resolved.
EndsAt time.Time
// ResolvedAt is set when the state is first resolved. That is to say, when the state first transitions
// from Alerting, NoData, or Error to Normal. It is reset to zero when the state transitions from Normal
// to any other state.
ResolvedAt *time.Time
LastSentAt *time.Time
LastEvaluationString string
LastEvaluationTime time.Time
EvaluationDuration time.Duration
@ -134,14 +136,6 @@ func (a *State) SetNormal(reason string, startsAt, endsAt time.Time) {
a.Error = nil
}
// Resolve sets the State to Normal. It updates the StateReason, the end time, and sets Resolved to true.
func (a *State) Resolve(reason string, endsAt time.Time) {
a.State = eval.Normal
a.StateReason = reason
a.Resolved = true
a.EndsAt = endsAt
}
// Maintain updates the end time using the most recent evaluation.
func (a *State) Maintain(interval int64, evaluatedAt time.Time) {
a.EndsAt = nextEndsTime(interval, evaluatedAt)
@ -400,19 +394,31 @@ func resultKeepLast(state *State, rule *models.AlertRule, result eval.Result, lo
}
}
func (a *State) NeedsSending(resendDelay time.Duration) bool {
switch a.State {
case eval.Pending:
// We do not send notifications for pending states
// NeedsSending returns true if the given state needs to be sent to the Alertmanager.
// Reasons for sending include:
// - The state has been resolved since the last notification.
// - The state is firing and the last notification was sent at least resendDelay ago.
// - The state was resolved within the resolvedRetention period, and the last notification was sent at least resendDelay ago.
func (a *State) NeedsSending(resendDelay time.Duration, resolvedRetention time.Duration) bool {
if a.State == eval.Pending {
// We do not send notifications for pending states.
return false
case eval.Normal:
// We should send a notification if the state is Normal because it was resolved
return a.Resolved
default:
// We should send, and re-send notifications, each time LastSentAt is <= LastEvaluationTime + resendDelay
nextSent := a.LastSentAt.Add(resendDelay)
return nextSent.Before(a.LastEvaluationTime) || nextSent.Equal(a.LastEvaluationTime)
}
// We should send a notification if the state has been resolved since the last notification.
if a.ResolvedAt != nil && (a.LastSentAt == nil || a.ResolvedAt.After(*a.LastSentAt)) {
return true
}
// For normal states, we should only be sending if this is a resolved notification or a re-send of the resolved
// notification within the resolvedRetention period.
if a.State == eval.Normal && (a.ResolvedAt == nil || a.LastEvaluationTime.Sub(*a.ResolvedAt) > resolvedRetention) {
return false
}
// We should send, and re-send notifications, each time LastSentAt is <= LastEvaluationTime + resendDelay.
// This can include normal->normal transitions that were resolved in recent past evaluations.
return a.LastSentAt == nil || !a.LastSentAt.Add(resendDelay).After(a.LastEvaluationTime)
}
func (a *State) Equals(b *State) bool {

View File

@ -11,11 +11,12 @@ import (
"github.com/benbjohnson/clock"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/grafana/alerting/models"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/alerting/models"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -350,10 +351,11 @@ func TestEnd(t *testing.T) {
func TestNeedsSending(t *testing.T) {
evaluationTime, _ := time.Parse("2006-01-02", "2021-03-25")
testCases := []struct {
name string
resendDelay time.Duration
expected bool
testState *State
name string
resendDelay time.Duration
resolvedRetention time.Duration
expected bool
testState *State
}{
{
name: "state: alerting and LastSentAt before LastEvaluationTime + ResendDelay",
@ -362,7 +364,7 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.Alerting,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-2 * time.Minute),
LastSentAt: util.Pointer(evaluationTime.Add(-2 * time.Minute)),
},
},
{
@ -372,7 +374,7 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.Alerting,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime,
LastSentAt: util.Pointer(evaluationTime),
},
},
{
@ -382,7 +384,7 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.Alerting,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-1 * time.Minute),
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
@ -400,18 +402,54 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.Alerting,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime,
LastSentAt: util.Pointer(evaluationTime),
},
},
{
name: "state: normal + resolved should send without waiting",
name: "state: normal + resolved should send without waiting if ResolvedAt > LastSentAt",
resendDelay: 1 * time.Minute,
expected: true,
testState: &State{
State: eval.Normal,
Resolved: true,
ResolvedAt: util.Pointer(evaluationTime),
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime,
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
name: "state: normal + recently resolved should send with wait",
resendDelay: 1 * time.Minute,
resolvedRetention: 15 * time.Minute,
expected: true,
testState: &State{
State: eval.Normal,
ResolvedAt: util.Pointer(evaluationTime.Add(-2 * time.Minute)),
LastEvaluationTime: evaluationTime,
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
name: "state: normal + recently resolved should not send without wait",
resendDelay: 2 * time.Minute,
resolvedRetention: 15 * time.Minute,
expected: false,
testState: &State{
State: eval.Normal,
ResolvedAt: util.Pointer(evaluationTime.Add(-2 * time.Minute)),
LastEvaluationTime: evaluationTime,
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
name: "state: normal + not recently resolved should not send even with wait",
resendDelay: 1 * time.Minute,
resolvedRetention: 15 * time.Minute,
expected: false,
testState: &State{
State: eval.Normal,
ResolvedAt: util.Pointer(evaluationTime.Add(-16 * time.Minute)),
LastEvaluationTime: evaluationTime,
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
@ -420,9 +458,9 @@ func TestNeedsSending(t *testing.T) {
expected: false,
testState: &State{
State: eval.Normal,
Resolved: false,
ResolvedAt: util.Pointer(time.Time{}),
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-1 * time.Minute),
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
@ -432,7 +470,7 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.NoData,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-1 * time.Minute),
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
@ -442,7 +480,7 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.NoData,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second),
LastSentAt: util.Pointer(evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second)),
},
},
{
@ -452,7 +490,7 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.Error,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-1 * time.Minute),
LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)),
},
},
{
@ -462,14 +500,14 @@ func TestNeedsSending(t *testing.T) {
testState: &State{
State: eval.Error,
LastEvaluationTime: evaluationTime,
LastSentAt: evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second),
LastSentAt: util.Pointer(evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second)),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.testState.NeedsSending(tc.resendDelay))
assert.Equal(t, tc.expected, tc.testState.NeedsSending(tc.resendDelay, tc.resolvedRetention))
})
}
}
@ -531,13 +569,6 @@ func TestGetLastEvaluationValuesForCondition(t *testing.T) {
})
}
func TestResolve(t *testing.T) {
s := State{State: eval.Alerting, EndsAt: time.Now().Add(time.Minute)}
expected := State{State: eval.Normal, StateReason: "This is a reason", EndsAt: time.Now(), Resolved: true}
s.Resolve("This is a reason", expected.EndsAt)
assert.Equal(t, expected, s)
}
func TestShouldTakeImage(t *testing.T) {
tests := []struct {
name string

View File

@ -6,11 +6,12 @@ import (
"strings"
"time"
alertingCluster "github.com/grafana/alerting/cluster"
dstls "github.com/grafana/dskit/crypto/tls"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"gopkg.in/ini.v1"
alertingCluster "github.com/grafana/alerting/cluster"
"github.com/grafana/grafana/pkg/util"
)
@ -113,6 +114,9 @@ type UnifiedAlertingSettings struct {
// Retention period for Alertmanager notification log entries.
NotificationLogRetention time.Duration
// Duration for which a resolved alert state transition will continue to be sent to the Alertmanager.
ResolvedAlertRetention time.Duration
}
type RecordingRuleSettings struct {
@ -435,6 +439,11 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error {
return err
}
uaCfg.ResolvedAlertRetention, err = gtime.ParseDuration(valueAsString(ua, "resolved_alert_retention", (15 * time.Minute).String()))
if err != nil {
return err
}
cfg.UnifiedAlerting = uaCfg
return nil
}

View File

@ -0,0 +1,541 @@
package timeinterval
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/generated/clientset/versioned"
"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/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/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 TestIntegrationResourceIdentifier(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().TimeIntervals("default")
newInterval := &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.TimeIntervalSpec{
Name: "time-newInterval",
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
},
}
t.Run("create should fail if object name is specified", func(t *testing.T) {
interval := newInterval.DeepCopy()
interval.Name = "time-newInterval"
_, err := client.Create(ctx, interval, v1.CreateOptions{})
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
})
var resourceID string
t.Run("create should succeed and provide resource name", func(t *testing.T) {
actual, err := client.Create(ctx, newInterval, v1.CreateOptions{})
require.NoError(t, err)
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
resourceID = actual.Name
})
var existingInterval *v0alpha1.TimeInterval
t.Run("resource should be available by the identifier", func(t *testing.T) {
actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
require.NoError(t, err)
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
require.Equal(t, newInterval.Spec, actual.Spec)
existingInterval = actual
})
t.Run("update should fail if name in the specification changes", func(t *testing.T) {
if existingInterval == nil {
t.Skip()
}
updated := existingInterval.DeepCopy()
updated.Spec.Name = "another-newInterval"
_, err := client.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
})
}
func TestIntegrationTimeIntervalAccessControl(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
canCreate bool
canDelete bool
}
reader := helper.CreateUser("IntervalsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
},
},
})
writer := helper.CreateUser("IntervalsWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite,
},
},
})
deleter := helper.CreateUser("IntervalsDeleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete,
},
},
})
testCases := []testCase{
{
user: org1.Admin,
canRead: true,
canUpdate: true,
canCreate: true,
canDelete: true,
},
{
user: org1.Editor,
canRead: true,
canUpdate: true,
canCreate: true,
canDelete: true,
},
{
user: org1.Viewer,
canRead: true,
},
{
user: reader,
canRead: true,
},
{
user: writer,
canRead: true,
canCreate: true,
canUpdate: true,
},
{
user: deleter,
canRead: true,
canDelete: true,
},
}
admin := org1.Admin
adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig())
require.NoError(t, err)
adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("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().TimeIntervals("default")
var expected = &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Annotations: map[string]string{
"grafana.com/provenance": "",
},
},
Spec: v0alpha1.TimeIntervalSpec{
Name: fmt.Sprintf("time-interval-1-%s", tc.user.Identity.GetLogin()),
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
},
}
d, err := json.Marshal(expected)
require.NoError(t, err)
if tc.canCreate {
t.Run("should be able to create time interval", func(t *testing.T) {
actual, err := client.Create(ctx, expected, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.Equal(t, expected.Spec, actual.Spec)
t.Run("should fail if already exists", func(t *testing.T) {
_, err := client.Create(ctx, actual, v1.CreateOptions{})
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
})
expected = actual
})
} else {
t.Run("should be forbidden to create", func(t *testing.T) {
_, err := client.Create(ctx, expected, v1.CreateOptions{})
require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
})
// create resource to proceed with other tests
expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.NotNil(t, expected)
}
if tc.canRead {
t.Run("should be able to list time intervals", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1)
})
t.Run("should be able to read time interval by resource identifier", func(t *testing.T) {
got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, expected, got)
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 time intervals", func(t *testing.T) {
_, err := client.List(ctx, v1.ListOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
t.Run("should be forbidden to read time interval by name", func(t *testing.T) {
_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
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.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
}
updatedExpected := expected.DeepCopy()
updatedExpected.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2)
d, err = json.Marshal(updatedExpected)
require.NoError(t, err)
if tc.canUpdate {
t.Run("should be able to update time interval", func(t *testing.T) {
updated, err := client.Update(ctx, updatedExpected, 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 := updatedExpected.DeepCopy()
up.Name = "notFound"
_, err := client.Update(ctx, up, v1.UpdateOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to update time interval", func(t *testing.T) {
_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
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 := updatedExpected.DeepCopy()
up.Name = "notFound"
_, err := client.Update(ctx, up, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
}
deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
if tc.canDelete {
t.Run("should be able to delete time interval", func(t *testing.T) {
err := client.Delete(ctx, expected.Name, 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.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to delete time interval", func(t *testing.T) {
err := client.Delete(ctx, expected.Name, deleteOptions)
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.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
}
if tc.canRead {
t.Run("should get empty list if no mute timings", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 0)
})
}
})
}
}
func TestIntegrationTimeIntervalProvisioning(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().TimeIntervals("default")
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac)
require.NoError(t, err)
created, err := adminClient.Create(ctx, &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.TimeIntervalSpec{
Name: "time-interval-1",
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
},
}, v1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, "", created.Annotations["grafana.com/provenance"])
t.Run("should provide provenance status", func(t *testing.T) {
require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{
MuteTimeInterval: config.MuteTimeInterval{
Name: created.Spec.Name,
},
}, admin.Identity.GetOrgID(), "API"))
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, "API", got.Annotations["grafana.com/provenance"])
})
t.Run("should not let update if provisioned", func(t *testing.T) {
updated := created.DeepCopy()
updated.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2)
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
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, created.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
}
func TestIntegrationTimeIntervalOptimisticConcurrency(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().TimeIntervals("default")
interval := v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.TimeIntervalSpec{
Name: "time-interval",
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
},
}
created, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, created)
require.NotEmpty(t, created.ResourceVersion)
t.Run("should forbid if version does not match", func(t *testing.T) {
updated := created.DeepCopy()
updated.ResourceVersion = "test"
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
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 := created.DeepCopy()
updated.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2)
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) {
updated := created.DeepCopy()
updated.ResourceVersion = ""
updated.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2)
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.NoError(t, err)
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion)
})
t.Run("should fail to delete if version does not match", func(t *testing.T) {
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer("something"),
},
})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
})
t.Run("should succeed if version matches", func(t *testing.T) {
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer(actual.ResourceVersion),
},
})
require.NoError(t, err)
})
t.Run("should succeed if version is empty", func(t *testing.T) {
actual, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer(actual.ResourceVersion),
},
})
require.NoError(t, err)
})
}
func TestIntegrationTimeIntervalPatch(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().TimeIntervals("default")
interval := v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.TimeIntervalSpec{
Name: "time-interval",
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
},
}
current, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, current)
require.NotEmpty(t, current.ResourceVersion)
t.Run("should patch with merge patch", func(t *testing.T) {
patch := `{
"spec": {
"time_intervals" : []
}
}`
result, err := adminClient.Patch(ctx, current.Name, types.MergePatchType, []byte(patch), v1.PatchOptions{})
require.NoError(t, err)
require.Empty(t, result.Spec.TimeIntervals)
current = result
})
t.Run("should patch with json patch", func(t *testing.T) {
expected := v0alpha1.IntervalGenerator{}.Generate()
patch := []map[string]interface{}{
{
"op": "add",
"path": "/spec/time_intervals/-",
"value": expected,
},
}
patchData, err := json.Marshal(patch)
require.NoError(t, err)
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
require.NoError(t, err)
expectedSpec := *current.Spec.DeepCopy()
expectedSpec.TimeIntervals = []v0alpha1.Interval{
expected,
}
require.EqualValues(t, expectedSpec, result.Spec)
current = result
})
}

View File

@ -28,8 +28,11 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
@ -40,6 +43,8 @@ import (
"github.com/grafana/grafana/pkg/tests/testinfra"
)
const Org1 = "Org1"
type K8sTestHelper struct {
t *testing.T
env server.TestEnv
@ -63,7 +68,7 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper {
namespacer: request.GetNamespaceMapper(nil),
}
c.Org1 = c.createTestUsers("Org1")
c.Org1 = c.createTestUsers(Org1)
c.OrgB = c.createTestUsers("OrgB")
c.loadAPIGroups()
@ -88,6 +93,10 @@ func (c *K8sTestHelper) loadAPIGroups() {
}
}
func (c *K8sTestHelper) GetEnv() server.TestEnv {
return c.env
}
func (c *K8sTestHelper) Shutdown() {
err := c.env.Server.Shutdown(context.Background(), "done")
require.NoError(c.t, err)
@ -172,7 +181,7 @@ func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string {
}
out, err := json.MarshalIndent(copy, "", " ")
//fmt.Printf("%s", out)
// fmt.Printf("%s", out)
require.NoError(c.t, err)
return string(out)
}
@ -374,7 +383,16 @@ func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: unstructuredMap}
}
func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers {
func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers {
c.t.Helper()
return OrgUsers{
Admin: c.CreateUser("admin", orgName, org.RoleAdmin, nil),
Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil),
Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil),
}
}
func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.RoleType, permissions []resourcepermissions.SetResourcePermissionCommand) User {
c.t.Helper()
store := c.env.SQLStore
@ -389,9 +407,17 @@ func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers {
require.NoError(c.t, err)
orgId := int64(1)
if orgName != "Org1" {
orgId, err = orgService.GetOrCreate(context.Background(), orgName)
require.NoError(c.t, err)
if orgName != Org1 {
o, err := orgService.GetByName(context.Background(), &org.GetOrgByNameQuery{Name: orgName})
if err != nil {
if !org.ErrOrgNotFound.Is(err) {
require.NoError(c.t, err)
}
orgId, err = orgService.GetOrCreate(context.Background(), orgName)
require.NoError(c.t, err)
} else {
orgId = o.ID
}
}
c.env.Cfg.AutoAssignOrg = true
c.env.Cfg.AutoAssignOrgId = int(orgId)
@ -407,37 +433,52 @@ func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers {
require.NoError(c.t, err)
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
createUser := func(key string, role org.RoleType) User {
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
DefaultOrgRole: string(role),
Password: user.Password(key),
Login: fmt.Sprintf("%s-%d", key, orgId),
OrgID: orgId,
})
require.NoError(c.t, err)
require.Equal(c.t, orgId, u.OrgID)
require.True(c.t, u.ID > 0)
s, err := userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
UserID: u.ID,
Login: u.Login,
Email: u.Email,
OrgID: orgId,
})
require.NoError(c.t, err)
require.Equal(c.t, orgId, s.OrgID)
require.Equal(c.t, role, s.OrgRole) // make sure the role was set properly
u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{
DefaultOrgRole: string(basicRole),
Password: user.Password(name),
Login: fmt.Sprintf("%s-%d", name, orgId),
OrgID: orgId,
})
require.NoError(c.t, err)
require.Equal(c.t, orgId, u.OrgID)
require.True(c.t, u.ID > 0)
return User{
Identity: s,
password: key,
baseURL: baseUrl,
}
s, err := userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{
UserID: u.ID,
Login: u.Login,
Email: u.Email,
OrgID: orgId,
})
require.NoError(c.t, err)
require.Equal(c.t, orgId, s.OrgID)
require.Equal(c.t, basicRole, s.OrgRole) // make sure the role was set properly
usr := User{
Identity: s,
password: name,
baseURL: baseUrl,
}
return OrgUsers{
Admin: createUser("admin", org.RoleAdmin),
Editor: createUser("editor", org.RoleEditor),
Viewer: createUser("viewer", org.RoleViewer),
if len(permissions) > 0 {
c.SetPermissions(usr, permissions)
}
return usr
}
func (c *K8sTestHelper) SetPermissions(user User, permissions []resourcepermissions.SetResourcePermissionCommand) {
id, err := user.Identity.GetID().UserID()
require.NoError(c.t, err)
permissionsStore := resourcepermissions.NewStore(c.env.Cfg, c.env.SQLStore, featuremgmt.WithFeatures())
for _, permission := range permissions {
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
user.Identity.GetOrgID(),
accesscontrol.User{ID: id},
permission, nil)
require.NoError(c.t, err)
}
}

View File

@ -41,6 +41,7 @@ export function QueryTemplatesList() {
const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || '';
return {
index: index.toString(),
uid: queryTemplate.uid,
datasourceRef,
datasourceType,
createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0,

View File

@ -1,16 +1,67 @@
import React from 'react';
import { reportInteraction, getAppEvents } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { IconButton } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createSuccessNotification } from 'app/core/copy/appNotification';
import { t } from 'app/core/internationalization';
import { useDeleteQueryTemplateMutation } from 'app/features/query-library';
import { dispatch } from 'app/store/store';
import { ShowConfirmModalEvent } from 'app/types/events';
import ExploreRunQueryButton from '../../ExploreRunQueryButton';
import { useQueryLibraryListStyles } from './styles';
interface ActionsCellProps {
queryUid?: string;
query?: DataQuery;
rootDatasourceUid?: string;
}
function ActionsCell({ query, rootDatasourceUid }: ActionsCellProps) {
return <ExploreRunQueryButton queries={query ? [query] : []} rootDatasourceUid={rootDatasourceUid} />;
function ActionsCell({ query, rootDatasourceUid, queryUid }: ActionsCellProps) {
const [deleteQueryTemplate] = useDeleteQueryTemplateMutation();
const styles = useQueryLibraryListStyles();
const onDeleteQuery = (queryUid: string) => {
const performDelete = (queryUid: string) => {
deleteQueryTemplate({ uid: queryUid });
dispatch(notifyApp(createSuccessNotification(t('explore.query-library.query-deleted', 'Query deleted'))));
reportInteraction('grafana_explore_query_library_deleted');
};
getAppEvents().publish(
new ShowConfirmModalEvent({
title: t('explore.query-library.delete-query-title', 'Delete query'),
text: t(
'explore.query-library.delete-query-text',
"You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?"
),
yesText: t('query-library.delete-query-button', 'Delete query'),
icon: 'trash-alt',
onConfirm: () => performDelete(queryUid),
})
);
};
return (
<div className={styles.cell}>
<IconButton
className={styles.actionButton}
size="lg"
name="trash-alt"
title={t('explore.query-library.delete-query', 'Delete query')}
tooltip={t('explore.query-library.delete-query', 'Delete query')}
onClick={() => {
if (queryUid) {
onDeleteQuery(queryUid);
}
}}
/>
<ExploreRunQueryButton queries={query ? [query] : []} rootDatasourceUid={rootDatasourceUid} />
</div>
);
}
export default ActionsCell;

View File

@ -26,7 +26,7 @@ const columns: Array<Column<QueryTemplateRow>> = [
id: 'actions',
header: '',
cell: ({ row: { original } }) => (
<ActionsCell query={original.query} rootDatasourceUid={original.datasourceRef?.uid} />
<ActionsCell query={original.query} rootDatasourceUid={original.datasourceRef?.uid} queryUid={original.uid} />
),
},
];

View File

@ -34,4 +34,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
WebkitLineClamp: 1,
overflow: 'hidden',
}),
cell: css({
display: 'flex',
alignItems: 'center',
'&:last-child': {
justifyContent: 'end',
},
}),
actionButton: css({
padding: theme.spacing(1),
}),
});

View File

@ -7,4 +7,5 @@ export type QueryTemplateRow = {
datasourceRef?: DataSourceRef | null;
datasourceType?: string;
createdAtTimestamp?: number;
uid?: string;
};

View File

@ -1,6 +1,6 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { AddQueryTemplateCommand, QueryTemplate } from '../types';
import { AddQueryTemplateCommand, DeleteQueryTemplateCommand, QueryTemplate } from '../types';
import { convertAddQueryTemplateCommandToDataQuerySpec, convertDataQueryResponseToQueryTemplates } from './mappers';
import { baseQuery } from './query';
@ -21,6 +21,13 @@ export const queryLibraryApi = createApi({
}),
invalidatesTags: ['QueryTemplatesList'],
}),
deleteQueryTemplate: builder.mutation<void, DeleteQueryTemplateCommand>({
query: ({ uid }) => ({
url: `${uid}`,
method: 'DELETE',
}),
invalidatesTags: ['QueryTemplatesList'],
}),
}),
reducerPath: 'queryLibrary',
});

View File

@ -24,15 +24,20 @@ export enum QueryTemplateKinds {
*/
export const BASE_URL = `/apis/${API_VERSION}/namespaces/default/querytemplates/`;
// URL is optional for these requests
interface QueryLibraryBackendRequest extends Pick<BackendSrvRequest, 'data' | 'method'> {
url?: string;
}
/**
* TODO: similar code is duplicated in many places. To be unified in #86960
*/
export const baseQuery: BaseQueryFn<Pick<BackendSrvRequest, 'data' | 'method'>, DataQuerySpecResponse, Error> = async (
export const baseQuery: BaseQueryFn<QueryLibraryBackendRequest, DataQuerySpecResponse, Error> = async (
requestOptions
) => {
try {
const responseObservable = getBackendSrv().fetch<DataQuerySpecResponse>({
url: BASE_URL,
url: `${BASE_URL}${requestOptions.url ?? ''}`,
showErrorAlert: true,
method: requestOptions.method || 'GET',
data: requestOptions.data,

View File

@ -12,7 +12,8 @@ import { config } from '@grafana/runtime';
import { queryLibraryApi } from './api/factory';
import { mockData } from './api/mocks';
export const { useAllQueryTemplatesQuery, useAddQueryTemplateMutation } = queryLibraryApi;
export const { useAllQueryTemplatesQuery, useAddQueryTemplateMutation, useDeleteQueryTemplateMutation } =
queryLibraryApi;
export function isQueryLibraryEnabled() {
return config.featureToggles.queryLibrary;

View File

@ -11,3 +11,7 @@ export type AddQueryTemplateCommand = {
title: string;
targets: DataQuery[];
};
export type DeleteQueryTemplateCommand = {
uid: string;
};

View File

@ -532,6 +532,12 @@
"scan-for-older-logs": "Scan for older logs",
"stop-scan": "Stop scan"
},
"query-library": {
"delete-query": "Delete query",
"delete-query-text": "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?",
"delete-query-title": "Delete query",
"query-deleted": "Query deleted"
},
"rich-history": {
"close-tooltip": "Close query history",
"datasource-a-z": "Data source A-Z",
@ -1559,6 +1565,9 @@
"role-label": "Role"
}
},
"query-library": {
"delete-query-button": "Delete query"
},
"query-operation": {
"header": {
"collapse-row": "Collapse query row",

View File

@ -532,6 +532,12 @@
"scan-for-older-logs": "Ŝčäʼn ƒőř őľđęř ľőģş",
"stop-scan": "Ŝŧőp şčäʼn"
},
"query-library": {
"delete-query": "Đęľęŧę qūęřy",
"delete-query-text": "Ÿőū'řę äþőūŧ ŧő řęmővę ŧĥįş qūęřy ƒřőm ŧĥę qūęřy ľįþřäřy. Ŧĥįş äčŧįőʼn čäʼnʼnőŧ þę ūʼnđőʼnę. Đő yőū ŵäʼnŧ ŧő čőʼnŧįʼnūę?",
"delete-query-title": "Đęľęŧę qūęřy",
"query-deleted": "Qūęřy đęľęŧęđ"
},
"rich-history": {
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
"datasource-a-z": "Đäŧä şőūřčę Å-Ż",
@ -1559,6 +1565,9 @@
"role-label": "Ŗőľę"
}
},
"query-library": {
"delete-query-button": "Đęľęŧę qūęřy"
},
"query-operation": {
"header": {
"collapse-row": "Cőľľäpşę qūęřy řőŵ",