Files
Georges Chaudy 7002ab90ae unistore: save returns a writecloser (#107955)
* unistore: save returns a writecloser

* go-lint

* address comments
2025-07-11 18:25:48 +02:00

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
}