Skip to content

Commit c95d7bd

Browse files
authored
Start to flesh out crane optimize. (#879)
* Start to flesh out crane optimize. This is a hidden command, which roundtrips a remote image to a target image through `tarball.LayerFromOpener(layer.Uncompressed)`. Right now this does nothing to force estargz (still need `GGCR_EXPERIMENT_ESTARGZ=1`) or prioritize files (need `estargz.WithPrioritizedFiles(foo)`), but want to start the convo. Fixes: #878 * Add --prioritize flag to prioritize files * Fix headers, drop history * Drop unused variable * Add explicit option for estargz * Add a warning comment to crane.Optimize
1 parent aae0202 commit c95d7bd

File tree

5 files changed

+273
-37
lines changed

5 files changed

+273
-37
lines changed

Diff for: cmd/crane/cmd/optimize.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2020 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"log"
19+
20+
"github.com/google/go-containerregistry/pkg/crane"
21+
"github.com/spf13/cobra"
22+
)
23+
24+
// NewCmdOptimize creates a new cobra.Command for the optimize subcommand.
25+
func NewCmdOptimize(options *[]crane.Option) *cobra.Command {
26+
var files []string
27+
28+
cmd := &cobra.Command{
29+
Use: "optimize SRC DST",
30+
Hidden: true,
31+
Aliases: []string{"opt"},
32+
Short: "Optimize a remote container image from src to dst",
33+
Args: cobra.ExactArgs(2),
34+
Run: func(_ *cobra.Command, args []string) {
35+
src, dst := args[0], args[1]
36+
if err := crane.Optimize(src, dst, files, *options...); err != nil {
37+
log.Fatal(err)
38+
}
39+
},
40+
}
41+
42+
cmd.Flags().StringSliceVar(&files, "prioritize", nil,
43+
"The list of files to prioritize in the optimized image.")
44+
45+
return cmd
46+
}

Diff for: cmd/crane/cmd/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func New(use, short string, options []crane.Option) *cobra.Command {
8686
NewCmdExport(&options),
8787
NewCmdList(&options),
8888
NewCmdManifest(&options),
89+
NewCmdOptimize(&options),
8990
NewCmdPull(&options),
9091
NewCmdPush(&options),
9192
NewCmdRebase(&options),

Diff for: pkg/crane/optimize.go

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright 2020 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package crane
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
21+
"github.com/containerd/stargz-snapshotter/estargz"
22+
"github.com/google/go-containerregistry/pkg/logs"
23+
"github.com/google/go-containerregistry/pkg/name"
24+
v1 "github.com/google/go-containerregistry/pkg/v1"
25+
"github.com/google/go-containerregistry/pkg/v1/empty"
26+
"github.com/google/go-containerregistry/pkg/v1/mutate"
27+
"github.com/google/go-containerregistry/pkg/v1/remote"
28+
"github.com/google/go-containerregistry/pkg/v1/tarball"
29+
"github.com/google/go-containerregistry/pkg/v1/types"
30+
)
31+
32+
// Optimize optimizes a remote image or index from src to dst.
33+
// THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE WITHOUT WARNING.
34+
func Optimize(src, dst string, prioritize []string, opt ...Option) error {
35+
o := makeOptions(opt...)
36+
srcRef, err := name.ParseReference(src, o.name...)
37+
if err != nil {
38+
return fmt.Errorf("parsing reference %q: %v", src, err)
39+
}
40+
41+
dstRef, err := name.ParseReference(dst, o.name...)
42+
if err != nil {
43+
return fmt.Errorf("parsing reference for %q: %v", dst, err)
44+
}
45+
46+
logs.Progress.Printf("Optimizing from %v to %v", srcRef, dstRef)
47+
desc, err := remote.Get(srcRef, o.remote...)
48+
if err != nil {
49+
return fmt.Errorf("fetching %q: %v", src, err)
50+
}
51+
52+
switch desc.MediaType {
53+
case types.OCIImageIndex, types.DockerManifestList:
54+
// Handle indexes separately.
55+
if o.platform != nil {
56+
// If platform is explicitly set, don't optimize the whole index, just the appropriate image.
57+
if err := optimizeAndPushImage(desc, dstRef, prioritize, o); err != nil {
58+
return fmt.Errorf("failed to optimize image: %v", err)
59+
}
60+
} else {
61+
if err := optimizeAndPushIndex(desc, dstRef, prioritize, o); err != nil {
62+
return fmt.Errorf("failed to optimize index: %v", err)
63+
}
64+
}
65+
66+
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
67+
return errors.New("docker schema 1 images are not supported")
68+
69+
default:
70+
// Assume anything else is an image, since some registries don't set mediaTypes properly.
71+
if err := optimizeAndPushImage(desc, dstRef, prioritize, o); err != nil {
72+
return fmt.Errorf("failed to optimize image: %v", err)
73+
}
74+
}
75+
76+
return nil
77+
}
78+
79+
func optimizeAndPushImage(desc *remote.Descriptor, dstRef name.Reference, prioritize []string, o options) error {
80+
img, err := desc.Image()
81+
if err != nil {
82+
return err
83+
}
84+
85+
oimg, err := optimizeImage(img, prioritize)
86+
if err != nil {
87+
return err
88+
}
89+
90+
return remote.Write(dstRef, oimg, o.remote...)
91+
}
92+
93+
func optimizeImage(img v1.Image, prioritize []string) (v1.Image, error) {
94+
cfg, err := img.ConfigFile()
95+
if err != nil {
96+
return nil, err
97+
}
98+
ocfg := cfg.DeepCopy()
99+
ocfg.History = nil
100+
ocfg.RootFS.DiffIDs = nil
101+
102+
oimg, err := mutate.ConfigFile(empty.Image, ocfg)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
layers, err := img.Layers()
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
olayers := make([]mutate.Addendum, 0, len(layers))
113+
for _, layer := range layers {
114+
olayer, err := tarball.LayerFromOpener(layer.Uncompressed,
115+
tarball.WithEstargz,
116+
tarball.WithEstargzOptions(estargz.WithPrioritizedFiles(prioritize)))
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
olayers = append(olayers, mutate.Addendum{
122+
Layer: olayer,
123+
MediaType: types.DockerLayer,
124+
})
125+
}
126+
127+
return mutate.Append(oimg, olayers...)
128+
}
129+
130+
func optimizeAndPushIndex(desc *remote.Descriptor, dstRef name.Reference, prioritize []string, o options) error {
131+
idx, err := desc.ImageIndex()
132+
if err != nil {
133+
return err
134+
}
135+
136+
oidx, err := optimizeIndex(idx, prioritize)
137+
if err != nil {
138+
return err
139+
}
140+
141+
return remote.WriteIndex(dstRef, oidx, o.remote...)
142+
}
143+
144+
func optimizeIndex(idx v1.ImageIndex, prioritize []string) (v1.ImageIndex, error) {
145+
im, err := idx.IndexManifest()
146+
if err != nil {
147+
return nil, err
148+
}
149+
150+
// Build an image for each child from the base and append it to a new index to produce the result.
151+
adds := make([]mutate.IndexAddendum, 0, len(im.Manifests))
152+
for _, desc := range im.Manifests {
153+
img, err := idx.Image(desc.Digest)
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
oimg, err := optimizeImage(img, prioritize)
159+
if err != nil {
160+
return nil, err
161+
}
162+
adds = append(adds, mutate.IndexAddendum{
163+
Add: oimg,
164+
Descriptor: v1.Descriptor{
165+
URLs: desc.URLs,
166+
MediaType: desc.MediaType,
167+
Annotations: desc.Annotations,
168+
Platform: desc.Platform,
169+
},
170+
})
171+
}
172+
173+
idxType, err := idx.MediaType()
174+
if err != nil {
175+
return nil, err
176+
}
177+
178+
return mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), idxType), nil
179+
}

Diff for: pkg/v1/tarball/layer.go

+43-32
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,45 @@ func WithEstargzOptions(opts ...estargz.Option) LayerOption {
133133
}
134134
}
135135

136+
// WithEstargz is a functional option that explicitly enables estargz support.
137+
func WithEstargz(l *layer) {
138+
oguncompressed := l.uncompressedopener
139+
estargz := func() (io.ReadCloser, error) {
140+
crc, err := oguncompressed()
141+
if err != nil {
142+
return nil, err
143+
}
144+
eopts := append(l.estgzopts, estargz.WithCompressionLevel(l.compression))
145+
rc, h, err := gestargz.ReadCloser(crc, eopts...)
146+
if err != nil {
147+
return nil, err
148+
}
149+
l.annotations[estargz.TOCJSONDigestAnnotation] = h.String()
150+
return &and.ReadCloser{
151+
Reader: rc,
152+
CloseFunc: func() error {
153+
err := rc.Close()
154+
if err != nil {
155+
return err
156+
}
157+
// As an optimization, leverage the DiffID exposed by the estargz ReadCloser
158+
l.diffID, err = v1.NewHash(rc.DiffID().String())
159+
return err
160+
},
161+
}, nil
162+
}
163+
uncompressed := func() (io.ReadCloser, error) {
164+
urc, err := estargz()
165+
if err != nil {
166+
return nil, err
167+
}
168+
return v1util.GunzipReadCloser(urc)
169+
}
170+
171+
l.compressedopener = estargz
172+
l.uncompressedopener = uncompressed
173+
}
174+
136175
// LayerFromFile returns a v1.Layer given a tarball
137176
func LayerFromFile(path string, opts ...LayerOption) (v1.Layer, error) {
138177
opener := func() (io.ReadCloser, error) {
@@ -168,6 +207,10 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
168207
annotations: make(map[string]string, 1),
169208
}
170209

210+
if estgz := os.Getenv("GGCR_EXPERIMENT_ESTARGZ"); estgz == "1" {
211+
opts = append([]LayerOption{WithEstargz}, opts...)
212+
}
213+
171214
if compressed {
172215
layer.compressedopener = opener
173216
layer.uncompressedopener = func() (io.ReadCloser, error) {
@@ -177,38 +220,6 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
177220
}
178221
return ggzip.UnzipReadCloser(urc)
179222
}
180-
} else if estgz := os.Getenv("GGCR_EXPERIMENT_ESTARGZ"); estgz == "1" {
181-
layer.compressedopener = func() (io.ReadCloser, error) {
182-
crc, err := opener()
183-
if err != nil {
184-
return nil, err
185-
}
186-
eopts := append(layer.estgzopts, estargz.WithCompressionLevel(layer.compression))
187-
rc, h, err := gestargz.ReadCloser(crc, eopts...)
188-
if err != nil {
189-
return nil, err
190-
}
191-
layer.annotations[estargz.TOCJSONDigestAnnotation] = h.String()
192-
return &and.ReadCloser{
193-
Reader: rc,
194-
CloseFunc: func() error {
195-
err := rc.Close()
196-
if err != nil {
197-
return err
198-
}
199-
// As an optimization, leverage the DiffID exposed by the estargz ReadCloser
200-
layer.diffID, err = v1.NewHash(rc.DiffID().String())
201-
return err
202-
},
203-
}, nil
204-
}
205-
layer.uncompressedopener = func() (io.ReadCloser, error) {
206-
urc, err := layer.compressedopener()
207-
if err != nil {
208-
return nil, err
209-
}
210-
return v1util.GunzipReadCloser(urc)
211-
}
212223
} else {
213224
layer.uncompressedopener = opener
214225
layer.compressedopener = func() (io.ReadCloser, error) {

Diff for: pkg/v1/tarball/layer_test.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,10 @@ func TestLayerFromFile(t *testing.T) {
7979
}
8080

8181
func TestLayerFromFileEstargz(t *testing.T) {
82-
os.Setenv("GGCR_EXPERIMENT_ESTARGZ", "1")
83-
defer os.Unsetenv("GGCR_EXPERIMENT_ESTARGZ")
8482
setupFixtures(t)
8583
defer teardownFixtures(t)
8684

87-
tarLayer, err := LayerFromFile("testdata/content.tar")
85+
tarLayer, err := LayerFromFile("testdata/content.tar", WithEstargz)
8886
if err != nil {
8987
t.Fatalf("Unable to create layer from tar file: %v", err)
9088
}
@@ -93,7 +91,7 @@ func TestLayerFromFileEstargz(t *testing.T) {
9391
t.Errorf("validate.Layer(tarLayer): %v", err)
9492
}
9593

96-
tarLayerDefaultCompression, err := LayerFromFile("testdata/content.tar", WithCompressionLevel(gzip.DefaultCompression))
94+
tarLayerDefaultCompression, err := LayerFromFile("testdata/content.tar", WithEstargz, WithCompressionLevel(gzip.DefaultCompression))
9795
if err != nil {
9896
t.Fatalf("Unable to create layer with 'Default' compression from tar file: %v", err)
9997
}
@@ -109,7 +107,7 @@ func TestLayerFromFileEstargz(t *testing.T) {
109107
t.Fatal("Unable to generate digest with 'Default' compression", err)
110108
}
111109

112-
tarLayerSpeedCompression, err := LayerFromFile("testdata/content.tar", WithCompressionLevel(gzip.BestSpeed))
110+
tarLayerSpeedCompression, err := LayerFromFile("testdata/content.tar", WithEstargz, WithCompressionLevel(gzip.BestSpeed))
113111
if err != nil {
114112
t.Fatalf("Unable to create layer with 'BestSpeed' compression from tar file: %v", err)
115113
}
@@ -136,6 +134,7 @@ func TestLayerFromFileEstargz(t *testing.T) {
136134
}
137135

138136
tarLayerPrioritizedFiles, err := LayerFromFile("testdata/content.tar",
137+
WithEstargz,
139138
// We compare with default, so pass for apples-to-apples comparison.
140139
WithCompressionLevel(gzip.DefaultCompression),
141140
// By passing a list of priority files, we expect the layer to be different.

0 commit comments

Comments
 (0)