From e692a01926cd296f3b77ee7f6ba1308b5ecd6aca Mon Sep 17 00:00:00 2001 From: Cody Roseborough Date: Thu, 11 Feb 2021 23:58:09 +0000 Subject: [PATCH] Add shared content label to namespaces Adds shared content labels to namespaces allowing content to be shared between namespaces if that namespace is specifically tagged as being sharable by adding the `containerd.io/namespace/sharable` label to the namespace. Signed-off-by: Cody Roseborough --- docs/ops.md | 4 + labels/labels.go | 4 + metadata/buckets.go | 42 +++++++++- metadata/buckets_test.go | 171 +++++++++++++++++++++++++++++++++++++++ metadata/content.go | 13 ++- metadata/images.go | 22 +++++ 6 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 metadata/buckets_test.go diff --git a/docs/ops.md b/docs/ops.md index c884db083..2e14beda1 100644 --- a/docs/ops.md +++ b/docs/ops.md @@ -232,3 +232,7 @@ The default is "shared". While this is largely the most desired policy, one can [plugins.bolt] content_sharing_policy = "isolated" ``` + +It is possible to share only the contents of a specific namespace by adding the label `containerd.io/namespace.shareable=true` to that namespace. +This will share the contents of the namespace even if the content sharing policy is set to isolated and make its images usable by all other namespaces. +If the label value is set to anything other than `true`, the namespace content will not be shared. diff --git a/labels/labels.go b/labels/labels.go index d76ff2cf9..6be006c3e 100644 --- a/labels/labels.go +++ b/labels/labels.go @@ -19,3 +19,7 @@ package labels // LabelUncompressed is added to compressed layer contents. // The value is digest of the uncompressed content. const LabelUncompressed = "containerd.io/uncompressed" + +// LabelSharedNamespace is added to a namespace to allow that namespaces +// contents to be shared. +const LabelSharedNamespace = "containerd.io/namespace.shareable" diff --git a/metadata/buckets.go b/metadata/buckets.go index fa947fb25..9103d16ed 100644 --- a/metadata/buckets.go +++ b/metadata/buckets.go @@ -15,7 +15,7 @@ */ // Package metadata stores all labels and object specific metadata by namespace. -// This package also contains the main garbage collection logic for cleaning up +// This package also contains the main garbage collection logic for cleaning up // resources consistently and atomically. Resources used by backends will be // tracked in the metadata store to be exposed to consumers of this package. // @@ -115,6 +115,7 @@ package metadata import ( + "github.com/containerd/containerd/labels" digest "github.com/opencontainers/go-digest" bolt "go.etcd.io/bbolt" ) @@ -182,6 +183,45 @@ func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error) return bkt, nil } +func namespacesBucketPath() []byte { + return bucketKeyVersion +} + +func getNamespacesBucket(tx *bolt.Tx) *bolt.Bucket { + return getBucket(tx, namespacesBucketPath()) +} + +// Given a namespace string and a bolt transaction +// return true if the ns has the shared label in it. +func hasSharedLabel(tx *bolt.Tx, ns string) bool { + labelsBkt := getNamespaceLabelsBucket(tx, ns) + if labelsBkt == nil { + return false + } + cur := labelsBkt.Cursor() + for k, v := cur.First(); k != nil; k, v = cur.Next() { + if string(k) == labels.LabelSharedNamespace && string(v) == "true" { + return true + } + } + return false +} + +func getShareableBucket(tx *bolt.Tx, dgst digest.Digest) *bolt.Bucket { + var bkt *bolt.Bucket + nsbkt := getNamespacesBucket(tx) + cur := nsbkt.Cursor() + for k, _ := cur.First(); k != nil; k, _ = cur.Next() { + // If this bucket has shared label + // get the bucket and return it. + if hasSharedLabel(tx, string(k)) { + bkt = getBlobBucket(tx, string(k), dgst) + break + } + } + return bkt +} + func namespaceLabelsBucketPath(namespace string) [][]byte { return [][]byte{bucketKeyVersion, []byte(namespace), bucketKeyObjectLabels} } diff --git a/metadata/buckets_test.go b/metadata/buckets_test.go new file mode 100644 index 000000000..727082f02 --- /dev/null +++ b/metadata/buckets_test.go @@ -0,0 +1,171 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package metadata + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/containerd/containerd/labels" + digest "github.com/opencontainers/go-digest" + "github.com/pkg/errors" + bolt "go.etcd.io/bbolt" +) + +func TestHasSharedLabel(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "bucket-testing-") + if err != nil { + t.Error(err) + } + + db, err := bolt.Open(filepath.Join(tmpdir, "metadata.db"), 0660, nil) + if err != nil { + t.Error(err) + } + + err = createNamespaceLabelsBucket(db, "testing-with-shareable", true) + if err != nil { + t.Error(err) + } + + err = createNamespaceLabelsBucket(db, "testing-without-shareable", false) + if err != nil { + t.Error(err) + } + + err = db.View(func(tx *bolt.Tx) error { + if !hasSharedLabel(tx, "testing-with-shareable") { + return errors.New("hasSharedLabel should return true when label is set") + } + if hasSharedLabel(tx, "testing-without-shareable") { + return errors.New("hasSharedLabel should return false when label is not set") + } + return nil + }) + + if err != nil { + t.Error(err) + } +} + +func TestGetShareableBucket(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "bucket-testing-") + if err != nil { + t.Error(err) + } + + db, err := bolt.Open(filepath.Join(tmpdir, "metadata.db"), 0660, nil) + if err != nil { + t.Error(err) + } + + goodDigest := digest.FromString("gooddigest") + imagePresentNS := "has-image-is-shareable" + imageAbsentNS := "image-absent" + + // Create two namespaces, empty for now + err = db.Update(func(tx *bolt.Tx) error { + _, err := createImagesBucket(tx, imagePresentNS) + if err != nil { + return err + } + + _, err = createImagesBucket(tx, imageAbsentNS) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + t.Error(err) + } + + // Test that getShareableBucket is correctly returning nothing when a + // a bucket with that digest is not present in any namespace. + err = db.View(func(tx *bolt.Tx) error { + if bkt := getShareableBucket(tx, goodDigest); bkt != nil { + return errors.New("getShareableBucket should return nil if digest is not present") + } + return nil + }) + + if err != nil { + t.Error(err) + } + + // Create a blob bucket in one of the namespaces with a well-known digest + err = db.Update(func(tx *bolt.Tx) error { + _, err = createBlobBucket(tx, imagePresentNS, goodDigest) + if err != nil { + return err + } + return nil + }) + + if err != nil { + t.Error(err) + } + + // Verify that it is still not retrievable if the shareable label is not present + err = db.View(func(tx *bolt.Tx) error { + if bkt := getShareableBucket(tx, goodDigest); bkt != nil { + return errors.New("getShareableBucket should return nil if digest is present but doesn't have shareable label") + } + return nil + }) + + if err != nil { + t.Error(err) + } + + // Create the namespace labels bucket and mark it as shareable + err = createNamespaceLabelsBucket(db, imagePresentNS, true) + if err != nil { + t.Error(err) + } + + // Verify that this digest is retrievable from getShareableBucket + err = db.View(func(tx *bolt.Tx) error { + if bkt := getShareableBucket(tx, goodDigest); bkt == nil { + return errors.New("getShareableBucket should not return nil if digest is present") + } + return nil + }) + + if err != nil { + t.Error(err) + } +} + +func createNamespaceLabelsBucket(db transactor, ns string, shareable bool) error { + err := db.Update(func(tx *bolt.Tx) error { + err := withNamespacesLabelsBucket(tx, ns, func(bkt *bolt.Bucket) error { + if shareable { + err := bkt.Put([]byte(labels.LabelSharedNamespace), []byte("true")) + if err != nil { + return err + } + } + return nil + }) + return err + }) + return err +} diff --git a/metadata/content.go b/metadata/content.go index a3858afec..f12f7b160 100644 --- a/metadata/content.go +++ b/metadata/content.go @@ -76,6 +76,10 @@ func (cs *contentStore) Info(ctx context.Context, dgst digest.Digest) (content.I var info content.Info if err := view(ctx, cs.db, func(tx *bolt.Tx) error { bkt := getBlobBucket(tx, ns, dgst) + if bkt == nil { + // try to find shareable bkt before erroring + bkt = getShareableBucket(tx, dgst) + } if bkt == nil { return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", dgst) } @@ -103,10 +107,13 @@ func (cs *contentStore) Update(ctx context.Context, info content.Info, fieldpath } if err := update(ctx, cs.db, func(tx *bolt.Tx) error { bkt := getBlobBucket(tx, ns, info.Digest) + if bkt == nil { + // try to find a shareable bkt before erroring + bkt = getShareableBucket(tx, info.Digest) + } if bkt == nil { return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", info.Digest) } - if err := readInfo(&updated, bkt); err != nil { return errors.Wrapf(err, "info %q", info.Digest) } @@ -699,6 +706,10 @@ func (cs *contentStore) checkAccess(ctx context.Context, dgst digest.Digest) err return view(ctx, cs.db, func(tx *bolt.Tx) error { bkt := getBlobBucket(tx, ns, dgst) + if bkt == nil { + // try to find shareable bkt before erroring + bkt = getShareableBucket(tx, dgst) + } if bkt == nil { return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", dgst) } diff --git a/metadata/images.go b/metadata/images.go index cace4e180..46553c0c8 100644 --- a/metadata/images.go +++ b/metadata/images.go @@ -55,6 +55,28 @@ func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error) if err := view(ctx, s.db, func(tx *bolt.Tx) error { bkt := getImagesBucket(tx, namespace) + if bkt == nil || bkt.Bucket([]byte(name)) == nil { + nsbkt := getNamespacesBucket(tx) + cur := nsbkt.Cursor() + for k, _ := cur.First(); k != nil; k, _ = cur.Next() { + // If this namespace has the sharedlabel + if hasSharedLabel(tx, string(k)) { + // and has the image we are looking for + bkt = getImagesBucket(tx, string(k)) + if bkt == nil { + continue + } + + ibkt := bkt.Bucket([]byte(name)) + if ibkt == nil { + continue + } + // we are done + break + } + + } + } if bkt == nil { return errors.Wrapf(errdefs.ErrNotFound, "image %q", name) }