Add delete target to image remove

Adds atomicity to image delete when deleting from a list.

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan 2023-08-18 16:04:09 -07:00
parent f8fb2dad39
commit 2ce971d890
No known key found for this signature in database
GPG Key ID: F58C5D0A4405ACDB
5 changed files with 128 additions and 38 deletions

View File

@ -20,14 +20,12 @@ import (
"context" "context"
imagesapi "github.com/containerd/containerd/api/services/images/v1" imagesapi "github.com/containerd/containerd/api/services/images/v1"
"github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images" "github.com/containerd/containerd/images"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/pkg/epoch" "github.com/containerd/containerd/pkg/epoch"
"github.com/containerd/containerd/protobuf" "github.com/containerd/containerd/protobuf"
ptypes "github.com/containerd/containerd/protobuf/types" ptypes "github.com/containerd/containerd/protobuf/types"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
@ -108,11 +106,14 @@ func (s *remoteImages) Delete(ctx context.Context, name string, opts ...images.D
return err return err
} }
} }
_, err := s.client.Delete(ctx, &imagesapi.DeleteImageRequest{ req := &imagesapi.DeleteImageRequest{
Name: name, Name: name,
Sync: do.Synchronous, Sync: do.Synchronous,
}) }
if do.Target != nil {
req.Target = oci.DescriptorToProto(*do.Target)
}
_, err := s.client.Delete(ctx, req)
return errdefs.FromGRPC(err) return errdefs.FromGRPC(err)
} }
@ -120,7 +121,7 @@ func imageToProto(image *images.Image) *imagesapi.Image {
return &imagesapi.Image{ return &imagesapi.Image{
Name: image.Name, Name: image.Name,
Labels: image.Labels, Labels: image.Labels,
Target: descToProto(&image.Target), Target: oci.DescriptorToProto(image.Target),
CreatedAt: protobuf.ToTimestamp(image.CreatedAt), CreatedAt: protobuf.ToTimestamp(image.CreatedAt),
UpdatedAt: protobuf.ToTimestamp(image.UpdatedAt), UpdatedAt: protobuf.ToTimestamp(image.UpdatedAt),
} }
@ -130,7 +131,7 @@ func imageFromProto(imagepb *imagesapi.Image) images.Image {
return images.Image{ return images.Image{
Name: imagepb.Name, Name: imagepb.Name,
Labels: imagepb.Labels, Labels: imagepb.Labels,
Target: descFromProto(imagepb.Target), Target: oci.DescriptorFromProto(imagepb.Target),
CreatedAt: protobuf.FromTimestamp(imagepb.CreatedAt), CreatedAt: protobuf.FromTimestamp(imagepb.CreatedAt),
UpdatedAt: protobuf.FromTimestamp(imagepb.UpdatedAt), UpdatedAt: protobuf.FromTimestamp(imagepb.UpdatedAt),
} }
@ -146,21 +147,3 @@ func imagesFromProto(imagespb []*imagesapi.Image) []images.Image {
return images return images
} }
func descFromProto(desc *types.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: desc.MediaType,
Size: desc.Size,
Digest: digest.Digest(desc.Digest),
Annotations: desc.Annotations,
}
}
func descToProto(desc *ocispec.Descriptor) *types.Descriptor {
return &types.Descriptor{
MediaType: desc.MediaType,
Size: desc.Size,
Digest: desc.Digest.String(),
Annotations: desc.Annotations,
}
}

View File

@ -58,6 +58,7 @@ type Image struct {
// DeleteOptions provide options on image delete // DeleteOptions provide options on image delete
type DeleteOptions struct { type DeleteOptions struct {
Synchronous bool Synchronous bool
Target *ocispec.Descriptor
} }
// DeleteOpt allows configuring a delete operation // DeleteOpt allows configuring a delete operation
@ -72,6 +73,16 @@ func SynchronousDelete() DeleteOpt {
} }
} }
// DeleteTarget is used to specify the target value an image is expected
// to have when deleting. If the image has a different target, then
// NotFound is returned.
func DeleteTarget(target *ocispec.Descriptor) DeleteOpt {
return func(ctx context.Context, o *DeleteOptions) error {
o.Target = target
return nil
}
}
// Store and interact with images // Store and interact with images
type Store interface { type Store interface {
Get(ctx context.Context, name string) (Image, error) Get(ctx context.Context, name string) (Image, error)

View File

@ -266,6 +266,13 @@ func (s *imageStore) Delete(ctx context.Context, name string, opts ...images.Del
return err return err
} }
var options images.DeleteOptions
for _, opt := range opts {
if err := opt(ctx, &options); err != nil {
return err
}
}
return update(ctx, s.db, func(tx *bolt.Tx) error { return update(ctx, s.db, func(tx *bolt.Tx) error {
bkt := getImagesBucket(tx, namespace) bkt := getImagesBucket(tx, namespace)
if bkt == nil { if bkt == nil {
@ -276,6 +283,22 @@ func (s *imageStore) Delete(ctx context.Context, name string, opts ...images.Del
return err return err
} }
if options.Target != nil && options.Target.Digest != "" {
ibkt := bkt.Bucket([]byte(name))
if ibkt == nil {
return fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)
}
var check images.Image
if err := readImage(&check, ibkt); err != nil {
return fmt.Errorf("image %q: %w", name, err)
}
if check.Target.Digest != options.Target.Digest {
return fmt.Errorf("image %q has target %v, not %v: %w", name, check.Target.Digest, options.Target.Digest, errdefs.ErrNotFound)
}
}
if err = bkt.DeleteBucket([]byte(name)); err != nil { if err = bkt.DeleteBucket([]byte(name)); err != nil {
if err == bolt.ErrBucketNotFound { if err == bolt.ErrBucketNotFound {
err = fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound) err = fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)

View File

@ -154,10 +154,11 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
name string name string
original images.Image original images.Image
createerr error createerr error
input images.Image input images.Image // Input target size determines target digest, base image uses 10
fieldpaths []string fieldpaths []string
expected images.Image expected images.Image
cause error cause error
deleteerr error
}{ }{
{ {
name: "Touch", name: "Touch",
@ -239,7 +240,7 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
"boo": "boo", "boo": "boo",
}, },
Target: ocispec.Descriptor{ Target: ocispec.Descriptor{
Size: 20, // ignored Size: 10,
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
Annotations: map[string]string{ Annotations: map[string]string{
"not": "bar", "not": "bar",
@ -272,7 +273,7 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
"boo": "boo", "boo": "boo",
}, },
Target: ocispec.Descriptor{ Target: ocispec.Descriptor{
Size: 20, // ignored Size: 10,
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
Annotations: map[string]string{ Annotations: map[string]string{
"foo": "boo", "foo": "boo",
@ -303,7 +304,7 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
"baz": "bunk", "baz": "bunk",
}, },
Target: ocispec.Descriptor{ Target: ocispec.Descriptor{
Size: 20, // ignored Size: 10,
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
Annotations: map[string]string{ Annotations: map[string]string{
"foo": "bar", "foo": "bar",
@ -335,7 +336,7 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
"baz": "bunk", "baz": "bunk",
}, },
Target: ocispec.Descriptor{ Target: ocispec.Descriptor{
Size: 20, // ignored Size: 10,
MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
Annotations: map[string]string{ Annotations: map[string]string{
"foo": "baz", "foo": "baz",
@ -360,7 +361,7 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
}, },
}, },
{ {
name: "ReplaceTarget", // target must be updated as a unit name: "ReplaceTargetTypeAndAnnotations", // target must be updated as a unit
original: imageBase(), original: imageBase(),
input: images.Image{ input: images.Image{
Target: ocispec.Descriptor{ Target: ocispec.Descriptor{
@ -386,6 +387,57 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
}, },
}, },
}, },
{
name: "ReplaceTargetFieldpath", // target must be updated as a unit
original: imageBase(),
input: images.Image{
Target: ocispec.Descriptor{
Size: 20,
MediaType: "application/vnd.oci.blab+replaced",
Annotations: map[string]string{
"fox": "dog",
},
},
},
fieldpaths: []string{"target"},
expected: images.Image{
Labels: map[string]string{ // Labels not updated
"foo": "bar",
"baz": "boo",
},
Target: ocispec.Descriptor{
Size: 20,
MediaType: "application/vnd.oci.blab+replaced",
Annotations: map[string]string{
"fox": "dog",
},
},
},
deleteerr: errdefs.ErrNotFound,
},
{
name: "ReplaceTarget", // target must be updated as a unit
original: imageBase(),
input: images.Image{
Target: ocispec.Descriptor{
Size: 20,
MediaType: "application/vnd.oci.blab+replaced",
Annotations: map[string]string{
"fox": "dog",
},
},
},
expected: images.Image{
Target: ocispec.Descriptor{
Size: 20,
MediaType: "application/vnd.oci.blab+replaced",
Annotations: map[string]string{
"fox": "dog",
},
},
},
deleteerr: errdefs.ErrNotFound,
},
{ {
name: "EmptySize", name: "EmptySize",
original: imageBase(), original: imageBase(),
@ -486,11 +538,9 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
} }
testcase.expected.Name = testcase.name testcase.expected.Name = testcase.name
if testcase.original.Target.Digest == "" { testcase.original.Target.Digest = digest.FromString(fmt.Sprintf("%s-%d", testcase.name, testcase.original.Target.Size))
testcase.original.Target.Digest = digest.FromString(testcase.name) testcase.input.Target.Digest = digest.FromString(fmt.Sprintf("%s-%d", testcase.name, testcase.input.Target.Size))
testcase.input.Target.Digest = testcase.original.Target.Digest testcase.expected.Target.Digest = testcase.input.Target.Digest
testcase.expected.Target.Digest = testcase.original.Target.Digest
}
// Create // Create
now := time.Now() now := time.Now()
@ -538,6 +588,22 @@ func TestImagesCreateUpdateDelete(t *testing.T) {
} }
checkImagesEqual(t, &result, &testcase.expected, "get after failed to get expected result") checkImagesEqual(t, &result, &testcase.expected, "get after failed to get expected result")
if testcase.original.Target.Digest != testcase.expected.Target.Digest {
t.Log("Delete should fail")
}
// Delete
err = store.Delete(ctx, testcase.original.Name, images.DeleteTarget(&testcase.original.Target))
if err != nil {
if testcase.deleteerr == nil {
t.Fatal(err)
}
if !errors.Is(err, testcase.deleteerr) {
t.Fatal("unexpected error", err, ", expected", testcase.deleteerr)
}
} else if testcase.deleteerr != nil {
t.Fatal("no error on deleted, expected", testcase.deleteerr)
}
}) })
} }
} }

View File

@ -176,7 +176,14 @@ func (l *local) Update(ctx context.Context, req *imagesapi.UpdateImageRequest, _
func (l *local) Delete(ctx context.Context, req *imagesapi.DeleteImageRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) { func (l *local) Delete(ctx context.Context, req *imagesapi.DeleteImageRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
log.G(ctx).WithField("name", req.Name).Debugf("delete image") log.G(ctx).WithField("name", req.Name).Debugf("delete image")
if err := l.store.Delete(ctx, req.Name); err != nil { var opts []images.DeleteOpt
if req.Target != nil {
desc := descFromProto(req.Target)
opts = append(opts, images.DeleteTarget(&desc))
}
// Sync option handled here after event is published
if err := l.store.Delete(ctx, req.Name, opts...); err != nil {
return nil, errdefs.ToGRPC(err) return nil, errdefs.ToGRPC(err)
} }