Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.18.2

This file was deleted.

3 changes: 3 additions & 0 deletions Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.19.0
Original file line number Diff line number Diff line change
@@ -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
169 changes: 169 additions & 0 deletions cmd/nerdctl/container/container_run_stargz_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this helper function is useful for non-stargz tests, it should be moved out of *_stargz_*_test.go

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)
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ktock Can you release v0.19.1 ?

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
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
5 changes: 5 additions & 0 deletions pkg/imgutil/imgutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
26 changes: 25 additions & 1 deletion pkg/imgutil/snapshotter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"},
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
39 changes: 39 additions & 0 deletions pkg/imgutil/stargz_verifier_linux.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading