diff --git a/cmd/dt/unwrap/unwrap.go b/cmd/dt/unwrap/unwrap.go index 9b47052..f2ce1cd 100644 --- a/cmd/dt/unwrap/unwrap.go +++ b/cmd/dt/unwrap/unwrap.go @@ -47,7 +47,9 @@ type Config struct { Auth Auth ContainerRegistryAuth Auth - SayYes bool + // Interactive enables interacting with the user + Interactive bool + SayYes bool } // Auth defines the authentication information to access the container registry @@ -90,6 +92,13 @@ func WithKeepArtifacts(keepArtifacts bool) func(c *Config) { } } +// WithInteractive configures the Interactive of the WrapConfig +func WithInteractive(interactive bool) func(c *Config) { + return func(c *Config) { + c.Interactive = interactive + } +} + // ShouldFetchChartArtifacts returns true if the chart artifacts should be fetched func (c *Config) ShouldFetchChartArtifacts(inputChart string) bool { if chartutils.IsRemoteChart(inputChart) { @@ -204,10 +213,21 @@ func NewConfig(opts ...Option) *Config { } // Chart unwraps a Helm chart -func Chart(inputChart, registryURL, pushChartURL string, opts ...Option) error { +func Chart(inputChart, registryURL, pushChartURL string, opts ...Option) (string, error) { return unwrapChart(inputChart, registryURL, pushChartURL, opts...) } -func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) error { + +func askYesNoQuestion(msg string, cfg *Config) bool { + if cfg.SayYes { + return true + } + if !cfg.Interactive { + return false + } + return widgets.ShowYesNoQuestion(msg) +} + +func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) (string, error) { cfg := NewConfig(opts...) @@ -215,11 +235,11 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) e parentLog := cfg.GetLogger() if registryURL == "" { - return fmt.Errorf("the registry cannot be empty") + return "", 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 "", fmt.Errorf("failed to create temporary directory: %w", err) } l := parentLog.StartSection(fmt.Sprintf("Unwrapping Helm chart %q", inputChart)) @@ -239,12 +259,12 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) e ), ) if err != nil { - return err + return "", err } wrap, err := wrapping.Load(chartPath) if err != nil { - return err + return "", err } if err := l.ExecuteStep(fmt.Sprintf("Relocating %q with prefix %q", wrap.ChartDir(), registryURL), func() error { return relocator.RelocateChartDir( @@ -252,24 +272,28 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) e relocator.Recursive, relocator.WithAnnotationsKey(cfg.AnnotationsKey), ) }); err != nil { - return l.Failf("failed to relocate %q: %w", chartPath, err) + return "", l.Failf("failed to relocate %q: %w", chartPath, err) } l.Infof("Helm chart relocated successfully") - lenImages := showImagesSummary(wrap, l) + images := getImageList(wrap, l) - if lenImages > 0 && (cfg.SayYes || widgets.ShowYesNoQuestion(l.PrefixText("Do you want to push the wrapped images to the OCI registry?"))) { - if err := l.Section("Pushing Images", func(subLog log.SectionLogger) error { - return pushChartImagesAndVerify(ctx, wrap, cfg) - }); err != nil { - return l.Failf("Failed to push images: %w", err) + if len(images) > 0 { + // If we are not in interactive mode, we do not show the list of images + if cfg.Interactive { + showImagesSummary(images, l) + } + 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))...)) + }); err != nil { + return "", l.Failf("Failed to push images: %w", err) + } + l.Printf(widgets.TerminalSpacer) } - l.Printf(widgets.TerminalSpacer) } - var successMessage = "Helm chart unwrapped successfully" - - if cfg.SayYes || widgets.ShowYesNoQuestion(l.PrefixText("Do you want to push the Helm chart to the OCI registry?")) { + 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 @@ -286,19 +310,13 @@ func unwrapChart(inputChart, registryURL, pushChartURL string, opts ...Option) e return pushChart(ctx, wrap, pushChartURL, cfg) }) }); err != nil { - return l.Failf("Failed to push Helm chart: %w", err) + return "", l.Failf("Failed to push Helm chart: %w", err) } l.Infof("Helm chart successfully pushed") - - successMessage = fmt.Sprintf(`%s: You can use it now by running "helm install %s --generate-name"`, successMessage, fullChartURL) + return fullChartURL, nil } - - l.Printf(widgets.TerminalSpacer) - - parentLog.Successf(successMessage) - - return nil + return "", nil } func pushChartImagesAndVerify(ctx context.Context, wrap wrapping.Wrap, cfg *Config) error { @@ -334,24 +352,28 @@ func pushChartImagesAndVerify(ctx context.Context, wrap wrapping.Wrap, cfg *Conf return nil } -func showImagesSummary(wrap wrapping.Lockable, l log.SectionLogger) int { +func getImageList(wrap wrapping.Lockable, l log.SectionLogger) imagelock.ImageList { lock, err := wrap.GetImagesLock() + if err != nil { l.Debugf("failed to load list of images: failed to load lock file: %v", err) - return 0 + return imagelock.ImageList{} } if len(lock.Images) == 0 { l.Warnf("The bundle does not include any image") - return 0 + return imagelock.ImageList{} } - _ = l.Section(fmt.Sprintf("The wrap includes the following %d images:\n", len(lock.Images)), func(log.SectionLogger) error { - for _, img := range lock.Images { + return lock.Images +} + +func showImagesSummary(images imagelock.ImageList, l log.SectionLogger) { + _ = l.Section(fmt.Sprintf("The wrap includes the following %d images:\n", len(images)), func(log.SectionLogger) error { + for _, img := range images { l.Printf(img.Image) } l.Printf(widgets.TerminalSpacer) return nil }) - return len(lock.Images) } func normalizeOCIURL(url string) string { @@ -429,15 +451,26 @@ func NewCmd(cfg *config.Config) *cobra.Command { if err != nil { return fmt.Errorf("failed to create temporary directory: %v", err) } - return unwrapChart(inputChart, registryURL, pushChartURL, + fullChartURL, err := unwrapChart(inputChart, registryURL, pushChartURL, WithLogger(l), WithSayYes(sayYes), WithContext(ctx), WithVersion(version), + WithInteractive(true), WithInsecure(cfg.Insecure), WithTempDirectory(tempDir), WithUsePlainHTTP(cfg.UsePlainHTTP), ) + 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) + } + l.Printf(widgets.TerminalSpacer) + l.Successf(successMessage) + return nil }, } diff --git a/cmd/dt/unwrap_test.go b/cmd/dt/unwrap_test.go index d3f2962..7b46a77 100644 --- a/cmd/dt/unwrap_test.go +++ b/cmd/dt/unwrap_test.go @@ -59,7 +59,8 @@ 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), } - require.NoError(t, unwrap.Chart(inputChart, targetRegistry, chartTargetRegistry, opts...)) + _, err := unwrap.Chart(inputChart, targetRegistry, chartTargetRegistry, opts...) + require.NoError(t, err) } else { dt(args...).AssertSuccessMatch(t, "") } @@ -256,7 +257,8 @@ func (suite *CmdSuite) TestUnwrapCommand() { unwrap.WithSayYes(true), unwrap.WithContainerRegistryAuth(username, password), } - require.NoError(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 875c0e4..34b8590 100644 --- a/cmd/dt/wrap/wrap.go +++ b/cmd/dt/wrap/wrap.go @@ -46,6 +46,7 @@ type Config struct { FetchArtifacts bool Auth Auth ContainerRegistryAuth Auth + OutputFile string } // WithKeepArtifacts configures the KeepArtifacts of the WrapConfig @@ -55,6 +56,13 @@ func WithKeepArtifacts(keepArtifacts bool) func(c *Config) { } } +// WithOutputFile configures the OutputFile of the WrapConfig +func WithOutputFile(outputFile string) func(c *Config) { + return func(c *Config) { + c.OutputFile = outputFile + } +} + // WithAuth configures the Auth of the wrap Config func WithAuth(username, password string) func(c *Config) { return func(c *Config) { @@ -195,8 +203,8 @@ func NewConfig(opts ...Option) *Config { } // Chart wraps a Helm chart -func Chart(inputPath string, outputFile string, opts ...Option) error { - return wrapChart(inputPath, outputFile, opts...) +func Chart(inputPath string, opts ...Option) (string, error) { + return wrapChart(inputPath, opts...) } // ResolveInputChartPath resolves the input chart into a local uncompressed chart path @@ -344,7 +352,7 @@ func pullImages(wrap wrapping.Wrap, cfg *Config) error { return nil } -func wrapChart(inputPath string, outputFile string, opts ...Option) error { +func wrapChart(inputPath string, opts ...Option) (string, error) { cfg := NewConfig(opts...) ctx := cfg.Context @@ -356,17 +364,17 @@ func wrapChart(inputPath string, outputFile string, opts ...Option) error { chartPath, err := ResolveInputChartPath(inputPath, subCfg) if err != nil { - return err + return "", err } tmpDir, err := cfg.GetTemporaryDirectory() if err != nil { - return fmt.Errorf("failed to create temporary directory: %w", err) + return "", fmt.Errorf("failed to create temporary directory: %w", err) } wrap, err := wrapping.Create(chartPath, filepath.Join(tmpDir, "wrap"), chartutils.WithAnnotationsKey(cfg.AnnotationsKey), ) if err != nil { - return l.Failf("failed to create wrap: %v", err) + return "", l.Failf("failed to create wrap: %v", err) } chart := wrap.Chart() @@ -374,15 +382,17 @@ func wrapChart(inputPath string, outputFile string, opts ...Option) error { if cfg.ShouldFetchChartArtifacts(inputPath) { chartURL := fmt.Sprintf("%s:%s", inputPath, chart.Version()) if err := fetchArtifacts(chartURL, filepath.Join(wrap.RootDir(), artifacts.HelmChartArtifactMetadataDir), subCfg); err != nil { - return err + return "", err } } chartRoot := chart.RootDir() if err := validateWrapLock(wrap, subCfg); err != nil { - return err + return "", err } + outputFile := cfg.OutputFile + if outputFile == "" { outputBaseName := fmt.Sprintf("%s-%s.wrap.tgz", chart.Name(), chart.Version()) if outputFile, err = filepath.Abs(outputBaseName); err != nil { @@ -391,7 +401,7 @@ func wrapChart(inputPath string, outputFile string, opts ...Option) error { } } if err := pullImages(wrap, subCfg); err != nil { - return err + return "", err } if cfg.Carvelize { @@ -402,7 +412,7 @@ func wrapChart(inputPath string, outputFile string, opts ...Option) error { chartutils.WithLog(childLog), ) }); err != nil { - return l.Failf("%w", err) + return "", l.Failf("%w", err) } l.Infof("Carvel bundle created successfully") } @@ -415,14 +425,11 @@ func wrapChart(inputPath string, outputFile string, opts ...Option) error { }) }, ); err != nil { - return l.Failf("failed to wrap Helm chart: %w", err) + return "", l.Failf("failed to wrap Helm chart: %w", err) } l.Infof("Compressed into %q", outputFile) - l.Printf(widgets.TerminalSpacer) - - parentLog.Successf("Helm chart wrapped into %q", outputFile) - return nil + return outputFile, nil } // NewCmd builds a new wrap command @@ -460,20 +467,27 @@ This command will pull all the container images and wrap it into a single tarbal parentLog := cfg.Logger() - if err := wrapChart(chartPath, outputFile, + wrappedChart, err := wrapChart(chartPath, WithLogger(parentLog), WithAnnotationsKey(cfg.AnnotationsKey), WithContext(ctx), WithPlatforms(platforms), WithVersion(version), WithFetchArtifacts(fetchArtifacts), WithCarvelize(carvelize), WithUsePlainHTTP(cfg.UsePlainHTTP), WithInsecure(cfg.Insecure), + WithOutputFile(outputFile), WithTempDirectory(tmpDir), - ); err != nil { + ) + + if err != nil { if _, ok := err.(*log.LoggedError); ok { // We already logged it, lets be less verbose return fmt.Errorf("failed to wrap Helm chart: %v", err) } return err } + + parentLog.Printf(widgets.TerminalSpacer) + parentLog.Successf("Helm chart wrapped into %q", wrappedChart) + return nil }, } diff --git a/cmd/dt/wrap_test.go b/cmd/dt/wrap_test.go index fa03b30..3243574 100644 --- a/cmd/dt/wrap_test.go +++ b/cmd/dt/wrap_test.go @@ -128,9 +128,11 @@ func testChartWrap(t *testing.T, sb *tu.Sandbox, inputChart string, expectedLock wrap.WithCarvelize(cfg.GenerateCarvelBundle), wrap.WithFetchArtifacts(cfg.FetchArtifacts), wrap.WithAuth(cfg.Auth.Username, cfg.Auth.Password), + wrap.WithOutputFile(expectedWrapFile), wrap.WithContainerRegistryAuth(cfg.ContainerRegistryAuth.Username, cfg.ContainerRegistryAuth.Password), } - require.NoError(t, wrap.Chart(inputChart, expectedWrapFile, opts...)) + _, err := wrap.Chart(inputChart, opts...) + require.NoError(t, err) } else { if len(cfg.Images) == 0 { dt(args...).AssertSuccessMatch(t, "No images found in Images.lock")