diff --git a/pkg/storage/unified/search/document.go b/pkg/storage/unified/search/document.go index e30c55ca6ae..ebe568af1e0 100644 --- a/pkg/storage/unified/search/document.go +++ b/pkg/storage/unified/search/document.go @@ -65,6 +65,11 @@ func (s *StandardDocumentBuilders) GetDocumentBuilders() ([]resource.DocumentBui }, nil }) + users, err := GetUserBuilder() + if err != nil { + return nil, err + } + return []resource.DocumentBuilderInfo{ // The default builder { @@ -72,5 +77,7 @@ func (s *StandardDocumentBuilders) GetDocumentBuilders() ([]resource.DocumentBui }, // Dashboard builder dashboards, + // User builder + users, }, err } diff --git a/pkg/storage/unified/search/testdata/doc/user-example-out.json b/pkg/storage/unified/search/testdata/doc/user-example-out.json new file mode 100644 index 00000000000..4445b0cdba6 --- /dev/null +++ b/pkg/storage/unified/search/testdata/doc/user-example-out.json @@ -0,0 +1,17 @@ +{ + "key": { + "namespace": "default", + "group": "iam.grafana.app", + "resource": "users", + "name": "example" + }, + "name": "example", + "rv": 1234, + "title": "example", + "title_ngram": "example", + "title_phrase": "example", + "fields": { + "email": "example@example.com", + "login": "example" + } +} diff --git a/pkg/storage/unified/search/testdata/doc/user-example.json b/pkg/storage/unified/search/testdata/doc/user-example.json new file mode 100644 index 00000000000..8c6387d0cb2 --- /dev/null +++ b/pkg/storage/unified/search/testdata/doc/user-example.json @@ -0,0 +1,13 @@ +{ + "apiVersion": "iam.grafana.app/v0alpha1", + "kind": "User", + "metadata": { + "name": "example", + "namespace": "default" + }, + "spec": { + "login": "example", + "email": "example@example.com", + "role": "Viewer" + } +} diff --git a/pkg/storage/unified/search/user.go b/pkg/storage/unified/search/user.go new file mode 100644 index 00000000000..ac8b466dfa1 --- /dev/null +++ b/pkg/storage/unified/search/user.go @@ -0,0 +1,80 @@ +package search + +import ( + "context" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + USER_EMAIL = "email" + USER_LOGIN = "login" +) + +func GetUserBuilder() (resource.DocumentBuilderInfo, error) { + fields, err := resource.NewSearchableDocumentFields([]*resourcepb.ResourceTableColumnDefinition{ + { + Name: USER_EMAIL, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + Description: "The email address of the user", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + Filterable: true, + }, + }, + { + Name: USER_LOGIN, + Type: resourcepb.ResourceTableColumnDefinition_STRING, + Description: "The login of the user", + Properties: &resourcepb.ResourceTableColumnDefinition_Properties{ + UniqueValues: true, + Filterable: true, + }, + }, + }) + return resource.DocumentBuilderInfo{ + GroupResource: iamv0.UserResourceInfo.GroupResource(), + Fields: fields, + Builder: new(userDocumentBuilder), + }, err +} + +var _ resource.DocumentBuilder = new(userDocumentBuilder) + +type userDocumentBuilder struct{} + +// BuildDocument implements resource.DocumentBuilder. +func (u *userDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb.ResourceKey, rv int64, value []byte) (*resource.IndexableDocument, error) { + tmp := &unstructured.Unstructured{} + err := tmp.UnmarshalJSON(value) + if err != nil { + return nil, err + } + + obj, err := utils.MetaAccessor(tmp) + if err != nil { + return nil, err + } + + doc := resource.NewIndexableDocument(key, rv, obj) + + if spec, err := obj.GetSpec(); err == nil { + if m, ok := spec.(map[string]any); ok { + email, _ := m[USER_EMAIL].(string) + login, _ := m[USER_LOGIN].(string) + if email != "" || login != "" { + doc.Fields = make(map[string]any) + if email != "" { + doc.Fields[USER_EMAIL] = email + } + if login != "" { + doc.Fields[USER_LOGIN] = login + } + } + } + } + return doc, nil +} diff --git a/pkg/storage/unified/search/user_test.go b/pkg/storage/unified/search/user_test.go new file mode 100644 index 00000000000..31f5c87e5ce --- /dev/null +++ b/pkg/storage/unified/search/user_test.go @@ -0,0 +1,73 @@ +package search_test + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/storage/unified/search" +) + +func TestUserDocumentBuilder(t *testing.T) { + info, err := search.GetUserBuilder() + require.NoError(t, err) + require.NotNil(t, info.Builder) + + builder := info.Builder + + base := filepath.Join("testdata", "doc") + inPath := filepath.Join(base, "user-example.json") + outPath := filepath.Join(base, "user-example-out.json") + + // nolint:gosec + raw, err := os.ReadFile(inPath) + if err != nil { + t.Fatalf("missing input fixture %s: %v", inPath, err) + } + + gr := iamv0.UserResourceInfo.GroupResource() + key := &resourcepb.ResourceKey{ + Namespace: "default", + Group: gr.Group, + Resource: gr.Resource, + Name: "example", // set by input file metadata.name + } + + doc, err := builder.BuildDocument(context.Background(), key, 1234, raw) + require.NoError(t, err) + require.NotNil(t, doc) + + // Core + require.Equal(t, key.Name, doc.Name) + require.Equal(t, int64(1234), doc.RV) + require.Equal(t, key, doc.Key) + require.Equal(t, key.Name, doc.Title) + + // Custom field assertions (email/login added to Fields) + if assert.NotNil(t, doc.Fields) { + assert.Equal(t, "example@example.com", doc.Fields[search.USER_EMAIL]) + assert.Equal(t, "example", doc.Fields[search.USER_LOGIN]) + } + + // Snapshot compare + out, err := json.MarshalIndent(doc, "", " ") + require.NoError(t, err) + + // nolint:gosec + expected, readErr := os.ReadFile(outPath) + if readErr != nil { + err2 := os.MkdirAll(filepath.Dir(outPath), 0o755) + require.NoError(t, err2) + err2 = os.WriteFile(outPath, out, 0o600) + require.NoError(t, err2) + t.Fatalf("snapshot created at %s; re-run tests", outPath) + } + assert.JSONEq(t, string(expected), string(out)) +}