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:
parent
c4664bdac6
commit
e692a01926
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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
171
metadata/buckets_test.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user