diff --git a/Dockerfile b/Dockerfile index 66931905fb5..079a0375b3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build ARG BUILDKIT_VERSION=v0.33.1@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY +ARG STARGZ_SNAPSHOTTER_VERSION=v0.19.0@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.3@3cd28929043ba2847633c32b806a6ccda8cd030f # Extra deps: Rootless diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 deleted file mode 100644 index e03654c4bac..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2 +++ /dev/null @@ -1,3 +0,0 @@ -515a3c3af0012f192ace31fb79e910597977c77227e976680aeaaef6e9ae50a9 stargz-snapshotter-v0.18.2-linux-amd64.tar.gz -97719faad48fb55c92a49abb9f12f9890dcd0d2da7215c4c21581f135b95abc9 stargz-snapshotter-v0.18.2-linux-arm64.tar.gz -f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.19.0 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.19.0 new file mode 100644 index 00000000000..9bcbf036f42 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.19.0 @@ -0,0 +1,3 @@ +696487c151b275bfc7818dcaba4eaadd40b8c4a8f87a60af919d8967f2e31ea1 stargz-snapshotter-v0.19.0-linux-amd64.tar.gz +7b2e32dca387b50c0a4930defa7925262cd7e271ab1e01b494fba337c534c918 stargz-snapshotter-v0.19.0-linux-arm64.tar.gz +f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service diff --git a/cmd/nerdctl/container/container_run_stargz_linux_test.go b/cmd/nerdctl/container/container_run_stargz_linux_test.go index 3298a492130..caa7ced41bb 100644 --- a/cmd/nerdctl/container/container_run_stargz_linux_test.go +++ b/cmd/nerdctl/container/container_run_stargz_linux_test.go @@ -17,13 +17,37 @@ package container import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" "testing" + digest "github.com/opencontainers/go-digest" + imagespecversioned "github.com/opencontainers/image-spec/specs-go" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "gotest.tools/v3/assert" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/defaults" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/platforms" + "github.com/containerd/stargz-snapshotter/estargz" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestRunStargz(t *testing.T) { @@ -41,3 +65,148 @@ func TestRunStargz(t *testing.T) { testCase.Run(t) } + +// createInvalidESGZImage creates an eStargz image from the first layer of srcRef (an eStargz image) with an extra +// file added to the layer, while keeping the original image config. As the diffID in the config (the one of srcRef) +// doesn't match the modified layer, the resulting image has the same ChainID as srcRef but different contents and +// TOC digest. The image is stored in containerd as newRef. +func createInvalidESGZImage(ctx context.Context, t tig.T, client *containerd.Client, srcRef, newRef string) { + cs := client.ContentStore() + srcImg, err := client.GetImage(ctx, srcRef) + assert.NilError(t, err) + srcManifest, err := images.Manifest(ctx, cs, srcImg.Target(), platforms.Default()) + assert.NilError(t, err) + layer, err := content.ReadBlob(ctx, cs, srcManifest.Layers[0]) + assert.NilError(t, err) + + var modified bytes.Buffer + tw := tar.NewWriter(&modified) + gz, err := gzip.NewReader(bytes.NewReader(layer)) + assert.NilError(t, err) + tr := tar.NewReader(gz) + for { + h, err := tr.Next() + if err == io.EOF { + break + } + assert.NilError(t, err) + assert.NilError(t, tw.WriteHeader(h)) + _, err = io.Copy(tw, tr) + assert.NilError(t, err) + } + contents := "MODIFIED" + assert.NilError(t, tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "/modified.txt", Size: int64(len(contents))})) + _, err = tw.Write([]byte(contents)) + assert.NilError(t, err) + assert.NilError(t, tw.Close()) + + modifiedData := modified.Bytes() + esgz, err := estargz.Build(io.NewSectionReader(bytes.NewReader(modifiedData), 0, int64(len(modifiedData)))) + assert.NilError(t, err) + defer esgz.Close() + layerData, err := io.ReadAll(esgz) + assert.NilError(t, err) + layerDesc := imagespec.Descriptor{ + MediaType: imagespec.MediaTypeImageLayerGzip, + Digest: digest.FromBytes(layerData), + Size: int64(len(layerData)), + Annotations: map[string]string{estargz.TOCJSONDigestAnnotation: esgz.TOCDigest().String()}, + } + assert.NilError(t, content.WriteBlob(ctx, cs, "layer-invalid", bytes.NewReader(layerData), layerDesc)) + + srcConfig, err := images.Config(ctx, cs, srcImg.Target(), platforms.Default()) + assert.NilError(t, err) + manifest := &imagespec.Manifest{ + Versioned: imagespecversioned.Versioned{SchemaVersion: 2}, + MediaType: imagespec.MediaTypeImageManifest, + Config: srcConfig, + Layers: []imagespec.Descriptor{layerDesc}, + } + manifestBlob, err := json.Marshal(manifest) + assert.NilError(t, err) + manifestDesc := imagespec.Descriptor{ + MediaType: imagespec.MediaTypeImageManifest, + Digest: digest.FromBytes(manifestBlob), + Size: int64(len(manifestBlob)), + } + labels := map[string]string{} + for i, d := range []imagespec.Descriptor{layerDesc, srcConfig} { + labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = d.Digest.String() + } + assert.NilError(t, content.WriteBlob(ctx, cs, "manifest-invalid", bytes.NewReader(manifestBlob), manifestDesc, content.WithLabels(labels))) + _, err = client.ImageService().Create(ctx, images.Image{Name: newRef, Target: manifestDesc}) + assert.NilError(t, err) +} + +func newTestContainerdClient(helpers test.Helpers) *containerd.Client { + addr := defaults.DefaultAddress + if rootlessutil.IsRootless() { + stateDir, err := rootlessutil.RootlessKitStateDir() + assert.NilError(helpers.T(), err) + childPid, err := rootlessutil.RootlessKitChildPid(stateDir) + assert.NilError(helpers.T(), err) + addr = filepath.Join("/proc", fmt.Sprintf("%d", childPid), "root", defaults.DefaultAddress) + } + client, err := containerd.New(addr, containerd.WithDefaultNamespace(string(helpers.Read(nerdtest.Namespace)))) + assert.NilError(helpers.T(), err) + return client +} + +func TestPullStargzInvalidChainID(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Require = require.All( + nerdtest.Stargz, + nerdtest.Registry, + require.Amd64, + require.Not(nerdtest.Docker), + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + reg.Setup(data, helpers) + base := fmt.Sprintf("127.0.0.1:%d/alpine", reg.Port) + esgz := base + ":esgz" + invalid := base + ":esgz-invalid" + + // A valid eStargz image + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + helpers.Ensure("image", "convert", "--estargz", "--oci", testutil.AlpineImage, esgz) + helpers.Ensure("push", esgz) + + // An eStargz image whose layer doesn't match the diffID in the config + client := newTestContainerdClient(helpers) + defer client.Close() + createInvalidESGZImage(context.Background(), helpers.T(), client, esgz, invalid) + helpers.Ensure("push", invalid) + + // Remove local copies so that the following pulls fetch from the registry + helpers.Ensure("rmi", "-f", esgz, invalid) + + data.Labels().Set("esgz", esgz) + data.Labels().Set("invalid", invalid) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if v := data.Labels().Get("esgz"); v != "" { + helpers.Anyhow("rmi", "-f", v) + } + if v := data.Labels().Get("invalid"); v != "" { + helpers.Anyhow("rmi", "-f", v) + } + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("--snapshotter=stargz", "pull", data.Labels().Get("invalid")) + return helpers.Command("--snapshotter=stargz", "pull", data.Labels().Get("esgz")) + } + + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, []error{errors.New("existing images contain invalid ChainIDs")}, nil) + + testCase.Run(t) +} diff --git a/go.mod b/go.mod index 9ac9569fe8f..13b25596754 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.12.0 //gomodjail:unconfined - github.com/containerd/containerd/v2 v2.4.1 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.4.2 //gomodjail:unconfined github.com/containerd/continuity v0.5.0 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -20,9 +20,9 @@ require ( github.com/containerd/nerdctl/mod/tigron v0.0.0 //gomodjail:unconfined github.com/containerd/nydus-snapshotter v0.16.1 //gomodjail:unconfined github.com/containerd/platforms v1.0.0-rc.5 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined - github.com/containerd/stargz-snapshotter/ipfs v0.18.2 //gomodjail:unconfined + github.com/containerd/stargz-snapshotter v0.19.1-0.20261021040618-950b4ccca10e //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/estargz v0.19.1-0.20261021040618-950b4ccca10e //gomodjail:unconfined + github.com/containerd/stargz-snapshotter/ipfs v0.19.1-0.20261021040618-950b4ccca10e //gomodjail:unconfined github.com/containerd/typeurl/v2 v2.3.0 github.com/docker/cli v29.8.2+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.8.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index ce8b14930a3..0605944858f 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/q github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.12.0 h1:kuQm82SbDrCuO4n7hf2L8zsBtZLuympyq5X/VotfX2A= github.com/containerd/containerd/api v1.12.0/go.mod h1:EBcSzoi9Vl18cdODaXUCskf3D2NT8lsSXeZJnU5jIUc= -github.com/containerd/containerd/v2 v2.4.1 h1:DUx/ZJN7cEu0WuzHClDB+68H/bqMEH5pWoEjf0ae4hc= -github.com/containerd/containerd/v2 v2.4.1/go.mod h1:vgLdtvl3prFk1d3ZVqQcsDD5Q9IKM+vAIdU54U85w+I= +github.com/containerd/containerd/v2 v2.4.2 h1:p6Qg+xbEz8lvwdhHDx0U469xENvp4LgGBx+E7djXkGw= +github.com/containerd/containerd/v2 v2.4.2/go.mod h1:vgLdtvl3prFk1d3ZVqQcsDD5Q9IKM+vAIdU54U85w+I= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -56,12 +56,12 @@ github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8o github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= -github.com/containerd/stargz-snapshotter v0.18.2 h1:Ev/sxfQUjwzJQ9eqy3XzttcQ3osMIqkQgMYlcET+10M= -github.com/containerd/stargz-snapshotter v0.18.2/go.mod h1:iS0a4lgCFjGbdBJNrm1jwvaMFGGnQ6PZ5Sd09i060h8= -github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= -github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= -github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+XesH/6BBuJcdtV6ymGlGg= -github.com/containerd/stargz-snapshotter/ipfs v0.18.2/go.mod h1:a7cHDMpUfj5f3aZ3/50jEyapRZGjyaOKSDgFH4eiEnE= +github.com/containerd/stargz-snapshotter v0.19.1-0.20261021040618-950b4ccca10e h1:h89FAJsBnxeMRtImuMvMcS+kH2Q6xkKScKvu1QWzJlg= +github.com/containerd/stargz-snapshotter v0.19.1-0.20261021040618-950b4ccca10e/go.mod h1:WeMcYgz863cTxQmdteTOZk7HqCVTCZqsdZXVnryiWtk= +github.com/containerd/stargz-snapshotter/estargz v0.19.1-0.20261021040618-950b4ccca10e h1:hk8nOG35A7gWIgQLWg++yryownNnijPcpoj1lcpGDRQ= +github.com/containerd/stargz-snapshotter/estargz v0.19.1-0.20261021040618-950b4ccca10e/go.mod h1:Cr0pxOkuN7Y61+Sik6X6/nPtePW7hsKWdBt+MoHQRr0= +github.com/containerd/stargz-snapshotter/ipfs v0.19.1-0.20261021040618-950b4ccca10e h1:XAet8hk+0IYgPHGws2bcewH4CssgkLbXSlcm+kMQ61I= +github.com/containerd/stargz-snapshotter/ipfs v0.19.1-0.20261021040618-950b4ccca10e/go.mod h1:TyzpPN7LJWyGNbGGN/fK5+mZOK2nC/JU5ouivJ7g4iI= github.com/containerd/ttrpc v1.2.10 h1:qQvy3mokhoXON7FXiOOc05O7Up6nrNj6amJF8urNvDc= github.com/containerd/ttrpc v1.2.10/go.mod h1:YrmEQKkMbBA4EM26AzVAIwHGIYgptkL4CJ/yVTaDyBE= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go index 227f1dbd37b..1b505b376c0 100644 --- a/pkg/imgutil/imgutil.go +++ b/pkg/imgutil/imgutil.go @@ -246,6 +246,11 @@ func PullImage(ctx context.Context, client *containerd.Client, resolver remotes. // different remote snapshotters will update pull.Config separately snOpt.apply(config, ref, options.RFlags) + if snOpt.needsVerify() { + if err := snOpt.verify(ctx, client, ref, resolver); err != nil { + return nil, fmt.Errorf("snapshot verifier returned an error: %w", err) + } + } } else { log.G(ctx).Debugf("The image will not be unpacked. Platforms=%v.", options.OCISpecPlatform) } diff --git a/pkg/imgutil/snapshotter.go b/pkg/imgutil/snapshotter.go index 15f63e7c00e..95c59179f31 100644 --- a/pkg/imgutil/snapshotter.go +++ b/pkg/imgutil/snapshotter.go @@ -17,10 +17,12 @@ package imgutil import ( + "context" "strings" containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/remotes" ctdsnapshotters "github.com/containerd/containerd/v2/pkg/snapshotters" "github.com/containerd/log" "github.com/containerd/stargz-snapshotter/fs/source" @@ -44,7 +46,7 @@ const ( // remote snapshotters explicitly handled by nerdctl var builtinRemoteSnapshotterOpts = map[string]snapshotterOpts{ snapshotterNameOverlaybd: &remoteSnapshotterOpts{snapshotter: "overlaybd"}, - snapshotterNameStargz: &remoteSnapshotterOpts{snapshotter: "stargz", extraLabels: stargzExtraLabels}, + snapshotterNameStargz: &remoteSnapshotterOpts{snapshotter: "stargz", extraLabels: stargzExtraLabels, verifier: stargzVerifier}, snapshotterNameNydus: &remoteSnapshotterOpts{snapshotter: "nydus"}, snapshotterNameSoci: &remoteSnapshotterOpts{snapshotter: "soci", extraLabels: sociExtraLabels}, snapshotterNameCvmfs: &remoteSnapshotterOpts{snapshotter: "cvmfs-snapshotter"}, @@ -55,6 +57,8 @@ var builtinRemoteSnapshotterOpts = map[string]snapshotterOpts{ type snapshotterOpts interface { apply(config *pull.Config, ref string, rFlags types.RemoteSnapshotterFlags) isRemote() bool + verify(ctx context.Context, client *containerd.Client, ref string, resolver remotes.Resolver) error + needsVerify() bool } // getSnapshotterOpts get snapshotter opts by fuzzy matching of the snapshotter name @@ -76,6 +80,7 @@ func getSnapshotterOpts(snapshotter string) snapshotterOpts { type remoteSnapshotterOpts struct { snapshotter string extraLabels func(func(images.Handler) images.Handler, types.RemoteSnapshotterFlags) func(images.Handler) images.Handler + verifier func(ctx context.Context, client *containerd.Client, ref string, resolver remotes.Resolver) error } func (rs *remoteSnapshotterOpts) isRemote() bool { @@ -94,6 +99,17 @@ func (rs *remoteSnapshotterOpts) apply(config *pull.Config, ref string, rFlags t ) } +func (rs *remoteSnapshotterOpts) verify(ctx context.Context, client *containerd.Client, ref string, resolver remotes.Resolver) error { + if rs.verifier != nil { + return rs.verifier(ctx, client, ref, resolver) + } + return nil +} + +func (rs *remoteSnapshotterOpts) needsVerify() bool { + return rs.verifier != nil +} + // defaultSnapshotterOpts is for snapshotters that // not handled separately type defaultSnapshotterOpts struct { @@ -111,6 +127,14 @@ func (dsn *defaultSnapshotterOpts) isRemote() bool { return false } +func (dsn *defaultSnapshotterOpts) verify(ctx context.Context, client *containerd.Client, ref string, resolver remotes.Resolver) error { + return nil +} + +func (dsn *defaultSnapshotterOpts) needsVerify() bool { + return false +} + func stargzExtraLabels(f func(images.Handler) images.Handler, rFlags types.RemoteSnapshotterFlags) func(images.Handler) images.Handler { return source.AppendExtraLabelsHandler(prefetchSize, f) } diff --git a/pkg/imgutil/stargz_verifier_linux.go b/pkg/imgutil/stargz_verifier_linux.go new file mode 100644 index 00000000000..19437564bd1 --- /dev/null +++ b/pkg/imgutil/stargz_verifier_linux.go @@ -0,0 +1,39 @@ +/* + 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 imgutil + +import ( + "context" + "fmt" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/remotes" + "github.com/containerd/stargz-snapshotter/service/verifier" + + "github.com/containerd/nerdctl/v2/pkg/referenceutil" +) + +// stargzVerifier normalizes ref (e.g., adds the default tag) because the underlying +// verifier requires a tag or a digest. It is linux-only: stargz-snapshotter's service package depends on +// linux-only containerd packages (e.g., overlayutils). +func stargzVerifier(ctx context.Context, client *containerd.Client, ref string, resolver remotes.Resolver) error { + parsed, err := referenceutil.Parse(ref) + if err != nil { + return fmt.Errorf("failed to parse image reference %q: %w", ref, err) + } + return verifier.VerifyImage(ctx, client, parsed.String(), resolver) +} diff --git a/pkg/imgutil/stargz_verifier_other.go b/pkg/imgutil/stargz_verifier_other.go new file mode 100644 index 00000000000..46363d9f01d --- /dev/null +++ b/pkg/imgutil/stargz_verifier_other.go @@ -0,0 +1,29 @@ +//go:build !linux + +/* + 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 imgutil + +import ( + "context" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/remotes" +) + +// stargzVerifier is nil on non-Linux platforms (stargz-snapshotter's verifier is linux-only). +var stargzVerifier func(ctx context.Context, client *containerd.Client, ref string, resolver remotes.Resolver) error diff --git a/pkg/imgutil/transfer.go b/pkg/imgutil/transfer.go index 86c5aee5dba..4c6b523ddfe 100644 --- a/pkg/imgutil/transfer.go +++ b/pkg/imgutil/transfer.go @@ -125,6 +125,38 @@ func PullImageWithTransfer(ctx context.Context, client *containerd.Client, parse } defer cleanup() + snapshotter := options.GOptions.Snapshotter + snOpt := getSnapshotterOpts(snapshotter) + if snOpt.needsVerify() { + var dOpts []dockerconfigresolver.Opt + if options.GOptions.InsecureRegistry { + log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", parsedReference.Domain) + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir)) + resolver, err := dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...) + if err != nil { + return nil, err + } + if err := snOpt.verify(ctx, client, rawRef, resolver); err != nil { + if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) { + return nil, fmt.Errorf("snapshot verifier returned an error: %w", err) + } + if !options.GOptions.InsecureRegistry { + return nil, fmt.Errorf("server doesn't seem to support HTTPS: %w", err) + } + log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain) + dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) + resolver, err = dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...) + if err != nil { + return nil, err + } + if err := snOpt.verify(ctx, client, rawRef, resolver); err != nil { + return nil, fmt.Errorf("snapshot verifier returned an error: %w", err) + } + } + } + transferErr := doTransfer(ctx, client, fetcher, store, options.Quiet, progressWriter) if transferErr != nil && (errors.Is(transferErr, http.ErrSchemeMismatch) || errutil.IsErrConnectionRefused(transferErr) || errutil.IsErrHTTPResponseToHTTPSClient(transferErr) || errutil.IsErrTLSHandshakeFailure(transferErr)) { @@ -156,9 +188,6 @@ func PullImageWithTransfer(ctx context.Context, client *containerd.Client, parse return nil, err } - snapshotter := options.GOptions.Snapshotter - snOpt := getSnapshotterOpts(snapshotter) - return &EnsuredImage{ Ref: rawRef, Image: containerdImage,