Skip to content

Commit e692a01

Browse files
author
Cody Roseborough
committed
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 <[email protected]>
1 parent c4664bd commit e692a01

6 files changed

Lines changed: 254 additions & 2 deletions

File tree

docs/ops.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,7 @@ The default is "shared". While this is largely the most desired policy, one can
232232
[plugins.bolt]
233233
content_sharing_policy = "isolated"
234234
```
235+
236+
It is possible to share only the contents of a specific namespace by adding the label `containerd.io/namespace.shareable=true` to that namespace.
237+
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.
238+
If the label value is set to anything other than `true`, the namespace content will not be shared.

labels/labels.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ package labels
1919
// LabelUncompressed is added to compressed layer contents.
2020
// The value is digest of the uncompressed content.
2121
const LabelUncompressed = "containerd.io/uncompressed"
22+
23+
// LabelSharedNamespace is added to a namespace to allow that namespaces
24+
// contents to be shared.
25+
const LabelSharedNamespace = "containerd.io/namespace.shareable"

metadata/buckets.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
// Package metadata stores all labels and object specific metadata by namespace.
18-
// This package also contains the main garbage collection logic for cleaning up
18+
// This package also contains the main garbage collection logic for cleaning up
1919
// resources consistently and atomically. Resources used by backends will be
2020
// tracked in the metadata store to be exposed to consumers of this package.
2121
//
@@ -115,6 +115,7 @@
115115
package metadata
116116

117117
import (
118+
"github.com/containerd/containerd/labels"
118119
digest "github.com/opencontainers/go-digest"
119120
bolt "go.etcd.io/bbolt"
120121
)
@@ -182,6 +183,45 @@ func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error)
182183
return bkt, nil
183184
}
184185

186+
func namespacesBucketPath() []byte {
187+
return bucketKeyVersion
188+
}
189+
190+
func getNamespacesBucket(tx *bolt.Tx) *bolt.Bucket {
191+
return getBucket(tx, namespacesBucketPath())
192+
}
193+
194+
// Given a namespace string and a bolt transaction
195+
// return true if the ns has the shared label in it.
196+
func hasSharedLabel(tx *bolt.Tx, ns string) bool {
197+
labelsBkt := getNamespaceLabelsBucket(tx, ns)
198+
if labelsBkt == nil {
199+
return false
200+
}
201+
cur := labelsBkt.Cursor()
202+
for k, v := cur.First(); k != nil; k, v = cur.Next() {
203+
if string(k) == labels.LabelSharedNamespace && string(v) == "true" {
204+
return true
205+
}
206+
}
207+
return false
208+
}
209+
210+
func getShareableBucket(tx *bolt.Tx, dgst digest.Digest) *bolt.Bucket {
211+
var bkt *bolt.Bucket
212+
nsbkt := getNamespacesBucket(tx)
213+
cur := nsbkt.Cursor()
214+
for k, _ := cur.First(); k != nil; k, _ = cur.Next() {
215+
// If this bucket has shared label
216+
// get the bucket and return it.
217+
if hasSharedLabel(tx, string(k)) {
218+
bkt = getBlobBucket(tx, string(k), dgst)
219+
break
220+
}
221+
}
222+
return bkt
223+
}
224+
185225
func namespaceLabelsBucketPath(namespace string) [][]byte {
186226
return [][]byte{bucketKeyVersion, []byte(namespace), bucketKeyObjectLabels}
187227
}

metadata/buckets_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metadata
18+
19+
import (
20+
"io/ioutil"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/containerd/containerd/labels"
25+
digest "github.com/opencontainers/go-digest"
26+
"github.com/pkg/errors"
27+
bolt "go.etcd.io/bbolt"
28+
)
29+
30+
func TestHasSharedLabel(t *testing.T) {
31+
tmpdir, err := ioutil.TempDir("", "bucket-testing-")
32+
if err != nil {
33+
t.Error(err)
34+
}
35+
36+
db, err := bolt.Open(filepath.Join(tmpdir, "metadata.db"), 0660, nil)
37+
if err != nil {
38+
t.Error(err)
39+
}
40+
41+
err = createNamespaceLabelsBucket(db, "testing-with-shareable", true)
42+
if err != nil {
43+
t.Error(err)
44+
}
45+
46+
err = createNamespaceLabelsBucket(db, "testing-without-shareable", false)
47+
if err != nil {
48+
t.Error(err)
49+
}
50+
51+
err = db.View(func(tx *bolt.Tx) error {
52+
if !hasSharedLabel(tx, "testing-with-shareable") {
53+
return errors.New("hasSharedLabel should return true when label is set")
54+
}
55+
if hasSharedLabel(tx, "testing-without-shareable") {
56+
return errors.New("hasSharedLabel should return false when label is not set")
57+
}
58+
return nil
59+
})
60+
61+
if err != nil {
62+
t.Error(err)
63+
}
64+
}
65+
66+
func TestGetShareableBucket(t *testing.T) {
67+
tmpdir, err := ioutil.TempDir("", "bucket-testing-")
68+
if err != nil {
69+
t.Error(err)
70+
}
71+
72+
db, err := bolt.Open(filepath.Join(tmpdir, "metadata.db"), 0660, nil)
73+
if err != nil {
74+
t.Error(err)
75+
}
76+
77+
goodDigest := digest.FromString("gooddigest")
78+
imagePresentNS := "has-image-is-shareable"
79+
imageAbsentNS := "image-absent"
80+
81+
// Create two namespaces, empty for now
82+
err = db.Update(func(tx *bolt.Tx) error {
83+
_, err := createImagesBucket(tx, imagePresentNS)
84+
if err != nil {
85+
return err
86+
}
87+
88+
_, err = createImagesBucket(tx, imageAbsentNS)
89+
if err != nil {
90+
return err
91+
}
92+
93+
return nil
94+
})
95+
96+
if err != nil {
97+
t.Error(err)
98+
}
99+
100+
// Test that getShareableBucket is correctly returning nothing when a
101+
// a bucket with that digest is not present in any namespace.
102+
err = db.View(func(tx *bolt.Tx) error {
103+
if bkt := getShareableBucket(tx, goodDigest); bkt != nil {
104+
return errors.New("getShareableBucket should return nil if digest is not present")
105+
}
106+
return nil
107+
})
108+
109+
if err != nil {
110+
t.Error(err)
111+
}
112+
113+
// Create a blob bucket in one of the namespaces with a well-known digest
114+
err = db.Update(func(tx *bolt.Tx) error {
115+
_, err = createBlobBucket(tx, imagePresentNS, goodDigest)
116+
if err != nil {
117+
return err
118+
}
119+
return nil
120+
})
121+
122+
if err != nil {
123+
t.Error(err)
124+
}
125+
126+
// Verify that it is still not retrievable if the shareable label is not present
127+
err = db.View(func(tx *bolt.Tx) error {
128+
if bkt := getShareableBucket(tx, goodDigest); bkt != nil {
129+
return errors.New("getShareableBucket should return nil if digest is present but doesn't have shareable label")
130+
}
131+
return nil
132+
})
133+
134+
if err != nil {
135+
t.Error(err)
136+
}
137+
138+
// Create the namespace labels bucket and mark it as shareable
139+
err = createNamespaceLabelsBucket(db, imagePresentNS, true)
140+
if err != nil {
141+
t.Error(err)
142+
}
143+
144+
// Verify that this digest is retrievable from getShareableBucket
145+
err = db.View(func(tx *bolt.Tx) error {
146+
if bkt := getShareableBucket(tx, goodDigest); bkt == nil {
147+
return errors.New("getShareableBucket should not return nil if digest is present")
148+
}
149+
return nil
150+
})
151+
152+
if err != nil {
153+
t.Error(err)
154+
}
155+
}
156+
157+
func createNamespaceLabelsBucket(db transactor, ns string, shareable bool) error {
158+
err := db.Update(func(tx *bolt.Tx) error {
159+
err := withNamespacesLabelsBucket(tx, ns, func(bkt *bolt.Bucket) error {
160+
if shareable {
161+
err := bkt.Put([]byte(labels.LabelSharedNamespace), []byte("true"))
162+
if err != nil {
163+
return err
164+
}
165+
}
166+
return nil
167+
})
168+
return err
169+
})
170+
return err
171+
}

metadata/content.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ func (cs *contentStore) Info(ctx context.Context, dgst digest.Digest) (content.I
7676
var info content.Info
7777
if err := view(ctx, cs.db, func(tx *bolt.Tx) error {
7878
bkt := getBlobBucket(tx, ns, dgst)
79+
if bkt == nil {
80+
// try to find shareable bkt before erroring
81+
bkt = getShareableBucket(tx, dgst)
82+
}
7983
if bkt == nil {
8084
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", dgst)
8185
}
@@ -103,10 +107,13 @@ func (cs *contentStore) Update(ctx context.Context, info content.Info, fieldpath
103107
}
104108
if err := update(ctx, cs.db, func(tx *bolt.Tx) error {
105109
bkt := getBlobBucket(tx, ns, info.Digest)
110+
if bkt == nil {
111+
// try to find a shareable bkt before erroring
112+
bkt = getShareableBucket(tx, info.Digest)
113+
}
106114
if bkt == nil {
107115
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", info.Digest)
108116
}
109-
110117
if err := readInfo(&updated, bkt); err != nil {
111118
return errors.Wrapf(err, "info %q", info.Digest)
112119
}
@@ -699,6 +706,10 @@ func (cs *contentStore) checkAccess(ctx context.Context, dgst digest.Digest) err
699706

700707
return view(ctx, cs.db, func(tx *bolt.Tx) error {
701708
bkt := getBlobBucket(tx, ns, dgst)
709+
if bkt == nil {
710+
// try to find shareable bkt before erroring
711+
bkt = getShareableBucket(tx, dgst)
712+
}
702713
if bkt == nil {
703714
return errors.Wrapf(errdefs.ErrNotFound, "content digest %v", dgst)
704715
}

metadata/images.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error)
5555

5656
if err := view(ctx, s.db, func(tx *bolt.Tx) error {
5757
bkt := getImagesBucket(tx, namespace)
58+
if bkt == nil || bkt.Bucket([]byte(name)) == nil {
59+
nsbkt := getNamespacesBucket(tx)
60+
cur := nsbkt.Cursor()
61+
for k, _ := cur.First(); k != nil; k, _ = cur.Next() {
62+
// If this namespace has the sharedlabel
63+
if hasSharedLabel(tx, string(k)) {
64+
// and has the image we are looking for
65+
bkt = getImagesBucket(tx, string(k))
66+
if bkt == nil {
67+
continue
68+
}
69+
70+
ibkt := bkt.Bucket([]byte(name))
71+
if ibkt == nil {
72+
continue
73+
}
74+
// we are done
75+
break
76+
}
77+
78+
}
79+
}
5880
if bkt == nil {
5981
return errors.Wrapf(errdefs.ErrNotFound, "image %q", name)
6082
}

0 commit comments

Comments
 (0)