Add shared content label to namespaces
Signed-off-by: Henry Wang <henwang@amazon.com>
This commit is contained in:
parent
d4641e1ce1
commit
2e080bf491
@ -73,10 +73,15 @@ func ContentCrossNSIsolatedSuite(t *testing.T, name string, storeFn StoreInitFn)
|
|||||||
t.Run("CrossNamespaceIsolate", makeTest(t, name, storeFn, checkCrossNSIsolate))
|
t.Run("CrossNamespaceIsolate", makeTest(t, name, storeFn, checkCrossNSIsolate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContentSharedNSIsolatedSuite runs a test suite for shared namespaces under isolated content policy
|
||||||
|
func ContentSharedNSIsolatedSuite(t *testing.T, name string, storeFn StoreInitFn) {
|
||||||
|
t.Run("SharedNamespaceIsolate", makeTest(t, name, storeFn, checkSharedNSIsolate))
|
||||||
|
}
|
||||||
|
|
||||||
// ContextWrapper is used to decorate new context used inside the test
|
// ContextWrapper is used to decorate new context used inside the test
|
||||||
// before using the context on the content store.
|
// before using the context on the content store.
|
||||||
// This can be used to support leasing and multiple namespaces tests.
|
// This can be used to support leasing and multiple namespaces tests.
|
||||||
type ContextWrapper func(ctx context.Context) (context.Context, func(context.Context) error, error)
|
type ContextWrapper func(ctx context.Context, sharedNS bool) (context.Context, func(context.Context) error, error)
|
||||||
|
|
||||||
type wrapperKey struct{}
|
type wrapperKey struct{}
|
||||||
|
|
||||||
@ -121,7 +126,7 @@ func makeTest(t *testing.T, name string, storeFn func(ctx context.Context, root
|
|||||||
w, ok := ctx.Value(wrapperKey{}).(ContextWrapper)
|
w, ok := ctx.Value(wrapperKey{}).(ContextWrapper)
|
||||||
if ok {
|
if ok {
|
||||||
var done func(context.Context) error
|
var done func(context.Context) error
|
||||||
ctx, done, err = w(ctx)
|
ctx, done, err = w(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error wrapping context: %+v", err)
|
t.Fatalf("Error wrapping context: %+v", err)
|
||||||
}
|
}
|
||||||
@ -824,7 +829,7 @@ func checkCrossNSShare(ctx context.Context, t *testing.T, cs content.Store) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx2, done, err := wrap(context.Background())
|
ctx2, done, err := wrap(context.Background(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -876,7 +881,7 @@ func checkCrossNSAppend(ctx context.Context, t *testing.T, cs content.Store) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx2, done, err := wrap(context.Background())
|
ctx2, done, err := wrap(context.Background(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -945,7 +950,7 @@ func checkCrossNSIsolate(ctx context.Context, t *testing.T, cs content.Store) {
|
|||||||
}
|
}
|
||||||
t2 := time.Now()
|
t2 := time.Now()
|
||||||
|
|
||||||
ctx2, done, err := wrap(context.Background())
|
ctx2, done, err := wrap(context.Background(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -962,6 +967,64 @@ func checkCrossNSIsolate(ctx context.Context, t *testing.T, cs content.Store) {
|
|||||||
checkNewlyCreated(t, w, t1, t2, t3, t4)
|
checkNewlyCreated(t, w, t1, t2, t3, t4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkSharedNSIsolate(ctx context.Context, t *testing.T, cs content.Store) {
|
||||||
|
wrap, ok := ctx.Value(wrapperKey{}).(ContextWrapper)
|
||||||
|
if !ok {
|
||||||
|
t.Skip("multiple contexts not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx1, done1, err := wrap(context.Background(), true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer done1(ctx1)
|
||||||
|
|
||||||
|
var size int64 = 1000
|
||||||
|
b, d := createContent(size)
|
||||||
|
ref := fmt.Sprintf("ref-%d", size)
|
||||||
|
t1 := time.Now()
|
||||||
|
|
||||||
|
if err := content.WriteBlob(ctx1, cs, ref, bytes.NewReader(b), ocispec.Descriptor{Size: size, Digest: d}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx2, done2, err := wrap(context.Background(), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer done2(ctx2)
|
||||||
|
|
||||||
|
w, err := cs.Writer(ctx2, content.WithRef(ref), content.WithDescriptor(ocispec.Descriptor{Size: size, Digest: d}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
t2 := time.Now()
|
||||||
|
|
||||||
|
checkStatus(t, w, content.Status{
|
||||||
|
Ref: ref,
|
||||||
|
Offset: size,
|
||||||
|
Total: size,
|
||||||
|
}, d, t1, t2, t1, t2)
|
||||||
|
|
||||||
|
if err := w.Commit(ctx2, size, d); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t3 := time.Now()
|
||||||
|
|
||||||
|
info := content.Info{
|
||||||
|
Digest: d,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
if err := checkContent(ctx1, cs, d, info, t1, t3, t1, t3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkContent(ctx2, cs, d, info, t1, t3, t1, t3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkStatus(t *testing.T, w content.Writer, expected content.Status, d digest.Digest, preStart, postStart, preUpdate, postUpdate time.Time) {
|
func checkStatus(t *testing.T, w content.Writer, expected content.Status, d digest.Digest, preStart, postStart, preUpdate, postUpdate time.Time) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
st, err := w.Status()
|
st, err := w.Status()
|
||||||
|
@ -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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In "isolated" mode, it is also possible to share only the contents of a specific namespace by adding the label `containerd.io/namespace.shareable=true` to that namespace.
|
||||||
|
This will make its blobs available in all other namespaces even if the content sharing policy is set to "isolated".
|
||||||
|
If the label value is set to anything other than `true`, the namespace content will not be shared.
|
||||||
|
@ -41,7 +41,7 @@ func newContentStore(ctx context.Context, root string) (context.Context, content
|
|||||||
name = testsuite.Name(ctx)
|
name = testsuite.Name(ctx)
|
||||||
)
|
)
|
||||||
|
|
||||||
wrap := func(ctx context.Context) (context.Context, func(context.Context) error, error) {
|
wrap := func(ctx context.Context, sharedNS bool) (context.Context, func(context.Context) error, error) {
|
||||||
n := atomic.AddUint64(&count, 1)
|
n := atomic.AddUint64(&count, 1)
|
||||||
ctx = namespaces.WithNamespace(ctx, fmt.Sprintf("%s-n%d", name, n))
|
ctx = namespaces.WithNamespace(ctx, fmt.Sprintf("%s-n%d", name, n))
|
||||||
return client.WithLease(ctx)
|
return client.WithLease(ctx)
|
||||||
|
@ -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"
|
||||||
|
@ -398,7 +398,7 @@ func (cs *contentStore) Writer(ctx context.Context, opts ...content.WriterOpt) (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cs.shared {
|
if cs.shared || isSharedContent(tx, wOpts.Desc.Digest) {
|
||||||
if st, err := cs.Store.Info(ctx, wOpts.Desc.Digest); err == nil {
|
if st, err := cs.Store.Info(ctx, wOpts.Desc.Digest); err == nil {
|
||||||
// Ensure the expected size is the same, it is likely
|
// Ensure the expected size is the same, it is likely
|
||||||
// an error if the size is mismatched but the caller
|
// an error if the size is mismatched but the caller
|
||||||
@ -706,6 +706,33 @@ func (cs *contentStore) checkAccess(ctx context.Context, dgst digest.Digest) err
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSharedContent(tx *bolt.Tx, dgst digest.Digest) bool {
|
||||||
|
v1bkt := tx.Bucket(bucketKeyVersion)
|
||||||
|
if v1bkt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// iterate through each namespace
|
||||||
|
v1c := v1bkt.Cursor()
|
||||||
|
for nk, _ := v1c.First(); nk != nil; nk, _ = v1c.Next() {
|
||||||
|
ns := string(nk)
|
||||||
|
lbkt := getNamespaceLabelsBucket(tx, ns)
|
||||||
|
if lbkt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// iterate through each label
|
||||||
|
lbc := lbkt.Cursor()
|
||||||
|
for k, v := lbc.First(); k != nil; k, v = lbc.Next() {
|
||||||
|
if string(k) == labels.LabelSharedNamespace {
|
||||||
|
if string(v) == "true" && getBlobBucket(tx, ns, dgst) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func validateInfo(info *content.Info) error {
|
func validateInfo(info *content.Info) error {
|
||||||
for k, v := range info.Labels {
|
for k, v := range info.Labels {
|
||||||
if err := labels.Validate(k, v); err != nil {
|
if err := labels.Validate(k, v); err != nil {
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/containerd/containerd/content/local"
|
"github.com/containerd/containerd/content/local"
|
||||||
"github.com/containerd/containerd/content/testsuite"
|
"github.com/containerd/containerd/content/testsuite"
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
|
"github.com/containerd/containerd/labels"
|
||||||
"github.com/containerd/containerd/leases"
|
"github.com/containerd/containerd/leases"
|
||||||
"github.com/containerd/containerd/namespaces"
|
"github.com/containerd/containerd/namespaces"
|
||||||
digest "github.com/opencontainers/go-digest"
|
digest "github.com/opencontainers/go-digest"
|
||||||
@ -52,9 +53,18 @@ func createContentStore(ctx context.Context, root string, opts ...DBOpt) (contex
|
|||||||
count uint64
|
count uint64
|
||||||
name = testsuite.Name(ctx)
|
name = testsuite.Name(ctx)
|
||||||
)
|
)
|
||||||
wrap := func(ctx context.Context) (context.Context, func(context.Context) error, error) {
|
wrap := func(ctx context.Context, sharedNS bool) (context.Context, func(context.Context) error, error) {
|
||||||
n := atomic.AddUint64(&count, 1)
|
n := atomic.AddUint64(&count, 1)
|
||||||
return namespaces.WithNamespace(ctx, fmt.Sprintf("%s-n%d", name, n)), func(context.Context) error {
|
ctx2 := namespaces.WithNamespace(ctx, fmt.Sprintf("%s-n%d", name, n))
|
||||||
|
if sharedNS {
|
||||||
|
db.Update(func(tx *bolt.Tx) error {
|
||||||
|
if ns, err := namespaces.NamespaceRequired(ctx2); err == nil {
|
||||||
|
return NewNamespaceStore(tx).SetLabel(ctx2, ns, labels.LabelSharedNamespace, "true")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ctx2, func(context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -78,6 +88,10 @@ func TestContent(t *testing.T) {
|
|||||||
t, "metadata", createContentStoreWithPolicy([]DBOpt{
|
t, "metadata", createContentStoreWithPolicy([]DBOpt{
|
||||||
WithPolicyIsolated,
|
WithPolicyIsolated,
|
||||||
}...))
|
}...))
|
||||||
|
testsuite.ContentSharedNSIsolatedSuite(
|
||||||
|
t, "metadata", createContentStoreWithPolicy([]DBOpt{
|
||||||
|
WithPolicyIsolated,
|
||||||
|
}...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContentLeased(t *testing.T) {
|
func TestContentLeased(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user