mirror of https://github.com/grafana/grafana
parent
15b958b2d1
commit
382d5d4e01
@ -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") |
||||
} |
Loading…
Reference in new issue