Skip to content

Commit fb85afc

Browse files
committed
add soci convert feature
Signed-off-by: Arjun Raja Yogidas <[email protected]>
1 parent 694c405 commit fb85afc

File tree

5 files changed

+199
-25
lines changed

5 files changed

+199
-25
lines changed

cmd/nerdctl/image/image_convert.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ func convertCommand() *cobra.Command {
8989
cmd.Flags().String("overlaybd-dbstr", "", "Database config string for overlaybd")
9090
// #endregion
9191

92+
// #region soci flags
93+
cmd.Flags().Bool("soci", false, "Convert image to SOCI Index V2 format.")
94+
cmd.Flags().Int64("soci-min-layer-size", -1, "The minimum size of layers that will be converted to SOCI Index V2 format")
95+
cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans")
96+
// #endregion
97+
9298
// #region generic flags
9399
cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers")
94100
cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types")
@@ -213,6 +219,21 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
213219
}
214220
// #endregion
215221

222+
// #region soci flags
223+
soci, err := cmd.Flags().GetBool("soci")
224+
if err != nil {
225+
return types.ImageConvertOptions{}, err
226+
}
227+
sociMinLayerSize, err := cmd.Flags().GetInt64("soci-min-layer-size")
228+
if err != nil {
229+
return types.ImageConvertOptions{}, err
230+
}
231+
sociSpanSize, err := cmd.Flags().GetInt64("soci-span-size")
232+
if err != nil {
233+
return types.ImageConvertOptions{}, err
234+
}
235+
// #endregion
236+
216237
// #region generic flags
217238
uncompress, err := cmd.Flags().GetBool("uncompress")
218239
if err != nil {
@@ -268,6 +289,13 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
268289
OverlayFsType: overlaybdFsType,
269290
OverlaydbDBStr: overlaybdDbstr,
270291
// #endregion
292+
// #region soci flags
293+
Soci: soci,
294+
SociOptions: types.SociOptions{
295+
SpanSize: sociSpanSize,
296+
MinLayerSize: sociMinLayerSize,
297+
},
298+
// #endregion
271299
// #region generic flags
272300
Uncompress: uncompress,
273301
Oci: oci,

cmd/nerdctl/image/image_convert_linux_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ func TestImageConvert(t *testing.T) {
8888
},
8989
Expected: test.Expects(0, nil, nil),
9090
},
91+
{
92+
Description: "soci",
93+
Require: require.All(
94+
require.Not(nerdtest.Docker),
95+
nerdtest.Soci,
96+
),
97+
Cleanup: func(data test.Data, helpers test.Helpers) {
98+
helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
99+
},
100+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
101+
return helpers.Command("image", "convert", "--soci",
102+
"--soci-span-size", "2097152",
103+
"--soci-min-layer-size", "20971520",
104+
testutil.CommonImage, data.Identifier("converted-image"))
105+
},
106+
Expected: test.Expects(0, nil, nil),
107+
},
91108
},
92109
}
93110

pkg/api/types/image_types.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package types
1919
import (
2020
"io"
2121

22-
"github.com/opencontainers/image-spec/specs-go/v1"
22+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2323
)
2424

2525
// ImageListOptions specifies options for `nerdctl image list`.
@@ -124,6 +124,12 @@ type ImageConvertOptions struct {
124124
OverlaydbDBStr string
125125
// #endregion
126126

127+
// #region soci flags
128+
// Soci convert image to SOCI format. Should be used in conjunction with '--oci'
129+
Soci bool
130+
// SociOptions contains SOCI-specific options
131+
SociOptions SociOptions
132+
// #endregion
127133
}
128134

129135
// ImageCryptOptions specifies options for `nerdctl image encrypt` and `nerdctl image decrypt`.
@@ -200,7 +206,7 @@ type ImagePullOptions struct {
200206
// If nil, it will unpack automatically if only 1 platform is specified.
201207
Unpack *bool
202208
// Content for specific platforms. Empty if `--all-platforms` is true
203-
OCISpecPlatform []v1.Platform
209+
OCISpecPlatform []ocispec.Platform
204210
// Pull mode
205211
Mode string
206212
// Suppress verbose output

pkg/cmd/image/convert.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
converterutil "github.com/containerd/nerdctl/v2/pkg/imgutil/converter"
4848
"github.com/containerd/nerdctl/v2/pkg/platformutil"
4949
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
50+
"github.com/containerd/nerdctl/v2/pkg/snapshotterutil"
5051
)
5152

5253
func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRawRef string, options types.ImageConvertOptions) error {
@@ -86,8 +87,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
8687
zstdchunked := options.ZstdChunked
8788
overlaybd := options.Overlaybd
8889
nydus := options.Nydus
90+
soci := options.Soci
8991
var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error)
90-
if estargz || zstd || zstdchunked || overlaybd || nydus {
92+
if estargz || zstd || zstdchunked || overlaybd || nydus || soci {
9193
convertCount := 0
9294
if estargz {
9395
convertCount++
@@ -104,9 +106,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
104106
if nydus {
105107
convertCount++
106108
}
109+
if soci {
110+
convertCount++
111+
}
107112

108113
if convertCount > 1 {
109-
return errors.New("options --estargz, --zstdchunked, --overlaybd and --nydus lead to conflict, only one of them can be used")
114+
return errors.New("options --estargz, --zstdchunked, --overlaybd, --nydus and --soci lead to conflict, only one of them can be used")
110115
}
111116

112117
var convertFunc converter.ConvertFunc
@@ -164,6 +169,16 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
164169
)),
165170
)
166171
convertType = "nydus"
172+
case soci:
173+
// Convert image to SOCI format
174+
convertedRef, err := snapshotterutil.ConvertSoci(ctx, client, srcRef, targetRef, options.GOptions, options.Platforms, options.SociOptions)
175+
if err != nil {
176+
return fmt.Errorf("failed to convert image to SOCI format: %w", err)
177+
}
178+
res := converterutil.ConvertedImageInfo{
179+
Image: convertedRef,
180+
}
181+
return printConvertedImage(options.Stdout, options, res)
167182
}
168183

169184
if convertType != "overlaybd" {

pkg/snapshotterutil/sociutil.go

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,26 @@ package snapshotterutil
1818

1919
import (
2020
"bufio"
21+
"context"
22+
"fmt"
2123
"os"
2224
"os/exec"
2325
"strconv"
2426
"strings"
2527

28+
"github.com/containerd/containerd/v2/client"
2629
"github.com/containerd/log"
2730

2831
"github.com/containerd/nerdctl/v2/pkg/api/types"
2932
)
3033

31-
// CreateSoci creates a SOCI index(`rawRef`)
32-
func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string, sOpts types.SociOptions) error {
34+
// setupSociCommand creates and sets up a SOCI command with common configuration
35+
func setupSociCommand(gOpts types.GlobalCommandOptions) (*exec.Cmd, error) {
3336
sociExecutable, err := exec.LookPath("soci")
3437
if err != nil {
3538
log.L.WithError(err).Error("soci executable not found in path $PATH")
3639
log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md")
37-
return err
40+
return nil, err
3841
}
3942

4043
sociCmd := exec.Command(sociExecutable)
@@ -47,7 +50,77 @@ func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform boo
4750
if gOpts.Namespace != "" {
4851
sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace)
4952
}
50-
// #endregion
53+
54+
return sociCmd, nil
55+
}
56+
57+
// ConvertSoci converts an image to SOCI format and returns the converted image reference with digest
58+
func ConvertSoci(ctx context.Context, client *client.Client, srcRef string, destRef string, gOpts types.GlobalCommandOptions, platforms []string, sOpts types.SociOptions) (string, error) {
59+
// Check minimum required version
60+
// const minRequiredVersion = "0.9.0"
61+
// if err := checkSociVersion(minRequiredVersion); err != nil {
62+
// return err
63+
// }
64+
65+
sociCmd, err := setupSociCommand(gOpts)
66+
if err != nil {
67+
return "", err
68+
}
69+
70+
// TODO: Implement conversion logic
71+
sociCmd.Args = append(sociCmd.Args, "convert")
72+
73+
if len(platforms) > 0 {
74+
// multiple values need to be passed as separate, repeating flags in soci as it uses urfave
75+
// https://github.com/urfave/cli/blob/main/docs/v2/examples/flags.md#multiple-values-per-single-flag
76+
for _, p := range platforms {
77+
sociCmd.Args = append(sociCmd.Args, "--platform", p)
78+
}
79+
}
80+
81+
if sOpts.SpanSize != -1 {
82+
sociCmd.Args = append(sociCmd.Args, "--span-size", strconv.FormatInt(sOpts.SpanSize, 10))
83+
}
84+
85+
if sOpts.MinLayerSize != -1 {
86+
sociCmd.Args = append(sociCmd.Args, "--min-layer-size", strconv.FormatInt(sOpts.MinLayerSize, 10))
87+
}
88+
89+
sociCmd.Args = append(sociCmd.Args, srcRef, destRef)
90+
91+
log.L.Infof("Converting image from %s to %s using SOCI format", srcRef, destRef)
92+
93+
err = processSociIO(sociCmd)
94+
if err != nil {
95+
return "", err
96+
}
97+
err = sociCmd.Wait()
98+
if err != nil {
99+
return "", err
100+
}
101+
102+
// Get the converted image's digest
103+
img, err := client.GetImage(ctx, destRef)
104+
if err != nil {
105+
return "", fmt.Errorf("failed to get converted image: %w", err)
106+
}
107+
108+
// Return the full reference with digest
109+
return fmt.Sprintf("%s@%s", destRef, img.Target().Digest), nil
110+
}
111+
112+
// CreateSoci creates a SOCI index(`rawRef`)
113+
func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string, sOpts types.SociOptions) error {
114+
// Check minimum required version
115+
// const minRequiredVersion = "0.9.0"
116+
// if err := checkSociVersion(minRequiredVersion); err != nil {
117+
// return err
118+
// }
119+
120+
sociCmd, err := setupSociCommand(gOpts)
121+
if err != nil {
122+
return err
123+
}
51124

52125
// Global flags have to be put before subcommand before soci upgrades to urfave v3.
53126
// https://github.com/urfave/cli/issues/1113
@@ -73,7 +146,7 @@ func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform boo
73146
// --timeout, --debug, --content-store
74147
sociCmd.Args = append(sociCmd.Args, rawRef)
75148

76-
log.L.Debugf("running %s %v", sociExecutable, sociCmd.Args)
149+
log.L.Debugf("running soci %v", sociCmd.Args)
77150

78151
err = processSociIO(sociCmd)
79152
if err != nil {
@@ -88,25 +161,11 @@ func CreateSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform boo
88161
func PushSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool, platforms []string) error {
89162
log.L.Debugf("pushing SOCI index: %s", rawRef)
90163

91-
sociExecutable, err := exec.LookPath("soci")
164+
sociCmd, err := setupSociCommand(gOpts)
92165
if err != nil {
93-
log.L.WithError(err).Error("soci executable not found in path $PATH")
94-
log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md")
95166
return err
96167
}
97168

98-
sociCmd := exec.Command(sociExecutable)
99-
sociCmd.Env = os.Environ()
100-
101-
// #region for global flags.
102-
if gOpts.Address != "" {
103-
sociCmd.Args = append(sociCmd.Args, "--address", gOpts.Address)
104-
}
105-
if gOpts.Namespace != "" {
106-
sociCmd.Args = append(sociCmd.Args, "--namespace", gOpts.Namespace)
107-
}
108-
// #endregion
109-
110169
// Global flags have to be put before subcommand before soci upgrades to urfave v3.
111170
// https://github.com/urfave/cli/issues/1113
112171
sociCmd.Args = append(sociCmd.Args, "push")
@@ -131,7 +190,7 @@ func PushSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool,
131190
}
132191
sociCmd.Args = append(sociCmd.Args, rawRef)
133192

134-
log.L.Debugf("running %s %v", sociExecutable, sociCmd.Args)
193+
log.L.Debugf("running soci %v", sociCmd.Args)
135194

136195
err = processSociIO(sociCmd)
137196
if err != nil {
@@ -140,6 +199,55 @@ func PushSoci(rawRef string, gOpts types.GlobalCommandOptions, allPlatform bool,
140199
return sociCmd.Wait()
141200
}
142201

202+
// checkSociVersion checks if the installed SOCI version meets the minimum required version
203+
// func checkSociVersion(minVersion string) error {
204+
// sociExecutable, err := exec.LookPath("soci")
205+
// if err != nil {
206+
// log.L.WithError(err).Error("soci executable not found in path $PATH")
207+
// log.L.Info("you might consider installing soci from: https://github.com/awslabs/soci-snapshotter/blob/main/docs/install.md")
208+
// return err
209+
// }
210+
211+
// cmd := exec.Command(sociExecutable, "--version")
212+
// output, err := cmd.Output()
213+
// if err != nil {
214+
// return fmt.Errorf("failed to get soci version: %w", err)
215+
// }
216+
217+
// // Parse version from output
218+
// // Example output format: "soci version v0.9.0 737f61a3db40c386f997c1f126344158aa3ad43c"
219+
// versionStr := strings.TrimSpace(string(output))
220+
// parts := strings.Fields(versionStr)
221+
// if len(parts) < 3 {
222+
// return fmt.Errorf("unexpected soci version output format: %s", versionStr)
223+
// }
224+
225+
// // Extract version number without 'v' prefix
226+
// installedVersion := strings.TrimPrefix(parts[2], "v")
227+
228+
// // Compare versions
229+
// v1, err := semver.NewVersion(installedVersion)
230+
// if err != nil {
231+
// return fmt.Errorf("failed to parse installed version %s: %w", installedVersion, err)
232+
// }
233+
234+
// v2, err := semver.NewVersion(minVersion)
235+
// if err != nil {
236+
// return fmt.Errorf("failed to parse minimum required version %s: %w", minVersion, err)
237+
// }
238+
239+
// if v1.LessThan(v2) {
240+
// return fmt.Errorf("installed soci version %s is older than required version %s", installedVersion, minVersion)
241+
// }
242+
243+
// // Log the full version info including commit hash for debugging purposes
244+
// if len(parts) > 3 {
245+
// log.L.Debugf("soci version: %s (commit: %s)", installedVersion, parts[3])
246+
// }
247+
248+
// return nil
249+
// }
250+
143251
func processSociIO(sociCmd *exec.Cmd) error {
144252
stdout, err := sociCmd.StdoutPipe()
145253
if err != nil {

0 commit comments

Comments
 (0)