diff --git a/content/local/store.go b/content/local/store.go index c7854cf9d..14a98881d 100644 --- a/content/local/store.go +++ b/content/local/store.go @@ -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)) }) } diff --git a/content/local/store_test.go b/content/local/store_test.go index 02e65f5c4..442ad8202 100644 --- a/content/local/store_test.go +++ b/content/local/store_test.go @@ -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 } diff --git a/content/local/store_unix.go b/content/local/store_unix.go index 46eab02c5..0d500b84d 100644 --- a/content/local/store_unix.go +++ b/content/local/store_unix.go @@ -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() +} diff --git a/content/local/store_windows.go b/content/local/store_windows.go index 7fb6ad43a..5f12ea5c4 100644 --- a/content/local/store_windows.go +++ b/content/local/store_windows.go @@ -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() +} diff --git a/content/local/writer.go b/content/local/writer.go index c4f1a94f3..8f1e92ded 100644 --- a/content/local/writer.go +++ b/content/local/writer.go @@ -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 } diff --git a/content/testsuite/testsuite.go b/content/testsuite/testsuite.go index 5c4bc371c..d5d4fa3a9 100644 --- a/content/testsuite/testsuite.go +++ b/content/testsuite/testsuite.go @@ -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)) } diff --git a/content_test.go b/content_test.go index 2a0c68630..6fc7818ae 100644 --- a/content_test.go +++ b/content_test.go @@ -39,5 +39,4 @@ func TestContentClient(t *testing.T) { t.Skip() } testsuite.ContentSuite(t, "ContentClient", newContentStore) - testsuite.ContentLabelSuite(t, "ContentClient", newContentStore) } diff --git a/metadata/content_test.go b/metadata/content_test.go index 2e4d833ab..d51a2dbb5 100644 --- a/metadata/content_test.go +++ b/metadata/content_test.go @@ -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) }