Skip to content

Commit 1cab3a5

Browse files
committed
Make estargz compression-algorithm-agnostic and support zstd (a.k.a. zstd:chunked)
This is the subset of containerd#281. Initially, eStargz is based on gzip compression. But, through zstd:chunked work, it turned out that eStargz is not limited to gzip compression and the same chunking & verifying & prefetching method can be applied to other compression algorithms as well (e.g. zstd). This commit makes `estargz` pkg configurable and agnostic about compression algorithms. For supporting non-gzip compression, the user must implement `estargz.Decompressor` and `estargz.Compressor` interfaces and must plug them to `estargz` tools (e.g. `estargz.Open` and `estargz.NewWriterWithCompression`). `estargz` also provides test suite that is usable for testing these non-gzip eStargz implementations. This commit comes with `zstdchunked` pkg that support zstd compression for eStargz (a.k.a. zstd:chunked), based on the above extensibility. `zstdchunked` pkg contains `zstdchunked.Decompressor` and `zstdchunked.Compressor` that allows `estargz` pkg to use zstd compression (i.e. zstd:chunked) instead of gzip. Layer converter and filesystem now support zstd:chunked leveraging `zstdchunked` pkg. `ctr-remote image optimize` and `ctr-remote image convert` support `--zstdchunked` option that omits zstd-based eStargz and filesystem supports zstd-based eStargz layers by default. Signed-off-by: Kohei Tokunaga <[email protected]>
1 parent 17b648d commit 1cab3a5

35 files changed

+3937
-2298
lines changed

cmd/ctr-remote/commands/convert.go

+45-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import (
2727
"github.com/containerd/containerd/images/converter/uncompress"
2828
"github.com/containerd/containerd/platforms"
2929
"github.com/containerd/stargz-snapshotter/estargz"
30+
"github.com/containerd/stargz-snapshotter/nativeconverter"
3031
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
32+
zstdchunkedconvert "github.com/containerd/stargz-snapshotter/nativeconverter/zstdchunked"
3133
"github.com/containerd/stargz-snapshotter/recorder"
3234
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3335
"github.com/pkg/errors"
@@ -66,6 +68,11 @@ When '--all-platforms' is given all images in a manifest list must be available.
6668
Usage: "eStargz chunk size",
6769
Value: 0,
6870
},
71+
// zstd:chunked flags
72+
cli.BoolFlag{
73+
Name: "zstdchunked",
74+
Usage: "use zstd compression instead of gzip (a.k.a zstd:chunked). Must be used in conjunction with '--oci'.",
75+
},
6976
// generic flags
7077
cli.BoolFlag{
7178
Name: "uncompress",
@@ -96,7 +103,10 @@ When '--all-platforms' is given all images in a manifest list must be available.
96103
return errors.New("src and target image need to be specified")
97104
}
98105

99-
if !context.Bool("all-platforms") {
106+
var platformMC platforms.MatchComparer
107+
if context.Bool("all-platforms") {
108+
platformMC = platforms.All
109+
} else {
100110
if pss := context.StringSlice("platform"); len(pss) > 0 {
101111
var all []ocispec.Platform
102112
for _, ps := range pss {
@@ -106,31 +116,57 @@ When '--all-platforms' is given all images in a manifest list must be available.
106116
}
107117
all = append(all, p)
108118
}
109-
convertOpts = append(convertOpts, converter.WithPlatform(platforms.Ordered(all...)))
119+
platformMC = platforms.Ordered(all...)
110120
} else {
111-
convertOpts = append(convertOpts, converter.WithPlatform(platforms.DefaultStrict()))
121+
platformMC = platforms.DefaultStrict()
112122
}
113123
}
124+
convertOpts = append(convertOpts, converter.WithPlatform(platformMC))
114125

126+
var layerConvertFunc converter.ConvertFunc
115127
if context.Bool("estargz") {
116128
esgzOpts, err := getESGZConvertOpts(context)
117129
if err != nil {
118130
return err
119131
}
120-
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(estargzconvert.LayerConvertFunc(esgzOpts...)))
132+
layerConvertFunc = estargzconvert.LayerConvertFunc(esgzOpts...)
121133
if !context.Bool("oci") {
122134
logrus.Warn("option --estargz should be used in conjunction with --oci")
123135
}
124136
if context.Bool("uncompress") {
125137
return errors.New("option --estargz conflicts with --uncompress")
126138
}
139+
if context.Bool("zstdchunked") {
140+
return errors.New("option --estargz conflicts with --zstdchunked")
141+
}
142+
}
143+
144+
if context.Bool("zstdchunked") {
145+
esgzOpts, err := getESGZConvertOpts(context)
146+
if err != nil {
147+
return err
148+
}
149+
layerConvertFunc = zstdchunkedconvert.LayerConvertFunc(esgzOpts...)
150+
if !context.Bool("oci") {
151+
return errors.New("option --zstdchunked must be used in conjunction with --oci")
152+
}
153+
if context.Bool("uncompress") {
154+
return errors.New("option --zstdchunked conflicts with --uncompress")
155+
}
127156
}
128157

129158
if context.Bool("uncompress") {
130-
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(uncompress.LayerConvertFunc))
159+
layerConvertFunc = uncompress.LayerConvertFunc
160+
}
161+
162+
if layerConvertFunc == nil {
163+
return errors.New("specify layer converter")
131164
}
165+
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(layerConvertFunc))
132166

167+
var docker2oci bool
133168
if context.Bool("oci") {
169+
docker2oci = true
134170
convertOpts = append(convertOpts, converter.WithDockerToOCI(true))
135171
}
136172

@@ -140,6 +176,10 @@ When '--all-platforms' is given all images in a manifest list must be available.
140176
}
141177
defer cancel()
142178

179+
convertOpts = append(convertOpts, converter.WithIndexConvertFunc(
180+
// index converter patched for zstd compression
181+
// TODO: upstream this to containerd/containerd
182+
nativeconverter.IndexConvertFunc(layerConvertFunc, docker2oci, platformMC)))
143183
newImg, err := converter.Convert(ctx, client, targetRef, srcRef, convertOpts...)
144184
if err != nil {
145185
return err
+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 commands
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"io"
23+
24+
"github.com/containerd/containerd/cmd/ctr/commands"
25+
"github.com/containerd/stargz-snapshotter/estargz"
26+
"github.com/containerd/stargz-snapshotter/zstdchunked"
27+
digest "github.com/opencontainers/go-digest"
28+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
29+
"github.com/pkg/errors"
30+
"github.com/urfave/cli"
31+
)
32+
33+
var GetTOCDigestCommand = cli.Command{
34+
Name: "gettocdigest",
35+
Usage: "get the digest of TOC of a layer",
36+
ArgsUsage: "<layer digest>",
37+
Flags: []cli.Flag{
38+
// zstd:chunked flags
39+
cli.BoolFlag{
40+
Name: "zstdchunked",
41+
Usage: "parse layer as zstd:chunked",
42+
},
43+
// other flags for debugging
44+
cli.BoolFlag{
45+
Name: "dump-toc",
46+
Usage: "dump TOC instead of digest. Note that the dumped TOC might be formatted with indents so may have different digest against the original in the layer",
47+
},
48+
},
49+
Action: func(clicontext *cli.Context) error {
50+
layerDgstStr := clicontext.Args().Get(0)
51+
if layerDgstStr == "" {
52+
return errors.New("layer digest need to be specified")
53+
}
54+
55+
client, ctx, cancel, err := commands.NewClient(clicontext)
56+
if err != nil {
57+
return err
58+
}
59+
defer cancel()
60+
61+
layerDgst, err := digest.Parse(layerDgstStr)
62+
if err != nil {
63+
return err
64+
}
65+
ra, err := client.ContentStore().ReaderAt(ctx, ocispec.Descriptor{Digest: layerDgst})
66+
if err != nil {
67+
return err
68+
}
69+
defer ra.Close()
70+
71+
footerSize := estargz.FooterSize
72+
if clicontext.Bool("zstdchunked") {
73+
footerSize = zstdchunked.FooterSize
74+
}
75+
footer := make([]byte, footerSize)
76+
if _, err := ra.ReadAt(footer, ra.Size()-int64(footerSize)); err != nil {
77+
return errors.Wrapf(err, "error reading footer")
78+
}
79+
80+
var decompressor estargz.Decompressor
81+
decompressor = new(estargz.GzipDecompressor)
82+
if clicontext.Bool("zstdchunked") {
83+
decompressor = new(zstdchunked.Decompressor)
84+
}
85+
86+
tocOff, tocSize, err := decompressor.ParseFooter(footer)
87+
if err != nil {
88+
return errors.Wrapf(err, "error parsing footer")
89+
}
90+
if tocSize <= 0 {
91+
tocSize = ra.Size() - tocOff - int64(footerSize)
92+
}
93+
toc, tocDgst, err := decompressor.ParseTOC(io.NewSectionReader(ra, tocOff, tocSize))
94+
if err != nil {
95+
return errors.Wrapf(err, "error parsing TOC")
96+
}
97+
98+
if clicontext.Bool("dump-toc") {
99+
tocJSON, err := json.MarshalIndent(toc, "", "\t")
100+
if err != nil {
101+
return errors.Wrapf(err, "failed to marshal toc")
102+
}
103+
fmt.Println(string(tocJSON))
104+
return nil
105+
}
106+
fmt.Println(tocDgst.String())
107+
return nil
108+
},
109+
}

cmd/ctr-remote/commands/optimize.go

+30-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import (
3131
"github.com/containerd/containerd/platforms"
3232
"github.com/containerd/stargz-snapshotter/analyzer"
3333
"github.com/containerd/stargz-snapshotter/estargz"
34+
"github.com/containerd/stargz-snapshotter/nativeconverter"
3435
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
36+
zstdchunkedconvert "github.com/containerd/stargz-snapshotter/nativeconverter/zstdchunked"
3537
"github.com/containerd/stargz-snapshotter/recorder"
3638
"github.com/containerd/stargz-snapshotter/util/containerdutil"
3739
"github.com/opencontainers/go-digest"
@@ -77,6 +79,10 @@ var OptimizeCommand = cli.Command{
7779
Name: "oci",
7880
Usage: "convert Docker media types to OCI media types",
7981
},
82+
cli.BoolFlag{
83+
Name: "zstdchunked",
84+
Usage: "use zstd compression instead of gzip (a.k.a zstd:chunked)",
85+
},
8086
}, samplerFlags...),
8187
Action: func(clicontext *cli.Context) error {
8288
convertOpts := []converter.Opt{}
@@ -86,7 +92,10 @@ var OptimizeCommand = cli.Command{
8692
return errors.New("src and target image need to be specified")
8793
}
8894

89-
if !clicontext.Bool("all-platforms") {
95+
var platformMC platforms.MatchComparer
96+
if clicontext.Bool("all-platforms") {
97+
platformMC = platforms.All
98+
} else {
9099
if pss := clicontext.StringSlice("platform"); len(pss) > 0 {
91100
var all []ocispec.Platform
92101
for _, ps := range pss {
@@ -96,15 +105,21 @@ var OptimizeCommand = cli.Command{
96105
}
97106
all = append(all, p)
98107
}
99-
convertOpts = append(convertOpts, converter.WithPlatform(platforms.Ordered(all...)))
108+
platformMC = platforms.Ordered(all...)
100109
} else {
101-
convertOpts = append(convertOpts, converter.WithPlatform(platforms.DefaultStrict()))
110+
platformMC = platforms.DefaultStrict()
102111
}
103112
}
113+
convertOpts = append(convertOpts, converter.WithPlatform(platformMC))
104114

115+
var docker2oci bool
105116
if clicontext.Bool("oci") {
117+
docker2oci = true
106118
convertOpts = append(convertOpts, converter.WithDockerToOCI(true))
107119
} else {
120+
if clicontext.Bool("zstdchunked") {
121+
return errors.New("option --zstdchunked must be used in conjunction with --oci")
122+
}
108123
logrus.Warn("option --oci should be used as well")
109124
}
110125

@@ -129,12 +144,21 @@ var OptimizeCommand = cli.Command{
129144
return errors.Wrapf(err, "failed output record file")
130145
}
131146
}
132-
f := estargzconvert.LayerConvertWithLayerOptsFunc(esgzOptsPerLayer)
147+
var f converter.ConvertFunc
148+
if clicontext.Bool("zstdchunked") {
149+
f = zstdchunkedconvert.LayerConvertWithLayerOptsFunc(esgzOptsPerLayer)
150+
} else {
151+
f = estargzconvert.LayerConvertWithLayerOptsFunc(esgzOptsPerLayer)
152+
}
133153
if wrapper != nil {
134154
f = wrapper(f)
135155
}
136-
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(logWrapper(f)))
137-
156+
layerConvertFunc := logWrapper(f)
157+
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(layerConvertFunc))
158+
convertOpts = append(convertOpts, converter.WithIndexConvertFunc(
159+
// index converter patched for zstd compression
160+
// TODO: upstream this to containerd/containerd
161+
nativeconverter.IndexConvertFunc(layerConvertFunc, docker2oci, platformMC)))
138162
newImg, err := converter.Convert(ctx, client, targetRef, srcRef, convertOpts...)
139163
if err != nil {
140164
return err

cmd/ctr-remote/main.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ func init() {
3131
}
3232

3333
func main() {
34-
customCommands := []cli.Command{commands.RpullCommand, commands.OptimizeCommand, commands.ConvertCommand}
34+
customCommands := []cli.Command{
35+
commands.RpullCommand,
36+
commands.OptimizeCommand,
37+
commands.ConvertCommand,
38+
commands.GetTOCDigestCommand,
39+
}
3540
app := app.New()
3641
for i := range app.Commands {
3742
if app.Commands[i].Name == "images" {

0 commit comments

Comments
 (0)