From c233a6dcb1ba9930ca3e3117ce76d79fcacd2301 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Mon, 28 Mar 2022 08:40:12 -0400 Subject: [PATCH] Add dry-run mode to skopeo-sync Taking over #1459 to drive it to completion. Signed-off-by: Ted Wexler Signed-off-by: Daniel J Walsh --- cmd/skopeo/sync.go | 44 ++++++++++++++++++++++++++-------------- completions/bash/skopeo | 1 + docs/skopeo-sync.1.md | 4 ++++ systemtest/080-sync.bats | 26 ++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 systemtest/080-sync.bats diff --git a/cmd/skopeo/sync.go b/cmd/skopeo/sync.go index a7dd4814..bdae25f8 100644 --- a/cmd/skopeo/sync.go +++ b/cmd/skopeo/sync.go @@ -42,6 +42,7 @@ type syncOptions struct { destination string // Destination registry name scoped bool // When true, namespace copied images at destination using the source repository name all bool // Copy all of the images if an image in the source is a list + dryRun bool // Don't actually copy anything, just output what it would have done preserveDigests bool // Preserve digests during sync keepGoing bool // Whether or not to abort the sync if there are any errors during syncing the images } @@ -110,6 +111,7 @@ See skopeo-sync(1) for details. flags.StringVarP(&opts.destination, "dest", "d", "", "DESTINATION transport type") flags.BoolVar(&opts.scoped, "scoped", false, "Images at DESTINATION are prefix using the full source image path as scope") flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list") + flags.BoolVar(&opts.dryRun, "dry-run", false, "Run without actually copying data") flags.BoolVar(&opts.preserveDigests, "preserve-digests", false, "Preserve digests of images and lists") flags.BoolVarP(&opts.keepGoing, "keep-going", "", false, "Do not abort the sync if any image copy fails") flags.AddFlagSet(&sharedFlags) @@ -597,6 +599,10 @@ func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) { } errorsPresent := false imagesNumber := 0 + if opts.dryRun { + logrus.Warn("Running in dry-run mode") + } + for _, srcRepo := range srcRepoList { options.SourceCtx = srcRepo.Context for counter, ref := range srcRepo.ImageRefs { @@ -623,29 +629,37 @@ func (opts *syncOptions) run(args []string, stdout io.Writer) (retErr error) { return err } - logrus.WithFields(logrus.Fields{ + fromToFields := logrus.Fields{ "from": transports.ImageName(ref), "to": transports.ImageName(destRef), - }).Infof("Copying image ref %d/%d", counter+1, len(srcRepo.ImageRefs)) - - if err = retry.RetryIfNecessary(ctx, func() error { - _, err = copy.Image(ctx, policyContext, destRef, ref, &options) - return err - }, opts.retryOpts); err != nil { - if !opts.keepGoing { - return errors.Wrapf(err, "Error copying ref %q", transports.ImageName(ref)) + } + if opts.dryRun { + logrus.WithFields(fromToFields).Infof("Would have copied image ref %d/%d", counter+1, len(srcRepo.ImageRefs)) + } else { + logrus.WithFields(fromToFields).Infof("Copying image ref %d/%d", counter+1, len(srcRepo.ImageRefs)) + if err = retry.RetryIfNecessary(ctx, func() error { + _, err = copy.Image(ctx, policyContext, destRef, ref, &options) + return err + }, opts.retryOpts); err != nil { + if !opts.keepGoing { + return errors.Wrapf(err, "Error copying ref %q", transports.ImageName(ref)) + } + // log the error, keep a note that there was a failure and move on to the next + // image ref + errorsPresent = true + logrus.WithError(err).Errorf("Error copying ref %q", transports.ImageName(ref)) + continue } - // log the error, keep a note that there was a failure and move on to the next - // image ref - errorsPresent = true - logrus.WithError(err).Errorf("Error copying ref %q", transports.ImageName(ref)) - continue } imagesNumber++ } } - logrus.Infof("Synced %d images from %d sources", imagesNumber, len(srcRepoList)) + if opts.dryRun { + logrus.Infof("Would have synced %d images from %d sources", imagesNumber, len(srcRepoList)) + } else { + logrus.Infof("Synced %d images from %d sources", imagesNumber, len(srcRepoList)) + } if !errorsPresent { return nil } diff --git a/completions/bash/skopeo b/completions/bash/skopeo index a847669b..91d2ad9e 100644 --- a/completions/bash/skopeo +++ b/completions/bash/skopeo @@ -106,6 +106,7 @@ _skopeo_sync() { --all --dest-no-creds --dest-tls-verify + --dry-run --remove-signatures --scoped --src-no-creds diff --git a/docs/skopeo-sync.1.md b/docs/skopeo-sync.1.md index 0c3ab4f8..314cad2c 100644 --- a/docs/skopeo-sync.1.md +++ b/docs/skopeo-sync.1.md @@ -50,6 +50,10 @@ Path of the authentication file for the source registry. Uses path given by `--a Path of the authentication file for the destination registry. Uses path given by `--authfile`, if not provided. +**--dry-run** + +Run the sync without actually copying data to the destination. + **--src**, **-s** _transport_ Transport for the source repository. **--dest**, **-d** _transport_ Destination transport. diff --git a/systemtest/080-sync.bats b/systemtest/080-sync.bats new file mode 100644 index 00000000..413f7b3a --- /dev/null +++ b/systemtest/080-sync.bats @@ -0,0 +1,26 @@ +#!/usr/bin/env bats +# +# Sync tests +# + +load helpers + +function setup() { + standard_setup +} + +@test "sync: --dry-run" { + local remote_image=quay.io/libpod/busybox:latest + local dir=$TESTDIR/dir + + run_skopeo sync --dry-run --src docker --dest dir --scoped $remote_image $dir + expect_output --substring "Would have copied image" + expect_output --substring "from=\"docker://${remote_image}\" to=\"dir:${dir}/${remote_image}\"" + expect_output --substring "Would have synced 1 images from 1 sources" +} + +teardown() { + standard_teardown +} + +# vim: filetype=sh