Merge pull request #7964 from dmcgowan/transfer-image-store-references

[transfer] update imagestore interface to support multiple references
This commit is contained in:
Akihiro Suda 2023-02-14 11:22:27 +09:00 committed by GitHub
commit 4e2eb8ba4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 996 additions and 212 deletions

View File

@ -6757,32 +6757,12 @@ file {
json_name: "manifestLimit" json_name: "manifestLimit"
} }
field { field {
name: "prefix" name: "extra_references"
number: 6 number: 6
label: LABEL_OPTIONAL label: LABEL_REPEATED
type: TYPE_STRING type: TYPE_MESSAGE
json_name: "prefix" type_name: ".containerd.types.transfer.ImageReference"
} json_name: "extraReferences"
field {
name: "check_prefix"
number: 7
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "checkPrefix"
}
field {
name: "digest_refs"
number: 8
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "digestRefs"
}
field {
name: "always_digest"
number: 9
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "alwaysDigest"
} }
field { field {
name: "unpacks" name: "unpacks"
@ -6831,6 +6811,44 @@ file {
json_name: "snapshotter" json_name: "snapshotter"
} }
} }
message_type {
name: "ImageReference"
field {
name: "name"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "name"
}
field {
name: "is_prefix"
number: 2
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "isPrefix"
}
field {
name: "allow_overwrite"
number: 3
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "allowOverwrite"
}
field {
name: "add_digest"
number: 4
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "addDigest"
}
field {
name: "skip_named_digest"
number: 5
label: LABEL_OPTIONAL
type: TYPE_BOOL
json_name: "skipNamedDigest"
}
}
options { options {
go_package: "github.com/containerd/containerd/api/types/transfer" go_package: "github.com/containerd/containerd/api/types/transfer"
} }

View File

@ -46,14 +46,8 @@ type ImageStore struct {
Platforms []*types.Platform `protobuf:"bytes,3,rep,name=platforms,proto3" json:"platforms,omitempty"` Platforms []*types.Platform `protobuf:"bytes,3,rep,name=platforms,proto3" json:"platforms,omitempty"`
AllMetadata bool `protobuf:"varint,4,opt,name=all_metadata,json=allMetadata,proto3" json:"all_metadata,omitempty"` AllMetadata bool `protobuf:"varint,4,opt,name=all_metadata,json=allMetadata,proto3" json:"all_metadata,omitempty"`
ManifestLimit uint32 `protobuf:"varint,5,opt,name=manifest_limit,json=manifestLimit,proto3" json:"manifest_limit,omitempty"` ManifestLimit uint32 `protobuf:"varint,5,opt,name=manifest_limit,json=manifestLimit,proto3" json:"manifest_limit,omitempty"`
// prefix is the intended image name prefix for imported images // extra_references are used to set image names on imports of sub-images from the index
Prefix string `protobuf:"bytes,6,opt,name=prefix,proto3" json:"prefix,omitempty"` ExtraReferences []*ImageReference `protobuf:"bytes,6,rep,name=extra_references,json=extraReferences,proto3" json:"extra_references,omitempty"`
// check_prefix only stores images with the prefix
CheckPrefix bool `protobuf:"varint,7,opt,name=check_prefix,json=checkPrefix,proto3" json:"check_prefix,omitempty"`
// digest_refs adds digest references for images using prefix
DigestRefs bool `protobuf:"varint,8,opt,name=digest_refs,json=digestRefs,proto3" json:"digest_refs,omitempty"`
// always_digest includes a digest image even when a non-digest image is stored
AlwaysDigest bool `protobuf:"varint,9,opt,name=always_digest,json=alwaysDigest,proto3" json:"always_digest,omitempty"`
Unpacks []*UnpackConfiguration `protobuf:"bytes,10,rep,name=unpacks,proto3" json:"unpacks,omitempty"` Unpacks []*UnpackConfiguration `protobuf:"bytes,10,rep,name=unpacks,proto3" json:"unpacks,omitempty"`
} }
@ -124,32 +118,11 @@ func (x *ImageStore) GetManifestLimit() uint32 {
return 0 return 0
} }
func (x *ImageStore) GetPrefix() string { func (x *ImageStore) GetExtraReferences() []*ImageReference {
if x != nil { if x != nil {
return x.Prefix return x.ExtraReferences
} }
return "" return nil
}
func (x *ImageStore) GetCheckPrefix() bool {
if x != nil {
return x.CheckPrefix
}
return false
}
func (x *ImageStore) GetDigestRefs() bool {
if x != nil {
return x.DigestRefs
}
return false
}
func (x *ImageStore) GetAlwaysDigest() bool {
if x != nil {
return x.AlwaysDigest
}
return false
} }
func (x *ImageStore) GetUnpacks() []*UnpackConfiguration { func (x *ImageStore) GetUnpacks() []*UnpackConfiguration {
@ -217,6 +190,103 @@ func (x *UnpackConfiguration) GetSnapshotter() string {
return "" return ""
} }
// ImageReference is used to create or find a reference for an image
type ImageReference struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// is_prefix determines whether the Name should be considered
// a prefix (without tag or digest).
// For lookup, this may allow matching multiple tags.
// For store, this must have a tag or digest added.
IsPrefix bool `protobuf:"varint,2,opt,name=is_prefix,json=isPrefix,proto3" json:"is_prefix,omitempty"`
// allow_overwrite allows overwriting or ignoring the name if
// another reference is provided (such as through an annotation).
// Only used if IsPrefix is true.
AllowOverwrite bool `protobuf:"varint,3,opt,name=allow_overwrite,json=allowOverwrite,proto3" json:"allow_overwrite,omitempty"`
// add_digest adds the manifest digest to the reference.
// For lookup, this allows matching tags with any digest.
// For store, this allows adding the digest to the name.
// Only used if IsPrefix is true.
AddDigest bool `protobuf:"varint,4,opt,name=add_digest,json=addDigest,proto3" json:"add_digest,omitempty"`
// skip_named_digest only considers digest references which do not
// have a non-digested named reference.
// For lookup, this will deduplicate digest references when there is a named match.
// For store, this only adds this digest reference when there is no matching full
// name reference from the prefix.
// Only used if IsPrefix is true.
SkipNamedDigest bool `protobuf:"varint,5,opt,name=skip_named_digest,json=skipNamedDigest,proto3" json:"skip_named_digest,omitempty"`
}
func (x *ImageReference) Reset() {
*x = ImageReference{}
if protoimpl.UnsafeEnabled {
mi := &file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ImageReference) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ImageReference) ProtoMessage() {}
func (x *ImageReference) ProtoReflect() protoreflect.Message {
mi := &file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ImageReference.ProtoReflect.Descriptor instead.
func (*ImageReference) Descriptor() ([]byte, []int) {
return file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDescGZIP(), []int{2}
}
func (x *ImageReference) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ImageReference) GetIsPrefix() bool {
if x != nil {
return x.IsPrefix
}
return false
}
func (x *ImageReference) GetAllowOverwrite() bool {
if x != nil {
return x.AllowOverwrite
}
return false
}
func (x *ImageReference) GetAddDigest() bool {
if x != nil {
return x.AddDigest
}
return false
}
func (x *ImageReference) GetSkipNamedDigest() bool {
if x != nil {
return x.SkipNamedDigest
}
return false
}
var File_github_com_containerd_containerd_api_types_transfer_imagestore_proto protoreflect.FileDescriptor var File_github_com_containerd_containerd_api_types_transfer_imagestore_proto protoreflect.FileDescriptor
var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDesc = []byte{ var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDesc = []byte{
@ -229,7 +299,7 @@ var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_ra
0x72, 0x1a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x72, 0x1a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x70, 0x6c, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x70, 0x6c,
0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf5, 0x03, 0x0a, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x03, 0x0a,
0x0a, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x0a, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x49, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
@ -244,35 +314,44 @@ var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_ra
0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x61, 0x6c, 0x6c, 0x4d, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x61, 0x6c, 0x6c, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x61, 0x6e, 0x69, 0x66,
0x65, 0x73, 0x74, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x0d, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0d, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x54,
0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63,
0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x74, 0x72, 0x61, 0x6e,
0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x69, 0x67, 0x73, 0x66, 0x65, 0x72, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65,
0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x6e, 0x63, 0x65, 0x52, 0x0f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65,
0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x52, 0x65, 0x66, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x07, 0x75, 0x6e, 0x70, 0x61, 0x63, 0x6b, 0x73, 0x18,
0x77, 0x61, 0x79, 0x73, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x08, 0x52, 0x0c, 0x61, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x72, 0x64, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65,
0x48, 0x0a, 0x07, 0x75, 0x6e, 0x70, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x72, 0x2e, 0x55, 0x6e, 0x70, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72,
0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x74, 0x79, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x75, 0x6e, 0x70, 0x61, 0x63, 0x6b, 0x73, 0x1a, 0x39,
0x70, 0x65, 0x73, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2e, 0x55, 0x6e, 0x70, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a,
0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6f, 0x0a, 0x13, 0x55, 0x6e, 0x70,
0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x52, 0x07, 0x75, 0x6e, 0x70, 0x61, 0x63, 0x6b, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x12, 0x36, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01,
0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x08,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70,
0x3a, 0x02, 0x38, 0x01, 0x22, 0x6f, 0x0a, 0x13, 0x55, 0x6e, 0x70, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x73, 0x68, 0x6f, 0x74, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73,
0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x08, 0x70, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x74, 0x65, 0x72, 0x22, 0xb5, 0x01, 0x0a, 0x0e, 0x49,
0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x2e, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02,
0x6f, 0x72, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x74, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x27,
0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x77, 0x72, 0x69, 0x74,
0x6f, 0x74, 0x74, 0x65, 0x72, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4f, 0x76,
0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x63, 0x65, 0x72, 0x77, 0x72, 0x69, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x64, 0x64, 0x5f, 0x64,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x64, 0x64,
0x70, 0x65, 0x73, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x6e,
0x6f, 0x74, 0x6f, 0x33, 0x61, 0x6d, 0x65, 0x64, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0f, 0x73, 0x6b, 0x69, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x64, 0x44, 0x69, 0x67, 0x65,
0x73, 0x74, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73,
0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
} }
var ( var (
@ -287,23 +366,25 @@ func file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_r
return file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDescData return file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDescData
} }
var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_goTypes = []interface{}{ var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_goTypes = []interface{}{
(*ImageStore)(nil), // 0: containerd.types.transfer.ImageStore (*ImageStore)(nil), // 0: containerd.types.transfer.ImageStore
(*UnpackConfiguration)(nil), // 1: containerd.types.transfer.UnpackConfiguration (*UnpackConfiguration)(nil), // 1: containerd.types.transfer.UnpackConfiguration
nil, // 2: containerd.types.transfer.ImageStore.LabelsEntry (*ImageReference)(nil), // 2: containerd.types.transfer.ImageReference
(*types.Platform)(nil), // 3: containerd.types.Platform nil, // 3: containerd.types.transfer.ImageStore.LabelsEntry
(*types.Platform)(nil), // 4: containerd.types.Platform
} }
var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_depIdxs = []int32{ var file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_depIdxs = []int32{
2, // 0: containerd.types.transfer.ImageStore.labels:type_name -> containerd.types.transfer.ImageStore.LabelsEntry 3, // 0: containerd.types.transfer.ImageStore.labels:type_name -> containerd.types.transfer.ImageStore.LabelsEntry
3, // 1: containerd.types.transfer.ImageStore.platforms:type_name -> containerd.types.Platform 4, // 1: containerd.types.transfer.ImageStore.platforms:type_name -> containerd.types.Platform
1, // 2: containerd.types.transfer.ImageStore.unpacks:type_name -> containerd.types.transfer.UnpackConfiguration 2, // 2: containerd.types.transfer.ImageStore.extra_references:type_name -> containerd.types.transfer.ImageReference
3, // 3: containerd.types.transfer.UnpackConfiguration.platform:type_name -> containerd.types.Platform 1, // 3: containerd.types.transfer.ImageStore.unpacks:type_name -> containerd.types.transfer.UnpackConfiguration
4, // [4:4] is the sub-list for method output_type 4, // 4: containerd.types.transfer.UnpackConfiguration.platform:type_name -> containerd.types.Platform
4, // [4:4] is the sub-list for method input_type 5, // [5:5] is the sub-list for method output_type
4, // [4:4] is the sub-list for extension type_name 5, // [5:5] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension extendee 5, // [5:5] is the sub-list for extension type_name
0, // [0:4] is the sub-list for field type_name 5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
} }
func init() { file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_init() } func init() { file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_init() }
@ -336,6 +417,18 @@ func file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_i
return nil return nil
} }
} }
file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ImageReference); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@ -343,7 +436,7 @@ func file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_i
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDesc, RawDescriptor: file_github_com_containerd_containerd_api_types_transfer_imagestore_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 3, NumMessages: 4,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -34,14 +34,8 @@ message ImageStore {
// Import naming // Import naming
// prefix is the intended image name prefix for imported images // extra_references are used to set image names on imports of sub-images from the index
string prefix = 6; repeated ImageReference extra_references = 6;
// check_prefix only stores images with the prefix
bool check_prefix = 7;
// digest_refs adds digest references for images using prefix
bool digest_refs = 8;
// always_digest includes a digest image even when a non-digest image is stored
bool always_digest = 9;
// Unpack Configuration, multiple allowed // Unpack Configuration, multiple allowed
@ -56,3 +50,33 @@ message UnpackConfiguration {
// snapshotter to unpack to, if not provided default for platform shoudl be used // snapshotter to unpack to, if not provided default for platform shoudl be used
string snapshotter = 2; string snapshotter = 2;
} }
// ImageReference is used to create or find a reference for an image
message ImageReference {
string name = 1;
// is_prefix determines whether the Name should be considered
// a prefix (without tag or digest).
// For lookup, this may allow matching multiple tags.
// For store, this must have a tag or digest added.
bool is_prefix = 2;
// allow_overwrite allows overwriting or ignoring the name if
// another reference is provided (such as through an annotation).
// Only used if IsPrefix is true.
bool allow_overwrite = 3;
// add_digest adds the manifest digest to the reference.
// For lookup, this allows matching tags with any digest.
// For store, this allows adding the digest to the name.
// Only used if IsPrefix is true.
bool add_digest = 4;
// skip_named_digest only considers digest references which do not
// have a non-digested named reference.
// For lookup, this will deduplicate digest references when there is a named match.
// For store, this only adds this digest reference when there is no matching full
// name reference from the prefix.
// Only used if IsPrefix is true.
bool skip_named_digest = 5;
}

View File

@ -114,16 +114,17 @@ If foobar.tar contains an OCI ref named "latest" and anonymous ref "sha256:deadb
if !context.BoolT("local") { if !context.BoolT("local") {
var opts []image.StoreOpt var opts []image.StoreOpt
prefix := context.String("base-name") prefix := context.String("base-name")
var overwrite bool
if prefix == "" { if prefix == "" {
prefix = fmt.Sprintf("import-%s", time.Now().Format("2006-01-02")) prefix = fmt.Sprintf("import-%s", time.Now().Format("2006-01-02"))
opts = append(opts, image.WithNamePrefix(prefix, false)) // Allow overwriting auto-generated prefix with named annotation
} else { overwrite = true
// When provided, filter out references which do not match
opts = append(opts, image.WithNamePrefix(prefix, true))
} }
if context.Bool("digests") { if context.Bool("digests") {
opts = append(opts, image.WithDigestRefs(!context.Bool("skip-digest-for-named"))) opts = append(opts, image.WithDigestRef(prefix, overwrite, !context.Bool("skip-digest-for-named")))
} else {
opts = append(opts, image.WithNamedPrefix(prefix, overwrite))
} }
// TODO: Add platform options // TODO: Add platform options

View File

@ -20,12 +20,15 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"hash/fnv"
"io" "io"
"os"
"math/rand" "math/rand"
"os"
"reflect" "reflect"
"runtime" "runtime"
"strings"
"sync"
"testing" "testing"
"time" "time"
@ -36,6 +39,9 @@ import (
"github.com/containerd/containerd/images/archive" "github.com/containerd/containerd/images/archive"
"github.com/containerd/containerd/leases" "github.com/containerd/containerd/leases"
"github.com/containerd/containerd/oci" "github.com/containerd/containerd/oci"
"github.com/containerd/containerd/pkg/transfer"
tarchive "github.com/containerd/containerd/pkg/transfer/archive"
"github.com/containerd/containerd/pkg/transfer/image"
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
@ -165,8 +171,8 @@ func TestImport(t *testing.T) {
empty := []byte("{}") empty := []byte("{}")
version := []byte("1.0") version := []byte("1.0")
c1, d2 := createConfig(runtime.GOOS, runtime.GOARCH) c1, d2 := createConfig(runtime.GOOS, runtime.GOARCH, "test")
badConfig, _ := createConfig("foo", "lish") badConfig, _ := createConfig("foo", "lish", "test")
m1, d3, expManifest := createManifest(c1, [][]byte{b1}) m1, d3, expManifest := createManifest(c1, [][]byte{b1})
@ -381,11 +387,11 @@ func createContent(size int64, seed int64) ([]byte, digest.Digest) {
return b, digest.FromBytes(b) return b, digest.FromBytes(b)
} }
func createConfig(osName, archName string) ([]byte, digest.Digest) { func createConfig(osName, archName, author string) ([]byte, digest.Digest) {
image := ocispec.Image{ image := ocispec.Image{
OS: osName, OS: osName,
Architecture: archName, Architecture: archName,
Author: "test", Author: author,
} }
b, _ := json.Marshal(image) b, _ := json.Marshal(image)
@ -450,3 +456,234 @@ func createIndex(manifest []byte, tags ...string) []byte {
return b return b
} }
func TestTransferImport(t *testing.T) {
ctx, cancel := testContext(t)
defer cancel()
client, err := newClient(t, address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
for _, testCase := range []struct {
// Name is the name of the test
Name string
// Images is the names of the images to create
// [0]: Index name or ""
// [1:]: Additional images and manifest to import
// Images ending with @ will have digest appended and use the digest of the previously imported image
// A space can be used to seperate a repo name and tag, only the tag will be set in the imported image
Images []string
Opts []image.StoreOpt
}{
{
Name: "Basic",
Images: []string{"", "registry.test/basic:latest"},
Opts: []image.StoreOpt{image.WithNamedPrefix("unused", true)},
},
{
Name: "IndexRef",
Images: []string{"registry.test/index-ref:latest", ""},
},
{
Name: "AllRefs",
Images: []string{"registry.test/all-refs:index", "registry.test/all-refs:1"},
Opts: []image.StoreOpt{image.WithNamedPrefix("registry.test/all-refs", false)},
},
{
Name: "DigestRefs",
Images: []string{"registry.test/all-refs:index", "registry.test/all-refs:1", "registry.test/all-refs@"},
Opts: []image.StoreOpt{image.WithDigestRef("registry.test/all-refs", false, false)},
},
{
Name: "DigestRefsSkipNamed",
Images: []string{"registry.test/all-refs:index", "registry.test/all-refs:1"},
Opts: []image.StoreOpt{image.WithDigestRef("registry.test/all-refs", false, true)},
},
{
Name: "DigestOnly",
Images: []string{"", "", "imported-image@"},
Opts: []image.StoreOpt{image.WithDigestRef("imported-image", false, true)},
},
{
Name: "OverwriteDigestRefs",
Images: []string{"registry.test/all-refs:index", "registry.test/all-refs:1", "someimportname@"},
Opts: []image.StoreOpt{image.WithDigestRef("someimportname", true, false)},
},
{
Name: "TagOnlyRef",
Images: []string{"", "registry.test/myimage thebest"},
Opts: []image.StoreOpt{image.WithNamedPrefix("registry.test/myimage", false)},
},
{
Name: "TagOnlyOverwriteDigestRefs",
Images: []string{"registry.test/all-refs:index", "registry.test/basename latest", "registry.test/basename@"},
Opts: []image.StoreOpt{image.WithDigestRef("registry.test/basename", true, false)},
},
} {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
tc := tartest.TarContext{}
files := []tartest.WriterToTar{
tc.Dir("blobs", 0755),
tc.Dir("blobs/sha256", 0755),
}
descs, tws := createImages(tc, testCase.Images...)
files = append(files, tws...)
files = append(files, tc.File("oci-layout", []byte(`{"imageLayoutVersion":"1.0.0"}`), 0644))
r := tartest.TarFromWriterTo(tartest.TarAll(files...))
var idxName string
if len(testCase.Images) > 0 {
idxName = testCase.Images[0]
}
is := image.NewStore(idxName, testCase.Opts...)
iis := tarchive.NewImageImportStream(r, "")
progressTracker := &imagesProgress{}
err := client.Transfer(ctx, iis, is, transfer.WithProgress(progressTracker.Progress))
closeErr := r.Close()
if err != nil {
t.Fatal(err)
}
if closeErr != nil {
t.Fatal(closeErr)
}
imgs := progressTracker.getImages()
if len(descs) != len(imgs) {
t.Fatalf("unexpected number of images saved:\n\t(%d) %v\nexpected image map:\n\t(%d) %v", len(imgs), imgs, len(descs), descs)
}
store := client.ImageService()
for _, image := range imgs {
desc, ok := descs[image]
if !ok {
t.Fatalf("saved image %q not found in expected list\nimages saved:\n\t(%d) %v\nexpected image map:\n\t(%d) %v", image, len(progressTracker.images), progressTracker.images, len(descs), descs)
}
img, err := store.Get(ctx, image)
if err != nil {
t.Fatalf("error getting image %s: %v", image, err)
}
if img.Target.Digest != desc.Digest {
t.Fatalf("digests don't match for %s: got %s, expected %s", image, img.Target.Digest, desc.Digest)
}
if img.Target.MediaType != desc.MediaType {
t.Fatalf("media type don't match for %s: got %s, expected %s", image, img.Target.MediaType, desc.MediaType)
}
if img.Target.Size != desc.Size {
t.Fatalf("size don't match for %s: got %d, expected %d", image, img.Target.Size, desc.Size)
}
}
})
}
}
type imagesProgress struct {
sync.Mutex
images []string
}
func (ip *imagesProgress) Progress(p transfer.Progress) {
ip.Lock()
if p.Event == "saved" {
ip.images = append(ip.images, p.Name)
}
ip.Unlock()
}
func (ip *imagesProgress) getImages() []string {
ip.Lock()
imgs := ip.images
ip.Unlock()
return imgs
}
func createImages(tc tartest.TarContext, imageNames ...string) (descs map[string]ocispec.Descriptor, tw []tartest.WriterToTar) {
descs = map[string]ocispec.Descriptor{}
idx := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
}
if len(imageNames) > 1 {
var lastManifest ocispec.Descriptor
for _, image := range imageNames[1:] {
if image != "" && image[len(image)-1] == '@' {
image = image[:len(image)-1]
descs[fmt.Sprintf("%s@%s", image, lastManifest.Digest)] = lastManifest
continue
}
seed := hash64(image)
bb, b := createContent(128, seed)
tw = append(tw, tc.File("blobs/sha256/"+b.Encoded(), bb, 0644))
cb, c := createConfig("linux", "amd64", image)
tw = append(tw, tc.File("blobs/sha256/"+c.Encoded(), cb, 0644))
mb, m, _ := createManifest(cb, [][]byte{bb})
tw = append(tw, tc.File("blobs/sha256/"+m.Encoded(), mb, 0644))
annotations := map[string]string{}
if image != "" {
if parts := strings.SplitN(image, " ", 2); len(parts) == 2 {
annotations[ocispec.AnnotationRefName] = parts[1]
image = strings.Join(parts, ":")
} else {
annotations[images.AnnotationImageName] = image
}
}
md := ocispec.Descriptor{
Digest: m,
Size: int64(len(mb)),
MediaType: ocispec.MediaTypeImageManifest,
Annotations: annotations,
}
// If image is empty, but has base and digest, still use digest
// If image is not a full reference, then add base if provided?
if image != "" {
descs[image] = md
}
idx.Manifests = append(idx.Manifests, md)
lastManifest = md
}
}
ib, _ := json.Marshal(idx)
id := ocispec.Descriptor{
Digest: digest.FromBytes(ib),
Size: int64(len(ib)),
MediaType: ocispec.MediaTypeImageIndex,
}
tw = append(tw, tc.File("index.json", ib, 0644))
var idxName string
if len(imageNames) > 0 {
idxName = imageNames[0]
}
if idxName != "" {
descs[idxName] = id
}
return
}
func hash64(s string) int64 {
h := fnv.New64a()
h.Write([]byte(s))
return int64(h.Sum64())
}

View File

@ -48,15 +48,42 @@ type Store struct {
labelMap func(ocispec.Descriptor) []string labelMap func(ocispec.Descriptor) []string
manifestLimit int manifestLimit int
//import image options // extraReferences are used to store or lookup multiple references
namePrefix string extraReferences []Reference
checkPrefix bool
digestRefs bool
alwaysDigest bool
unpacks []UnpackConfiguration unpacks []UnpackConfiguration
} }
// Reference is used to create or find a reference for an image
type Reference struct {
Name string
// IsPrefix determines whether the Name should be considered
// a prefix (without tag or digest).
// For lookup, this may allow matching multiple tags.
// For store, this must have a tag or digest added.
IsPrefix bool
// AllowOverwrite allows overwriting or ignoring the name if
// another reference is provided (such as through an annotation).
// Only used if IsPrefix is true.
AllowOverwrite bool
// AddDigest adds the manifest digest to the reference.
// For lookup, this allows matching tags with any digest.
// For store, this allows adding the digest to the name.
// Only used if IsPrefix is true.
AddDigest bool
// SkipNamedDigest only considers digest references which do not
// have a non-digested named reference.
// For lookup, this will deduplicate digest references when there is a named match.
// For store, this only adds this digest reference when there is no matching full
// name reference from the prefix.
// Only used if IsPrefix is true.
SkipNamedDigest bool
}
// UnpackConfiguration specifies the platform and snapshotter to use for resolving // UnpackConfiguration specifies the platform and snapshotter to use for resolving
// the unpack Platform, if snapshotter is not specified the platform default will // the unpack Platform, if snapshotter is not specified the platform default will
// be used. // be used.
@ -93,22 +120,51 @@ func WithAllMetadata(s *Store) {
s.allMetadata = true s.allMetadata = true
} }
// WithNamePrefix sets the name prefix for imported images, if // WithNamedPrefix uses a named prefix to references images which only have a tag name
// check is enabled, then only images with the prefix are stored. // reference in the annotation or check full references annotations against. Images
func WithNamePrefix(prefix string, check bool) StoreOpt { // with no reference resolved from matching annotations will not be stored.
// - name: image name prefix to append a tag to or check full name references with
// - allowOverwrite: allows the tag to be overwritten by full name reference inside
// the image which does not have name as the prefix
func WithNamedPrefix(name string, allowOverwrite bool) StoreOpt {
ref := Reference{
Name: name,
IsPrefix: true,
AllowOverwrite: allowOverwrite,
}
return func(s *Store) { return func(s *Store) {
s.namePrefix = prefix s.extraReferences = append(s.extraReferences, ref)
s.checkPrefix = check
} }
} }
// WithDigestRefs sets digest refs for imported images, if // WithNamedPrefix uses a named prefix to references images which only have a tag name
// always is enabled, then digest refs are added even if a // reference in the annotation or check full references annotations against and
// non-digest image name is added for the same image. // additionally may add a digest reference. Images with no references resolved
func WithDigestRefs(always bool) StoreOpt { // from matching annotations may be stored by digest.
// - name: image name prefix to append a tag to or check full name references with
// - allowOverwrite: allows the tag to be overwritten by full name reference inside
// the image which does not have name as the prefix
// - skipNamed: is set if no digest reference should be created if a named reference
// is successfully resolved from the annotations.
func WithDigestRef(name string, allowOverwrite bool, skipNamed bool) StoreOpt {
ref := Reference{
Name: name,
IsPrefix: true,
AllowOverwrite: allowOverwrite,
AddDigest: true,
SkipNamedDigest: skipNamed,
}
return func(s *Store) { return func(s *Store) {
s.digestRefs = true s.extraReferences = append(s.extraReferences, ref)
s.alwaysDigest = always }
}
func WithExtraReference(name string) StoreOpt {
ref := Reference{
Name: name,
}
return func(s *Store) {
s.extraReferences = append(s.extraReferences, ref)
} }
} }
@ -163,64 +219,114 @@ func (is *Store) ImageFilter(h images.HandlerFunc, cs content.Store) images.Hand
return h return h
} }
func (is *Store) Store(ctx context.Context, desc ocispec.Descriptor, store images.Store) (images.Image, error) { func (is *Store) Store(ctx context.Context, desc ocispec.Descriptor, store images.Store) ([]images.Image, error) {
img := images.Image{ var imgs []images.Image
// If import ref type, store references from annotation or prefix
if refSource, ok := desc.Annotations["io.containerd.import.ref-source"]; ok {
switch refSource {
case "annotation":
for _, ref := range is.extraReferences {
// Only use prefix references for annotation matching
if !ref.IsPrefix {
continue
}
var nameT func(string) string
if ref.AllowOverwrite {
nameT = archive.AddRefPrefix(ref.Name)
} else {
nameT = archive.FilterRefPrefix(ref.Name)
}
name := imageName(desc.Annotations, nameT)
if name == "" {
// If digested, add digest reference
if ref.AddDigest {
imgs = append(imgs, images.Image{
Name: fmt.Sprintf("%s@%s", ref.Name, desc.Digest),
Target: desc,
Labels: is.imageLabels,
})
}
continue
}
imgs = append(imgs, images.Image{
Name: name,
Target: desc,
Labels: is.imageLabels,
})
// If a named reference was found and SkipNamedDigest is true, do
// not use this reference
if ref.AddDigest && !ref.SkipNamedDigest {
imgs = append(imgs, images.Image{
Name: fmt.Sprintf("%s@%s", ref.Name, desc.Digest),
Target: desc,
Labels: is.imageLabels,
})
}
}
default:
return nil, fmt.Errorf("ref source not supported: %w", errdefs.ErrInvalidArgument)
}
delete(desc.Annotations, "io.containerd.import.ref-source")
} else {
if is.imageName != "" {
imgs = append(imgs, images.Image{
Name: is.imageName, Name: is.imageName,
Target: desc, Target: desc,
Labels: is.imageLabels, Labels: is.imageLabels,
})
} }
// Handle imported image names // If extra references, store all complete references (skip prefixes)
if refType, ok := desc.Annotations["io.containerd.import.ref-type"]; ok { for _, ref := range is.extraReferences {
var nameT func(string) string if ref.IsPrefix {
if is.checkPrefix { continue
nameT = archive.FilterRefPrefix(is.namePrefix)
} else {
nameT = archive.AddRefPrefix(is.namePrefix)
} }
name := imageName(desc.Annotations, nameT) name := ref.Name
switch refType { if ref.AddDigest {
case "name": name = fmt.Sprintf("%s@%s", name, desc.Digest)
if name == "" {
return images.Image{}, fmt.Errorf("no image name: %w", errdefs.ErrNotFound)
} }
img.Name = name imgs = append(imgs, images.Image{
case "digest": Name: name,
if !is.digestRefs || (!is.alwaysDigest && name != "") { Target: desc,
return images.Image{}, fmt.Errorf("no digest refs: %w", errdefs.ErrNotFound) Labels: is.imageLabels,
})
} }
img.Name = fmt.Sprintf("%s@%s", is.namePrefix, desc.Digest)
default:
return images.Image{}, fmt.Errorf("ref type not supported: %w", errdefs.ErrInvalidArgument)
}
delete(desc.Annotations, "io.containerd.import.ref-type")
} else if img.Name == "" {
// No valid image combination found
return images.Image{}, fmt.Errorf("no image name found: %w", errdefs.ErrNotFound)
} }
for { if len(imgs) == 0 {
if created, err := store.Create(ctx, img); err != nil { return nil, fmt.Errorf("no image name found: %w", errdefs.ErrNotFound)
}
for i := 0; i < len(imgs); {
if created, err := store.Create(ctx, imgs[i]); err != nil {
if !errdefs.IsAlreadyExists(err) { if !errdefs.IsAlreadyExists(err) {
return images.Image{}, err return nil, err
} }
updated, err := store.Update(ctx, img) updated, err := store.Update(ctx, imgs[i])
if err != nil { if err != nil {
// if image was removed, try create again // if image was removed, try create again
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
// Keep trying same image
continue continue
} }
return images.Image{}, err return nil, err
} }
img = updated imgs[i] = updated
} else { } else {
img = created imgs[i] = created
} }
return img, nil i++
} }
return imgs, nil
} }
func (is *Store) Get(ctx context.Context, store images.Store) (images.Image, error) { func (is *Store) Get(ctx context.Context, store images.Store) (images.Image, error) {
@ -244,10 +350,7 @@ func (is *Store) MarshalAny(context.Context, streaming.StreamCreator) (typeurl.A
ManifestLimit: uint32(is.manifestLimit), ManifestLimit: uint32(is.manifestLimit),
AllMetadata: is.allMetadata, AllMetadata: is.allMetadata,
Platforms: platformsToProto(is.platforms), Platforms: platformsToProto(is.platforms),
Prefix: is.namePrefix, ExtraReferences: referencesToProto(is.extraReferences),
CheckPrefix: is.checkPrefix,
DigestRefs: is.digestRefs,
AlwaysDigest: is.alwaysDigest,
Unpacks: unpackToProto(is.unpacks), Unpacks: unpackToProto(is.unpacks),
} }
return typeurl.MarshalAny(s) return typeurl.MarshalAny(s)
@ -264,10 +367,7 @@ func (is *Store) UnmarshalAny(ctx context.Context, sm streaming.StreamGetter, a
is.manifestLimit = int(s.ManifestLimit) is.manifestLimit = int(s.ManifestLimit)
is.allMetadata = s.AllMetadata is.allMetadata = s.AllMetadata
is.platforms = platformFromProto(s.Platforms) is.platforms = platformFromProto(s.Platforms)
is.namePrefix = s.Prefix is.extraReferences = referencesFromProto(s.ExtraReferences)
is.checkPrefix = s.CheckPrefix
is.digestRefs = s.DigestRefs
is.alwaysDigest = s.AlwaysDigest
is.unpacks = unpackFromProto(s.Unpacks) is.unpacks = unpackFromProto(s.Unpacks)
return nil return nil
@ -297,6 +397,33 @@ func platformFromProto(platforms []*types.Platform) []ocispec.Platform {
return op return op
} }
func referencesToProto(references []Reference) []*transfertypes.ImageReference {
ir := make([]*transfertypes.ImageReference, len(references))
for i := range references {
r := transfertypes.ImageReference{
Name: references[i].Name,
IsPrefix: references[i].IsPrefix,
AllowOverwrite: references[i].AllowOverwrite,
AddDigest: references[i].AddDigest,
SkipNamedDigest: references[i].SkipNamedDigest,
}
ir[i] = &r
}
return ir
}
func referencesFromProto(references []*transfertypes.ImageReference) []Reference {
or := make([]Reference, len(references))
for i := range references {
or[i].Name = references[i].Name
or[i].IsPrefix = references[i].IsPrefix
or[i].AllowOverwrite = references[i].AllowOverwrite
or[i].AddDigest = references[i].AddDigest
or[i].SkipNamedDigest = references[i].SkipNamedDigest
}
return or
}
func unpackToProto(uc []UnpackConfiguration) []*transfertypes.UnpackConfiguration { func unpackToProto(uc []UnpackConfiguration) []*transfertypes.UnpackConfiguration {
auc := make([]*transfertypes.UnpackConfiguration, len(uc)) auc := make([]*transfertypes.UnpackConfiguration, len(uc))
for i := range uc { for i := range uc {
@ -326,15 +453,23 @@ func unpackFromProto(auc []*transfertypes.UnpackConfiguration) []UnpackConfigura
return uc return uc
} }
func imageName(annotations map[string]string, ociCleanup func(string) string) string { func imageName(annotations map[string]string, cleanup func(string) string) string {
name := annotations[images.AnnotationImageName] name := annotations[images.AnnotationImageName]
if name != "" { if name != "" {
if cleanup != nil {
// containerd reference name should be full reference and not
// modified, if it is incomplete or does not match a specified
// prefix, do not use the reference
if cleanName := cleanup(name); cleanName != name {
name = ""
}
}
return name return name
} }
name = annotations[ocispec.AnnotationRefName] name = annotations[ocispec.AnnotationRefName]
if name != "" { if name != "" {
if ociCleanup != nil { if cleanup != nil {
name = ociCleanup(name) name = cleanup(name)
} }
} }
return name return name

View File

@ -0,0 +1,275 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package image
import (
"context"
"errors"
"testing"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestStore(t *testing.T) {
for _, testCase := range []struct {
Name string
ImageStore *Store
// Annotations are the different references annotations to run the test with,
// the possible values:
// - "OCI": Uses the OCI defined annotation "org.opencontainers.image.ref.name"
// This annotation may be a full reference or tag only
// - "containerd": Uses the containerd defined annotation "io.containerd.image.name"
// This annotation is always a full reference as used by containerd
// - "Annotation": Sets the annotation flag but does not set a reference annotation
// Use this case to test the default where no reference is provided
// - "NoAnnotation": Does not set the annotation flag
// Use this case to test storing of the index images by reference
Annotations []string
ImageName string
Images []string
Err error
}{
{
Name: "Prefix",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/image",
IsPrefix: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/image:latest",
Images: []string{"registry.test/image:latest"},
},
{
Name: "Overwrite",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "placeholder",
IsPrefix: true,
AllowOverwrite: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/image:latest",
Images: []string{"registry.test/image:latest"},
},
{
Name: "TagOnly",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/image",
IsPrefix: true,
},
},
},
Annotations: []string{"OCI"},
ImageName: "latest",
Images: []string{"registry.test/image:latest"},
},
{
Name: "AddDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/base",
IsPrefix: true,
AddDigest: true,
},
},
},
Annotations: []string{"Annotation"},
Images: []string{"registry.test/base@"},
},
{
Name: "NameAndDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/base",
IsPrefix: true,
AddDigest: true,
},
},
},
Annotations: []string{"OCI"},
ImageName: "latest",
Images: []string{"registry.test/base:latest", "registry.test/base@"},
},
{
Name: "NameSkipDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/base",
IsPrefix: true,
AddDigest: true,
SkipNamedDigest: true,
},
},
},
Annotations: []string{"OCI"},
ImageName: "latest",
Images: []string{"registry.test/base:latest"},
},
{
Name: "OverwriteNameDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "base-name",
IsPrefix: true,
AllowOverwrite: true,
AddDigest: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/base:latest",
Images: []string{"registry.test/base:latest", "base-name@"},
},
{
Name: "OverwriteNameSkipDigest",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "base-name",
IsPrefix: true,
AllowOverwrite: true,
AddDigest: true,
SkipNamedDigest: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/base:latest",
Images: []string{"registry.test/base:latest"},
},
{
Name: "ReferenceNotFound",
ImageStore: &Store{
extraReferences: []Reference{
{
Name: "registry.test/image",
IsPrefix: true,
},
},
},
Annotations: []string{"OCI", "containerd"},
ImageName: "registry.test/base:latest",
Err: errdefs.ErrNotFound,
},
{
Name: "NoReference",
ImageStore: &Store{},
Annotations: []string{"Annotation", "NoAnnotation"},
Err: errdefs.ErrNotFound,
},
{
Name: "ImageName",
ImageStore: &Store{
imageName: "registry.test/index:latest",
},
Annotations: []string{"NoAnnotation"},
Images: []string{"registry.test/index:latest"},
},
} {
testCase := testCase
for _, a := range testCase.Annotations {
name := testCase.Name + "_" + a
dgst := digest.Canonical.FromString(name)
desc := ocispec.Descriptor{
Digest: dgst,
Annotations: map[string]string{},
}
expected := make([]string, len(testCase.Images))
for i, img := range testCase.Images {
if img[len(img)-1] == '@' {
img = img + dgst.String()
}
expected[i] = img
}
switch a {
case "containerd":
desc.Annotations["io.containerd.import.ref-source"] = "annotation"
desc.Annotations[images.AnnotationImageName] = testCase.ImageName
case "OCI":
desc.Annotations["io.containerd.import.ref-source"] = "annotation"
desc.Annotations[ocispec.AnnotationRefName] = testCase.ImageName
case "Annotation":
desc.Annotations["io.containerd.import.ref-source"] = "annotation"
}
t.Run(name, func(t *testing.T) {
imgs, err := testCase.ImageStore.Store(context.Background(), desc, nopImageStore{})
if err != nil {
if testCase.Err == nil {
t.Fatal(err)
}
if !errors.Is(err, testCase.Err) {
t.Fatalf("unexpected error %v: expeceted %v", err, testCase.Err)
}
return
} else if testCase.Err != nil {
t.Fatalf("succeeded but expected error: %v", testCase.Err)
}
if len(imgs) != len(expected) {
t.Fatalf("mismatched array length\nexpected:\n\t%v\nactual\n\t%v", expected, imgs)
}
for i, name := range expected {
if imgs[i].Name != name {
t.Fatalf("wrong image name %q, expected %q", imgs[i].Name, name)
}
if imgs[i].Target.Digest != dgst {
t.Fatalf("wrong image digest %s, expected %s", imgs[i].Target.Digest, dgst)
}
}
})
}
}
}
type nopImageStore struct{}
func (nopImageStore) Get(ctx context.Context, name string) (images.Image, error) {
return images.Image{}, errdefs.ErrNotFound
}
func (nopImageStore) List(ctx context.Context, filters ...string) ([]images.Image, error) {
return nil, nil
}
func (nopImageStore) Create(ctx context.Context, image images.Image) (images.Image, error) {
return image, nil
}
func (nopImageStore) Update(ctx context.Context, image images.Image, fieldpaths ...string) (images.Image, error) {
return image, nil
}
func (nopImageStore) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error {
return nil
}

View File

@ -23,6 +23,7 @@ import (
"github.com/containerd/containerd/content" "github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images" "github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/pkg/transfer" "github.com/containerd/containerd/pkg/transfer"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
@ -67,14 +68,8 @@ func (ts *localTransferService) importStream(ctx context.Context, i transfer.Ima
} }
for _, m := range idx.Manifests { for _, m := range idx.Manifests {
m1 := m m.Annotations = mergeMap(m.Annotations, map[string]string{"io.containerd.import.ref-source": "annotation"})
m1.Annotations = mergeMap(m.Annotations, map[string]string{"io.containerd.import.ref-type": "name"}) descriptors = append(descriptors, m)
descriptors = append(descriptors, m1)
// If add digest references, add twice
m2 := m
m2.Annotations = mergeMap(m.Annotations, map[string]string{"io.containerd.import.ref-type": "digest"})
descriptors = append(descriptors, m2)
} }
return idx.Manifests, nil return idx.Manifests, nil
@ -85,25 +80,29 @@ func (ts *localTransferService) importStream(ctx context.Context, i transfer.Ima
} }
if err := images.WalkNotEmpty(ctx, handler, index); err != nil { if err := images.WalkNotEmpty(ctx, handler, index); err != nil {
// TODO: Handle Not Empty as a special case on the input
return err return err
} }
for _, desc := range descriptors { for _, desc := range descriptors {
img, err := is.Store(ctx, desc, ts.images) imgs, err := is.Store(ctx, desc, ts.images)
if err != nil { if err != nil {
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
log.G(ctx).Infof("No images store for %s", desc.Digest)
continue continue
} }
return err return err
} }
if tops.Progress != nil { if tops.Progress != nil {
for _, img := range imgs {
tops.Progress(transfer.Progress{ tops.Progress(transfer.Progress{
Event: "saved", Event: "saved",
Name: img.Name, Name: img.Name,
}) })
} }
} }
}
if tops.Progress != nil { if tops.Progress != nil {
tops.Progress(transfer.Progress{ tops.Progress(transfer.Progress{

View File

@ -198,18 +198,19 @@ func (ts *localTransferService) pull(ctx context.Context, ir transfer.ImageFetch
} }
} }
img, err := is.Store(ctx, desc, ts.images) imgs, err := is.Store(ctx, desc, ts.images)
if err != nil { if err != nil {
return err return err
} }
if tops.Progress != nil { if tops.Progress != nil {
for _, img := range imgs {
tops.Progress(transfer.Progress{ tops.Progress(transfer.Progress{
Event: "saved", Event: "saved",
Name: img.Name, Name: img.Name,
//Digest: img.Target.Digest.String(),
}) })
} }
}
if tops.Progress != nil { if tops.Progress != nil {
tops.Progress(transfer.Progress{ tops.Progress(transfer.Progress{

View File

@ -57,10 +57,11 @@ type ImageFilterer interface {
ImageFilter(images.HandlerFunc, content.Store) images.HandlerFunc ImageFilter(images.HandlerFunc, content.Store) images.HandlerFunc
} }
// ImageStorer is a type which is capable of storing an image to // ImageStorer is a type which is capable of storing images for
// for a provided descriptor // the provided descriptor. The descriptor may be any type of manifest
// including an index with multiple image references.
type ImageStorer interface { type ImageStorer interface {
Store(context.Context, ocispec.Descriptor, images.Store) (images.Image, error) Store(context.Context, ocispec.Descriptor, images.Store) ([]images.Image, error)
} }
// ImageGetter is type which returns an image from an image store // ImageGetter is type which returns an image from an image store