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:
Yuri Tseretyan
2024-07-08 22:45:30 +03:00
committed by GitHub
parent 63e715f6a9
commit 5ae5fa3a7a
8 changed files with 221 additions and 43 deletions
@@ -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,
}
}