Skip to content

Commit 5ca3ac6

Browse files
committed
add Image content converter
Go example: ```go opts := []converter.Opt{ // convert Docker media types to OCI ones converter.WithDocker2OCI(true), // convert tar.gz layers to uncompressed tar layers converter.WithLayerConvertFunc(uncompress.LayerConvertFunc), } srcRef := "example.com/foo:orig" dstRef := "example.com/foo:converted" dstImg, err = converter.Convert(ctx, client, dstRef, srcRef, opts...) fmt.Println(dstImg.Target) ``` ctr example: `ctr images convert --oci --uncompress example.com/foo:orig example.com/foo:converted` Go test: `go test -exec sudo -test.root -test.run TestConvert` The implementation is from containerd/stargz-snapshotter#224, but eStargz-specific functions are not included in this PR. eStargz converter can be specified by importing `estargz` package and using `WithLayerConvertFunc(estargz.LayerConvertFunc)` option. This converter interface will be potentially useful for converting zstd and ocicrypt layers as well. Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1 parent 9b9de47 commit 5ca3ac6

File tree

9 files changed

+1024
-1
lines changed

9 files changed

+1024
-1
lines changed

cmd/ctr/commands/images/convert.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package images
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/containerd/containerd/cmd/ctr/commands"
23+
"github.com/containerd/containerd/images/converter"
24+
"github.com/containerd/containerd/images/converter/uncompress"
25+
"github.com/containerd/containerd/platforms"
26+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
27+
"github.com/pkg/errors"
28+
"github.com/urfave/cli"
29+
)
30+
31+
var convertCommand = cli.Command{
32+
Name: "convert",
33+
Usage: "convert an image",
34+
ArgsUsage: "[flags] <source_ref> <target_ref>",
35+
Description: `Convert an image format.
36+
37+
e.g., 'ctr convert --uncompress --oci example.com/foo:orig example.com/foo:converted'
38+
39+
Use '--platform' to define the output platform.
40+
When '--all-platforms' is given all images in a manifest list must be available.
41+
`,
42+
Flags: []cli.Flag{
43+
// generic flags
44+
cli.BoolFlag{
45+
Name: "uncompress",
46+
Usage: "convert tar.gz layers to uncompressed tar layers",
47+
},
48+
cli.BoolFlag{
49+
Name: "oci",
50+
Usage: "convert Docker media types to OCI media types",
51+
},
52+
// platform flags
53+
cli.StringSliceFlag{
54+
Name: "platform",
55+
Usage: "Pull content from a specific platform",
56+
Value: &cli.StringSlice{},
57+
},
58+
cli.BoolFlag{
59+
Name: "all-platforms",
60+
Usage: "exports content from all platforms",
61+
},
62+
},
63+
Action: func(context *cli.Context) error {
64+
var convertOpts []converter.Opt
65+
srcRef := context.Args().Get(0)
66+
targetRef := context.Args().Get(1)
67+
if srcRef == "" || targetRef == "" {
68+
return errors.New("src and target image need to be specified")
69+
}
70+
71+
if !context.Bool("all-platforms") {
72+
if pss := context.StringSlice("platform"); len(pss) > 0 {
73+
var all []ocispec.Platform
74+
for _, ps := range pss {
75+
p, err := platforms.Parse(ps)
76+
if err != nil {
77+
return errors.Wrapf(err, "invalid platform %q", ps)
78+
}
79+
all = append(all, p)
80+
}
81+
convertOpts = append(convertOpts, converter.WithPlatform(platforms.Ordered(all...)))
82+
} else {
83+
convertOpts = append(convertOpts, converter.WithPlatform(platforms.DefaultStrict()))
84+
}
85+
}
86+
87+
if context.Bool("uncompress") {
88+
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(uncompress.LayerConvertFunc))
89+
}
90+
91+
if context.Bool("oci") {
92+
convertOpts = append(convertOpts, converter.WithDockerToOCI(true))
93+
}
94+
95+
client, ctx, cancel, err := commands.NewClient(context)
96+
if err != nil {
97+
return err
98+
}
99+
defer cancel()
100+
101+
newImg, err := converter.Convert(ctx, client, targetRef, srcRef, convertOpts...)
102+
if err != nil {
103+
return err
104+
}
105+
fmt.Fprintln(context.App.Writer, newImg.Target.Digest.String())
106+
return nil
107+
},
108+
}

cmd/ctr/commands/images/images.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ var Command = cli.Command{
5050
removeCommand,
5151
tagCommand,
5252
setLabelsCommand,
53+
convertCommand,
5354
},
5455
}
5556

convert_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package containerd
18+
19+
import (
20+
"testing"
21+
22+
"github.com/containerd/containerd/images"
23+
"github.com/containerd/containerd/images/converter"
24+
"github.com/containerd/containerd/images/converter/uncompress"
25+
"github.com/containerd/containerd/platforms"
26+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
27+
"gotest.tools/v3/assert"
28+
)
29+
30+
// TestConvert creates an image from testImage, with the following conversion:
31+
// - Media type: Docker -> OCI
32+
// - Layer type: tar.gz -> tar
33+
// - Arch: Multi -> Single
34+
func TestConvert(t *testing.T) {
35+
if testing.Short() {
36+
t.Skip()
37+
}
38+
ctx, cancel := testContext(t)
39+
defer cancel()
40+
41+
client, err := New(address)
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
defer client.Close()
46+
47+
_, err = client.Fetch(ctx, testImage)
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
dstRef := testImage + "-testconvert"
52+
defPlat := platforms.DefaultStrict()
53+
opts := []converter.Opt{
54+
converter.WithDockerToOCI(true),
55+
converter.WithLayerConvertFunc(uncompress.LayerConvertFunc),
56+
converter.WithPlatform(defPlat),
57+
}
58+
dstImg, err := converter.Convert(ctx, client, dstRef, testImage, opts...)
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
defer func() {
63+
if deleteErr := client.ImageService().Delete(ctx, dstRef); deleteErr != nil {
64+
t.Fatal(deleteErr)
65+
}
66+
}()
67+
cs := client.ContentStore()
68+
plats, err := images.Platforms(ctx, cs, dstImg.Target)
69+
if err != nil {
70+
t.Fatal(err)
71+
}
72+
// Assert that the image does not have any extra arch.
73+
assert.Equal(t, 1, len(plats))
74+
assert.Check(t, defPlat.Match(plats[0]))
75+
76+
// Assert that the media type is converted to OCI and also uncompressed
77+
mani, err := images.Manifest(ctx, cs, dstImg.Target, defPlat)
78+
if err != nil {
79+
t.Fatal(err)
80+
}
81+
for _, l := range mani.Layers {
82+
if plats[0].OS == "windows" {
83+
assert.Equal(t, ocispec.MediaTypeImageLayerNonDistributable, l.MediaType)
84+
} else {
85+
assert.Equal(t, ocispec.MediaTypeImageLayer, l.MediaType)
86+
}
87+
}
88+
}

images/converter/converter.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package converter provides image converter
18+
package converter
19+
20+
import (
21+
"context"
22+
23+
"github.com/containerd/containerd/content"
24+
"github.com/containerd/containerd/images"
25+
"github.com/containerd/containerd/leases"
26+
"github.com/containerd/containerd/platforms"
27+
)
28+
29+
type convertOpts struct {
30+
layerConvertFunc ConvertFunc
31+
docker2oci bool
32+
indexConvertFunc ConvertFunc
33+
platformMC platforms.MatchComparer
34+
}
35+
36+
// Opt is an option for Convert()
37+
type Opt func(*convertOpts) error
38+
39+
// WithLayerConvertFunc specifies the function that converts layers.
40+
func WithLayerConvertFunc(fn ConvertFunc) Opt {
41+
return func(copts *convertOpts) error {
42+
copts.layerConvertFunc = fn
43+
return nil
44+
}
45+
}
46+
47+
// WithDockerToOCI converts Docker media types into OCI ones.
48+
func WithDockerToOCI(v bool) Opt {
49+
return func(copts *convertOpts) error {
50+
copts.docker2oci = true
51+
return nil
52+
}
53+
}
54+
55+
// WithPlatform specifies the platform.
56+
// Defaults to all platforms.
57+
func WithPlatform(p platforms.MatchComparer) Opt {
58+
return func(copts *convertOpts) error {
59+
copts.platformMC = p
60+
return nil
61+
}
62+
}
63+
64+
// WithIndexConvertFunc specifies the function that converts manifests and index (manifest lists).
65+
// Defaults to DefaultIndexConvertFunc.
66+
func WithIndexConvertFunc(fn ConvertFunc) Opt {
67+
return func(copts *convertOpts) error {
68+
copts.indexConvertFunc = fn
69+
return nil
70+
}
71+
}
72+
73+
// Client is implemented by *containerd.Client .
74+
type Client interface {
75+
WithLease(ctx context.Context, opts ...leases.Opt) (context.Context, func(context.Context) error, error)
76+
ContentStore() content.Store
77+
ImageService() images.Store
78+
}
79+
80+
// Convert converts an image.
81+
func Convert(ctx context.Context, client Client, dstRef, srcRef string, opts ...Opt) (*images.Image, error) {
82+
var copts convertOpts
83+
for _, o := range opts {
84+
if err := o(&copts); err != nil {
85+
return nil, err
86+
}
87+
}
88+
if copts.platformMC == nil {
89+
copts.platformMC = platforms.All
90+
}
91+
if copts.indexConvertFunc == nil {
92+
copts.indexConvertFunc = DefaultIndexConvertFunc(copts.layerConvertFunc, copts.docker2oci, copts.platformMC)
93+
}
94+
95+
ctx, done, err := client.WithLease(ctx)
96+
if err != nil {
97+
return nil, err
98+
}
99+
defer done(ctx)
100+
101+
cs := client.ContentStore()
102+
is := client.ImageService()
103+
srcImg, err := is.Get(ctx, srcRef)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
dstDesc, err := copts.indexConvertFunc(ctx, cs, srcImg.Target)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
dstImg := srcImg
114+
dstImg.Name = dstRef
115+
if dstDesc != nil {
116+
dstImg.Target = *dstDesc
117+
}
118+
var res images.Image
119+
if dstRef != srcRef {
120+
_ = is.Delete(ctx, dstRef)
121+
res, err = is.Create(ctx, dstImg)
122+
} else {
123+
res, err = is.Update(ctx, dstImg)
124+
}
125+
return &res, err
126+
}

0 commit comments

Comments
 (0)