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) }