mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Support field selectors in time interval API (#90022)
* fix kind of TimeInterval * register custom fields for selectors * support field selectors in legacy storage * support selectors in storage ===== Misc * refactor conversions to build in one place * hide implementation of provenance status behind accessors to use the key in selectors * fix provenance error
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -20,7 +25,7 @@ const (
|
||||
|
||||
var (
|
||||
TimeIntervalResourceInfo = common.NewResourceInfo(GROUP, VERSION,
|
||||
"timeintervals", "timeinterval", "TimeIntervals",
|
||||
"timeintervals", "timeinterval", "TimeInterval",
|
||||
func() runtime.Object { return &TimeInterval{} },
|
||||
func() runtime.Object { return &TimeIntervalList{} },
|
||||
)
|
||||
@@ -51,9 +56,36 @@ func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error {
|
||||
&ReceiverList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, g)
|
||||
|
||||
err := scheme.AddFieldLabelConversionFunc(
|
||||
TimeIntervalResourceInfo.GroupVersionKind(),
|
||||
func(label, value string) (string, string, error) {
|
||||
fieldSet := SelectableTimeIntervalsFields(&TimeInterval{})
|
||||
for key := range fieldSet {
|
||||
if label == key {
|
||||
return label, value, nil
|
||||
}
|
||||
}
|
||||
return "", "", fmt.Errorf("field label not supported for %s: %s", scope.ScopeNodeResourceInfo.GroupVersionKind(), label)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SelectableTimeIntervalsFields(obj *TimeInterval) fields.Set {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{
|
||||
"metadata.provenance": obj.GetProvenanceStatus(),
|
||||
"spec.name": obj.Spec.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
|
||||
46
pkg/apis/alerting_notifications/v0alpha1/types_ext.go
Normal file
46
pkg/apis/alerting_notifications/v0alpha1/types_ext.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package v0alpha1
|
||||
|
||||
const ProvenanceStatusAnnotationKey = "grafana.com/provenance"
|
||||
const ProvenanceStatusNone = "none"
|
||||
|
||||
func (o *TimeInterval) GetProvenanceStatus() string {
|
||||
if o == nil || o.Annotations == nil {
|
||||
return ""
|
||||
}
|
||||
s, ok := o.Annotations[ProvenanceStatusAnnotationKey]
|
||||
if !ok || s == "" {
|
||||
return ProvenanceStatusNone
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (o *TimeInterval) SetProvenanceStatus(status string) {
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string, 1)
|
||||
}
|
||||
if status == "" {
|
||||
status = ProvenanceStatusNone
|
||||
}
|
||||
o.Annotations[ProvenanceStatusAnnotationKey] = status
|
||||
}
|
||||
|
||||
func (o *Receiver) GetProvenanceStatus() string {
|
||||
if o == nil || o.Annotations == nil {
|
||||
return ""
|
||||
}
|
||||
s, ok := o.Annotations[ProvenanceStatusAnnotationKey]
|
||||
if !ok || s == "" {
|
||||
return ProvenanceStatusNone
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (o *Receiver) SetProvenanceStatus(status string) {
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string, 1)
|
||||
}
|
||||
if status == "" {
|
||||
status = ProvenanceStatusNone
|
||||
}
|
||||
o.Annotations[ProvenanceStatusAnnotationKey] = status
|
||||
}
|
||||
@@ -54,19 +54,18 @@ func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver,
|
||||
}
|
||||
|
||||
uid := getUID(receiver) // TODO replace to stable UID when we switch to normal storage
|
||||
return &model.Receiver{
|
||||
r := &model.Receiver{
|
||||
TypeMeta: resourceInfo.TypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(uid), // 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(provenance),
|
||||
},
|
||||
UID: types.UID(uid), // This is needed to make PATCH work
|
||||
Name: uid, // TODO replace to stable UID when we switch to normal storage
|
||||
Namespace: namespacer(orgID),
|
||||
ResourceVersion: "", // TODO: Implement optimistic concurrency.
|
||||
},
|
||||
Spec: spec,
|
||||
}, nil
|
||||
}
|
||||
r.SetProvenanceStatus(string(provenance))
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiReceiver, error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"hash/fnv"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
|
||||
@@ -19,7 +20,7 @@ func getIntervalUID(t definitions.MuteTimeInterval) string {
|
||||
return fmt.Sprintf("%016x", sum.Sum64())
|
||||
}
|
||||
|
||||
func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeIntervalList, error) {
|
||||
func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TimeIntervalList, error) {
|
||||
data, err := json.Marshal(intervals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -30,23 +31,15 @@ func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval
|
||||
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,
|
||||
})
|
||||
item := buildTimeInterval(orgID, interval, spec, namespacer)
|
||||
if selector != nil && !selector.Empty() && !selector.Matches(model.SelectableTimeIntervalsFields(&item)) {
|
||||
continue
|
||||
}
|
||||
result.Items = append(result.Items, item)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -61,21 +54,24 @@ func convertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, na
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := buildTimeInterval(orgID, interval, spec, namespacer)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func buildTimeInterval(orgID int64, interval definitions.MuteTimeInterval, spec model.TimeIntervalSpec, namespacer request.NamespaceMapper) model.TimeInterval {
|
||||
uid := getIntervalUID(interval) // TODO replace to stable UID when we switch to normal storage
|
||||
return &model.TimeInterval{
|
||||
i := 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),
|
||||
},
|
||||
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),
|
||||
ResourceVersion: interval.Version,
|
||||
},
|
||||
Spec: spec,
|
||||
}, nil
|
||||
}
|
||||
i.SetProvenanceStatus(string(interval.Provenance))
|
||||
return i
|
||||
}
|
||||
|
||||
func convertToDomainModel(interval *model.TimeInterval) (definitions.MuteTimeInterval, error) {
|
||||
|
||||
@@ -59,7 +59,7 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
|
||||
func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOptions) (runtime.Object, error) {
|
||||
orgId, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -70,7 +70,7 @@ func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertToK8sResources(orgId, res, s.namespacer)
|
||||
return convertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) {
|
||||
|
||||
@@ -4,10 +4,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"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"
|
||||
apistore "k8s.io/apiserver/pkg/storage"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
@@ -63,7 +66,7 @@ func NewStorage(
|
||||
NewListFunc: resourceInfo.NewListFunc,
|
||||
KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
|
||||
KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
|
||||
PredicateFunc: grafanaregistry.Matcher,
|
||||
PredicateFunc: Matcher,
|
||||
DefaultQualifiedResource: resourceInfo.GroupResource(),
|
||||
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
|
||||
TableConvertor: legacyStore.tableConverter,
|
||||
@@ -71,7 +74,7 @@ func NewStorage(
|
||||
UpdateStrategy: strategy,
|
||||
DeleteStrategy: strategy,
|
||||
}
|
||||
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
|
||||
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
|
||||
if err := s.CompleteWithOptions(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -79,3 +82,19 @@ func NewStorage(
|
||||
}
|
||||
return legacyStore, nil
|
||||
}
|
||||
|
||||
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||
if s, ok := obj.(*model.TimeInterval); ok {
|
||||
return s.Labels, model.SelectableTimeIntervalsFields(s), nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("object of type %T is not supported", obj)
|
||||
}
|
||||
|
||||
// Matcher returns a generic.SelectionPredicate that matches on label and field selectors.
|
||||
func Matcher(label labels.Selector, field fields.Selector) apistore.SelectionPredicate {
|
||||
return apistore.SelectionPredicate{
|
||||
Label: label,
|
||||
Field: field,
|
||||
GetAttrs: GetAttrs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ var (
|
||||
ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one."))
|
||||
|
||||
ErrProvenanceChangeNotAllowed = errutil.Forbidden("alerting.notifications.invalidProvenance").MustTemplate(
|
||||
"Resource with provenance status '{{ .Public.CurrentProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'",
|
||||
errutil.WithPublic("Resource with provenance status '{{ .Public.CurrentProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'. You must use appropriate API to manage this resource"),
|
||||
"Resource with provenance status '{{ .Public.SourceProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'",
|
||||
errutil.WithPublic("Resource with provenance status '{{ .Public.SourceProvenance }}' cannot be managed via API that handles resources with provenance status '{{ .Public.TargetProvenance }}'. You must use appropriate API to manage this resource"),
|
||||
)
|
||||
|
||||
ErrVersionConflict = errutil.Conflict("alerting.notifications.conflict")
|
||||
|
||||
@@ -192,15 +192,13 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
|
||||
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),
|
||||
},
|
||||
}
|
||||
expected.SetProvenanceStatus("")
|
||||
d, err := json.Marshal(expected)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -363,7 +361,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
|
||||
},
|
||||
}, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", created.Annotations["grafana.com/provenance"])
|
||||
require.Equal(t, "none", created.GetProvenanceStatus())
|
||||
|
||||
t.Run("should provide provenance status", func(t *testing.T) {
|
||||
require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{
|
||||
@@ -374,7 +372,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
|
||||
|
||||
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "API", got.Annotations["grafana.com/provenance"])
|
||||
require.Equal(t, "API", got.GetProvenanceStatus())
|
||||
})
|
||||
t.Run("should not let update if provisioned", func(t *testing.T) {
|
||||
updated := created.DeepCopy()
|
||||
@@ -540,3 +538,91 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
|
||||
current = result
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationTimeIntervalListSelector(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")
|
||||
|
||||
interval1 := &v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v0alpha1.TimeIntervalSpec{
|
||||
Name: "test1",
|
||||
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
|
||||
},
|
||||
}
|
||||
interval1, err = adminClient.Create(ctx, interval1, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
interval2 := &v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v0alpha1.TimeIntervalSpec{
|
||||
Name: "test2",
|
||||
TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2),
|
||||
},
|
||||
}
|
||||
interval2, err = adminClient.Create(ctx, interval2, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles, zanzana.NewNoopClient())
|
||||
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{
|
||||
MuteTimeInterval: config.MuteTimeInterval{
|
||||
Name: interval2.Spec.Name,
|
||||
},
|
||||
}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
|
||||
interval2, err = adminClient.Get(ctx, interval2.Name, v1.GetOptions{})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
intervals, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, intervals.Items, 2)
|
||||
|
||||
t.Run("should filter by interval name", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "spec.name=" + interval1.Spec.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, interval1.Name, list.Items[0].Name)
|
||||
})
|
||||
|
||||
t.Run("should filter by interval metadata name", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + interval2.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, interval2.Name, list.Items[0].Name)
|
||||
})
|
||||
|
||||
t.Run("should filter by multiple filters", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s,metadata.provenance=%s", interval2.Name, "API"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, interval2.Name, list.Items[0].Name)
|
||||
})
|
||||
|
||||
t.Run("should be empty when filter does not match", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s,metadata.provenance=%s", interval2.Name, "unknown"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user