Skip to content

Commit

Permalink
Add crane flatten (#1104)
Browse files Browse the repository at this point in the history
* Add crane flatten

This is adapted from #735 but changes the UX a bit to match what we've
come to expect from crane.

Add pkg/crane.Upload as well, for uploading a layer.

* ./hack/update-codegen.sh

* Support --insecure in flatten

* Make --platform work

* Make flatten location-independent
  • Loading branch information
jonjohnsonjr committed Aug 12, 2021
1 parent bcbf8d3 commit bea59b9
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
144 changes: 144 additions & 0 deletions cmd/crane/cmd/flatten.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"compress/gzip"
"encoding/json"
"fmt"
"log"

"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/spf13/cobra"
)

// NewCmdFlatten creates a new cobra.Command for the flatten subcommand.
func NewCmdFlatten(options *[]crane.Option) *cobra.Command {
var newRef string

flattenCmd := &cobra.Command{
Use: "flatten",
Short: "Flatten an image's layers into a single layer",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Pull image and get config.
ref := args[0]

// If the new ref isn't provided, write over the original image.
// If that ref was provided by digest (e.g., output from
// another crane command), then strip that and push the
// mutated image by digest instead.
if newRef == "" {
newRef = ref
}

// Stupid hack to support insecure flag.
nameOpt := []name.Option{}
if ok, err := cmd.Parent().PersistentFlags().GetBool("insecure"); err != nil {
log.Fatalf("flag problems: %v", err)
} else if ok {
nameOpt = append(nameOpt, name.Insecure)
}
r, err := name.ParseReference(newRef, nameOpt...)
if err != nil {
log.Fatalf("parsing %s: %v", newRef, err)
}

desc, err := crane.Head(ref, *options...)
if err != nil {
log.Fatalf("checking %s: %v", ref, err)
}
if !cmd.Parent().PersistentFlags().Changed("platform") && desc.MediaType.IsIndex() {
log.Fatalf("flattening an index is not yet supported")
}

old, err := crane.Pull(ref, *options...)
if err != nil {
log.Fatalf("pulling %s: %v", ref, err)
}

m, err := old.Manifest()
if err != nil {
log.Fatalf("reading manifest: %v", err)
}

cf, err := old.ConfigFile()
if err != nil {
log.Fatalf("getting config: %v", err)
}
cf = cf.DeepCopy()

oldHistory, err := json.Marshal(cf.History)
if err != nil {
log.Fatalf("marshal history")
}

// Clear layer-specific config file information.
cf.RootFS.DiffIDs = []v1.Hash{}
cf.History = []v1.History{}

img, err := mutate.ConfigFile(empty.Image, cf)
if err != nil {
log.Fatalf("mutating config: %v", err)
}

// TODO: Make compression configurable?
layer := stream.NewLayer(mutate.Extract(old), stream.WithCompressionLevel(gzip.BestCompression))

img, err = mutate.Append(img, mutate.Addendum{
Layer: layer,
History: v1.History{
CreatedBy: fmt.Sprintf("%s flatten %s", cmd.Parent().Use, desc.Digest),
Comment: string(oldHistory),
},
})
if err != nil {
log.Fatalf("appending layers: %v", err)
}

// Retain any annotations from the original image.
if len(m.Annotations) != 0 {
img = mutate.Annotations(img, m.Annotations).(v1.Image)
}

if _, ok := r.(name.Digest); ok {
// If we're pushing by digest, we need to upload the layer first.
if err := crane.Upload(layer, r.Context().String(), *options...); err != nil {
log.Fatalf("uploading layer: %v", err)
}
digest, err := img.Digest()
if err != nil {
log.Fatalf("digesting new image: %v", err)
}
newRef = r.Context().Digest(digest.String()).String()
}
if err := crane.Push(img, newRef, *options...); err != nil {
log.Fatalf("pushing %s: %v", newRef, err)
}
digest, err := img.Digest()
if err != nil {
log.Fatalf("digesting new image: %v", err)
}
fmt.Println(r.Context().Digest(digest.String()))
},
}
flattenCmd.Flags().StringVarP(&newRef, "tag", "t", "", "New tag to apply to flattened image. If not provided, push by digest to the original image repository.")
return flattenCmd
}
1 change: 1 addition & 0 deletions cmd/crane/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func New(use, short string, options []crane.Option) *cobra.Command {
NewCmdDelete(&options),
NewCmdDigest(&options),
NewCmdExport(&options),
NewCmdFlatten(&options),
NewCmdList(&options),
NewCmdManifest(&options),
NewCmdOptimize(&options),
Expand Down
1 change: 1 addition & 0 deletions cmd/crane/doc/crane.md

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

27 changes: 27 additions & 0 deletions cmd/crane/doc/crane_flatten.md

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

10 changes: 10 additions & 0 deletions pkg/crane/crane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ func TestCraneRegistry(t *testing.T) {
if len(repos) != 2 {
t.Fatalf("wanted 2 repos, got %d", len(repos))
}

// Test pushing layer
layer, err = img.LayerByDigest(manifest.Layers[1].Digest)
if err != nil {
t.Fatal(err)
}
if err := crane.Upload(layer, dst); err != nil {
t.Fatal(err)
}
}

func TestCraneCopyIndex(t *testing.T) {
Expand Down Expand Up @@ -531,6 +540,7 @@ func TestBadInputs(t *testing.T) {
err error
}{
{"Push(_, invalid)", crane.Push(nil, invalid)},
{"Upload(_, invalid)", crane.Upload(nil, invalid)},
{"Delete(invalid)", crane.Delete(invalid)},
{"Delete: 404", crane.Delete(valid404)},
{"Save(_, invalid)", crane.Save(nil, invalid, "")},
Expand Down
11 changes: 11 additions & 0 deletions pkg/crane/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,14 @@ func Push(img v1.Image, dst string, opt ...Option) error {
}
return remote.Write(tag, img, o.remote...)
}

// Upload pushes the v1.Layer to a given repo.
func Upload(layer v1.Layer, repo string, opt ...Option) error {
o := makeOptions(opt...)
ref, err := name.NewRepository(repo, o.name...)
if err != nil {
return fmt.Errorf("parsing repo %q: %v", repo, err)
}

return remote.WriteLayer(ref, layer, o.remote...)
}

0 comments on commit bea59b9

Please sign in to comment.