Skip to content

Commit 4247f26

Browse files
linxiuleileilei.lin
authored and
leilei.lin
committed
metadata: define content sharing policy
This changeset modifies the metadata store to allow one to set a "content sharing policy" that defines how blobs are shared between namespaces in the content store. The default mode "shared" will make blobs available in all namespaces once it is pulled into any namespace. The blob will be pulled into the namespace if a writer is opened with the "Expected" digest that is already present in the backend. The alternative mode, "isolated" requires that clients prove they have access to the content by providing all of the content to the ingest before the blob is added to the namespace. Both modes share backing data, while "shared" will reduce total bandwidth across namespaces, at the cost of allowing access to any blob just by knowing its digest. Note: Most functional codes and changelog of this commit originate from Stephen J Day <[email protected]>, see containerd@40455aa Fixes containerd#1713 Fixes containerd#2865 Signed-off-by: Eric Lin <[email protected]>
1 parent cc06a65 commit 4247f26

File tree

7 files changed

+221
-20
lines changed

7 files changed

+221
-20
lines changed

content/testsuite/testsuite.go

+73-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,16 @@ import (
3838
"gotest.tools/assert"
3939
)
4040

41+
const (
42+
emptyDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
43+
)
44+
45+
// StoreInitFn initializes content store with given root and returns a function for
46+
// destroying the content store
47+
type StoreInitFn func(ctx context.Context, root string) (context.Context, content.Store, func() error, error)
48+
4149
// ContentSuite runs a test suite on the content store given a factory function.
42-
func ContentSuite(t *testing.T, name string, storeFn func(ctx context.Context, root string) (context.Context, content.Store, func() error, error)) {
50+
func ContentSuite(t *testing.T, name string, storeFn StoreInitFn) {
4351
t.Run("Writer", makeTest(t, name, storeFn, checkContentStoreWriter))
4452
t.Run("UpdateStatus", makeTest(t, name, storeFn, checkUpdateStatus))
4553
t.Run("CommitExists", makeTest(t, name, storeFn, checkCommitExists))
@@ -52,10 +60,18 @@ func ContentSuite(t *testing.T, name string, storeFn func(ctx context.Context, r
5260
t.Run("SmallBlob", makeTest(t, name, storeFn, checkSmallBlob))
5361
t.Run("Labels", makeTest(t, name, storeFn, checkLabels))
5462

63+
t.Run("CommitErrorState", makeTest(t, name, storeFn, checkCommitErrorState))
64+
}
65+
66+
// ContentCrossNSSharedSuite runs a test suite under shared content policy
67+
func ContentCrossNSSharedSuite(t *testing.T, name string, storeFn StoreInitFn) {
5568
t.Run("CrossNamespaceAppend", makeTest(t, name, storeFn, checkCrossNSAppend))
5669
t.Run("CrossNamespaceShare", makeTest(t, name, storeFn, checkCrossNSShare))
70+
}
5771

58-
t.Run("CommitErrorState", makeTest(t, name, storeFn, checkCommitErrorState))
72+
// ContentCrossNSIsolatedSuite runs a test suite under isolated content policy
73+
func ContentCrossNSIsolatedSuite(t *testing.T, name string, storeFn StoreInitFn) {
74+
t.Run("CrossNamespaceIsolate", makeTest(t, name, storeFn, checkCrossNSIsolate))
5975
}
6076

6177
// ContextWrapper is used to decorate new context used inside the test
@@ -890,6 +906,38 @@ func checkCrossNSAppend(ctx context.Context, t *testing.T, cs content.Store) {
890906

891907
}
892908

909+
func checkCrossNSIsolate(ctx context.Context, t *testing.T, cs content.Store) {
910+
wrap, ok := ctx.Value(wrapperKey{}).(ContextWrapper)
911+
if !ok {
912+
t.Skip("multiple contexts not supported")
913+
}
914+
915+
var size int64 = 1000
916+
b, d := createContent(size)
917+
ref := fmt.Sprintf("ref-%d", size)
918+
t1 := time.Now()
919+
920+
if err := content.WriteBlob(ctx, cs, ref, bytes.NewReader(b), ocispec.Descriptor{Size: size, Digest: d}); err != nil {
921+
t.Fatal(err)
922+
}
923+
t2 := time.Now()
924+
925+
ctx2, done, err := wrap(context.Background())
926+
if err != nil {
927+
t.Fatal(err)
928+
}
929+
defer done(ctx2)
930+
931+
t3 := time.Now()
932+
w, err := cs.Writer(ctx2, content.WithRef(ref), content.WithDescriptor(ocispec.Descriptor{Size: size, Digest: d}))
933+
if err != nil {
934+
t.Fatal(err)
935+
}
936+
t4 := time.Now()
937+
938+
checkNewlyCreated(t, w, t1, t2, t3, t4)
939+
}
940+
893941
func checkStatus(t *testing.T, w content.Writer, expected content.Status, d digest.Digest, preStart, postStart, preUpdate, postUpdate time.Time) {
894942
t.Helper()
895943
st, err := w.Status()
@@ -934,6 +982,29 @@ func checkStatus(t *testing.T, w content.Writer, expected content.Status, d dige
934982
}
935983
}
936984

985+
func checkNewlyCreated(t *testing.T, w content.Writer, preStart, postStart, preUpdate, postUpdate time.Time) {
986+
t.Helper()
987+
st, err := w.Status()
988+
if err != nil {
989+
t.Fatalf("failed to get status: %v", err)
990+
}
991+
992+
wd := w.Digest()
993+
if wd != emptyDigest {
994+
t.Fatalf("unexpected digest %v, expected %v", wd, emptyDigest)
995+
}
996+
997+
if st.Offset != 0 {
998+
t.Fatalf("unexpected offset %v", st.Offset)
999+
}
1000+
1001+
if runtime.GOOS != "windows" {
1002+
if st.StartedAt.After(postUpdate) || st.StartedAt.Before(postStart) {
1003+
t.Fatalf("unexpected started at time %s, expected between %s and %s", st.StartedAt, postStart, postUpdate)
1004+
}
1005+
}
1006+
}
1007+
9371008
func checkInfo(ctx context.Context, cs content.Store, d digest.Digest, expected content.Info, c1, c2, u1, u2 time.Time) error {
9381009
info, err := cs.Info(ctx, d)
9391010
if err != nil {

docs/ops.md

+18
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,21 @@ The linux runtime allows a few options to be set to configure the shim and the r
220220
# (this only need to be set on kernel < 3.18)
221221
shim_no_newns = true
222222
```
223+
224+
### Bolt Metadata Plugin
225+
226+
The bolt metadata plugin allows configuration of the content sharing policy between namespaces.
227+
228+
The default mode "shared" will make blobs available in all namespaces once it is pulled into any namespace.
229+
The blob will be pulled into the namespace if a writer is opened with the "Expected" digest that is already present in the backend.
230+
231+
The alternative mode, "isolated" requires that clients prove they have access to the content by providing all of the content to the ingest before the blob is added to the namespace.
232+
233+
Both modes share backing data, while "shared" will reduce total bandwidth across namespaces, at the cost of allowing access to any blob just by knowing its digest.
234+
235+
The default is "shared". While this is largely the most desired policy, one can change to "isolated" mode with the following configuration:
236+
237+
```toml
238+
[plugins.bolt]
239+
content_sharing_policy = "isolated"
240+
```

metadata/content.go

+29-12
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,31 @@ import (
3838

3939
type contentStore struct {
4040
content.Store
41-
db *DB
42-
l sync.RWMutex
41+
db *DB
42+
shared bool
43+
l sync.RWMutex
4344
}
4445

4546
// newContentStore returns a namespaced content store using an existing
4647
// content store interface.
47-
func newContentStore(db *DB, cs content.Store) *contentStore {
48+
// policy defines the sharing behavior for content between namespaces. Both
49+
// modes will result in shared storage in the backend for committed. Choose
50+
// "shared" to prevent separate namespaces from having to pull the same content
51+
// twice. Choose "isolated" if the content must not be shared between
52+
// namespaces.
53+
//
54+
// If the policy is "shared", writes will try to resolve the "expected" digest
55+
// against the backend, allowing imports of content from other namespaces. In
56+
// "isolated" mode, the client must prove they have the content by providing
57+
// the entire blob before the content can be added to another namespace.
58+
//
59+
// Since we have only two policies right now, it's simpler using bool to
60+
// represent it internally.
61+
func newContentStore(db *DB, shared bool, cs content.Store) *contentStore {
4862
return &contentStore{
49-
Store: cs,
50-
db: db,
63+
Store: cs,
64+
db: db,
65+
shared: shared,
5166
}
5267
}
5368

@@ -383,13 +398,15 @@ func (cs *contentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (
383398
return nil
384399
}
385400

386-
if st, err := cs.Store.Info(ctx, wOpts.Desc.Digest); err == nil {
387-
// Ensure the expected size is the same, it is likely
388-
// an error if the size is mismatched but the caller
389-
// must resolve this on commit
390-
if wOpts.Desc.Size == 0 || wOpts.Desc.Size == st.Size {
391-
shared = true
392-
wOpts.Desc.Size = st.Size
401+
if cs.shared {
402+
if st, err := cs.Store.Info(ctx, wOpts.Desc.Digest); err == nil {
403+
// Ensure the expected size is the same, it is likely
404+
// an error if the size is mismatched but the caller
405+
// must resolve this on commit
406+
if wOpts.Desc.Size == 0 || wOpts.Desc.Size == st.Size {
407+
shared = true
408+
wOpts.Desc.Size = st.Size
409+
}
393410
}
394411
}
395412
}

metadata/content_test.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import (
3636
bolt "go.etcd.io/bbolt"
3737
)
3838

39-
func createContentStore(ctx context.Context, root string) (context.Context, content.Store, func() error, error) {
39+
func createContentStore(ctx context.Context, root string, opts ...DBOpt) (context.Context, content.Store, func() error, error) {
4040
// TODO: Use mocked or in-memory store
4141
cs, err := local.NewStore(root)
4242
if err != nil {
@@ -60,13 +60,24 @@ func createContentStore(ctx context.Context, root string) (context.Context, cont
6060
}
6161
ctx = testsuite.SetContextWrapper(ctx, wrap)
6262

63-
return ctx, NewDB(db, cs, nil).ContentStore(), func() error {
63+
return ctx, NewDB(db, cs, nil, opts...).ContentStore(), func() error {
6464
return db.Close()
6565
}, nil
6666
}
6767

68+
func createContentStoreWithPolicy(opts ...DBOpt) testsuite.StoreInitFn {
69+
return func(ctx context.Context, root string) (context.Context, content.Store, func() error, error) {
70+
return createContentStore(ctx, root, opts...)
71+
}
72+
}
73+
6874
func TestContent(t *testing.T) {
69-
testsuite.ContentSuite(t, "metadata", createContentStore)
75+
testsuite.ContentSuite(t, "metadata", createContentStoreWithPolicy())
76+
testsuite.ContentCrossNSSharedSuite(t, "metadata", createContentStoreWithPolicy())
77+
testsuite.ContentCrossNSIsolatedSuite(
78+
t, "metadata", createContentStoreWithPolicy([]DBOpt{
79+
WithPolicyIsolated,
80+
}...))
7081
}
7182

7283
func TestContentLeased(t *testing.T) {

metadata/db.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ const (
4646
dbVersion = 3
4747
)
4848

49+
// DBOpt configures how we set up the DB
50+
type DBOpt func(*dbOptions)
51+
52+
// WithPolicyIsolated isolates contents between namespaces
53+
func WithPolicyIsolated(o *dbOptions) {
54+
o.shared = false
55+
}
56+
57+
// dbOptions configure db options.
58+
type dbOptions struct {
59+
shared bool
60+
}
61+
4962
// DB represents a metadata database backed by a bolt
5063
// database. The database is fully namespaced and stores
5164
// image, container, namespace, snapshot, and content data
@@ -72,19 +85,28 @@ type DB struct {
7285
// mutationCallbacks are called after each mutation with the flag
7386
// set indicating whether any dirty flags are set
7487
mutationCallbacks []func(bool)
88+
89+
dbopts dbOptions
7590
}
7691

7792
// NewDB creates a new metadata database using the provided
7893
// bolt database, content store, and snapshotters.
79-
func NewDB(db *bolt.DB, cs content.Store, ss map[string]snapshots.Snapshotter) *DB {
94+
func NewDB(db *bolt.DB, cs content.Store, ss map[string]snapshots.Snapshotter, opts ...DBOpt) *DB {
8095
m := &DB{
8196
db: db,
8297
ss: make(map[string]*snapshotter, len(ss)),
8398
dirtySS: map[string]struct{}{},
99+
dbopts: dbOptions{
100+
shared: true,
101+
},
102+
}
103+
104+
for _, opt := range opts {
105+
opt(&m.dbopts)
84106
}
85107

86108
// Initialize data stores
87-
m.cs = newContentStore(m, cs)
109+
m.cs = newContentStore(m, m.dbopts.shared, cs)
88110
for name, sn := range ss {
89111
m.ss[name] = newSnapshotter(m, name, sn)
90112
}

services/server/config/config.go

+38
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,44 @@ type ProxyPlugin struct {
8383
Address string `toml:"address"`
8484
}
8585

86+
// BoltConfig defines the configuration values for the bolt plugin, which is
87+
// loaded here, rather than back registered in the metadata package.
88+
type BoltConfig struct {
89+
// ContentSharingPolicy sets the sharing policy for content between
90+
// namespaces.
91+
//
92+
// The default mode "shared" will make blobs available in all
93+
// namespaces once it is pulled into any namespace. The blob will be pulled
94+
// into the namespace if a writer is opened with the "Expected" digest that
95+
// is already present in the backend.
96+
//
97+
// The alternative mode, "isolated" requires that clients prove they have
98+
// access to the content by providing all of the content to the ingest
99+
// before the blob is added to the namespace.
100+
//
101+
// Both modes share backing data, while "shared" will reduce total
102+
// bandwidth across namespaces, at the cost of allowing access to any blob
103+
// just by knowing its digest.
104+
ContentSharingPolicy string `toml:"content_sharing_policy"`
105+
}
106+
107+
const (
108+
// SharingPolicyShared represents the "shared" sharing policy
109+
SharingPolicyShared = "shared"
110+
// SharingPolicyIsolated represents the "isolated" sharing policy
111+
SharingPolicyIsolated = "isolated"
112+
)
113+
114+
// Validate validates if BoltConfig is valid
115+
func (bc *BoltConfig) Validate() error {
116+
switch bc.ContentSharingPolicy {
117+
case SharingPolicyShared, SharingPolicyIsolated:
118+
return nil
119+
default:
120+
return errors.Wrapf(errdefs.ErrInvalidArgument, "unknown policy: %s", bc.ContentSharingPolicy)
121+
}
122+
}
123+
86124
// Decode unmarshals a plugin specific configuration by plugin id
87125
func (c *Config) Decode(id string, v interface{}) (interface{}, error) {
88126
data, ok := c.Plugins[id]

services/server/server.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ func LoadPlugins(ctx context.Context, config *srvconfig.Config) ([]*plugin.Regis
238238
plugin.ContentPlugin,
239239
plugin.SnapshotPlugin,
240240
},
241+
Config: &srvconfig.BoltConfig{
242+
ContentSharingPolicy: srvconfig.SharingPolicyShared,
243+
},
241244
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
242245
if err := os.MkdirAll(ic.Root, 0711); err != nil {
243246
return nil, err
@@ -265,14 +268,35 @@ func LoadPlugins(ctx context.Context, config *srvconfig.Config) ([]*plugin.Regis
265268
snapshotters[name] = sn.(snapshots.Snapshotter)
266269
}
267270

271+
shared := true
272+
ic.Meta.Exports["policy"] = srvconfig.SharingPolicyShared
273+
if cfg, ok := ic.Config.(*srvconfig.BoltConfig); ok {
274+
if cfg.ContentSharingPolicy != "" {
275+
if err := cfg.Validate(); err != nil {
276+
return nil, err
277+
}
278+
if cfg.ContentSharingPolicy == srvconfig.SharingPolicyIsolated {
279+
ic.Meta.Exports["policy"] = srvconfig.SharingPolicyIsolated
280+
shared = false
281+
}
282+
283+
log.L.WithField("policy", cfg.ContentSharingPolicy).Info("metadata content store policy set")
284+
}
285+
}
286+
268287
path := filepath.Join(ic.Root, "meta.db")
269288
ic.Meta.Exports["path"] = path
270289

271290
db, err := bolt.Open(path, 0644, nil)
272291
if err != nil {
273292
return nil, err
274293
}
275-
mdb := metadata.NewDB(db, cs.(content.Store), snapshotters)
294+
295+
var dbopts []metadata.DBOpt
296+
if !shared {
297+
dbopts = append(dbopts, metadata.WithPolicyIsolated)
298+
}
299+
mdb := metadata.NewDB(db, cs.(content.Store), snapshotters, dbopts...)
276300
if err := mdb.Init(ic.Context); err != nil {
277301
return nil, err
278302
}

0 commit comments

Comments
 (0)