Skip to content

Commit c47323f

Browse files
author
Kern Walster
committed
Use default platform in convert
Before this change, the SOCI cli defaulted to converting all platforms. This is inconsistent with nerdctl's pull, push, and convert functions. It also leads to weird issues like this not working: ``` nerdctl pull docker.io/library/busybox:latest soci convert docker.io/library/busybox:latest other:latest-soci ``` the solution is either to pull with `--all-platforms` or convert with `--platform`. The latter had an additional bug that convert touched all manifests, even if you didn't convert them. This change makes SOCI default to single platform conversions and fixes the manifests bug. SOCI convert now behaves like nerdctl pull/convert/push. Signed-off-by: Kern Walster <walster@amazon.com>
1 parent 8bbfe95 commit c47323f

8 files changed

Lines changed: 164 additions & 34 deletions

File tree

cmd/soci/commands/convert.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727
"github.com/containerd/containerd/errdefs"
2828
"github.com/containerd/containerd/images"
2929
"github.com/containerd/containerd/reference"
30-
v1 "github.com/opencontainers/image-spec/specs-go/v1"
3130
"github.com/urfave/cli"
3231
)
3332

@@ -147,13 +146,7 @@ var ConvertCommand = cli.Command{
147146
}
148147
defer done(ctx)
149148

150-
var platforms []v1.Platform
151-
explicitPlatforms := cliContext.StringSlice(internal.PlatformFlagKey)
152-
if len(explicitPlatforms) > 0 {
153-
platforms, err = internal.GetPlatforms(ctx, cliContext, srcImg, cs)
154-
} else {
155-
platforms, err = images.Platforms(ctx, cs, srcImg.Target)
156-
}
149+
platforms, err := internal.GetPlatforms(ctx, cliContext, srcImg, cs)
157150
if err != nil {
158151
return err
159152
}

cmd/soci/commands/create.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/awslabs/soci-snapshotter/cmd/soci/commands/internal"
2424
"github.com/awslabs/soci-snapshotter/soci"
2525
"github.com/awslabs/soci-snapshotter/soci/store"
26+
"github.com/containerd/platforms"
2627
"github.com/urfave/cli"
2728
)
2829

@@ -100,6 +101,9 @@ var CreateCommand = cli.Command{
100101
if err != nil {
101102
return err
102103
}
104+
if len(ps) == 0 {
105+
ps = append(ps, platforms.DefaultSpec())
106+
}
103107

104108
artifactsDb, err := soci.NewDB(soci.ArtifactsDbPath(cliContext.GlobalString("root")))
105109
if err != nil {

cmd/soci/commands/index/list.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package index
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"io"
2223
"os"
@@ -26,6 +27,7 @@ import (
2627

2728
"github.com/awslabs/soci-snapshotter/cmd/soci/commands/internal"
2829
"github.com/awslabs/soci-snapshotter/soci"
30+
"github.com/containerd/containerd/errdefs"
2931
"github.com/containerd/containerd/images"
3032
"github.com/containerd/platforms"
3133
specs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -119,6 +121,9 @@ var listCommand = cli.Command{
119121
for _, plat := range plats {
120122
desc, err := soci.GetImageManifestDescriptor(ctx, cs, img.Target, platforms.OnlyStrict(plat))
121123
if err != nil {
124+
if errors.Is(err, errdefs.ErrNotFound) {
125+
return fmt.Errorf("image manifest for platform %s: %w", platforms.Format(plat), err)
126+
}
122127
return err
123128
}
124129
filters = append(filters, originalDigestFilter(desc.Digest.String()))

cmd/soci/commands/internal/platform.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ var PlatformFlags = []cli.Flag{
4747
// The order of preference is:
4848
// 1) all platforms supported by the image if the `all-plaforms` flag is set
4949
// 2) the set of platforms specified by the `platform` flag
50-
// 3) the default platform
50+
// 3) An empty platform slice. The consumer is responsible for setting an appropriate platform
5151
//
5252
// This method is not suitable for situations where the default should be all supported platforms (e.g. the `soci index list` command)
5353
func GetPlatforms(ctx context.Context, cliContext *cli.Context, img images.Image, cs content.Store) ([]ocispec.Platform, error) {
@@ -56,7 +56,7 @@ func GetPlatforms(ctx context.Context, cliContext *cli.Context, img images.Image
5656
}
5757
ps := cliContext.StringSlice(PlatformFlagKey)
5858
if len(ps) == 0 {
59-
return []ocispec.Platform{platforms.DefaultSpec()}, nil
59+
return []ocispec.Platform{}, nil
6060
}
6161
var result []ocispec.Platform
6262
for _, p := range ps {

cmd/soci/commands/push.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/awslabs/soci-snapshotter/soci"
3232
"github.com/awslabs/soci-snapshotter/soci/store"
3333
"github.com/containerd/containerd/reference"
34+
"github.com/containerd/platforms"
3435
dockercliconfig "github.com/docker/cli/cli/config"
3536
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3637
"github.com/urfave/cli"
@@ -98,6 +99,9 @@ if they are available in the snapshotter's local content store.
9899
if err != nil {
99100
return err
100101
}
102+
if len(ps) == 0 {
103+
ps = append(ps, platforms.DefaultSpec())
104+
}
101105

102106
artifactsDb, err := soci.NewDB(soci.ArtifactsDbPath(cliContext.GlobalString("root")))
103107
if err != nil {

integration/convert_test.go

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,24 @@ func TestConvert(t *testing.T) {
146146
convertedRef := imgRef + "-soci"
147147

148148
sh.X("nerdctl", "pull", "--all-platforms", imgRef)
149+
sh.X("soci", "convert", "--all-platforms", "--min-layer-size", "0", imgRef, convertedRef)
150+
151+
imgDigest := getImageDigest(sh, imgRef)
152+
convertedDigest := getImageDigest(sh, convertedRef)
153+
154+
validateConversion(t, sh, imgDigest, convertedDigest)
155+
})
156+
}
157+
})
158+
159+
t.Run("single platform conversion", func(t *testing.T) {
160+
for _, imageName := range convertImages {
161+
t.Run(imageName, func(t *testing.T) {
162+
rebootContainerd(t, sh, "", "")
163+
imgRef := dockerhub(imageName).ref
164+
convertedRef := imgRef + "-soci"
165+
166+
sh.X("nerdctl", "pull", imgRef)
149167
sh.X("soci", "convert", "--min-layer-size", "0", imgRef, convertedRef)
150168

151169
imgDigest := getImageDigest(sh, imgRef)
@@ -165,8 +183,8 @@ func TestConvert(t *testing.T) {
165183
convertedRef2 := imgRef + "-soci-2"
166184

167185
sh.X("nerdctl", "pull", "--all-platforms", imgRef)
168-
sh.X("soci", "convert", imgRef, convertedRef1)
169-
sh.X("soci", "convert", convertedRef1, convertedRef2)
186+
sh.X("soci", "convert", "--all-platforms", imgRef, convertedRef1)
187+
sh.X("soci", "convert", "--all-platforms", convertedRef1, convertedRef2)
170188

171189
convertedDigest1 := getImageDigest(sh, convertedRef1)
172190
convertedDigest2 := getImageDigest(sh, convertedRef2)
@@ -177,7 +195,7 @@ func TestConvert(t *testing.T) {
177195
}
178196
})
179197

180-
t.Run("single platform conversion", func(t *testing.T) {
198+
t.Run("single image manifest conversion", func(t *testing.T) {
181199
images := []string{cloudwatchAgentx86ImageRef}
182200
for _, imgRef := range images {
183201
t.Run(imgRef, func(t *testing.T) {
@@ -215,21 +233,89 @@ func TestConvert(t *testing.T) {
215233

216234
}
217235

236+
type convertAndPushTestConfig struct {
237+
PullPlatforms string
238+
ConvertPlatforms string
239+
PushPlatforms string
240+
}
241+
242+
func (c convertAndPushTestConfig) String() string {
243+
return strings.Join([]string{c.PullPlatforms, c.ConvertPlatforms, c.PushPlatforms}, ",")
244+
}
245+
218246
func TestConvertAndPush(t *testing.T) {
219247
registryConfig := newRegistryConfig()
220248
sh, done := newShellWithRegistry(t, registryConfig)
221249
defer done()
222-
for _, imageName := range convertImages {
223-
t.Run(imageName, func(t *testing.T) {
224-
rebootContainerd(t, sh, "", "")
225-
img := dockerhub(imageName)
226-
convertedImg := registryConfig.mirror(imageName)
227250

228-
sh.X("nerdctl", "pull", "--all-platforms", img.ref)
229-
sh.X("soci", "convert", img.ref, convertedImg.ref)
230-
sh.X("nerdctl", "login", "--username", registryConfig.user, "--password", registryConfig.pass, convertedImg.ref)
231-
sh.X("nerdctl", "push", "--all-platforms", convertedImg.ref)
232-
})
251+
imageName := nginxImage
252+
253+
platformOptions := map[string][]string{
254+
"one": {"--platform", "linux/x86_64"},
255+
"all": {"--all-platforms"},
256+
}
257+
258+
convertFailureMessages := map[convertAndPushTestConfig]string{
259+
// Any config not in this list should succeed on convert (no error message)
260+
{PullPlatforms: "one", ConvertPlatforms: "all", PushPlatforms: "one"}: "not found",
261+
{PullPlatforms: "one", ConvertPlatforms: "all", PushPlatforms: "all"}: "not found",
262+
}
263+
264+
pushFailureMessages := map[convertAndPushTestConfig]string{
265+
// Any config not in this list should succeed on push (no error message)
266+
{PullPlatforms: "one", ConvertPlatforms: "one", PushPlatforms: "all"}: "not found",
267+
}
268+
269+
for pullPlatform := range platformOptions {
270+
for convertPlatform := range platformOptions {
271+
for pushPlatform := range platformOptions {
272+
test := convertAndPushTestConfig{
273+
PullPlatforms: pullPlatform,
274+
ConvertPlatforms: convertPlatform,
275+
PushPlatforms: pushPlatform,
276+
}
277+
278+
t.Run(test.String(), func(t *testing.T) {
279+
rebootContainerd(t, sh, "", "")
280+
img := dockerhub(imageName)
281+
convertedImg := registryConfig.mirror(imageName)
282+
283+
pull := []string{"nerdctl", "pull"}
284+
convert := []string{"soci", "convert"}
285+
push := []string{"nerdctl", "push"}
286+
287+
pull = append(pull, platformOptions[pullPlatform]...)
288+
convert = append(convert, platformOptions[convertPlatform]...)
289+
push = append(push, platformOptions[pushPlatform]...)
290+
291+
convertFailureMessage, expectedConvertFailure := convertFailureMessages[test]
292+
pushFailureMessage := pushFailureMessages[test]
293+
294+
sh.X(append(pull, img.ref)...)
295+
o, err := sh.CombinedOLog(append(convert, img.ref, convertedImg.ref)...)
296+
validateErrorOutput(t, "convert", string(o), err, convertFailureMessage)
297+
if expectedConvertFailure {
298+
// If we expected a convert error and we got the correct one, then the test is done.
299+
// We should push because we know it will fail.
300+
return
301+
}
302+
sh.X("nerdctl", "login", "--username", registryConfig.user, "--password", registryConfig.pass, convertedImg.ref)
303+
o, err = sh.CombinedOLog(append(push, convertedImg.ref)...)
304+
validateErrorOutput(t, "push", string(o), err, pushFailureMessage)
305+
})
306+
307+
}
308+
}
309+
}
310+
}
311+
312+
func validateErrorOutput(t *testing.T, name string, o string, err error, expectedError string) {
313+
if expectedError != "" {
314+
if !strings.Contains(o, expectedError) {
315+
t.Fatalf("%s: expected error %q, got %q", name, expectedError, o)
316+
}
317+
} else if err != nil {
318+
t.Fatalf("%s: unexpected error: %v", name, err)
233319
}
234320
}
235321

soci/soci_convert.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/awslabs/soci-snapshotter/soci/store"
2929
"github.com/awslabs/soci-snapshotter/util/ociutil"
3030
"github.com/containerd/containerd/images"
31+
"github.com/containerd/errdefs"
3132
"github.com/containerd/platforms"
3233
"github.com/opencontainers/go-digest"
3334
"github.com/opencontainers/image-spec/specs-go"
@@ -45,7 +46,9 @@ type convertConfig struct {
4546
// ConvertWithPlatforms sets the platforms that will be indexed during conversion
4647
func ConvertWithPlatforms(platforms ...ocispec.Platform) ConvertOption {
4748
return func(cc *convertConfig) error {
48-
cc.platforms = platforms
49+
if len(platforms) != 0 {
50+
cc.platforms = platforms
51+
}
4952
return nil
5053
}
5154
}
@@ -82,17 +85,26 @@ func (b *IndexBuilder) Convert(ctx context.Context, img images.Image, opts ...Co
8285
if len(allPlatforms) == 0 {
8386
return nil, errors.New("image does not support any platforms")
8487
}
85-
if images.IsManifestType(img.Target.MediaType) && img.Target.Platform == nil {
88+
defaultPlatform := platforms.DefaultSpec()
89+
if images.IsManifestType(img.Target.MediaType) {
8690
// If the image's target descriptor is a single manifest, then it will not
8791
// contain a platform because that information is stored in the image config instead.
8892
// If we directly use this descriptor in the converted image,
8993
// runtimes will not be able to pull the correct manifest.
9094
// We know the actual platform by inspecting the image config in `images.Platforms`,
9195
// so we add that to the target descriptor.
92-
img.Target.Platform = &allPlatforms[0]
96+
imagePlatform := allPlatforms[0]
97+
img.Target.Platform = &imagePlatform
98+
99+
// We also set the default platform as the image's platform.
100+
// We shouldn't force the user to specify the platform if
101+
// we know the only platform the image supports. This allows users
102+
// to convert single platform images on non-native hardware
103+
// (e.g. converting an arm64 image on an amd64 machine without explicitly specifying the platform)
104+
defaultPlatform = imagePlatform
93105
}
94106
convertCfg := convertConfig{
95-
platforms: allPlatforms,
107+
platforms: []ocispec.Platform{defaultPlatform},
96108
gcRoot: true,
97109
}
98110
for _, opt := range opts {
@@ -226,10 +238,33 @@ func (b *IndexBuilder) newOciIndex(ctx context.Context, img images.Image) (ocisp
226238
func (b *IndexBuilder) annotateImages(ctx context.Context, ociIndex *ocispec.Index, sociIndexes []*IndexWithMetadata) error {
227239
for i := 0; i < len(ociIndex.Manifests); i++ {
228240
manifestDesc := &ociIndex.Manifests[i]
241+
242+
var indexWithMetadata *IndexWithMetadata
243+
idx := slices.IndexFunc(sociIndexes, func(i *IndexWithMetadata) bool { return i.ManifestDesc.Digest == manifestDesc.Digest })
244+
if idx >= 0 {
245+
indexWithMetadata = sociIndexes[idx]
246+
}
247+
229248
// images.Manifest validates the mediatype, no need to do it ourselves like
230249
// we did when loading the OCI index
231250
manifest, err := images.Manifest(ctx, b.contentStore, *manifestDesc, nil)
232251
if err != nil {
252+
// If the manifest is not found:
253+
// if there is an index for the image: error. The manifest was unexpectedly deleted
254+
// if there is not an index for the image: skip.
255+
// In this case, we are working with a multi-platform image where the user only
256+
// pulled a subset of the platforms. We should convert the platforms
257+
// requested and not touch anything else. The user can still push the image as a reduced
258+
// platform (i.e. without --all-platforms).
259+
// This is the case of:
260+
// nerdctl pull $IMAGE
261+
// soci convert $IMAGE $IMAGE-soci
262+
// nerdctl push $IMAGE-soci
263+
if errors.Is(err, errdefs.ErrNotFound) {
264+
if indexWithMetadata == nil {
265+
continue
266+
}
267+
}
233268
return err
234269
}
235270
// Some Registries don't like mixing Docker V2 manifests with OCI image manifests.
@@ -242,10 +277,7 @@ func (b *IndexBuilder) annotateImages(ctx context.Context, ociIndex *ocispec.Ind
242277
// We also aren't modifying image contents at all, so we don't need to modify the config contents.
243278
manifest.Config.MediaType = ocispec.MediaTypeImageConfig
244279

245-
idx := slices.IndexFunc(sociIndexes, func(i *IndexWithMetadata) bool { return i.ManifestDesc.Digest == manifestDesc.Digest })
246-
if idx >= 0 {
247-
indexWithMetadata := sociIndexes[idx]
248-
280+
if indexWithMetadata != nil {
249281
if manifest.Annotations == nil {
250282
manifest.Annotations = make(map[string]string)
251283
}

soci/soci_index.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ import (
3434
"github.com/awslabs/soci-snapshotter/ztoc"
3535
"github.com/awslabs/soci-snapshotter/ztoc/compression"
3636
"github.com/containerd/containerd/content"
37+
"github.com/containerd/errdefs"
38+
"oras.land/oras-go/v2/errdef"
3739

3840
"github.com/containerd/containerd/images"
3941
"github.com/containerd/log"
4042
"github.com/containerd/platforms"
4143
"github.com/opencontainers/go-digest"
4244
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
43-
44-
"oras.land/oras-go/v2/errdef"
4545
)
4646

4747
const (
@@ -501,6 +501,9 @@ func (b *IndexBuilder) build(ctx context.Context, img images.Image, buildCfg bui
501501
// images.Manifest, images.Children will error out when reading the manifest blob (this happens on containerd side)
502502
imgManifestDesc, err := GetImageManifestDescriptor(ctx, b.contentStore, img.Target, platformMatcher)
503503
if err != nil {
504+
if errors.Is(err, errdefs.ErrNotFound) {
505+
return nil, fmt.Errorf("image manifest for %s: %w", platforms.Format(buildCfg.platform), err)
506+
}
504507
return nil, err
505508
}
506509
manifest, err := images.Manifest(ctx, b.contentStore, img.Target, platformMatcher)
@@ -726,8 +729,11 @@ func GetImageManifestDescriptor(ctx context.Context, cs content.Store, imageTarg
726729
return &manifest, nil
727730
}
728731
}
729-
return nil, errors.New("image manifest not found")
732+
return nil, errdefs.ErrNotFound
730733
} else if images.IsManifestType(imageTarget.MediaType) {
734+
if imageTarget.Platform != nil && !platform.Match(*imageTarget.Platform) {
735+
return nil, errdefs.ErrNotFound
736+
}
731737
return &imageTarget, nil
732738
}
733739

0 commit comments

Comments
 (0)