Alerting: Add field selectors for k8s receivers API (#93015)

Add field selectors for k8s receivers API

metadata.provenance
spec.title
This commit is contained in:
Matthew Jacobson 2024-09-10 10:58:14 -04:00 committed by GitHub
parent f64b121ddb
commit eea28172e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 148 additions and 4 deletions

View File

@ -106,6 +106,22 @@ func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error {
return err
}
err = scheme.AddFieldLabelConversionFunc(
ReceiverResourceInfo.GroupVersionKind(),
func(label, value string) (string, string, error) {
fieldSet := SelectableReceiverFields(&Receiver{})
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
}
@ -119,6 +135,16 @@ func SelectableTimeIntervalsFields(obj *TimeInterval) fields.Set {
})
}
func SelectableReceiverFields(obj *Receiver) fields.Set {
if obj == nil {
return nil
}
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{
"metadata.provenance": obj.GetProvenanceStatus(),
"spec.title": obj.Spec.Title,
})
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()

View File

@ -4,6 +4,7 @@ import (
"maps"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
@ -13,7 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
)
func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, namespacer request.NamespaceMapper) (*model.ReceiverList, error) {
func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, namespacer request.NamespaceMapper, selector fields.Selector) (*model.ReceiverList, error) {
result := &model.ReceiverList{
Items: make([]model.Receiver, 0, len(receivers)),
}
@ -22,6 +23,9 @@ func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, namespac
if err != nil {
return nil, err
}
if selector != nil && !selector.Empty() && !selector.Matches(model.SelectableReceiverFields(k8sResource)) {
continue
}
result.Items = append(result.Items, *k8sResource)
}
return result, nil

View File

@ -61,7 +61,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
@ -85,7 +85,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) {

View File

@ -1,11 +1,17 @@
package receiver
import (
"fmt"
"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"
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"
@ -39,7 +45,7 @@ func NewStorage(
s := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
PredicateFunc: grafanaregistry.Matcher,
PredicateFunc: Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: legacyStore.tableConverter,
@ -55,3 +61,19 @@ func NewStorage(
}
return legacyStore, nil
}
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
if s, ok := obj.(*model.Receiver); ok {
return s.Labels, model.SelectableReceiverFields(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,
}
}

View File

@ -934,6 +934,98 @@ func TestIntegrationCRUD(t *testing.T) {
})
}
func TestIntegrationReceiverListSelector(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().Receivers("default")
require.NoError(t, err)
recv1 := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "test-receiver-1",
Integrations: []v0alpha1.Integration{
createIntegration(t, "email"),
},
},
}
recv1, err = adminClient.Create(ctx, recv1, v1.CreateOptions{})
require.NoError(t, err)
recv2 := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "test-receiver-2",
Integrations: []v0alpha1.Integration{
createIntegration(t, "email"),
},
},
}
recv2, err = adminClient.Create(ctx, recv2, 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.EmbeddedContactPoint{
UID: *recv2.Spec.Integrations[0].Uid,
}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
recv2, err = adminClient.Get(ctx, recv2.Name, v1.GetOptions{})
require.NoError(t, err)
receivers, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, receivers.Items, 3) // Includes default.
t.Run("should filter by receiver name", func(t *testing.T) {
list, err := adminClient.List(ctx, v1.ListOptions{
FieldSelector: "spec.title=" + recv1.Spec.Title,
})
require.NoError(t, err)
require.Len(t, list.Items, 1)
require.Equal(t, recv1.Name, list.Items[0].Name)
})
t.Run("should filter by metadata name", func(t *testing.T) {
list, err := adminClient.List(ctx, v1.ListOptions{
FieldSelector: "metadata.name=" + recv2.Name,
})
require.NoError(t, err)
require.Len(t, list.Items, 1)
require.Equal(t, recv2.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", recv2.Name, "API"),
})
require.NoError(t, err)
require.Len(t, list.Items, 1)
require.Equal(t, recv2.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", recv2.Name, "unknown"),
})
require.NoError(t, err)
require.Empty(t, list.Items)
})
}
func createIntegration(t *testing.T, integrationType string) v0alpha1.Integration {
cfg, ok := notify.AllKnownConfigsForTesting[integrationType]
require.Truef(t, ok, "no known config for integration type %s", integrationType)