mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 20:24:41 +08:00
Compare commits
7 Commits
docs/add-t
...
index-owne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57c30681d | ||
|
|
b378907585 | ||
|
|
62bdae94ed | ||
|
|
0091b44b2a | ||
|
|
307e9cdce3 | ||
|
|
66eb5e35cd | ||
|
|
a95de85062 |
@@ -248,6 +248,7 @@ const injectedRtkApi = api
|
|||||||
permission: queryArg.permission,
|
permission: queryArg.permission,
|
||||||
sort: queryArg.sort,
|
sort: queryArg.sort,
|
||||||
limit: queryArg.limit,
|
limit: queryArg.limit,
|
||||||
|
ownerReference: queryArg.ownerReference,
|
||||||
explain: queryArg.explain,
|
explain: queryArg.explain,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -673,6 +674,8 @@ export type SearchDashboardsAndFoldersApiArg = {
|
|||||||
sort?: string;
|
sort?: string;
|
||||||
/** number of results to return */
|
/** number of results to return */
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
/** filter by owner reference in the format {Group}/{Kind}/{Name} */
|
||||||
|
ownerReference?: string;
|
||||||
/** add debugging info that may help explain why the result matched */
|
/** add debugging info that may help explain why the result matched */
|
||||||
explain?: boolean;
|
explain?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -181,6 +181,32 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *
|
|||||||
Schema: spec.Int64Property(),
|
Schema: spec.Int64Property(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ParameterProps: spec3.ParameterProps{
|
||||||
|
Name: "ownerReference", // singular
|
||||||
|
In: "query",
|
||||||
|
Description: "filter by owner reference in the format {Group}/{Kind}/{Name}",
|
||||||
|
Required: false,
|
||||||
|
Schema: spec.StringProperty(),
|
||||||
|
Examples: map[string]*spec3.Example{
|
||||||
|
"": {
|
||||||
|
ExampleProps: spec3.ExampleProps{},
|
||||||
|
},
|
||||||
|
"team": {
|
||||||
|
ExampleProps: spec3.ExampleProps{
|
||||||
|
Summary: "Team owner reference",
|
||||||
|
Value: "iam.grafana.app/Team/xyz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
ExampleProps: spec3.ExampleProps{
|
||||||
|
Summary: "User owner reference",
|
||||||
|
Value: "iam.grafana.app/User/abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ParameterProps: spec3.ParameterProps{
|
ParameterProps: spec3.ParameterProps{
|
||||||
Name: "explain",
|
Name: "explain",
|
||||||
@@ -440,6 +466,15 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ownerReferences filter
|
||||||
|
if vals, ok := queryParams["ownerReference"]; ok {
|
||||||
|
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
||||||
|
Key: resource.SEARCH_FIELD_OWNER_REFERENCES,
|
||||||
|
Operator: "=",
|
||||||
|
Values: vals,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// The libraryPanel filter
|
// The libraryPanel filter
|
||||||
if libraryPanel, ok := queryParams["libraryPanel"]; ok {
|
if libraryPanel, ok := queryParams["libraryPanel"]; ok {
|
||||||
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
||||||
|
|||||||
@@ -129,6 +129,23 @@ func (b *FolderAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
|||||||
Version: runtime.APIVersionInternal,
|
Version: runtime.APIVersionInternal,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Allow searching by owner reference
|
||||||
|
gvk := gv.WithKind("Folder")
|
||||||
|
err := scheme.AddFieldLabelConversionFunc(
|
||||||
|
gvk,
|
||||||
|
func(label, value string) (string, string, error) {
|
||||||
|
if label == "metadata.name" || label == "metadata.namespace" {
|
||||||
|
return label, value, nil
|
||||||
|
}
|
||||||
|
if label == "search.ownerReference" { // TODO: this should become more general
|
||||||
|
return label, value, nil
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("field label not supported for %s: %s", gvk, label)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// If multiple versions exist, then register conversions from zz_generated.conversion.go
|
// If multiple versions exist, then register conversions from zz_generated.conversion.go
|
||||||
// if err := playlist.RegisterConversions(scheme); err != nil {
|
// if err := playlist.RegisterConversions(scheme); err != nil {
|
||||||
// return err
|
// return err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/selection"
|
"k8s.io/apimachinery/pkg/selection"
|
||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
|
|
||||||
@@ -120,6 +121,22 @@ func toListRequest(k *resourcepb.ResourceKey, opts storage.ListOptions) (*resour
|
|||||||
if opts.Predicate.Field != nil && !opts.Predicate.Field.Empty() {
|
if opts.Predicate.Field != nil && !opts.Predicate.Field.Empty() {
|
||||||
requirements := opts.Predicate.Field.Requirements()
|
requirements := opts.Predicate.Field.Requirements()
|
||||||
for _, r := range requirements {
|
for _, r := range requirements {
|
||||||
|
// NOTE: requires: scheme.AddFieldLabelConversionFunc(
|
||||||
|
if r.Field == "search.ownerReference" {
|
||||||
|
if len(requirements) > 1 {
|
||||||
|
return nil, predicate, apierrors.NewBadRequest("search.ownerReference only supports one requirement")
|
||||||
|
}
|
||||||
|
req.Options.Fields = []*resourcepb.Requirement{{
|
||||||
|
Key: r.Field,
|
||||||
|
Operator: string(r.Operator),
|
||||||
|
Values: []string{r.Value},
|
||||||
|
}}
|
||||||
|
|
||||||
|
// with only one requirement, we do not need to transform the predicate to exclude this pseudo field
|
||||||
|
predicate.Field = fields.Everything()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
requirement := &resourcepb.Requirement{Key: r.Field, Operator: string(r.Operator)}
|
requirement := &resourcepb.Requirement{Key: r.Field, Operator: string(r.Operator)}
|
||||||
if r.Value != "" {
|
if r.Value != "" {
|
||||||
requirement.Values = append(requirement.Values, r.Value)
|
requirement.Values = append(requirement.Values, r.Value)
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ type IndexableDocument struct {
|
|||||||
// metadata, annotations, or external data linked at index time
|
// metadata, annotations, or external data linked at index time
|
||||||
Fields map[string]any `json:"fields,omitempty"`
|
Fields map[string]any `json:"fields,omitempty"`
|
||||||
|
|
||||||
|
// The list of owner references,
|
||||||
|
// each value is of the form {group}/{kind}/{name}
|
||||||
|
// ex: iam.grafana.app/Team/abc-engineering
|
||||||
|
OwnerReferences []string `json:"ownerReferences,omitempty"`
|
||||||
|
|
||||||
// Maintain a list of resource references.
|
// Maintain a list of resource references.
|
||||||
// Someday this will likely be part of https://github.com/grafana/gamma
|
// Someday this will likely be part of https://github.com/grafana/gamma
|
||||||
References ResourceReferences `json:"references,omitempty"`
|
References ResourceReferences `json:"references,omitempty"`
|
||||||
@@ -217,6 +222,10 @@ func NewIndexableDocument(key *resourcepb.ResourceKey, rv int64, obj utils.Grafa
|
|||||||
if err != nil && tt != nil {
|
if err != nil && tt != nil {
|
||||||
doc.Updated = tt.UnixMilli()
|
doc.Updated = tt.UnixMilli()
|
||||||
}
|
}
|
||||||
|
for _, owner := range obj.GetOwnerReferences() {
|
||||||
|
gv, _ := schema.ParseGroupVersion(owner.APIVersion)
|
||||||
|
doc.OwnerReferences = append(doc.OwnerReferences, fmt.Sprintf("%s/%s/%s", gv.Group, owner.Kind, owner.Name))
|
||||||
|
}
|
||||||
return doc.UpdateCopyFields()
|
return doc.UpdateCopyFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +304,7 @@ const SEARCH_FIELD_TITLE_PHRASE = "title_phrase" // filtering/sorting on title b
|
|||||||
const SEARCH_FIELD_DESCRIPTION = "description"
|
const SEARCH_FIELD_DESCRIPTION = "description"
|
||||||
const SEARCH_FIELD_TAGS = "tags"
|
const SEARCH_FIELD_TAGS = "tags"
|
||||||
const SEARCH_FIELD_LABELS = "labels" // All labels, not a specific one
|
const SEARCH_FIELD_LABELS = "labels" // All labels, not a specific one
|
||||||
|
const SEARCH_FIELD_OWNER_REFERENCES = "ownerReferences"
|
||||||
|
|
||||||
const SEARCH_FIELD_FOLDER = "folder"
|
const SEARCH_FIELD_FOLDER = "folder"
|
||||||
const SEARCH_FIELD_CREATED = "created"
|
const SEARCH_FIELD_CREATED = "created"
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ func TestStandardDocumentBuilder(t *testing.T) {
|
|||||||
"id": "something"
|
"id": "something"
|
||||||
},
|
},
|
||||||
"managedBy": "repo:something",
|
"managedBy": "repo:something",
|
||||||
|
"ownerReferences": [
|
||||||
|
"iam.grafana.app/Team/engineering",
|
||||||
|
"iam.grafana.app/User/test"
|
||||||
|
],
|
||||||
"source": {
|
"source": {
|
||||||
"path": "path/in/system.json",
|
"path": "path/in/system.json",
|
||||||
"checksum": "xyz"
|
"checksum": "xyz"
|
||||||
|
|||||||
@@ -16,10 +16,41 @@ func (s *server) tryFieldSelector(ctx context.Context, req *resourcepb.ListReque
|
|||||||
for _, v := range req.Options.Fields {
|
for _, v := range req.Options.Fields {
|
||||||
if v.Key == "metadata.name" && v.Operator == `=` {
|
if v.Key == "metadata.name" && v.Operator == `=` {
|
||||||
names = v.Values
|
names = v.Values
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: support other field selectors
|
// Search by owner reference
|
||||||
|
if v.Key == "search.ownerReference" {
|
||||||
|
if len(req.Options.Fields) > 1 {
|
||||||
|
return &resourcepb.ListResponse{
|
||||||
|
Error: NewBadRequestError("multiple fields found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.Search(ctx, &resourcepb.ResourceSearchRequest{
|
||||||
|
Fields: []string{}, // no extra fields
|
||||||
|
Options: &resourcepb.ListOptions{
|
||||||
|
Key: req.Options.Key,
|
||||||
|
Fields: []*resourcepb.Requirement{{
|
||||||
|
Key: SEARCH_FIELD_OWNER_REFERENCES,
|
||||||
|
Operator: v.Operator,
|
||||||
|
Values: v.Values,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &resourcepb.ListResponse{
|
||||||
|
Error: AsErrorResult(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(results.Results.Rows) < 1 { // nothing found
|
||||||
|
return &resourcepb.ListResponse{
|
||||||
|
ResourceVersion: 1, // TODO, search result should include when it was indexed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, res := range results.Results.Rows {
|
||||||
|
names = append(names, res.Key.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The required names
|
// The required names
|
||||||
@@ -42,9 +73,6 @@ func (s *server) tryFieldSelector(ctx context.Context, req *resourcepb.ListReque
|
|||||||
Value: found.Value,
|
Value: found.Value,
|
||||||
ResourceVersion: found.ResourceVersion,
|
ResourceVersion: found.ResourceVersion,
|
||||||
})
|
})
|
||||||
if found.ResourceVersion > rsp.ResourceVersion {
|
|
||||||
rsp.ResourceVersion = found.ResourceVersion
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rsp
|
return rsp
|
||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
claims "github.com/grafana/authlib/types"
|
||||||
"github.com/grafana/dskit/backoff"
|
"github.com/grafana/dskit/backoff"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/validation"
|
"github.com/grafana/grafana/pkg/apimachinery/validation"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
|||||||
@@ -13,7 +13,16 @@
|
|||||||
"grafana.app/repoPath": "path/in/system.json",
|
"grafana.app/repoPath": "path/in/system.json",
|
||||||
"grafana.app/repoHash": "xyz",
|
"grafana.app/repoHash": "xyz",
|
||||||
"grafana.app/updatedTimestamp": "2024-07-01T10:11:12Z"
|
"grafana.app/updatedTimestamp": "2024-07-01T10:11:12Z"
|
||||||
}
|
},
|
||||||
|
"ownerReferences": [{
|
||||||
|
"apiVersion": "iam.grafana.app/v1alpha1",
|
||||||
|
"kind": "Team",
|
||||||
|
"name": "engineering"
|
||||||
|
}, {
|
||||||
|
"apiVersion": "iam.grafana.app/v1alpha1",
|
||||||
|
"kind": "User",
|
||||||
|
"name": "test"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"title": "Test Playlist from Unified Storage",
|
"title": "Test Playlist from Unified Storage",
|
||||||
|
|||||||
@@ -1559,17 +1559,20 @@ var termFields = []string{
|
|||||||
// Convert a "requirement" into a bleve query
|
// Convert a "requirement" into a bleve query
|
||||||
func requirementQuery(req *resourcepb.Requirement, prefix string) (query.Query, *resourcepb.ErrorResult) {
|
func requirementQuery(req *resourcepb.Requirement, prefix string) (query.Query, *resourcepb.ErrorResult) {
|
||||||
switch selection.Operator(req.Operator) {
|
switch selection.Operator(req.Operator) {
|
||||||
case selection.Equals, selection.DoubleEquals:
|
case selection.Equals:
|
||||||
if len(req.Values) == 0 {
|
if len(req.Values) == 0 {
|
||||||
return query.NewMatchAllQuery(), nil
|
return query.NewMatchAllQuery(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: special case for login and email to use term query only because those fields are using keyword analyzer
|
// FIXME: special case for login and email to use term query only because those fields are using keyword analyzer
|
||||||
// This should be fixed by using the info from the schema
|
// This should be fixed by using the info from the schema
|
||||||
if (req.Key == "login" || req.Key == "email") && len(req.Values) == 1 {
|
if len(req.Values) == 1 {
|
||||||
tq := bleve.NewTermQuery(req.Values[0])
|
switch req.Key {
|
||||||
tq.SetField(prefix + req.Key)
|
case "login", "email", resource.SEARCH_FIELD_OWNER_REFERENCES:
|
||||||
return tq, nil
|
tq := bleve.NewTermQuery(req.Values[0])
|
||||||
|
tq.SetField(prefix + req.Key)
|
||||||
|
return tq, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Values) == 1 {
|
if len(req.Values) == 1 {
|
||||||
@@ -1585,11 +1588,6 @@ func requirementQuery(req *resourcepb.Requirement, prefix string) (query.Query,
|
|||||||
|
|
||||||
return query.NewConjunctionQuery(conjuncts), nil
|
return query.NewConjunctionQuery(conjuncts), nil
|
||||||
|
|
||||||
case selection.NotEquals:
|
|
||||||
case selection.DoesNotExist:
|
|
||||||
case selection.GreaterThan:
|
|
||||||
case selection.LessThan:
|
|
||||||
case selection.Exists:
|
|
||||||
case selection.In:
|
case selection.In:
|
||||||
if len(req.Values) == 0 {
|
if len(req.Values) == 0 {
|
||||||
return query.NewMatchAllQuery(), nil
|
return query.NewMatchAllQuery(), nil
|
||||||
@@ -1622,6 +1620,14 @@ func requirementQuery(req *resourcepb.Requirement, prefix string) (query.Query,
|
|||||||
boolQuery.AddMust(notEmptyQuery)
|
boolQuery.AddMust(notEmptyQuery)
|
||||||
|
|
||||||
return boolQuery, nil
|
return boolQuery, nil
|
||||||
|
|
||||||
|
// will fall through to the BadRequestError
|
||||||
|
case selection.DoubleEquals:
|
||||||
|
case selection.NotEquals:
|
||||||
|
case selection.DoesNotExist:
|
||||||
|
case selection.GreaterThan:
|
||||||
|
case selection.LessThan:
|
||||||
|
case selection.Exists:
|
||||||
}
|
}
|
||||||
return nil, resource.NewBadRequestError(
|
return nil, resource.NewBadRequestError(
|
||||||
fmt.Sprintf("unsupported query operation (%s %s %v)", req.Key, req.Operator, req.Values),
|
fmt.Sprintf("unsupported query operation (%s %s %v)", req.Key, req.Operator, req.Values),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func getBleveDocMappings(fields resource.SearchableDocumentFields) *mapping.Docu
|
|||||||
}
|
}
|
||||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_DESCRIPTION, descriptionMapping)
|
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_DESCRIPTION, descriptionMapping)
|
||||||
|
|
||||||
tagsMapping := &mapping.FieldMapping{
|
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TAGS, &mapping.FieldMapping{
|
||||||
Name: resource.SEARCH_FIELD_TAGS,
|
Name: resource.SEARCH_FIELD_TAGS,
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Analyzer: keyword.Name,
|
Analyzer: keyword.Name,
|
||||||
@@ -69,8 +69,18 @@ func getBleveDocMappings(fields resource.SearchableDocumentFields) *mapping.Docu
|
|||||||
IncludeTermVectors: false,
|
IncludeTermVectors: false,
|
||||||
IncludeInAll: true,
|
IncludeInAll: true,
|
||||||
DocValues: false,
|
DocValues: false,
|
||||||
}
|
})
|
||||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TAGS, tagsMapping)
|
|
||||||
|
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_OWNER_REFERENCES, &mapping.FieldMapping{
|
||||||
|
Name: resource.SEARCH_FIELD_OWNER_REFERENCES,
|
||||||
|
Type: "text",
|
||||||
|
Analyzer: keyword.Name,
|
||||||
|
Store: false,
|
||||||
|
Index: true,
|
||||||
|
IncludeTermVectors: false,
|
||||||
|
IncludeInAll: false,
|
||||||
|
DocValues: false,
|
||||||
|
})
|
||||||
|
|
||||||
folderMapping := &mapping.FieldMapping{
|
folderMapping := &mapping.FieldMapping{
|
||||||
Name: resource.SEARCH_FIELD_FOLDER,
|
Name: resource.SEARCH_FIELD_FOLDER,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func TestDocumentMapping(t *testing.T) {
|
|||||||
Checksum: "ooo",
|
Checksum: "ooo",
|
||||||
TimestampMillis: 1234,
|
TimestampMillis: 1234,
|
||||||
},
|
},
|
||||||
|
OwnerReferences: []string{"iam.grafana.app/Team/devops", "iam.grafana.app/User/xyz"},
|
||||||
}
|
}
|
||||||
data.UpdateCopyFields()
|
data.UpdateCopyFields()
|
||||||
|
|
||||||
@@ -49,5 +50,5 @@ func TestDocumentMapping(t *testing.T) {
|
|||||||
|
|
||||||
fmt.Printf("DOC: fields %d\n", len(doc.Fields))
|
fmt.Printf("DOC: fields %d\n", len(doc.Fields))
|
||||||
fmt.Printf("DOC: size %d\n", doc.Size())
|
fmt.Printf("DOC: size %d\n", doc.Size())
|
||||||
require.Equal(t, 17, len(doc.Fields))
|
require.Equal(t, 19, len(doc.Fields))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,9 @@
|
|||||||
"kind": "repo",
|
"kind": "repo",
|
||||||
"id": "MyGIT"
|
"id": "MyGIT"
|
||||||
},
|
},
|
||||||
"managedBy": "repo:MyGIT"
|
"managedBy": "repo:MyGIT",
|
||||||
|
"ownerReferences": [
|
||||||
|
"iam.grafana.app/Team/engineering",
|
||||||
|
"iam.grafana.app/User/test"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,16 @@
|
|||||||
"annotations": {
|
"annotations": {
|
||||||
"grafana.app/createdBy": "user:1",
|
"grafana.app/createdBy": "user:1",
|
||||||
"grafana.app/repoName": "MyGIT"
|
"grafana.app/repoName": "MyGIT"
|
||||||
}
|
},
|
||||||
|
"ownerReferences": [{
|
||||||
|
"apiVersion": "iam.grafana.app/v1alpha1",
|
||||||
|
"kind": "Team",
|
||||||
|
"name": "engineering"
|
||||||
|
}, {
|
||||||
|
"apiVersion": "iam.grafana.app/v1alpha1",
|
||||||
|
"kind": "User",
|
||||||
|
"name": "test"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"title": "test-aaa"
|
"title": "test-aaa"
|
||||||
|
|||||||
@@ -2183,3 +2183,79 @@ func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T
|
|||||||
require.Equal(t, expectedLabels, accessor.GetLabels())
|
require.Equal(t, expectedLabels, accessor.GetLabels())
|
||||||
require.Equal(t, expectedAnnotations, accessor.GetAnnotations())
|
require.Equal(t, expectedAnnotations, accessor.GetAnnotations())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test finding folders with an owner
|
||||||
|
func TestIntegrationFolderWithOwner(t *testing.T) {
|
||||||
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||||
|
DisableAnonymous: true,
|
||||||
|
AppModeProduction: true,
|
||||||
|
APIServerStorageType: "unified",
|
||||||
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||||
|
folders.RESOURCEGROUP: {
|
||||||
|
DualWriterMode: grafanarest.Mode5,
|
||||||
|
},
|
||||||
|
"dashboards.dashboard.grafana.app": {
|
||||||
|
DualWriterMode: grafanarest.Mode5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EnableFeatureToggles: []string{
|
||||||
|
featuremgmt.FlagUnifiedStorageSearch,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: helper.Org1.Admin,
|
||||||
|
GVR: gvr,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Without owner
|
||||||
|
folder := &unstructured.Unstructured{
|
||||||
|
Object: map[string]any{
|
||||||
|
"spec": map[string]any{
|
||||||
|
"title": "Folder without owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
folder.SetName("folderA")
|
||||||
|
out, err := client.Resource.Create(context.Background(), folder, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, folder.GetName(), out.GetName())
|
||||||
|
|
||||||
|
// with owner
|
||||||
|
folder = &unstructured.Unstructured{
|
||||||
|
Object: map[string]any{
|
||||||
|
"spec": map[string]any{
|
||||||
|
"title": "Folder with owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
folder.SetName("folderB")
|
||||||
|
folder.SetOwnerReferences([]metav1.OwnerReference{{
|
||||||
|
APIVersion: "iam.grafana.app/v0alpha1",
|
||||||
|
Kind: "Team",
|
||||||
|
Name: "engineering",
|
||||||
|
UID: "123456", // required by k8s
|
||||||
|
}})
|
||||||
|
out, err = client.Resource.Create(context.Background(), folder, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, folder.GetName(), out.GetName())
|
||||||
|
|
||||||
|
// Get everything
|
||||||
|
results, err := client.Resource.List(context.Background(), metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []string{"folderA", "folderB"}, getNames(results.Items))
|
||||||
|
|
||||||
|
// Find results with a specific owner
|
||||||
|
results, err = client.Resource.List(context.Background(), metav1.ListOptions{
|
||||||
|
FieldSelector: "search.ownerReference=iam.grafana.app/Team/engineering",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []string{"folderB"}, getNames(results.Items))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNames(items []unstructured.Unstructured) []string {
|
||||||
|
names := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
names = append(names, item.GetName())
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|||||||
@@ -1864,6 +1864,25 @@
|
|||||||
"format": "int64"
|
"format": "int64"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ownerReference",
|
||||||
|
"in": "query",
|
||||||
|
"description": "filter by owner reference in the format {Group}/{Kind}/{Name}",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"": {},
|
||||||
|
"team": {
|
||||||
|
"summary": "Team owner reference",
|
||||||
|
"value": "iam.grafana.app/Team/xyz"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"summary": "User owner reference",
|
||||||
|
"value": "iam.grafana.app/User/abc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "explain",
|
"name": "explain",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
|
|||||||
Reference in New Issue
Block a user