diff --git a/cli/cmd/app_hostname_ls.go b/cli/cmd/app_hostname_ls.go index 7e755e425..bd881d0c2 100644 --- a/cli/cmd/app_hostname_ls.go +++ b/cli/cmd/app_hostname_ls.go @@ -79,7 +79,7 @@ replicated app hostname ls --app myapp --output json`, func (r *runners) listAppHostnames(ctx context.Context, outputFormat string) error { // Only show spinners for table output showSpinners := outputFormat == "table" - log := logger.NewLogger(r.w) + log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) // Resolve app ID from slug or ID appSlugOrID := r.appSlug diff --git a/cli/cmd/app_rm.go b/cli/cmd/app_rm.go index 6d3e4fbef..219b58be8 100644 --- a/cli/cmd/app_rm.go +++ b/cli/cmd/app_rm.go @@ -54,15 +54,22 @@ replicated app delete "Custom App" --output json`, } func (r *runners) deleteApp(ctx context.Context, cmd *cobra.Command, appName string, opts deleteAppOpts, outputFormat string) error { - log := logger.NewLogger(r.w) + log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) + showSpinners := outputFormat == "table" - log.ActionWithSpinner("Fetching App") + if showSpinners { + log.ActionWithSpinner("Fetching App") + } app, err := r.kotsAPI.GetApp(ctx, appName, true) if err != nil { - log.FinishSpinnerWithError() + if showSpinners { + log.FinishSpinnerWithError() + } return errors.Wrap(err, "list apps") } - log.FinishSpinner() + if showSpinners { + log.FinishSpinner() + } apps := []types.AppAndChannels{ { @@ -86,13 +93,19 @@ func (r *runners) deleteApp(ctx context.Context, cmd *cobra.Command, appName str } } - log.ActionWithSpinner("Deleting App") + if showSpinners { + log.ActionWithSpinner("Deleting App") + } err = r.kotsAPI.DeleteKOTSApp(ctx, app.ID) if err != nil { - log.FinishSpinnerWithError() + if showSpinners { + log.FinishSpinnerWithError() + } return errors.Wrap(err, "delete app") } - log.FinishSpinner() + if showSpinners { + log.FinishSpinner() + } return nil } diff --git a/cli/cmd/cluster_kubeconfig.go b/cli/cmd/cluster_kubeconfig.go index 88615e989..2fcf57421 100644 --- a/cli/cmd/cluster_kubeconfig.go +++ b/cli/cmd/cluster_kubeconfig.go @@ -108,7 +108,7 @@ func (r *runners) kubeconfigCluster(_ *cobra.Command, args []string) error { } if r.args.kubeconfigStdout { - fmt.Println(string(kubeconfig)) + fmt.Print(string(kubeconfig)) return nil } @@ -123,7 +123,7 @@ func (r *runners) kubeconfigCluster(_ *cobra.Command, args []string) error { return errors.Wrap(err, "write kubeconfig") } - fmt.Printf("kubeconfig written to %s\n", r.args.kubeconfigPath) + fmt.Fprintf(os.Stderr, "kubeconfig written to %s\n", r.args.kubeconfigPath) return nil } @@ -180,7 +180,7 @@ func (r *runners) kubeconfigCluster(_ *cobra.Command, args []string) error { for _, backupPath := range backupPaths { err := os.Remove(backupPath) if err != nil { - fmt.Printf("failed to remove backup kubeconfig: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "failed to remove backup kubeconfig: %s\n", err.Error()) } } }() @@ -215,7 +215,7 @@ func (r *runners) kubeconfigCluster(_ *cobra.Command, args []string) error { return errors.Wrap(err, "write kubeconfig") } - fmt.Printf(" ✓ Updated kubernetes context '%s' in '%s'\n", mergedConfig.CurrentContext, kubeconfigPaths[0]) + fmt.Fprintf(os.Stderr, " ✓ Updated kubernetes context '%s' in '%s'\n", mergedConfig.CurrentContext, kubeconfigPaths[0]) return nil } diff --git a/cli/cmd/cluster_prepare.go b/cli/cmd/cluster_prepare.go index e0affc2d7..965ad9de3 100644 --- a/cli/cmd/cluster_prepare.go +++ b/cli/cmd/cluster_prepare.go @@ -154,7 +154,7 @@ func (r *runners) prepareCluster(_ *cobra.Command, args []string) error { return errors.New("no app specified") } - log := logger.NewLogger(r.w) + log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) release, err := prepareRelease(r, log) if err != nil { diff --git a/cli/cmd/installer_create.go b/cli/cmd/installer_create.go index 101c4b8ac..38ebf0256 100644 --- a/cli/cmd/installer_create.go +++ b/cli/cmd/installer_create.go @@ -58,7 +58,7 @@ func (r *runners) installerCreate(_ *cobra.Command, _ []string) error { return errors.Errorf("Installer specs are only supported for KOTS applications, app %q has type %q", r.appID, r.appType) } - log := logger.NewLogger(r.w) + log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) if r.args.createInstallerAutoDefaults { log.ActionWithSpinner("Reading Environment") err := r.setKOTSDefaultInstallerParams() diff --git a/cli/cmd/lint.go b/cli/cmd/lint.go index 3a18d8f24..1b4662d44 100644 --- a/cli/cmd/lint.go +++ b/cli/cmd/lint.go @@ -147,8 +147,12 @@ func (r *runners) runLint(cmd *cobra.Command, args []string) error { autoDiscoveryMode := len(config.Charts) == 0 && len(config.Preflights) == 0 && len(config.Manifests) == 0 if autoDiscoveryMode { - fmt.Fprintf(r.w, "No .replicated config found. Auto-discovering lintable resources in current directory...\n\n") - r.w.Flush() + showAutoDiscoveryMessages := r.outputFormat == "table" + + if showAutoDiscoveryMessages { + fmt.Fprintf(r.w, "No .replicated config found. Auto-discovering lintable resources in current directory...\n\n") + r.w.Flush() + } // Auto-discover Helm charts (for counting and display) chartPaths, err := lint2.DiscoverChartPaths(filepath.Join(".", "**")) @@ -190,18 +194,28 @@ func (r *runners) runLint(cmd *cobra.Command, args []string) error { } // Print what was discovered - fmt.Fprintf(r.w, "Discovered resources:\n") - fmt.Fprintf(r.w, " - %d Helm chart(s)\n", len(chartPaths)) - fmt.Fprintf(r.w, " - %d Preflight spec(s)\n", len(preflightPaths)) - fmt.Fprintf(r.w, " - %d Support Bundle spec(s)\n", len(sbPaths)) - fmt.Fprintf(r.w, " - %d HelmChart manifest(s)\n\n", len(helmChartPaths)) - r.w.Flush() + if showAutoDiscoveryMessages { + fmt.Fprintf(r.w, "Discovered resources:\n") + fmt.Fprintf(r.w, " - %d Helm chart(s)\n", len(chartPaths)) + fmt.Fprintf(r.w, " - %d Preflight spec(s)\n", len(preflightPaths)) + fmt.Fprintf(r.w, " - %d Support Bundle spec(s)\n", len(sbPaths)) + fmt.Fprintf(r.w, " - %d HelmChart manifest(s)\n\n", len(helmChartPaths)) + r.w.Flush() + } // If nothing was found and EC linting is not enabled, exit early. // EC linting runs after this block, so don't bail out when it's enabled. if len(chartPaths) == 0 && len(preflightPaths) == 0 && len(sbPaths) == 0 && !config.ReplLint.Linters.EmbeddedCluster.IsEnabled() { - fmt.Fprintf(r.w, "No lintable resources found in current directory.\n") - r.w.Flush() + if showAutoDiscoveryMessages { + fmt.Fprintf(r.w, "No lintable resources found in current directory.\n") + r.w.Flush() + } + if r.outputFormat == "json" { + output.Summary = r.calculateOverallSummary(output) + if err := print.LintResults(r.outputFormat, r.w, output); err != nil { + return errors.Wrap(err, "failed to print JSON output to stdout") + } + } return nil } } diff --git a/cli/cmd/release_create.go b/cli/cmd/release_create.go index 9c8eb2971..b167823d5 100644 --- a/cli/cmd/release_create.go +++ b/cli/cmd/release_create.go @@ -230,7 +230,7 @@ func (r *runners) releaseCreate(cmd *cobra.Command, args []string) (err error) { printIfError(cmd, err) }() - log := logger.NewLogger(r.w) + log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) if r.outputFormat == "json" { // suppress log lines for machine-readable output log.Silence() diff --git a/cli/cmd/release_download.go b/cli/cmd/release_download.go index 80e81cc11..a152ff807 100644 --- a/cli/cmd/release_download.go +++ b/cli/cmd/release_download.go @@ -105,7 +105,7 @@ func (r *runners) releaseDownload(command *cobra.Command, args []string) error { return r.releaseInspect(command, args) } - log := logger.NewLogger(os.Stdout) + log := logger.NewLogger(os.Stderr) // Determine sequence to download var seq int64 @@ -280,7 +280,7 @@ func (r *runners) downloadReleaseArchive(seq int64, dest string) error { } defer os.RemoveAll(tempDir) - log := logger.NewLogger(os.Stdout) + log := logger.NewLogger(os.Stderr) if err := kotsrelease.Save(tempDir, release, log); err != nil { return errors.Wrap(err, "save release to temp dir") } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 226f41d34..866370836 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -9,6 +9,7 @@ import ( "text/tabwriter" "github.com/Masterminds/sprig/v3" + "github.com/mattn/go-isatty" "github.com/pkg/errors" "github.com/replicatedhq/replicated/client" replicatedcache "github.com/replicatedhq/replicated/pkg/cache" @@ -104,20 +105,31 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { w := tabwriter.NewWriter(stdout, minWidth, tabWidth, padding, padChar, tabwriter.TabIndent) + stdoutIsTTY := false + if f, ok := stdout.(*os.File); ok { + stdoutIsTTY = isatty.IsTerminal(f.Fd()) + } + // get api client and app ID after flags are parsed runCmds := &runners{ - rootCmd: rootCmd, - stdin: stdin, - w: w, + rootCmd: rootCmd, + stdin: stdin, + w: w, + stdoutIsTTY: stdoutIsTTY, } if runCmds.rootCmd == nil { runCmds.rootCmd = GetRootCmd() } if stderr != nil { runCmds.rootCmd.SetErr(stderr) + runCmds.rootCmd.SetOut(stderr) } if stdout != nil { - runCmds.rootCmd.SetOut(stdout) + defaultHelpFunc := runCmds.rootCmd.HelpFunc() + runCmds.rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + cmd.SetOut(stdout) + defaultHelpFunc(cmd, args) + }) } // Setup PersistentPreRun to handle --debug flag diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index d9f376d06..1f54411a4 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -24,6 +24,7 @@ type runners struct { stdin io.Reader outputFormat string w *tabwriter.Writer + stdoutIsTTY bool rootCmd *cobra.Command args runnerArgs diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 808e258c7..f883b95ba 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -18,6 +18,7 @@ type Logger struct { spinnerArgs []interface{} isSilent bool isVerbose bool + isTerminal *bool } func NewLogger(writer io.Writer) *Logger { @@ -26,6 +27,27 @@ func NewLogger(writer io.Writer) *Logger { } } +// SetIsTerminal lets callers override TTY detection. Useful when the +// logger writes through a wrapper (e.g. tabwriter) that hides the +// underlying file descriptor. +func (l *Logger) SetIsTerminal(isTerminal bool) *Logger { + if l == nil { + return l + } + l.isTerminal = &isTerminal + return l +} + +func (l *Logger) isTTY() bool { + if l.isTerminal != nil { + return *l.isTerminal + } + if f, ok := l.w.(*os.File); ok { + return isatty.IsTerminal(f.Fd()) + } + return false +} + func (l *Logger) Silence() { if l == nil { return @@ -107,7 +129,7 @@ func (l *Logger) ActionWithSpinner(msg string, args ...interface{}) { fmt.Fprintf(l.w, " • ") fmt.Fprintf(l.w, msg, args...) - if isatty.IsTerminal(os.Stdout.Fd()) { + if l.isTTY() { s := spin.New() fmt.Fprintf(l.w, " %s", s.Next()) @@ -140,7 +162,7 @@ func (l *Logger) ChildActionWithSpinner(msg string, args ...interface{}) { fmt.Fprintf(l.w, " • ") fmt.Fprintf(l.w, msg, args...) - if isatty.IsTerminal(os.Stdout.Fd()) { + if l.isTTY() { s := spin.New() fmt.Fprintf(l.w, " %s", s.Next()) @@ -178,7 +200,7 @@ func (l *Logger) FinishChildSpinner() { green.Fprintf(l.w, " ✓") fmt.Fprintf(l.w, " \n") - if isatty.IsTerminal(os.Stdout.Fd()) { + if l.isTTY() { l.spinnerStopCh <- true close(l.spinnerStopCh) } @@ -207,7 +229,7 @@ func (l *Logger) FinishSpinner() { green.Fprintf(l.w, " ✓") fmt.Fprintf(l.w, " \n") - if isatty.IsTerminal(os.Stdout.Fd()) { + if l.isTTY() { l.spinnerStopCh <- true close(l.spinnerStopCh) } @@ -226,7 +248,7 @@ func (l *Logger) FinishSpinnerWithError() { red.Fprintf(l.w, " ✗") fmt.Fprintf(l.w, " \n") - if isatty.IsTerminal(os.Stdout.Fd()) { + if l.isTTY() { l.spinnerStopCh <- true close(l.spinnerStopCh) }