mirror of https://github.com/linkerd/linkerd2.git
243 lines
7.7 KiB
Go
243 lines
7.7 KiB
Go
package cmd
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"os"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/fatih/color"
|
||
"github.com/linkerd/linkerd2/cli/flag"
|
||
jaeger "github.com/linkerd/linkerd2/jaeger/cmd"
|
||
multicluster "github.com/linkerd/linkerd2/multicluster/cmd"
|
||
viz "github.com/linkerd/linkerd2/viz/cmd"
|
||
log "github.com/sirupsen/logrus"
|
||
"github.com/spf13/cobra"
|
||
corev1 "k8s.io/api/core/v1"
|
||
"k8s.io/client-go/tools/clientcmd"
|
||
)
|
||
|
||
const (
|
||
defaultLinkerdNamespace = "linkerd"
|
||
defaultCNINamespace = "linkerd-cni"
|
||
defaultLinkerdVizNamespace = "linkerd-viz"
|
||
defaultClusterDomain = "cluster.local"
|
||
defaultDockerRegistry = "ghcr.io/linkerd"
|
||
|
||
jsonOutput = "json"
|
||
tableOutput = "table"
|
||
wideOutput = "wide"
|
||
|
||
maxRps = 100.0
|
||
)
|
||
|
||
var (
|
||
// special handling for Windows, on all other platforms these resolve to
|
||
// os.Stdout and os.Stderr, thanks to https://github.com/mattn/go-colorable
|
||
stdout = color.Output
|
||
stderr = color.Error
|
||
|
||
okStatus = color.New(color.FgGreen, color.Bold).SprintFunc()("\u221A") // √
|
||
warnStatus = color.New(color.FgYellow, color.Bold).SprintFunc()("\u203C") // ‼
|
||
failStatus = color.New(color.FgRed, color.Bold).SprintFunc()("\u00D7") // ×
|
||
|
||
controlPlaneNamespace string
|
||
cniNamespace string
|
||
apiAddr string // An empty value means "use the Kubernetes configuration"
|
||
kubeconfigPath string
|
||
kubeContext string
|
||
defaultNamespace string // Default namespace taken from current kubectl context
|
||
impersonate string
|
||
impersonateGroup []string
|
||
verbose bool
|
||
|
||
// These regexs are not as strict as they could be, but are a quick and dirty
|
||
// sanity check against illegal characters.
|
||
alphaNumDash = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
||
alphaNumDashDot = regexp.MustCompile(`^[\.a-zA-Z0-9-]+$`)
|
||
alphaNumDashDotSlashColon = regexp.MustCompile(`^[\./a-zA-Z0-9-:]+$`)
|
||
|
||
// Full Rust log level syntax at
|
||
// https://docs.rs/env_logger/0.6.0/env_logger/#enabling-logging
|
||
r = strings.NewReplacer("\t", "", "\n", "")
|
||
validProxyLogLevel = regexp.MustCompile(r.Replace(`
|
||
^(
|
||
(
|
||
(trace|debug|warn|info|error)|
|
||
(\w|::)+|
|
||
((\w|::)+=(trace|debug|warn|info|error))
|
||
)(?:,|$)
|
||
)+$`))
|
||
)
|
||
|
||
// RootCmd represents the root Cobra command
|
||
var RootCmd = &cobra.Command{
|
||
Use: "linkerd",
|
||
Short: "linkerd manages the Linkerd service mesh",
|
||
Long: `linkerd manages the Linkerd service mesh.`,
|
||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||
// enable / disable logging
|
||
if verbose {
|
||
log.SetLevel(log.DebugLevel)
|
||
} else {
|
||
log.SetLevel(log.PanicLevel)
|
||
}
|
||
|
||
controlPlaneNamespaceFromEnv := os.Getenv("LINKERD_NAMESPACE")
|
||
if controlPlaneNamespace == defaultLinkerdNamespace && controlPlaneNamespaceFromEnv != "" {
|
||
controlPlaneNamespace = controlPlaneNamespaceFromEnv
|
||
}
|
||
|
||
if !alphaNumDash.MatchString(controlPlaneNamespace) {
|
||
return fmt.Errorf("%s is not a valid namespace", controlPlaneNamespace)
|
||
}
|
||
|
||
return nil
|
||
},
|
||
}
|
||
|
||
func init() {
|
||
defaultNamespace = getDefaultNamespace()
|
||
RootCmd.PersistentFlags().StringVarP(&controlPlaneNamespace, "linkerd-namespace", "L", defaultLinkerdNamespace, "Namespace in which Linkerd is installed ($LINKERD_NAMESPACE)")
|
||
RootCmd.PersistentFlags().StringVarP(&cniNamespace, "cni-namespace", "", defaultCNINamespace, "Namespace in which the Linkerd CNI plugin is installed")
|
||
RootCmd.PersistentFlags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests")
|
||
RootCmd.PersistentFlags().StringVar(&kubeContext, "context", "", "Name of the kubeconfig context to use")
|
||
RootCmd.PersistentFlags().StringVar(&impersonate, "as", "", "Username to impersonate for Kubernetes operations")
|
||
RootCmd.PersistentFlags().StringArrayVar(&impersonateGroup, "as-group", []string{}, "Group to impersonate for Kubernetes operations")
|
||
RootCmd.PersistentFlags().StringVar(&apiAddr, "api-addr", "", "Override kubeconfig and communicate directly with the control plane at host:port (mostly for testing)")
|
||
RootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Turn on debug logging")
|
||
RootCmd.AddCommand(newCmdAlpha())
|
||
RootCmd.AddCommand(newCmdCheck())
|
||
RootCmd.AddCommand(newCmdCompletion())
|
||
RootCmd.AddCommand(newCmdDashboard())
|
||
RootCmd.AddCommand(newCmdDiagnostics())
|
||
RootCmd.AddCommand(newCmdDoc())
|
||
RootCmd.AddCommand(newCmdEdges())
|
||
RootCmd.AddCommand(newCmdEndpoints())
|
||
RootCmd.AddCommand(newCmdInject())
|
||
RootCmd.AddCommand(newCmdInstall())
|
||
RootCmd.AddCommand(newCmdInstallCNIPlugin())
|
||
RootCmd.AddCommand(newCmdInstallSP())
|
||
RootCmd.AddCommand(newCmdMetrics())
|
||
RootCmd.AddCommand(newCmdProfile())
|
||
RootCmd.AddCommand(newCmdRoutes())
|
||
RootCmd.AddCommand(newCmdStat())
|
||
RootCmd.AddCommand(newCmdTap())
|
||
RootCmd.AddCommand(newCmdTop())
|
||
RootCmd.AddCommand(newCmdUninject())
|
||
RootCmd.AddCommand(newCmdUpgrade())
|
||
RootCmd.AddCommand(newCmdVersion())
|
||
RootCmd.AddCommand(newCmdUninstall())
|
||
|
||
// Extension Sub Commands
|
||
RootCmd.AddCommand(jaeger.NewCmdJaeger())
|
||
RootCmd.AddCommand(multicluster.NewCmdMulticluster())
|
||
RootCmd.AddCommand(viz.NewCmdViz())
|
||
}
|
||
|
||
type statOptionsBase struct {
|
||
namespace string
|
||
timeWindow string
|
||
outputFormat string
|
||
}
|
||
|
||
func newStatOptionsBase() *statOptionsBase {
|
||
return &statOptionsBase{
|
||
namespace: defaultNamespace,
|
||
timeWindow: "1m",
|
||
outputFormat: tableOutput,
|
||
}
|
||
}
|
||
|
||
func (o *statOptionsBase) validateOutputFormat() error {
|
||
switch o.outputFormat {
|
||
case tableOutput, jsonOutput, wideOutput:
|
||
return nil
|
||
default:
|
||
return fmt.Errorf("--output currently only supports %s, %s and %s", tableOutput, jsonOutput, wideOutput)
|
||
}
|
||
}
|
||
|
||
func renderStats(buffer bytes.Buffer, options *statOptionsBase) string {
|
||
var out string
|
||
switch options.outputFormat {
|
||
case jsonOutput:
|
||
out = buffer.String()
|
||
default:
|
||
// strip left padding on the first column
|
||
b := buffer.Bytes()
|
||
if len(b) > padding {
|
||
out = string(b[padding:])
|
||
}
|
||
out = strings.Replace(out, "\n"+strings.Repeat(" ", padding), "\n", -1)
|
||
}
|
||
|
||
return out
|
||
}
|
||
|
||
// getRequestRate calculates request rate from Public API BasicStats.
|
||
func getRequestRate(success, failure uint64, timeWindow string) float64 {
|
||
windowLength, err := time.ParseDuration(timeWindow)
|
||
if err != nil {
|
||
log.Error(err.Error())
|
||
return 0.0
|
||
}
|
||
return float64(success+failure) / windowLength.Seconds()
|
||
}
|
||
|
||
// getSuccessRate calculates success rate from Public API BasicStats.
|
||
func getSuccessRate(success, failure uint64) float64 {
|
||
if success+failure == 0 {
|
||
return 0.0
|
||
}
|
||
return float64(success) / float64(success+failure)
|
||
}
|
||
|
||
// getDefaultNamespace fetches the default namespace
|
||
// used in the current KubeConfig context
|
||
func getDefaultNamespace() string {
|
||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||
|
||
if kubeconfigPath != "" {
|
||
rules.ExplicitPath = kubeconfigPath
|
||
}
|
||
|
||
overrides := &clientcmd.ConfigOverrides{CurrentContext: kubeContext}
|
||
kubeCfg := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
|
||
ns, _, err := kubeCfg.Namespace()
|
||
|
||
if err != nil {
|
||
log.Warnf(`could not set namespace from kubectl context, using 'default' namespace: %s
|
||
ensure the KUBECONFIG path %s is valid`, err, kubeconfigPath)
|
||
return corev1.NamespaceDefault
|
||
}
|
||
|
||
return ns
|
||
}
|
||
|
||
// registryOverride replaces the registry-portion of the provided image with the provided registry.
|
||
func registryOverride(image, newRegistry string) string {
|
||
if image == "" {
|
||
return image
|
||
}
|
||
registry := newRegistry
|
||
if registry != "" && !strings.HasSuffix(registry, slash) {
|
||
registry += slash
|
||
}
|
||
imageName := image
|
||
if strings.Contains(image, slash) {
|
||
imageName = image[strings.LastIndex(image, slash)+1:]
|
||
}
|
||
return registry + imageName
|
||
}
|
||
|
||
func flattenFlags(flags ...[]flag.Flag) []flag.Flag {
|
||
out := []flag.Flag{}
|
||
for _, f := range flags {
|
||
out = append(out, f...)
|
||
}
|
||
return out
|
||
}
|