Export remote snapshotter label handler
Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
This commit is contained in:
		| @@ -42,10 +42,10 @@ import ( | |||||||
| 	"github.com/containerd/containerd" | 	"github.com/containerd/containerd" | ||||||
| 	"github.com/containerd/containerd/errdefs" | 	"github.com/containerd/containerd/errdefs" | ||||||
| 	containerdimages "github.com/containerd/containerd/images" | 	containerdimages "github.com/containerd/containerd/images" | ||||||
| 	"github.com/containerd/containerd/labels" |  | ||||||
| 	"github.com/containerd/containerd/log" | 	"github.com/containerd/containerd/log" | ||||||
| 	"github.com/containerd/containerd/pkg/cri/annotations" | 	"github.com/containerd/containerd/pkg/cri/annotations" | ||||||
| 	criconfig "github.com/containerd/containerd/pkg/cri/config" | 	criconfig "github.com/containerd/containerd/pkg/cri/config" | ||||||
|  | 	snpkg "github.com/containerd/containerd/pkg/snapshotters" | ||||||
| 	distribution "github.com/containerd/containerd/reference/docker" | 	distribution "github.com/containerd/containerd/reference/docker" | ||||||
| 	"github.com/containerd/containerd/remotes/docker" | 	"github.com/containerd/containerd/remotes/docker" | ||||||
| 	"github.com/containerd/containerd/remotes/docker/config" | 	"github.com/containerd/containerd/remotes/docker/config" | ||||||
| @@ -170,7 +170,7 @@ func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest) | |||||||
| 	pullOpts = append(pullOpts, c.encryptedImagesPullOpts()...) | 	pullOpts = append(pullOpts, c.encryptedImagesPullOpts()...) | ||||||
| 	if !c.config.ContainerdConfig.DisableSnapshotAnnotations { | 	if !c.config.ContainerdConfig.DisableSnapshotAnnotations { | ||||||
| 		pullOpts = append(pullOpts, | 		pullOpts = append(pullOpts, | ||||||
| 			containerd.WithImageHandlerWrapper(appendInfoHandlerWrapper(ref))) | 			containerd.WithImageHandlerWrapper(snpkg.AppendInfoHandlerWrapper(ref))) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if c.config.ContainerdConfig.DiscardUnpackedLayers { | 	if c.config.ContainerdConfig.DiscardUnpackedLayers { | ||||||
| @@ -552,76 +552,6 @@ func (c *criService) encryptedImagesPullOpts() []containerd.RemoteOpt { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	// targetRefLabel is a label which contains image reference and will be passed |  | ||||||
| 	// to snapshotters. |  | ||||||
| 	targetRefLabel = "containerd.io/snapshot/cri.image-ref" |  | ||||||
| 	// targetManifestDigestLabel is a label which contains manifest digest and will be passed |  | ||||||
| 	// to snapshotters. |  | ||||||
| 	targetManifestDigestLabel = "containerd.io/snapshot/cri.manifest-digest" |  | ||||||
| 	// targetLayerDigestLabel is a label which contains layer digest and will be passed |  | ||||||
| 	// to snapshotters. |  | ||||||
| 	targetLayerDigestLabel = "containerd.io/snapshot/cri.layer-digest" |  | ||||||
| 	// targetImageLayersLabel is a label which contains layer digests contained in |  | ||||||
| 	// the target image and will be passed to snapshotters for preparing layers in |  | ||||||
| 	// parallel. Skipping some layers is allowed and only affects performance. |  | ||||||
| 	targetImageLayersLabel = "containerd.io/snapshot/cri.image-layers" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // appendInfoHandlerWrapper makes a handler which appends some basic information |  | ||||||
| // of images like digests for manifest and their child layers as annotations during unpack. |  | ||||||
| // These annotations will be passed to snapshotters as labels. These labels will be |  | ||||||
| // used mainly by stargz-based snapshotters for querying image contents from the |  | ||||||
| // registry. |  | ||||||
| func appendInfoHandlerWrapper(ref string) func(f containerdimages.Handler) containerdimages.Handler { |  | ||||||
| 	return func(f containerdimages.Handler) containerdimages.Handler { |  | ||||||
| 		return containerdimages.HandlerFunc(func(ctx context.Context, desc imagespec.Descriptor) ([]imagespec.Descriptor, error) { |  | ||||||
| 			children, err := f.Handle(ctx, desc) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, err |  | ||||||
| 			} |  | ||||||
| 			switch desc.MediaType { |  | ||||||
| 			case imagespec.MediaTypeImageManifest, containerdimages.MediaTypeDockerSchema2Manifest: |  | ||||||
| 				for i := range children { |  | ||||||
| 					c := &children[i] |  | ||||||
| 					if containerdimages.IsLayerType(c.MediaType) { |  | ||||||
| 						if c.Annotations == nil { |  | ||||||
| 							c.Annotations = make(map[string]string) |  | ||||||
| 						} |  | ||||||
| 						c.Annotations[targetRefLabel] = ref |  | ||||||
| 						c.Annotations[targetLayerDigestLabel] = c.Digest.String() |  | ||||||
| 						c.Annotations[targetImageLayersLabel] = getLayers(ctx, targetImageLayersLabel, children[i:], labels.Validate) |  | ||||||
| 						c.Annotations[targetManifestDigestLabel] = desc.Digest.String() |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			return children, nil |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // getLayers returns comma-separated digests based on the passed list of |  | ||||||
| // descriptors. The returned list contains as many digests as possible as well |  | ||||||
| // as meets the label validation. |  | ||||||
| func getLayers(ctx context.Context, key string, descs []imagespec.Descriptor, validate func(k, v string) error) (layers string) { |  | ||||||
| 	var item string |  | ||||||
| 	for _, l := range descs { |  | ||||||
| 		if containerdimages.IsLayerType(l.MediaType) { |  | ||||||
| 			item = l.Digest.String() |  | ||||||
| 			if layers != "" { |  | ||||||
| 				item = "," + item |  | ||||||
| 			} |  | ||||||
| 			// This avoids the label hits the size limitation. |  | ||||||
| 			if err := validate(key, layers+item); err != nil { |  | ||||||
| 				log.G(ctx).WithError(err).WithField("label", key).Debugf("%q is omitted in the layers list", l.Digest.String()) |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 			layers += item |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	// minPullProgressReportInternal is used to prevent the reporter from | 	// minPullProgressReportInternal is used to prevent the reporter from | ||||||
| 	// eating more CPU resources | 	// eating more CPU resources | ||||||
|   | |||||||
| @@ -20,11 +20,8 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	digest "github.com/opencontainers/go-digest" |  | ||||||
| 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	runtime "k8s.io/cri-api/pkg/apis/runtime/v1" | 	runtime "k8s.io/cri-api/pkg/apis/runtime/v1" | ||||||
|  |  | ||||||
| @@ -338,51 +335,6 @@ func TestEncryptedImagePullOpts(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestImageLayersLabel(t *testing.T) { |  | ||||||
| 	sampleKey := "sampleKey" |  | ||||||
| 	sampleDigest, err := digest.Parse("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	sampleMaxSize := 300 |  | ||||||
| 	sampleValidate := func(k, v string) error { |  | ||||||
| 		if (len(k) + len(v)) > sampleMaxSize { |  | ||||||
| 			return fmt.Errorf("invalid: %q: %q", k, v) |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name      string |  | ||||||
| 		layersNum int |  | ||||||
| 		wantNum   int |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:      "valid number of layers", |  | ||||||
| 			layersNum: 2, |  | ||||||
| 			wantNum:   2, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:      "many layers", |  | ||||||
| 			layersNum: 5, // hits sampleMaxSize (300 chars). |  | ||||||
| 			wantNum:   4, // layers should be omitted for avoiding invalid label. |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			var sampleLayers []imagespec.Descriptor |  | ||||||
| 			for i := 0; i < tt.layersNum; i++ { |  | ||||||
| 				sampleLayers = append(sampleLayers, imagespec.Descriptor{ |  | ||||||
| 					MediaType: imagespec.MediaTypeImageLayerGzip, |  | ||||||
| 					Digest:    sampleDigest, |  | ||||||
| 				}) |  | ||||||
| 			} |  | ||||||
| 			gotS := getLayers(context.Background(), sampleKey, sampleLayers, sampleValidate) |  | ||||||
| 			got := len(strings.Split(gotS, ",")) |  | ||||||
| 			assert.Equal(t, tt.wantNum, got) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestSnapshotterFromPodSandboxConfig(t *testing.T) { | func TestSnapshotterFromPodSandboxConfig(t *testing.T) { | ||||||
| 	defaultSnashotter := "native" | 	defaultSnashotter := "native" | ||||||
| 	runtimeSnapshotter := "devmapper" | 	runtimeSnapshotter := "devmapper" | ||||||
|   | |||||||
							
								
								
									
										97
									
								
								pkg/snapshotters/annotations.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								pkg/snapshotters/annotations.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | /* | ||||||
|  |    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 snapshotters | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  |  | ||||||
|  | 	"github.com/containerd/containerd/images" | ||||||
|  | 	"github.com/containerd/containerd/labels" | ||||||
|  | 	"github.com/containerd/containerd/log" | ||||||
|  | 	ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NOTE: The following labels contain "cri" prefix but they are not specific to CRI and | ||||||
|  | // can be used by non-CRI clients as well for enabling remote snapshotters. We need to | ||||||
|  | // retain that string for keeping compatibility with snapshotter implementations. | ||||||
|  | const ( | ||||||
|  | 	// TargetRefLabel is a label which contains image reference and will be passed | ||||||
|  | 	// to snapshotters. | ||||||
|  | 	TargetRefLabel = "containerd.io/snapshot/cri.image-ref" | ||||||
|  | 	// TargetManifestDigestLabel is a label which contains manifest digest and will be passed | ||||||
|  | 	// to snapshotters. | ||||||
|  | 	TargetManifestDigestLabel = "containerd.io/snapshot/cri.manifest-digest" | ||||||
|  | 	// TargetLayerDigestLabel is a label which contains layer digest and will be passed | ||||||
|  | 	// to snapshotters. | ||||||
|  | 	TargetLayerDigestLabel = "containerd.io/snapshot/cri.layer-digest" | ||||||
|  | 	// TargetImageLayersLabel is a label which contains layer digests contained in | ||||||
|  | 	// the target image and will be passed to snapshotters for preparing layers in | ||||||
|  | 	// parallel. Skipping some layers is allowed and only affects performance. | ||||||
|  | 	TargetImageLayersLabel = "containerd.io/snapshot/cri.image-layers" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // AppendInfoHandlerWrapper makes a handler which appends some basic information | ||||||
|  | // of images like digests for manifest and their child layers as annotations during unpack. | ||||||
|  | // These annotations will be passed to snapshotters as labels. These labels will be | ||||||
|  | // used mainly by remote snapshotters for querying image contents from the remote location. | ||||||
|  | func AppendInfoHandlerWrapper(ref string) func(f images.Handler) images.Handler { | ||||||
|  | 	return func(f images.Handler) images.Handler { | ||||||
|  | 		return images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { | ||||||
|  | 			children, err := f.Handle(ctx, desc) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			switch desc.MediaType { | ||||||
|  | 			case ocispec.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest: | ||||||
|  | 				for i := range children { | ||||||
|  | 					c := &children[i] | ||||||
|  | 					if images.IsLayerType(c.MediaType) { | ||||||
|  | 						if c.Annotations == nil { | ||||||
|  | 							c.Annotations = make(map[string]string) | ||||||
|  | 						} | ||||||
|  | 						c.Annotations[TargetRefLabel] = ref | ||||||
|  | 						c.Annotations[TargetLayerDigestLabel] = c.Digest.String() | ||||||
|  | 						c.Annotations[TargetImageLayersLabel] = getLayers(ctx, TargetImageLayersLabel, children[i:], labels.Validate) | ||||||
|  | 						c.Annotations[TargetManifestDigestLabel] = desc.Digest.String() | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			return children, nil | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getLayers returns comma-separated digests based on the passed list of | ||||||
|  | // descriptors. The returned list contains as many digests as possible as well | ||||||
|  | // as meets the label validation. | ||||||
|  | func getLayers(ctx context.Context, key string, descs []ocispec.Descriptor, validate func(k, v string) error) (layers string) { | ||||||
|  | 	for _, l := range descs { | ||||||
|  | 		if images.IsLayerType(l.MediaType) { | ||||||
|  | 			item := l.Digest.String() | ||||||
|  | 			if layers != "" { | ||||||
|  | 				item = "," + item | ||||||
|  | 			} | ||||||
|  | 			// This avoids the label hits the size limitation. | ||||||
|  | 			if err := validate(key, layers+item); err != nil { | ||||||
|  | 				log.G(ctx).WithError(err).WithField("label", key).WithField("digest", l.Digest.String()).Debug("omitting digest in the layers list") | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			layers += item | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								pkg/snapshotters/annotations_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								pkg/snapshotters/annotations_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | /* | ||||||
|  |    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 snapshotters | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	digest "github.com/opencontainers/go-digest" | ||||||
|  | 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestImageLayersLabel(t *testing.T) { | ||||||
|  | 	sampleKey := "sampleKey" | ||||||
|  | 	sampleDigest, err := digest.Parse("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	sampleMaxSize := 300 | ||||||
|  | 	sampleValidate := func(k, v string) error { | ||||||
|  | 		if (len(k) + len(v)) > sampleMaxSize { | ||||||
|  | 			return fmt.Errorf("invalid: %q: %q", k, v) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name      string | ||||||
|  | 		layersNum int | ||||||
|  | 		wantNum   int | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:      "valid number of layers", | ||||||
|  | 			layersNum: 2, | ||||||
|  | 			wantNum:   2, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:      "many layers", | ||||||
|  | 			layersNum: 5, // hits sampleMaxSize (300 chars). | ||||||
|  | 			wantNum:   4, // layers should be omitted for avoiding invalid label. | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		tt := tt | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			sampleLayers := make([]imagespec.Descriptor, 0, tt.layersNum) | ||||||
|  | 			for i := 0; i < tt.layersNum; i++ { | ||||||
|  | 				sampleLayers = append(sampleLayers, imagespec.Descriptor{ | ||||||
|  | 					MediaType: imagespec.MediaTypeImageLayerGzip, | ||||||
|  | 					Digest:    sampleDigest, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 			gotS := getLayers(context.Background(), sampleKey, sampleLayers, sampleValidate) | ||||||
|  | 			got := len(strings.Split(gotS, ",")) | ||||||
|  | 			assert.Equal(t, tt.wantNum, got) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Kohei Tokunaga
					Kohei Tokunaga