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 <stephen.day@docker.com>, see
40455aade8
Fixes #1713 Fixes #2865
Signed-off-by: Eric Lin <linxiulei@gmail.com>
This commit is contained in:
@@ -38,16 +38,31 @@ import (
|
||||
|
||||
type contentStore struct {
|
||||
content.Store
|
||||
db *DB
|
||||
l sync.RWMutex
|
||||
db *DB
|
||||
shared bool
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
// newContentStore returns a namespaced content store using an existing
|
||||
// content store interface.
|
||||
func newContentStore(db *DB, cs content.Store) *contentStore {
|
||||
// policy defines the sharing behavior for content between namespaces. Both
|
||||
// modes will result in shared storage in the backend for committed. Choose
|
||||
// "shared" to prevent separate namespaces from having to pull the same content
|
||||
// twice. Choose "isolated" if the content must not be shared between
|
||||
// namespaces.
|
||||
//
|
||||
// If the policy is "shared", writes will try to resolve the "expected" digest
|
||||
// against the backend, allowing imports of content from other namespaces. In
|
||||
// "isolated" mode, the client must prove they have the content by providing
|
||||
// the entire blob before the content can be added to another namespace.
|
||||
//
|
||||
// Since we have only two policies right now, it's simpler using bool to
|
||||
// represent it internally.
|
||||
func newContentStore(db *DB, shared bool, cs content.Store) *contentStore {
|
||||
return &contentStore{
|
||||
Store: cs,
|
||||
db: db,
|
||||
Store: cs,
|
||||
db: db,
|
||||
shared: shared,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,13 +398,15 @@ func (cs *contentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (
|
||||
return nil
|
||||
}
|
||||
|
||||
if st, err := cs.Store.Info(ctx, wOpts.Desc.Digest); err == nil {
|
||||
// Ensure the expected size is the same, it is likely
|
||||
// an error if the size is mismatched but the caller
|
||||
// must resolve this on commit
|
||||
if wOpts.Desc.Size == 0 || wOpts.Desc.Size == st.Size {
|
||||
shared = true
|
||||
wOpts.Desc.Size = st.Size
|
||||
if cs.shared {
|
||||
if st, err := cs.Store.Info(ctx, wOpts.Desc.Digest); err == nil {
|
||||
// Ensure the expected size is the same, it is likely
|
||||
// an error if the size is mismatched but the caller
|
||||
// must resolve this on commit
|
||||
if wOpts.Desc.Size == 0 || wOpts.Desc.Size == st.Size {
|
||||
shared = true
|
||||
wOpts.Desc.Size = st.Size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func createContentStore(ctx context.Context, root string) (context.Context, content.Store, func() error, error) {
|
||||
func createContentStore(ctx context.Context, root string, opts ...DBOpt) (context.Context, content.Store, func() error, error) {
|
||||
// TODO: Use mocked or in-memory store
|
||||
cs, err := local.NewStore(root)
|
||||
if err != nil {
|
||||
@@ -60,13 +60,24 @@ func createContentStore(ctx context.Context, root string) (context.Context, cont
|
||||
}
|
||||
ctx = testsuite.SetContextWrapper(ctx, wrap)
|
||||
|
||||
return ctx, NewDB(db, cs, nil).ContentStore(), func() error {
|
||||
return ctx, NewDB(db, cs, nil, opts...).ContentStore(), func() error {
|
||||
return db.Close()
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createContentStoreWithPolicy(opts ...DBOpt) testsuite.StoreInitFn {
|
||||
return func(ctx context.Context, root string) (context.Context, content.Store, func() error, error) {
|
||||
return createContentStore(ctx, root, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContent(t *testing.T) {
|
||||
testsuite.ContentSuite(t, "metadata", createContentStore)
|
||||
testsuite.ContentSuite(t, "metadata", createContentStoreWithPolicy())
|
||||
testsuite.ContentCrossNSSharedSuite(t, "metadata", createContentStoreWithPolicy())
|
||||
testsuite.ContentCrossNSIsolatedSuite(
|
||||
t, "metadata", createContentStoreWithPolicy([]DBOpt{
|
||||
WithPolicyIsolated,
|
||||
}...))
|
||||
}
|
||||
|
||||
func TestContentLeased(t *testing.T) {
|
||||
|
||||
@@ -46,6 +46,19 @@ const (
|
||||
dbVersion = 3
|
||||
)
|
||||
|
||||
// DBOpt configures how we set up the DB
|
||||
type DBOpt func(*dbOptions)
|
||||
|
||||
// WithPolicyIsolated isolates contents between namespaces
|
||||
func WithPolicyIsolated(o *dbOptions) {
|
||||
o.shared = false
|
||||
}
|
||||
|
||||
// dbOptions configure db options.
|
||||
type dbOptions struct {
|
||||
shared bool
|
||||
}
|
||||
|
||||
// DB represents a metadata database backed by a bolt
|
||||
// database. The database is fully namespaced and stores
|
||||
// image, container, namespace, snapshot, and content data
|
||||
@@ -72,19 +85,28 @@ type DB struct {
|
||||
// mutationCallbacks are called after each mutation with the flag
|
||||
// set indicating whether any dirty flags are set
|
||||
mutationCallbacks []func(bool)
|
||||
|
||||
dbopts dbOptions
|
||||
}
|
||||
|
||||
// NewDB creates a new metadata database using the provided
|
||||
// bolt database, content store, and snapshotters.
|
||||
func NewDB(db *bolt.DB, cs content.Store, ss map[string]snapshots.Snapshotter) *DB {
|
||||
func NewDB(db *bolt.DB, cs content.Store, ss map[string]snapshots.Snapshotter, opts ...DBOpt) *DB {
|
||||
m := &DB{
|
||||
db: db,
|
||||
ss: make(map[string]*snapshotter, len(ss)),
|
||||
dirtySS: map[string]struct{}{},
|
||||
dbopts: dbOptions{
|
||||
shared: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&m.dbopts)
|
||||
}
|
||||
|
||||
// Initialize data stores
|
||||
m.cs = newContentStore(m, cs)
|
||||
m.cs = newContentStore(m, m.dbopts.shared, cs)
|
||||
for name, sn := range ss {
|
||||
m.ss[name] = newSnapshotter(m, name, sn)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user