From 7a0296eeb3d74fb0ac6ca60ed66e59e1d792ea75 Mon Sep 17 00:00:00 2001 From: Bridget McErlean Date: Tue, 7 Jan 2020 08:34:27 -0500 Subject: [PATCH] Add a dry run option for the image commands Signed-off-by: Bridget McErlean --- cmd/sonobuoy/app/args.go | 8 ++ cmd/sonobuoy/app/images.go | 62 +++++++--- pkg/image/docker_client.go | 115 ++++++++++++++++++ .../{images_test.go => docker_client_test.go} | 8 +- pkg/image/dryrun_client.go | 56 +++++++++ pkg/image/image.go | 90 ++------------ 6 files changed, 236 insertions(+), 103 deletions(-) create mode 100644 pkg/image/docker_client.go rename pkg/image/{images_test.go => docker_client_test.go} (97%) create mode 100644 pkg/image/dryrun_client.go diff --git a/cmd/sonobuoy/app/args.go b/cmd/sonobuoy/app/args.go index 762538ba8..be367ae88 100644 --- a/cmd/sonobuoy/app/args.go +++ b/cmd/sonobuoy/app/args.go @@ -366,3 +366,11 @@ func AddShortFlag(flag *bool, flags *pflag.FlagSet) { "If true, prints just the sonobuoy version", ) } + +// AddDryRunFlag adds a boolean flag to perform a dry-run of image operations. +func AddDryRunFlag(flag *bool, flags *pflag.FlagSet) { + flags.BoolVar( + flag, "dry-run", false, + "If true, only print the image operations that would be performed.", + ) +} diff --git a/cmd/sonobuoy/app/images.go b/cmd/sonobuoy/app/images.go index 75a7ab642..b7cfb7417 100644 --- a/cmd/sonobuoy/app/images.go +++ b/cmd/sonobuoy/app/images.go @@ -42,6 +42,7 @@ type imagesFlags struct { plugins []string kubeconfig Kubeconfig customRegistry string + dryRun bool } func NewCmdImages() *cobra.Command { @@ -72,11 +73,19 @@ func NewCmdImages() *cobra.Command { func pullCmd() *cobra.Command { var flags imagesFlags + pullCmd := &cobra.Command{ Use: "pull", Short: "Pulls images to local docker client for a specific plugin", Run: func(cmd *cobra.Command, args []string) { - if errs := pullImages(flags.plugins, flags.kubeconfig, flags.e2eRegistryConfig); len(errs) > 0 { + var client image.Client + if flags.dryRun { + client = image.DryRunClient{} + } else { + client = image.NewDockerClient() + } + + if errs := pullImages(flags.plugins, flags.kubeconfig, flags.e2eRegistryConfig, client); len(errs) > 0 { for _, err := range errs { errlog.LogError(err) } @@ -88,6 +97,7 @@ func pullCmd() *cobra.Command { AddE2ERegistryConfigFlag(&flags.e2eRegistryConfig, pullCmd.Flags()) AddKubeconfigFlag(&flags.kubeconfig, pullCmd.Flags()) AddPluginListFlag(&flags.plugins, pullCmd.Flags()) + AddDryRunFlag(&flags.dryRun, pullCmd.Flags()) return pullCmd } @@ -104,7 +114,14 @@ func pushCmd() *cobra.Command { return nil }, Run: func(cmd *cobra.Command, args []string) { - if errs := pushImages(flags.plugins, flags.kubeconfig, flags.customRegistry, flags.e2eRegistryConfig); len(errs) > 0 { + var client image.Client + if flags.dryRun { + client = image.DryRunClient{} + } else { + client = image.NewDockerClient() + } + + if errs := pushImages(flags.plugins, flags.kubeconfig, flags.customRegistry, flags.e2eRegistryConfig, client); len(errs) > 0 { for _, err := range errs { errlog.LogError(err) } @@ -117,6 +134,7 @@ func pushCmd() *cobra.Command { AddKubeconfigFlag(&flags.kubeconfig, pushCmd.Flags()) AddPluginListFlag(&flags.plugins, pushCmd.Flags()) AddCustomRegistryFlag(&flags.customRegistry, pushCmd.Flags()) + AddDryRunFlag(&flags.dryRun, pushCmd.Flags()) pushCmd.MarkFlagRequired(customRegistryFlag) return pushCmd @@ -128,7 +146,14 @@ func downloadCmd() *cobra.Command { Use: "download", Short: "Saves downloaded images from local docker client to a tar file", Run: func(cmd *cobra.Command, args []string) { - if err := downloadImages(flags.plugins, flags.kubeconfig, flags.e2eRegistryConfig); err != nil { + var client image.Client + if flags.dryRun { + client = image.DryRunClient{} + } else { + client = image.NewDockerClient() + } + + if err := downloadImages(flags.plugins, flags.kubeconfig, flags.e2eRegistryConfig, client); err != nil { errlog.LogError(err) os.Exit(1) } @@ -138,6 +163,7 @@ func downloadCmd() *cobra.Command { AddE2ERegistryConfigFlag(&flags.e2eRegistryConfig, downloadCmd.Flags()) AddKubeconfigFlag(&flags.kubeconfig, downloadCmd.Flags()) AddPluginListFlag(&flags.plugins, downloadCmd.Flags()) + AddDryRunFlag(&flags.dryRun, downloadCmd.Flags()) return downloadCmd } @@ -147,7 +173,14 @@ func deleteCmd() *cobra.Command { Use: "delete", Short: "Deletes all images downloaded to local docker client", Run: func(cmd *cobra.Command, args []string) { - if errs := deleteImages(flags.plugins, flags.kubeconfig, flags.e2eRegistryConfig); len(errs) > 0 { + var client image.Client + if flags.dryRun { + client = image.DryRunClient{} + } else { + client = image.NewDockerClient() + } + + if errs := deleteImages(flags.plugins, flags.kubeconfig, flags.e2eRegistryConfig, client); len(errs) > 0 { for _, err := range errs { errlog.LogError(err) } @@ -159,6 +192,7 @@ func deleteCmd() *cobra.Command { AddE2ERegistryConfigFlag(&flags.e2eRegistryConfig, deleteCmd.Flags()) AddKubeconfigFlag(&flags.kubeconfig, deleteCmd.Flags()) AddPluginListFlag(&flags.plugins, deleteCmd.Flags()) + AddDryRunFlag(&flags.dryRun, deleteCmd.Flags()) return deleteCmd } @@ -209,7 +243,7 @@ func listImages(plugins []string, kubeconfig Kubeconfig) error { return nil } -func pullImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig string) []error { +func pullImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig string, client image.Client) []error { images := []string{ config.DefaultImage, } @@ -235,11 +269,10 @@ func pullImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig strin } } - imageClient := image.NewImageClient() - return imageClient.PullImages(images, numDockerRetries) + return client.PullImages(images, numDockerRetries) } -func downloadImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig string) error { +func downloadImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig string, client image.Client) error { for _, plugin := range plugins { switch plugin { case e2ePlugin: @@ -253,8 +286,7 @@ func downloadImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig s return errors.Wrap(err, "couldn't get images") } - imageClient := image.NewImageClient() - fileName, err := imageClient.DownloadImages(images, version) + fileName, err := client.DownloadImages(images, version) if err != nil { return err } @@ -269,7 +301,7 @@ func downloadImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig s return nil } -func pushImages(plugins []string, kubeconfig Kubeconfig, customRegistry, e2eRegistryConfig string) []error { +func pushImages(plugins []string, kubeconfig Kubeconfig, customRegistry, e2eRegistryConfig string, client image.Client) []error { imagePairs := []image.TagPair{ { Src: config.DefaultImage, @@ -305,11 +337,10 @@ func pushImages(plugins []string, kubeconfig Kubeconfig, customRegistry, e2eRegi } } - imageClient := image.NewImageClient() - return imageClient.PushImages(imagePairs, numDockerRetries) + return client.PushImages(imagePairs, numDockerRetries) } -func deleteImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig string) []error { +func deleteImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig string, client image.Client) []error { images := []string{ config.DefaultImage, } @@ -335,8 +366,7 @@ func deleteImages(plugins []string, kubeconfig Kubeconfig, e2eRegistryConfig str } } - imageClient := image.NewImageClient() - return imageClient.DeleteImages(images, numDockerRetries) + return client.DeleteImages(images, numDockerRetries) } func substituteRegistry(image string, customRegistry string) string { diff --git a/pkg/image/docker_client.go b/pkg/image/docker_client.go new file mode 100644 index 000000000..6740be12d --- /dev/null +++ b/pkg/image/docker_client.go @@ -0,0 +1,115 @@ +/* +Copyright the Sonobuoy contributors 2019 + +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 image + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/vmware-tanzu/sonobuoy/pkg/image/docker" +) + +// DockerClient is an implementation of Client that uses the local docker installation +// to interact with images. +type DockerClient struct { + dockerClient docker.Docker +} + +// TagPair represents a source image and a destination image that it will be tagged and +// pushed as. +type TagPair struct { + Src string + Dst string +} + +// NewDockerClient returns a DockerClient that can interact with the local docker installation. +func NewDockerClient() Client { + return DockerClient{ + dockerClient: docker.LocalDocker{}, + } +} + +// PullImages pulls the given list of images, skipping if they are already present on the machine. +// It will retry for the provided number of retries on failure. +func (i DockerClient) PullImages(images []string, retries int) []error { + errs := []error{} + for _, image := range images { + err := i.dockerClient.PullIfNotPresent(image, retries) + if err != nil { + errs = append(errs, errors.Wrapf(err, "couldn't pull image: %v", image)) + } + } + return errs +} + +// PushImages will tag each of the source images as the destination image and push. +// It will skip the operation if the image source and destination are equal. +// It will retry for the provided number of retries on failure. +func (i DockerClient) PushImages(images []TagPair, retries int) []error { + errs := []error{} + for _, image := range images { + // Skip if the source/dest are equal + if image.Src == image.Dst { + fmt.Printf("Skipping public image: %s\n", image.Src) + continue + } + + err := i.dockerClient.Tag(image.Src, image.Dst, retries) + if err != nil { + errs = append(errs, errors.Wrapf(err, "couldn't tag image %q as %q", image.Src, image.Dst)) + } + + err = i.dockerClient.Push(image.Dst, retries) + if err != nil { + errs = append(errs, errors.Wrapf(err, "couldn't push image: %v", image.Dst)) + } + } + return errs +} + +// DownloadImages exports the list of images to a tar file. The provided version will be included in the +// resulting file name. +func (i DockerClient) DownloadImages(images []string, version string) (string, error) { + fileName := getTarFileName(version) + + err := i.dockerClient.Save(images, fileName) + if err != nil { + return "", errors.Wrap(err, "couldn't save images to tar") + } + + return fileName, nil +} + +// DeleteImages deletes the given list of images from the local machine. +// It will retry for the provided number of retries on failure. +func (i DockerClient) DeleteImages(images []string, retries int) []error { + errs := []error{} + + for _, image := range images { + err := i.dockerClient.Rmi(image, retries) + if err != nil { + errs = append(errs, errors.Wrapf(err, "couldn't delete image: %v", image)) + } + } + + return errs +} + +// getTarFileName returns a filename matching the version of Kubernetes images are exported +func getTarFileName(version string) string { + return fmt.Sprintf("kubernetes_e2e_images_%s.tar", version) +} diff --git a/pkg/image/images_test.go b/pkg/image/docker_client_test.go similarity index 97% rename from pkg/image/images_test.go rename to pkg/image/docker_client_test.go index cf71fca39..9a0040cf2 100644 --- a/pkg/image/images_test.go +++ b/pkg/image/docker_client_test.go @@ -132,7 +132,7 @@ func TestPushImages(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - imgClient := ImageClient{ + imgClient := DockerClient{ dockerClient: tc.client, } @@ -177,7 +177,7 @@ func TestPullImages(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - imgClient := ImageClient{ + imgClient := DockerClient{ dockerClient: tc.client, } @@ -217,7 +217,7 @@ func TestDownloadImages(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - imgClient := ImageClient{ + imgClient := DockerClient{ dockerClient: tc.client, } @@ -255,7 +255,7 @@ func TestDeleteImages(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - imgClient := ImageClient{ + imgClient := DockerClient{ dockerClient: tc.client, } diff --git a/pkg/image/dryrun_client.go b/pkg/image/dryrun_client.go new file mode 100644 index 000000000..5d79ff805 --- /dev/null +++ b/pkg/image/dryrun_client.go @@ -0,0 +1,56 @@ +/* +Copyright the Sonobuoy contributors 2020 + +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 image + +import ( + "github.com/sirupsen/logrus" +) + +// DryRunClient is an implementation of Client that logs the image operations that would +// be performed rather than performing them. +type DryRunClient struct{} + +// PullImages logs the images that would be pulled. +func (i DryRunClient) PullImages(images []string, retries int) []error { + for _, image := range images { + logrus.Infof("Pulling image: %s", image) + } + return []error{} +} + +// PushImages logs what the images would be tagged and pushed as. +func (i DryRunClient) PushImages(images []TagPair, retries int) []error { + for _, image := range images { + logrus.Infof("Tagging image: %s as %s", image.Src, image.Dst) + logrus.Infof("Pushing image: %s", image.Dst) + } + return []error{} +} + +// DownloadImages logs that the images would be saved and returns the tarball name. +func (i DryRunClient) DownloadImages(images []string, version string) (string, error) { + logrus.Info("Saving images") + return getTarFileName(version), nil +} + +// DeleteImages logs which images would be deleted. +func (i DryRunClient) DeleteImages(images []string, retries int) []error { + for _, image := range images { + logrus.Infof("Deleting image: %s\n", image) + } + return []error{} +} diff --git a/pkg/image/image.go b/pkg/image/image.go index 58139e921..afd4d10e2 100644 --- a/pkg/image/image.go +++ b/pkg/image/image.go @@ -1,5 +1,5 @@ /* -Copyright the Sonobuoy contributors 2019 +Copyright the Sonobuoy contributors 2020 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,86 +16,10 @@ limitations under the License. package image -import ( - "fmt" - - "github.com/pkg/errors" - "github.com/vmware-tanzu/sonobuoy/pkg/image/docker" -) - -type ImageClient struct { - dockerClient docker.Docker -} - -type TagPair struct { - Src string - Dst string -} - -func NewImageClient() ImageClient { - return ImageClient{ - dockerClient: docker.LocalDocker{}, - } -} - -func (i ImageClient) PullImages(images []string, retries int) []error { - errs := []error{} - for _, image := range images { - err := i.dockerClient.PullIfNotPresent(image, retries) - if err != nil { - errs = append(errs, errors.Wrapf(err, "couldn't pull image: %v", image)) - } - } - return errs -} - -func (i ImageClient) PushImages(images []TagPair, retries int) []error { - errs := []error{} - for _, image := range images { - // Skip if the source/dest are equal - if image.Src == image.Dst { - fmt.Printf("Skipping public image: %s\n", image.Src) - continue - } - - err := i.dockerClient.Tag(image.Src, image.Dst, retries) - if err != nil { - errs = append(errs, errors.Wrapf(err, "couldn't tag image %q as %q", image.Src, image.Dst)) - } - - err = i.dockerClient.Push(image.Dst, retries) - if err != nil { - errs = append(errs, errors.Wrapf(err, "couldn't push image: %v", image.Dst)) - } - } - return errs -} - -func (i ImageClient) DownloadImages(images []string, version string) (string, error) { - fileName := getTarFileName(version) - - err := i.dockerClient.Save(images, fileName) - if err != nil { - return "", errors.Wrap(err, "couldn't save images to tar") - } - - return fileName, nil -} - -func (i ImageClient) DeleteImages(images []string, retries int) []error { - errs := []error{} - - for _, image := range images { - err := i.dockerClient.Rmi(image, retries) - if err != nil { - errs = append(errs, errors.Wrapf(err, "couldn't delete image: %v", image)) - } - } - - return errs -} - -// getTarFileName returns a filename matching the version of Kubernetes images are exported -func getTarFileName(version string) string { - return fmt.Sprintf("kubernetes_e2e_images_%s.tar", version) +// Client is the interface for interacting with images. +type Client interface { + PullImages(images []string, retries int) []error + PushImages(images []TagPair, retries int) []error + DownloadImages(images []string, version string) (string, error) + DeleteImages(images []string, retries int) []error }