diff --git a/src/cmd/common/table.go b/src/cmd/common/table.go index 0b98266fe7..596c9dc3af 100644 --- a/src/cmd/common/table.go +++ b/src/cmd/common/table.go @@ -6,16 +6,19 @@ package common import ( "context" "fmt" + "os" "path/filepath" "github.com/defenseunicorns/pkg/helpers/v2" "github.com/fatih/color" - "github.com/zarf-dev/zarf/src/pkg/lint" "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/message" ) +// OutputWriter provides a writer to stdout for user-focused output +var OutputWriter = os.Stdout + // PrintFindings prints the findings in the LintError as a table. func PrintFindings(ctx context.Context, lintErr *lint.LintError) { mapOfFindingsByPath := lint.GroupFindingsByPath(lintErr.Findings, lintErr.PackageName) @@ -42,9 +45,11 @@ func PrintFindings(ctx context.Context, lintErr *lint.LintError) { } else { packagePathFromUser = filepath.Join(lintErr.BaseDir, findings[0].PackagePathOverride) } + + // Print table to our OutputWriter message.Notef("Linting package %q at %s", findings[0].PackageNameOverride, packagePathFromUser) logger.From(ctx).Info("linting package", "name", findings[0].PackageNameOverride, "path", packagePathFromUser) - message.Table([]string{"Type", "Path", "Message"}, lintData) + message.TableWithWriter(OutputWriter, []string{"Type", "Path", "Message"}, lintData) } } diff --git a/src/cmd/connect.go b/src/cmd/connect.go index 358a9e1ac3..9a7a13a241 100644 --- a/src/cmd/connect.go +++ b/src/cmd/connect.go @@ -97,9 +97,6 @@ var connectListCmd = &cobra.Command{ if err != nil { return err } - // HACK: Re-initializing PTerm with a stderr writer isn't great, but it lets us render these - // tables for backwards compatibility - message.InitializePTerm(logger.DestinationDefault) message.PrintConnectStringTable(connections) return nil }, diff --git a/src/cmd/dev.go b/src/cmd/dev.go index 2bd49ee3f5..c7471d72ca 100644 --- a/src/cmd/dev.go +++ b/src/cmd/dev.go @@ -262,11 +262,6 @@ var devFindImagesCmd = &cobra.Command{ var lintErr *lint.LintError if errors.As(err, &lintErr) { - // HACK(mkcp): Re-initializing PTerm with a stderr writer isn't great, but it lets us render these lint - // tables below for backwards compatibility - if logger.Enabled(ctx) { - message.InitializePTerm(logger.DestinationDefault) - } common.PrintFindings(ctx, lintErr) } if err != nil { @@ -314,11 +309,6 @@ var devLintCmd = &cobra.Command{ err := lint.Validate(ctx, pkgConfig.CreateOpts.BaseDir, pkgConfig.CreateOpts.Flavor, pkgConfig.CreateOpts.SetVariables) var lintErr *lint.LintError if errors.As(err, &lintErr) { - // HACK(mkcp): Re-initializing PTerm with a stderr writer isn't great, but it lets us render these lint - // tables below for backwards compatibility - if logger.Enabled(ctx) { - message.InitializePTerm(logger.DestinationDefault) - } common.PrintFindings(ctx, lintErr) // Do not return an error if the findings are all warnings. if lintErr.OnlyWarnings() { diff --git a/src/cmd/package.go b/src/cmd/package.go index 5ab1f4e2b9..52b640279c 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -14,8 +14,6 @@ import ( "runtime" "strings" - "github.com/zarf-dev/zarf/src/pkg/logger" - "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/pkg/helpers/v2" "github.com/spf13/cobra" @@ -29,6 +27,7 @@ import ( "github.com/zarf-dev/zarf/src/internal/packager2" "github.com/zarf-dev/zarf/src/pkg/cluster" "github.com/zarf-dev/zarf/src/pkg/lint" + "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/message" "github.com/zarf-dev/zarf/src/pkg/packager" "github.com/zarf-dev/zarf/src/pkg/packager/filters" @@ -237,9 +236,6 @@ var packageInspectCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to inspect package: %w", err) } - // HACK(mkcp): This init call ensures we still can still print Yaml when message is disabled. Remove when we - // release structured logged and don't have to disable message globally in pre-run. - message.InitializePTerm(logger.DestinationDefault) err = utils.ColorPrintYAML(output, nil, false) if err != nil { return err @@ -281,12 +277,8 @@ var packageListCmd = &cobra.Command{ }) } - // NOTE(mkcp): Renders table with message. header := []string{"Package", "Version", "Components"} - // HACK(mkcp): Similar to `package inspect`, we do want to use message here but we have to make sure our feature - // flagging doesn't disable this. Nothing happens after this so it's safe, but still very hacky. - message.InitializePTerm(logger.DestinationDefault) - message.Table(header, packageData) + message.TableWithWriter(message.OutputWriter, header, packageData) // Print out any unmarshalling errors if err != nil { diff --git a/src/cmd/root.go b/src/cmd/root.go index 1e02891796..27957c6299 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -40,6 +40,8 @@ var ( SkipLogFile bool // NoColor is a flag to disable colors in output NoColor bool + // OutputWriter provides a default writer to Stdout for user-facing command output + OutputWriter = os.Stdout ) var rootCmd = &cobra.Command{ diff --git a/src/pkg/message/connect.go b/src/pkg/message/connect.go index 499a9e3602..755ccf764a 100644 --- a/src/pkg/message/connect.go +++ b/src/pkg/message/connect.go @@ -22,6 +22,6 @@ func PrintConnectStringTable(connectStrings types.ConnectStrings) { // Create the table output with the data header := []string{"Connect Command", "Description"} - Table(header, connectData) + TableWithWriter(OutputWriter, header, connectData) } } diff --git a/src/pkg/message/credentials.go b/src/pkg/message/credentials.go index 998fa41283..42f472c62b 100644 --- a/src/pkg/message/credentials.go +++ b/src/pkg/message/credentials.go @@ -55,7 +55,7 @@ func PrintCredentialTable(state *types.ZarfState, componentsToDeploy []types.Dep if len(loginData) > 0 { header := []string{"Application", "Username", "Password", "Connect", "Get-Creds Key"} - Table(header, loginData) + TableWithWriter(OutputWriter, header, loginData) } } diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go index 0eea8b326c..22d49df673 100644 --- a/src/pkg/message/message.go +++ b/src/pkg/message/message.go @@ -33,17 +33,18 @@ const ( TermWidth = 100 ) -// NoProgress tracks whether spinner/progress bars show updates. -var NoProgress bool - -// RuleLine creates a line of ━ as wide as the terminal -var RuleLine = strings.Repeat("━", TermWidth) - -// logLevel holds the pterm compatible log level integer -var logLevel = InfoLevel - -// logFile acts as a buffer for logFile generation -var logFile *PausableWriter +var ( + // NoProgress tracks whether spinner/progress bars show updates. + NoProgress bool + // RuleLine creates a line of ━ as wide as the terminal + RuleLine = strings.Repeat("━", TermWidth) + // OutputWriter provides a default writer to Stdout for user-focused output like tables and yaml + OutputWriter = os.Stdout + // logLevel holds the pterm compatible log level integer + logLevel = InfoLevel + // logFile acts as a buffer for logFile generation + logFile *PausableWriter +) // DebugWriter represents a writer interface that writes to message.Debug type DebugWriter struct{} @@ -249,6 +250,11 @@ func Paragraphn(n int, format string, a ...any) string { // Table prints a padded table containing the specified header and data func Table(header []string, data [][]string) { + TableWithWriter(nil, header, data) +} + +// TableWithWriter prints a padded table containing the specified header and data to the optional writer. +func TableWithWriter(writer io.Writer, header []string, data [][]string) { pterm.Println() // To avoid side effects make copies of the header and data before adding padding @@ -271,8 +277,12 @@ func Table(header []string, data [][]string) { table = append(table, pterm.TableData{row}...) } - //nolint:errcheck // never returns an error - pterm.DefaultTable.WithHasHeader().WithData(table).Render() + // Use DefaultTable writer if none is provided + tPrinter := pterm.DefaultTable + if writer != nil { + tPrinter.Writer = writer + } + _ = tPrinter.WithHasHeader().WithData(table).Render() //nolint:errcheck } func debugPrinter(offset int, a ...any) { diff --git a/src/pkg/utils/yaml.go b/src/pkg/utils/yaml.go index e0b8a3e953..1c19f070b5 100644 --- a/src/pkg/utils/yaml.go +++ b/src/pkg/utils/yaml.go @@ -103,8 +103,8 @@ func ColorPrintYAML(data any, hints map[string]string, spaceRootLists bool) erro outputYAML = ansiRegex.ReplaceAllString(outputYAML, "") } - pterm.Println() - pterm.Println(outputYAML) + content := strings.Join([]string{"\n", outputYAML}, "") + pterm.Fprintln(message.OutputWriter, content) return nil } diff --git a/src/test/e2e/06_create_sbom_test.go b/src/test/e2e/06_create_sbom_test.go index 9426a21d4c..d8e9d5eabc 100644 --- a/src/test/e2e/06_create_sbom_test.go +++ b/src/test/e2e/06_create_sbom_test.go @@ -55,7 +55,7 @@ func TestCreateSBOM(t *testing.T) { stdOut, _, err := e2e.Zarf(t, "package", "inspect", tarPath, "--list-images") require.NoError(t, err) - require.Equal(t, "- ghcr.io/zarf-dev/doom-game:0.0.1\n", stdOut) + require.Contains(t, stdOut, "- ghcr.io/zarf-dev/doom-game:0.0.1\n") // Pull the current zarf binary version to find the corresponding init package version, _, err := e2e.Zarf(t, "version") diff --git a/src/test/e2e/12_lint_test.go b/src/test/e2e/12_lint_test.go index 2c77b14056..6f6ffbf26c 100644 --- a/src/test/e2e/12_lint_test.go +++ b/src/test/e2e/12_lint_test.go @@ -31,31 +31,32 @@ func TestLint(t *testing.T) { configPath := filepath.Join(testPackagePath, "zarf-config.toml") osSetErr := os.Setenv("ZARF_CONFIG", configPath) require.NoError(t, osSetErr, "Unable to set ZARF_CONFIG") - _, stderr, err := e2e.Zarf(t, "dev", "lint", testPackagePath, "-f", "good-flavor") + stdOut, stdErr, err := e2e.Zarf(t, "dev", "lint", testPackagePath, "-f", "good-flavor") osUnsetErr := os.Unsetenv("ZARF_CONFIG") require.NoError(t, osUnsetErr, "Unable to cleanup ZARF_CONFIG") require.Error(t, err, "Require an exit code since there was warnings / errors") - strippedStderr := e2e.StripMessageFormatting(stderr) + strippedStdOut := e2e.StripMessageFormatting(stdOut) + strippedStdErr := e2e.StripMessageFormatting(stdErr) key := "WHATEVER_IMAGE" - require.Contains(t, strippedStderr, lang.UnsetVarLintWarning) - require.Contains(t, strippedStderr, fmt.Sprintf(lang.PkgValidateTemplateDeprecation, key, key, key)) - require.Contains(t, strippedStderr, ".components.[2].repos.[0] | Unpinned repository") - require.Contains(t, strippedStderr, ".metadata | Additional property description1 is not allowed") - require.Contains(t, strippedStderr, ".components.[0].import | Additional property not-path is not allowed") + require.Contains(t, strippedStdOut, lang.UnsetVarLintWarning) + require.Contains(t, strippedStdOut, fmt.Sprintf(lang.PkgValidateTemplateDeprecation, key, key, key)) + require.Contains(t, strippedStdOut, ".components.[2].repos.[0] | Unpinned repository") + require.Contains(t, strippedStdOut, ".metadata | Additional property description1 is not allowed") + require.Contains(t, strippedStdOut, ".components.[0].import | Additional property not-path is not allowed") // Testing the import / compose on lint is working - require.Contains(t, strippedStderr, ".components.[1].images.[0] | Image not pinned with digest - registry.com:9001/whatever/image:latest") + require.Contains(t, strippedStdOut, ".components.[1].images.[0] | Image not pinned with digest - registry.com:9001/whatever/image:latest") // Testing import / compose + variables are working - require.Contains(t, strippedStderr, ".components.[2].images.[3] | Image not pinned with digest - busybox:latest") + require.Contains(t, strippedStdOut, ".components.[2].images.[3] | Image not pinned with digest - busybox:latest") // Testing OCI imports get linted - require.Contains(t, strippedStderr, ".components.[0].images.[0] | Image not pinned with digest - ghcr.io/zarf-dev/doom-game:0.0.1") + require.Contains(t, strippedStdOut, ".components.[0].images.[0] | Image not pinned with digest - ghcr.io/zarf-dev/doom-game:0.0.1") // Check flavors - require.NotContains(t, strippedStderr, "image-in-bad-flavor-component:unpinned") - require.Contains(t, strippedStderr, "image-in-good-flavor-component:unpinned") + require.NotContains(t, strippedStdOut, "image-in-bad-flavor-component:unpinned") + require.Contains(t, strippedStdOut, "image-in-good-flavor-component:unpinned") // Check reported filepaths - require.Contains(t, strippedStderr, "Linting package \"dos-games\" at oci://ghcr.io/zarf-dev/packages/dos-games:1.1.0") - require.Contains(t, strippedStderr, fmt.Sprintf("Linting package \"lint\" at %s", testPackagePath)) + require.Contains(t, strippedStdErr, "Linting package \"dos-games\" at oci://ghcr.io/zarf-dev/packages/dos-games:1.1.0") + require.Contains(t, strippedStdErr, fmt.Sprintf("Linting package \"lint\" at %s", testPackagePath)) }) } diff --git a/src/test/e2e/14_oci_compose_test.go b/src/test/e2e/14_oci_compose_test.go index 3f8c0a452b..81fdd8c2d5 100644 --- a/src/test/e2e/14_oci_compose_test.go +++ b/src/test/e2e/14_oci_compose_test.go @@ -100,7 +100,7 @@ func (suite *PublishCopySkeletonSuite) Test_1_Compose_Everything_Inception() { _, _, err = e2e.Zarf(suite.T(), "package", "create", importception, "-o", "build", "--plain-http", "--confirm") suite.NoError(err) - _, stdErr, err := e2e.Zarf(suite.T(), "package", "inspect", importEverythingPath) + stdOut, _, err := e2e.Zarf(suite.T(), "package", "inspect", importEverythingPath) suite.NoError(err) targets := []string{ @@ -109,7 +109,7 @@ func (suite *PublishCopySkeletonSuite) Test_1_Compose_Everything_Inception() { } for _, target := range targets { - suite.Contains(stdErr, target) + suite.Contains(stdOut, target) } } diff --git a/src/test/e2e/24_variables_test.go b/src/test/e2e/24_variables_test.go index b583a314d4..255988d4f8 100644 --- a/src/test/e2e/24_variables_test.go +++ b/src/test/e2e/24_variables_test.go @@ -58,11 +58,11 @@ func TestVariables(t *testing.T) { stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", path, "--confirm", "--set", "SITE_NAME=Lula Web", "--set", "AWS_REGION=unicorn-land", "-l", "trace") require.NoError(t, err, stdOut, stdErr) // Verify that the variables were shown to the user in the formats we expect - require.Contains(t, stdErr, "currently set to 'Defense Unicorns' (default)") - require.Contains(t, stdErr, "currently set to 'Lula Web'") - require.Contains(t, stdErr, "currently set to '**sanitized**'") + require.Contains(t, stdOut, "currently set to 'Defense Unicorns' (default)") + require.Contains(t, stdOut, "currently set to 'Lula Web'") + require.Contains(t, stdOut, "currently set to '**sanitized**'") // Verify that the sensitive variable 'unicorn-land' was not printed to the screen - require.NotContains(t, stdErr, "unicorn-land") + require.NotContains(t, stdOut, "unicorn-land") logText := e2e.GetLogFileContents(t, e2e.StripMessageFormatting(stdErr)) // Verify that the sensitive variable 'unicorn-land' was not included in the log diff --git a/src/test/e2e/25_helm_test.go b/src/test/e2e/25_helm_test.go index a741c000ac..933fda4eb7 100644 --- a/src/test/e2e/25_helm_test.go +++ b/src/test/e2e/25_helm_test.go @@ -96,8 +96,8 @@ func testHelmChartsExample(t *testing.T) { helmChartsPkg := filepath.Join("build", fmt.Sprintf("zarf-package-helm-charts-%s-0.0.1.tar.zst", e2e.Arch)) stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", helmChartsPkg, "--confirm") require.NoError(t, err, stdOut, stdErr) - require.Contains(t, string(stdErr), "registryOverrides", "registry overrides was not saved to build data") - require.Contains(t, string(stdErr), "docker.io", "docker.io not found in registry overrides") + require.Contains(t, stdOut, "registryOverrides", "registry overrides was not saved to build data") + require.Contains(t, stdOut, "docker.io", "docker.io not found in registry overrides") // Remove the example package. stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", "helm-charts", "--confirm")