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

Implement annotation-based rebase hints #960

Merged
merged 10 commits into from
Aug 16, 2021
184 changes: 159 additions & 25 deletions cmd/crane/cmd/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
package cmd

import (
"errors"
"fmt"
"log"

"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
)

Expand All @@ -29,48 +35,176 @@ func NewCmdRebase(options *[]crane.Option) *cobra.Command {
rebaseCmd := &cobra.Command{
Use: "rebase",
Short: "Rebase an image onto a new base image",
Args: cobra.NoArgs,
RunE: func(*cobra.Command, []string) error {
origImg, err := crane.Pull(orig, *options...)
if err != nil {
return fmt.Errorf("pulling %s: %v", orig, err)
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if orig == "" {
orig = args[0]
} else if len(args) != 0 || args[0] != "" {
return fmt.Errorf("cannot use --original with positional argument")
}

// 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 rebased == "" {
rebased = orig
}

oldBaseImg, err := crane.Pull(oldBase, *options...)
// 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(rebased, nameOpt...)
if err != nil {
return fmt.Errorf("pulling %s: %v", oldBase, err)
log.Fatalf("parsing %s: %v", rebased, err)
}

newBaseImg, err := crane.Pull(newBase, *options...)
desc, err := crane.Head(orig, *options...)
if err != nil {
return fmt.Errorf("pulling %s: %v", newBase, err)
log.Fatalf("checking %s: %v", orig, err)
}
if !cmd.Parent().PersistentFlags().Changed("platform") && desc.MediaType.IsIndex() {
log.Fatalf("flattening an index is not yet supported")
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
}

img, err := mutate.Rebase(origImg, oldBaseImg, newBaseImg)
origImg, err := crane.Pull(orig, *options...)
if err != nil {
return err
}
origMf, err := origImg.Manifest()
if err != nil {
return err
}
anns := origMf.Annotations
if newBase == "" && anns != nil {
newBase = anns[specsv1.AnnotationBaseImageName]
}
if newBase == "" {
return errors.New("could not determine new base image from annotations")
}
newBaseRef, err := name.ParseReference(newBase)
if err != nil {
return fmt.Errorf("rebasing: %v", err)
return err
}
if oldBase == "" && anns != nil {
oldBaseDigest := anns[specsv1.AnnotationBaseImageDigest]
oldBase = newBaseRef.Context().Digest(oldBaseDigest).String()
}
if oldBase == "" {
return errors.New("could not determine old base image by digest from annotations")
}

if err := crane.Push(img, rebased, *options...); err != nil {
return fmt.Errorf("pushing %s: %v", rebased, err)
rebasedImg, err := rebaseImage(origImg, oldBase, newBase, *options...)
if err != nil {
return fmt.Errorf("rebasing image: %v", err)
}

digest, err := img.Digest()
rebasedDigest, err := rebasedImg.Digest()
if err != nil {
return fmt.Errorf("digesting rebased: %v", err)
return fmt.Errorf("digesting new image: %v", err)
}
origDigest, err := origImg.Digest()
if err != nil {
return err
}
if rebasedDigest == origDigest {
logs.Warn.Println("rebasing was no-op")
}

logs.Progress.Println("pushing rebased image as", rebased)
if _, ok := r.(name.Digest); ok {
digest, err := rebasedImg.Digest()
if err != nil {
log.Fatalf("digesting new image: %v", err)
}
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
rebased = r.Context().Digest(digest.String()).String()
}
if err := crane.Push(rebasedImg, rebased, *options...); err != nil {
log.Fatalf("pushing %s: %v", rebased, err)
}
fmt.Println(digest.String())

fmt.Println(r.Context().Digest(rebasedDigest.String()))
return nil
},
}
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")
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
return rebaseCmd
}

// rebaseImage parses the references and uses them to perform a rebase on the
// original image.
//
// If oldBase or newBase are "", rebaseImage attempts to derive them using
// annotations in the original image. If those annotations are not found,
// rebaseImage returns an error.
//
// If rebasing is successful, base image annotations are set on the resulting
// image to facilitate implicit rebasing next time.
func rebaseImage(orig v1.Image, oldBase, newBase string, opt ...crane.Option) (v1.Image, error) {
m, err := orig.Manifest()
if err != nil {
return nil, err
}
if newBase == "" && m.Annotations != nil {
newBase = m.Annotations[specsv1.AnnotationBaseImageName]
if newBase != "" {
logs.Debug.Printf("Detected new base from %q annotation: %s", specsv1.AnnotationBaseImageName, newBase)
}
}
if newBase == "" {
return nil, fmt.Errorf("either new base or %q annotation is required", specsv1.AnnotationBaseImageName)
}
newBaseImg, err := crane.Pull(newBase, opt...)
if err != nil {
return nil, err
}

if oldBase == "" && m.Annotations != nil {
oldBase = m.Annotations[specsv1.AnnotationBaseImageDigest]
if oldBase != "" {
newBaseRef, err := name.ParseReference(newBase)
if err != nil {
return nil, err
}

oldBase = newBaseRef.Context().Digest(oldBase).String()
logs.Debug.Printf("Detected old base from %q annotation: %s", specsv1.AnnotationBaseImageDigest, oldBase)
}
}
if oldBase == "" {
return nil, fmt.Errorf("either old base or %q annotation is required", specsv1.AnnotationBaseImageDigest)
}

oldBaseImg, err := crane.Pull(oldBase, opt...)
if err != nil {
return nil, err
}

// NB: if newBase is an index, we need to grab the index's digest to
// annotate the resulting image, even though we pull the
// platform-specific image to rebase.
newBaseDigest, err := crane.Digest(newBase, opt...)
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

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

// Update base image annotations for the new image manifest.
logs.Debug.Printf("Setting annotation %q: %q", specsv1.AnnotationBaseImageDigest, newBaseDigest)
logs.Debug.Printf("Setting annotation %q: %q", specsv1.AnnotationBaseImageName, newBase)
return mutate.Annotations(rebased, map[string]string{
specsv1.AnnotationBaseImageDigest: newBaseDigest,
specsv1.AnnotationBaseImageName: newBase,
}).(v1.Image), nil
}
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.

55 changes: 0 additions & 55 deletions cmd/crane/rebase_cloudbuild.yaml

This file was deleted.

62 changes: 62 additions & 0 deletions cmd/crane/rebase_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/bash
set -ex

tmp=$(mktemp -d)

go install ./cmd/registry
go build -o ./crane ./cmd/crane

# Start a local registry.
registry &
PID=$!
function cleanup {
kill $PID
rm -r ${tmp}
rm ./crane
}
trap cleanup EXIT

sleep 1 # Wait for registry to be up.

# Create an image localhost:1338/base containing a.txt
echo a > ${tmp}/a.txt
old_base=$(./crane append -f <(tar -f - -c ${tmp}) -t localhost:1338/base)
rm ${tmp}/a.txt

# Append to that image localhost:1338/rebaseme
echo top > ${tmp}/top.txt
orig=$(./crane append -f <(tar -f - -c ${tmp}) -b ${old_base} -t localhost:1338/rebaseme)
rm ${tmp}/top.txt

# Annotate that image as the base image (by ref and digest)
# TODO: do this with a flag to --append
orig=$(./crane mutate ${orig} \
--annotation org.opencontainers.image.base.name=localhost:1338/base \
--annotation org.opencontainers.image.base.digest=$(./crane digest localhost:1338/base))

# Update localhost:1338/base containing b.txt
echo b > ${tmp}/b.txt
new_base=$(./crane append -f <(tar -f - -c ${tmp}) -t localhost:1338/base)
rm ${tmp}/b.txt

# Rebase using annotations
rebased=$(./crane rebase ${orig})
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 amazing 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks. I still need to hook it up into CI tests...


# List files in the rebased image.
./crane export ${rebased} - | tar -tvf -

# Extract b.txt out of the rebased image.
./crane export ${rebased} - | tar -Oxf - ${tmp:1}/b.txt

# Extract top.txt out of the rebased image.
./crane export ${rebased} - | tar -Oxf - ${tmp:1}/top.txt

# a.txt is _not_ in the rebased image.
set +e
./crane export ${rebased} - | tar -Oxf - ${tmp:1}/a.txt # this should fail
code=$?
echo "finding a.txt exited ${code}"
if [[ $code -ne 1 ]]; then
echo "a.txt was found in rebased image"
exit 1
fi
1 change: 1 addition & 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...
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
}
}
Expand Down