Skip to content

Commit fb589a7

Browse files
authored
Merge pull request #5043 from IRCody/shared_namespace
Add a sharedNamespace label
2 parents 9eb08bf + e692a01 commit fb589a7

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)