From 7fcf22751fa2ef73d7899bfb786fda5758c13445 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 9 Mar 2021 10:28:24 -0500 Subject: [PATCH] WIP: Implement annotation-based rebase hints Adds crane.Rebase helper function for higher-level functionality including annotation-based hints --- .gitignore | 2 +- cmd/crane/cmd/rebase.go | 36 ++++++-------- pkg/crane/rebase.go | 91 ++++++++++++++++++++++++++++++++++++ pkg/v1/mutate/mutate_test.go | 34 ++++++++++++++ pkg/v1/mutate/rebase.go | 14 ++++++ 5 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 pkg/crane/rebase.go diff --git a/.gitignore b/.gitignore index 5264ecc8e5..dc577f2542 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .project bazel* .idea -*.iml \ No newline at end of file +*.iml diff --git a/cmd/crane/cmd/rebase.go b/cmd/crane/cmd/rebase.go index 4226223f7b..894d472f4b 100644 --- a/cmd/crane/cmd/rebase.go +++ b/cmd/crane/cmd/rebase.go @@ -19,7 +19,6 @@ import ( "log" "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/spf13/cobra" ) @@ -30,33 +29,29 @@ 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] } - - oldBaseImg, err := crane.Pull(oldBase, *options...) - if err != nil { - log.Fatalf("pulling %s: %v", oldBase, err) + if orig == "" { + log.Fatal("--old_base or positional arg is required") } - newBaseImg, err := crane.Pull(newBase, *options...) + rebasedImg, err := crane.Rebase(orig, oldBase, newBase) if err != nil { - log.Fatalf("pulling %s: %v", newBase, err) + log.Fatalf("rebasing image: %v", err) } - img, err := mutate.Rebase(origImg, oldBaseImg, newBaseImg) - if err != nil { - log.Fatalf("rebasing: %v", err) + if rebased == "" { + log.Println("pushing rebased image as", orig) + rebased = orig } - - 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() + digest, err := rebasedImg.Digest() if err != nil { log.Fatalf("digesting rebased: %v", err) } @@ -67,10 +62,5 @@ func NewCmdRebase(options *[]crane.Option) *cobra.Command { 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") return rebaseCmd } diff --git a/pkg/crane/rebase.go b/pkg/crane/rebase.go new file mode 100644 index 0000000000..b324235e7e --- /dev/null +++ b/pkg/crane/rebase.go @@ -0,0 +1,91 @@ +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 ( + // TODO: better annotations + baseDigestAnnotation = "base-digest" + baseTagAnnotation = "base-tag" +) + +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 oldBase == "" && m.Annotations != nil { + oldBase = m.Annotations[baseDigestAnnotation] + if oldBase != "" { + 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) + } + 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) + } + + 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 + } + newBaseRef, err := name.ParseReference(newBase, o.name...) + 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 annotations for the new image manifest. + // - the current ("old") base image is the new base image by digest. + // - the future ("new") base image is the new base image by tag. + d, err := newBaseImg.Digest() + if err != nil { + return nil, err + } + if m.Annotations == nil { + m.Annotations = map[string]string{} + } + m.Annotations[baseDigestAnnotation] = newBaseRef.Context().Digest(d.String()).String() + m.Annotations[baseTagAnnotation] = newBaseRef.String() + log.Printf("Set annotation %q: %s", baseDigestAnnotation, m.Annotations[baseDigestAnnotation]) + log.Printf("Set annotation %q: %s", baseTagAnnotation, m.Annotations[baseTagAnnotation]) + return mutate.Manifest(rebased, m) +} diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go index a19e94232c..50a02de6f5 100644 --- a/pkg/v1/mutate/mutate_test.go +++ b/pkg/v1/mutate/mutate_test.go @@ -644,3 +644,37 @@ func (m mockLayer) Compressed() (io.ReadCloser, error) { func (m mockLayer) Uncompressed() (io.ReadCloser, error) { return ioutil.NopCloser(strings.NewReader("uncompressed")), nil } + +func TestManifest(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) + } + + newmf := old.DeepCopy() + newmf.Annotations = map[string]string{ + "foo": "bar", + "hello": "world", + } + newimg, err := mutate.Manifest(img, newmf) + 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) + } +} diff --git a/pkg/v1/mutate/rebase.go b/pkg/v1/mutate/rebase.go index a86a65243b..9076b6b9dc 100644 --- a/pkg/v1/mutate/rebase.go +++ b/pkg/v1/mutate/rebase.go @@ -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) } } @@ -142,3 +143,16 @@ func createAddendums(startHistory, startLayer int, history []v1.History, layers return adds } + +func Manifest(base v1.Image, m *v1.Manifest) (v1.Image, error) { + cfg, err := base.ConfigFile() + if err != nil { + return nil, err + } + return &image{ + base: base, + manifest: m.DeepCopy(), + configFile: cfg.DeepCopy(), + computed: true, + }, nil +}