Skip to content

Commit

Permalink
feat: Add zstd compression to crane append and mutate
Browse files Browse the repository at this point in the history
  • Loading branch information
ReillyBrogan committed Sep 28, 2023
1 parent dbcd01c commit 188e23b
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 10 deletions.
5 changes: 4 additions & 1 deletion cmd/crane/cmd/append.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cmd
import (
"fmt"

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

appendCmd := &cobra.Command{
Use: "append",
Expand Down Expand Up @@ -63,7 +65,7 @@ container image.`,
}
}

img, err := crane.Append(base, newLayers...)
img, err := crane.AppendWithCompression(base, comp, newLayers...)
if err != nil {
return fmt.Errorf("appending %v: %w", newLayers, err)
}
Expand Down Expand Up @@ -111,6 +113,7 @@ container image.`,
appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to")
appendCmd.Flags().StringVarP(&newTag, "new_tag", "t", "", "Tag to apply to resulting image")
appendCmd.Flags().StringSliceVarP(&newLayers, "new_layer", "f", []string{}, "Path to tarball to append to image")
appendCmd.Flags().VarP(&comp, "compression", "c", "Compression to use for new layers")
appendCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image")
appendCmd.Flags().BoolVar(&annotate, "set-base-image-annotations", false, "If true, annotate the resulting image as being based on the base image")
appendCmd.Flags().BoolVar(&ociEmptyBase, "oci-empty-base", false, "If true, empty base image will have OCI media types instead of Docker")
Expand Down
5 changes: 4 additions & 1 deletion cmd/crane/cmd/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"strings"

"github.com/google/go-containerregistry/pkg/compression"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand All @@ -30,6 +31,7 @@ import (
func NewCmdMutate(options *[]crane.Option) *cobra.Command {
var labels map[string]string
var annotations map[string]string
var comp = compression.GZip
var envVars keyToValue
var entrypoint, cmd []string
var newLayers []string
Expand Down Expand Up @@ -67,7 +69,7 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
return fmt.Errorf("pulling %s: %w", ref, err)
}
if len(newLayers) != 0 {
img, err = crane.Append(img, newLayers...)
img, err = crane.AppendWithCompression(img, comp, newLayers...)
if err != nil {
return fmt.Errorf("appending %v: %w", newLayers, err)
}
Expand Down Expand Up @@ -174,6 +176,7 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
mutateCmd.Flags().StringToStringVarP(&annotations, "annotation", "a", nil, "New annotations to add")
mutateCmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "New labels to add")
mutateCmd.Flags().VarP(&envVars, "env", "e", "New envvar to add")
mutateCmd.Flags().VarP(&comp, "compression", "c", "Compression to use for new layers")
mutateCmd.Flags().StringSliceVar(&entrypoint, "entrypoint", nil, "New entrypoint to set")
mutateCmd.Flags().StringSliceVar(&cmd, "cmd", nil, "New cmd to set")
mutateCmd.Flags().StringVar(&newRepo, "repo", "", "Repository to push the mutated image to. If provided, push by digest to this repository.")
Expand Down
1 change: 1 addition & 0 deletions cmd/crane/doc/crane_append.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cmd/crane/doc/crane_mutate.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions pkg/compression/compression.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// Package compression abstracts over gzip and zstd.
package compression

import "errors"

// Compression is an enumeration of the supported compression algorithms
type Compression string

Expand All @@ -24,3 +26,25 @@ const (
GZip Compression = "gzip"
ZStd Compression = "zstd"
)

// Used by fmt.Print and Cobra in help text
func (e *Compression) String() string {
return string(*e)
}

func (e *Compression) Set(v string) error {
switch v {
case "none", "gzip", "zstd":
*e = Compression(v)
return nil
default:
return errors.New(`must be one of "none", "gzip, or "zstd"`)
}
}

// Used in Cobra help text
func (e *Compression) Type() string {
return "Compression"
}

var ErrZStdNonOci = errors.New("ZSTD compression can only be used with an OCI base image")
23 changes: 22 additions & 1 deletion pkg/crane/append.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"os"

"github.com/google/go-containerregistry/internal/windows"
"github.com/google/go-containerregistry/pkg/compression"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/stream"
Expand All @@ -40,6 +41,16 @@ func isWindows(img v1.Image) (bool, error) {
// "windows"), the contents of the tarballs will be modified to be suitable for
// a Windows container image.`,
func Append(base v1.Image, paths ...string) (v1.Image, error) {
return AppendWithCompression(base, compression.GZip, paths...)
}

// Append reads a layer from path and appends it the the v1.Image base
// using the specified compression
//
// If the base image is a Windows base image (i.e., its config.OS is
// "windows"), the contents of the tarballs will be modified to be suitable for
// a Windows container image.`,
func AppendWithCompression(base v1.Image, comp compression.Compression, paths ...string) (v1.Image, error) {
if base == nil {
return nil, fmt.Errorf("invalid argument: base")
}
Expand All @@ -61,6 +72,12 @@ func Append(base v1.Image, paths ...string) (v1.Image, error) {
layerType = types.OCILayer
}

if comp == compression.ZStd && layerType == types.OCILayer {
layerType = types.OCILayerZStd
} else if comp == compression.ZStd {
return nil, compression.ErrZStdNonOci
}

layers := make([]v1.Layer, 0, len(paths))
for _, path := range paths {
layer, err := getLayer(path, layerType)
Expand Down Expand Up @@ -90,7 +107,11 @@ func getLayer(path string, layerType types.MediaType) (v1.Layer, error) {
return stream.NewLayer(f, stream.WithMediaType(layerType)), nil
}

return tarball.LayerFromFile(path, tarball.WithMediaType(layerType))
if layerType == types.OCILayerZStd {
return tarball.LayerFromFile(path, tarball.WithMediaType(layerType), tarball.WithCompression(compression.ZStd))
} else {
return tarball.LayerFromFile(path, tarball.WithMediaType(layerType))
}
}

// If we're dealing with a named pipe, trying to open it multiple times will
Expand Down
43 changes: 41 additions & 2 deletions pkg/crane/append_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
package crane_test

import (
"errors"
"testing"

"github.com/google/go-containerregistry/pkg/compression"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
Expand All @@ -25,7 +27,7 @@ import (

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

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

func TestAppendWithOCIBaseImageZstd(t *testing.T) {
base := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
img, err := crane.AppendWithCompression(base, compression.ZStd, "testdata/content.tar")

if err != nil {
t.Fatalf("crane.Append(): %v", err)
}

layers, err := img.Layers()

if err != nil {
t.Fatalf("img.Layers(): %v", err)
}

mediaType, err := layers[0].MediaType()

if err != nil {
t.Fatalf("layers[0].MediaType(): %v", err)
}

if got, want := mediaType, types.OCILayerZStd; got != want {
t.Errorf("MediaType(): want %q, got %q", want, got)
}
}

func TestAppendWithDockerBaseImage(t *testing.T) {
img, err := crane.Append(empty.Image, "testdata/content.tar")
img, err := crane.AppendWithCompression(empty.Image, compression.GZip, "testdata/content.tar")

if err != nil {
t.Fatalf("crane.Append(): %v", err)
Expand All @@ -71,3 +98,15 @@ func TestAppendWithDockerBaseImage(t *testing.T) {
t.Errorf("MediaType(): want %q, got %q", want, got)
}
}

func TestAppendWithDockerBaseImageZstd(t *testing.T) {
img, err := crane.AppendWithCompression(empty.Image, compression.ZStd, "testdata/content.tar")

if img != nil {
t.Fatalf("Unexpected success")
}

if !errors.Is(err, compression.ErrZStdNonOci) {
t.Fatalf("Unexpected error: %v", err)
}
}
5 changes: 3 additions & 2 deletions pkg/crane/crane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

"github.com/google/go-containerregistry/internal/compare"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/compression"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
Expand Down Expand Up @@ -439,7 +440,7 @@ func TestCraneFilesystem(t *testing.T) {
tw.Flush()
tw.Close()

img, err = crane.Append(img, tmp.Name())
img, err = crane.AppendWithCompression(img, compression.GZip, tmp.Name())
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -500,7 +501,7 @@ func TestStreamingAppend(t *testing.T) {

os.Stdin = tmp

img, err := crane.Append(empty.Image, "-")
img, err := crane.AppendWithCompression(empty.Image, compression.GZip, "-")
if err != nil {
t.Fatal(err)
}
Expand Down
16 changes: 13 additions & 3 deletions pkg/v1/tarball/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)

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

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

if _, ok := seenLayerDigests[hex]; ok {
continue
Expand Down

0 comments on commit 188e23b

Please sign in to comment.