diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 199f3fa7a..ca3a43ba5 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -70,6 +70,9 @@ var RootCmd = &cobra.Command{ if err := resolveDockerfilePath(); err != nil { return errors.Wrap(err, "error resolving dockerfile path") } + if len(opts.Destinations) == 0 && opts.ImageNameDigestFile != "" { + return errors.New("You must provide --destination if setting ImageNameDigestFile") + } } return nil }, @@ -134,6 +137,7 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { RootCmd.PersistentFlags().StringVarP(&opts.CacheRepo, "cache-repo", "", "", "Specify a repository to use as a cache, otherwise one will be inferred from the destination provided") RootCmd.PersistentFlags().StringVarP(&opts.CacheDir, "cache-dir", "", "/cache", "Specify a local directory to use as a cache.") RootCmd.PersistentFlags().StringVarP(&opts.DigestFile, "digest-file", "", "", "Specify a file to save the digest of the built image to.") + RootCmd.PersistentFlags().StringVarP(&opts.ImageNameDigestFile, "image-name-with-digest-file", "", "", "Specify a file to save the image name w/ digest of the built image to.") RootCmd.PersistentFlags().StringVarP(&opts.OCILayoutPath, "oci-layout-path", "", "", "Path to save the OCI image layout of the built image.") RootCmd.PersistentFlags().BoolVarP(&opts.Cache, "cache", "", false, "Use cache when building image") RootCmd.PersistentFlags().BoolVarP(&opts.Cleanup, "cleanup", "", false, "Clean the filesystem at the end") @@ -240,6 +244,7 @@ func resolveRelativePaths() error { &opts.CacheDir, &opts.TarPath, &opts.DigestFile, + &opts.ImageNameDigestFile, } for _, p := range optsPaths { diff --git a/pkg/config/options.go b/pkg/config/options.go index 44af681ec..ee5206ef2 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -37,6 +37,7 @@ type KanikoOptions struct { Target string CacheRepo string DigestFile string + ImageNameDigestFile string OCILayoutPath string Destinations multiArg BuildArgs multiArg diff --git a/pkg/executor/push.go b/pkg/executor/push.go index 565ac52a4..c5ea7c05b 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -96,17 +96,29 @@ func CheckPushPermissions(opts *config.KanikoOptions) error { return nil } +func getDigest(image v1.Image) ([]byte, error) { + digest, err := image.Digest() + if err != nil { + return nil, err + } + return []byte(digest.String()), nil +} + // DoPush is responsible for pushing image to the destinations specified in opts func DoPush(image v1.Image, opts *config.KanikoOptions) error { t := timing.Start("Total Push Time") - - if opts.DigestFile != "" { - digest, err := image.Digest() + var digestByteArray []byte + var builder strings.Builder + if opts.DigestFile != "" || opts.ImageNameDigestFile != "" { + var err error + digestByteArray, err = getDigest(image) if err != nil { return errors.Wrap(err, "error fetching digest") } - digestByteArray := []byte(digest.String()) - err = ioutil.WriteFile(opts.DigestFile, digestByteArray, 0644) + } + + if opts.DigestFile != "" { + err := ioutil.WriteFile(opts.DigestFile, digestByteArray, 0644) if err != nil { return errors.Wrap(err, "writing digest to file failed") } @@ -128,9 +140,21 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error { if err != nil { return errors.Wrap(err, "getting tag for destination") } + if opts.ImageNameDigestFile != "" { + imageName := []byte(destRef.Repository.Name() + "@") + builder.Write(append(imageName, digestByteArray...)) + builder.WriteString("\n") + } destRefs = append(destRefs, destRef) } + if opts.ImageNameDigestFile != "" { + err := ioutil.WriteFile(opts.ImageNameDigestFile, []byte(builder.String()), 0644) + if err != nil { + return errors.Wrap(err, "writing digest to file failed") + } + } + if opts.TarPath != "" { tagToImage := map[name.Tag]v1.Image{} for _, destRef := range destRefs { diff --git a/pkg/executor/push_test.go b/pkg/executor/push_test.go index f771e400e..838561dde 100644 --- a/pkg/executor/push_test.go +++ b/pkg/executor/push_test.go @@ -192,3 +192,34 @@ func TestOCILayoutPath(t *testing.T) { got, err := layoutImage.Manifest() testutil.CheckErrorAndDeepEqual(t, false, err, want, got) } + +func TestImageNameDigestFile(t *testing.T) { + image, err := random.Image(1024, 4) + if err != nil { + t.Fatalf("could not create image: %s", err) + } + + digest, err := image.Digest() + if err != nil { + t.Fatalf("could not get image digest: %s", err) + } + + opts := config.KanikoOptions{ + NoPush: true, + Destinations: []string{"gcr.io/foo/bar:latest", "bob/image"}, + ImageNameDigestFile: "tmpFile", + } + + defer os.Remove("tmpFile") + + if err := DoPush(image, &opts); err != nil { + t.Fatalf("could not push image: %s", err) + } + + want := []byte("gcr.io/foo/bar@" + digest.String() + "\nindex.docker.io/bob/image@" + digest.String() + "\n") + + got, err := ioutil.ReadFile("tmpFile") + + testutil.CheckErrorAndDeepEqual(t, false, err, want, got) + +}