Skip to content

Commit

Permalink
WIP: Implement annotation-based rebase hints
Browse files Browse the repository at this point in the history
Adds crane.Rebase helper function for higher-level functionality
including annotation-based hints
  • Loading branch information
imjasonh committed Apr 16, 2021
1 parent 23990da commit e994afe
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
.project
bazel*
.idea
*.iml
*.iml
62 changes: 36 additions & 26 deletions cmd/crane/cmd/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"log"

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

Expand All @@ -30,47 +30,57 @@ func NewCmdRebase(options *[]crane.Option) *cobra.Command {
rebaseCmd := &cobra.Command{
Use: "rebase",
Short: "Rebase an image onto a new base image",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
origImg, err := crane.Pull(orig, *options...)
if err != nil {
log.Fatalf("pulling %s: %v", orig, err)
Args: cobra.MaximumNArgs(1),
Run: func(_ *cobra.Command, args []string) {
if orig == "" {
orig = args[0]
} else if len(args) != 0 || args[0] != "" {
log.Fatalf("cannot use --original with positional argument")
}
if orig == "" {
log.Fatal("--old_base or positional arg is required")
}

oldBaseImg, err := crane.Pull(oldBase, *options...)
rebasedImg, err := crane.Rebase(orig, oldBase, newBase)
if err != nil {
log.Fatalf("pulling %s: %v", oldBase, err)
log.Fatalf("rebasing image: %v", err)
}

newBaseImg, err := crane.Pull(newBase, *options...)
// 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
// rebased image by digest instead.
if rebased == "" {
log.Println("pushing rebased image as", orig)
rebased = orig
}
digest, err := rebasedImg.Digest()
if err != nil {
log.Fatalf("pulling %s: %v", newBase, err)
log.Fatalf("digesting new image: %v", err)
}

img, err := mutate.Rebase(origImg, oldBaseImg, newBaseImg)
r, err := name.ParseReference(rebased)
if err != nil {
log.Fatalf("rebasing: %v", err)
log.Fatalf("parsing %s: %v", rebased, err)
}
if _, ok := r.(name.Digest); ok {
rebased = r.Context().Digest(digest.String()).String()
}

if err := crane.Push(img, rebased, *options...); err != nil {
if err := crane.Push(rebasedImg, rebased, *options...); err != nil {
log.Fatalf("pushing %s: %v", rebased, err)
}

digest, err := img.Digest()
rebasedRef, err := name.ParseReference(rebased)
if err != nil {
log.Fatalf("digesting rebased: %v", err)
log.Fatalf("parsing %q: %v", rebased, err)
}
fmt.Println(digest.String())

fmt.Println(rebasedRef.Context().Digest(digest.String()))
},
}
rebaseCmd.Flags().StringVarP(&orig, "original", "", "", "Original image to rebase")
rebaseCmd.Flags().StringVarP(&oldBase, "old_base", "", "", "Old base image to remove")
rebaseCmd.Flags().StringVarP(&newBase, "new_base", "", "", "New base image to insert")
rebaseCmd.Flags().StringVarP(&rebased, "rebased", "", "", "Tag to apply to rebased image")

rebaseCmd.MarkFlagRequired("original")
rebaseCmd.MarkFlagRequired("old_base")
rebaseCmd.MarkFlagRequired("new_base")
rebaseCmd.MarkFlagRequired("rebased")
rebaseCmd.Flags().StringVar(&orig, "original", "", "Original image to rebase; use positional arg instead")
rebaseCmd.Flags().StringVar(&oldBase, "old_base", "", "Old base image to remove")
rebaseCmd.Flags().StringVar(&newBase, "new_base", "", "New base image to insert")
rebaseCmd.Flags().StringVarP(&rebased, "tag", "t", "", "Tag to apply to rebased image")
return rebaseCmd
}
4 changes: 2 additions & 2 deletions cmd/crane/doc/crane_rebase.md

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

111 changes: 111 additions & 0 deletions pkg/crane/rebase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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 crane

import (
"fmt"
"log"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
)

const (
baseDigestAnnotation = "org.opencontainers.image.base.digest"
baseTagAnnotation = "org.opencontainers.image.base.tag"
)

// Rebase parses the references and uses them to perform a rebase.
//
// If oldBase or newBase are "", Rebase attempts to derive them using
// annotations in the original image. If those annotations are not found,
// Rebase returns an error.
//
// If rebasing is successful, base image annotations are set on the resulting
// image to facilitate implicit rebasing next time.
func Rebase(orig, oldBase, newBase string, opt ...Option) (v1.Image, error) {
o := makeOptions(opt...)
origRef, err := name.ParseReference(orig, o.name...)
if err != nil {
return nil, fmt.Errorf("parsing tag %q: %v", origRef, err)
}
origImg, err := remote.Image(origRef, o.remote...)

m, err := origImg.Manifest()
if err != nil {
return nil, err
}
if newBase == "" && m.Annotations != nil {
newBase = m.Annotations[baseTagAnnotation]
if newBase != "" {
log.Printf("Detected new base from %q annotation: %s", baseTagAnnotation, newBase)
}
}
if newBase == "" {
return nil, fmt.Errorf("either newBase or %q annotation is required", baseTagAnnotation)
}
newBaseRef, err := name.ParseReference(newBase, o.name...)
if err != nil {
return nil, err
}
if oldBase == "" && m.Annotations != nil {
oldBase = m.Annotations[baseDigestAnnotation]
if oldBase != "" {
oldBase = newBaseRef.Context().Digest(oldBase).String()
log.Printf("Detected old base from %q annotation: %s", baseDigestAnnotation, oldBase)
}
}
if oldBase == "" {
return nil, fmt.Errorf("either oldBase or %q annotation is required", baseDigestAnnotation)
}

oldBaseRef, err := name.ParseReference(oldBase, o.name...)
if err != nil {
return nil, err
}
oldBaseImg, err := remote.Image(oldBaseRef, o.remote...)
if err != nil {
return nil, err
}
newBaseImg, err := remote.Image(newBaseRef, o.remote...)
if err != nil {
return nil, err
}

rebased, err := mutate.Rebase(origImg, oldBaseImg, newBaseImg)
if err != nil {
return nil, err
}
m, err = rebased.Manifest()
if err != nil {
return nil, err
}

// Update base image annotations for the new image manifest.
d, err := newBaseImg.Digest()
if err != nil {
return nil, err
}
newDigest := d.String()
newTag := newBaseRef.String()
log.Printf("Setting annotation %q: %q", baseDigestAnnotation, newDigest)
log.Printf("Setting annotation %q: %q", baseTagAnnotation, newTag)
return mutate.Annotations(rebased, map[string]string{
baseDigestAnnotation: newDigest,
baseTagAnnotation: newTag,
})
}
32 changes: 32 additions & 0 deletions pkg/v1/mutate/mutate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,3 +644,35 @@ func (m mockLayer) Compressed() (io.ReadCloser, error) {
func (m mockLayer) Uncompressed() (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("uncompressed")), nil
}

func TestAnnotations(t *testing.T) {
img, err := random.Image(10, 10)
if err != nil {
t.Fatal(err)
}

old, err := img.Manifest()
if err != nil {
t.Fatal(err)
}

newimg, err := mutate.Annotations(img, map[string]string{
"foo": "bar",
"hello": "world",
})
if err != nil {
t.Fatal(err)
}

newmf, err := newimg.Manifest()
if err != nil {
t.Fatal(err)
}

if d := cmp.Diff(old, newmf); d == "" {
t.Fatal("no changes")
}
if err := validate.Image(newimg); err != nil {
t.Fatal(err)
}
}
28 changes: 28 additions & 0 deletions pkg/v1/mutate/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %v", i, orig, err)
}
if oldLayerDigest != origLayerDigest {
// TODO: this is a bad error message...
return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
}
}
Expand Down Expand Up @@ -142,3 +143,30 @@ func createAddendums(startHistory, startLayer int, history []v1.History, layers

return adds
}

func Annotations(base v1.Image, anns map[string]string) (v1.Image, error) {
mf, err := base.Manifest()
if err != nil {
return nil, err
}
mf = mf.DeepCopy()

if mf.Annotations == nil {
mf.Annotations = map[string]string{}
}
for k, v := range anns {
mf.Annotations[k] = v
}

cfg, err := base.ConfigFile()
if err != nil {
return nil, err
}

return &image{
base: base,
configFile: cfg.DeepCopy(),
manifest: mf,
computed: true,
}, nil
}

0 comments on commit e994afe

Please sign in to comment.