diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8b6f80d..338a115 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,5 @@ +version: 2 + release: target_commitish: '{{ .Commit }}' builds: diff --git a/README.md b/README.md index 4f0bb28..1ab9cba 100644 --- a/README.md +++ b/README.md @@ -79,22 +79,22 @@ Wrapping a chart consists of packaging the chart into a tar.gz, including all co Even more exciting, we don't need to download the Helm chart for wrapping it. We can point the tool to any reachable Helm chart and the tool will take care of packaging and downloading everything for us. For example: ```console -$ helm dt wrap oci://docker.io/bitnamicharts/kibana +$ helm dt wrap oci://docker.io/bitnamicharts/kibana --version 11.2.23 » Wrapping Helm chart "oci://docker.io/bitnamicharts/kibana" ✔ Helm chart downloaded to "/var/folders/mn/j41xvgsx7l90_hn0hlwj9p180000gp/T/chart-1177363375/chart-1516625348/kibana" ✔ Images.lock file "/var/folders/mn/j41xvgsx7l90_hn0hlwj9p180000gp/T/chart-1177363375/chart-1516625348/kibana/Images.lock" does not exist ✔ Images.lock file written to "/var/folders/mn/j41xvgsx7l90_hn0hlwj9p180000gp/T/chart-1177363375/chart-1516625348/kibana/Images.lock" » Pulling images into "/var/folders/mn/j41xvgsx7l90_hn0hlwj9p180000gp/T/chart-1177363375/chart-1516625348/kibana/images" ✔ All images pulled successfully - ✔ Helm chart wrapped to "/tmp/workspace/kibana/kibana-10.4.8.wrap.tgz" - 🎉 Helm chart wrapped into "/tmp/workspace/kibana/kibana-10.4.8.wrap.tgz" + ✔ Helm chart wrapped to "/tmp/workspace/kibana/kibana-11.2.23.wrap.tgz" + 🎉 Helm chart wrapped into "/tmp/workspace/kibana/kibana-11.2.23.wrap.tgz" ``` Note that depending on the number of images needed by the Helm chart (remember, a wrap has the full set of image dependencies, not only the ones set on _values.yaml_) the size of the generated wrap might be considerably large: ```console -$ ls -l kibana-10.4.8.wrap.tgz --rw-r--r-- 1 martinpe staff 731200979 Aug 4 15:17 kibana-10.4.8.wrap.tgz +$ ls -l kibana-11.2.23.wrap.tgz +-rw-r--r-- 1 martinpe staff 731200979 Aug 4 15:17 kibana-11.2.23.wrap.tgz ``` If you want to make changes on the Helm chart, you can pass a directory to the wrap command. For example, if we wanted to wrap the previously pulled mariadb Helm chart, we could just do: @@ -120,14 +120,14 @@ Currently, `dt` supports moving artifacts that follow certain conventions. That For example: ```console -$ helm dt wrap --fetch-artifacts oci://docker.io/bitnamicharts/kibana +$ helm dt wrap --fetch-artifacts oci://docker.io/bitnamicharts/kibana --version 11.2.23 ... - 🎉 Helm chart wrapped into "/tmp/workspace/distribution-tooling-for-helm/kibana-10.4.8.wrap.tgz" + 🎉 Helm chart wrapped into "/tmp/workspace/distribution-tooling-for-helm/kibana-11.2.23.wrap.tgz" -$ tar -tzf "/tmp/workspace/distribution-tooling-for-helm/kibana-10.4.8.wrap.tgz" | grep artifacts -kibana-10.4.8/artifacts/images/kibana/kibana/8.10.4-debian-11-r0.sig -kibana-10.4.8/artifacts/images/kibana/kibana/8.10.4-debian-11-r0.metadata -kibana-10.4.8/artifacts/images/kibana/kibana/8.10.4-debian-11-r0.metadata.sig +$ tar -tzf "/tmp/workspace/distribution-tooling-for-helm/kibana-11.2.23.wrap.tgz" | grep artifacts +kibana-11.2.23/artifacts/images/kibana/kibana/8.15.2-debian-12-r0.sig +kibana-11.2.23/artifacts/images/kibana/kibana/8.15.2-debian-12-r0.metadata +kibana-11.2.23/artifacts/images/kibana/kibana/8.15.2-debian-12-r0.metadata.sig ... ``` @@ -156,14 +156,14 @@ Unwrapping a Helm chart can be done either to a local folder or to a target OCI At that moment your Helm chart will be ready to be used from the target registry without any dependencies to the source. By default, the tool will run in dry-run mode and require you to confirm actions but you can speed everything up with the `--yes` parameter. ```console -$ helm dt unwrap kibana-10.4.8.wrap.tgz demo.goharbor.io/helm-plugin/ --yes - » Unwrapping Helm chart "kibana-10.4.8.wrap.tgz" +$ helm dt unwrap kibana-11.2.23.wrap.tgz demo.goharbor.io/helm-plugin/ --yes --version 11.2.23 + » Unwrapping Helm chart "kibana-11.2.23.wrap.tgz" ✔ Helm chart uncompressed to "/var/folders/mn/j41xvgsx7l90_hn0hlwj9p180000gp/T/chart-586072428/at-wrap2428431258" ✔ Helm chart relocated successfully » The wrap includes the following 2 images: - demo.goharbor.io/helm-plugin/bitnami/kibana:8.9.0-debian-11-r9 - demo.goharbor.io/helm-plugin/bitnami/os-shell:11-debian-11-r25 + demo.goharbor.io/helm-plugin/bitnami/kibana:8.15.2-debian-12-r0 + demo.goharbor.io/helm-plugin/bitnami/os-shell:12-debian-12-r30 » Pushing Images ✔ All images pushed successfully @@ -176,6 +176,34 @@ $ helm dt unwrap kibana-10.4.8.wrap.tgz demo.goharbor.io/helm-plugin/ --yes If your wrap includes bundled artifacts (if you wrapped it using the `--fetch-artifacts` flag), they will be also pushed to the remote registry. +Unwrapping a Helm chart to a different target than the original using `--push-repository` flag. +By unwrapping the Helm chart to a target OCI registry the `dt` tool will unwrap the wrapped file, proceed to push the container +images into the registry you have specified by `--push-repository`, relocate the references from the Helm chart and preserve the original target +registry. +Finally will push the relocated Helm chart to the registry you have specified on `--push-repository` as well when `--push-chart-url` is not used. + + +```console +$ helm dt ./kibana-11.2.23.wrap.tgz demo.goharbor.io/read/helm-plugin --push-repository demo.goharbor.io/write/helm-plugin --push-chart-url demo.goharbor.io/write/helm-plugin/bitnami + » Unwrapping Helm chart "./kibana-11.2.23.wrap.tgz" + ✔ Helm chart uncompressed to "/var/folders/s1/7qw_wynd0ljdwf9l51dbs2cm0000gq/T/chart-1986792895/dt-wrap174906721" + ✔ Helm chart relocated successfully + » The wrap includes the following 2 images: + + demo.goharbor.io/read/helm-plugin/bitnami/kibana:8.15.2-debian-12-r0q + demo.goharbor.io/read/helm-plugin/bitnami/os-shell:12-debian-12-r30 + + Do you want to push the wrapped images to the OCI registry? [y/N]: Yes + » Pushing Images + ✔ All images pushed successfully + ✔ Chart "/var/folders/s1/7qw_wynd0ljdwf9l51dbs2cm0000gq/T/chart-1986792895/dt-wrap174906721/chart" lock is valid + + Do you want to push the Helm chart to the OCI registry? [y/N]: Yes + ✔ Helm chart successfully pushed + + 🎉 Helm chart unwrapped successfully: You can use it now by running "helm install oci://demo.goharbor.io/read/helm-plugin/kibana --generate-name --version 11.2.23" +``` + ## Advanced Usage That was all as per the basic most basic and powerful usage. If you're interested in some other additional goodies then we will dig next into some specific finer-grained commands. diff --git a/cmd/dt/annotate/annotate.go b/cmd/dt/annotate/annotate.go index e12a8e5..24213cb 100644 --- a/cmd/dt/annotate/annotate.go +++ b/cmd/dt/annotate/annotate.go @@ -12,6 +12,7 @@ import ( // NewCmd builds a new annotate command func NewCmd(cfg *config.Config) *cobra.Command { + return &cobra.Command{ Use: "annotate CHART_PATH", Short: "Annotates a Helm chart (Experimental)", @@ -32,7 +33,6 @@ Use it cautiously. Very often the complete list of images cannot be guessed from chartutils.WithAnnotationsKey(cfg.AnnotationsKey), chartutils.WithLog(l), ) - }) if err != nil { diff --git a/cmd/dt/push/push.go b/cmd/dt/push/push.go index 479d051..dbf9e10 100644 --- a/cmd/dt/push/push.go +++ b/cmd/dt/push/push.go @@ -15,17 +15,17 @@ import ( ) // ChartImages pushes the images from the Images.lock -func ChartImages(wrap wrapping.Wrap, imagesDir string, opts ...chartutils.Option) error { - return pushImages(wrap, imagesDir, opts...) +func ChartImages(wrap wrapping.Wrap, imagesDir string, registryURL string, pushRepository string, opts ...chartutils.Option) error { + return pushImages(wrap, imagesDir, registryURL, pushRepository, opts...) } -func pushImages(wrap wrapping.Wrap, imagesDir string, opts ...chartutils.Option) error { +func pushImages(wrap wrapping.Wrap, imagesDir string, registryURL string, pushRepository string, opts ...chartutils.Option) error { lock, err := wrap.GetImagesLock() if err != nil { return fmt.Errorf("failed to load Images.lock: %v", err) } - return chartutils.PushImages(lock, imagesDir, opts...) + return chartutils.PushImages(lock, imagesDir, registryURL, pushRepository, opts...) } // NewCmd builds a new push command @@ -62,6 +62,8 @@ func NewCmd(cfg *config.Config) *cobra.Command { if err := pushImages( chart, imagesDir, + "", + "", chartutils.WithLog(silent.NewLogger()), chartutils.WithContext(ctx), chartutils.WithProgressBar(subLog.ProgressBar()), diff --git a/cmd/dt/relocate/relocate.go b/cmd/dt/relocate/relocate.go index f5b2e8c..80dbd8f 100644 --- a/cmd/dt/relocate/relocate.go +++ b/cmd/dt/relocate/relocate.go @@ -11,6 +11,7 @@ import ( // NewCmd builds a new relocate command func NewCmd(cfg *config.Config) *cobra.Command { + valuesFiles := []string{"values.yaml"} skipImageRelocation := false cmd := &cobra.Command{ diff --git a/cmd/dt/unwrap/unwrap.go b/cmd/dt/unwrap/unwrap.go index 0e98be3..e55c146 100644 --- a/cmd/dt/unwrap/unwrap.go +++ b/cmd/dt/unwrap/unwrap.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "github.com/spf13/cobra" "github.com/vmware-labs/distribution-tooling-for-helm/cmd/dt/config" @@ -18,10 +19,8 @@ import ( "github.com/vmware-labs/distribution-tooling-for-helm/pkg/chartutils" "github.com/vmware-labs/distribution-tooling-for-helm/pkg/imagelock" "github.com/vmware-labs/distribution-tooling-for-helm/pkg/log" - "github.com/vmware-labs/distribution-tooling-for-helm/pkg/log/silent" - "github.com/vmware-labs/distribution-tooling-for-helm/pkg/log/logrus" - + "github.com/vmware-labs/distribution-tooling-for-helm/pkg/log/silent" "github.com/vmware-labs/distribution-tooling-for-helm/pkg/relocator" "github.com/vmware-labs/distribution-tooling-for-helm/pkg/utils" "github.com/vmware-labs/distribution-tooling-for-helm/pkg/wrapping" @@ -238,8 +237,8 @@ func NewConfig(opts ...Option) *Config { } // Chart unwraps a Helm chart -func Chart(inputChart, registryURL, pushChartURL string, opts ...Option) (string, error) { - return unwrapChart(inputChart, registryURL, pushChartURL, opts...) +func Chart(inputChart, registryURL, pushRepository string, pushChartURL string, opts ...Option) (string, *chartutils.Chart, error) { + return unwrapChart(inputChart, registryURL, pushRepository, pushChartURL, opts...) } func askYesNoQuestion(msg string, cfg *Config) bool { @@ -252,7 +251,7 @@ func askYesNoQuestion(msg string, cfg *Config) bool { return widgets.ShowYesNoQuestion(msg) } -func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) (string, error) { +func unwrapChart(inputChart, registryURL, pushRepository string, pushChartURL string, opts ...Option) (string, *chartutils.Chart, error) { cfg := NewConfig(opts...) @@ -260,11 +259,12 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) ( parentLog := cfg.GetLogger() if registryURL == "" { - return "", fmt.Errorf("the registry cannot be empty") + return "", nil, fmt.Errorf("the registry cannot be empty") } + tempDir, err := cfg.GetTemporaryDirectory() if err != nil { - return "", fmt.Errorf("failed to create temporary directory: %w", err) + return "", nil, fmt.Errorf("failed to create temporary directory: %w", err) } l := parentLog.StartSection(fmt.Sprintf("Unwrapping Helm chart %q", inputChart)) @@ -284,12 +284,12 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) ( ), ) if err != nil { - return "", err + return "", nil, err } wrap, err := wrapping.Load(chartPath) if err != nil { - return "", err + return "", nil, err } if err := l.ExecuteStep(fmt.Sprintf("Relocating %q with prefix %q", wrap.ChartDir(), registryURL), func() error { return relocator.RelocateChartDir( @@ -298,7 +298,7 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) ( relocator.WithSkipImageRelocation(cfg.SkipImageRelocation), ) }); err != nil { - return "", l.Failf("failed to relocate %q: %w", chartPath, err) + return "", nil, l.Failf("failed to relocate %q: %w", chartPath, err) } l.Infof("Helm chart relocated successfully") @@ -311,23 +311,27 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) ( } if askYesNoQuestion(l.PrefixText("Do you want to push the wrapped images to the OCI registry?"), cfg) { if err := l.Section("Pushing Images", func(subLog log.SectionLogger) error { - return pushChartImagesAndVerify(ctx, wrap, NewConfig(append(opts, WithLogger(subLog))...)) + return pushChartImagesAndVerify(ctx, wrap, registryURL, pushRepository, NewConfig(append(opts, WithLogger(subLog))...)) }); err != nil { - return "", l.Failf("Failed to push images: %w", err) + return "", nil, l.Failf("Failed to push images: %w", err) } l.Printf(widgets.TerminalSpacer) } } if askYesNoQuestion(l.PrefixText("Do you want to push the Helm chart to the OCI registry?"), cfg) { + if pushChartURL == "" { - pushChartURL = registryURL - // we will push the chart to the same registry as the containers + if pushRepository == "" { + // we will push the chart to the same registry as the containers + pushChartURL = registryURL + } else { + // we will push the chart to the same registry as defined by pushRepository + pushChartURL = pushRepository + } cfg.Auth = cfg.ContainerRegistryAuth } pushChartURL = normalizeOCIURL(pushChartURL) - fullChartURL := fmt.Sprintf("%s/%s", pushChartURL, wrap.Chart().Name()) - if err := l.ExecuteStep(fmt.Sprintf("Pushing Helm chart to %q", pushChartURL), func() error { return utils.ExecuteWithRetry(maxRetries, func(try int, prevErr error) error { if try > 0 { @@ -336,16 +340,24 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) ( return pushChart(ctx, wrap, pushChartURL, cfg) }) }); err != nil { - return "", l.Failf("Failed to push Helm chart: %w", err) + return "", nil, l.Failf("Failed to push Helm chart: %w", err) } l.Infof("Helm chart successfully pushed") - return fullChartURL, nil + + fullChartURL := fmt.Sprintf("%s/%s", pushChartURL, wrap.Chart().Name()) + + // point chart url to the registryURL if pushRepository is set + if pushRepository != "" && strings.Contains(pushChartURL, pushRepository) { + fullChartURL = strings.Replace(fullChartURL, pushRepository, registryURL, 1) + } + + return fullChartURL, wrap.Chart(), nil } - return "", nil + return "", wrap.Chart(), nil } -func pushChartImagesAndVerify(ctx context.Context, wrap wrapping.Wrap, cfg *Config) error { +func pushChartImagesAndVerify(ctx context.Context, wrap wrapping.Wrap, registryURL string, pushRepository string, cfg *Config) error { lockFile := wrap.LockFilePath() l := cfg.GetLogger() @@ -355,6 +367,8 @@ func pushChartImagesAndVerify(ctx context.Context, wrap wrapping.Wrap, cfg *Conf if err := push.ChartImages( wrap, wrap.ImagesDir(), + registryURL, + pushRepository, chartutils.WithLog(silent.NewLogger()), chartutils.WithContext(ctx), chartutils.WithArtifactsDir(wrap.ImageArtifactsDir()), @@ -454,10 +468,12 @@ func NewCmd(cfg *config.Config) *cobra.Command { var ( sayYes bool pushChartURL string + pushRepository string version string skipImageRelocation bool skipPullImages bool ) + valuesFiles := []string{"values.yaml"} cmd := &cobra.Command{ Use: "unwrap FILE OCI_URI", @@ -480,7 +496,8 @@ func NewCmd(cfg *config.Config) *cobra.Command { if err != nil { return fmt.Errorf("failed to create temporary directory: %v", err) } - fullChartURL, err := unwrapChart(inputChart, registryURL, pushChartURL, + + fullChartURL, chart, err := unwrapChart(inputChart, registryURL, pushRepository, pushChartURL, WithLogger(l), WithSayYes(sayYes), WithContext(ctx), @@ -496,9 +513,11 @@ func NewCmd(cfg *config.Config) *cobra.Command { if err != nil { return err } + var successMessage = "Helm chart unwrapped successfully" if fullChartURL != "" { - successMessage = fmt.Sprintf(`%s: You can use it now by running "helm install %s --generate-name"`, successMessage, fullChartURL) + //successMessage = fmt.Sprintf(`%s: You can use it now by running "helm install %s --generate-name"`, successMessage, fullChartURL) + successMessage = fmt.Sprintf(`%s: "helm install %s --generate-name --version %s"`, successMessage, fullChartURL, chart.Version()) } l.Printf(widgets.TerminalSpacer) l.Successf(successMessage) @@ -508,6 +527,7 @@ func NewCmd(cfg *config.Config) *cobra.Command { cmd.PersistentFlags().StringVar(&version, "version", version, "when unwrapping remote Helm charts from OCI, version to request") cmd.PersistentFlags().StringVar(&pushChartURL, "push-chart-url", pushChartURL, "push the unwrapped Helm chart to the given URL") + cmd.PersistentFlags().StringVar(&pushRepository, "push-repository", pushRepository, "the repository to be used instead for write operations e.g (demo.goharbor.io/write/test_repo)") cmd.PersistentFlags().BoolVar(&sayYes, "yes", sayYes, "respond 'yes' to any yes/no question") cmd.PersistentFlags().StringSliceVar(&valuesFiles, "values", valuesFiles, "values files to relocate images (can specify multiple)") cmd.PersistentFlags().BoolVar(&skipImageRelocation, "skip-image-relocation", skipImageRelocation, "Skip relocating image references in the different files") diff --git a/cmd/dt/unwrap_test.go b/cmd/dt/unwrap_test.go index 7b46a77..ad52ebf 100644 --- a/cmd/dt/unwrap_test.go +++ b/cmd/dt/unwrap_test.go @@ -59,7 +59,7 @@ func testChartUnwrap(t *testing.T, sb *tu.Sandbox, inputChart string, targetRegi unwrap.WithAuth(cfg.Auth.Username, cfg.Auth.Password), unwrap.WithContainerRegistryAuth(cfg.ContainerRegistryAuth.Username, cfg.ContainerRegistryAuth.Password), } - _, err := unwrap.Chart(inputChart, targetRegistry, chartTargetRegistry, opts...) + _, _, err := unwrap.Chart(inputChart, targetRegistry, "", chartTargetRegistry, opts...) require.NoError(t, err) } else { dt(args...).AssertSuccessMatch(t, "") @@ -257,7 +257,7 @@ func (suite *CmdSuite) TestUnwrapCommand() { unwrap.WithSayYes(true), unwrap.WithContainerRegistryAuth(username, password), } - _, err := unwrap.Chart(wrapDir, targetRegistry, "", opts...) + _, _, err := unwrap.Chart(wrapDir, targetRegistry, "", "", opts...) require.NoError(err) } else { dt(args...).AssertSuccessMatch(suite.T(), "") diff --git a/cmd/dt/wrap/wrap.go b/cmd/dt/wrap/wrap.go index c3a8de5..7fc29f0 100644 --- a/cmd/dt/wrap/wrap.go +++ b/cmd/dt/wrap/wrap.go @@ -448,8 +448,10 @@ func NewCmd(cfg *config.Config) *cobra.Command { var platforms []string var fetchArtifacts bool var carvelize bool + var skipPullImages bool var examples = ` # Wrap a Helm chart from a local folder + $ dt wrap examples/mariadb # Wrap a Helm chart in an OCI registry diff --git a/pkg/chartutils/images.go b/pkg/chartutils/images.go index e038600..4a43a16 100644 --- a/pkg/chartutils/images.go +++ b/pkg/chartutils/images.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" @@ -123,7 +124,7 @@ func PullImages(lock *imagelock.ImagesLock, imagesDir string, opts ...Option) er } // PushImages push the list of images in imagesDir to the destination specified in the ImagesLock -func PushImages(lock *imagelock.ImagesLock, imagesDir string, opts ...Option) error { +func PushImages(lock *imagelock.ImagesLock, imagesDir string, registryURL string, pushRepository string, opts ...Option) error { cfg := NewConfiguration(opts...) l := cfg.Log @@ -152,8 +153,22 @@ func PushImages(lock *imagelock.ImagesLock, imagesDir string, opts ...Option) er case <-ctx.Done(): return fmt.Errorf("cancelled execution") default: + + // registryURL is the original unwrap target + // pushRepository is the replacement target for registryURL when set + // this is not empty only when called from unwrap cmd + if pushRepository != "" && registryURL != "" { + ociPrefix := "oci://" + registryURL = strings.TrimPrefix(registryURL, ociPrefix) + pushRepository = strings.TrimPrefix(pushRepository, ociPrefix) + // replace prefix matching registryURL with pushRepository + imgData.Image = strings.Replace(imgData.Image, registryURL, pushRepository, 1) + l.Debugf("image: %v", imgData.Image) + } + p.Add(1) p.UpdateTitle(fmt.Sprintf("Pushing image %q", imgData.Image)) + err := utils.ExecuteWithRetry(maxRetries, func(try int, prevErr error) error { if try > 0 { // The context is done, so we are not retrying, just return the error @@ -265,6 +280,7 @@ func buildImageIndex(image *imagelock.ChartImage, imagesDir string) (v1.ImageInd } func pushImage(imgData *imagelock.ChartImage, imagesDir string, o crane.Options) error { + idx, err := buildImageIndex(imgData, imagesDir) if err != nil { return fmt.Errorf("failed to build image index: %w", err) @@ -290,6 +306,9 @@ func pullImage(image string, digest imagelock.DigestInfo, imagesDir string, o cr imgDir := getImageLayoutDir(imagesDir, digest) src := fmt.Sprintf("%s@%s", image, digest.Digest) + if strings.Contains(image, string(digest.Digest)) { + src = image + } ref, err := name.ParseReference(src, o.Name...) if err != nil { return "", fmt.Errorf("parsing reference %q: %w", src, err) diff --git a/pkg/chartutils/images_test.go b/pkg/chartutils/images_test.go index 935fdf7..e7965c5 100644 --- a/pkg/chartutils/images_test.go +++ b/pkg/chartutils/images_test.go @@ -158,7 +158,7 @@ func (suite *ChartUtilsTestSuite) TestPushImages() { require.NoError(err) lock, err := imagelock.FromYAMLFile(filepath.Join(chartDir, "Images.lock")) require.NoError(err) - require.NoError(PushImages(lock, imagesDir)) + require.NoError(PushImages(lock, imagesDir, "", "")) // Verify the images were pushed for _, img := range images {