From dbf384a5a857b635e0ea8b160645be81e60b3858 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Tue, 31 Jan 2023 22:26:27 +0900 Subject: [PATCH] Export remote snapshotter label handler Signed-off-by: Kohei Tokunaga --- pkg/cri/server/image_pull.go | 74 +-------------------- pkg/cri/server/image_pull_test.go | 48 -------------- pkg/snapshotters/annotations.go | 97 ++++++++++++++++++++++++++++ pkg/snapshotters/annotations_test.go | 74 +++++++++++++++++++++ 4 files changed, 173 insertions(+), 120 deletions(-) create mode 100644 pkg/snapshotters/annotations.go create mode 100644 pkg/snapshotters/annotations_test.go diff --git a/pkg/cri/server/image_pull.go b/pkg/cri/server/image_pull.go index b1b55ec74..5671672c8 100644 --- a/pkg/cri/server/image_pull.go +++ b/pkg/cri/server/image_pull.go @@ -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 diff --git a/pkg/cri/server/image_pull_test.go b/pkg/cri/server/image_pull_test.go index aa5c6ca32..428fb1cd4 100644 --- a/pkg/cri/server/image_pull_test.go +++ b/pkg/cri/server/image_pull_test.go @@ -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" diff --git a/pkg/snapshotters/annotations.go b/pkg/snapshotters/annotations.go new file mode 100644 index 000000000..c7ad97c15 --- /dev/null +++ b/pkg/snapshotters/annotations.go @@ -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 +} diff --git a/pkg/snapshotters/annotations_test.go b/pkg/snapshotters/annotations_test.go new file mode 100644 index 000000000..f972f8c72 --- /dev/null +++ b/pkg/snapshotters/annotations_test.go @@ -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) + }) + } +}