Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add crane flatten #1104

Merged
merged 5 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions cmd/crane/cmd/flatten.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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(_ *cobra.Command, args []string) {
// Pull image and get config.
ref := args[0]

desc, err := crane.Head(ref, *options...)
if err != nil {
log.Fatalf("checking %s: %v", ref, err)
}
if 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("crane flatten %s", ref),
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 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #960 if you rebase by digest we'll push to :rebased unless you tell us what to push to. I don't love it, but I especially don't love it if it's going to disagree with the UX of crane flatten <img-by-digest>.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love that either, can do the same thing in #960 that we do here?

if newRef == "" {
newRef = ref
}
r, err := name.ParseReference(newRef)
if err != nil {
log.Fatalf("parsing %s: %v", newRef, err)
}
if _, ok := r.(name.Digest); ok {
// If we're pushing by digest, we need to upload the layer first.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the stream.Layer's digest won't be known until it's uploaded, right? Does pushing to some fallback tag instead help here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not push to a fallback tag because that might overwrite an existing tag or pollute the registry.

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...)
}