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 <cdr@amazon.com>
This commit is contained in:
Cody Roseborough 2021-02-11 23:58:09 +00:00
parent c4664bdac6
commit e692a01926
6 changed files with 254 additions and 2 deletions

View File

@ -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.

View File

@ -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"

View File

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

171
metadata/buckets_test.go Normal file
View File

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

View File

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

View File

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