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/errdefs" | ||||
| 	containerdimages "github.com/containerd/containerd/images" | ||||
| 	"github.com/containerd/containerd/labels" | ||||
| 	"github.com/containerd/containerd/log" | ||||
| 	"github.com/containerd/containerd/pkg/cri/annotations" | ||||
| 	criconfig "github.com/containerd/containerd/pkg/cri/config" | ||||
| 	snpkg "github.com/containerd/containerd/pkg/snapshotters" | ||||
| 	distribution "github.com/containerd/containerd/reference/docker" | ||||
| 	"github.com/containerd/containerd/remotes/docker" | ||||
| 	"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()...) | ||||
| 	if !c.config.ContainerdConfig.DisableSnapshotAnnotations { | ||||
| 		pullOpts = append(pullOpts, | ||||
| 			containerd.WithImageHandlerWrapper(appendInfoHandlerWrapper(ref))) | ||||
| 			containerd.WithImageHandlerWrapper(snpkg.AppendInfoHandlerWrapper(ref))) | ||||
| 	} | ||||
|  | ||||
| 	if c.config.ContainerdConfig.DiscardUnpackedLayers { | ||||
| @@ -552,76 +552,6 @@ func (c *criService) encryptedImagesPullOpts() []containerd.RemoteOpt { | ||||
| 	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 ( | ||||
| 	// minPullProgressReportInternal is used to prevent the reporter from | ||||
| 	// eating more CPU resources | ||||
|   | ||||
| @@ -20,11 +20,8 @@ import ( | ||||
| 	"context" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	digest "github.com/opencontainers/go-digest" | ||||
| 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	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) { | ||||
| 	defaultSnashotter := "native" | ||||
| 	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