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..c70ff16524 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,31 @@ 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") } - - 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 +64,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..26d7d8b1eb --- /dev/null +++ b/pkg/crane/rebase.go @@ -0,0 +1,114 @@ +// 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 ( + // TODO: better annotations + baseDigestAnnotation = "base-digest" + baseTagAnnotation = "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. + // - base-digest is the new base image digest. + // - base-tag is the new base image by tag. + d, err := newBaseImg.Digest() + if err != nil { + return nil, err + } + newDigest := d.String() + newTag := newBaseRef.String() + log.Printf("Set annotation %q: %s", baseDigestAnnotation, newDigest) + log.Printf("Set annotation %q: %s", baseTagAnnotation, newTag) + return mutate.Annotations(rebased, map[string]string{ + baseDigestAnnotation: newDigest, + baseTagAnnotation: newTag, + }) +} diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go index a19e94232c..bfa0a53ec2 100644 --- a/pkg/v1/mutate/mutate_test.go +++ b/pkg/v1/mutate/mutate_test.go @@ -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) + } +} diff --git a/pkg/v1/mutate/rebase.go b/pkg/v1/mutate/rebase.go index a86a65243b..96b1f668ee 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,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 +}