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:
		| @@ -8,6 +8,7 @@ import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"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 | ||||
| // stored under a verifiable digest. | ||||
| // | ||||
| @@ -34,16 +48,27 @@ var ( | ||||
| // including resumable ingest. | ||||
| type store struct { | ||||
| 	root string | ||||
| 	ls   LabelStore | ||||
| } | ||||
|  | ||||
| // NewStore returns a local content store | ||||
| 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) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &store{ | ||||
| 		root: root, | ||||
| 		ls:   ls, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| @@ -57,16 +82,23 @@ func (s *store) Info(ctx context.Context, dgst digest.Digest) (content.Info, err | ||||
|  | ||||
| 		return content.Info{}, err | ||||
| 	} | ||||
|  | ||||
| 	return s.info(dgst, fi), nil | ||||
| 	var labels map[string]string | ||||
| 	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{ | ||||
| 		Digest:    dgst, | ||||
| 		Size:      fi.Size(), | ||||
| 		CreatedAt: fi.ModTime(), | ||||
| 		UpdatedAt: fi.ModTime(), | ||||
| 		UpdatedAt: getATime(fi), | ||||
| 		Labels:    labels, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -111,8 +143,66 @@ 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) { | ||||
| 	// TODO: Support persisting and updating mutable content data | ||||
| 	return content.Info{}, errors.Wrapf(errdefs.ErrFailedPrecondition, "update not supported on immutable content store") | ||||
| 	if s.ls == nil { | ||||
| 		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 { | ||||
| @@ -154,7 +244,14 @@ func (s *store) Walk(ctx context.Context, fn content.WalkFunc, filters ...string | ||||
| 			// 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" | ||||
| 	"reflect" | ||||
| 	"runtime" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| @@ -23,9 +24,55 @@ import ( | ||||
| 	"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) { | ||||
| 	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 { | ||||
| 			return nil, nil, err | ||||
| 		} | ||||
|   | ||||
| @@ -18,3 +18,12 @@ func getStartTime(fi os.FileInfo) time.Time { | ||||
|  | ||||
| 	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 { | ||||
| 	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 { | ||||
| 	var base content.Info | ||||
| 	for _, opt := range opts { | ||||
| 		if err := opt(&base); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if w.fp == nil { | ||||
| 		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 | ||||
| 	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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,12 +21,6 @@ import ( | ||||
| 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("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)) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -39,5 +39,4 @@ func TestContentClient(t *testing.T) { | ||||
| 		t.Skip() | ||||
| 	} | ||||
| 	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) { | ||||
| 	testsuite.ContentSuite(t, "metadata", createContentStore) | ||||
| 	testsuite.ContentLabelSuite(t, "metadata", createContentStore) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Derek McGowan
					Derek McGowan