From a28ea75ebb92fa2e09cdc184f2af67de0ab18d1a Mon Sep 17 00:00:00 2001 From: Ryan Egesdahl Date: Fri, 8 Dec 2023 00:46:44 -0800 Subject: [PATCH] Add dry-run mode to copy skopeo-copy Dry-run functionality was merged into the sync command in #1608. Here, the same sort of functionality is added to the copy command as well. Signed-off-by: Ryan Egesdahl --- cmd/skopeo/copy.go | 12 ++++++++++++ docs/skopeo-copy.1.md | 4 ++++ integration/copy_test.go | 16 ++++++++++++++++ systemtest/020-copy.bats | 9 +++++++++ 4 files changed, 41 insertions(+) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 6a7e8013..a2575014 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -19,6 +19,7 @@ import ( "github.com/containers/image/v5/transports/alltransports" encconfig "github.com/containers/ocicrypt/config" enchelpers "github.com/containers/ocicrypt/helpers" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -39,6 +40,7 @@ type copyOptions struct { format commonFlag.OptionalString // Force conversion of the image to a specified format quiet bool // Suppress output information when copying images all bool // Copy all of the images if the source is a list + dryRun bool // Don't actually copy anything, just output what it would have done multiArch commonFlag.OptionalString // How to handle multi architecture images preserveDigests bool // Preserve digests during copy encryptLayer []int // The list of layers to encrypt @@ -82,6 +84,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images") 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 images") flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`) flags.BoolVar(&opts.preserveDigests, "preserve-digests", false, "Preserve digests of images and lists") flags.BoolVar(&opts.removeSignatures, "remove-signatures", false, "Do not copy signatures from SOURCE-IMAGE") @@ -126,6 +129,10 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) { opts.deprecatedTLSVerify.warnIfUsed([]string{"--src-tls-verify", "--dest-tls-verify"}) imageNames := args + if opts.dryRun { + logrus.Warn("Running in dry-run mode") + } + if err := reexecIfNecessaryForImages(imageNames...); err != nil { return err } @@ -282,6 +289,11 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) { opts.destImage.warnAboutIneffectiveOptions(destRef.Transport()) + if opts.dryRun { + logrus.Info(fmt.Sprintf("Would have copied from=%s to=%s", imageNames[0], imageNames[1])) + return nil + } + return retry.IfNecessary(ctx, func() error { manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ RemoveSignatures: opts.removeSignatures, diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index 43b007db..97a9e136 100644 --- a/docs/skopeo-copy.1.md +++ b/docs/skopeo-copy.1.md @@ -32,6 +32,10 @@ If _source-image_ refers to a list of images, instead of copying just the image architecture (subject to the use of the global --override-os, --override-arch and --override-variant options), attempt to copy all of the images in the list, and the list itself. +**--dry-run** + +Run the sync without actually copying data to the destination. + **--authfile** _path_ Path of the authentication file. Default is ${XDG_RUNTIME\_DIR}/containers/auth.json, which is set using `skopeo login`. diff --git a/integration/copy_test.go b/integration/copy_test.go index 639b62fb..f8602753 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "io" "io/fs" "log" "net/http" @@ -127,6 +128,13 @@ func (s *copySuite) TestCopyAllWithManifestListRoundTrip() { assert.Equal(t, "", out) } +func (s *copySuite) TestCopyDryRun() { + t := s.T() + dir := t.TempDir() + assertSkopeoSucceeds(t, "", "copy", "--dry-run", knownListImage, "dir:"+dir) + assertDirIsEmpty(t, dir) +} + func (s *copySuite) TestCopyAllWithManifestListConverge() { t := s.T() oci1 := t.TempDir() @@ -662,6 +670,14 @@ func assertSchema1DirImagesAreEqualExceptNames(t *testing.T, dir1, ref1, dir2, r assert.Equal(t, "", out) } +func assertDirIsEmpty(t *testing.T, dir string) { + d, err := os.Open(dir) + require.NoError(t, err) + defer d.Close() + _, err = d.Readdirnames(1) + assert.Equal(t, err, io.EOF) +} + // Streaming (skopeo copy) func (s *copySuite) TestCopyStreaming() { t := s.T() diff --git a/systemtest/020-copy.bats b/systemtest/020-copy.bats index 7ab3e65f..c7decf1a 100644 --- a/systemtest/020-copy.bats +++ b/systemtest/020-copy.bats @@ -158,6 +158,15 @@ function setup() { expect_output "amd64" } +@test "copy: --dry-run" { + local remote_image=docker://quay.io/libpod/busybox:latest + local dir=$TESTDIR/dir + + run_skopeo copy --dry-run $remote_image oci:$dir:latest + expect_output --substring "Running in dry-run mode" + expect_output --substring "Would have copied from=${remote_image} to=oci:${dir}:latest" +} + teardown() { podman rm -f reg