mattermost/server/cmd/mmctl/commands/root.go
Ben Schumacher 3df25e6904
[MM-65830] Fix mmctl system status exit code for health check failures (#33970)
* [MM-65830] Fix mmctl system status exit code for health check failures

Make mmctl system status return non-zero exit codes when health checks fail.
This addresses the customer blocker issue where AWS ECS health checks were
broken after the distroless Docker image change, as they rely on exit codes
to determine service health.

Changes:
- Return error (non-zero exit) when server status != "OK"
- Return error when database_status != "OK"
- Return error when filestore_status != "OK"
- Return success (exit code 0) only when all components are healthy
- Add comprehensive test coverage for all health check scenarios
- Maintain backward compatibility for missing status fields

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [MM-65830] Refactor mmctl system status to exit silently on health failures

Update mmctl system status to exit with code 1 when components are unhealthy
without outputting error messages. This addresses the customer blocker where
AWS ECS health checks were broken after the distroless Docker image change,
as they rely on exit codes to determine service health.

Changes:
- Add withClientAndExitCode wrapper for commands returning (bool, error)
- Modify systemStatusCmdF to return (bool, error) instead of error
- Return (true, nil) when any component is unhealthy (causes exit code 1)
- Return (false, nil) when all components are healthy (causes exit code 0)
- Return (false, error) for actual API/network failures
- Always print status information before checking health
- Update comprehensive test coverage for all scenarios

The refactored approach uses a clean adapter pattern that converts (bool, error)
signatures to standard error returns while delegating to existing withClient
infrastructure to avoid code duplication.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [MM-65830] Ensure printer output is flushed before exit

Add printer.Flush() call before os.Exit(1) in withClientAndExitCode wrapper
to ensure all buffered output is properly written before the program exits.
This ensures status information is always displayed to users even when
exiting with non-zero exit codes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add godoc comment

* Output content then error out

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-06 10:49:46 +00:00

123 lines
4.8 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package commands
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime/debug"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
)
func Run(args []string) error {
viper.SetEnvPrefix("mmctl")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.SetDefault("local-socket-path", model.LocalModeSocketPath)
viper.AutomaticEnv()
RootCmd.PersistentFlags().String("config", filepath.Join(xdgConfigHomeVar, configParent, configFileName), "path to the configuration file")
_ = viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config"))
RootCmd.PersistentFlags().Bool("suppress-warnings", false, "disables printing warning messages")
_ = viper.BindPFlag("suppress-warnings", RootCmd.PersistentFlags().Lookup("suppress-warnings"))
RootCmd.PersistentFlags().String("format", "plain", "the format of the command output [plain, json]")
_ = viper.BindPFlag("format", RootCmd.PersistentFlags().Lookup("format"))
_ = RootCmd.PersistentFlags().MarkHidden("format")
RootCmd.PersistentFlags().Bool("json", false, "the output format will be in json format")
_ = viper.BindPFlag("json", RootCmd.PersistentFlags().Lookup("json"))
RootCmd.PersistentFlags().Bool("strict", false, "will only run commands if the mmctl version matches the server one")
_ = viper.BindPFlag("strict", RootCmd.PersistentFlags().Lookup("strict"))
RootCmd.PersistentFlags().Bool("insecure-sha1-intermediate", false, "allows to use insecure TLS protocols, such as SHA-1")
_ = viper.BindPFlag("insecure-sha1-intermediate", RootCmd.PersistentFlags().Lookup("insecure-sha1-intermediate"))
RootCmd.PersistentFlags().Bool("insecure-tls-version", false, "allows to use TLS versions 1.0 and 1.1")
_ = viper.BindPFlag("insecure-tls-version", RootCmd.PersistentFlags().Lookup("insecure-tls-version"))
RootCmd.PersistentFlags().Bool("local", false, "allows communicating with the server through a unix socket")
_ = viper.BindPFlag("local", RootCmd.PersistentFlags().Lookup("local"))
RootCmd.PersistentFlags().Bool("short-stat", false, "short stat will provide useful statistical data")
_ = RootCmd.PersistentFlags().MarkHidden("short-stat")
RootCmd.PersistentFlags().Bool("no-stat", false, "the statistical data won't be displayed")
_ = RootCmd.PersistentFlags().MarkHidden("no-stat")
RootCmd.PersistentFlags().Bool("disable-pager", false, "disables paged output")
_ = viper.BindPFlag("disable-pager", RootCmd.PersistentFlags().Lookup("disable-pager"))
RootCmd.PersistentFlags().Bool("quiet", false, "prevent mmctl to generate output for the commands")
_ = viper.BindPFlag("quiet", RootCmd.PersistentFlags().Lookup("quiet"))
RootCmd.SetArgs(args)
defer func() {
if x := recover(); x != nil {
printPanic(x)
_ = printer.Flush()
os.Exit(1)
}
}()
err := RootCmd.Execute()
// Flush the printer first before printing any error
_ = printer.Flush()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
}
return err
}
func printPanic(x any) {
u, err := url.Parse("https://github.com/mattermost/mattermost/issues/new")
if err != nil {
panic(err)
}
q := u.Query()
q.Add("title", "[mmctl] [bug] panic on v"+Version)
q.Add("body", "<!--- Please provide the stack trace -->\n")
u.RawQuery = q.Encode()
printer.PrintError("Uh oh! Something unexpected happened :( Would you mind reporting it?")
printer.PrintError(u.String() + "\n")
printer.PrintError(fmt.Sprintf("%s", x))
printer.PrintError(string(debug.Stack()))
}
var RootCmd = &cobra.Command{
Use: "mmctl",
Short: "Remote client for the Open Source, self-hosted Slack-alternative",
Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`,
DisableAutoGenTag: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
for i, arg := range args {
args[i] = strings.TrimSpace(arg)
}
format := viper.GetString("format")
if viper.GetBool("disable-pager") {
printer.OverrideEnablePager(false)
}
printer.SetCommand(cmd)
isJSON := viper.GetBool("json")
if isJSON || format == printer.FormatJSON {
printer.SetFormat(printer.FormatJSON)
} else {
printer.SetFormat(printer.FormatPlain)
}
quiet := viper.GetBool("quiet")
printer.SetQuiet(quiet)
perPage, err := cmd.Flags().GetInt("per-page")
if err == nil && perPage > MaxPageSize {
printer.PrintError(fmt.Sprintf("Per page value is greater than the maximum allowed. Mattermost might only return %d items.", MaxPageSize))
}
},
SilenceUsage: true,
SilenceErrors: true,
}