Skip to content

Commit 188e23b

Browse files
committed
feat: Add zstd compression to crane append and mutate
1 parent dbcd01c commit 188e23b

File tree

9 files changed

+113
-10
lines changed

9 files changed

+113
-10
lines changed

cmd/crane/cmd/append.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package cmd
1717
import (
1818
"fmt"
1919

20+
"github.com/google/go-containerregistry/pkg/compression"
2021
"github.com/google/go-containerregistry/pkg/crane"
2122
"github.com/google/go-containerregistry/pkg/logs"
2223
"github.com/google/go-containerregistry/pkg/name"
@@ -33,6 +34,7 @@ func NewCmdAppend(options *[]crane.Option) *cobra.Command {
3334
var baseRef, newTag, outFile string
3435
var newLayers []string
3536
var annotate, ociEmptyBase bool
37+
var comp = compression.GZip
3638

3739
appendCmd := &cobra.Command{
3840
Use: "append",
@@ -63,7 +65,7 @@ container image.`,
6365
}
6466
}
6567

66-
img, err := crane.Append(base, newLayers...)
68+
img, err := crane.AppendWithCompression(base, comp, newLayers...)
6769
if err != nil {
6870
return fmt.Errorf("appending %v: %w", newLayers, err)
6971
}
@@ -111,6 +113,7 @@ container image.`,
111113
appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to")
112114
appendCmd.Flags().StringVarP(&newTag, "new_tag", "t", "", "Tag to apply to resulting image")
113115
appendCmd.Flags().StringSliceVarP(&newLayers, "new_layer", "f", []string{}, "Path to tarball to append to image")
116+
appendCmd.Flags().VarP(&comp, "compression", "c", "Compression to use for new layers")
114117
appendCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image")
115118
appendCmd.Flags().BoolVar(&annotate, "set-base-image-annotations", false, "If true, annotate the resulting image as being based on the base image")
116119
appendCmd.Flags().BoolVar(&ociEmptyBase, "oci-empty-base", false, "If true, empty base image will have OCI media types instead of Docker")

cmd/crane/cmd/mutate.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"strings"
2121

22+
"github.com/google/go-containerregistry/pkg/compression"
2223
"github.com/google/go-containerregistry/pkg/crane"
2324
"github.com/google/go-containerregistry/pkg/name"
2425
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -30,6 +31,7 @@ import (
3031
func NewCmdMutate(options *[]crane.Option) *cobra.Command {
3132
var labels map[string]string
3233
var annotations map[string]string
34+
var comp = compression.GZip
3335
var envVars keyToValue
3436
var entrypoint, cmd []string
3537
var newLayers []string
@@ -67,7 +69,7 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
6769
return fmt.Errorf("pulling %s: %w", ref, err)
6870
}
6971
if len(newLayers) != 0 {
70-
img, err = crane.Append(img, newLayers...)
72+
img, err = crane.AppendWithCompression(img, comp, newLayers...)
7173
if err != nil {
7274
return fmt.Errorf("appending %v: %w", newLayers, err)
7375
}
@@ -174,6 +176,7 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
174176
mutateCmd.Flags().StringToStringVarP(&annotations, "annotation", "a", nil, "New annotations to add")
175177
mutateCmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "New labels to add")
176178
mutateCmd.Flags().VarP(&envVars, "env", "e", "New envvar to add")
179+
mutateCmd.Flags().VarP(&comp, "compression", "c", "Compression to use for new layers")
177180
mutateCmd.Flags().StringSliceVar(&entrypoint, "entrypoint", nil, "New entrypoint to set")
178181
mutateCmd.Flags().StringSliceVar(&cmd, "cmd", nil, "New cmd to set")
179182
mutateCmd.Flags().StringVar(&newRepo, "repo", "", "Repository to push the mutated image to. If provided, push by digest to this repository.")

cmd/crane/doc/crane_append.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/crane/doc/crane_mutate.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/compression/compression.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
// Package compression abstracts over gzip and zstd.
1616
package compression
1717

18+
import "errors"
19+
1820
// Compression is an enumeration of the supported compression algorithms
1921
type Compression string
2022

@@ -24,3 +26,25 @@ const (
2426
GZip Compression = "gzip"
2527
ZStd Compression = "zstd"
2628
)
29+
30+
// Used by fmt.Print and Cobra in help text
31+
func (e *Compression) String() string {
32+
return string(*e)
33+
}
34+
35+
func (e *Compression) Set(v string) error {
36+
switch v {
37+
case "none", "gzip", "zstd":
38+
*e = Compression(v)
39+
return nil
40+
default:
41+
return errors.New(`must be one of "none", "gzip, or "zstd"`)
42+
}
43+
}
44+
45+
// Used in Cobra help text
46+
func (e *Compression) Type() string {
47+
return "Compression"
48+
}
49+
50+
var ErrZStdNonOci = errors.New("ZSTD compression can only be used with an OCI base image")

pkg/crane/append.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"os"
2020

2121
"github.com/google/go-containerregistry/internal/windows"
22+
"github.com/google/go-containerregistry/pkg/compression"
2223
v1 "github.com/google/go-containerregistry/pkg/v1"
2324
"github.com/google/go-containerregistry/pkg/v1/mutate"
2425
"github.com/google/go-containerregistry/pkg/v1/stream"
@@ -40,6 +41,16 @@ func isWindows(img v1.Image) (bool, error) {
4041
// "windows"), the contents of the tarballs will be modified to be suitable for
4142
// a Windows container image.`,
4243
func Append(base v1.Image, paths ...string) (v1.Image, error) {
44+
return AppendWithCompression(base, compression.GZip, paths...)
45+
}
46+
47+
// Append reads a layer from path and appends it the the v1.Image base
48+
// using the specified compression
49+
//
50+
// If the base image is a Windows base image (i.e., its config.OS is
51+
// "windows"), the contents of the tarballs will be modified to be suitable for
52+
// a Windows container image.`,
53+
func AppendWithCompression(base v1.Image, comp compression.Compression, paths ...string) (v1.Image, error) {
4354
if base == nil {
4455
return nil, fmt.Errorf("invalid argument: base")
4556
}
@@ -61,6 +72,12 @@ func Append(base v1.Image, paths ...string) (v1.Image, error) {
6172
layerType = types.OCILayer
6273
}
6374

75+
if comp == compression.ZStd && layerType == types.OCILayer {
76+
layerType = types.OCILayerZStd
77+
} else if comp == compression.ZStd {
78+
return nil, compression.ErrZStdNonOci
79+
}
80+
6481
layers := make([]v1.Layer, 0, len(paths))
6582
for _, path := range paths {
6683
layer, err := getLayer(path, layerType)
@@ -90,7 +107,11 @@ func getLayer(path string, layerType types.MediaType) (v1.Layer, error) {
90107
return stream.NewLayer(f, stream.WithMediaType(layerType)), nil
91108
}
92109

93-
return tarball.LayerFromFile(path, tarball.WithMediaType(layerType))
110+
if layerType == types.OCILayerZStd {
111+
return tarball.LayerFromFile(path, tarball.WithMediaType(layerType), tarball.WithCompression(compression.ZStd))
112+
} else {
113+
return tarball.LayerFromFile(path, tarball.WithMediaType(layerType))
114+
}
94115
}
95116

96117
// If we're dealing with a named pipe, trying to open it multiple times will

pkg/crane/append_test.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
package crane_test
1616

1717
import (
18+
"errors"
1819
"testing"
1920

21+
"github.com/google/go-containerregistry/pkg/compression"
2022
"github.com/google/go-containerregistry/pkg/crane"
2123
"github.com/google/go-containerregistry/pkg/v1/empty"
2224
"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -25,7 +27,7 @@ import (
2527

2628
func TestAppendWithOCIBaseImage(t *testing.T) {
2729
base := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
28-
img, err := crane.Append(base, "testdata/content.tar")
30+
img, err := crane.AppendWithCompression(base, compression.GZip, "testdata/content.tar")
2931

3032
if err != nil {
3133
t.Fatalf("crane.Append(): %v", err)
@@ -48,8 +50,33 @@ func TestAppendWithOCIBaseImage(t *testing.T) {
4850
}
4951
}
5052

53+
func TestAppendWithOCIBaseImageZstd(t *testing.T) {
54+
base := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
55+
img, err := crane.AppendWithCompression(base, compression.ZStd, "testdata/content.tar")
56+
57+
if err != nil {
58+
t.Fatalf("crane.Append(): %v", err)
59+
}
60+
61+
layers, err := img.Layers()
62+
63+
if err != nil {
64+
t.Fatalf("img.Layers(): %v", err)
65+
}
66+
67+
mediaType, err := layers[0].MediaType()
68+
69+
if err != nil {
70+
t.Fatalf("layers[0].MediaType(): %v", err)
71+
}
72+
73+
if got, want := mediaType, types.OCILayerZStd; got != want {
74+
t.Errorf("MediaType(): want %q, got %q", want, got)
75+
}
76+
}
77+
5178
func TestAppendWithDockerBaseImage(t *testing.T) {
52-
img, err := crane.Append(empty.Image, "testdata/content.tar")
79+
img, err := crane.AppendWithCompression(empty.Image, compression.GZip, "testdata/content.tar")
5380

5481
if err != nil {
5582
t.Fatalf("crane.Append(): %v", err)
@@ -71,3 +98,15 @@ func TestAppendWithDockerBaseImage(t *testing.T) {
7198
t.Errorf("MediaType(): want %q, got %q", want, got)
7299
}
73100
}
101+
102+
func TestAppendWithDockerBaseImageZstd(t *testing.T) {
103+
img, err := crane.AppendWithCompression(empty.Image, compression.ZStd, "testdata/content.tar")
104+
105+
if img != nil {
106+
t.Fatalf("Unexpected success")
107+
}
108+
109+
if !errors.Is(err, compression.ErrZStdNonOci) {
110+
t.Fatalf("Unexpected error: %v", err)
111+
}
112+
}

pkg/crane/crane_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
"github.com/google/go-containerregistry/internal/compare"
3232
"github.com/google/go-containerregistry/pkg/authn"
33+
"github.com/google/go-containerregistry/pkg/compression"
3334
"github.com/google/go-containerregistry/pkg/crane"
3435
"github.com/google/go-containerregistry/pkg/name"
3536
"github.com/google/go-containerregistry/pkg/registry"
@@ -439,7 +440,7 @@ func TestCraneFilesystem(t *testing.T) {
439440
tw.Flush()
440441
tw.Close()
441442

442-
img, err = crane.Append(img, tmp.Name())
443+
img, err = crane.AppendWithCompression(img, compression.GZip, tmp.Name())
443444
if err != nil {
444445
t.Fatal(err)
445446
}
@@ -500,7 +501,7 @@ func TestStreamingAppend(t *testing.T) {
500501

501502
os.Stdin = tmp
502503

503-
img, err := crane.Append(empty.Image, "-")
504+
img, err := crane.AppendWithCompression(empty.Image, compression.GZip, "-")
504505
if err != nil {
505506
t.Fatal(err)
506507
}

pkg/v1/tarball/write.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/google/go-containerregistry/pkg/name"
2929
v1 "github.com/google/go-containerregistry/pkg/v1"
3030
"github.com/google/go-containerregistry/pkg/v1/partial"
31+
"github.com/google/go-containerregistry/pkg/v1/types"
3132
)
3233

3334
// WriteToFile writes in the compressed format to a tarball, on disk.
@@ -182,9 +183,18 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
182183
// Drop the algorithm prefix, e.g. "sha256:"
183184
hex := d.Hex
184185

185-
// gunzip expects certain file extensions:
186-
// https://www.gnu.org/software/gzip/manual/html_node/Overview.html
187-
layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex)
186+
mt, err := l.MediaType()
187+
if err != nil {
188+
return sendProgressWriterReturn(pw, err)
189+
}
190+
// OCILayerZStd is the only layer type that currently supports ZSTD-compression
191+
if mt == types.OCILayerZStd {
192+
layerFiles[i] = fmt.Sprintf("%s.tar.zst", hex)
193+
} else {
194+
// gunzip expects certain file extensions:
195+
// https://www.gnu.org/software/gzip/manual/html_node/Overview.html
196+
layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex)
197+
}
188198

189199
if _, ok := seenLayerDigests[hex]; ok {
190200
continue

0 commit comments

Comments
 (0)