linkerd2/cli/cmd/check.go

181 lines
5.5 KiB
Go

package cmd
import (
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/linkerd/linkerd2/pkg/healthcheck"
"github.com/spf13/cobra"
)
type checkOptions struct {
versionOverride string
preInstallOnly bool
dataPlaneOnly bool
wait time.Duration
namespace string
singleNamespace bool
}
func newCheckOptions() *checkOptions {
return &checkOptions{
versionOverride: "",
preInstallOnly: false,
dataPlaneOnly: false,
wait: 300 * time.Second,
namespace: "",
singleNamespace: false,
}
}
func newCmdCheck() *cobra.Command {
options := newCheckOptions()
cmd := &cobra.Command{
Use: "check",
Short: "Check the Linkerd installation for potential problems",
Long: `Check the Linkerd installation for potential problems.
The check command will perform a series of checks to validate that the linkerd
CLI and control plane are configured correctly. If the command encounters a
failure it will print additional information about the failure and exit with a
non-zero exit code.`,
Example: ` # Check that the Linkerd control plane is up and running
linkerd check
# Check that the Linkerd control plane can be installed in the "test" namespace
linkerd check --pre --linkerd-namespace test
# Check that the Linkerd data plane proxies in the "app" namespace are up and running
linkerd check --proxy --namespace app`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return configureAndRunChecks(stdout, options)
},
}
cmd.Args = cobra.NoArgs
cmd.PersistentFlags().StringVar(&options.versionOverride, "expected-version", options.versionOverride, "Overrides the version used when checking if Linkerd is running the latest version (mostly for testing)")
cmd.PersistentFlags().BoolVar(&options.preInstallOnly, "pre", options.preInstallOnly, "Only run pre-installation checks, to determine if the control plane can be installed")
cmd.PersistentFlags().BoolVar(&options.dataPlaneOnly, "proxy", options.dataPlaneOnly, "Only run data-plane checks, to determine if the data plane is healthy")
cmd.PersistentFlags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
cmd.PersistentFlags().BoolVar(&options.singleNamespace, "single-namespace", options.singleNamespace, "When running pre-installation checks (--pre), only check the permissions required to operate the control plane in a single namespace")
return cmd
}
func configureAndRunChecks(w io.Writer, options *checkOptions) error {
err := options.validate()
if err != nil {
return fmt.Errorf("Validation error when executing check command: %v", err)
}
checks := []healthcheck.CategoryID{
healthcheck.KubernetesAPIChecks,
healthcheck.KubernetesVersionChecks,
healthcheck.LinkerdVersionChecks,
}
if options.preInstallOnly {
if options.singleNamespace {
checks = append(checks, healthcheck.LinkerdPreInstallSingleNamespaceChecks)
} else {
checks = append(checks, healthcheck.LinkerdPreInstallClusterChecks)
}
checks = append(checks, healthcheck.LinkerdPreInstallChecks)
} else {
checks = append(checks, healthcheck.LinkerdControlPlaneExistenceChecks)
checks = append(checks, healthcheck.LinkerdAPIChecks)
if !options.singleNamespace {
checks = append(checks, healthcheck.LinkerdServiceProfileChecks)
}
if options.dataPlaneOnly {
checks = append(checks, healthcheck.LinkerdDataPlaneChecks)
} else {
checks = append(checks, healthcheck.LinkerdControlPlaneVersionChecks)
}
}
hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
ControlPlaneNamespace: controlPlaneNamespace,
DataPlaneNamespace: options.namespace,
KubeConfig: kubeconfigPath,
KubeContext: kubeContext,
APIAddr: apiAddr,
VersionOverride: options.versionOverride,
RetryDeadline: time.Now().Add(options.wait),
})
success := runChecks(w, hc)
// this empty line separates final results from the checks list in the output
fmt.Fprintln(w, "")
if !success {
fmt.Fprintf(w, "Status check results are %s\n", failStatus)
os.Exit(2)
}
fmt.Fprintf(w, "Status check results are %s\n", okStatus)
return nil
}
func (o *checkOptions) validate() error {
if o.preInstallOnly && o.dataPlaneOnly {
return errors.New("--pre and --proxy flags are mutually exclusive")
}
return nil
}
func runChecks(w io.Writer, hc *healthcheck.HealthChecker) bool {
var lastCategory healthcheck.CategoryID
spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
spin.Writer = w
prettyPrintResults := func(result *healthcheck.CheckResult) {
if lastCategory != result.Category {
if lastCategory != "" {
fmt.Fprintln(w)
}
fmt.Fprintln(w, result.Category)
fmt.Fprintln(w, strings.Repeat("-", len(result.Category)))
lastCategory = result.Category
}
spin.Stop()
if result.Retry {
spin.Suffix = fmt.Sprintf(" %s -- %s", result.Description, result.Err)
spin.Color("bold")
return
}
status := okStatus
if result.Err != nil {
status = failStatus
if result.Warning {
status = warnStatus
}
}
fmt.Fprintf(w, "%s %s\n", status, result.Description)
if result.Err != nil {
fmt.Fprintf(w, " %s\n", result.Err)
if result.HintAnchor != "" {
fmt.Fprintf(w, " see %s%s for hints\n", healthcheck.HintBaseURL, result.HintAnchor)
}
}
}
return hc.RunChecks(prettyPrintResults)
}