add fs based store
This commit is contained in:
@@ -575,6 +575,9 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWet
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hamba/avro/v2 v2.17.2 h1:6PKpEWzJfNnvBgn7m2/8WYaDOUASxfDU+Jyb4ojDgFY=
|
||||
github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo=
|
||||
|
||||
@@ -13,7 +13,8 @@ type WriteEvent struct {
|
||||
Key *ResourceKey // the request key
|
||||
Requester identity.Requester
|
||||
Operation ResourceOperation
|
||||
PreviousRV int64 // only for Update+Delete
|
||||
PreviousRV int64 // only for Update+Delete
|
||||
Message string // commit message
|
||||
|
||||
// The raw JSON payload
|
||||
// NOTE, this is never mutated, only parsed and validated
|
||||
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
k8s.io/apimachinery v0.29.3
|
||||
github.com/hack-pad/hackpadfs v0.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -12,6 +12,7 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240613114114-5e2f08de316d h1:/UE5JdF+0hxll7EuuO7zRzAxXrvAxQo5M9eqOepc2mQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
|
||||
github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMIxMw=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
context "context"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type MemoryStore interface {
|
||||
Read(context.Context, *ReadRequest) (*ReadResponse, error)
|
||||
WriteEvent(context.Context, *WriteEvent) (int64, error)
|
||||
}
|
||||
|
||||
func NewMemoryStore() MemoryStore {
|
||||
return &memoryStore{
|
||||
store: make(map[string]*namespacedResources),
|
||||
}
|
||||
}
|
||||
|
||||
type memoryStore struct {
|
||||
counter atomic.Int64
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Key is group+resource
|
||||
store map[string]*namespacedResources
|
||||
}
|
||||
|
||||
type namespacedResources struct {
|
||||
// Lookup by resource name
|
||||
namespace map[string]*resourceInfo
|
||||
}
|
||||
|
||||
type resourceInfo struct {
|
||||
history []resourceValue
|
||||
}
|
||||
|
||||
type resourceValue struct {
|
||||
rv int64
|
||||
event WriteEvent // saves the whole thing for now
|
||||
blobHash string
|
||||
}
|
||||
|
||||
func (s *memoryStore) get(key *ResourceKey) *resourceValue {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
found, ok := s.store[key.Group+"/"+key.Resource]
|
||||
if !ok || found.namespace == nil {
|
||||
return nil
|
||||
}
|
||||
resource, ok := found.namespace[key.Namespace]
|
||||
if !ok || len(resource.history) < 1 {
|
||||
return nil
|
||||
}
|
||||
if key.ResourceVersion > 0 {
|
||||
for idx, v := range resource.history {
|
||||
if v.rv == key.ResourceVersion {
|
||||
return &resource.history[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
latest := resource.history[0]
|
||||
if latest.event.Operation == ResourceOperation_DELETED {
|
||||
return nil
|
||||
}
|
||||
return &latest // the first one
|
||||
}
|
||||
|
||||
func (s *memoryStore) Read(_ context.Context, req *ReadRequest) (*ReadResponse, error) {
|
||||
val := s.get(req.Key)
|
||||
if val == nil {
|
||||
return &ReadResponse{
|
||||
Status: &StatusResult{
|
||||
Status: "Failure",
|
||||
Reason: "not found",
|
||||
Code: 404,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
rsp := &ReadResponse{
|
||||
ResourceVersion: val.rv,
|
||||
Value: val.event.Value,
|
||||
}
|
||||
if val.event.Blob != nil {
|
||||
rsp.BlobUrl = "#blob"
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (s *memoryStore) WriteEvent(_ context.Context, req *WriteEvent) (int64, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
val := resourceValue{
|
||||
rv: s.counter.Add(1),
|
||||
event: *req,
|
||||
}
|
||||
if req.Blob != nil {
|
||||
hasher := sha1.New()
|
||||
_, err := hasher.Write(req.Blob.Value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
val.blobHash = base64.URLEncoding.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// Now append the value
|
||||
key := req.Key
|
||||
found, ok := s.store[key.Group+"/"+key.Resource]
|
||||
if !ok {
|
||||
found = &namespacedResources{}
|
||||
s.store[key.Group+"/"+key.Resource] = found
|
||||
}
|
||||
if found.namespace == nil {
|
||||
found.namespace = make(map[string]*resourceInfo)
|
||||
}
|
||||
|
||||
resource, ok := found.namespace[key.Namespace]
|
||||
if !ok {
|
||||
resource = &resourceInfo{}
|
||||
found.namespace[key.Namespace] = resource
|
||||
}
|
||||
if resource.history == nil {
|
||||
resource.history = []resourceValue{val}
|
||||
} else {
|
||||
resource.history = append([]resourceValue{val}, resource.history...)
|
||||
}
|
||||
return val.rv, nil
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hack-pad/hackpadfs"
|
||||
"github.com/hack-pad/hackpadfs/mem"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type FileSystemStoreOptions struct {
|
||||
// OTel tracer
|
||||
Tracer trace.Tracer
|
||||
|
||||
// Get the next EventID. When not set, this will default to snowflake IDs
|
||||
NextEventID func() int64
|
||||
|
||||
// Root file system -- null will be in memory
|
||||
Root hackpadfs.FS
|
||||
}
|
||||
|
||||
func NewFSStore(opts FileSystemStoreOptions) (ResourceStoreServer, error) {
|
||||
if opts.Tracer == nil {
|
||||
opts.Tracer = noop.NewTracerProvider().Tracer("testing")
|
||||
}
|
||||
|
||||
var err error
|
||||
root := opts.Root
|
||||
if root == nil {
|
||||
root, err = mem.NewFS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
store := &fsStore{root: root}
|
||||
store.writer, err = NewResourceWriter(WriterOptions{
|
||||
Tracer: opts.Tracer,
|
||||
Reader: store.Read,
|
||||
Appender: store.append,
|
||||
})
|
||||
|
||||
return store, err
|
||||
}
|
||||
|
||||
var _ ResourceStoreServer = &fsStore{}
|
||||
|
||||
type fsStore struct {
|
||||
writer ResourceWriter
|
||||
|
||||
root hackpadfs.FS
|
||||
}
|
||||
|
||||
type fsEvent struct {
|
||||
ResourceVersion int64 `json:"resourceVersion"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Operation string `json:"operation,omitempty"`
|
||||
Value json.RawMessage `json:"value,omitempty"`
|
||||
BlobPath string `json:"blob,omitempty"`
|
||||
}
|
||||
|
||||
// The only write command
|
||||
func (f *fsStore) append(ctx context.Context, event *WriteEvent) (int64, error) {
|
||||
body := fsEvent{
|
||||
ResourceVersion: event.EventID,
|
||||
Message: event.Message,
|
||||
Operation: event.Operation.String(),
|
||||
Value: event.Value,
|
||||
// Blob...
|
||||
}
|
||||
// For this case, we will treat them the same
|
||||
event.Key.ResourceVersion = 0
|
||||
dir := event.Key.NamespacedPath()
|
||||
err := hackpadfs.MkdirAll(f.root, dir, 0750)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(&body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dir, fmt.Sprintf("%d.json", event.EventID))
|
||||
file, err := hackpadfs.OpenFile(f.root, fpath, hackpadfs.FlagWriteOnly|hackpadfs.FlagCreate, 0750)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = hackpadfs.WriteFile(file, bytes)
|
||||
return event.EventID, err
|
||||
}
|
||||
|
||||
// Read implements ResourceStoreServer.
|
||||
func (f *fsStore) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, error) {
|
||||
rv := req.Key.ResourceVersion
|
||||
req.Key.ResourceVersion = 0
|
||||
|
||||
fname := "--x--"
|
||||
dir := req.Key.NamespacedPath()
|
||||
if rv > 0 {
|
||||
fname = fmt.Sprintf("%d.json", rv)
|
||||
} else {
|
||||
files, err := hackpadfs.ReadDir(f.root, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
a := files[i].Name()
|
||||
b := files[j].Name()
|
||||
return a > b // ?? should we parse the numbers ???
|
||||
})
|
||||
|
||||
// The first matching file
|
||||
for _, v := range files {
|
||||
fname = v.Name()
|
||||
if strings.HasSuffix(fname, ".json") {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evt, err := f.open(filepath.Join(dir, fname))
|
||||
if err != nil || evt.Operation == ResourceOperation_DELETED.String() {
|
||||
return nil, apierrors.NewNotFound(schema.GroupResource{
|
||||
Group: req.Key.Group,
|
||||
Resource: req.Key.Resource,
|
||||
}, req.Key.Name)
|
||||
}
|
||||
|
||||
return &ReadResponse{
|
||||
ResourceVersion: evt.ResourceVersion,
|
||||
Value: evt.Value,
|
||||
Message: evt.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fsStore) open(p string) (*fsEvent, error) {
|
||||
raw, err := hackpadfs.ReadFile(f.root, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evt := &fsEvent{}
|
||||
err = json.Unmarshal(raw, evt)
|
||||
return evt, err
|
||||
}
|
||||
|
||||
func (f *fsStore) Create(ctx context.Context, req *CreateRequest) (*CreateResponse, error) {
|
||||
return f.writer.Create(ctx, req)
|
||||
}
|
||||
|
||||
// Update implements ResourceStoreServer.
|
||||
func (f *fsStore) Update(ctx context.Context, req *UpdateRequest) (*UpdateResponse, error) {
|
||||
return f.writer.Update(ctx, req)
|
||||
}
|
||||
|
||||
// Delete implements ResourceStoreServer.
|
||||
func (f *fsStore) Delete(ctx context.Context, req *DeleteRequest) (*DeleteResponse, error) {
|
||||
return f.writer.Delete(ctx, req)
|
||||
}
|
||||
|
||||
// IsHealthy implements ResourceStoreServer.
|
||||
func (f *fsStore) IsHealthy(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
|
||||
return &HealthCheckResponse{Status: HealthCheckResponse_SERVING}, nil
|
||||
}
|
||||
|
||||
// List implements ResourceStoreServer.
|
||||
func (f *fsStore) List(ctx context.Context, req *ListRequest) (*ListResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Watch implements ResourceStoreServer.
|
||||
func (f *fsStore) Watch(*WatchRequest, ResourceStore_WatchServer) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/bwmarrin/snowflake"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -69,7 +70,8 @@ func NewResourceWriter(opts WriterOptions) (ResourceWriter, error) {
|
||||
if opts.NextEventID == nil {
|
||||
eventNode, err := snowflake.NewNode(opts.NodeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error initializing snowflake id generator :: %w", err)
|
||||
return nil, apierrors.NewInternalError(
|
||||
fmt.Errorf("error initializing snowflake id generator :: %w", err))
|
||||
}
|
||||
opts.NextEventID = func() int64 {
|
||||
return eventNode.Generate().Int64()
|
||||
@@ -112,45 +114,48 @@ func (s *writeServer) newEvent(ctx context.Context, key *ResourceKey, value, old
|
||||
|
||||
obj, err := utils.MetaAccessor(dummy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid object in json")
|
||||
return nil, apierrors.NewBadRequest("invalid object in json")
|
||||
}
|
||||
if obj.GetUID() == "" {
|
||||
return nil, fmt.Errorf("the UID must be set")
|
||||
return nil, apierrors.NewBadRequest("the UID must be set")
|
||||
}
|
||||
if obj.GetGenerateName() != "" {
|
||||
return nil, fmt.Errorf("can not save value with generate name")
|
||||
return nil, apierrors.NewBadRequest("can not save value with generate name")
|
||||
}
|
||||
gvk := obj.GetGroupVersionKind()
|
||||
if gvk.Kind == "" {
|
||||
return nil, fmt.Errorf("expecting resources with a kind in the body")
|
||||
return nil, apierrors.NewBadRequest("expecting resources with a kind in the body")
|
||||
}
|
||||
if gvk.Version == "" {
|
||||
return nil, fmt.Errorf("expecting resources with an apiVersion")
|
||||
return nil, apierrors.NewBadRequest("expecting resources with an apiVersion")
|
||||
}
|
||||
if gvk.Group != "" && gvk.Group != key.Group {
|
||||
return nil, fmt.Errorf("group in key does not match group in the body (%s != %s)", key.Group, gvk.Group)
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("group in key does not match group in the body (%s != %s)", key.Group, gvk.Group),
|
||||
)
|
||||
}
|
||||
if obj.GetName() != key.Name {
|
||||
return nil, fmt.Errorf("key name does not match the name in the body")
|
||||
return nil, apierrors.NewBadRequest("key name does not match the name in the body")
|
||||
}
|
||||
if obj.GetNamespace() != key.Namespace {
|
||||
return nil, fmt.Errorf("key namespace does not match the namespace in the body")
|
||||
return nil, apierrors.NewBadRequest("key namespace does not match the namespace in the body")
|
||||
}
|
||||
folder := obj.GetFolder()
|
||||
if folder != "" {
|
||||
if s.opts.FolderAccess == nil {
|
||||
return nil, fmt.Errorf("folders are not supported")
|
||||
return nil, apierrors.NewBadRequest("folders are not supported")
|
||||
} else if !s.opts.FolderAccess(ctx, event.Requester, folder) {
|
||||
return nil, fmt.Errorf("unable to add resource to folder") // 403?
|
||||
return nil, apierrors.NewBadRequest("unable to add resource to folder") // 403?
|
||||
}
|
||||
}
|
||||
origin, err := obj.GetOriginInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid origin info")
|
||||
return nil, apierrors.NewBadRequest("invalid origin info")
|
||||
}
|
||||
if origin != nil && s.opts.OriginAccess != nil {
|
||||
if !s.opts.OriginAccess(ctx, event.Requester, origin.Name) {
|
||||
return nil, fmt.Errorf("not allowed to write resource to origin (%s)", origin.Name)
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("not allowed to write resource to origin (%s)", origin.Name))
|
||||
}
|
||||
}
|
||||
event.Object = obj
|
||||
@@ -160,22 +165,25 @@ func (s *writeServer) newEvent(ctx context.Context, key *ResourceKey, value, old
|
||||
dummy := &dummyObject{}
|
||||
err = json.Unmarshal(oldValue, dummy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading old json value")
|
||||
return nil, apierrors.NewBadRequest("error reading old json value")
|
||||
}
|
||||
old, err := utils.MetaAccessor(dummy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid object inside old json")
|
||||
return nil, apierrors.NewBadRequest("invalid object inside old json")
|
||||
}
|
||||
if key.Name != old.GetName() {
|
||||
return nil, fmt.Errorf("the old value has a different name (%s != %s)", key.Name, old.GetName())
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("the old value has a different name (%s != %s)", key.Name, old.GetName()))
|
||||
}
|
||||
|
||||
// Can not change creation timestamps+user
|
||||
if obj.GetCreatedBy() != old.GetCreatedBy() {
|
||||
return nil, fmt.Errorf("can not change the created by metadata (%s != %s)", obj.GetCreatedBy(), old.GetCreatedBy())
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("can not change the created by metadata (%s != %s)", obj.GetCreatedBy(), old.GetCreatedBy()))
|
||||
}
|
||||
if obj.GetCreationTimestamp() != old.GetCreationTimestamp() {
|
||||
return nil, fmt.Errorf("can not change the CreationTimestamp metadata (%v != %v)", obj.GetCreationTimestamp(), old.GetCreationTimestamp())
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("can not change the CreationTimestamp metadata (%v != %v)", obj.GetCreationTimestamp(), old.GetCreationTimestamp()))
|
||||
}
|
||||
|
||||
oldFolder := obj.GetFolder()
|
||||
@@ -194,7 +202,7 @@ func (s *writeServer) Create(ctx context.Context, req *CreateRequest) (*CreateRe
|
||||
defer span.End()
|
||||
|
||||
if req.Key.ResourceVersion > 0 {
|
||||
return nil, fmt.Errorf("can not update a specific resource version")
|
||||
return nil, apierrors.NewBadRequest("can not update a specific resource version")
|
||||
}
|
||||
|
||||
event, err := s.newEvent(ctx, req.Key, req.Value, nil)
|
||||
@@ -203,27 +211,28 @@ func (s *writeServer) Create(ctx context.Context, req *CreateRequest) (*CreateRe
|
||||
}
|
||||
event.Operation = ResourceOperation_CREATED
|
||||
event.Blob = req.Blob
|
||||
event.Message = req.Message
|
||||
|
||||
rsp := &CreateResponse{}
|
||||
// Make sure the created by user is accurate
|
||||
//----------------------------------------
|
||||
val := event.Object.GetCreatedBy()
|
||||
if val != "" && val != event.Requester.GetUID().String() {
|
||||
return nil, fmt.Errorf("created by annotation does not match: metadata.annotations#" + utils.AnnoKeyCreatedBy)
|
||||
return nil, apierrors.NewBadRequest("created by annotation does not match: metadata.annotations#" + utils.AnnoKeyCreatedBy)
|
||||
}
|
||||
|
||||
// Create can not have updated properties
|
||||
//----------------------------------------
|
||||
if event.Object.GetUpdatedBy() != "" {
|
||||
return nil, fmt.Errorf("unexpected metadata.annotations#" + utils.AnnoKeyCreatedBy)
|
||||
return nil, apierrors.NewBadRequest("unexpected metadata.annotations#" + utils.AnnoKeyCreatedBy)
|
||||
}
|
||||
|
||||
ts, err := event.Object.GetUpdatedTimestamp()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(fmt.Sprintf("invalid timestamp: %s", err))
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid timestamp: %s", err))
|
||||
}
|
||||
if ts != nil {
|
||||
return nil, fmt.Errorf("unexpected metadata.annotations#" + utils.AnnoKeyUpdatedTimestamp)
|
||||
return nil, apierrors.NewBadRequest("unexpected metadata.annotations#" + utils.AnnoKeyUpdatedTimestamp)
|
||||
}
|
||||
|
||||
// Append and set the resource version
|
||||
@@ -238,7 +247,7 @@ func (s *writeServer) Update(ctx context.Context, req *UpdateRequest) (*UpdateRe
|
||||
|
||||
rsp := &UpdateResponse{}
|
||||
if req.Key.ResourceVersion < 0 {
|
||||
return nil, fmt.Errorf("update must include the previous version")
|
||||
return nil, apierrors.NewBadRequest("update must include the previous version")
|
||||
}
|
||||
|
||||
latest, err := s.opts.Reader(ctx, &ReadRequest{
|
||||
@@ -248,7 +257,7 @@ func (s *writeServer) Update(ctx context.Context, req *UpdateRequest) (*UpdateRe
|
||||
return nil, err
|
||||
}
|
||||
if latest.Value == nil {
|
||||
return nil, fmt.Errorf("current value does not exist")
|
||||
return nil, apierrors.NewBadRequest("current value does not exist")
|
||||
}
|
||||
|
||||
event, err := s.newEvent(ctx, req.Key, req.Value, latest.Value)
|
||||
@@ -257,12 +266,13 @@ func (s *writeServer) Update(ctx context.Context, req *UpdateRequest) (*UpdateRe
|
||||
}
|
||||
event.Operation = ResourceOperation_UPDATED
|
||||
event.PreviousRV = latest.ResourceVersion
|
||||
event.Message = req.Message
|
||||
|
||||
// Make sure the update user is accurate
|
||||
//----------------------------------------
|
||||
val := event.Object.GetUpdatedBy()
|
||||
if val != "" && val != event.Requester.GetUID().String() {
|
||||
return nil, fmt.Errorf("updated by annotation does not match: metadata.annotations#" + utils.AnnoKeyUpdatedBy)
|
||||
return nil, apierrors.NewBadRequest("updated by annotation does not match: metadata.annotations#" + utils.AnnoKeyUpdatedBy)
|
||||
}
|
||||
|
||||
rsp.ResourceVersion, err = s.opts.Appender(ctx, event)
|
||||
@@ -276,7 +286,7 @@ func (s *writeServer) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRe
|
||||
|
||||
rsp := &DeleteResponse{}
|
||||
if req.Key.ResourceVersion < 0 {
|
||||
return nil, fmt.Errorf("update must include the previous version")
|
||||
return nil, apierrors.NewBadRequest("update must include the previous version")
|
||||
}
|
||||
|
||||
latest, err := s.opts.Reader(ctx, &ReadRequest{
|
||||
@@ -286,7 +296,7 @@ func (s *writeServer) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRe
|
||||
return nil, err
|
||||
}
|
||||
if latest.ResourceVersion != req.Key.ResourceVersion {
|
||||
return nil, fmt.Errorf("deletion request does not match current revision (%d != %d)", req.Key.ResourceVersion, latest.ResourceVersion)
|
||||
return nil, ErrOptimisticLockingFailed
|
||||
}
|
||||
|
||||
now := metav1.NewTime(time.Now())
|
||||
@@ -298,12 +308,13 @@ func (s *writeServer) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRe
|
||||
}
|
||||
event.Requester, err = identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user")
|
||||
return nil, apierrors.NewBadRequest("unable to get user")
|
||||
}
|
||||
marker := &DeletedMarker{}
|
||||
err = json.Unmarshal(latest.Value, marker)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read previous object, %w", err)
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("unable to read previous object, %v", err))
|
||||
}
|
||||
event.Object, err = utils.MetaAccessor(marker)
|
||||
if err != nil {
|
||||
@@ -321,7 +332,8 @@ func (s *writeServer) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRe
|
||||
marker.Annotations["RestoreResourceVersion"] = fmt.Sprintf("%d", event.PreviousRV)
|
||||
event.Value, err = json.Marshal(marker)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable creating deletion marker, %w", err)
|
||||
return nil, apierrors.NewBadRequest(
|
||||
fmt.Sprintf("unable creating deletion marker, %v", err))
|
||||
}
|
||||
|
||||
rsp.ResourceVersion, err = s.opts.Appender(ctx, event)
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hack-pad/hackpadfs"
|
||||
hackos "github.com/hack-pad/hackpadfs/os"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
@@ -16,7 +19,6 @@ import (
|
||||
)
|
||||
|
||||
func TestWriter(t *testing.T) {
|
||||
tracer := noop.NewTracerProvider().Tracer("testing")
|
||||
testUserA := &identity.StaticRequester{
|
||||
Namespace: identity.NamespaceUser,
|
||||
UserID: 123,
|
||||
@@ -26,11 +28,17 @@ func TestWriter(t *testing.T) {
|
||||
}
|
||||
ctx := identity.WithRequester(context.Background(), testUserA)
|
||||
|
||||
store := NewMemoryStore()
|
||||
writer, err := NewResourceWriter(WriterOptions{
|
||||
Tracer: tracer,
|
||||
Reader: store.Read,
|
||||
Appender: store.WriteEvent,
|
||||
var root hackpadfs.FS
|
||||
if false {
|
||||
tmp, err := os.MkdirTemp("", "xxx-*")
|
||||
require.NoError(t, err)
|
||||
|
||||
root, err = hackos.NewFS().Sub(tmp[1:])
|
||||
require.NoError(t, err)
|
||||
fmt.Printf("ROOT: %s\n\n", tmp)
|
||||
}
|
||||
store, err := NewFSStore(FileSystemStoreOptions{
|
||||
Root: root,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -42,7 +50,7 @@ func TestWriter(t *testing.T) {
|
||||
Namespace: "default",
|
||||
Name: "fdgsv37qslr0ga",
|
||||
}
|
||||
created, err := writer.Create(ctx, &CreateRequest{
|
||||
created, err := store.Create(ctx, &CreateRequest{
|
||||
Value: raw,
|
||||
Key: key,
|
||||
})
|
||||
@@ -69,7 +77,7 @@ func TestWriter(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
key.ResourceVersion = created.ResourceVersion
|
||||
updated, err := writer.Update(ctx, &UpdateRequest{Key: key, Value: raw})
|
||||
updated, err := store.Update(ctx, &UpdateRequest{Key: key, Value: raw})
|
||||
require.NoError(t, err)
|
||||
require.True(t, updated.ResourceVersion > created.ResourceVersion)
|
||||
|
||||
@@ -80,15 +88,15 @@ func TestWriter(t *testing.T) {
|
||||
require.Equal(t, updated.ResourceVersion, found.ResourceVersion)
|
||||
|
||||
key.ResourceVersion = updated.ResourceVersion
|
||||
deleted, err := writer.Delete(ctx, &DeleteRequest{Key: key})
|
||||
deleted, err := store.Delete(ctx, &DeleteRequest{Key: key})
|
||||
require.NoError(t, err)
|
||||
require.True(t, deleted.ResourceVersion > updated.ResourceVersion)
|
||||
|
||||
// We should get not found when trying to read the latest value
|
||||
key.ResourceVersion = 0
|
||||
found, _ = store.Read(ctx, &ReadRequest{Key: key})
|
||||
require.Equal(t, int32(404), found.Status.Code)
|
||||
require.Nil(t, found.Value)
|
||||
found, err = store.Read(ctx, &ReadRequest{Key: key})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user