mirror of https://github.com/linkerd/linkerd2.git
306 lines
12 KiB
Go
306 lines
12 KiB
Go
package cmd
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"os"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/spf13/pflag"
|
||
|
||
"github.com/fatih/color"
|
||
log "github.com/sirupsen/logrus"
|
||
"github.com/spf13/cobra"
|
||
k8sResource "k8s.io/apimachinery/pkg/api/resource"
|
||
)
|
||
|
||
const (
|
||
defaultNamespace = "linkerd"
|
||
defaultClusterDomain = "cluster.local"
|
||
defaultDockerRegistry = "gcr.io/linkerd-io"
|
||
|
||
jsonOutput = "json"
|
||
tableOutput = "table"
|
||
wideOutput = "wide"
|
||
)
|
||
|
||
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
|
||
apiAddr string // An empty value means "use the Kubernetes configuration"
|
||
kubeconfigPath string
|
||
kubeContext string
|
||
impersonate 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 == defaultNamespace && controlPlaneNamespaceFromEnv != "" {
|
||
controlPlaneNamespace = controlPlaneNamespaceFromEnv
|
||
}
|
||
|
||
if !alphaNumDash.MatchString(controlPlaneNamespace) {
|
||
return fmt.Errorf("%s is not a valid namespace", controlPlaneNamespace)
|
||
}
|
||
|
||
return nil
|
||
},
|
||
}
|
||
|
||
func init() {
|
||
RootCmd.PersistentFlags().StringVarP(&controlPlaneNamespace, "linkerd-namespace", "l", defaultNamespace, "Namespace in which Linkerd is installed [$LINKERD_NAMESPACE]")
|
||
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().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(newCmdCheck())
|
||
RootCmd.AddCommand(newCmdCompletion())
|
||
RootCmd.AddCommand(newCmdDashboard())
|
||
RootCmd.AddCommand(newCmdDoc())
|
||
RootCmd.AddCommand(newCmdEdges())
|
||
RootCmd.AddCommand(newCmdEndpoints())
|
||
RootCmd.AddCommand(newCmdGet())
|
||
RootCmd.AddCommand(newCmdInject())
|
||
RootCmd.AddCommand(newCmdInternal())
|
||
RootCmd.AddCommand(newCmdInstall())
|
||
RootCmd.AddCommand(newCmdInstallCNIPlugin())
|
||
RootCmd.AddCommand(newCmdInstallSP())
|
||
RootCmd.AddCommand(newCmdLogs())
|
||
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())
|
||
}
|
||
|
||
type statOptionsBase struct {
|
||
namespace string
|
||
timeWindow string
|
||
outputFormat string
|
||
}
|
||
|
||
func newStatOptionsBase() *statOptionsBase {
|
||
return &statOptionsBase{
|
||
namespace: "default",
|
||
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
|
||
out = string(buffer.Bytes()[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)
|
||
}
|
||
|
||
// proxyConfigOptions holds values for command line flags that apply to both the
|
||
// install and inject commands. All fields in this struct should have
|
||
// corresponding flags added in the addProxyConfigFlags func later in this file.
|
||
type proxyConfigOptions struct {
|
||
proxyVersion string
|
||
proxyImage string
|
||
initImage string
|
||
initImageVersion string
|
||
dockerRegistry string
|
||
imagePullPolicy string
|
||
ignoreInboundPorts []uint
|
||
ignoreOutboundPorts []uint
|
||
proxyUID int64
|
||
proxyLogLevel string
|
||
proxyInboundPort uint
|
||
proxyOutboundPort uint
|
||
proxyControlPort uint
|
||
proxyAdminPort uint
|
||
proxyCPURequest string
|
||
proxyMemoryRequest string
|
||
proxyCPULimit string
|
||
proxyMemoryLimit string
|
||
enableExternalProfiles bool
|
||
// ignoreCluster is not validated by validate().
|
||
ignoreCluster bool
|
||
disableIdentity bool
|
||
disableTap bool
|
||
}
|
||
|
||
func (options *proxyConfigOptions) validate() error {
|
||
if options.proxyVersion != "" && !alphaNumDashDot.MatchString(options.proxyVersion) {
|
||
return fmt.Errorf("%s is not a valid version", options.proxyVersion)
|
||
}
|
||
|
||
if options.initImageVersion != "" && !alphaNumDashDot.MatchString(options.initImageVersion) {
|
||
return fmt.Errorf("%s is not a valid version", options.initImageVersion)
|
||
}
|
||
|
||
if options.dockerRegistry != "" && !alphaNumDashDotSlashColon.MatchString(options.dockerRegistry) {
|
||
return fmt.Errorf("%s is not a valid Docker registry. The url can contain only letters, numbers, dash, dot, slash and colon", options.dockerRegistry)
|
||
}
|
||
|
||
if options.imagePullPolicy != "" && options.imagePullPolicy != "Always" && options.imagePullPolicy != "IfNotPresent" && options.imagePullPolicy != "Never" {
|
||
return fmt.Errorf("--image-pull-policy must be one of: Always, IfNotPresent, Never")
|
||
}
|
||
|
||
if options.proxyCPURequest != "" {
|
||
if _, err := k8sResource.ParseQuantity(options.proxyCPURequest); err != nil {
|
||
return fmt.Errorf("Invalid cpu request '%s' for --proxy-cpu-request flag", options.proxyCPURequest)
|
||
}
|
||
}
|
||
|
||
if options.proxyMemoryRequest != "" {
|
||
if _, err := k8sResource.ParseQuantity(options.proxyMemoryRequest); err != nil {
|
||
return fmt.Errorf("Invalid memory request '%s' for --proxy-memory-request flag", options.proxyMemoryRequest)
|
||
}
|
||
}
|
||
|
||
if options.proxyCPULimit != "" {
|
||
cpuLimit, err := k8sResource.ParseQuantity(options.proxyCPULimit)
|
||
if err != nil {
|
||
return fmt.Errorf("Invalid cpu limit '%s' for --proxy-cpu-limit flag", options.proxyCPULimit)
|
||
}
|
||
if options.proxyCPURequest != "" {
|
||
// Not checking for error because option proxyCPURequest was already validated
|
||
if cpuRequest, _ := k8sResource.ParseQuantity(options.proxyCPURequest); cpuRequest.MilliValue() > cpuLimit.MilliValue() {
|
||
return fmt.Errorf("The cpu limit '%s' cannot be lower than the cpu request '%s'", options.proxyCPULimit, options.proxyCPURequest)
|
||
}
|
||
}
|
||
}
|
||
|
||
if options.proxyMemoryLimit != "" {
|
||
memoryLimit, err := k8sResource.ParseQuantity(options.proxyMemoryLimit)
|
||
if err != nil {
|
||
return fmt.Errorf("Invalid memory limit '%s' for --proxy-memory-limit flag", options.proxyMemoryLimit)
|
||
}
|
||
if options.proxyMemoryRequest != "" {
|
||
// Not checking for error because option proxyMemoryRequest was already validated
|
||
if memoryRequest, _ := k8sResource.ParseQuantity(options.proxyMemoryRequest); memoryRequest.Value() > memoryLimit.Value() {
|
||
return fmt.Errorf("The memory limit '%s' cannot be lower than the memory request '%s'", options.proxyMemoryLimit, options.proxyMemoryRequest)
|
||
}
|
||
}
|
||
}
|
||
|
||
if options.proxyLogLevel != "" && !validProxyLogLevel.MatchString(options.proxyLogLevel) {
|
||
return fmt.Errorf("\"%s\" is not a valid proxy log level - for allowed syntax check https://docs.rs/env_logger/0.6.0/env_logger/#enabling-logging",
|
||
options.proxyLogLevel)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// registryOverride replaces the registry of the provided image if the image is
|
||
// using the default registry and the provided registry is not the default.
|
||
func registryOverride(image, registry string) string {
|
||
return strings.Replace(image, defaultDockerRegistry, registry, 1)
|
||
}
|
||
|
||
func (options *proxyConfigOptions) flagSet(e pflag.ErrorHandling) *pflag.FlagSet {
|
||
flags := pflag.NewFlagSet("proxy", e)
|
||
flags.StringVarP(&options.proxyVersion, "proxy-version", "v", options.proxyVersion, "Tag to be used for the Linkerd proxy images")
|
||
flags.StringVar(&options.proxyImage, "proxy-image", options.proxyImage, "Linkerd proxy container image name")
|
||
flags.StringVar(&options.initImage, "init-image", options.initImage, "Linkerd init container image name")
|
||
flags.StringVar(&options.initImageVersion, "init-image-version", options.initImageVersion, "Linkerd init container image version")
|
||
flags.StringVar(&options.dockerRegistry, "registry", options.dockerRegistry, "Docker registry to pull images from")
|
||
flags.StringVar(&options.imagePullPolicy, "image-pull-policy", options.imagePullPolicy, "Docker image pull policy")
|
||
flags.UintVar(&options.proxyInboundPort, "inbound-port", options.proxyInboundPort, "Proxy port to use for inbound traffic")
|
||
flags.UintVar(&options.proxyOutboundPort, "outbound-port", options.proxyOutboundPort, "Proxy port to use for outbound traffic")
|
||
flags.UintSliceVar(&options.ignoreInboundPorts, "skip-inbound-ports", options.ignoreInboundPorts, "Ports that should skip the proxy and send directly to the application")
|
||
flags.UintSliceVar(&options.ignoreOutboundPorts, "skip-outbound-ports", options.ignoreOutboundPorts, "Outbound ports that should skip the proxy")
|
||
flags.Int64Var(&options.proxyUID, "proxy-uid", options.proxyUID, "Run the proxy under this user ID")
|
||
flags.StringVar(&options.proxyLogLevel, "proxy-log-level", options.proxyLogLevel, "Log level for the proxy")
|
||
flags.UintVar(&options.proxyControlPort, "control-port", options.proxyControlPort, "Proxy port to use for control")
|
||
flags.UintVar(&options.proxyAdminPort, "admin-port", options.proxyAdminPort, "Proxy port to serve metrics on")
|
||
flags.StringVar(&options.proxyCPURequest, "proxy-cpu-request", options.proxyCPURequest, "Amount of CPU units that the proxy sidecar requests")
|
||
flags.StringVar(&options.proxyMemoryRequest, "proxy-memory-request", options.proxyMemoryRequest, "Amount of Memory that the proxy sidecar requests")
|
||
flags.StringVar(&options.proxyCPULimit, "proxy-cpu-limit", options.proxyCPULimit, "Maximum amount of CPU units that the proxy sidecar can use")
|
||
flags.StringVar(&options.proxyMemoryLimit, "proxy-memory-limit", options.proxyMemoryLimit, "Maximum amount of Memory that the proxy sidecar can use")
|
||
flags.BoolVar(&options.enableExternalProfiles, "enable-external-profiles", options.enableExternalProfiles, "Enable service profiles for non-Kubernetes services")
|
||
|
||
// Deprecated flags
|
||
flags.StringVar(&options.proxyMemoryRequest, "proxy-memory", options.proxyMemoryRequest, "Amount of Memory that the proxy sidecar requests")
|
||
flags.StringVar(&options.proxyCPURequest, "proxy-cpu", options.proxyCPURequest, "Amount of CPU units that the proxy sidecar requests")
|
||
flags.MarkDeprecated("proxy-memory", "use --proxy-memory-request instead")
|
||
flags.MarkDeprecated("proxy-cpu", "use --proxy-cpu-request instead")
|
||
|
||
return flags
|
||
}
|