| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cmd |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "regexp" |
| "strings" |
| ) |
| |
| import ( |
| "github.com/spf13/cobra" |
| "istio.io/pkg/log" |
| "sigs.k8s.io/yaml" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/istioctl/pkg/util/handlers" |
| "github.com/apache/dubbo-go-pixiu/istioctl/pkg/writer/envoy/clusters" |
| "github.com/apache/dubbo-go-pixiu/istioctl/pkg/writer/envoy/configdump" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/host" |
| ) |
| |
| const ( |
| jsonOutput = "json" |
| yamlOutput = "yaml" |
| summaryOutput = "short" |
| prometheusOutput = "prom" |
| prometheusMergedOutput = "prom-merged" |
| ) |
| |
| var ( |
| fqdn, direction, subset string |
| port int |
| verboseProxyConfig bool |
| |
| address, listenerType, statsType string |
| |
| routeName string |
| |
| clusterName, status string |
| |
| // output format (yaml or short) |
| outputFormat string |
| ) |
| |
| // Level is an enumeration of all supported log levels. |
| type Level int |
| |
| const ( |
| defaultLoggerName = "level" |
| defaultOutputLevel = WarningLevel |
| ) |
| |
| const ( |
| // OffLevel disables logging |
| OffLevel Level = iota |
| // CriticalLevel enables critical level logging |
| CriticalLevel |
| // ErrorLevel enables error level logging |
| ErrorLevel |
| // WarningLevel enables warning level logging |
| WarningLevel |
| // InfoLevel enables info level logging |
| InfoLevel |
| // DebugLevel enables debug level logging |
| DebugLevel |
| // TraceLevel enables trace level logging |
| TraceLevel |
| ) |
| |
| // existing sorted active loggers |
| var activeLoggers = []string{ |
| "admin", |
| "aws", |
| "assert", |
| "backtrace", |
| "client", |
| "config", |
| "connection", |
| "conn_handler", // Added through https://github.com/envoyproxy/envoy/pull/8263 |
| "dubbo", |
| "file", |
| "filter", |
| "forward_proxy", |
| "grpc", |
| "hc", |
| "health_checker", |
| "http", |
| "http2", |
| "hystrix", |
| "init", |
| "io", |
| "jwt", |
| "kafka", |
| "lua", |
| "main", |
| "misc", |
| "mongo", |
| "quic", |
| "pool", |
| "rbac", |
| "redis", |
| "router", |
| "runtime", |
| "stats", |
| "secret", |
| "tap", |
| "testing", |
| "thrift", |
| "tracing", |
| "upstream", |
| "udp", |
| "wasm", |
| } |
| |
| var levelToString = map[Level]string{ |
| TraceLevel: "trace", |
| DebugLevel: "debug", |
| InfoLevel: "info", |
| WarningLevel: "warning", |
| ErrorLevel: "error", |
| CriticalLevel: "critical", |
| OffLevel: "off", |
| } |
| |
| var stringToLevel = map[string]Level{ |
| "trace": TraceLevel, |
| "debug": DebugLevel, |
| "info": InfoLevel, |
| "warning": WarningLevel, |
| "error": ErrorLevel, |
| "critical": CriticalLevel, |
| "off": OffLevel, |
| } |
| |
| var ( |
| loggerLevelString = "" |
| reset = false |
| ) |
| |
| func extractConfigDump(podName, podNamespace string) ([]byte, error) { |
| kubeClient, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create k8s client: %v", err) |
| } |
| path := "config_dump" |
| debug, err := kubeClient.EnvoyDo(context.TODO(), podName, podNamespace, "GET", path) |
| if err != nil { |
| return nil, fmt.Errorf("failed to execute command on %s.%s sidecar: %v", podName, podNamespace, err) |
| } |
| return debug, err |
| } |
| |
| func setupPodConfigdumpWriter(podName, podNamespace string, out io.Writer) (*configdump.ConfigWriter, error) { |
| debug, err := extractConfigDump(podName, podNamespace) |
| if err != nil { |
| return nil, err |
| } |
| return setupConfigdumpEnvoyConfigWriter(debug, out) |
| } |
| |
| func readFile(filename string) ([]byte, error) { |
| file := os.Stdin |
| if filename != "-" { |
| var err error |
| file, err = os.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| } |
| defer func() { |
| if err := file.Close(); err != nil { |
| log.Errorf("failed to close %s: %s", filename, err) |
| } |
| }() |
| return io.ReadAll(file) |
| } |
| |
| func setupFileConfigdumpWriter(filename string, out io.Writer) (*configdump.ConfigWriter, error) { |
| data, err := readFile(filename) |
| if err != nil { |
| return nil, err |
| } |
| return setupConfigdumpEnvoyConfigWriter(data, out) |
| } |
| |
| func setupConfigdumpEnvoyConfigWriter(debug []byte, out io.Writer) (*configdump.ConfigWriter, error) { |
| cw := &configdump.ConfigWriter{Stdout: out} |
| err := cw.Prime(debug) |
| if err != nil { |
| return nil, err |
| } |
| return cw, nil |
| } |
| |
| func setupEnvoyClusterStatsConfig(podName, podNamespace string, outputFormat string) (string, error) { |
| kubeClient, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return "", fmt.Errorf("failed to create Kubernetes client: %v", err) |
| } |
| path := "clusters" |
| if outputFormat == jsonOutput || outputFormat == yamlOutput { |
| // for yaml output we will convert the json to yaml when printed |
| path += "?format=json" |
| } |
| result, err := kubeClient.EnvoyDo(context.TODO(), podName, podNamespace, "GET", path) |
| if err != nil { |
| return "", fmt.Errorf("failed to execute command on Envoy: %v", err) |
| } |
| return string(result), nil |
| } |
| |
| func setupEnvoyServerStatsConfig(podName, podNamespace string, outputFormat string) (string, error) { |
| kubeClient, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return "", fmt.Errorf("failed to create Kubernetes client: %v", err) |
| } |
| path := "stats" |
| port := 15000 |
| if outputFormat == jsonOutput || outputFormat == yamlOutput { |
| // for yaml output we will convert the json to yaml when printed |
| path += "?format=json" |
| } else if outputFormat == prometheusOutput { |
| path += "/prometheus" |
| } else if outputFormat == prometheusMergedOutput { |
| path += "/prometheus" |
| port = 15020 |
| } |
| |
| result, err := kubeClient.EnvoyDoWithPort(context.Background(), podName, podNamespace, "GET", path, port) |
| if err != nil { |
| return "", fmt.Errorf("failed to execute command on Envoy: %v", err) |
| } |
| return string(result), nil |
| } |
| |
| func setupEnvoyLogConfig(param, podName, podNamespace string) (string, error) { |
| kubeClient, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return "", fmt.Errorf("failed to create Kubernetes client: %v", err) |
| } |
| path := "logging" |
| if param != "" { |
| path = path + "?" + param |
| } |
| result, err := kubeClient.EnvoyDo(context.TODO(), podName, podNamespace, "POST", path) |
| if err != nil { |
| return "", fmt.Errorf("failed to execute command on Envoy: %v", err) |
| } |
| return string(result), nil |
| } |
| |
| func getLogLevelFromConfigMap() (string, error) { |
| valuesConfig, err := getValuesFromConfigMap(kubeconfig, "") |
| if err != nil { |
| return "", err |
| } |
| var values struct { |
| SidecarInjectorWebhook struct { |
| Global struct { |
| Proxy struct { |
| LogLevel string `json:"logLevel"` |
| } `json:"proxy"` |
| } `json:"global"` |
| } `json:"sidecarInjectorWebhook"` |
| } |
| if err := yaml.Unmarshal([]byte(valuesConfig), &values); err != nil { |
| return "", fmt.Errorf("failed to parse values config: %v [%v]", err, valuesConfig) |
| } |
| return values.SidecarInjectorWebhook.Global.Proxy.LogLevel, nil |
| } |
| |
| func setupPodClustersWriter(podName, podNamespace string, out io.Writer) (*clusters.ConfigWriter, error) { |
| kubeClient, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create k8s client: %v", err) |
| } |
| path := "clusters?format=json" |
| debug, err := kubeClient.EnvoyDo(context.TODO(), podName, podNamespace, "GET", path) |
| if err != nil { |
| return nil, fmt.Errorf("failed to execute command on Envoy: %v", err) |
| } |
| return setupClustersEnvoyConfigWriter(debug, out) |
| } |
| |
| func setupFileClustersWriter(filename string, out io.Writer) (*clusters.ConfigWriter, error) { |
| file, err := os.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| defer func() { |
| if err := file.Close(); err != nil { |
| log.Errorf("failed to close %s: %s", filename, err) |
| } |
| }() |
| data, err := io.ReadAll(file) |
| if err != nil { |
| return nil, err |
| } |
| return setupClustersEnvoyConfigWriter(data, out) |
| } |
| |
| // TODO(fisherxu): migrate this to config dump when implemented in Envoy |
| // Issue to track -> https://github.com/envoyproxy/envoy/issues/3362 |
| func setupClustersEnvoyConfigWriter(debug []byte, out io.Writer) (*clusters.ConfigWriter, error) { |
| cw := &clusters.ConfigWriter{Stdout: out} |
| err := cw.Prime(debug) |
| if err != nil { |
| return nil, err |
| } |
| return cw, nil |
| } |
| |
| func clusterConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| clusterConfigCmd := &cobra.Command{ |
| Use: "cluster [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves cluster configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about cluster configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve summary about cluster configuration for a given pod from Envoy. |
| istioctl proxy-config clusters <pod-name[.namespace]> |
| |
| # Retrieve cluster summary for clusters with port 9080. |
| istioctl proxy-config clusters <pod-name[.namespace]> --port 9080 |
| |
| # Retrieve full cluster dump for clusters that are inbound with a FQDN of details.default.svc.cluster.local. |
| istioctl proxy-config clusters <pod-name[.namespace]> --fqdn details.default.svc.cluster.local --direction inbound -o json |
| |
| # Retrieve cluster summary without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/config_dump' > envoy-config.json |
| istioctl proxy-config clusters --file envoy-config.json |
| `, |
| Aliases: []string{"clusters", "c"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("cluster requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter *configdump.ConfigWriter |
| var err error |
| if len(args) == 1 { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter, err = setupPodConfigdumpWriter(podName, podNamespace, c.OutOrStdout()) |
| } else { |
| configWriter, err = setupFileConfigdumpWriter(configDumpFile, c.OutOrStdout()) |
| } |
| if err != nil { |
| return err |
| } |
| filter := configdump.ClusterFilter{ |
| FQDN: host.Name(fqdn), |
| Port: port, |
| Subset: subset, |
| Direction: model.TrafficDirection(direction), |
| } |
| switch outputFormat { |
| case summaryOutput: |
| return configWriter.PrintClusterSummary(filter) |
| case jsonOutput, yamlOutput: |
| return configWriter.PrintClusterDump(filter, outputFormat) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| clusterConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| clusterConfigCmd.PersistentFlags().StringVar(&fqdn, "fqdn", "", "Filter clusters by substring of Service FQDN field") |
| clusterConfigCmd.PersistentFlags().StringVar(&direction, "direction", "", "Filter clusters by Direction field") |
| clusterConfigCmd.PersistentFlags().StringVar(&subset, "subset", "", "Filter clusters by substring of Subset field") |
| clusterConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter clusters by Port field") |
| clusterConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump JSON file") |
| |
| return clusterConfigCmd |
| } |
| |
| func allConfigCmd() *cobra.Command { |
| allConfigCmd := &cobra.Command{ |
| Use: "all [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves all configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about all configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve summary about all configuration for a given pod from Envoy. |
| istioctl proxy-config all <pod-name[.namespace]> |
| |
| # Retrieve full cluster dump as JSON |
| istioctl proxy-config all <pod-name[.namespace]> -o json |
| |
| # Retrieve full cluster dump with short syntax |
| istioctl pc a <pod-name[.namespace]> |
| |
| # Retrieve cluster summary without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/config_dump' > envoy-config.json |
| istioctl proxy-config all --file envoy-config.json |
| `, |
| Aliases: []string{"a"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("all requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| switch outputFormat { |
| case jsonOutput, yamlOutput: |
| var dump []byte |
| var err error |
| if len(args) == 1 { |
| podName, podNamespace, err := getPodName(args[0]) |
| if err != nil { |
| return err |
| } |
| dump, err = extractConfigDump(podName, podNamespace) |
| if err != nil { |
| return err |
| } |
| } else { |
| dump, err = readFile(configDumpFile) |
| if err != nil { |
| return err |
| } |
| } |
| if outputFormat == yamlOutput { |
| if dump, err = yaml.JSONToYAML(dump); err != nil { |
| return err |
| } |
| } |
| fmt.Fprintln(c.OutOrStdout(), string(dump)) |
| |
| case summaryOutput: |
| var configWriter *configdump.ConfigWriter |
| if len(args) == 1 { |
| podName, podNamespace, err := getPodName(args[0]) |
| if err != nil { |
| return err |
| } |
| configWriter, err = setupPodConfigdumpWriter(podName, podNamespace, c.OutOrStdout()) |
| if err != nil { |
| return err |
| } |
| } else { |
| var err error |
| configWriter, err = setupFileConfigdumpWriter(configDumpFile, c.OutOrStdout()) |
| if err != nil { |
| return err |
| } |
| } |
| return configWriter.PrintFullSummary( |
| configdump.ClusterFilter{ |
| FQDN: host.Name(fqdn), |
| Port: port, |
| Subset: subset, |
| Direction: model.TrafficDirection(direction), |
| }, |
| configdump.ListenerFilter{ |
| Address: address, |
| Port: uint32(port), |
| Type: listenerType, |
| Verbose: verboseProxyConfig, |
| }, |
| configdump.RouteFilter{ |
| Name: routeName, |
| Verbose: verboseProxyConfig, |
| }, |
| ) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| return nil |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| allConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| allConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump file") |
| allConfigCmd.PersistentFlags().BoolVar(&verboseProxyConfig, "verbose", true, "Output more information") |
| |
| // cluster |
| allConfigCmd.PersistentFlags().StringVar(&fqdn, "fqdn", "", "Filter clusters by substring of Service FQDN field") |
| allConfigCmd.PersistentFlags().StringVar(&direction, "direction", "", "Filter clusters by Direction field") |
| allConfigCmd.PersistentFlags().StringVar(&subset, "subset", "", "Filter clusters by substring of Subset field") |
| |
| // applies to cluster and route |
| allConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter clusters and listeners by Port field") |
| |
| // Listener |
| allConfigCmd.PersistentFlags().StringVar(&address, "address", "", "Filter listeners by address field") |
| allConfigCmd.PersistentFlags().StringVar(&listenerType, "type", "", "Filter listeners by type field") |
| |
| // route |
| allConfigCmd.PersistentFlags().StringVar(&routeName, "name", "", "Filter listeners by route name field") |
| |
| return allConfigCmd |
| } |
| |
| func listenerConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| listenerConfigCmd := &cobra.Command{ |
| Use: "listener [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves listener configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about listener configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve summary about listener configuration for a given pod from Envoy. |
| istioctl proxy-config listeners <pod-name[.namespace]> |
| |
| # Retrieve listener summary for listeners with port 9080. |
| istioctl proxy-config listeners <pod-name[.namespace]> --port 9080 |
| |
| # Retrieve full listener dump for HTTP listeners with a wildcard address (0.0.0.0). |
| istioctl proxy-config listeners <pod-name[.namespace]> --type HTTP --address 0.0.0.0 -o json |
| |
| # Retrieve listener summary without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/config_dump' > envoy-config.json |
| istioctl proxy-config listeners --file envoy-config.json |
| `, |
| Aliases: []string{"listeners", "l"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("listener requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter *configdump.ConfigWriter |
| var err error |
| if len(args) == 1 { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter, err = setupPodConfigdumpWriter(podName, podNamespace, c.OutOrStdout()) |
| } else { |
| configWriter, err = setupFileConfigdumpWriter(configDumpFile, c.OutOrStdout()) |
| } |
| if err != nil { |
| return err |
| } |
| filter := configdump.ListenerFilter{ |
| Address: address, |
| Port: uint32(port), |
| Type: listenerType, |
| Verbose: verboseProxyConfig, |
| } |
| |
| switch outputFormat { |
| case summaryOutput: |
| return configWriter.PrintListenerSummary(filter) |
| case jsonOutput, yamlOutput: |
| return configWriter.PrintListenerDump(filter, outputFormat) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| listenerConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| listenerConfigCmd.PersistentFlags().StringVar(&address, "address", "", "Filter listeners by address field") |
| listenerConfigCmd.PersistentFlags().StringVar(&listenerType, "type", "", "Filter listeners by type field") |
| listenerConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter listeners by Port field") |
| listenerConfigCmd.PersistentFlags().BoolVar(&verboseProxyConfig, "verbose", true, "Output more information") |
| listenerConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump JSON file") |
| |
| return listenerConfigCmd |
| } |
| |
| func statsConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| statsConfigCmd := &cobra.Command{ |
| Use: "envoy-stats [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves Envoy metrics in the specified pod", |
| Long: `Retrieve Envoy emitted metrics for the specified pod.`, |
| Example: ` # Retrieve Envoy emitted metrics for the specified pod. |
| istioctl experimental envoy-stats <pod-name[.namespace]> |
| |
| # Retrieve Envoy server metrics in prometheus format |
| istioctl experimental envoy-stats <pod-name[.namespace]> --output prom |
| |
| # Retrieve Envoy server metrics in prometheus format with merged application metrics |
| istioctl experimental envoy-stats <pod-name[.namespace]> --output prom-merged |
| |
| # Retrieve Envoy cluster metrics |
| istioctl experimental envoy-stats <pod-name[.namespace]> --type clusters |
| `, |
| Aliases: []string{"es"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if len(args) != 1 && (labelSelector == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("stats requires pod name or label selector") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var stats string |
| var err error |
| |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| if statsType == "" || statsType == "server" { |
| stats, err = setupEnvoyServerStatsConfig(podName, podNamespace, outputFormat) |
| if err != nil { |
| return err |
| } |
| } else if statsType == "cluster" || statsType == "clusters" { |
| stats, err = setupEnvoyClusterStatsConfig(podName, podNamespace, outputFormat) |
| if err != nil { |
| return err |
| } |
| } else { |
| return fmt.Errorf("unknown stats type %s", statsType) |
| } |
| |
| switch outputFormat { |
| // convert the json output to yaml |
| case yamlOutput: |
| var out []byte |
| if out, err = yaml.JSONToYAML([]byte(stats)); err != nil { |
| return err |
| } |
| _, _ = fmt.Fprint(c.OutOrStdout(), string(out)) |
| default: |
| _, _ = fmt.Fprint(c.OutOrStdout(), stats) |
| } |
| |
| return nil |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| statsConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|prom") |
| statsConfigCmd.PersistentFlags().StringVarP(&statsType, "type", "t", "server", "Where to grab the stats: one of server|clusters") |
| |
| return statsConfigCmd |
| } |
| |
| func logCmd() *cobra.Command { |
| var podName, podNamespace string |
| var podNames []string |
| |
| logCmd := &cobra.Command{ |
| Use: "log [<type>/]<name>[.<namespace>]", |
| Short: "(experimental) Retrieves logging levels of the Envoy in the specified pod", |
| Long: "(experimental) Retrieve information about logging levels of the Envoy instance in the specified pod, and update optionally", |
| Example: ` # Retrieve information about logging levels for a given pod from Envoy. |
| istioctl proxy-config log <pod-name[.namespace]> |
| |
| # Update levels of the all loggers |
| istioctl proxy-config log <pod-name[.namespace]> --level none |
| |
| # Update levels of the specified loggers. |
| istioctl proxy-config log <pod-name[.namespace]> --level http:debug,redis:debug |
| |
| # Reset levels of all the loggers to default value (warning). |
| istioctl proxy-config log <pod-name[.namespace]> -r |
| `, |
| Aliases: []string{"o"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if labelSelector == "" && len(args) < 1 { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("log requires pod name or --selector") |
| } |
| if reset && loggerLevelString != "" { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("--level cannot be combined with --reset") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var err error |
| var loggerNames []string |
| if labelSelector != "" { |
| if podNames, podNamespace, err = getPodNameBySelector(labelSelector); err != nil { |
| return err |
| } |
| for _, pod := range podNames { |
| name, err = setupEnvoyLogConfig("", pod, podNamespace) |
| loggerNames = append(loggerNames, name) |
| } |
| if err != nil { |
| return err |
| } |
| if len(podNames) > 0 { |
| podName = podNames[0] |
| } |
| } else { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| name, err := setupEnvoyLogConfig("", podName, podNamespace) |
| loggerNames = append(loggerNames, name) |
| if err != nil { |
| return err |
| } |
| } |
| |
| destLoggerLevels := map[string]Level{} |
| if reset { |
| // reset logging level to `defaultOutputLevel`, and ignore the `level` option |
| levelString, _ := getLogLevelFromConfigMap() |
| level, ok := stringToLevel[levelString] |
| if ok { |
| destLoggerLevels[defaultLoggerName] = level |
| } else { |
| log.Warnf("unable to get logLevel from ConfigMap istio-sidecar-injector, using default value: %v", |
| levelToString[defaultOutputLevel]) |
| destLoggerLevels[defaultLoggerName] = defaultOutputLevel |
| } |
| } else if loggerLevelString != "" { |
| levels := strings.Split(loggerLevelString, ",") |
| for _, ol := range levels { |
| if !strings.Contains(ol, ":") && !strings.Contains(ol, "=") { |
| level, ok := stringToLevel[ol] |
| if ok { |
| destLoggerLevels = map[string]Level{ |
| defaultLoggerName: level, |
| } |
| } else { |
| return fmt.Errorf("unrecognized logging level: %v", ol) |
| } |
| } else { |
| loggerLevel := regexp.MustCompile(`[:=]`).Split(ol, 2) |
| for _, logName := range loggerNames { |
| if !strings.Contains(logName, loggerLevel[0]) { |
| return fmt.Errorf("unrecognized logger name: %v", loggerLevel[0]) |
| } |
| } |
| level, ok := stringToLevel[loggerLevel[1]] |
| if !ok { |
| return fmt.Errorf("unrecognized logging level: %v", loggerLevel[1]) |
| } |
| destLoggerLevels[loggerLevel[0]] = level |
| } |
| } |
| } |
| |
| var resp string |
| if len(destLoggerLevels) == 0 { |
| resp, err = setupEnvoyLogConfig("", podName, podNamespace) |
| } else { |
| if ll, ok := destLoggerLevels[defaultLoggerName]; ok { |
| // update levels of all loggers first |
| resp, err = setupEnvoyLogConfig(defaultLoggerName+"="+levelToString[ll], podName, podNamespace) |
| delete(destLoggerLevels, defaultLoggerName) |
| } |
| for lg, ll := range destLoggerLevels { |
| resp, err = setupEnvoyLogConfig(lg+"="+levelToString[ll], podName, podNamespace) |
| } |
| } |
| if err != nil { |
| return err |
| } |
| _, _ = fmt.Fprint(c.OutOrStdout(), resp) |
| return nil |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| levelListString := fmt.Sprintf("[%s, %s, %s, %s, %s, %s, %s]", |
| levelToString[TraceLevel], |
| levelToString[DebugLevel], |
| levelToString[InfoLevel], |
| levelToString[WarningLevel], |
| levelToString[ErrorLevel], |
| levelToString[CriticalLevel], |
| levelToString[OffLevel]) |
| s := strings.Join(activeLoggers, ", ") |
| |
| logCmd.PersistentFlags().BoolVarP(&reset, "reset", "r", reset, "Reset levels to default value (warning).") |
| logCmd.PersistentFlags().StringVarP(&labelSelector, "selector", "l", "", "Label selector") |
| logCmd.PersistentFlags().StringVar(&loggerLevelString, "level", loggerLevelString, |
| fmt.Sprintf("Comma-separated minimum per-logger level of messages to output, in the form of"+ |
| " [<logger>:]<level>,[<logger>:]<level>,... where logger can be one of %s and level can be one of %s", |
| s, levelListString)) |
| |
| return logCmd |
| } |
| |
| func routeConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| routeConfigCmd := &cobra.Command{ |
| Use: "route [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves route configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about route configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve summary about route configuration for a given pod from Envoy. |
| istioctl proxy-config routes <pod-name[.namespace]> |
| |
| # Retrieve route summary for route 9080. |
| istioctl proxy-config route <pod-name[.namespace]> --name 9080 |
| |
| # Retrieve full route dump for route 9080 |
| istioctl proxy-config route <pod-name[.namespace]> --name 9080 -o json |
| |
| # Retrieve route summary without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/config_dump' > envoy-config.json |
| istioctl proxy-config routes --file envoy-config.json |
| `, |
| Aliases: []string{"routes", "r"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("route requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter *configdump.ConfigWriter |
| var err error |
| if len(args) == 1 { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter, err = setupPodConfigdumpWriter(podName, podNamespace, c.OutOrStdout()) |
| } else { |
| configWriter, err = setupFileConfigdumpWriter(configDumpFile, c.OutOrStdout()) |
| } |
| if err != nil { |
| return err |
| } |
| filter := configdump.RouteFilter{ |
| Name: routeName, |
| Verbose: verboseProxyConfig, |
| } |
| switch outputFormat { |
| case summaryOutput: |
| return configWriter.PrintRouteSummary(filter) |
| case jsonOutput, yamlOutput: |
| return configWriter.PrintRouteDump(filter, outputFormat) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| routeConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| routeConfigCmd.PersistentFlags().StringVar(&routeName, "name", "", "Filter listeners by route name field") |
| routeConfigCmd.PersistentFlags().BoolVar(&verboseProxyConfig, "verbose", true, "Output more information") |
| routeConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump JSON file") |
| |
| return routeConfigCmd |
| } |
| |
| func endpointConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| endpointConfigCmd := &cobra.Command{ |
| Use: "endpoint [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves endpoint configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about endpoint configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve full endpoint configuration for a given pod from Envoy. |
| istioctl proxy-config endpoint <pod-name[.namespace]> |
| |
| # Retrieve endpoint summary for endpoint with port 9080. |
| istioctl proxy-config endpoint <pod-name[.namespace]> --port 9080 |
| |
| # Retrieve full endpoint with a address (172.17.0.2). |
| istioctl proxy-config endpoint <pod-name[.namespace]> --address 172.17.0.2 -o json |
| |
| # Retrieve full endpoint with a cluster name (outbound|9411||zipkin.dubbo-system.svc.cluster.local). |
| istioctl proxy-config endpoint <pod-name[.namespace]> --cluster "outbound|9411||zipkin.dubbo-system.svc.cluster.local" -o json |
| # Retrieve full endpoint with the status (healthy). |
| istioctl proxy-config endpoint <pod-name[.namespace]> --status healthy -ojson |
| |
| # Retrieve endpoint summary without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/clusters?format=json' > envoy-clusters.json |
| istioctl proxy-config endpoints --file envoy-clusters.json |
| `, |
| Aliases: []string{"endpoints", "ep"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("endpoints requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter *clusters.ConfigWriter |
| var err error |
| if len(args) == 1 { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter, err = setupPodClustersWriter(podName, podNamespace, c.OutOrStdout()) |
| } else { |
| configWriter, err = setupFileClustersWriter(configDumpFile, c.OutOrStdout()) |
| } |
| if err != nil { |
| return err |
| } |
| |
| filter := clusters.EndpointFilter{ |
| Address: address, |
| Port: uint32(port), |
| Cluster: clusterName, |
| Status: status, |
| } |
| |
| switch outputFormat { |
| case summaryOutput: |
| return configWriter.PrintEndpointsSummary(filter) |
| case jsonOutput, yamlOutput: |
| return configWriter.PrintEndpoints(filter, outputFormat) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| endpointConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| endpointConfigCmd.PersistentFlags().StringVar(&address, "address", "", "Filter endpoints by address field") |
| endpointConfigCmd.PersistentFlags().IntVar(&port, "port", 0, "Filter endpoints by Port field") |
| endpointConfigCmd.PersistentFlags().StringVar(&clusterName, "cluster", "", "Filter endpoints by cluster name field") |
| endpointConfigCmd.PersistentFlags().StringVar(&status, "status", "", "Filter endpoints by status field") |
| endpointConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump JSON file") |
| |
| return endpointConfigCmd |
| } |
| |
| func bootstrapConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| // Shadow outputVariable since this command uses a different default value |
| var outputFormat string |
| |
| bootstrapConfigCmd := &cobra.Command{ |
| Use: "bootstrap [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves bootstrap configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about bootstrap configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve full bootstrap configuration for a given pod from Envoy. |
| istioctl proxy-config bootstrap <pod-name[.namespace]> |
| |
| # Retrieve full bootstrap without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/config_dump' > envoy-config.json |
| istioctl proxy-config bootstrap --file envoy-config.json |
| |
| # Show a human-readable Istio and Envoy version summary |
| istioctl proxy-config bootstrap -o short |
| `, |
| Aliases: []string{"b"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("bootstrap requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter *configdump.ConfigWriter |
| var err error |
| if len(args) == 1 { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter, err = setupPodConfigdumpWriter(podName, podNamespace, c.OutOrStdout()) |
| } else { |
| configWriter, err = setupFileConfigdumpWriter(configDumpFile, c.OutOrStdout()) |
| } |
| if err != nil { |
| return err |
| } |
| |
| switch outputFormat { |
| case summaryOutput: |
| return configWriter.PrintVersionSummary() |
| case jsonOutput, yamlOutput: |
| return configWriter.PrintBootstrapDump(outputFormat) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| bootstrapConfigCmd.Flags().StringVarP(&outputFormat, "output", "o", jsonOutput, "Output format: one of json|yaml|short") |
| bootstrapConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump JSON file") |
| |
| return bootstrapConfigCmd |
| } |
| |
| func secretConfigCmd() *cobra.Command { |
| var podName, podNamespace string |
| |
| secretConfigCmd := &cobra.Command{ |
| Use: "secret [<type>/]<name>[.<namespace>]", |
| Short: "Retrieves secret configuration for the Envoy in the specified pod", |
| Long: `Retrieve information about secret configuration for the Envoy instance in the specified pod.`, |
| Example: ` # Retrieve full secret configuration for a given pod from Envoy. |
| istioctl proxy-config secret <pod-name[.namespace]> |
| |
| # Retrieve full bootstrap without using Kubernetes API |
| ssh <user@hostname> 'curl localhost:15000/config_dump' > envoy-config.json |
| istioctl proxy-config secret --file envoy-config.json`, |
| Aliases: []string{"secrets", "s"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if (len(args) == 1) != (configDumpFile == "") { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("secret requires pod name or --file parameter") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter *configdump.ConfigWriter |
| var err error |
| if len(args) == 1 { |
| if podName, podNamespace, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter, err = setupPodConfigdumpWriter(podName, podNamespace, c.OutOrStdout()) |
| } else { |
| configWriter, err = setupFileConfigdumpWriter(configDumpFile, c.OutOrStdout()) |
| } |
| if err != nil { |
| return err |
| } |
| switch outputFormat { |
| case summaryOutput: |
| return configWriter.PrintSecretSummary() |
| case jsonOutput, yamlOutput: |
| return configWriter.PrintSecretDump(outputFormat) |
| default: |
| return fmt.Errorf("output format %q not supported", outputFormat) |
| } |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| secretConfigCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| secretConfigCmd.PersistentFlags().StringVarP(&configDumpFile, "file", "f", "", |
| "Envoy config dump JSON file") |
| secretConfigCmd.Long += "\n\n" + ExperimentalMsg |
| return secretConfigCmd |
| } |
| |
| func rootCACompareConfigCmd() *cobra.Command { |
| var podName1, podName2, podNamespace1, podNamespace2 string |
| |
| rootCACompareConfigCmd := &cobra.Command{ |
| Use: "rootca-compare [pod/]<name-1>[.<namespace-1>] [pod/]<name-2>[.<namespace-2>]", |
| Short: "Compare ROOTCA values for the two given pods", |
| Long: `Compare ROOTCA values for given 2 pods to check the connectivity between them.`, |
| Example: ` # Compare ROOTCA values for given 2 pods to check the connectivity between them. |
| istioctl proxy-config rootca-compare <pod-name-1[.namespace]> <pod-name-2[.namespace]>`, |
| Aliases: []string{"rc"}, |
| Args: func(cmd *cobra.Command, args []string) error { |
| if len(args) != 2 { |
| cmd.Println(cmd.UsageString()) |
| return fmt.Errorf("rootca-compare requires 2 pods as an argument") |
| } |
| return nil |
| }, |
| RunE: func(c *cobra.Command, args []string) error { |
| var configWriter1, configWriter2 *configdump.ConfigWriter |
| var err error |
| if len(args) == 2 { |
| if podName1, podNamespace1, err = getPodName(args[0]); err != nil { |
| return err |
| } |
| configWriter1, err = setupPodConfigdumpWriter(podName1, podNamespace1, c.OutOrStdout()) |
| if err != nil { |
| return err |
| } |
| |
| if podName2, podNamespace2, err = getPodName(args[1]); err != nil { |
| return err |
| } |
| configWriter2, err = setupPodConfigdumpWriter(podName2, podNamespace2, c.OutOrStdout()) |
| if err != nil { |
| return err |
| } |
| } else { |
| c.Println(c.UsageString()) |
| return fmt.Errorf("rootca-compare requires 2 pods as an argument") |
| } |
| |
| rootCA1, err1 := configWriter1.PrintPodRootCAFromDynamicSecretDump() |
| if err1 != nil { |
| return fmt.Errorf("error when retrieving ROOTCA of [%s]: %v", args[0], err1) |
| } |
| rootCA2, err2 := configWriter2.PrintPodRootCAFromDynamicSecretDump() |
| if err2 != nil { |
| return fmt.Errorf("error when retrieving ROOTCA of [%s]: %v", args[1], err2) |
| } |
| |
| var returnErr error |
| if rootCA1 == rootCA2 { |
| report := fmt.Sprintf("Both [%s.%s] and [%s.%s] have the identical ROOTCA, theoretically the connectivity between them is available", |
| podName1, podNamespace1, podName2, podNamespace2) |
| c.Println(report) |
| returnErr = nil |
| } else { |
| report := fmt.Sprintf("Both [%s.%s] and [%s.%s] have the non identical ROOTCA, theoretically the connectivity between them is unavailable", |
| podName1, podNamespace1, podName2, podNamespace2) |
| returnErr = fmt.Errorf(report) |
| } |
| return returnErr |
| }, |
| ValidArgsFunction: validPodsNameArgs, |
| } |
| |
| rootCACompareConfigCmd.Long += "\n\n" + ExperimentalMsg |
| return rootCACompareConfigCmd |
| } |
| |
| func proxyConfig() *cobra.Command { |
| configCmd := &cobra.Command{ |
| Use: "proxy-config", |
| Short: "Retrieve information about proxy configuration from Envoy [kube only]", |
| Long: `A group of commands used to retrieve information about proxy configuration from the Envoy config dump`, |
| Example: ` # Retrieve information about proxy configuration from an Envoy instance. |
| istioctl proxy-config <clusters|listeners|routes|endpoints|bootstrap|log|secret> <pod-name[.namespace]>`, |
| Aliases: []string{"pc"}, |
| } |
| |
| configCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", summaryOutput, "Output format: one of json|yaml|short") |
| |
| configCmd.AddCommand(clusterConfigCmd()) |
| configCmd.AddCommand(allConfigCmd()) |
| configCmd.AddCommand(listenerConfigCmd()) |
| configCmd.AddCommand(logCmd()) |
| configCmd.AddCommand(routeConfigCmd()) |
| configCmd.AddCommand(bootstrapConfigCmd()) |
| configCmd.AddCommand(endpointConfigCmd()) |
| configCmd.AddCommand(secretConfigCmd()) |
| configCmd.AddCommand(rootCACompareConfigCmd()) |
| |
| return configCmd |
| } |
| |
| func getPodName(podflag string) (string, string, error) { |
| kubeClient, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return "", "", fmt.Errorf("failed to create k8s client: %w", err) |
| } |
| var podName, ns string |
| podName, ns, err = handlers.InferPodInfoFromTypedResource(podflag, |
| handlers.HandleNamespace(namespace, defaultNamespace), |
| kubeClient.UtilFactory()) |
| if err != nil { |
| return "", "", err |
| } |
| return podName, ns, nil |
| } |
| |
| func getPodNameBySelector(labelSelector string) ([]string, string, error) { |
| var ( |
| podNames []string |
| ns string |
| ) |
| client, err := kubeClient(kubeconfig, configContext) |
| if err != nil { |
| return nil, "", fmt.Errorf("failed to create k8s client: %w", err) |
| } |
| pl, err := client.PodsForSelector(context.TODO(), handlers.HandleNamespace(namespace, defaultNamespace), labelSelector) |
| if err != nil { |
| return nil, "", fmt.Errorf("not able to locate pod with selector %s: %v", labelSelector, err) |
| } |
| if len(pl.Items) < 1 { |
| return nil, "", errors.New("no pods found") |
| } |
| for _, pod := range pl.Items { |
| podNames = append(podNames, pod.Name) |
| } |
| ns = pl.Items[0].Namespace |
| return podNames, ns, nil |
| } |