Add support for label storage in local content store
Allows running tests which require labels on the content store Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
parent
f2ae8a020a
commit
7b08bcdb65
@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,6 +28,19 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LabelStore is used to store mutable labels for digests
|
||||||
|
type LabelStore interface {
|
||||||
|
// Get returns all the labels for the given digest
|
||||||
|
Get(digest.Digest) (map[string]string, error)
|
||||||
|
|
||||||
|
// Set sets all the labels for a given digest
|
||||||
|
Set(digest.Digest, map[string]string) error
|
||||||
|
|
||||||
|
// Update replaces the given labels for a digest,
|
||||||
|
// a key with an empty value removes a label.
|
||||||
|
Update(digest.Digest, map[string]string) (map[string]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Store is digest-keyed store for content. All data written into the store is
|
// Store is digest-keyed store for content. All data written into the store is
|
||||||
// stored under a verifiable digest.
|
// stored under a verifiable digest.
|
||||||
//
|
//
|
||||||
@ -34,16 +48,27 @@ var (
|
|||||||
// including resumable ingest.
|
// including resumable ingest.
|
||||||
type store struct {
|
type store struct {
|
||||||
root string
|
root string
|
||||||
|
ls LabelStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore returns a local content store
|
// NewStore returns a local content store
|
||||||
func NewStore(root string) (content.Store, error) {
|
func NewStore(root string) (content.Store, error) {
|
||||||
|
return NewLabeledStore(root, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLabeledStore returns a new content store using the provided label store
|
||||||
|
//
|
||||||
|
// Note: content stores which are used underneath a metadata store may not
|
||||||
|
// require labels and should use `NewStore`. `NewLabeledStore` is primarily
|
||||||
|
// useful for tests or standalone implementations.
|
||||||
|
func NewLabeledStore(root string, ls LabelStore) (content.Store, error) {
|
||||||
if err := os.MkdirAll(filepath.Join(root, "ingest"), 0777); err != nil && !os.IsExist(err) {
|
if err := os.MkdirAll(filepath.Join(root, "ingest"), 0777); err != nil && !os.IsExist(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &store{
|
return &store{
|
||||||
root: root,
|
root: root,
|
||||||
|
ls: ls,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,16 +82,23 @@ func (s *store) Info(ctx context.Context, dgst digest.Digest) (content.Info, err
|
|||||||
|
|
||||||
return content.Info{}, err
|
return content.Info{}, err
|
||||||
}
|
}
|
||||||
|
var labels map[string]string
|
||||||
return s.info(dgst, fi), nil
|
if s.ls != nil {
|
||||||
|
labels, err = s.ls.Get(dgst)
|
||||||
|
if err != nil {
|
||||||
|
return content.Info{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.info(dgst, fi, labels), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) info(dgst digest.Digest, fi os.FileInfo) content.Info {
|
func (s *store) info(dgst digest.Digest, fi os.FileInfo, labels map[string]string) content.Info {
|
||||||
return content.Info{
|
return content.Info{
|
||||||
Digest: dgst,
|
Digest: dgst,
|
||||||
Size: fi.Size(),
|
Size: fi.Size(),
|
||||||
CreatedAt: fi.ModTime(),
|
CreatedAt: fi.ModTime(),
|
||||||
UpdatedAt: fi.ModTime(),
|
UpdatedAt: getATime(fi),
|
||||||
|
Labels: labels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,10 +143,68 @@ func (s *store) Delete(ctx context.Context, dgst digest.Digest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
|
func (s *store) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
|
||||||
// TODO: Support persisting and updating mutable content data
|
if s.ls == nil {
|
||||||
return content.Info{}, errors.Wrapf(errdefs.ErrFailedPrecondition, "update not supported on immutable content store")
|
return content.Info{}, errors.Wrapf(errdefs.ErrFailedPrecondition, "update not supported on immutable content store")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p := s.blobPath(info.Digest)
|
||||||
|
fi, err := os.Stat(p)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = errors.Wrapf(errdefs.ErrNotFound, "content %v", info.Digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
all bool
|
||||||
|
labels map[string]string
|
||||||
|
)
|
||||||
|
if len(fieldpaths) > 0 {
|
||||||
|
for _, path := range fieldpaths {
|
||||||
|
if strings.HasPrefix(path, "labels.") {
|
||||||
|
if labels == nil {
|
||||||
|
labels = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimPrefix(path, "labels.")
|
||||||
|
labels[key] = info.Labels[key]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch path {
|
||||||
|
case "labels":
|
||||||
|
all = true
|
||||||
|
labels = info.Labels
|
||||||
|
default:
|
||||||
|
return content.Info{}, errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on content info %q", path, info.Digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
all = true
|
||||||
|
labels = info.Labels
|
||||||
|
}
|
||||||
|
|
||||||
|
if all {
|
||||||
|
err = s.ls.Set(info.Digest, labels)
|
||||||
|
} else {
|
||||||
|
labels, err = s.ls.Update(info.Digest, labels)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return content.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info = s.info(info.Digest, fi, labels)
|
||||||
|
info.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := os.Chtimes(p, info.UpdatedAt, info.CreatedAt); err != nil {
|
||||||
|
log.G(ctx).WithError(err).Warnf("could not change access time for %s", info.Digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *store) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
|
func (s *store) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
|
||||||
// TODO: Support filters
|
// TODO: Support filters
|
||||||
root := filepath.Join(s.root, "blobs")
|
root := filepath.Join(s.root, "blobs")
|
||||||
@ -154,7 +244,14 @@ func (s *store) Walk(ctx context.Context, fn content.WalkFunc, filters ...string
|
|||||||
// store or extra paths not expected previously.
|
// store or extra paths not expected previously.
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(s.info(dgst, fi))
|
var labels map[string]string
|
||||||
|
if s.ls != nil {
|
||||||
|
labels, err = s.ls.Get(dgst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fn(s.info(dgst, fi, labels))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -23,9 +24,55 @@ import (
|
|||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type memoryLabelStore struct {
|
||||||
|
l sync.Mutex
|
||||||
|
labels map[digest.Digest]map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemoryLabelStore() LabelStore {
|
||||||
|
return &memoryLabelStore{
|
||||||
|
labels: map[digest.Digest]map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mls *memoryLabelStore) Get(d digest.Digest) (map[string]string, error) {
|
||||||
|
mls.l.Lock()
|
||||||
|
labels := mls.labels[d]
|
||||||
|
mls.l.Unlock()
|
||||||
|
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mls *memoryLabelStore) Set(d digest.Digest, labels map[string]string) error {
|
||||||
|
mls.l.Lock()
|
||||||
|
mls.labels[d] = labels
|
||||||
|
mls.l.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mls *memoryLabelStore) Update(d digest.Digest, update map[string]string) (map[string]string, error) {
|
||||||
|
mls.l.Lock()
|
||||||
|
labels, ok := mls.labels[d]
|
||||||
|
if !ok {
|
||||||
|
labels = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range update {
|
||||||
|
if v == "" {
|
||||||
|
delete(labels, k)
|
||||||
|
} else {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mls.labels[d] = labels
|
||||||
|
mls.l.Unlock()
|
||||||
|
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestContent(t *testing.T) {
|
func TestContent(t *testing.T) {
|
||||||
testsuite.ContentSuite(t, "fs", func(ctx context.Context, root string) (content.Store, func() error, error) {
|
testsuite.ContentSuite(t, "fs", func(ctx context.Context, root string) (content.Store, func() error, error) {
|
||||||
cs, err := NewStore(root)
|
cs, err := NewLabeledStore(root, newMemoryLabelStore())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -18,3 +18,12 @@ func getStartTime(fi os.FileInfo) time.Time {
|
|||||||
|
|
||||||
return fi.ModTime()
|
return fi.ModTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getATime(fi os.FileInfo) time.Time {
|
||||||
|
if st, ok := fi.Sys().(*syscall.Stat_t); ok {
|
||||||
|
return time.Unix(int64(sys.StatAtime(st).Sec),
|
||||||
|
int64(sys.StatAtime(st).Nsec))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fi.ModTime()
|
||||||
|
}
|
||||||
|
@ -8,3 +8,7 @@ import (
|
|||||||
func getStartTime(fi os.FileInfo) time.Time {
|
func getStartTime(fi os.FileInfo) time.Time {
|
||||||
return fi.ModTime()
|
return fi.ModTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getATime(fi os.FileInfo) time.Time {
|
||||||
|
return fi.ModTime()
|
||||||
|
}
|
||||||
|
@ -56,6 +56,13 @@ func (w *writer) Write(p []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
|
func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
|
||||||
|
var base content.Info
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err := opt(&base); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if w.fp == nil {
|
if w.fp == nil {
|
||||||
return errors.Wrap(errdefs.ErrFailedPrecondition, "cannot commit on closed writer")
|
return errors.Wrap(errdefs.ErrFailedPrecondition, "cannot commit on closed writer")
|
||||||
}
|
}
|
||||||
@ -123,6 +130,12 @@ func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest,
|
|||||||
w.fp = nil
|
w.fp = nil
|
||||||
unlock(w.ref)
|
unlock(w.ref)
|
||||||
|
|
||||||
|
if w.s.ls != nil && base.Labels != nil {
|
||||||
|
if err := w.s.ls.Set(dgst, base.Labels); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,12 +21,6 @@ import (
|
|||||||
func ContentSuite(t *testing.T, name string, storeFn func(ctx context.Context, root string) (content.Store, func() error, error)) {
|
func ContentSuite(t *testing.T, name string, storeFn func(ctx context.Context, root string) (content.Store, func() error, error)) {
|
||||||
t.Run("Writer", makeTest(t, name, storeFn, checkContentStoreWriter))
|
t.Run("Writer", makeTest(t, name, storeFn, checkContentStoreWriter))
|
||||||
t.Run("UploadStatus", makeTest(t, name, storeFn, checkUploadStatus))
|
t.Run("UploadStatus", makeTest(t, name, storeFn, checkUploadStatus))
|
||||||
}
|
|
||||||
|
|
||||||
// ContentLabelSuite runs a test suite for the content store supporting
|
|
||||||
// labels.
|
|
||||||
// TODO: Merge this with ContentSuite once all content stores support labels
|
|
||||||
func ContentLabelSuite(t *testing.T, name string, storeFn func(ctx context.Context, root string) (content.Store, func() error, error)) {
|
|
||||||
t.Run("Labels", makeTest(t, name, storeFn, checkLabels))
|
t.Run("Labels", makeTest(t, name, storeFn, checkLabels))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,5 +39,4 @@ func TestContentClient(t *testing.T) {
|
|||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
testsuite.ContentSuite(t, "ContentClient", newContentStore)
|
testsuite.ContentSuite(t, "ContentClient", newContentStore)
|
||||||
testsuite.ContentLabelSuite(t, "ContentClient", newContentStore)
|
|
||||||
}
|
}
|
||||||
|
@ -30,5 +30,4 @@ func createContentStore(ctx context.Context, root string) (content.Store, func()
|
|||||||
|
|
||||||
func TestContent(t *testing.T) {
|
func TestContent(t *testing.T) {
|
||||||
testsuite.ContentSuite(t, "metadata", createContentStore)
|
testsuite.ContentSuite(t, "metadata", createContentStore)
|
||||||
testsuite.ContentLabelSuite(t, "metadata", createContentStore)
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user