Merge pull request #5043 from IRCody/shared_namespace
Add a sharedNamespace label
This commit is contained in:
commit
fb589a7133
@ -232,3 +232,7 @@ The default is "shared". While this is largely the most desired policy, one can
|
|||||||
[plugins.bolt]
|
[plugins.bolt]
|
||||||
content_sharing_policy = "isolated"
|
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.
|
// LabelUncompressed is added to compressed layer contents.
|
||||||
// The value is digest of the uncompressed content.
|
// The value is digest of the uncompressed content.
|
||||||
const LabelUncompressed = "containerd.io/uncompressed"
|
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"
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
package metadata
|
package metadata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/containerd/containerd/labels"
|
||||||
digest "github.com/opencontainers/go-digest"
|
digest "github.com/opencontainers/go-digest"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
@ -182,6 +183,45 @@ func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error)
|
|||||||
return bkt, nil
|
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 {
|
func namespaceLabelsBucketPath(namespace string) [][]byte {
|
||||||
return [][]byte{bucketKeyVersion, []byte(namespace), bucketKeyObjectLabels}
|
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
|
var info content.Info
|
||||||
if err := view(ctx, cs.db, func(tx *bolt.Tx) error {
|
if err := view(ctx, cs.db, func(tx *bolt.Tx) error {
|
||||||
bkt := getBlobBucket(tx, ns, dgst)
|
bkt := getBlobBucket(tx, ns, dgst)
|
||||||
|
if bkt == nil {
|
||||||
|
// try to find shareable bkt before erroring
|
||||||
|
bkt = getShareableBucket(tx, dgst)
|
||||||
|
}
|
||||||
if bkt == nil {
|
if bkt == nil {
|
||||||
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", dgst)
|
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 {
|
if err := update(ctx, cs.db, func(tx *bolt.Tx) error {
|
||||||
bkt := getBlobBucket(tx, ns, info.Digest)
|
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 {
|
if bkt == nil {
|
||||||
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", info.Digest)
|
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", info.Digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := readInfo(&updated, bkt); err != nil {
|
if err := readInfo(&updated, bkt); err != nil {
|
||||||
return errors.Wrapf(err, "info %q", info.Digest)
|
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 {
|
return view(ctx, cs.db, func(tx *bolt.Tx) error {
|
||||||
bkt := getBlobBucket(tx, ns, dgst)
|
bkt := getBlobBucket(tx, ns, dgst)
|
||||||
|
if bkt == nil {
|
||||||
|
// try to find shareable bkt before erroring
|
||||||
|
bkt = getShareableBucket(tx, dgst)
|
||||||
|
}
|
||||||
if bkt == nil {
|
if bkt == nil {
|
||||||
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", dgst)
|
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 {
|
if err := view(ctx, s.db, func(tx *bolt.Tx) error {
|
||||||
bkt := getImagesBucket(tx, namespace)
|
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 {
|
if bkt == nil {
|
||||||
return errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
|
return errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user