...

Source file src/github.com/linkerd/linkerd2/cli/cmd/check.go

Documentation: github.com/linkerd/linkerd2/cli/cmd

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
    15  	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
    16  	"github.com/linkerd/linkerd2/pkg/healthcheck"
    17  	"github.com/linkerd/linkerd2/pkg/k8s"
    18  	"github.com/linkerd/linkerd2/pkg/version"
    19  	"github.com/spf13/cobra"
    20  	"github.com/spf13/pflag"
    21  	valuespkg "helm.sh/helm/v3/pkg/cli/values"
    22  	utilsexec "k8s.io/utils/exec"
    23  )
    24  
    25  type checkOptions struct {
    26  	versionOverride    string
    27  	preInstallOnly     bool
    28  	crdsOnly           bool
    29  	dataPlaneOnly      bool
    30  	wait               time.Duration
    31  	namespace          string
    32  	cniEnabled         bool
    33  	output             string
    34  	cliVersionOverride string
    35  }
    36  
    37  func newCheckOptions() *checkOptions {
    38  	return &checkOptions{
    39  		versionOverride:    "",
    40  		preInstallOnly:     false,
    41  		crdsOnly:           false,
    42  		dataPlaneOnly:      false,
    43  		wait:               300 * time.Second,
    44  		namespace:          "",
    45  		cniEnabled:         false,
    46  		output:             tableOutput,
    47  		cliVersionOverride: "",
    48  	}
    49  }
    50  
    51  // nonConfigFlagSet specifies flags not allowed with `linkerd check config`
    52  func (options *checkOptions) nonConfigFlagSet() *pflag.FlagSet {
    53  	flags := pflag.NewFlagSet("non-config-check", pflag.ExitOnError)
    54  
    55  	flags.BoolVar(&options.cniEnabled, "linkerd-cni-enabled", options.cniEnabled, "When running pre-installation checks (--pre), assume the linkerd-cni plugin is already installed, and a NET_ADMIN check is not needed")
    56  	flags.StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
    57  	flags.BoolVar(&options.preInstallOnly, "pre", options.preInstallOnly, "Only run pre-installation checks, to determine if the control plane can be installed")
    58  	flags.BoolVar(&options.crdsOnly, "crds", options.crdsOnly, "Only run checks which determine if the Linkerd CRDs have been installed")
    59  	flags.BoolVar(&options.dataPlaneOnly, "proxy", options.dataPlaneOnly, "Only run data-plane checks, to determine if the data plane is healthy")
    60  
    61  	return flags
    62  }
    63  
    64  // checkFlagSet specifies flags allowed with and without `config`
    65  func (options *checkOptions) checkFlagSet() *pflag.FlagSet {
    66  	flags := pflag.NewFlagSet("check", pflag.ExitOnError)
    67  
    68  	flags.StringVar(&options.versionOverride, "expected-version", options.versionOverride, "Overrides the version used when checking if Linkerd is running the latest version (mostly for testing)")
    69  	flags.StringVar(&options.cliVersionOverride, "cli-version-override", "", "Used to override the version of the cli (mostly for testing)")
    70  	flags.StringVarP(&options.output, "output", "o", options.output, "Output format. One of: table, json, short")
    71  	flags.DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
    72  
    73  	return flags
    74  }
    75  
    76  func (options *checkOptions) validate() error {
    77  	if options.preInstallOnly && options.dataPlaneOnly {
    78  		return errors.New("--pre and --proxy flags are mutually exclusive")
    79  	}
    80  	if options.preInstallOnly && options.crdsOnly {
    81  		return errors.New("--pre and --crds flags are mutually exclusive")
    82  	}
    83  	if !options.preInstallOnly && options.cniEnabled {
    84  		return errors.New("--linkerd-cni-enabled can only be used with --pre")
    85  	}
    86  	if options.output != tableOutput && options.output != jsonOutput && options.output != shortOutput {
    87  		return fmt.Errorf("Invalid output type '%s'. Supported output types are: %s, %s, %s", options.output, jsonOutput, tableOutput, shortOutput)
    88  	}
    89  	return nil
    90  }
    91  
    92  func newCmdCheck() *cobra.Command {
    93  	options := newCheckOptions()
    94  	checkFlags := options.checkFlagSet()
    95  	nonConfigFlags := options.nonConfigFlagSet()
    96  
    97  	cmd := &cobra.Command{
    98  		Use:   "check [flags]",
    99  		Args:  cobra.NoArgs,
   100  		Short: "Check the Linkerd installation for potential problems",
   101  		Long: `Check the Linkerd installation for potential problems.
   102  
   103  The check command will perform a series of checks to validate that the linkerd
   104  CLI and control plane are configured correctly. If the command encounters a
   105  failure it will print additional information about the failure and exit with a
   106  non-zero exit code.`,
   107  		Example: `  # Check that the Linkerd control plane is up and running
   108    linkerd check
   109  
   110    # Check that the Linkerd control plane can be installed in the "test" namespace
   111    linkerd check --pre --linkerd-namespace test
   112  
   113    # Check that the Linkerd data plane proxies in the "app" namespace are up and running
   114    linkerd check --proxy --namespace app`,
   115  		RunE: func(cmd *cobra.Command, args []string) error {
   116  			return configureAndRunChecks(cmd, stdout, stderr, options)
   117  		},
   118  	}
   119  
   120  	cmd.PersistentFlags().AddFlagSet(checkFlags)
   121  	cmd.Flags().AddFlagSet(nonConfigFlags)
   122  
   123  	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
   124  		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
   125  
   126  	return cmd
   127  }
   128  
   129  func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, options *checkOptions) error {
   130  	err := options.validate()
   131  	if err != nil {
   132  		return fmt.Errorf("Validation error when executing check command: %w", err)
   133  	}
   134  
   135  	if options.cliVersionOverride != "" {
   136  		version.Version = options.cliVersionOverride
   137  	}
   138  
   139  	checks := []healthcheck.CategoryID{
   140  		healthcheck.KubernetesAPIChecks,
   141  		healthcheck.KubernetesVersionChecks,
   142  		healthcheck.LinkerdVersionChecks,
   143  	}
   144  
   145  	crdManifest := bytes.Buffer{}
   146  	err = renderCRDs(&crdManifest, valuespkg.Options{})
   147  	if err != nil {
   148  		return err
   149  	}
   150  	var installManifest string
   151  	var values *charts.Values
   152  	if options.preInstallOnly {
   153  		checks = append(checks, healthcheck.LinkerdPreInstallChecks)
   154  		if options.cniEnabled {
   155  			checks = append(checks, healthcheck.LinkerdCNIPluginChecks)
   156  		}
   157  		values, installManifest, err = renderInstallManifest(cmd.Context())
   158  		if err != nil {
   159  			fmt.Fprintf(os.Stderr, "Error rendering install manifest: %s\n", err)
   160  			os.Exit(1)
   161  		}
   162  	} else if options.crdsOnly {
   163  		checks = append(checks, healthcheck.LinkerdCRDChecks)
   164  	} else {
   165  		checks = append(checks, healthcheck.LinkerdConfigChecks)
   166  
   167  		checks = append(checks, healthcheck.LinkerdControlPlaneExistenceChecks)
   168  		checks = append(checks, healthcheck.LinkerdIdentity)
   169  		checks = append(checks, healthcheck.LinkerdWebhooksAndAPISvcTLS)
   170  		checks = append(checks, healthcheck.LinkerdControlPlaneProxyChecks)
   171  
   172  		if options.dataPlaneOnly {
   173  			checks = append(checks, healthcheck.LinkerdDataPlaneChecks)
   174  			checks = append(checks, healthcheck.LinkerdIdentityDataPlane)
   175  			checks = append(checks, healthcheck.LinkerdOpaquePortsDefinitionChecks)
   176  		} else {
   177  			checks = append(checks, healthcheck.LinkerdControlPlaneVersionChecks)
   178  			checks = append(checks, healthcheck.LinkerdExtensionChecks)
   179  		}
   180  		checks = append(checks, healthcheck.LinkerdCNIPluginChecks)
   181  		checks = append(checks, healthcheck.LinkerdHAChecks)
   182  	}
   183  
   184  	hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
   185  		IsMainCheckCommand:    true,
   186  		ControlPlaneNamespace: controlPlaneNamespace,
   187  		CNINamespace:          cniNamespace,
   188  		DataPlaneNamespace:    options.namespace,
   189  		KubeConfig:            kubeconfigPath,
   190  		KubeContext:           kubeContext,
   191  		Impersonate:           impersonate,
   192  		ImpersonateGroup:      impersonateGroup,
   193  		APIAddr:               apiAddr,
   194  		VersionOverride:       options.versionOverride,
   195  		RetryDeadline:         time.Now().Add(options.wait),
   196  		CNIEnabled:            options.cniEnabled,
   197  		InstallManifest:       installManifest,
   198  		CRDManifest:           crdManifest.String(),
   199  		ChartValues:           values,
   200  	})
   201  
   202  	success, warning := healthcheck.RunChecks(wout, werr, hc, options.output)
   203  
   204  	if !options.preInstallOnly && !options.crdsOnly {
   205  		extensionSuccess, extensionWarning, err := runExtensionChecks(cmd, wout, werr, options)
   206  		if err != nil {
   207  			fmt.Fprintf(werr, "Failed to run extensions checks: %s\n", err)
   208  			os.Exit(1)
   209  		}
   210  
   211  		success = success && extensionSuccess
   212  		warning = warning || extensionWarning
   213  	}
   214  
   215  	healthcheck.PrintChecksResult(wout, options.output, success, warning)
   216  
   217  	if !success {
   218  		os.Exit(1)
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts *checkOptions) (bool, bool, error) {
   225  	kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
   226  	if err != nil {
   227  		return false, false, err
   228  	}
   229  
   230  	namespaces, err := kubeAPI.GetAllNamespacesWithExtensionLabel(cmd.Context())
   231  	if err != nil {
   232  		return false, false, err
   233  	}
   234  	nsLabels := []string{}
   235  	for _, ns := range namespaces {
   236  		ext := ns.Labels[k8s.LinkerdExtensionLabel]
   237  		nsLabels = append(nsLabels, ext)
   238  	}
   239  
   240  	exec := utilsexec.New()
   241  
   242  	extensions, missing := findExtensions(os.Getenv("PATH"), filepath.Glob, exec, nsLabels)
   243  
   244  	// no extensions to check
   245  	if len(extensions) == 0 && len(missing) == 0 {
   246  		return true, false, nil
   247  	}
   248  
   249  	extensionSuccess, extensionWarning := runExtensionsChecks(
   250  		wout, werr, extensions, missing, exec, getExtensionCheckFlags(cmd.Flags()), opts.output,
   251  	)
   252  	return extensionSuccess, extensionWarning, nil
   253  }
   254  
   255  func getExtensionCheckFlags(lf *pflag.FlagSet) []string {
   256  	extensionFlags := []string{
   257  		"api-addr", "context", "as", "as-group", "kubeconfig", "linkerd-namespace", "verbose",
   258  		"namespace", "proxy", "wait",
   259  	}
   260  	cmdLineFlags := []string{}
   261  	for _, flag := range extensionFlags {
   262  		f := lf.Lookup(flag)
   263  		if f != nil {
   264  			val := f.Value.String()
   265  			if val != "" {
   266  				cmdLineFlags = append(cmdLineFlags, fmt.Sprintf("--%s=%s", f.Name, val))
   267  			}
   268  		}
   269  	}
   270  	cmdLineFlags = append(cmdLineFlags, "--output=json")
   271  	return cmdLineFlags
   272  }
   273  
   274  func renderInstallManifest(ctx context.Context) (*charts.Values, string, error) {
   275  	// Create the default values.
   276  	k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 30*time.Second)
   277  	if err != nil {
   278  		return nil, "", err
   279  	}
   280  	values, err := charts.NewValues()
   281  	if err != nil {
   282  		return nil, "", err
   283  	}
   284  	err = initializeIssuerCredentials(ctx, k8sAPI, values)
   285  	if err != nil {
   286  		return nil, "", err
   287  	}
   288  
   289  	// Use empty valuesOverrides because there are no option values to merge.
   290  	var b strings.Builder
   291  	err = renderControlPlane(&b, values, map[string]interface{}{})
   292  	if err != nil {
   293  		return nil, "", err
   294  	}
   295  	return values, b.String(), nil
   296  }
   297  

View as plain text