Skip to content

Commit

Permalink
Add a dry run option for the image commands
Browse files Browse the repository at this point in the history
Signed-off-by: Bridget McErlean <bmcerlean@vmware.com>
  • Loading branch information
zubron committed Jan 7, 2020
1 parent 5ea9c23 commit 7a0296e
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 103 deletions.
8 changes: 8 additions & 0 deletions cmd/sonobuoy/app/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
}
62 changes: 46 additions & 16 deletions cmd/sonobuoy/app/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type imagesFlags struct {
plugins []string
kubeconfig Kubeconfig
customRegistry string
dryRun bool
}

func NewCmdImages() *cobra.Command {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}
Expand All @@ -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)
}
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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
}

Expand All @@ -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)
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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,
}
Expand All @@ -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:
Expand All @@ -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
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
}
Expand All @@ -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 {
Expand Down
115 changes: 115 additions & 0 deletions pkg/image/docker_client.go
Original file line number Diff line number Diff line change
@@ -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)
}
8 changes: 4 additions & 4 deletions pkg/image/images_test.go → pkg/image/docker_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
}

Expand Down
56 changes: 56 additions & 0 deletions pkg/image/dryrun_client.go
Original file line number Diff line number Diff line change
@@ -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{}
}
Loading

0 comments on commit 7a0296e

Please sign in to comment.