The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/storage/unified/resource/document.go

369 lines
11 KiB

package resource
import (
"context"
"fmt"
"strings"
"sync"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
// Convert raw resource bytes into an IndexableDocument
type DocumentBuilder interface {
// Convert raw bytes into an document that can be written
BuildDocument(ctx context.Context, key *ResourceKey, rv int64, value []byte) (*IndexableDocument, error)
}
// Registry of the searchable document fields
type SearchableDocumentFields interface {
Fields() []string
Field(name string) *ResourceTableColumnDefinition
}
// Some kinds will require special processing for their namespace
type NamespacedDocumentSupplier = func(ctx context.Context, namespace string, blob BlobSupport) (DocumentBuilder, error)
// Register how documents can be built for a resource
type DocumentBuilderInfo struct {
// The target resource (empty will be used to match anything)
GroupResource schema.GroupResource
// Defines the searchable fields
// NOTE: this does not include the root/common fields, only values specific to the the builder
Fields SearchableDocumentFields
// simple/static builders that do not depend on the environment can be declared once
Builder DocumentBuilder
// Complicated builders (eg dashboards!) will be declared dynamically and managed by the ResourceServer
Namespaced NamespacedDocumentSupplier
}
type DocumentBuilderSupplier interface {
GetDocumentBuilders() ([]DocumentBuilderInfo, error)
}
// IndexableDocument can be written to a ResourceIndex
// Although public, this is *NOT* an end user interface
type IndexableDocument struct {
// The resource key
Key *ResourceKey `json:"key"`
// The resource type ( for federated indexes )
Kind string `json:"kind,omitempty"`
// Resource version for the resource (if known)
RV int64 `json:"rv,omitempty"`
// The generic display name
Title string `json:"title,omitempty"`
// internal sort field for title ( don't set this directly )
TitleSort string `json:"title_sort,omitempty"`
// A generic description -- helpful in global search
Description string `json:"description,omitempty"`
// Like dashboard tags
Tags []string `json:"tags,omitempty"`
// Generic metadata labels
Labels map[string]string `json:"labels,omitempty"`
// The folder (K8s name)
Folder string `json:"folder,omitempty"`
// The first time this resource was saved
Created int64 `json:"created,omitempty"`
// Who created the resource (will be in the form `user:uid`)
CreatedBy string `json:"createdBy,omitempty"`
// The last time a user updated the spec
Updated int64 `json:"updated,omitempty"`
// Who updated the resource (will be in the form `user:uid`)
UpdatedBy string `json:"updatedBy,omitempty"`
// Searchable nested keys
// The key should exist from the fields defined in DocumentBuilderInfo
// This should not contain duplicate information from the results above
// The meaning of these fields changes depending on the field type
// These values typically come from the Spec, but may also come from status
// metadata, annotations, or external data linked at index time
Fields map[string]any `json:"fields,omitempty"`
// Maintain a list of resource references.
// Someday this will likely be part of https://github.com/grafana/gamma
References ResourceReferences `json:"reference,omitempty"`
// When the resource is managed by an upstream repository
RepoInfo *utils.ResourceRepositoryInfo `json:"repository,omitempty"`
}
func (m *IndexableDocument) Type() string {
return m.Key.Resource
}
type ResourceReference struct {
Relation string `json:"relation"` // eg: depends-on
Group string `json:"group,omitempty"` // the api group
Version string `json:"version,omitempty"` // the api version
Kind string `json:"kind,omitempty"` // panel, data source (for now)
Name string `json:"name"` // the UID / panel name
}
func (m ResourceReference) String() string {
var sb strings.Builder
sb.WriteString(m.Relation)
sb.WriteString(">>")
sb.WriteString(m.Group)
if m.Version != "" {
sb.WriteString("/")
sb.WriteString(m.Version)
}
if m.Kind != "" {
sb.WriteString("/")
sb.WriteString(m.Kind)
}
sb.WriteString("/")
sb.WriteString(m.Name)
return sb.String()
}
// Sortable list of references
type ResourceReferences []ResourceReference
func (m ResourceReferences) Len() int { return len(m) }
func (m ResourceReferences) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m ResourceReferences) Less(i, j int) bool {
a := m[i].String()
b := m[j].String()
return strings.Compare(a, b) > 0
}
// Create a new indexable document based on a generic k8s resource
func NewIndexableDocument(key *ResourceKey, rv int64, obj utils.GrafanaMetaAccessor) *IndexableDocument {
title := obj.FindTitle(key.Name)
if title == key.Name {
// TODO: something wrong with FindTitle
spec, err := obj.GetSpec()
if err == nil {
specValue, ok := spec.(map[string]any)
if ok {
specTitle, ok := specValue["title"].(string)
if ok {
title = specTitle
}
}
}
}
doc := &IndexableDocument{
Key: key,
Kind: key.Resource,
RV: rv,
Title: title, // We always want *something* to display
TitleSort: strings.ToLower(title), // Lowercase for case-insensitive sorting
Labels: obj.GetLabels(),
Folder: obj.GetFolder(),
CreatedBy: obj.GetCreatedBy(),
UpdatedBy: obj.GetUpdatedBy(),
}
doc.RepoInfo, _ = obj.GetRepositoryInfo()
ts := obj.GetCreationTimestamp()
if !ts.Time.IsZero() {
doc.Created = ts.Time.UnixMilli()
}
tt, err := obj.GetUpdatedTimestamp()
if err != nil && tt != nil {
doc.Updated = tt.UnixMilli()
}
return doc
}
func StandardDocumentBuilder() DocumentBuilder {
return &standardDocumentBuilder{}
}
type standardDocumentBuilder struct{}
func (s *standardDocumentBuilder) BuildDocument(ctx context.Context, key *ResourceKey, rv int64, value []byte) (*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 := NewIndexableDocument(key, rv, obj)
return doc, nil
}
type searchableDocumentFields struct {
names []string
fields map[string]*resourceTableColumn
}
// This requires unique names
func NewSearchableDocumentFields(columns []*ResourceTableColumnDefinition) (SearchableDocumentFields, error) {
f := &searchableDocumentFields{
names: make([]string, len(columns)),
fields: make(map[string]*resourceTableColumn),
}
for i, c := range columns {
if f.fields[c.Name] != nil {
return nil, fmt.Errorf("duplicate name")
}
col, err := newResourceTableColumn(c, i)
if err != nil {
return nil, err
}
f.names[i] = c.Name
f.fields[c.Name] = col
}
return f, nil
}
func (x *searchableDocumentFields) Fields() []string {
return x.names
}
func (x *searchableDocumentFields) Field(name string) *ResourceTableColumnDefinition {
f, ok := x.fields[name]
if ok {
return f.def
}
return nil
}
const SEARCH_FIELD_ID = "_id" // {namespace}/{group}/{resource}/{name}
const SEARCH_FIELD_KIND = "kind" // resource ( for federated index filtering )
const SEARCH_FIELD_GROUP_RESOURCE = "gr" // group/resource
const SEARCH_FIELD_NAMESPACE = "namespace"
const SEARCH_FIELD_NAME = "name"
const SEARCH_FIELD_RV = "rv"
const SEARCH_FIELD_TITLE = "title"
const SEARCH_FIELD_TITLE_SORT = "title_sort"
const SEARCH_FIELD_DESCRIPTION = "description"
const SEARCH_FIELD_TAGS = "tags"
const SEARCH_FIELD_LABELS = "labels" // All labels, not a specific one
const SEARCH_FIELD_FOLDER = "folder"
const SEARCH_FIELD_CREATED = "created"
const SEARCH_FIELD_CREATED_BY = "createdBy"
const SEARCH_FIELD_UPDATED = "updated"
const SEARCH_FIELD_UPDATED_BY = "updatedBy"
const SEARCH_FIELD_REPOSITORY = "repository"
const SEARCH_FIELD_REPOSITORY_HASH = "repository_hash"
const SEARCH_FIELD_SCORE = "_score" // the match score
const SEARCH_FIELD_EXPLAIN = "_explain" // score explanation as JSON object
var standardSearchFieldsInit sync.Once
var standardSearchFields SearchableDocumentFields
func StandardSearchFields() SearchableDocumentFields {
standardSearchFieldsInit.Do(func() {
var err error
standardSearchFields, err = NewSearchableDocumentFields([]*ResourceTableColumnDefinition{
{
Name: SEARCH_FIELD_ID,
Type: ResourceTableColumnDefinition_STRING,
Description: "Unique Identifier. {namespace}/{group}/{resource}/{name}",
Properties: &ResourceTableColumnDefinition_Properties{
NotNull: true,
},
},
{
Name: SEARCH_FIELD_GROUP_RESOURCE,
Type: ResourceTableColumnDefinition_STRING,
Description: "The resource kind: {group}/{resource}",
Properties: &ResourceTableColumnDefinition_Properties{
NotNull: true,
},
},
{
Name: SEARCH_FIELD_NAMESPACE,
Type: ResourceTableColumnDefinition_STRING,
Description: "Tenant isolation",
Properties: &ResourceTableColumnDefinition_Properties{
NotNull: true,
},
},
{
Name: SEARCH_FIELD_NAME,
Type: ResourceTableColumnDefinition_STRING,
Description: "Kubernetes name. Unique identifier within a namespace+group+resource",
Properties: &ResourceTableColumnDefinition_Properties{
NotNull: true,
},
},
{
Name: SEARCH_FIELD_TITLE,
Type: ResourceTableColumnDefinition_STRING,
Description: "Display name for the resource",
},
{
Name: SEARCH_FIELD_DESCRIPTION,
Type: ResourceTableColumnDefinition_STRING,
Description: "An account of the resource.",
Properties: &ResourceTableColumnDefinition_Properties{
FreeText: true,
},
},
{
Name: SEARCH_FIELD_TAGS,
Type: ResourceTableColumnDefinition_STRING,
IsArray: true,
Description: "Unique tags",
Properties: &ResourceTableColumnDefinition_Properties{
Filterable: true,
},
},
{
Name: SEARCH_FIELD_FOLDER,
Type: ResourceTableColumnDefinition_STRING,
Description: "Kubernetes name for the folder",
},
{
Name: SEARCH_FIELD_RV,
Type: ResourceTableColumnDefinition_INT64,
Description: "resource version",
},
{
Name: SEARCH_FIELD_CREATED,
Type: ResourceTableColumnDefinition_INT64,
Description: "created timestamp", // date?
},
})
if err != nil {
panic("failed to initialize standard search fields")
}
})
return standardSearchFields
}
// // Helper function to convert everything except the "Fields" property to values
// // NOTE: this is really to help testing things absent real backend index
// func IndexableDocumentStandardFields(doc *IndexableDocument) map[string]any {
// fields := make(map[string]any)
// // These should always exist
// fields[SEARCH_FIELD_ID] = doc.Key.SearchID()
// fields[SEARCH_FIELD_NAMESPACE] = doc.Key.Namespace
// fields[SEARCH_FIELD_NAME] = doc.Key.Name
// fields[SEARCH_FIELD_GROUP_RESOURCE] = fmt.Sprintf("%s/%s", doc.Key.Group, doc.Key.Resource)
// fields[SEARCH_FIELD_TITLE] = doc.Title
// return fields
// }