mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 00:36:23 +08:00
294 lines
7.7 KiB
Go
294 lines
7.7 KiB
Go
package resource
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"iter"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
dataSection = "unified/data"
|
|
)
|
|
|
|
// dataStore is a data store that uses a KV store to store data.
|
|
type dataStore struct {
|
|
kv KV
|
|
}
|
|
|
|
func newDataStore(kv KV) *dataStore {
|
|
return &dataStore{
|
|
kv: kv,
|
|
}
|
|
}
|
|
|
|
type DataObj struct {
|
|
Key DataKey
|
|
Value io.ReadCloser
|
|
}
|
|
|
|
type DataKey struct {
|
|
Namespace string
|
|
Group string
|
|
Resource string
|
|
Name string
|
|
ResourceVersion int64
|
|
Action DataAction
|
|
}
|
|
|
|
var (
|
|
// validNameRegex validates that a name contains only lowercase alphanumeric characters, '-' or '.'
|
|
// and starts and ends with an alphanumeric character
|
|
validNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$`)
|
|
)
|
|
|
|
func (k DataKey) String() string {
|
|
return fmt.Sprintf("%s/%s/%s/%s/%d~%s", k.Namespace, k.Group, k.Resource, k.Name, k.ResourceVersion, k.Action)
|
|
}
|
|
|
|
func (k DataKey) Equals(other DataKey) bool {
|
|
return k.Namespace == other.Namespace && k.Group == other.Group && k.Resource == other.Resource && k.Name == other.Name && k.ResourceVersion == other.ResourceVersion && k.Action == other.Action
|
|
}
|
|
|
|
func (k DataKey) Validate() error {
|
|
if k.Namespace == "" {
|
|
if k.Group != "" || k.Resource != "" || k.Name != "" {
|
|
return fmt.Errorf("namespace is required when group, resource, or name are provided")
|
|
}
|
|
return fmt.Errorf("namespace cannot be empty")
|
|
}
|
|
if k.Group == "" {
|
|
if k.Resource != "" || k.Name != "" {
|
|
return fmt.Errorf("group is required when resource or name are provided")
|
|
}
|
|
return fmt.Errorf("group cannot be empty")
|
|
}
|
|
if k.Resource == "" {
|
|
if k.Name != "" {
|
|
return fmt.Errorf("resource is required when name is provided")
|
|
}
|
|
return fmt.Errorf("resource cannot be empty")
|
|
}
|
|
if k.Name == "" {
|
|
return fmt.Errorf("name cannot be empty")
|
|
}
|
|
if k.Action == "" {
|
|
return fmt.Errorf("action cannot be empty")
|
|
}
|
|
|
|
// Validate each field against the naming rules
|
|
if !validNameRegex.MatchString(k.Namespace) {
|
|
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
|
|
}
|
|
if !validNameRegex.MatchString(k.Group) {
|
|
return fmt.Errorf("group '%s' is invalid", k.Group)
|
|
}
|
|
if !validNameRegex.MatchString(k.Resource) {
|
|
return fmt.Errorf("resource '%s' is invalid", k.Resource)
|
|
}
|
|
if !validNameRegex.MatchString(k.Name) {
|
|
return fmt.Errorf("name '%s' is invalid", k.Name)
|
|
}
|
|
|
|
switch k.Action {
|
|
case DataActionCreated, DataActionUpdated, DataActionDeleted:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("action '%s' is invalid: must be one of 'created', 'updated', or 'deleted'", k.Action)
|
|
}
|
|
}
|
|
|
|
type ListRequestKey struct {
|
|
Namespace string
|
|
Group string
|
|
Resource string
|
|
Name string
|
|
}
|
|
|
|
func (k ListRequestKey) Validate() error {
|
|
// Check hierarchical validation - if a field is empty, more specific fields should also be empty
|
|
if k.Namespace == "" {
|
|
if k.Group != "" || k.Resource != "" || k.Name != "" {
|
|
return fmt.Errorf("namespace is required when group, resource, or name are provided")
|
|
}
|
|
return nil // Empty namespace is allowed for ListRequestKey
|
|
}
|
|
if k.Group == "" {
|
|
if k.Resource != "" || k.Name != "" {
|
|
return fmt.Errorf("group is required when resource or name are provided")
|
|
}
|
|
// Only validate namespace if it's provided
|
|
if !validNameRegex.MatchString(k.Namespace) {
|
|
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
|
|
}
|
|
return nil
|
|
}
|
|
if k.Resource == "" {
|
|
if k.Name != "" {
|
|
return fmt.Errorf("resource is required when name is provided")
|
|
}
|
|
// Validate namespace and group if they're provided
|
|
if !validNameRegex.MatchString(k.Namespace) {
|
|
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
|
|
}
|
|
if !validNameRegex.MatchString(k.Group) {
|
|
return fmt.Errorf("group '%s' is invalid", k.Group)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// All fields are provided, validate each one
|
|
if !validNameRegex.MatchString(k.Namespace) {
|
|
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
|
|
}
|
|
if !validNameRegex.MatchString(k.Group) {
|
|
return fmt.Errorf("group '%s' is invalid", k.Group)
|
|
}
|
|
if !validNameRegex.MatchString(k.Resource) {
|
|
return fmt.Errorf("resource '%s' is invalid", k.Resource)
|
|
}
|
|
if k.Name != "" && !validNameRegex.MatchString(k.Name) {
|
|
return fmt.Errorf("name '%s' is invalid", k.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (k ListRequestKey) Prefix() string {
|
|
if k.Namespace == "" {
|
|
return ""
|
|
}
|
|
if k.Group == "" {
|
|
return fmt.Sprintf("%s/", k.Namespace)
|
|
}
|
|
if k.Resource == "" {
|
|
return fmt.Sprintf("%s/%s/", k.Namespace, k.Group)
|
|
}
|
|
if k.Name == "" {
|
|
return fmt.Sprintf("%s/%s/%s/", k.Namespace, k.Group, k.Resource)
|
|
}
|
|
return fmt.Sprintf("%s/%s/%s/%s/", k.Namespace, k.Group, k.Resource, k.Name)
|
|
}
|
|
|
|
type DataAction string
|
|
|
|
const (
|
|
DataActionCreated DataAction = "created"
|
|
DataActionUpdated DataAction = "updated"
|
|
DataActionDeleted DataAction = "deleted"
|
|
)
|
|
|
|
// Keys returns all keys for a given key by iterating through the KV store
|
|
func (d *dataStore) Keys(ctx context.Context, key ListRequestKey) iter.Seq2[DataKey, error] {
|
|
if err := key.Validate(); err != nil {
|
|
return func(yield func(DataKey, error) bool) {
|
|
yield(DataKey{}, err)
|
|
}
|
|
}
|
|
|
|
prefix := key.Prefix()
|
|
return func(yield func(DataKey, error) bool) {
|
|
for k, err := range d.kv.Keys(ctx, dataSection, ListOptions{
|
|
StartKey: prefix,
|
|
EndKey: PrefixRangeEnd(prefix),
|
|
}) {
|
|
if err != nil {
|
|
yield(DataKey{}, err)
|
|
return
|
|
}
|
|
key, err := ParseKey(k)
|
|
if err != nil {
|
|
yield(DataKey{}, err)
|
|
return
|
|
}
|
|
if !yield(key, nil) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// LastResourceVersion returns the last key for a given resource
|
|
func (d *dataStore) LastResourceVersion(ctx context.Context, key ListRequestKey) (DataKey, error) {
|
|
if err := key.Validate(); err != nil {
|
|
return DataKey{}, fmt.Errorf("invalid data key: %w", err)
|
|
}
|
|
if key.Group == "" || key.Resource == "" || key.Namespace == "" || key.Name == "" {
|
|
return DataKey{}, fmt.Errorf("group, resource, namespace or name is empty")
|
|
}
|
|
prefix := key.Prefix()
|
|
for key, err := range d.kv.Keys(ctx, dataSection, ListOptions{
|
|
StartKey: prefix,
|
|
EndKey: PrefixRangeEnd(prefix),
|
|
Limit: 1,
|
|
Sort: SortOrderDesc,
|
|
}) {
|
|
if err != nil {
|
|
return DataKey{}, err
|
|
}
|
|
return ParseKey(key)
|
|
}
|
|
return DataKey{}, ErrNotFound
|
|
}
|
|
|
|
func (d *dataStore) Get(ctx context.Context, key DataKey) (io.ReadCloser, error) {
|
|
if err := key.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid data key: %w", err)
|
|
}
|
|
|
|
return d.kv.Get(ctx, dataSection, key.String())
|
|
}
|
|
|
|
func (d *dataStore) Save(ctx context.Context, key DataKey, value io.Reader) error {
|
|
if err := key.Validate(); err != nil {
|
|
return fmt.Errorf("invalid data key: %w", err)
|
|
}
|
|
|
|
writer, err := d.kv.Save(ctx, dataSection, key.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(writer, value)
|
|
if err != nil {
|
|
_ = writer.Close()
|
|
return err
|
|
}
|
|
|
|
return writer.Close()
|
|
}
|
|
|
|
func (d *dataStore) Delete(ctx context.Context, key DataKey) error {
|
|
if err := key.Validate(); err != nil {
|
|
return fmt.Errorf("invalid data key: %w", err)
|
|
}
|
|
|
|
return d.kv.Delete(ctx, dataSection, key.String())
|
|
}
|
|
|
|
// ParseKey parses a string key into a DataKey struct
|
|
func ParseKey(key string) (DataKey, error) {
|
|
parts := strings.Split(key, "/")
|
|
if len(parts) != 5 {
|
|
return DataKey{}, fmt.Errorf("invalid key: %s", key)
|
|
}
|
|
uidActionParts := strings.Split(parts[4], "~")
|
|
if len(uidActionParts) != 2 {
|
|
return DataKey{}, fmt.Errorf("invalid key: %s", key)
|
|
}
|
|
rv, err := strconv.ParseInt(uidActionParts[0], 10, 64)
|
|
if err != nil {
|
|
return DataKey{}, fmt.Errorf("invalid resource version: %s", uidActionParts[0])
|
|
}
|
|
return DataKey{
|
|
Namespace: parts[0],
|
|
Group: parts[1],
|
|
Resource: parts[2],
|
|
Name: parts[3],
|
|
ResourceVersion: rv,
|
|
Action: DataAction(uidActionParts[1]),
|
|
}, nil
|
|
}
|