package cmd import ( "bytes" "errors" "fmt" "net" "os" "regexp" "strings" "time" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/clientcmd" "github.com/fatih/color" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" k8sResource "k8s.io/apimachinery/pkg/api/resource" ) const ( defaultLinkerdNamespace = "linkerd" defaultCNINamespace = "linkerd-cni" defaultClusterDomain = "cluster.local" defaultDockerRegistry = "gcr.io/linkerd-io" 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-:]+$`) alphaNumDashDotSlashColonUnderscore = 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(newCmdGet()) RootCmd.AddCommand(newCmdInject()) 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()) RootCmd.AddCommand(newCmdMulticluster()) RootCmd.AddCommand(newCmdUninstall()) } 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.Errorf("could not set namespace from kubectl context: ensure a valid KUBECONFIG path has been set") return corev1.NamespaceDefault } return ns } // 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 debugImage string debugImageVersion string dockerRegistry string imagePullPolicy string destinationGetNetworks []string ignoreInboundPorts []string ignoreOutboundPorts []string proxyUID int64 proxyLogLevel string proxyInboundPort uint proxyOutboundPort uint proxyControlPort uint proxyAdminPort uint proxyCPURequest string proxyMemoryRequest string proxyCPULimit string proxyMemoryLimit string enableExternalProfiles bool traceCollector string traceCollectorSvcAccount string waitBeforeExitSeconds uint64 ignoreCluster bool // not validated by validate() disableIdentity bool requireIdentityOnInboundPorts []string disableTap bool } func (options *proxyConfigOptions) validate() error { for _, network := range options.destinationGetNetworks { if _, _, err := net.ParseCIDR(network); err != nil { return fmt.Errorf("cannot parse destination get networks: %s", err) } } if options.disableIdentity && len(options.requireIdentityOnInboundPorts) > 0 { return errors.New("Identity must be enabled when --require-identity-on-inbound-ports is specified") } 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) } if err := validateRangeSlice(options.ignoreInboundPorts); err != nil { return err } if err := validateRangeSlice(options.ignoreOutboundPorts); err != nil { return err } 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.StringSliceVar(&options.ignoreInboundPorts, "skip-inbound-ports", options.ignoreInboundPorts, "Ports and/or port ranges (inclusive) that should skip the proxy and send directly to the application") flags.StringSliceVar(&options.ignoreOutboundPorts, "skip-outbound-ports", options.ignoreOutboundPorts, "Outbound ports and/or port ranges (inclusive) 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 }