diff --git a/CHANGELOG.md b/CHANGELOG.md index 15fc80192d..07fab92f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ FEATURES: * Transparent Proxy Egress * Add support for Destinations on the Service Defaults CRD. [[GH-1352](https://github.com/hashicorp/consul-k8s/pull/1352)] +* CLI: + * Add `consul-k8s proxy list` command for displaying Pods running Envoy managed by Consul. [[GH-1271](https://github.com/hashicorp/consul-k8s/pull/1271) + * Add `consul-k8s proxy read podname` command for displaying Envoy configuration for a given Pod. [[GH-1271](https://github.com/hashicorp/consul-k8s/pull/1271) * [Experimental] Cluster Peering: * Add support for ACLs and TLS. [[GH-1343](https://github.com/hashicorp/consul-k8s/pull/1343)] [[GH-1366](https://github.com/hashicorp/consul-k8s/pull/1366)] * Add support for Load Balancers or external addresses in front of Consul servers for peering stream. diff --git a/acceptance/framework/cli/cli.go b/acceptance/framework/cli/cli.go new file mode 100644 index 0000000000..012ac349a5 --- /dev/null +++ b/acceptance/framework/cli/cli.go @@ -0,0 +1,31 @@ +package cli + +import ( + "fmt" + "os/exec" + + "github.com/hashicorp/consul-k8s/acceptance/framework/config" +) + +// CLI provides access to compile and execute commands with the `consul-k8s` CLI. +type CLI struct { + initialized bool +} + +// NewCLI compiles the `consul-k8s` CLI and returns a handle to execute commands +// with the binary. +func NewCLI() (*CLI, error) { + cmd := exec.Command("go", "install", ".") + cmd.Dir = config.CLIPath + _, err := cmd.Output() + return &CLI{true}, err +} + +// Run runs the CLI with the given args. +func (c *CLI) Run(args ...string) ([]byte, error) { + if !c.initialized { + return nil, fmt.Errorf("CLI must be initialized before calling Run, use `cli.NewCLI()` to initialize.") + } + cmd := exec.Command("cli", args...) + return cmd.Output() +} diff --git a/acceptance/framework/consul/cli_cluster.go b/acceptance/framework/consul/cli_cluster.go index a758e67459..cf7f1851de 100644 --- a/acceptance/framework/consul/cli_cluster.go +++ b/acceptance/framework/consul/cli_cluster.go @@ -3,7 +3,6 @@ package consul import ( "context" "fmt" - "os/exec" "strings" "testing" "time" @@ -11,6 +10,7 @@ import ( "github.com/gruntwork-io/terratest/modules/helm" terratestk8s "github.com/gruntwork-io/terratest/modules/k8s" terratestLogger "github.com/gruntwork-io/terratest/modules/logger" + "github.com/hashicorp/consul-k8s/acceptance/framework/cli" "github.com/hashicorp/consul-k8s/acceptance/framework/config" "github.com/hashicorp/consul-k8s/acceptance/framework/environment" "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" @@ -44,6 +44,7 @@ type CLICluster struct { noCleanupOnFailure bool debugDirectory string logger terratestLogger.TestLogger + cli cli.CLI } // NewCLICluster creates a new Consul cluster struct which can be used to create @@ -90,6 +91,9 @@ func NewCLICluster( Logger: logger, } + cli, err := cli.NewCLI() + require.NoError(t, err) + return &CLICluster{ ctx: ctx, helmOptions: hopts, @@ -103,6 +107,7 @@ func NewCLICluster( noCleanupOnFailure: cfg.NoCleanupOnFailure, debugDirectory: cfg.DebugDirectory, logger: logger, + cli: *cli, } } @@ -129,7 +134,7 @@ func (c *CLICluster) Create(t *testing.T) { args = append(args, "-timeout", "15m") args = append(args, "-auto-approve") - out, err := c.runCLI(args) + out, err := c.cli.Run(args...) if err != nil { c.logger.Logf(t, "error running command `consul-k8s %s`: %s", strings.Join(args, " "), err.Error()) c.logger.Logf(t, "command stdout: %s", string(out)) @@ -162,7 +167,7 @@ func (c *CLICluster) Upgrade(t *testing.T, helmValues map[string]string) { args = append(args, "-timeout", "15m") args = append(args, "-auto-approve") - out, err := c.runCLI(args) + out, err := c.cli.Run(args...) if err != nil { c.logger.Logf(t, "error running command `consul-k8s %s`: %s", strings.Join(args, " "), err.Error()) c.logger.Logf(t, "command stdout: %s", string(out)) @@ -186,7 +191,7 @@ func (c *CLICluster) Destroy(t *testing.T) { // Use `go run` so that the CLI is recompiled and therefore uses the local // charts directory rather than the directory from whenever it was last // compiled. - out, err := c.runCLI(args) + out, err := c.cli.Run(args...) if err != nil { c.logger.Logf(t, "error running command `consul-k8s %s`: %s", strings.Join(args, " "), err.Error()) c.logger.Logf(t, "command stdout: %s", string(out)) @@ -267,6 +272,10 @@ func (c *CLICluster) SetupConsulClient(t *testing.T, secure bool) (*api.Client, return consulClient, config.Address } +func (c *CLICluster) CLI() cli.CLI { + return c.cli +} + func createOrUpdateNamespace(t *testing.T, client kubernetes.Interface, namespace string) { _, err := client.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) if errors.IsNotFound(err) { @@ -296,12 +305,3 @@ func (c *CLICluster) setKube(args []string) []string { return args } - -// runCLI runs the CLI with the given args. -// Use `go run` so that the CLI is recompiled and therefore uses the local -// charts directory rather than the directory from whenever it was last compiled. -func (c *CLICluster) runCLI(args []string) ([]byte, error) { - cmd := exec.Command("go", append([]string{"run", "."}, args...)...) - cmd.Dir = config.CLIPath - return cmd.Output() -} diff --git a/acceptance/tests/connect/connect_inject_test.go b/acceptance/tests/connect/connect_inject_test.go index 73ae61c352..5cba67f045 100644 --- a/acceptance/tests/connect/connect_inject_test.go +++ b/acceptance/tests/connect/connect_inject_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/hashicorp/consul-k8s/acceptance/framework/cli" "github.com/hashicorp/consul-k8s/acceptance/framework/consul" "github.com/hashicorp/consul-k8s/acceptance/framework/helpers" "github.com/hashicorp/consul-k8s/acceptance/framework/k8s" @@ -18,6 +19,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ipv4RegEx = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + // TestConnectInject tests that Connect works in a default and a secure installation. func TestConnectInject(t *testing.T) { cases := map[string]struct { @@ -60,6 +63,9 @@ func TestConnectInject(t *testing.T) { for name, c := range cases { t.Run(name, func(t *testing.T) { + cli, err := cli.NewCLI() + require.NoError(t, err) + cfg := suite.Config() ctx := suite.Environment().DefaultContext(t) @@ -80,6 +86,40 @@ func TestConnectInject(t *testing.T) { connHelper.TestConnectionFailureWithoutIntention(t) connHelper.CreateIntention(t) } + + // Run proxy list and get the two results. + listOut, err := cli.Run("proxy", "list") + require.NoError(t, err) + logger.Log(t, string(listOut)) + list := translateListOutput(listOut) + require.Equal(t, 2, len(list)) + for _, proxyType := range list { + require.Equal(t, "Sidecar", proxyType) + } + + // Run proxy read and check that the connection is present in the output. + retrier := &retry.Timer{Timeout: 160 * time.Second, Wait: 2 * time.Second} + retry.RunWith(retrier, t, func(r *retry.R) { + for podName := range list { + out, err := cli.Run("proxy", "read", podName) + require.NoError(t, err) + + output := string(out) + logger.Log(t, output) + + // Both proxies must see their own local agent and app as clusters. + require.Regexp(r, "local_agent.*STATIC", output) + require.Regexp(r, "local_app.*STATIC", output) + + // Static Client must have Static Server as a cluster and endpoint. + if strings.Contains(podName, "static-client") { + require.Regexp(r, "static-server.*static-server\\.default\\.dc1\\.internal.*EDS", output) + require.Regexp(r, ipv4RegEx+".*static-server.default.dc1.internal", output) + } + + } + }) + connHelper.TestConnectionSuccess(t) connHelper.TestConnectionFailureWhenUnhealthy(t) }) @@ -428,3 +468,22 @@ func TestConnectInject_MultiportServices(t *testing.T) { }) } } + +// translateListOutput takes the raw output from the proxy list command and +// translates the table into a map. +func translateListOutput(raw []byte) map[string]string { + formatted := make(map[string]string) + for _, pod := range strings.Split(strings.TrimSpace(string(raw)), "\n")[3:] { + row := strings.Split(strings.TrimSpace(pod), "\t") + + var name string + if len(row) == 3 { // Handle the case where namespace is present + name = fmt.Sprintf("%s/%s", strings.TrimSpace(row[0]), strings.TrimSpace(row[1])) + } else if len(row) == 2 { + name = strings.TrimSpace(row[0]) + } + formatted[name] = row[len(row)-1] + } + + return formatted +} diff --git a/cli/cmd/install/install.go b/cli/cmd/install/install.go index 0255a9ad21..3329c6fc58 100644 --- a/cli/cmd/install/install.go +++ b/cli/cmd/install/install.go @@ -172,9 +172,6 @@ func (c *Command) init() { }) c.help = c.set.Help() - - // c.Init() calls the embedded BaseCommand's initialization function. - c.Init() } // Run installs Consul into a Kubernetes cluster. diff --git a/cli/cmd/proxy/command.go b/cli/cmd/proxy/command.go new file mode 100644 index 0000000000..663893de5e --- /dev/null +++ b/cli/cmd/proxy/command.go @@ -0,0 +1,26 @@ +package proxy + +import ( + "fmt" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/mitchellh/cli" +) + +// ProxyCommand provides a synopsis for the proxy subcommands (e.g. read). +type ProxyCommand struct { + *common.BaseCommand +} + +// Run prints out information about the subcommands. +func (c *ProxyCommand) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *ProxyCommand) Help() string { + return fmt.Sprintf("%s\n\nUsage: consul-k8s proxy ", c.Synopsis()) +} + +func (c *ProxyCommand) Synopsis() string { + return "Inspect Envoy proxies managed by Consul." +} diff --git a/cli/cmd/proxy/list/command.go b/cli/cmd/proxy/list/command.go new file mode 100644 index 0000000000..4389eec0db --- /dev/null +++ b/cli/cmd/proxy/list/command.go @@ -0,0 +1,228 @@ +package list + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + helmCLI "helm.sh/helm/v3/pkg/cli" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// ListCommand is the command struct for the proxy list command. +type ListCommand struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + namespace string + + set *flag.Sets + + flagNamespace string + flagAllNamespaces bool + + flagKubeConfig string + flagKubeContext string + + once sync.Once + help string +} + +// init sets up flags and help text for the command. +func (c *ListCommand) init() { + c.set = flag.NewSets() + + f := c.set.NewSet("Command Options") + f.StringVar(&flag.StringVar{ + Name: "namespace", + Target: &c.flagNamespace, + Usage: "The namespace to list proxies in.", + Aliases: []string{"n"}, + }) + f.BoolVar(&flag.BoolVar{ + Name: "all-namespaces", + Target: &c.flagAllNamespaces, + Default: false, + Usage: "List pods in all namespaces.", + Aliases: []string{"A"}, + }) + + f = c.set.NewSet("Global Options") + f.StringVar(&flag.StringVar{ + Name: "kubeconfig", + Aliases: []string{"c"}, + Target: &c.flagKubeConfig, + Default: "", + Usage: "Set the path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: "context", + Target: &c.flagKubeContext, + Default: "", + Usage: "Set the Kubernetes context to use.", + }) + + c.help = c.set.Help() +} + +// Run executes the list command. +func (c *ListCommand) Run(args []string) int { + c.once.Do(c.init) + c.Log.ResetNamed("list") + defer common.CloseWithError(c.BaseCommand) + + // Parse the command line flags. + if err := c.set.Parse(args); err != nil { + c.UI.Output("Error parsing arguments: %v", err.Error(), terminal.WithErrorStyle()) + return 1 + } + + // Validate the command line flags. + if err := c.validateFlags(); err != nil { + c.UI.Output("Invalid argument: %v", err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if c.kubernetes == nil { + if err := c.initKubernetes(); err != nil { + c.UI.Output("Error initializing Kubernetes client", err.Error(), terminal.WithErrorStyle()) + return 1 + } + } + + pods, err := c.fetchPods() + if err != nil { + c.UI.Output("Error fetching pods:", err.Error(), terminal.WithErrorStyle()) + return 1 + } + + c.output(pods) + return 0 +} + +// Help returns a description of the command and how it is used. +func (c *ListCommand) Help() string { + c.once.Do(c.init) + return fmt.Sprintf("%s\n\nUsage: consul-k8s proxy list [flags]\n\n%s", c.Synopsis(), c.help) +} + +// Synopsis returns a one-line command summary. +func (c *ListCommand) Synopsis() string { + return "List all Pods running proxies managed by Consul." +} + +// validateFlags ensures that the flags passed in by the can be used. +func (c *ListCommand) validateFlags() error { + if len(c.set.Args()) > 0 { + return errors.New("should have no non-flag arguments") + } + if errs := validation.ValidateNamespaceName(c.flagNamespace, false); c.flagNamespace != "" && len(errs) > 0 { + return fmt.Errorf("invalid namespace name passed for -namespace/-n: %v", strings.Join(errs, "; ")) + } + return nil +} + +// initKubernetes initializes the Kubernetes client and sets the namespace based +// on the user-provided arguments. +func (c *ListCommand) initKubernetes() error { + settings := helmCLI.New() + + restConfig, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + return fmt.Errorf("error retrieving Kubernetes authentication %v", err) + } + if c.kubernetes, err = kubernetes.NewForConfig(restConfig); err != nil { + return fmt.Errorf("error creating Kubernetes client %v", err) + } + + if c.flagAllNamespaces { + c.namespace = "" // An empty namespace means all namespaces. + } else if c.flagNamespace != "" { + c.namespace = c.flagNamespace + } else { + c.namespace = settings.Namespace() + } + + return err +} + +// fetchPods fetches all pods in flagNamespace which run Consul proxies. +func (c *ListCommand) fetchPods() ([]v1.Pod, error) { + var pods []v1.Pod + + // Fetch all pods in the namespace with labels matching the gateway component names. + gatewaypods, err := c.kubernetes.CoreV1().Pods(c.namespace).List(c.Ctx, metav1.ListOptions{ + LabelSelector: "component in (ingress-gateway, mesh-gateway, terminating-gateway), chart=consul-helm", + }) + if err != nil { + return nil, err + } + pods = append(pods, gatewaypods.Items...) + + // Fetch all pods in the namespace with a label indicating they are an API gateway. + apigatewaypods, err := c.kubernetes.CoreV1().Pods(c.namespace).List(c.Ctx, metav1.ListOptions{ + LabelSelector: "api-gateway.consul.hashicorp.com/managed=true", + }) + if err != nil { + return nil, err + } + pods = append(pods, apigatewaypods.Items...) + + // Fetch all pods in the namespace with a label indicating they are a service networked by Consul. + sidecarpods, err := c.kubernetes.CoreV1().Pods(c.namespace).List(c.Ctx, metav1.ListOptions{ + LabelSelector: "consul.hashicorp.com/connect-inject-status=injected", + }) + if err != nil { + return nil, err + } + pods = append(pods, sidecarpods.Items...) + + return pods, nil +} + +// output prints a table of pods to the terminal. +func (c *ListCommand) output(pods []v1.Pod) { + if c.flagAllNamespaces { + c.UI.Output("Namespace: All Namespaces\n") + } else if c.namespace != "" { + c.UI.Output("Namespace: %s\n", c.namespace) + } + + var tbl *terminal.Table + if c.flagAllNamespaces { + tbl = terminal.NewTable("Namespace", "Name", "Type") + } else { + tbl = terminal.NewTable("Name", "Type") + } + + for _, pod := range pods { + var proxyType string + switch pod.Labels["component"] { + case "ingress-gateway": + proxyType = "Ingress Gateway" + case "mesh-gateway": + proxyType = "Mesh Gateway" + case "terminating-gateway": + proxyType = "Terminating Gateway" + case "api-gateway": + proxyType = "API Gateway" + default: + proxyType = "Sidecar" + } + + if c.flagAllNamespaces { + tbl.AddRow([]string{pod.Namespace, pod.Name, proxyType}, []string{}) + } else { + tbl.AddRow([]string{pod.Name, proxyType}, []string{}) + } + } + + c.UI.Table(tbl) +} diff --git a/cli/cmd/proxy/list/command_test.go b/cli/cmd/proxy/list/command_test.go new file mode 100644 index 0000000000..f0d552ca7f --- /dev/null +++ b/cli/cmd/proxy/list/command_test.go @@ -0,0 +1,329 @@ +package list + +import ( + "bytes" + "context" + "io" + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestFlagParsing(t *testing.T) { + cases := map[string]struct { + args []string + out int + }{ + "No args": { + args: []string{}, + out: 0, + }, + "Nonexistent flag passed, -foo bar": { + args: []string{"-foo", "bar"}, + out: 1, + }, + "Invalid argument passed, -namespace YOLO": { + args: []string{"-namespace", "YOLO"}, + out: 1, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := setupCommand(new(bytes.Buffer)) + c.kubernetes = fake.NewSimpleClientset() + out := c.Run(tc.args) + require.Equal(t, tc.out, out) + }) + } +} + +func TestFetchPods(t *testing.T) { + cases := map[string]struct { + namespace string + pods []v1.Pod + expectedPods int + }{ + "No pods": { + namespace: "default", + pods: []v1.Pod{}, + expectedPods: 0, + }, + "Gateway pods": { + namespace: "default", + pods: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-gateway", + Namespace: "default", + Labels: map[string]string{ + "component": "ingress-gateway", + "chart": "consul-helm", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "mesh-gateway", + Namespace: "default", + Labels: map[string]string{ + "component": "mesh-gateway", + "chart": "consul-helm", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "terminating-gateway", + Namespace: "default", + Labels: map[string]string{ + "component": "terminating-gateway", + "chart": "consul-helm", + }, + }, + }, + }, + expectedPods: 3, + }, + "API Gateway Pods": { + namespace: "default", + pods: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "api-gateway", + Namespace: "default", + Labels: map[string]string{ + "api-gateway.consul.hashicorp.com/managed": "true", + }, + }, + }, + }, + expectedPods: 1, + }, + "Sidecar Pods": { + namespace: "default", + pods: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{ + "consul.hashicorp.com/connect-inject-status": "injected", + }, + }, + }, + }, + expectedPods: 1, + }, + "All kinds of Pods": { + namespace: "default", + pods: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{ + "consul.hashicorp.com/connect-inject-status": "injected", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "mesh-gateway", + Namespace: "default", + Labels: map[string]string{ + "component": "mesh-gateway", + "chart": "consul-helm", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "api-gateway", + Namespace: "default", + Labels: map[string]string{ + "api-gateway.consul.hashicorp.com/managed": "true", + }, + }, + }, + }, + expectedPods: 3, + }, + "Pods in multiple namespaces": { + namespace: "", + pods: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "api-gateway", + Namespace: "consul", + Labels: map[string]string{ + "api-gateway.consul.hashicorp.com/managed": "true", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{ + "consul.hashicorp.com/connect-inject-status": "injected", + }, + }, + }, + }, + expectedPods: 2, + }, + "Pods which should not be fetched": { + namespace: "default", + pods: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "dont-fetch", + Namespace: "default", + Labels: map[string]string{}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{ + "consul.hashicorp.com/connect-inject-status": "injected", + }, + }, + }, + }, + expectedPods: 1, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := setupCommand(new(bytes.Buffer)) + c.kubernetes = fake.NewSimpleClientset(&v1.PodList{Items: tc.pods}) + c.flagNamespace = tc.namespace + + pods, err := c.fetchPods() + + require.NoError(t, err) + require.Equal(t, tc.expectedPods, len(pods)) + }) + } +} + +func TestListCommandOutput(t *testing.T) { + // These regular expressions must be present in the output. + expected := []string{ + "Namespace.*Name.*Type", + "consul.*mesh-gateway.*Mesh Gateway", + "consul.*terminating-gateway.*Terminating Gateway", + "default.*ingress-gateway.*Ingress Gateway", + "consul.*api-gateway.*Sidecar", + "default.*pod1.*Sidecar", + } + notExpected := []string{ + "default.*dont-fetch.*Sidecar", + } + + pods := []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-gateway", + Namespace: "default", + Labels: map[string]string{ + "component": "ingress-gateway", + "chart": "consul-helm", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "mesh-gateway", + Namespace: "consul", + Labels: map[string]string{ + "component": "mesh-gateway", + "chart": "consul-helm", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "terminating-gateway", + Namespace: "consul", + Labels: map[string]string{ + "component": "terminating-gateway", + "chart": "consul-helm", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "api-gateway", + Namespace: "consul", + Labels: map[string]string{ + "api-gateway.consul.hashicorp.com/managed": "true", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "dont-fetch", + Namespace: "default", + Labels: map[string]string{}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{ + "consul.hashicorp.com/connect-inject-status": "injected", + }, + }, + }, + } + client := fake.NewSimpleClientset(&v1.PodList{Items: pods}) + + buf := new(bytes.Buffer) + c := setupCommand(buf) + c.kubernetes = client + + out := c.Run([]string{"-A"}) + require.Equal(t, 0, out) + + actual := buf.String() + + for _, expression := range expected { + require.Regexp(t, expression, actual) + } + for _, expression := range notExpected { + require.NotRegexp(t, expression, actual) + } +} + +func setupCommand(buf io.Writer) *ListCommand { + // Log at a test level to standard out. + log := hclog.New(&hclog.LoggerOptions{ + Name: "test", + Level: hclog.Debug, + Output: os.Stdout, + }) + + // Setup and initialize the command struct + command := &ListCommand{ + BaseCommand: &common.BaseCommand{ + Log: log, + UI: terminal.NewUI(context.Background(), buf), + }, + } + command.init() + + return command +} diff --git a/cli/cmd/proxy/read/command.go b/cli/cmd/proxy/read/command.go new file mode 100644 index 0000000000..227adbfee9 --- /dev/null +++ b/cli/cmd/proxy/read/command.go @@ -0,0 +1,464 @@ +package read + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + helmCLI "helm.sh/helm/v3/pkg/cli" + "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/utils/strings/slices" +) + +// defaultAdminPort is the port where the Envoy admin API is exposed. +const defaultAdminPort int = 19000 + +const ( + Table = "table" + JSON = "json" + Raw = "raw" +) + +type ReadCommand struct { + *common.BaseCommand + + kubernetes kubernetes.Interface + + set *flag.Sets + + // Command Flags + flagNamespace string + flagPodName string + flagOutput string + + // Output Filtering Opts + flagClusters bool + flagListeners bool + flagRoutes bool + flagEndpoints bool + flagSecrets bool + flagFQDN string + flagAddress string + flagPort int + + // Global Flags + flagKubeConfig string + flagKubeContext string + + fetchConfig func(context.Context, common.PortForwarder) (*EnvoyConfig, error) + + restConfig *rest.Config + + once sync.Once + help string +} + +func (c *ReadCommand) init() { + if c.fetchConfig == nil { + c.fetchConfig = FetchConfig + } + + c.set = flag.NewSets() + f := c.set.NewSet("Command Options") + f.StringVar(&flag.StringVar{ + Name: "namespace", + Target: &c.flagNamespace, + Usage: "The namespace where the target Pod can be found.", + Aliases: []string{"n"}, + }) + f.StringVar(&flag.StringVar{ + Name: "output", + Target: &c.flagOutput, + Usage: "Output the Envoy configuration as 'table', 'json', or 'raw'.", + Default: Table, + Aliases: []string{"o"}, + }) + + f = c.set.NewSet("Output Filtering Options") + f.BoolVar(&flag.BoolVar{ + Name: "clusters", + Target: &c.flagClusters, + Usage: "Filter output to only show clusters.", + }) + f.BoolVar(&flag.BoolVar{ + Name: "listeners", + Target: &c.flagListeners, + Usage: "Filter output to only show listeners.", + }) + f.BoolVar(&flag.BoolVar{ + Name: "routes", + Target: &c.flagRoutes, + Usage: "Filter output to only show routes.", + }) + f.BoolVar(&flag.BoolVar{ + Name: "endpoints", + Target: &c.flagEndpoints, + Usage: "Filter output to only show endpoints.", + }) + f.BoolVar(&flag.BoolVar{ + Name: "secrets", + Target: &c.flagSecrets, + Usage: "Filter output to only show secrets.", + }) + f.StringVar(&flag.StringVar{ + Name: "fqdn", + Target: &c.flagFQDN, + Usage: "Filter cluster output to clusters with a fully qualified domain name which contains the given value. May be combined with -address and -port.", + }) + f.StringVar(&flag.StringVar{ + Name: "address", + Target: &c.flagAddress, + Usage: "Filter clusters, endpoints, and listeners output to those with addresses which contain the given value. May be combined with -fqdn and -port", + }) + f.IntVar(&flag.IntVar{ + Name: "port", + Target: &c.flagPort, + Usage: "Filter endpoints and listeners output to addresses with the given port number. May be combined with -fqdn and -address.", + Default: -1, + }) + + f = c.set.NewSet("GlobalOptions") + f.StringVar(&flag.StringVar{ + Name: "kubeconfig", + Aliases: []string{"c"}, + Target: &c.flagKubeConfig, + Usage: "Set the path to kubeconfig file.", + }) + f.StringVar(&flag.StringVar{ + Name: "context", + Target: &c.flagKubeContext, + Usage: "Set the Kubernetes context to use.", + }) + + c.help = c.set.Help() +} + +func (c *ReadCommand) Run(args []string) int { + c.once.Do(c.init) + c.Log.ResetNamed("read") + defer common.CloseWithError(c.BaseCommand) + + if err := c.parseFlags(args); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + c.UI.Output("\n" + c.Help()) + return 1 + } + + if err := c.validateFlags(); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + c.UI.Output("\n" + c.Help()) + return 1 + } + + if err := c.initKubernetes(); err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + adminPorts, err := c.fetchAdminPorts() + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + configs, err := c.fetchConfigs(adminPorts) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + err = c.outputConfigs(configs) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + return 0 +} + +func (c *ReadCommand) Help() string { + c.once.Do(c.init) + return fmt.Sprintf("%s\n\nUsage: consul-k8s proxy read [flags]\n\n%s", c.Synopsis(), c.help) +} + +func (c *ReadCommand) Synopsis() string { + return "Inspect the Envoy configuration for a given Pod." +} + +func (c *ReadCommand) parseFlags(args []string) error { + // Separate positional arguments from keyed arguments. + positional := []string{} + for _, arg := range args { + if strings.HasPrefix(arg, "-") { + break + } + positional = append(positional, arg) + } + keyed := args[len(positional):] + + if len(positional) != 1 { + return fmt.Errorf("Exactly one positional argument is required: ") + } + c.flagPodName = positional[0] + + if err := c.set.Parse(keyed); err != nil { + return err + } + + return nil +} + +func (c *ReadCommand) validateFlags() error { + if errs := validation.ValidateNamespaceName(c.flagNamespace, false); c.flagNamespace != "" && len(errs) > 0 { + return fmt.Errorf("invalid namespace name passed for -namespace/-n: %v", strings.Join(errs, "; ")) + } + if outputs := []string{Table, JSON, Raw}; !slices.Contains(outputs, c.flagOutput) { + return fmt.Errorf("-output must be one of %s.", strings.Join(outputs, ", ")) + } + return nil +} + +func (c *ReadCommand) initKubernetes() (err error) { + settings := helmCLI.New() + + if c.flagKubeConfig == "" { + settings.KubeConfig = c.flagKubeConfig + } + + if c.flagKubeContext == "" { + settings.KubeContext = c.flagKubeContext + } + + if c.restConfig == nil { + if c.restConfig, err = settings.RESTClientGetter().ToRESTConfig(); err != nil { + return fmt.Errorf("error creating Kubernetes REST config %v", err) + } + } + + if c.kubernetes == nil { + if c.kubernetes, err = kubernetes.NewForConfig(c.restConfig); err != nil { + return fmt.Errorf("error creating Kubernetes client %v", err) + } + } + + if c.flagNamespace == "" { + c.flagNamespace = settings.Namespace() + } + + return nil +} + +func (c *ReadCommand) fetchAdminPorts() (map[string]int, error) { + adminPorts := make(map[string]int, 0) + + pod, err := c.kubernetes.CoreV1().Pods(c.flagNamespace).Get(c.Ctx, c.flagPodName, metav1.GetOptions{}) + if err != nil { + return adminPorts, err + } + + connectService, isMultiport := pod.Annotations["consul.hashicorp.com/connect-service"] + + if !isMultiport { + // Return the default port configuration. + adminPorts[c.flagPodName] = defaultAdminPort + return adminPorts, nil + } + + for index, service := range strings.Split(connectService, ",") { + adminPorts[service] = defaultAdminPort + index + } + + return adminPorts, nil +} + +func (c *ReadCommand) fetchConfigs(adminPorts map[string]int) (map[string]*EnvoyConfig, error) { + configs := make(map[string]*EnvoyConfig, 0) + + for name, adminPort := range adminPorts { + pf := common.PortForward{ + Namespace: c.flagNamespace, + PodName: c.flagPodName, + RemotePort: adminPort, + KubeClient: c.kubernetes, + RestConfig: c.restConfig, + } + + config, err := c.fetchConfig(c.Ctx, &pf) + if err != nil { + return configs, err + } + + configs[name] = config + } + + return configs, nil +} + +func (c *ReadCommand) outputConfigs(configs map[string]*EnvoyConfig) error { + switch c.flagOutput { + case Table: + return c.outputTables(configs) + case JSON: + return c.outputJSON(configs) + case Raw: + return c.outputRaw(configs) + } + + return nil +} + +// shouldPrintTable takes the flag passed in for that table. If the flag is true, +// the table should always be printed. Otherwise, it should only be printed if +// no other table filtering flags are passed in. +func (c *ReadCommand) shouldPrintTable(table bool) bool { + if table { + return table + } + + // True if no other table filtering flags are passed in. + return !(c.flagClusters || c.flagEndpoints || c.flagListeners || c.flagRoutes || c.flagSecrets) +} + +func (c *ReadCommand) outputTables(configs map[string]*EnvoyConfig) error { + if c.flagFQDN != "" || c.flagAddress != "" || c.flagPort != -1 { + c.UI.Output("Filters applied", terminal.WithHeaderStyle()) + + if c.flagFQDN != "" { + c.UI.Output(fmt.Sprintf("Fully qualified domain names containing: %s", c.flagFQDN), terminal.WithInfoStyle()) + } + if c.flagAddress != "" { + c.UI.Output(fmt.Sprintf("Endpoint addresses containing: %s", c.flagAddress), terminal.WithInfoStyle()) + } + if c.flagPort != -1 { + c.UI.Output(fmt.Sprintf("Endpoint addresses with port number: %d", c.flagPort), terminal.WithInfoStyle()) + } + + c.UI.Output("") + } + + for name, config := range configs { + c.UI.Output(fmt.Sprintf("Envoy configuration for %s in namespace %s:", name, c.flagNamespace)) + + c.outputClustersTable(FilterClusters(config.Clusters, c.flagFQDN, c.flagAddress, c.flagPort)) + c.outputEndpointsTable(FilterEndpoints(config.Endpoints, c.flagAddress, c.flagPort)) + c.outputListenersTable(FilterListeners(config.Listeners, c.flagAddress, c.flagPort)) + c.outputRoutesTable(config.Routes) + c.outputSecretsTable(config.Secrets) + c.UI.Output("\n") + } + + return nil +} + +func (c *ReadCommand) outputJSON(configs map[string]*EnvoyConfig) error { + cfgs := make(map[string]interface{}) + for name, config := range configs { + cfg := make(map[string]interface{}) + if c.shouldPrintTable(c.flagClusters) { + cfg["clusters"] = FilterClusters(config.Clusters, c.flagFQDN, c.flagAddress, c.flagPort) + } + if c.shouldPrintTable(c.flagEndpoints) { + cfg["endpoints"] = FilterEndpoints(config.Endpoints, c.flagAddress, c.flagPort) + } + if c.shouldPrintTable(c.flagListeners) { + cfg["listeners"] = FilterListeners(config.Listeners, c.flagAddress, c.flagPort) + } + if c.shouldPrintTable(c.flagRoutes) { + cfg["routes"] = config.Routes + } + if c.shouldPrintTable(c.flagSecrets) { + cfg["secrets"] = config.Secrets + } + + cfgs[name] = cfg + } + + out, err := json.MarshalIndent(cfgs, "", "\t") + if err != nil { + return err + } + + c.UI.Output(string(out)) + + return nil +} + +func (c *ReadCommand) outputRaw(configs map[string]*EnvoyConfig) error { + cfgs := make(map[string]interface{}, 0) + for name, config := range configs { + var cfg interface{} + if err := json.Unmarshal(config.rawCfg, &cfg); err != nil { + return err + } + + cfgs[name] = cfg + } + + out, err := json.MarshalIndent(cfgs, "", "\t") + if err != nil { + return err + } + + c.UI.Output(string(out)) + + return nil +} + +func (c *ReadCommand) outputClustersTable(clusters []Cluster) { + if !c.shouldPrintTable(c.flagClusters) { + return + } + + c.UI.Output(fmt.Sprintf("Clusters (%d)", len(clusters)), terminal.WithHeaderStyle()) + table := terminal.NewTable("Name", "FQDN", "Endpoints", "Type", "Last Updated") + for _, cluster := range clusters { + table.AddRow([]string{cluster.Name, cluster.FullyQualifiedDomainName, strings.Join(cluster.Endpoints, ", "), + cluster.Type, cluster.LastUpdated}, []string{}) + } + c.UI.Table(table) + c.UI.Output("") +} + +func (c *ReadCommand) outputEndpointsTable(endpoints []Endpoint) { + if !c.shouldPrintTable(c.flagEndpoints) { + return + } + + c.UI.Output(fmt.Sprintf("Endpoints (%d)", len(endpoints)), terminal.WithHeaderStyle()) + c.UI.Table(formatEndpoints(endpoints)) +} + +func (c *ReadCommand) outputListenersTable(listeners []Listener) { + if !c.shouldPrintTable(c.flagListeners) { + return + } + + c.UI.Output(fmt.Sprintf("Listeners (%d)", len(listeners)), terminal.WithHeaderStyle()) + c.UI.Table(formatListeners(listeners)) +} + +func (c *ReadCommand) outputRoutesTable(routes []Route) { + if !c.shouldPrintTable(c.flagRoutes) { + return + } + + c.UI.Output(fmt.Sprintf("Routes (%d)", len(routes)), terminal.WithHeaderStyle()) + c.UI.Table(formatRoutes(routes)) +} + +func (c *ReadCommand) outputSecretsTable(secrets []Secret) { + if !c.shouldPrintTable(c.flagSecrets) { + return + } + + c.UI.Output(fmt.Sprintf("Secrets (%d)", len(secrets)), terminal.WithHeaderStyle()) + c.UI.Table(formatSecrets(secrets)) +} diff --git a/cli/cmd/proxy/read/command_test.go b/cli/cmd/proxy/read/command_test.go new file mode 100644 index 0000000000..5295484be8 --- /dev/null +++ b/cli/cmd/proxy/read/command_test.go @@ -0,0 +1,167 @@ +package read + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestFlagParsing(t *testing.T) { + cases := map[string]struct { + args []string + out int + }{ + "No args": { + args: []string{}, + out: 1, + }, + "Multiple podnames passed": { + args: []string{"podname", "podname2"}, + out: 1, + }, + "Nonexistent flag passed, -foo bar": { + args: []string{"podName", "-foo", "bar"}, + out: 1, + }, + "Invalid argument passed, -namespace YOLO": { + args: []string{"podName", "-namespace", "YOLO"}, + out: 1, + }, + "User passed incorrect output": { + args: []string{"podName", "-output", "image"}, + out: 1, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + c := setupCommand(new(bytes.Buffer)) + c.kubernetes = fake.NewSimpleClientset() + + out := c.Run(tc.args) + require.Equal(t, tc.out, out) + }) + } +} + +func TestReadCommandOutput(t *testing.T) { + podName := "fakePod" + + // These regular expressions must be present in the output. + expectedHeader := fmt.Sprintf("Envoy configuration for %s in namespace default:", podName) + expected := map[string][]string{ + "-clusters": {"==> Clusters \\(6\\)", + "Name.*FQDN.*Endpoints.*Type.*Last Updated", + "local_agent.*local_agent.*192\\.168\\.79\\.187:8502.*STATIC.*2022-05-13T04:22:39\\.553Z", + "local_app.*local_app.*127\\.0\\.0\\.1:8080.*STATIC.*2022-05-13T04:22:39\\.655Z", + "client.*client\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul.*EDS.*2022-06-09T00:39:12\\.948Z", + "frontend.*frontend\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul.*EDS.*2022-06-09T00:39:12\\.855Z", + "original-destination.*original-destination.*ORIGINAL_DST.*2022-05-13T04:22:39.743Z", + "server.*server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul.*EDS.*2022-06-09T00:39:12\\.754Z"}, + + "-endpoints": {"==> Endpoints \\(9\\)", + "Address:Port.*Cluster.*Weight.*Status", + "192.168.79.187:8502.*local_agent.*1.00.*HEALTHY", + "127.0.0.1:8080.*local_app.*1.00.*HEALTHY", + "192.168.31.201:20000.*1.00.*HEALTHY", + "192.168.47.235:20000.*1.00.*HEALTHY", + "192.168.71.254:20000.*1.00.*HEALTHY", + "192.168.63.120:20000.*1.00.*HEALTHY", + "192.168.18.110:20000.*1.00.*HEALTHY", + "192.168.52.101:20000.*1.00.*HEALTHY", + "192.168.65.131:20000.*1.00.*HEALTHY"}, + + "-listeners": {"==> Listeners \\(2\\)", + "Name.*Address:Port.*Direction.*Filter Chain Match.*Filters.*Last Updated", + "public_listener.*192\\.168\\.69\\.179:20000.*INBOUND.*Any.*\\* to local_app/.*2022-06-09T00:39:27\\.668Z", + "outbound_listener.*127.0.0.1:15001.*OUTBOUND.*10\\.100\\.134\\.173/32, 240\\.0\\.0\\.3/32.*to client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul.*2022-05-24T17:41:59\\.079Z", + "10\\.100\\.254\\.176/32, 240\\.0\\.0\\.4/32.*\\* to server\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul/", + "10\\.100\\.31\\.2/32, 240\\.0\\.0\\.2/32.*to frontend\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul", + "Any.*to original-destination"}, + + "-routes": {"==> Routes \\(2\\)", + "Name.*Destination Cluster.*Last Updated", + "public_listener.*local_app/.*2022-06-09T00:39:27.667Z", + "server.*server\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul/.*2022-05-24T17:41:59\\.078Z"}, + + "-secrets": {"==> Secrets \\(2\\)", + "Name.*Type.*Last Updated", + "default.*Dynamic Active.*2022-05-24T17:41:59.078Z", + "ROOTCA.*Dynamic Warming.*2022-03-15T05:14:22.868Z"}, + } + + cases := map[string][]string{ + "No filters": {}, + "Clusters": {"-clusters"}, + "Endpoints": {"-endpoints"}, + "Listeners": {"-listeners"}, + "Routes": {"-routes"}, + "Secrets": {"-secrets"}, + "Clusters and routes": {"-clusters", "-routes"}, + "Secrets then listeners": {"-secrets", "-listeners"}, + } + + fakePod := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: "default", + }, + } + + buf := new(bytes.Buffer) + c := setupCommand(buf) + c.kubernetes = fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{fakePod}}) + + // A fetchConfig function that just returns the test Envoy config. + c.fetchConfig = func(context.Context, common.PortForwarder) (*EnvoyConfig, error) { + return testEnvoyConfig, nil + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + args := append([]string{podName}, tc...) + out := c.Run(args) + require.Equal(t, 0, out) + + actual := buf.String() + + require.Regexp(t, expectedHeader, actual) + for _, table := range tc { + for _, expression := range expected[table] { + require.Regexp(t, expression, actual) + } + } + }) + } +} + +func setupCommand(buf io.Writer) *ReadCommand { + // Log at a test level to standard out. + log := hclog.New(&hclog.LoggerOptions{ + Name: "test", + Level: hclog.Debug, + Output: os.Stdout, + }) + + // Setup and initialize the command struct + command := &ReadCommand{ + BaseCommand: &common.BaseCommand{ + Log: log, + UI: terminal.NewUI(context.Background(), buf), + }, + } + command.init() + + return command +} diff --git a/cli/cmd/proxy/read/config.go b/cli/cmd/proxy/read/config.go new file mode 100644 index 0000000000..cc3fa8d7e5 --- /dev/null +++ b/cli/cmd/proxy/read/config.go @@ -0,0 +1,440 @@ +package read + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/hashicorp/consul-k8s/cli/common" +) + +// EnvoyConfig represents the configuration retrieved from a config dump at the +// admin endpoint. It wraps the Envoy ConfigDump struct to give us convenient +// access to the different sections of the config. +type EnvoyConfig struct { + rawCfg []byte + Clusters []Cluster + Endpoints []Endpoint + Listeners []Listener + Routes []Route + Secrets []Secret +} + +// Cluster represents a cluster in the Envoy config. +type Cluster struct { + Name string + FullyQualifiedDomainName string + Endpoints []string + Type string + LastUpdated string +} + +// Endpoint represents an endpoint in the Envoy config. +type Endpoint struct { + Address string + Cluster string + Weight float64 + Status string +} + +// Listener represents a listener in the Envoy config. +type Listener struct { + Name string + Address string + FilterChain []FilterChain + Direction string + LastUpdated string +} + +type FilterChain struct { + Filters []string + FilterChainMatch string +} + +// Route represents a route in the Envoy config. +type Route struct { + Name string + DestinationCluster string + LastUpdated string +} + +// Secret represents a secret in the Envoy config. +type Secret struct { + Name string + Type string + LastUpdated string +} + +// FetchConfig opens a port forward to the Envoy admin API and fetches the +// configuration from the config dump endpoint. +func FetchConfig(ctx context.Context, portForward common.PortForwarder) (*EnvoyConfig, error) { + endpoint, err := portForward.Open(ctx) + if err != nil { + return nil, err + } + defer portForward.Close() + + // Fetch the config dump + response, err := http.Get(fmt.Sprintf("http://%s/config_dump?include_eds", endpoint)) + if err != nil { + return nil, err + } + configDump, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + if err := response.Body.Close(); err != nil { + return nil, err + } + + // Fetch the clusters mapping + response, err = http.Get(fmt.Sprintf("http://%s/clusters?format=json", endpoint)) + if err != nil { + return nil, err + } + clusters, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + if err := response.Body.Close(); err != nil { + return nil, err + } + + config := fmt.Sprintf("{\n\"config_dump\":%s,\n\"clusters\":%s}", string(configDump), string(clusters)) + + envoyConfig := &EnvoyConfig{} + err = json.Unmarshal([]byte(config), envoyConfig) + if err != nil { + return nil, err + } + return envoyConfig, nil +} + +// JSON returns the original JSON Envoy config dump data which was used to create +// the Config object. +func (c *EnvoyConfig) JSON() []byte { + return c.rawCfg +} + +// UnmarshalJSON implements the json.Unmarshaler interface to unmarshal the raw +// config dump bytes into EnvoyConfig. It saves a copy of the original bytes +// which can be fetched with the JSON method. +func (c *EnvoyConfig) UnmarshalJSON(b []byte) error { + // Save the original config dump bytes for marshalling. We should treat this + // struct as immutable so this should be safe. + c.rawCfg = b + + var root root + err := json.Unmarshal(b, &root) + + clusterMapping, endpointMapping := make(map[string][]string), make(map[string]string) + for _, clusterStatus := range root.Clusters.ClusterStatuses { + var addresses []string + for _, status := range clusterStatus.HostStatuses { + address := fmt.Sprintf("%s:%d", status.Address.SocketAddress.Address, int(status.Address.SocketAddress.PortValue)) + addresses = append(addresses, address) + endpointMapping[address] = clusterStatus.Name + } + clusterMapping[clusterStatus.Name] = addresses + } + + // Dispatch each section to the appropriate parsing function by its type. + for _, config := range root.ConfigDump.Configs { + switch config["@type"] { + case "type.googleapis.com/envoy.admin.v3.ClustersConfigDump": + clusters, err := parseClusters(config, clusterMapping) + if err != nil { + return err + } + c.Clusters = clusters + case "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump": + endpoints, err := parseEndpoints(config, endpointMapping) + if err != nil { + return err + } + c.Endpoints = endpoints + case "type.googleapis.com/envoy.admin.v3.ListenersConfigDump": + listeners, err := parseListeners(config) + if err != nil { + return err + } + c.Listeners = listeners + case "type.googleapis.com/envoy.admin.v3.RoutesConfigDump": + routes, err := parseRoutes(config) + if err != nil { + return err + } + c.Routes = routes + case "type.googleapis.com/envoy.admin.v3.SecretsConfigDump": + secrets, err := parseSecrets(config) + if err != nil { + return err + } + c.Secrets = secrets + } + } + + return err +} + +func parseClusters(rawCfg map[string]interface{}, clusterMapping map[string][]string) ([]Cluster, error) { + clusters := make([]Cluster, 0) + + raw, err := json.Marshal(rawCfg) + if err != nil { + return clusters, err + } + + var clustersCD clustersConfigDump + if err = json.Unmarshal(raw, &clustersCD); err != nil { + return clusters, err + } + + for _, cluster := range append(clustersCD.StaticClusters, clustersCD.DynamicActiveClusters...) { + // Join nested endpoint data into a slice of strings. + endpoints := make([]string, 0) + for _, endpoint := range cluster.Cluster.LoadAssignment.Endpoints { + for _, lbEndpoint := range endpoint.LBEndpoints { + endpoints = append(endpoints, fmt.Sprintf("%s:%d", lbEndpoint.Endpoint.Address.SocketAddress.Address, + int(lbEndpoint.Endpoint.Address.SocketAddress.PortValue))) + } + } + + // Add addresses discovered by EDS if not already added + if addresses, ok := clusterMapping[cluster.Cluster.FQDN]; ok { + for _, endpoint := range addresses { + alreadyAdded := false + for _, existingEndpoint := range endpoints { + if existingEndpoint == endpoint { + alreadyAdded = true + break + } + } + if !alreadyAdded { + endpoints = append(endpoints, endpoint) + } + } + } + + clusters = append(clusters, Cluster{ + Name: strings.Split(cluster.Cluster.FQDN, ".")[0], + FullyQualifiedDomainName: cluster.Cluster.FQDN, + Endpoints: endpoints, + Type: cluster.Cluster.ClusterType, + LastUpdated: cluster.LastUpdated, + }) + } + + return clusters, nil +} + +func parseEndpoints(rawCfg map[string]interface{}, endpointMapping map[string]string) ([]Endpoint, error) { + endpoints := make([]Endpoint, 0) + + raw, err := json.Marshal(rawCfg) + if err != nil { + return endpoints, err + } + + var endpointsCD endpointsConfigDump + if err = json.Unmarshal(raw, &endpointsCD); err != nil { + return endpoints, err + } + + for _, endpointConfig := range append(endpointsCD.StaticEndpointConfigs, endpointsCD.DynamicEndpointConfigs...) { + for _, endpoint := range endpointConfig.EndpointConfig.Endpoints { + for _, lbEndpoint := range endpoint.LBEndpoints { + address := fmt.Sprintf("%s:%d", lbEndpoint.Endpoint.Address.SocketAddress.Address, int(lbEndpoint.Endpoint.Address.SocketAddress.PortValue)) + + cluster := endpointConfig.EndpointConfig.Name + // Fill in cluster from EDS endpoint mapping. + if edsCluster, ok := endpointMapping[address]; ok && cluster == "" { + cluster = edsCluster + } + + endpoints = append(endpoints, Endpoint{ + Address: address, + Cluster: cluster, + Weight: lbEndpoint.LoadBalancingWeight, + Status: lbEndpoint.HealthStatus, + }) + } + } + } + + return endpoints, nil +} + +func parseListeners(rawCfg map[string]interface{}) ([]Listener, error) { + listeners := make([]Listener, 0) + + raw, err := json.Marshal(rawCfg) + if err != nil { + return listeners, err + } + + var listenersCD listenersConfigDump + if err = json.Unmarshal(raw, &listenersCD); err != nil { + return listeners, err + } + + listenersConfig := []listenerConfig{} + for _, listener := range listenersCD.DynamicListeners { + listenersConfig = append(listenersConfig, listener.ActiveState) + } + listenersConfig = append(listenersConfig, listenersCD.StaticListeners...) + + for _, listener := range listenersConfig { + address := fmt.Sprintf("%s:%d", listener.Listener.Address.SocketAddress.Address, int(listener.Listener.Address.SocketAddress.PortValue)) + + // Format the filter chain configs into something more readable. + filterChain := []FilterChain{} + for _, chain := range listener.Listener.FilterChains { + filterChainMatch := []string{} + for _, prefixRange := range chain.FilterChainMatch.PrefixRanges { + filterChainMatch = append(filterChainMatch, fmt.Sprintf("%s/%d", prefixRange.AddressPrefix, int(prefixRange.PrefixLen))) + } + if len(filterChainMatch) == 0 { + filterChainMatch = append(filterChainMatch, "Any") + } + + filterChain = append(filterChain, FilterChain{ + FilterChainMatch: strings.Join(filterChainMatch, ", "), + Filters: formatFilters(chain), + }) + } + + direction := "UNSPECIFIED" + if listener.Listener.TrafficDirection != "" { + direction = listener.Listener.TrafficDirection + } + + listeners = append(listeners, Listener{ + Name: strings.Split(listener.Listener.Name, ":")[0], + Address: address, + FilterChain: filterChain, + Direction: direction, + LastUpdated: listener.LastUpdated, + }) + } + + return listeners, nil +} + +func parseRoutes(rawCfg map[string]interface{}) ([]Route, error) { + routes := make([]Route, 0) + + raw, err := json.Marshal(rawCfg) + if err != nil { + return routes, err + } + + var routesCD routesConfigDump + if err = json.Unmarshal(raw, &routesCD); err != nil { + return routes, err + } + + for _, route := range routesCD.StaticRouteConfigs { + destinationClusters := []string{} + for _, host := range route.RouteConfig.VirtualHosts { + for _, routeCfg := range host.Routes { + destinationClusters = append(destinationClusters, + fmt.Sprintf("%s%s", routeCfg.Route.Cluster, routeCfg.Match.Prefix)) + } + } + + routes = append(routes, Route{ + Name: route.RouteConfig.Name, + DestinationCluster: strings.Join(destinationClusters, ", "), + LastUpdated: route.LastUpdated, + }) + } + + return routes, nil +} + +func parseSecrets(rawCfg map[string]interface{}) ([]Secret, error) { + secrets := make([]Secret, 0) + + raw, err := json.Marshal(rawCfg) + if err != nil { + return secrets, err + } + + var secretsCD secretsConfigDump + if err = json.Unmarshal(raw, &secretsCD); err != nil { + return secrets, err + } + + for _, secret := range secretsCD.StaticSecrets { + secrets = append(secrets, Secret{ + Name: secret.Name, + Type: "Static", + LastUpdated: secret.LastUpdated, + }) + } + + for _, secret := range secretsCD.DynamicActiveSecrets { + secrets = append(secrets, Secret{ + Name: secret.Name, + Type: "Dynamic Active", + LastUpdated: secret.LastUpdated, + }) + } + + for _, secret := range secretsCD.DynamicWarmingSecrets { + secrets = append(secrets, Secret{ + Name: secret.Name, + Type: "Dynamic Warming", + LastUpdated: secret.LastUpdated, + }) + } + + return secrets, nil +} + +func formatFilters(filterChain filterChain) (filters []string) { + // Filters can have many custom configurations, each must be handled differently. + formatters := map[string]func(typedConfig) string{ + "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC": formatFilterRBAC, + "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy": formatFilterTCPProxy, + "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager": formatFilterHTTPConnectionManager, + } + + for _, chainFilter := range filterChain.Filters { + if formatter, ok := formatters[chainFilter.TypedConfig.Type]; ok { + filters = append(filters, formatter(chainFilter.TypedConfig)) + } + } + return +} + +func formatFilterTCPProxy(config typedConfig) (filter string) { + return "to " + config.Cluster +} + +func formatFilterRBAC(cfg typedConfig) (filter string) { + action := cfg.Rules.Action + for _, principal := range cfg.Rules.Policies.ConsulIntentions.Principals { + regex := principal.Authenticated.PrincipalName.SafeRegex.Regex + filter += fmt.Sprintf("%s %s", action, regex) + } + return +} + +func formatFilterHTTPConnectionManager(cfg typedConfig) (filter string) { + for _, host := range cfg.RouteConfig.VirtualHosts { + filter += strings.Join(host.Domains, ", ") + filter += " to " + + routes := "" + for _, route := range host.Routes { + routes += fmt.Sprintf("%s%s", route.Route.Cluster, route.Match.Prefix) + } + filter += routes + } + return +} diff --git a/cli/cmd/proxy/read/config_test.go b/cli/cmd/proxy/read/config_test.go new file mode 100644 index 0000000000..cce3c4c12a --- /dev/null +++ b/cli/cmd/proxy/read/config_test.go @@ -0,0 +1,261 @@ +package read + +import ( + "bytes" + "context" + "embed" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +//go:embed test_config_dump.json test_clusters.json +var fs embed.FS + +const ( + testConfigDump = "test_config_dump.json" + testClusters = "test_clusters.json" +) + +func TestUnmarshaling(t *testing.T) { + var envoyConfig EnvoyConfig + err := json.Unmarshal(rawEnvoyConfig(t), &envoyConfig) + require.NoError(t, err) + + require.Equal(t, testEnvoyConfig.Clusters, envoyConfig.Clusters) + require.Equal(t, testEnvoyConfig.Endpoints, envoyConfig.Endpoints) + require.Equal(t, testEnvoyConfig.Listeners, envoyConfig.Listeners) + require.Equal(t, testEnvoyConfig.Routes, envoyConfig.Routes) + require.Equal(t, testEnvoyConfig.Secrets, envoyConfig.Secrets) +} + +func TestJSON(t *testing.T) { + raw, err := fs.ReadFile(testConfigDump) + require.NoError(t, err) + expected := bytes.TrimSpace(raw) + + var envoyConfig EnvoyConfig + err = json.Unmarshal(raw, &envoyConfig) + require.NoError(t, err) + + actual := envoyConfig.JSON() + + require.Equal(t, expected, actual) +} + +func TestFetchConfig(t *testing.T) { + configDump, err := fs.ReadFile(testConfigDump) + require.NoError(t, err) + + clusters, err := fs.ReadFile(testClusters) + require.NoError(t, err) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/config_dump" { + w.Write(configDump) + } + if r.URL.Path == "/clusters" { + w.Write(clusters) + } + })) + defer mockServer.Close() + + mpf := &mockPortForwarder{ + openBehavior: func(ctx context.Context) (string, error) { + return strings.Replace(mockServer.URL, "http://", "", 1), nil + }, + } + + envoyConfig, err := FetchConfig(context.Background(), mpf) + + require.NoError(t, err) + + require.Equal(t, testEnvoyConfig.Clusters, envoyConfig.Clusters) + require.Equal(t, testEnvoyConfig.Endpoints, envoyConfig.Endpoints) + require.Equal(t, testEnvoyConfig.Listeners, envoyConfig.Listeners) + require.Equal(t, testEnvoyConfig.Routes, envoyConfig.Routes) + require.Equal(t, testEnvoyConfig.Secrets, envoyConfig.Secrets) +} + +type mockPortForwarder struct { + openBehavior func(context.Context) (string, error) +} + +func (m *mockPortForwarder) Open(ctx context.Context) (string, error) { return m.openBehavior(ctx) } +func (m *mockPortForwarder) Close() {} + +func rawEnvoyConfig(t *testing.T) []byte { + configDump, err := fs.ReadFile(testConfigDump) + require.NoError(t, err) + + clusters, err := fs.ReadFile(testClusters) + require.NoError(t, err) + + return []byte(fmt.Sprintf("{\n\"config_dump\":%s,\n\"clusters\":%s}", string(configDump), string(clusters))) +} + +// testEnvoyConfig is what we expect the config at `test_config_dump.json` to be. +var testEnvoyConfig = &EnvoyConfig{ + Clusters: []Cluster{ + { + Name: "local_agent", + FullyQualifiedDomainName: "local_agent", + Endpoints: []string{"192.168.79.187:8502", "172.18.0.2:8502"}, + Type: "STATIC", + LastUpdated: "2022-05-13T04:22:39.553Z", + }, + { + Name: "client", + FullyQualifiedDomainName: "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + Type: "EDS", + LastUpdated: "2022-06-09T00:39:12.948Z", + }, + { + Name: "frontend", + FullyQualifiedDomainName: "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + Type: "EDS", + LastUpdated: "2022-06-09T00:39:12.855Z", + }, + { + Name: "local_app", + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080", "127.0.0.1:0"}, + Type: "STATIC", + LastUpdated: "2022-05-13T04:22:39.655Z", + }, + { + Name: "original-destination", + FullyQualifiedDomainName: "original-destination", + Endpoints: []string{}, + Type: "ORIGINAL_DST", + LastUpdated: "2022-05-13T04:22:39.743Z", + }, + { + Name: "server", + FullyQualifiedDomainName: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + Type: "EDS", + LastUpdated: "2022-06-09T00:39:12.754Z", + }, + }, + Endpoints: []Endpoint{ + { + Address: "192.168.79.187:8502", + Cluster: "local_agent", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "127.0.0.1:8080", + Cluster: "local_app", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.31.201:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.47.235:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.71.254:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.63.120:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.18.110:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.52.101:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.65.131:20000", + Weight: 1, + Status: "HEALTHY", + }, + }, + Listeners: []Listener{ + { + Name: "public_listener", + Address: "192.168.69.179:20000", + FilterChain: []FilterChain{ + { + FilterChainMatch: "Any", + Filters: []string{"* to local_app/"}, + }, + }, + Direction: "INBOUND", + LastUpdated: "2022-06-09T00:39:27.668Z", + }, + { + Name: "outbound_listener", + Address: "127.0.0.1:15001", + FilterChain: []FilterChain{ + { + FilterChainMatch: "10.100.134.173/32, 240.0.0.3/32", + Filters: []string{"to client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul"}, + }, + { + FilterChainMatch: "10.100.254.176/32, 240.0.0.4/32", + Filters: []string{"* to server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/"}, + }, + { + FilterChainMatch: "10.100.31.2/32, 240.0.0.2/32", + Filters: []string{ + "to frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + }, + }, + { + FilterChainMatch: "Any", + Filters: []string{"to original-destination"}, + }, + }, + Direction: "OUTBOUND", + LastUpdated: "2022-05-24T17:41:59.079Z", + }, + }, + Routes: []Route{ + { + Name: "public_listener", + DestinationCluster: "local_app/", + LastUpdated: "2022-06-09T00:39:27.667Z", + }, + { + Name: "server", + DestinationCluster: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/", + LastUpdated: "2022-05-24T17:41:59.078Z", + }, + }, + Secrets: []Secret{ + { + Name: "default", + Type: "Dynamic Active", + LastUpdated: "2022-05-24T17:41:59.078Z", + }, + { + Name: "ROOTCA", + Type: "Dynamic Warming", + LastUpdated: "2022-03-15T05:14:22.868Z", + }, + }, +} diff --git a/cli/cmd/proxy/read/envoy_types.go b/cli/cmd/proxy/read/envoy_types.go new file mode 100644 index 0000000000..10f76101dd --- /dev/null +++ b/cli/cmd/proxy/read/envoy_types.go @@ -0,0 +1,269 @@ +package read + +/* Envoy Types +These types are based on the JSON returned from the Envoy Config Dump API on the +admin interface. They are a subset of what is returned from that API to support +unmarshaling in the ConfigDump struct. +Please refer to the Envoy config dump documentation when modifying or extending: +https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/config_dump.proto +*/ + +type root struct { + ConfigDump configDump `json:"config_dump"` + Clusters clusters `json:"clusters"` +} + +type configDump struct { + Configs []map[string]interface{} `json:"configs"` +} + +type clustersConfigDump struct { + ConfigType string `json:"@type"` + StaticClusters []clusterConfig `json:"static_clusters"` + DynamicActiveClusters []clusterConfig `json:"dynamic_active_clusters"` +} + +type clusterConfig struct { + Cluster clusterMeta `json:"cluster"` + LastUpdated string `json:"last_updated"` +} + +type clusterMeta struct { + FQDN string `json:"name"` + ClusterType string `json:"type"` + LoadAssignment loadAssignment `json:"load_assignment"` +} + +type loadAssignment struct { + Endpoints []endpoint `json:"endpoints"` +} + +type endpoint struct { + LBEndpoints []lbEndpoint `json:"lb_endpoints"` +} + +type lbEndpoint struct { + Endpoint ep `json:"endpoint"` + HealthStatus string `json:"health_status"` + LoadBalancingWeight float64 `json:"load_balancing_weight"` +} + +type ep struct { + Address address `json:"address"` +} + +type address struct { + SocketAddress socketAddress `json:"socket_address"` +} + +type socketAddress struct { + Address string `json:"address"` + PortValue float64 `json:"port_value"` +} + +type endpointsConfigDump struct { + ConfigType string `json:"@type"` + StaticEndpointConfigs []endpointConfigMap `json:"static_endpoint_configs"` + DynamicEndpointConfigs []endpointConfigMap `json:"dynamic_endpoint_configs"` +} + +type endpointConfigMap struct { + EndpointConfig endpointConfig `json:"endpoint_config"` +} + +type endpointConfig struct { + ConfigType string `json:"@type"` + Name string `json:"cluster_name"` + Endpoints []endpoint `json:"endpoints"` +} + +type listenersConfigDump struct { + ConfigType string `json:"@type"` + DynamicListeners []dynamicConfig `json:"dynamic_listeners"` + StaticListeners []listenerConfig `json:"static_listeners"` +} + +type dynamicConfig struct { + Name string `json:"name"` + ActiveState listenerConfig `json:"active_state"` +} + +type listenerConfig struct { + Listener listener `json:"listener"` + LastUpdated string `json:"last_updated"` +} + +type listener struct { + Name string `json:"name"` + Address address `json:"address"` + FilterChains []filterChain `json:"filter_chains"` + TrafficDirection string `json:"traffic_direction"` +} + +type filterChain struct { + Filters []filter `json:"filters"` + FilterChainMatch filterChainMatch `json:"filter_chain_match"` +} + +type filter struct { + Name string `json:"name"` + TypedConfig typedConfig `json:"typed_config"` +} + +type typedConfig struct { + Type string `json:"@type"` + Cluster string `json:"cluster"` + RouteConfig filterRouteConfig `json:"route_config"` + HttpFilters []httpFilter `json:"http_filters"` + Rules rules `json:"rules"` +} + +type filterRouteConfig struct { + Name string `json:"name"` + VirtualHosts []filterVirtualHost `json:"virtual_hosts"` +} + +type filterVirtualHost struct { + Name string `json:"name"` + Domains []string `json:"domains"` + Routes []filterRoute `json:"routes"` +} + +type filterRoute struct { + Match filterMatch `json:"match"` + Route filterRouteCluster `json:"route"` +} + +type filterMatch struct { + Prefix string `json:"prefix"` +} + +type filterRouteCluster struct { + Cluster string `json:"cluster"` +} + +type filterChainMatch struct { + PrefixRanges []prefixRange `json:"prefix_ranges"` +} + +type prefixRange struct { + AddressPrefix string `json:"address_prefix"` + PrefixLen float64 `json:"prefix_len"` +} + +type httpFilter struct { + TypedConfig httpTypedConfig `json:"typed_config"` +} + +type httpTypedConfig struct { + Rules rules `json:"rules"` +} + +type rules struct { + Action string `json:"action"` + Policies httpTypedConfigPolicies `json:"policies"` +} + +type httpTypedConfigPolicies struct { + ConsulIntentions httpTypedConfigConsulIntentions `json:"consul-intentions-layer4"` +} + +type httpTypedConfigConsulIntentions struct { + Principals []principal `json:"principals"` +} + +type principal struct { + Authenticated authenticated `json:"authenticated"` +} + +type authenticated struct { + PrincipalName principalName `json:"principal_name"` +} + +type principalName struct { + SafeRegex safeRegex `json:"safe_regex"` +} + +type safeRegex struct { + Regex string `json:"regex"` +} + +type routesConfigDump struct { + ConfigType string `json:"@type"` + StaticRouteConfigs []routeConfigMap `json:"static_route_configs"` +} + +type routeConfigMap struct { + RouteConfig routeConfig `json:"route_config"` + LastUpdated string `json:"last_updated"` +} + +type routeConfig struct { + Name string `json:"name"` + VirtualHosts []virtualHost `json:"virtual_hosts"` +} + +type virtualHost struct { + Routes []route `json:"routes"` +} + +type route struct { + Match routeMatch `json:"match"` + Route routeRoute `json:"route"` +} + +type routeMatch struct { + Prefix string `json:"prefix"` +} + +type routeRoute struct { + Cluster string `json:"cluster"` +} + +type secretsConfigDump struct { + ConfigType string `json:"@type"` + StaticSecrets []secretConfigMap `json:"static_secrets"` + DynamicActiveSecrets []secretConfigMap `json:"dynamic_active_secrets"` + DynamicWarmingSecrets []secretConfigMap `json:"dynamic_warming_secrets"` +} + +type secretConfigMap struct { + Name string `json:"name"` + Secret secret `json:"secret"` + LastUpdated string `json:"last_updated"` +} + +type secret struct { + Type string `json:"@type"` + TLSCertificate tlsCertificate `json:"tls_certificate"` + ValidationContext validationContext `json:"validation_context"` +} + +type tlsCertificate struct { + CertificateChain certificateChain `json:"certificate_chain"` +} + +type validationContext struct { + TrustedCA trustedCA `json:"trusted_ca"` +} + +type certificateChain struct { + InlineBytes string `json:"inline_bytes"` +} + +type trustedCA struct { + InlineBytes string `json:"inline_bytes"` +} + +type clusters struct { + ClusterStatuses []clusterStatus `json:"cluster_statuses"` +} + +type clusterStatus struct { + Name string `json:"name"` + HostStatuses []hostStatus `json:"host_statuses"` +} + +type hostStatus struct { + Address address `json:"address"` +} diff --git a/cli/cmd/proxy/read/filters.go b/cli/cmd/proxy/read/filters.go new file mode 100644 index 0000000000..c9d2d8f796 --- /dev/null +++ b/cli/cmd/proxy/read/filters.go @@ -0,0 +1,103 @@ +package read + +import ( + "strconv" + "strings" +) + +// FilterClusters takes a slice of clusters along with parameters for filtering +// those clusters. +// +// - `fqdn` filters clusters to only those with fully qualified domain names +// which contain the given value. +// - `address` filters clusters to only those with endpoint addresses which +// contain the given value. +// - `port` filters clusters to only those with endpoint addresses with ports +// that match the given value. If -1 is passed, no filtering will occur. +// +// The filters are applied in combination such that a cluster must adhere to +// all of the filtering values which are passed in. +func FilterClusters(clusters []Cluster, fqdn, address string, port int) []Cluster { + // No filtering no-op. + if fqdn == "" && address == "" && port == -1 { + return clusters + } + + portStr := ":" + strconv.Itoa(port) + + filtered := make([]Cluster, 0) + for _, cluster := range clusters { + if !strings.Contains(cluster.FullyQualifiedDomainName, fqdn) { + continue + } + + endpoints := strings.Join(cluster.Endpoints, " ") + if !strings.Contains(endpoints, address) || (port != -1 && !strings.Contains(endpoints, portStr)) { + continue + } + + hasFQDN := strings.Contains(cluster.FullyQualifiedDomainName, fqdn) + hasAddress := strings.Contains(endpoints, address) + hasPort := port == -1 || strings.Contains(endpoints, portStr) + + if hasFQDN && hasAddress && hasPort { + filtered = append(filtered, cluster) + } + } + + return filtered +} + +// FilterEndpoints takes a slice of endpoints along with parameters for filtering +// those endpoints: +// +// - `address` filters endpoints to only those with an address which contains +// the given value. +// - `port` filters endpoints to only those with an address which has a port +// that matches the given value. If -1 is passed, no filtering will occur. +// +// The filters are applied in combination such that an endpoint must adhere to +// all of the filtering values which are passed in. +func FilterEndpoints(endpoints []Endpoint, address string, port int) []Endpoint { + if address == "" && port == -1 { + return endpoints + } + + portStr := ":" + strconv.Itoa(port) + + filtered := make([]Endpoint, 0) + for _, endpoint := range endpoints { + if strings.Contains(endpoint.Address, address) && (port == -1 || strings.Contains(endpoint.Address, portStr)) { + filtered = append(filtered, endpoint) + } + } + + return filtered +} + +// FilterListeners takes a slice of listeners along with parameters for filtering +// those endpoints: +// +// - `address` filters listeners to only those with an address which contains +// the given value. +// - `port` filters listeners to only those with an address which has a port +// that matches the given value. If -1 is passed, no filtering will occur. +// +// The filters are applied in combination such that an listener must adhere to +// all of the filtering values which are passed in. +func FilterListeners(listeners []Listener, address string, port int) []Listener { + if address == "" && port == -1 { + return listeners + } + + portStr := ":" + strconv.Itoa(port) + + filtered := make([]Listener, 0) + for _, listener := range listeners { + if strings.Contains(listener.Address, address) && (port == -1 || strings.Contains(listener.Address, portStr)) { + filtered = append(filtered, listener) + } + } + + return filtered +} diff --git a/cli/cmd/proxy/read/filters_test.go b/cli/cmd/proxy/read/filters_test.go new file mode 100644 index 0000000000..48ff3a97da --- /dev/null +++ b/cli/cmd/proxy/read/filters_test.go @@ -0,0 +1,339 @@ +package read + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFilterClusters(t *testing.T) { + given := []Cluster{ + { + FullyQualifiedDomainName: "local_agent", + Endpoints: []string{"192.168.79.187:8502"}, + }, + { + FullyQualifiedDomainName: "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + { + FullyQualifiedDomainName: "local_admin", + Endpoints: []string{"127.0.0.1:5000"}, + }, + { + FullyQualifiedDomainName: "original-destination", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{"123.45.67.890:8080", "111.30.2.39:8080"}, + }, + } + + cases := map[string]struct { + fqdn string + address string + port int + expected []Cluster + }{ + "No filter": { + fqdn: "", + address: "", + port: -1, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_agent", + Endpoints: []string{"192.168.79.187:8502"}, + }, + { + FullyQualifiedDomainName: "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + { + FullyQualifiedDomainName: "local_admin", + Endpoints: []string{"127.0.0.1:5000"}, + }, + { + FullyQualifiedDomainName: "original-destination", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{"123.45.67.890:8080", "111.30.2.39:8080"}, + }, + }, + }, + "Filter FQDN": { + fqdn: "default", + address: "", + port: -1, + expected: []Cluster{ + { + FullyQualifiedDomainName: "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + }, + { + FullyQualifiedDomainName: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{"123.45.67.890:8080", "111.30.2.39:8080"}, + }, + }, + }, + "Filter address": { + fqdn: "", + address: "127.0.", + port: -1, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + { + FullyQualifiedDomainName: "local_admin", + Endpoints: []string{"127.0.0.1:5000"}, + }, + }, + }, + "Filter port": { + fqdn: "", + address: "", + port: 8080, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + { + FullyQualifiedDomainName: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{"123.45.67.890:8080", "111.30.2.39:8080"}, + }, + }, + }, + "Filter fqdn and address": { + fqdn: "local", + address: "127.0.0.1", + port: -1, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + { + FullyQualifiedDomainName: "local_admin", + Endpoints: []string{"127.0.0.1:5000"}, + }, + }, + }, + "Filter fqdn and port": { + fqdn: "local", + address: "", + port: 8080, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + }, + }, + "Filter address and port": { + fqdn: "", + address: "127.0.0.1", + port: 8080, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + }, + }, + }, + "Filter fqdn, address, and port": { + fqdn: "local", + address: "192.168.79.187", + port: 8502, + expected: []Cluster{ + { + FullyQualifiedDomainName: "local_agent", + Endpoints: []string{"192.168.79.187:8502"}, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + actual := FilterClusters(given, tc.fqdn, tc.address, tc.port) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestFilterEndpoints(t *testing.T) { + given := []Endpoint{ + { + Address: "192.168.79.187:8502", + }, + { + Address: "127.0.0.1:8080", + }, + { + Address: "192.168.31.201:20000", + }, + { + Address: "192.168.47.235:20000", + }, + { + Address: "192.168.71.254:20000", + }, + } + + cases := map[string]struct { + address string + port int + expected []Endpoint + }{ + "No filter": { + address: "", + port: -1, + expected: []Endpoint{ + { + Address: "192.168.79.187:8502", + }, + { + Address: "127.0.0.1:8080", + }, + { + Address: "192.168.31.201:20000", + }, + { + Address: "192.168.47.235:20000", + }, + { + Address: "192.168.71.254:20000", + }, + }, + }, + "Filter address": { + address: "127.0.0.1", + port: -1, + expected: []Endpoint{ + { + Address: "127.0.0.1:8080", + }, + }, + }, + "Filter port": { + address: "", + port: 20000, + expected: []Endpoint{ + { + Address: "192.168.31.201:20000", + }, + { + Address: "192.168.47.235:20000", + }, + { + Address: "192.168.71.254:20000", + }, + }, + }, + "Filter address and port": { + address: "235", + port: 20000, + expected: []Endpoint{ + { + Address: "192.168.47.235:20000", + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + actual := FilterEndpoints(given, tc.address, tc.port) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestFilterListeners(t *testing.T) { + given := []Listener{ + { + Address: "192.168.69.179:20000", + }, + { + Address: "127.0.0.1:15001", + }, + } + + cases := map[string]struct { + address string + port int + expected []Listener + }{ + "No filter": { + address: "", + port: -1, + expected: []Listener{ + { + Address: "192.168.69.179:20000", + }, + { + Address: "127.0.0.1:15001", + }, + }, + }, + "Filter address": { + address: "127.0.0.1", + port: -1, + expected: []Listener{ + { + Address: "127.0.0.1:15001", + }, + }, + }, + "Filter port": { + address: "", + port: 20000, + expected: []Listener{ + { + Address: "192.168.69.179:20000", + }, + }, + }, + "Filter address and port": { + address: "192.168.69.179", + port: 20000, + expected: []Listener{ + { + Address: "192.168.69.179:20000", + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + actual := FilterListeners(given, tc.address, tc.port) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/cli/cmd/proxy/read/format.go b/cli/cmd/proxy/read/format.go new file mode 100644 index 0000000000..97d5ada86a --- /dev/null +++ b/cli/cmd/proxy/read/format.go @@ -0,0 +1,76 @@ +package read + +import ( + "fmt" + "strings" + + "github.com/hashicorp/consul-k8s/cli/common/terminal" +) + +func formatClusters(clusters []Cluster) *terminal.Table { + table := terminal.NewTable("Name", "FQDN", "Endpoints", "Type", "Last Updated") + for _, cluster := range clusters { + table.AddRow([]string{cluster.Name, cluster.FullyQualifiedDomainName, strings.Join(cluster.Endpoints, ", "), + cluster.Type, cluster.LastUpdated}, []string{}) + } + + return table +} + +func formatEndpoints(endpoints []Endpoint) *terminal.Table { + table := terminal.NewTable("Address:Port", "Cluster", "Weight", "Status") + for _, endpoint := range endpoints { + var statusColor string + if endpoint.Status == "HEALTHY" { + statusColor = "green" + } else { + statusColor = "red" + } + + table.AddRow( + []string{endpoint.Address, endpoint.Cluster, fmt.Sprintf("%.2f", endpoint.Weight), endpoint.Status}, + []string{"", "", "", statusColor}) + } + + return table +} + +func formatListeners(listeners []Listener) *terminal.Table { + table := terminal.NewTable("Name", "Address:Port", "Direction", "Filter Chain Match", "Filters", "Last Updated") + for _, listener := range listeners { + for index, filter := range listener.FilterChain { + // Print each element of the filter chain in a separate line + // without repeating the name, address, etc. + filters := strings.Join(filter.Filters, "\n") + if index == 0 { + table.AddRow( + []string{listener.Name, listener.Address, listener.Direction, filter.FilterChainMatch, filters, listener.LastUpdated}, + []string{}) + } else { + table.AddRow( + []string{"", "", "", filter.FilterChainMatch, filters}, + []string{}) + } + } + } + + return table +} + +func formatRoutes(routes []Route) *terminal.Table { + table := terminal.NewTable("Name", "Destination Cluster", "Last Updated") + for _, route := range routes { + table.AddRow([]string{route.Name, route.DestinationCluster, route.LastUpdated}, []string{}) + } + + return table +} + +func formatSecrets(secrets []Secret) *terminal.Table { + table := terminal.NewTable("Name", "Type", "Last Updated") + for _, secret := range secrets { + table.AddRow([]string{secret.Name, secret.Type, secret.LastUpdated}, []string{}) + } + + return table +} diff --git a/cli/cmd/proxy/read/format_test.go b/cli/cmd/proxy/read/format_test.go new file mode 100644 index 0000000000..7d6f975d39 --- /dev/null +++ b/cli/cmd/proxy/read/format_test.go @@ -0,0 +1,312 @@ +package read + +import ( + "bytes" + "context" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/stretchr/testify/require" +) + +func TestFormatClusters(t *testing.T) { + // These regular expressions must be present in the output. + expected := []string{ + "Name.*FQDN.*Endpoints.*Type.*Last Updated", + "local_agent.*local_agent.*192\\.168\\.79\\.187:8502.*STATIC.*2022-05-13T04:22:39\\.553Z", + "local_app.*local_app.*127\\.0\\.0\\.1:8080.*STATIC.*2022-05-13T04:22:39\\.655Z", + "client.*client\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul.*EDS.*2022-06-09T00:39:12\\.948Z", + "frontend.*frontend\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul.*EDS.*2022-06-09T00:39:12\\.855Z", + "original-destination.*original-destination.*ORIGINAL_DST.*2022-05-13T04:22:39.743Z", + "server.*server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul.*EDS.*2022-06-09T00:39:12\\.754Z", + } + + given := []Cluster{ + { + Name: "local_agent", + FullyQualifiedDomainName: "local_agent", + Endpoints: []string{"192.168.79.187:8502"}, + Type: "STATIC", + LastUpdated: "2022-05-13T04:22:39.553Z", + }, + { + Name: "client", + FullyQualifiedDomainName: "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + Type: "EDS", + LastUpdated: "2022-06-09T00:39:12.948Z", + }, + { + Name: "frontend", + FullyQualifiedDomainName: "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + Type: "EDS", + LastUpdated: "2022-06-09T00:39:12.855Z", + }, + { + Name: "local_app", + FullyQualifiedDomainName: "local_app", + Endpoints: []string{"127.0.0.1:8080"}, + Type: "STATIC", + LastUpdated: "2022-05-13T04:22:39.655Z", + }, + { + Name: "original-destination", + FullyQualifiedDomainName: "original-destination", + Endpoints: []string{}, + Type: "ORIGINAL_DST", + LastUpdated: "2022-05-13T04:22:39.743Z", + }, + { + Name: "server", + FullyQualifiedDomainName: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + Endpoints: []string{}, + Type: "EDS", + LastUpdated: "2022-06-09T00:39:12.754Z", + }, + } + + expectedHeaders := []string{"Name", "FQDN", "Endpoints", "Type", "Last Updated"} + + table := formatClusters(given) + + require.Equal(t, expectedHeaders, table.Headers) + require.Equal(t, len(given), len(table.Rows)) + + buf := new(bytes.Buffer) + terminal.NewUI(context.Background(), buf).Table(table) + + actual := buf.String() + for _, expression := range expected { + require.Regexp(t, expression, actual) + } +} + +func TestFormatEndpoints(t *testing.T) { + // These regular expressions must be present in the output. + expected := []string{ + "Address:Port.*Cluster.*Weight.*Status", + "192.168.79.187:8502.*local_agent.*1.00.*HEALTHY", + "127.0.0.1:8080.*local_app.*1.00.*HEALTHY", + "192.168.31.201:20000.*1.00.*HEALTHY", + "192.168.47.235:20000.*1.00.*HEALTHY", + "192.168.71.254:20000.*1.00.*HEALTHY", + "192.168.63.120:20000.*1.00.*HEALTHY", + "192.168.18.110:20000.*1.00.*HEALTHY", + "192.168.52.101:20000.*1.00.*HEALTHY", + "192.168.65.131:20000.*1.00.*HEALTHY", + } + + given := []Endpoint{ + { + Address: "192.168.79.187:8502", + Cluster: "local_agent", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "127.0.0.1:8080", + Cluster: "local_app", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.31.201:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.47.235:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.71.254:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.63.120:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.18.110:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.52.101:20000", + Weight: 1, + Status: "HEALTHY", + }, + { + Address: "192.168.65.131:20000", + Weight: 1, + Status: "HEALTHY", + }, + } + + expectedHeaders := []string{"Address:Port", "Cluster", "Weight", "Status"} + + table := formatEndpoints(given) + + require.Equal(t, expectedHeaders, table.Headers) + require.Equal(t, len(given), len(table.Rows)) + + buf := new(bytes.Buffer) + terminal.NewUI(context.Background(), buf).Table(table) + + actual := buf.String() + for _, expression := range expected { + require.Regexp(t, expression, actual) + } +} + +func TestFormatListeners(t *testing.T) { + // These regular expressions must be present in the output. + expected := []string{ + "Name.*Address:Port.*Direction.*Filter Chain Match.*Filters.*Last Updated", + "public_listener.*192\\.168\\.69\\.179:20000.*INBOUND.*Any.*\\* -> local_app/.*2022-06-09T00:39:27\\.668Z", + "outbound_listener.*127.0.0.1:15001.*OUTBOUND.*10\\.100\\.134\\.173/32, 240\\.0\\.0\\.3/32.*-> client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul.*2022-05-24T17:41:59\\.079Z", + "10\\.100\\.254\\.176/32, 240\\.0\\.0\\.4/32.*\\* -> server\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul/", + "10\\.100\\.31\\.2/32, 240\\.0\\.0\\.2/32.*-> frontend\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul", + "Any.*-> original-destination", + } + + given := []Listener{ + { + Name: "public_listener", + Address: "192.168.69.179:20000", + FilterChain: []FilterChain{ + { + FilterChainMatch: "Any", + Filters: []string{"* -> local_app/"}, + }, + }, + Direction: "INBOUND", + LastUpdated: "2022-06-09T00:39:27.668Z", + }, + { + Name: "outbound_listener", + Address: "127.0.0.1:15001", + FilterChain: []FilterChain{ + { + FilterChainMatch: "10.100.134.173/32, 240.0.0.3/32", + Filters: []string{"-> client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul"}, + }, + { + FilterChainMatch: "10.100.254.176/32, 240.0.0.4/32", + Filters: []string{"* -> server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/"}, + }, + { + FilterChainMatch: "10.100.31.2/32, 240.0.0.2/32", + Filters: []string{ + "-> frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + }, + }, + { + FilterChainMatch: "Any", + Filters: []string{"-> original-destination"}, + }, + }, + Direction: "OUTBOUND", + LastUpdated: "2022-05-24T17:41:59.079Z", + }, + } + + expectedHeaders := []string{"Name", "Address:Port", "Direction", "Filter Chain Match", "Filters", "Last Updated"} + + // Listeners tables split filter chain information across rows. + expectedRowCount := 0 + for _, element := range given { + expectedRowCount += len(element.FilterChain) + } + + table := formatListeners(given) + + require.Equal(t, expectedHeaders, table.Headers) + require.Equal(t, expectedRowCount, len(table.Rows)) + + buf := new(bytes.Buffer) + terminal.NewUI(context.Background(), buf).Table(table) + + actual := buf.String() + for _, expression := range expected { + require.Regexp(t, expression, actual) + } +} + +func TestFormatRoutes(t *testing.T) { + // These regular expressions must be present in the output. + expected := []string{ + "Name.*Destination Cluster.*Last Updated", + "public_listener.*local_app/.*2022-06-09T00:39:27.667Z", + "server.*server\\.default\\.dc1\\.internal\\.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00\\.consul/.*2022-05-24T17:41:59\\.078Z", + } + + given := []Route{ + { + Name: "public_listener", + DestinationCluster: "local_app/", + LastUpdated: "2022-06-09T00:39:27.667Z", + }, + { + Name: "server", + DestinationCluster: "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/", + LastUpdated: "2022-05-24T17:41:59.078Z", + }, + } + + expectedHeaders := []string{"Name", "Destination Cluster", "Last Updated"} + + table := formatRoutes(given) + + require.Equal(t, expectedHeaders, table.Headers) + require.Equal(t, len(given), len(table.Rows)) + + buf := new(bytes.Buffer) + terminal.NewUI(context.Background(), buf).Table(table) + + actual := buf.String() + for _, expression := range expected { + require.Regexp(t, expression, actual) + } +} + +func TestFormatSecrets(t *testing.T) { + // These regular expressions must be present in the output. + expected := []string{ + "Name.*Type.*Last Updated", + "default.*Dynamic Active.*2022-05-24T17:41:59.078Z", + "ROOTCA.*Dynamic Warming.*2022-03-15T05:14:22.868Z", + } + + given := []Secret{ + { + Name: "default", + Type: "Dynamic Active", + LastUpdated: "2022-05-24T17:41:59.078Z", + }, + { + Name: "ROOTCA", + Type: "Dynamic Warming", + LastUpdated: "2022-03-15T05:14:22.868Z", + }, + } + + expectedHeaders := []string{"Name", "Type", "Last Updated"} + + table := formatSecrets(given) + + require.Equal(t, expectedHeaders, table.Headers) + require.Equal(t, len(given), len(table.Rows)) + + buf := new(bytes.Buffer) + terminal.NewUI(context.Background(), buf).Table(table) + + actual := buf.String() + for _, expression := range expected { + require.Regexp(t, expression, actual) + } +} diff --git a/cli/cmd/proxy/read/test_clusters.json b/cli/cmd/proxy/read/test_clusters.json new file mode 100644 index 0000000000..55a4c244b2 --- /dev/null +++ b/cli/cmd/proxy/read/test_clusters.json @@ -0,0 +1,302 @@ +{ + "cluster_statuses": [ + { + "name": "local_app", + "added_via_api": true, + "host_statuses": [ + { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 0 + } + }, + "stats": [ + { + "value": "6", + "name": "cx_connect_fail" + }, + { + "value": "38", + "name": "cx_total" + }, + { + "name": "rq_error" + }, + { + "name": "rq_success" + }, + { + "name": "rq_timeout" + }, + { + "name": "rq_total" + }, + { + "type": "GAUGE", + "name": "cx_active" + }, + { + "type": "GAUGE", + "name": "rq_active" + } + ], + "health_status": { + "eds_health_status": "HEALTHY" + }, + "weight": 1, + "locality": { + + } + } + ], + "circuit_breakers": { + "thresholds": [ + { + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + }, + { + "priority": "HIGH", + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + } + ] + }, + "observability_name": "local_app" + }, + { + "name": "self_admin", + "host_statuses": [ + { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 19000 + } + }, + "stats": [ + { + "name": "cx_connect_fail" + }, + { + "value": "1", + "name": "cx_total" + }, + { + "name": "rq_error" + }, + { + "value": "26", + "name": "rq_success" + }, + { + "name": "rq_timeout" + }, + { + "value": "26", + "name": "rq_total" + }, + { + "type": "GAUGE", + "value": "1", + "name": "cx_active" + }, + { + "type": "GAUGE", + "name": "rq_active" + } + ], + "health_status": { + "eds_health_status": "HEALTHY" + }, + "weight": 1, + "locality": { + + } + } + ], + "circuit_breakers": { + "thresholds": [ + { + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + }, + { + "priority": "HIGH", + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + } + ] + }, + "observability_name": "self_admin" + }, + { + "name": "original-destination", + "added_via_api": true, + "circuit_breakers": { + "thresholds": [ + { + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + }, + { + "priority": "HIGH", + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + } + ] + }, + "observability_name": "original-destination" + }, + { + "name": "static-server.default.dc1.internal.80d0b664-149b-52fa-c6ae-3dad962bdaa8.consul", + "added_via_api": true, + "host_statuses": [ + { + "address": { + "socket_address": { + "address": "10.244.0.74", + "port_value": 20000 + } + }, + "stats": [ + { + "name": "cx_connect_fail" + }, + { + "name": "cx_total" + }, + { + "name": "rq_error" + }, + { + "name": "rq_success" + }, + { + "name": "rq_timeout" + }, + { + "name": "rq_total" + }, + { + "type": "GAUGE", + "name": "cx_active" + }, + { + "type": "GAUGE", + "name": "rq_active" + } + ], + "health_status": { + "eds_health_status": "HEALTHY" + }, + "weight": 1, + "locality": { + + } + } + ], + "circuit_breakers": { + "thresholds": [ + { + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + }, + { + "priority": "HIGH", + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + } + ] + }, + "observability_name": "static-server.default.dc1.internal.80d0b664-149b-52fa-c6ae-3dad962bdaa8.consul" + }, + { + "name": "local_agent", + "host_statuses": [ + { + "address": { + "socket_address": { + "address": "172.18.0.2", + "port_value": 8502 + } + }, + "stats": [ + { + "name": "cx_connect_fail" + }, + { + "value": "1", + "name": "cx_total" + }, + { + "name": "rq_error" + }, + { + "name": "rq_success" + }, + { + "name": "rq_timeout" + }, + { + "value": "1", + "name": "rq_total" + }, + { + "type": "GAUGE", + "value": "1", + "name": "cx_active" + }, + { + "type": "GAUGE", + "value": "1", + "name": "rq_active" + } + ], + "health_status": { + "eds_health_status": "HEALTHY" + }, + "weight": 1, + "locality": { + + } + } + ], + "circuit_breakers": { + "thresholds": [ + { + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + }, + { + "priority": "HIGH", + "max_connections": 1024, + "max_pending_requests": 1024, + "max_requests": 1024, + "max_retries": 3 + } + ] + }, + "observability_name": "local_agent" + } + ] +} diff --git a/cli/cmd/proxy/read/test_config_dump.json b/cli/cmd/proxy/read/test_config_dump.json new file mode 100644 index 0000000000..982f50483d --- /dev/null +++ b/cli/cmd/proxy/read/test_config_dump.json @@ -0,0 +1,2019 @@ +{ + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump", + "bootstrap": { + "node": { + "id": "backend-658b679b45-d5xlb-backend-sidecar-proxy", + "cluster": "backend", + "metadata": { + "partition": "default", + "namespace": "default" + }, + "user_agent_name": "envoy", + "user_agent_build_version": { + "version": { + "major_number": 1, + "minor_number": 22 + }, + "metadata": { + "revision.status": "Clean", + "ssl.version": "BoringSSL", + "revision.sha": "dcd329a2e95b54f754b17aceca3f72724294b502", + "build.type": "RELEASE" + } + }, + "extensions": [ + { + "name": "envoy.filters.thrift.header_to_metadata", + "category": "envoy.thrift_proxy.filters" + }, + { + "name": "envoy.filters.thrift.rate_limit", + "category": "envoy.thrift_proxy.filters" + }, + { + "name": "envoy.filters.thrift.router", + "category": "envoy.thrift_proxy.filters" + }, + { + "name": "envoy.request_id.uuid", + "category": "envoy.request_id" + }, + { + "name": "dubbo", + "category": "envoy.dubbo_proxy.protocols" + }, + { + "name": "envoy.filters.connection_pools.tcp.generic", + "category": "envoy.upstreams" + }, + { + "name": "envoy.client_ssl_auth", + "category": "envoy.filters.network" + }, + { + "name": "envoy.echo", + "category": "envoy.filters.network" + }, + { + "name": "envoy.ext_authz", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.client_ssl_auth", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.connection_limit", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.direct_response", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.dubbo_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.echo", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.ext_authz", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.http_connection_manager", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.local_ratelimit", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.mongo_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.ratelimit", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.rbac", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.redis_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.sni_cluster", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.sni_dynamic_forward_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.tcp_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.thrift_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.wasm", + "category": "envoy.filters.network" + }, + { + "name": "envoy.filters.network.zookeeper_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.http_connection_manager", + "category": "envoy.filters.network" + }, + { + "name": "envoy.mongo_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.ratelimit", + "category": "envoy.filters.network" + }, + { + "name": "envoy.redis_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.tcp_proxy", + "category": "envoy.filters.network" + }, + { + "name": "envoy.dynamic.ot", + "category": "envoy.tracers" + }, + { + "name": "envoy.lightstep", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.datadog", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.dynamic_ot", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.lightstep", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.opencensus", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.skywalking", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.xray", + "category": "envoy.tracers" + }, + { + "name": "envoy.tracers.zipkin", + "category": "envoy.tracers" + }, + { + "name": "envoy.zipkin", + "category": "envoy.tracers" + }, + { + "name": "envoy.cluster.eds", + "category": "envoy.clusters" + }, + { + "name": "envoy.cluster.logical_dns", + "category": "envoy.clusters" + }, + { + "name": "envoy.cluster.original_dst", + "category": "envoy.clusters" + }, + { + "name": "envoy.cluster.static", + "category": "envoy.clusters" + }, + { + "name": "envoy.cluster.strict_dns", + "category": "envoy.clusters" + }, + { + "name": "envoy.clusters.aggregate", + "category": "envoy.clusters" + }, + { + "name": "envoy.clusters.dynamic_forward_proxy", + "category": "envoy.clusters" + }, + { + "name": "envoy.clusters.redis", + "category": "envoy.clusters" + }, + { + "name": "envoy.tls.cert_validator.default", + "category": "envoy.tls.cert_validator" + }, + { + "name": "envoy.tls.cert_validator.spiffe", + "category": "envoy.tls.cert_validator" + }, + { + "name": "envoy.http.original_ip_detection.custom_header", + "category": "envoy.http.original_ip_detection" + }, + { + "name": "envoy.http.original_ip_detection.xff", + "category": "envoy.http.original_ip_detection" + }, + { + "name": "envoy.retry_priorities.previous_priorities", + "category": "envoy.retry_priorities" + }, + { + "name": "envoy.filters.dubbo.router", + "category": "envoy.dubbo_proxy.filters" + }, + { + "name": "envoy.filters.listener.http_inspector", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.filters.listener.original_dst", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.filters.listener.original_src", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.filters.listener.proxy_protocol", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.filters.listener.tls_inspector", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.listener.http_inspector", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.listener.original_dst", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.listener.original_src", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.listener.proxy_protocol", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.listener.tls_inspector", + "category": "envoy.filters.listener" + }, + { + "name": "envoy.compression.brotli.decompressor", + "category": "envoy.compression.decompressor" + }, + { + "name": "envoy.compression.gzip.decompressor", + "category": "envoy.compression.decompressor" + }, + { + "name": "envoy.compression.zstd.decompressor", + "category": "envoy.compression.decompressor" + }, + { + "name": "envoy.internal_redirect_predicates.allow_listed_routes", + "category": "envoy.internal_redirect_predicates" + }, + { + "name": "envoy.internal_redirect_predicates.previous_routes", + "category": "envoy.internal_redirect_predicates" + }, + { + "name": "envoy.internal_redirect_predicates.safe_cross_scheme", + "category": "envoy.internal_redirect_predicates" + }, + { + "name": "envoy.dog_statsd", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.graphite_statsd", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.metrics_service", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.stat_sinks.dog_statsd", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.stat_sinks.graphite_statsd", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.stat_sinks.hystrix", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.stat_sinks.metrics_service", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.stat_sinks.statsd", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.stat_sinks.wasm", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.statsd", + "category": "envoy.stats_sinks" + }, + { + "name": "envoy.access_loggers.file", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.access_loggers.http_grpc", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.access_loggers.open_telemetry", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.access_loggers.stderr", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.access_loggers.stdout", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.access_loggers.tcp_grpc", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.access_loggers.wasm", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.file_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.http_grpc_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.open_telemetry_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.stderr_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.stdout_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.tcp_grpc_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.wasm_access_log", + "category": "envoy.access_loggers" + }, + { + "name": "envoy.filters.udp.dns_filter", + "category": "envoy.filters.udp_listener" + }, + { + "name": "envoy.filters.udp_listener.udp_proxy", + "category": "envoy.filters.udp_listener" + }, + { + "name": "envoy.access_loggers.extension_filters.cel", + "category": "envoy.access_logger.extension_filters" + }, + { + "name": "envoy.key_value.file_based", + "category": "envoy.common.key_value" + }, + { + "name": "envoy.rbac.matchers.upstream_ip_port", + "category": "envoy.rbac.matchers" + }, + { + "name": "composite-action", + "category": "envoy.matching.action" + }, + { + "name": "skip", + "category": "envoy.matching.action" + }, + { + "name": "envoy.matching.common_inputs.environment_variable", + "category": "envoy.matching.common_inputs" + }, + { + "name": "envoy.resource_monitors.fixed_heap", + "category": "envoy.resource_monitors" + }, + { + "name": "envoy.resource_monitors.injected_resource", + "category": "envoy.resource_monitors" + }, + { + "name": "envoy.watchdog.abort_action", + "category": "envoy.guarddog_actions" + }, + { + "name": "envoy.watchdog.profile_action", + "category": "envoy.guarddog_actions" + }, + { + "name": "default", + "category": "network.connection.client" + }, + { + "name": "envoy_internal", + "category": "network.connection.client" + }, + { + "name": "envoy.matching.custom_matchers.trie_matcher", + "category": "envoy.matching.network.custom_matchers" + }, + { + "name": "envoy.bandwidth_limit", + "category": "envoy.filters.http" + }, + { + "name": "envoy.buffer", + "category": "envoy.filters.http" + }, + { + "name": "envoy.cors", + "category": "envoy.filters.http" + }, + { + "name": "envoy.csrf", + "category": "envoy.filters.http" + }, + { + "name": "envoy.ext_authz", + "category": "envoy.filters.http" + }, + { + "name": "envoy.ext_proc", + "category": "envoy.filters.http" + }, + { + "name": "envoy.fault", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.adaptive_concurrency", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.admission_control", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.alternate_protocols_cache", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.aws_lambda", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.aws_request_signing", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.bandwidth_limit", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.buffer", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.cache", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.cdn_loop", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.composite", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.compressor", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.cors", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.csrf", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.decompressor", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.dynamic_forward_proxy", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.dynamo", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.ext_authz", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.ext_proc", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.fault", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.gcp_authn", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.grpc_http1_bridge", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.grpc_http1_reverse_bridge", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.grpc_json_transcoder", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.grpc_stats", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.grpc_web", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.header_to_metadata", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.health_check", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.ip_tagging", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.jwt_authn", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.local_ratelimit", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.lua", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.oauth2", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.on_demand", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.original_src", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.ratelimit", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.rbac", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.router", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.set_metadata", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.stateful_session", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.tap", + "category": "envoy.filters.http" + }, + { + "name": "envoy.filters.http.wasm", + "category": "envoy.filters.http" + }, + { + "name": "envoy.grpc_http1_bridge", + "category": "envoy.filters.http" + }, + { + "name": "envoy.grpc_json_transcoder", + "category": "envoy.filters.http" + }, + { + "name": "envoy.grpc_web", + "category": "envoy.filters.http" + }, + { + "name": "envoy.health_check", + "category": "envoy.filters.http" + }, + { + "name": "envoy.http_dynamo_filter", + "category": "envoy.filters.http" + }, + { + "name": "envoy.ip_tagging", + "category": "envoy.filters.http" + }, + { + "name": "envoy.local_rate_limit", + "category": "envoy.filters.http" + }, + { + "name": "envoy.lua", + "category": "envoy.filters.http" + }, + { + "name": "envoy.rate_limit", + "category": "envoy.filters.http" + }, + { + "name": "envoy.router", + "category": "envoy.filters.http" + }, + { + "name": "match-wrapper", + "category": "envoy.filters.http" + }, + { + "name": "envoy.quic.crypto_stream.server.quiche", + "category": "envoy.quic.server.crypto_stream" + }, + { + "name": "dubbo.hessian2", + "category": "envoy.dubbo_proxy.serializers" + }, + { + "name": "envoy.wasm.runtime.null", + "category": "envoy.wasm.runtime" + }, + { + "name": "envoy.wasm.runtime.v8", + "category": "envoy.wasm.runtime" + }, + { + "name": "envoy.matching.inputs.application_protocol", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.destination_ip", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.destination_port", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.direct_source_ip", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.server_name", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.source_ip", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.source_port", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.source_type", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.inputs.transport_protocol", + "category": "envoy.matching.network.input" + }, + { + "name": "envoy.matching.matchers.consistent_hashing", + "category": "envoy.matching.input_matchers" + }, + { + "name": "envoy.matching.matchers.ip", + "category": "envoy.matching.input_matchers" + }, + { + "name": "preserve_case", + "category": "envoy.http.stateful_header_formatters" + }, + { + "name": "envoy.config.validators.minimum_clusters", + "category": "envoy.config.validators" + }, + { + "name": "envoy.config.validators.minimum_clusters_validator", + "category": "envoy.config.validators" + }, + { + "name": "envoy.bootstrap.internal_listener", + "category": "envoy.bootstrap" + }, + { + "name": "envoy.bootstrap.wasm", + "category": "envoy.bootstrap" + }, + { + "name": "envoy.extensions.network.socket_interface.default_socket_interface", + "category": "envoy.bootstrap" + }, + { + "name": "envoy.grpc_credentials.aws_iam", + "category": "envoy.grpc_credentials" + }, + { + "name": "envoy.grpc_credentials.default", + "category": "envoy.grpc_credentials" + }, + { + "name": "envoy.grpc_credentials.file_based_metadata", + "category": "envoy.grpc_credentials" + }, + { + "name": "envoy.rate_limit_descriptors.expr", + "category": "envoy.rate_limit_descriptors" + }, + { + "name": "auto", + "category": "envoy.thrift_proxy.transports" + }, + { + "name": "framed", + "category": "envoy.thrift_proxy.transports" + }, + { + "name": "header", + "category": "envoy.thrift_proxy.transports" + }, + { + "name": "unframed", + "category": "envoy.thrift_proxy.transports" + }, + { + "name": "envoy.compression.brotli.compressor", + "category": "envoy.compression.compressor" + }, + { + "name": "envoy.compression.gzip.compressor", + "category": "envoy.compression.compressor" + }, + { + "name": "envoy.compression.zstd.compressor", + "category": "envoy.compression.compressor" + }, + { + "name": "envoy.network.dns_resolver.cares", + "category": "envoy.network.dns_resolver" + }, + { + "name": "envoy.health_checkers.redis", + "category": "envoy.health_checkers" + }, + { + "name": "envoy.ip", + "category": "envoy.resolvers" + }, + { + "name": "envoy.transport_sockets.alts", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.transport_sockets.quic", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.transport_sockets.raw_buffer", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.transport_sockets.starttls", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.transport_sockets.tap", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.transport_sockets.tcp_stats", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.transport_sockets.tls", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "raw_buffer", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "starttls", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "tls", + "category": "envoy.transport_sockets.downstream" + }, + { + "name": "envoy.formatter.metadata", + "category": "envoy.formatter" + }, + { + "name": "envoy.formatter.req_without_query", + "category": "envoy.formatter" + }, + { + "name": "auto", + "category": "envoy.thrift_proxy.protocols" + }, + { + "name": "binary", + "category": "envoy.thrift_proxy.protocols" + }, + { + "name": "binary/non-strict", + "category": "envoy.thrift_proxy.protocols" + }, + { + "name": "compact", + "category": "envoy.thrift_proxy.protocols" + }, + { + "name": "twitter", + "category": "envoy.thrift_proxy.protocols" + }, + { + "name": "envoy.extensions.http.cache.simple", + "category": "envoy.http.cache" + }, + { + "name": "envoy.quic.proof_source.filter_chain", + "category": "envoy.quic.proof_source" + }, + { + "name": "default", + "category": "envoy.dubbo_proxy.route_matchers" + }, + { + "name": "envoy.transport_sockets.alts", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.quic", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.raw_buffer", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.starttls", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.tap", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.tcp_stats", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.tls", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.transport_sockets.upstream_proxy_protocol", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "raw_buffer", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "starttls", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "tls", + "category": "envoy.transport_sockets.upstream" + }, + { + "name": "envoy.http.stateful_session.cookie", + "category": "envoy.http.stateful_session" + }, + { + "name": "envoy.retry_host_predicates.omit_canary_hosts", + "category": "envoy.retry_host_predicates" + }, + { + "name": "envoy.retry_host_predicates.omit_host_metadata", + "category": "envoy.retry_host_predicates" + }, + { + "name": "envoy.retry_host_predicates.previous_hosts", + "category": "envoy.retry_host_predicates" + }, + { + "name": "envoy.extensions.upstreams.http.v3.HttpProtocolOptions", + "category": "envoy.upstream_options" + }, + { + "name": "envoy.upstreams.http.http_protocol_options", + "category": "envoy.upstream_options" + }, + { + "name": "envoy.matching.inputs.request_headers", + "category": "envoy.matching.http.input" + }, + { + "name": "envoy.matching.inputs.request_trailers", + "category": "envoy.matching.http.input" + }, + { + "name": "envoy.matching.inputs.response_headers", + "category": "envoy.matching.http.input" + }, + { + "name": "envoy.matching.inputs.response_trailers", + "category": "envoy.matching.http.input" + } + ] + }, + "static_resources": { + "clusters": [ + { + "name": "local_agent", + "type": "STATIC", + "connect_timeout": "1s", + "http2_protocol_options": {}, + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "common_tls_context": { + "validation_context": { + "trusted_ca": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIIDQTCCAuigAwIBAgIUH1N2/59JC/WVKUx81/tPZhzvkbowCgYIKoZIzj0EAwIw\ngZExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5j\naXNjbzEaMBgGA1UECRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1\nMRcwFQYDVQQKEw5IYXNoaUNvcnAgSW5jLjEYMBYGA1UEAxMPQ29uc3VsIEFnZW50\nIENBMB4XDTIyMDUxMzA0MTYyMVoXDTMyMDUxMDA0MTcyMVowgZExCzAJBgNVBAYT\nAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEaMBgGA1UE\nCRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1MRcwFQYDVQQKEw5I\nYXNoaUNvcnAgSW5jLjEYMBYGA1UEAxMPQ29uc3VsIEFnZW50IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEKGiRtWa7AnHvX3/MRVsNUiAJljkkGSKd/sH2ayyF\nJjIibn/UkNE7vGrVcuPsh5/gZ5ATb6TQPU5EKARir51++aOCARowggEWMA4GA1Ud\nDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0T\nAQH/BAUwAwEB/zBoBgNVHQ4EYQRfNDY6MTQ6NTg6MWM6Y2E6N2Q6NTM6ZTM6MmM6\nNTM6NGI6NjE6NjA6MDA6NTM6MmQ6OTY6YWQ6MjI6OWE6YmY6MzE6YzE6OTg6MmM6\nMWU6YTY6YTI6MTk6ZjE6NmM6ZGQwagYDVR0jBGMwYYBfNDY6MTQ6NTg6MWM6Y2E6\nN2Q6NTM6ZTM6MmM6NTM6NGI6NjE6NjA6MDA6NTM6MmQ6OTY6YWQ6MjI6OWE6YmY6\nMzE6YzE6OTg6MmM6MWU6YTY6YTI6MTk6ZjE6NmM6ZGQwCgYIKoZIzj0EAwIDRwAw\nRAIgBr/joI/OhA3QgTbxCnz7k+YrNpuVz6dKGSqbUUZ0dW0CIGA5sUyhlCnd1NCg\nSUim9u7YFd9tZiew747LovPQmMAg\n-----END CERTIFICATE-----\n\n" + } + } + } + } + }, + "load_assignment": { + "cluster_name": "local_agent", + "endpoints": [ + { + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.79.187", + "port_value": 8502 + } + } + } + } + ] + } + ] + } + } + ] + }, + "dynamic_resources": { + "lds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "cds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "ads_config": { + "api_type": "DELTA_GRPC", + "grpc_services": [ + { + "envoy_grpc": { + "cluster_name": "local_agent" + }, + "initial_metadata": [ + { + "key": "x-consul-token" + } + ] + } + ], + "transport_api_version": "V3" + } + }, + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 19000 + } + } + }, + "stats_config": { + "stats_tags": [ + { + "tag_name": "consul.destination.custom_hash", + "regex": "^cluster\\.(?:passthrough~)?((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.service_subset", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.service", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.namespace", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.partition", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.datacenter", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.routing_type", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.trust_domain", + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)" + }, + { + "tag_name": "consul.destination.target", + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.destination.full_target", + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)" + }, + { + "tag_name": "consul.upstream.service", + "regex": "^(?:tcp|http)\\.upstream\\.(([^.]+)(?:\\.[^.]+)?(?:\\.[^.]+)?\\.[^.]+\\.)" + }, + { + "tag_name": "consul.upstream.datacenter", + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.[^.]+)?\\.([^.]+)\\.)" + }, + { + "tag_name": "consul.upstream.namespace", + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.([^.]+))?(?:\\.[^.]+)?\\.[^.]+\\.)" + }, + { + "tag_name": "consul.upstream.partition", + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.([^.]+))?\\.[^.]+\\.)" + }, + { + "tag_name": "consul.custom_hash", + "regex": "^cluster\\.((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.service_subset", + "regex": "^cluster\\.((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.service", + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.namespace", + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.datacenter", + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.routing_type", + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.trust_domain", + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)" + }, + { + "tag_name": "consul.target", + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)" + }, + { + "tag_name": "consul.full_target", + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)" + }, + { + "tag_name": "local_cluster", + "fixed_value": "backend" + }, + { + "tag_name": "consul.source.service", + "fixed_value": "backend" + }, + { + "tag_name": "consul.source.namespace", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.partition", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.datacenter", + "fixed_value": "dc1" + } + ], + "use_all_default_tags": true + } + }, + "last_updated": "2022-05-13T04:22:39.488Z" + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump", + "static_clusters": [ + { + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_agent", + "type": "STATIC", + "connect_timeout": "1s", + "http2_protocol_options": {}, + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "common_tls_context": { + "validation_context": { + "trusted_ca": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIIDQTCCAuigAwIBAgIUH1N2/59JC/WVKUx81/tPZhzvkbowCgYIKoZIzj0EAwIw\ngZExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5j\naXNjbzEaMBgGA1UECRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1\nMRcwFQYDVQQKEw5IYXNoaUNvcnAgSW5jLjEYMBYGA1UEAxMPQ29uc3VsIEFnZW50\nIENBMB4XDTIyMDUxMzA0MTYyMVoXDTMyMDUxMDA0MTcyMVowgZExCzAJBgNVBAYT\nAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEaMBgGA1UE\nCRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1MRcwFQYDVQQKEw5I\nYXNoaUNvcnAgSW5jLjEYMBYGA1UEAxMPQ29uc3VsIEFnZW50IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEKGiRtWa7AnHvX3/MRVsNUiAJljkkGSKd/sH2ayyF\nJjIibn/UkNE7vGrVcuPsh5/gZ5ATb6TQPU5EKARir51++aOCARowggEWMA4GA1Ud\nDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0T\nAQH/BAUwAwEB/zBoBgNVHQ4EYQRfNDY6MTQ6NTg6MWM6Y2E6N2Q6NTM6ZTM6MmM6\nNTM6NGI6NjE6NjA6MDA6NTM6MmQ6OTY6YWQ6MjI6OWE6YmY6MzE6YzE6OTg6MmM6\nMWU6YTY6YTI6MTk6ZjE6NmM6ZGQwagYDVR0jBGMwYYBfNDY6MTQ6NTg6MWM6Y2E6\nN2Q6NTM6ZTM6MmM6NTM6NGI6NjE6NjA6MDA6NTM6MmQ6OTY6YWQ6MjI6OWE6YmY6\nMzE6YzE6OTg6MmM6MWU6YTY6YTI6MTk6ZjE6NmM6ZGQwCgYIKoZIzj0EAwIDRwAw\nRAIgBr/joI/OhA3QgTbxCnz7k+YrNpuVz6dKGSqbUUZ0dW0CIGA5sUyhlCnd1NCg\nSUim9u7YFd9tZiew747LovPQmMAg\n-----END CERTIFICATE-----\n\n" + } + } + } + } + }, + "load_assignment": { + "cluster_name": "local_agent", + "endpoints": [ + { + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.79.187", + "port_value": 8502 + } + } + } + } + ] + } + ] + } + }, + "last_updated": "2022-05-13T04:22:39.553Z" + } + ], + "dynamic_active_clusters": [ + { + "version_info": "877646fa9c433fbe25febc99b0725a44ab606d9f5407b76a6b7513edd1ac8af2", + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + "type": "EDS", + "eds_cluster_config": { + "eds_config": { + "ads": {}, + "resource_api_version": "V3" + } + }, + "connect_timeout": "5s", + "circuit_breakers": {}, + "outlier_detection": {}, + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "common_tls_context": { + "tls_params": {}, + "tls_certificates": [ + { + "certificate_chain": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICHDCCAcGgAwIBAgIBWDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA2MDkwMDM4MTJa\nFw0yMjA2MTIwMDM4MTJaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARGkJfg\n3Hy48dvsqnsJ7Xmb00ZGzNaGl89++zg3PgppbvXiU+u3oK7qON/pQ3hinsjXnubr\n2y5RRaKjRke+HyTBo4H6MIH3MA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggr\nBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADApBgNVHQ4EIgQgJK3rY8+n\n0pg4r+1+uIsBl2YqMXGTQdZ45yR8lDILXtEwKwYDVR0jBCQwIoAg1Fr3vJvXDnVW\nt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwYAYDVR0RAQH/BFYwVIZSc3BpZmZlOi8v\nYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkxZjA4ZDAwLmNvbnN1bC9ucy9k\nZWZhdWx0L2RjL2RjMS9zdmMvYmFja2VuZDAKBggqhkjOPQQDAgNJADBGAiEAlik6\nBgXf8zAT3cV+ZDEz9d1oApzde8+HLoadXrDimzICIQCCDk3QL/rK5jTv6p0iqkY1\nqUPq56zIQeZ7SHUZe8Wm0A==\n-----END CERTIFICATE-----\n" + }, + "private_key": { + "inline_string": "[redacted]" + } + } + ], + "validation_context": { + "trusted_ca": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICEDCCAbWgAwIBAgIBBzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA1MTMwNDE4MDBa\nFw0zMjA1MTAwNDE4MDBaMDExLzAtBgNVBAMTJnByaS00OXhycXF6NC5jb25zdWwu\nY2EuYmMzODE1YzIuY29uc3VsMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwQHp\nkEZUr0LVB1ng/zQMQyEQnID/wN/0+Ve648F3Lz4+S7D/S41EQ1Q9ImrNGIlmJ1b2\ncL9WcsL1A1/Ts8xNSaOBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw\nAwEB/zApBgNVHQ4EIgQg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkw\nKwYDVR0jBCQwIoAg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwPwYD\nVR0RBDgwNoY0c3BpZmZlOi8vYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkx\nZjA4ZDAwLmNvbnN1bDAKBggqhkjOPQQDAgNJADBGAiEAhxIMzmCZT1sjtEKsjHIM\nC//yOo+pXZ62GQk+7rsE9HoCIQCxdLmYFUvOOiIXt4S15tvBpuJ6PdrKNlRTbmLx\nA5z5lQ==\n-----END CERTIFICATE-----\n" + }, + "match_subject_alt_names": [ + { + "exact": "spiffe://bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/ns/default/dc/dc1/svc/client" + } + ] + } + }, + "sni": "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + }, + "common_lb_config": { + "healthy_panic_threshold": {} + }, + "alt_stat_name": "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + }, + "last_updated": "2022-06-09T00:39:12.948Z" + }, + { + "version_info": "e0cc5c279829d13f01d2ba3a4d32546c0d3fc65b930bbb0bf66a68ad76e152a0", + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + "type": "EDS", + "eds_cluster_config": { + "eds_config": { + "ads": {}, + "resource_api_version": "V3" + } + }, + "connect_timeout": "5s", + "circuit_breakers": {}, + "outlier_detection": {}, + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "common_tls_context": { + "tls_params": {}, + "tls_certificates": [ + { + "certificate_chain": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICHDCCAcGgAwIBAgIBWDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA2MDkwMDM4MTJa\nFw0yMjA2MTIwMDM4MTJaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARGkJfg\n3Hy48dvsqnsJ7Xmb00ZGzNaGl89++zg3PgppbvXiU+u3oK7qON/pQ3hinsjXnubr\n2y5RRaKjRke+HyTBo4H6MIH3MA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggr\nBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADApBgNVHQ4EIgQgJK3rY8+n\n0pg4r+1+uIsBl2YqMXGTQdZ45yR8lDILXtEwKwYDVR0jBCQwIoAg1Fr3vJvXDnVW\nt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwYAYDVR0RAQH/BFYwVIZSc3BpZmZlOi8v\nYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkxZjA4ZDAwLmNvbnN1bC9ucy9k\nZWZhdWx0L2RjL2RjMS9zdmMvYmFja2VuZDAKBggqhkjOPQQDAgNJADBGAiEAlik6\nBgXf8zAT3cV+ZDEz9d1oApzde8+HLoadXrDimzICIQCCDk3QL/rK5jTv6p0iqkY1\nqUPq56zIQeZ7SHUZe8Wm0A==\n-----END CERTIFICATE-----\n" + }, + "private_key": { + "inline_string": "[redacted]" + } + } + ], + "validation_context": { + "trusted_ca": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICEDCCAbWgAwIBAgIBBzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA1MTMwNDE4MDBa\nFw0zMjA1MTAwNDE4MDBaMDExLzAtBgNVBAMTJnByaS00OXhycXF6NC5jb25zdWwu\nY2EuYmMzODE1YzIuY29uc3VsMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwQHp\nkEZUr0LVB1ng/zQMQyEQnID/wN/0+Ve648F3Lz4+S7D/S41EQ1Q9ImrNGIlmJ1b2\ncL9WcsL1A1/Ts8xNSaOBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw\nAwEB/zApBgNVHQ4EIgQg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkw\nKwYDVR0jBCQwIoAg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwPwYD\nVR0RBDgwNoY0c3BpZmZlOi8vYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkx\nZjA4ZDAwLmNvbnN1bDAKBggqhkjOPQQDAgNJADBGAiEAhxIMzmCZT1sjtEKsjHIM\nC//yOo+pXZ62GQk+7rsE9HoCIQCxdLmYFUvOOiIXt4S15tvBpuJ6PdrKNlRTbmLx\nA5z5lQ==\n-----END CERTIFICATE-----\n" + }, + "match_subject_alt_names": [ + { + "exact": "spiffe://bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/ns/default/dc/dc1/svc/frontend" + } + ] + } + }, + "sni": "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + }, + "common_lb_config": { + "healthy_panic_threshold": {} + }, + "alt_stat_name": "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + }, + "last_updated": "2022-06-09T00:39:12.855Z" + }, + { + "version_info": "642b6322013fd3f776d9644ece515db5c4901fe6a87575ab4ff07498ab5faa47", + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_app", + "type": "STATIC", + "connect_timeout": "5s", + "load_assignment": { + "cluster_name": "local_app", + "endpoints": [ + { + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 8080 + } + } + } + } + ] + } + ] + } + }, + "last_updated": "2022-05-13T04:22:39.655Z" + }, + { + "version_info": "dcd8d0247ba0149bfdc151428353b3f29d0665bf5c12af6a105a0abcc5af40ac", + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "original-destination", + "type": "ORIGINAL_DST", + "connect_timeout": "5s", + "lb_policy": "CLUSTER_PROVIDED" + }, + "last_updated": "2022-05-13T04:22:39.743Z" + }, + { + "version_info": "1454ffe616e7abd2b3bec548b88f3a14c0500a94de27f2b3e8e3913e7bfd3275", + "cluster": { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul", + "type": "EDS", + "eds_cluster_config": { + "eds_config": { + "ads": {}, + "resource_api_version": "V3" + } + }, + "connect_timeout": "5s", + "circuit_breakers": {}, + "outlier_detection": {}, + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "common_tls_context": { + "tls_params": {}, + "tls_certificates": [ + { + "certificate_chain": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICHDCCAcGgAwIBAgIBWDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA2MDkwMDM4MTJa\nFw0yMjA2MTIwMDM4MTJaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARGkJfg\n3Hy48dvsqnsJ7Xmb00ZGzNaGl89++zg3PgppbvXiU+u3oK7qON/pQ3hinsjXnubr\n2y5RRaKjRke+HyTBo4H6MIH3MA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggr\nBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADApBgNVHQ4EIgQgJK3rY8+n\n0pg4r+1+uIsBl2YqMXGTQdZ45yR8lDILXtEwKwYDVR0jBCQwIoAg1Fr3vJvXDnVW\nt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwYAYDVR0RAQH/BFYwVIZSc3BpZmZlOi8v\nYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkxZjA4ZDAwLmNvbnN1bC9ucy9k\nZWZhdWx0L2RjL2RjMS9zdmMvYmFja2VuZDAKBggqhkjOPQQDAgNJADBGAiEAlik6\nBgXf8zAT3cV+ZDEz9d1oApzde8+HLoadXrDimzICIQCCDk3QL/rK5jTv6p0iqkY1\nqUPq56zIQeZ7SHUZe8Wm0A==\n-----END CERTIFICATE-----\n" + }, + "private_key": { + "inline_string": "[redacted]" + } + } + ], + "validation_context": { + "trusted_ca": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICEDCCAbWgAwIBAgIBBzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA1MTMwNDE4MDBa\nFw0zMjA1MTAwNDE4MDBaMDExLzAtBgNVBAMTJnByaS00OXhycXF6NC5jb25zdWwu\nY2EuYmMzODE1YzIuY29uc3VsMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwQHp\nkEZUr0LVB1ng/zQMQyEQnID/wN/0+Ve648F3Lz4+S7D/S41EQ1Q9ImrNGIlmJ1b2\ncL9WcsL1A1/Ts8xNSaOBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw\nAwEB/zApBgNVHQ4EIgQg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkw\nKwYDVR0jBCQwIoAg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwPwYD\nVR0RBDgwNoY0c3BpZmZlOi8vYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkx\nZjA4ZDAwLmNvbnN1bDAKBggqhkjOPQQDAgNJADBGAiEAhxIMzmCZT1sjtEKsjHIM\nC//yOo+pXZ62GQk+7rsE9HoCIQCxdLmYFUvOOiIXt4S15tvBpuJ6PdrKNlRTbmLx\nA5z5lQ==\n-----END CERTIFICATE-----\n" + }, + "match_subject_alt_names": [ + { + "exact": "spiffe://bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul/ns/default/dc/dc1/svc/server" + } + ] + } + }, + "sni": "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + }, + "common_lb_config": { + "healthy_panic_threshold": {} + }, + "alt_stat_name": "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + }, + "last_updated": "2022-06-09T00:39:12.754Z" + } + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", + "static_endpoint_configs": [ + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "cluster_name": "local_agent", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.79.187", + "port_value": 8502 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + } + ] + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + } + ], + "dynamic_endpoint_configs": [ + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "cluster_name": "local_app", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 8080 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + } + ] + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + }, + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.31.201", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + }, + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.47.235", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + }, + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.71.254", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + } + ] + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + }, + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.63.120", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + } + ] + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + }, + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "endpoints": [ + { + "locality": {}, + "lb_endpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.18.110", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + }, + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.52.101", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + }, + { + "endpoint": { + "address": { + "socket_address": { + "address": "192.168.65.131", + "port_value": 20000 + } + }, + "health_check_config": {} + }, + "health_status": "HEALTHY", + "load_balancing_weight": 1 + } + ] + } + ], + "policy": { + "overprovisioning_factor": 140 + } + } + }, + { + "endpoint_config": { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "cluster_name": "original-destination", + "policy": { + "overprovisioning_factor": 140 + } + } + } + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump", + "dynamic_listeners": [ + { + "name": "public_listener:192.168.69.179:20000", + "active_state": { + "version_info": "42e63fea110536be20b84ab28ef1979efb5e20b967a752b8834314c4fcd58358", + "listener": { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "public_listener:192.168.69.179:20000", + "address": { + "socket_address": { + "address": "192.168.69.179", + "port_value": 20000 + } + }, + "filter_chains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "public_listener", + "route_config": { + "name": "public_listener", + "virtual_hosts": [ + { + "name": "public_listener", + "domains": ["*"], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "http_filters": [ + { + "name": "envoy.filters.http.rbac", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + "action": "DENY", + "policies": { + "consul-intentions-layer4": { + "permissions": [ + { + "any": true + } + ], + "principals": [ + { + "authenticated": { + "principal_name": { + "safe_regex": { + "google_re2": {}, + "regex": "^spiffe://[^/]+/ns/default/dc/[^/]+/svc/client$" + } + } + } + } + ] + } + } + } + } + }, + { + "name": "envoy.filters.http.router", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "random_sampling": {} + } + } + } + ], + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "common_tls_context": { + "tls_params": {}, + "tls_certificates": [ + { + "certificate_chain": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICHDCCAcGgAwIBAgIBWDAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA2MDkwMDM4MTJa\nFw0yMjA2MTIwMDM4MTJaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARGkJfg\n3Hy48dvsqnsJ7Xmb00ZGzNaGl89++zg3PgppbvXiU+u3oK7qON/pQ3hinsjXnubr\n2y5RRaKjRke+HyTBo4H6MIH3MA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggr\nBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADApBgNVHQ4EIgQgJK3rY8+n\n0pg4r+1+uIsBl2YqMXGTQdZ45yR8lDILXtEwKwYDVR0jBCQwIoAg1Fr3vJvXDnVW\nt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwYAYDVR0RAQH/BFYwVIZSc3BpZmZlOi8v\nYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkxZjA4ZDAwLmNvbnN1bC9ucy9k\nZWZhdWx0L2RjL2RjMS9zdmMvYmFja2VuZDAKBggqhkjOPQQDAgNJADBGAiEAlik6\nBgXf8zAT3cV+ZDEz9d1oApzde8+HLoadXrDimzICIQCCDk3QL/rK5jTv6p0iqkY1\nqUPq56zIQeZ7SHUZe8Wm0A==\n-----END CERTIFICATE-----\n" + }, + "private_key": { + "inline_string": "[redacted]" + } + } + ], + "validation_context": { + "trusted_ca": { + "inline_string": "-----BEGIN CERTIFICATE-----\nMIICEDCCAbWgAwIBAgIBBzAKBggqhkjOPQQDAjAxMS8wLQYDVQQDEyZwcmktNDl4\ncnFxejQuY29uc3VsLmNhLmJjMzgxNWMyLmNvbnN1bDAeFw0yMjA1MTMwNDE4MDBa\nFw0zMjA1MTAwNDE4MDBaMDExLzAtBgNVBAMTJnByaS00OXhycXF6NC5jb25zdWwu\nY2EuYmMzODE1YzIuY29uc3VsMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwQHp\nkEZUr0LVB1ng/zQMQyEQnID/wN/0+Ve648F3Lz4+S7D/S41EQ1Q9ImrNGIlmJ1b2\ncL9WcsL1A1/Ts8xNSaOBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw\nAwEB/zApBgNVHQ4EIgQg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkw\nKwYDVR0jBCQwIoAg1Fr3vJvXDnVWt0yAoYFvX8S/Kehuir9zvx8uM1UIaIkwPwYD\nVR0RBDgwNoY0c3BpZmZlOi8vYmMzODE1YzItMWEwZi1mM2ZmLWEyZTktMjBkNzkx\nZjA4ZDAwLmNvbnN1bDAKBggqhkjOPQQDAgNJADBGAiEAhxIMzmCZT1sjtEKsjHIM\nC//yOo+pXZ62GQk+7rsE9HoCIQCxdLmYFUvOOiIXt4S15tvBpuJ6PdrKNlRTbmLx\nA5z5lQ==\n-----END CERTIFICATE-----\n" + } + } + }, + "require_client_certificate": true + } + } + } + ], + "traffic_direction": "INBOUND" + }, + "last_updated": "2022-06-09T00:39:27.668Z" + } + }, + { + "name": "outbound_listener:127.0.0.1:15001", + "active_state": { + "version_info": "2bce2cd7828d5b1adbd820824ce6948fdaa00b4c824cba5ba0932aea95b7f5cd", + "listener": { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "outbound_listener:127.0.0.1:15001", + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 15001 + } + }, + "filter_chains": [ + { + "filter_chain_match": { + "prefix_ranges": [ + { + "address_prefix": "10.100.134.173", + "prefix_len": 32 + }, + { + "address_prefix": "240.0.0.3", + "prefix_len": 32 + } + ] + }, + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "stat_prefix": "upstream.client.default.default.dc1", + "cluster": "client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + } + ] + }, + { + "filter_chain_match": { + "prefix_ranges": [ + { + "address_prefix": "10.100.254.176", + "prefix_len": 32 + }, + { + "address_prefix": "240.0.0.4", + "prefix_len": 32 + } + ] + }, + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "upstream.server.default.default.dc1", + "route_config": { + "name": "server", + "virtual_hosts": [ + { + "name": "server.default.default.dc1", + "domains": ["*"], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + } + ] + } + ] + }, + "http_filters": [ + { + "name": "envoy.filters.http.router", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "random_sampling": {} + } + } + } + ] + }, + { + "filter_chain_match": { + "prefix_ranges": [ + { + "address_prefix": "10.100.31.2", + "prefix_len": 32 + }, + { + "address_prefix": "240.0.0.2", + "prefix_len": 32 + } + ] + }, + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "stat_prefix": "upstream.frontend.default.default.dc1", + "cluster": "frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + } + ] + }, + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "stat_prefix": "upstream.original-destination", + "cluster": "original-destination" + } + } + ] + } + ], + "listener_filters": [ + { + "name": "envoy.filters.listener.original_dst", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst" + } + } + ], + "traffic_direction": "OUTBOUND" + }, + "last_updated": "2022-05-24T17:41:59.079Z" + } + } + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.ScopedRoutesConfigDump" + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.RoutesConfigDump", + "static_route_configs": [ + { + "route_config": { + "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "name": "public_listener", + "virtual_hosts": [ + { + "name": "public_listener", + "domains": ["*"], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "last_updated": "2022-06-09T00:39:27.667Z" + }, + { + "route_config": { + "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "name": "server", + "virtual_hosts": [ + { + "name": "server.default.default.dc1", + "domains": ["*"], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "server.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul" + } + } + ] + } + ] + }, + "last_updated": "2022-05-24T17:41:59.078Z" + } + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.SecretsConfigDump", + "static_secrets": [], + "dynamic_active_secrets": [ + { + "name": "default", + "secret": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "name": "default", + "tls_certificate": { + "certificate_chain": { + "inline_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURQRENDQWlTZ0F3SUJBZ0lRTFNLbTFVWFVPWm5FdlllbjNLUWw4REFOQmdrcWhraUc5dzBCQVFzRkFEQVkKTVJZd0ZBWURWUVFLRXcxamJIVnpkR1Z5TG14dlkyRnNNQjRYRFRJeU1ETXhOVEExTVRJeU1sb1hEVEl5TURNeApOakExTVRReU1sb3dBRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFOYWFYMzlyCmppRU9pU3plK0hlSm42dmJxMnhHQW1aNEdHS04rQXZzWnMrbEIza3dmOXhZaWh5WHlIcHpXZ2VuRy9pazl3dGMKYTNURWZiMktZMVBIWXRzK2JtV2pwcU8xY2xacFNib1ZQWCtMYllsQmFSeDhVZWo5bENxOXJxRWVoTkt5K3AweQpzVmg2WVhlTjlxaWNEWGZzTnowdDZmNVMraW5LY0VCYzAwVGFUSEtReEs0VEh4R296SmxBMi9EeGNJc0tNdXB5CjAxZ1BXdXpWWHp6b1JwTTdKandRd0hGU3RBdy9TOHo0ZHhtZkd3aTl3Q0U0T1FRVmYwSFJpZ1ZiTUd0NUF1dkcKMXF3Q1Y3eVY5WW92a0pvbzhmSytqdDdrd1pLRFpWcDlvMDB4YkM4NzNwNnI0M05hd2lXOGJQNGNqNS9ob0xLUApGUTJuYmRmTGRFaUlYSzhDQXdFQUFhT0JtVENCbGpBT0JnTlZIUThCQWY4RUJBTUNCYUF3SFFZRFZSMGxCQll3CkZBWUlLd1lCQlFVSEF3RUdDQ3NHQVFVRkJ3TUNNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVUKRVI5T2JmWkhhWjh2NldZa281T2FPa28rczhnd05nWURWUjBSQVFIL0JDd3dLb1lvYzNCcFptWmxPaTh2WTJ4MQpjM1JsY2k1c2IyTmhiQzl1Y3k5bWIyOHZjMkV2WkdWbVlYVnNkREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBClN2SmRPcmJxWk1KUTFTQlpEUG11V3pmbEJXSEgrWVhJMVZRK1ZiaWZmQmJveHJTNlZuaDZVNEZKRHZsWTNSQWgKVDJES09MaVVySWVXY0tlMTlXajdVSDB6MVJPbXFvQ2YvazlPNWZqVDl0WGRTZVBmNjd0d2lMUWpXUklaQVJuMwpMTGlXaTMwQmpSWDEwSy9GS2pTaWRBbVZIVDZRT3AvcTEzSTE0Z2VaL0NPeThTd1pwZ2ZGN2RxY0RERjJwZ3ovCk4xSE8yLzJrbjhrWGNuNUZLWTRaNWh3RWlQOUlPOGVmSzIvNkMzSzkvOFJPNGdNWlZLTW9JVEUyZS84eW03Tm0KaVdyZHdvaWdLN0J5di9tMkVvWWdjb01vaDhkWnczMU9ZMUl4T1hqR1NIdVF2NWZzWlNObG1NaDNSbVlaVFY3dwpBSUZuWUc4QWlPV2hRd2JMUVRBc1lRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQy9UQ0NBZVdnQXdJQkFnSVJBS2IreFppZHE2bTFFbllpTllsVXBWY3dEUVlKS29aSWh2Y05BUUVMQlFBdwpHREVXTUJRR0ExVUVDaE1OWTJ4MWMzUmxjaTVzYjJOaGJEQWVGdzB5TWpBek1UVXdOVEV4TVRaYUZ3MHpNakF6Ck1USXdOVEV4TVRaYU1CZ3hGakFVQmdOVkJBb1REV05zZFhOMFpYSXViRzlqWVd3d2dnRWlNQTBHQ1NxR1NJYjMKRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEUllRNVowMGk4MVdmQzF5ZmZ6dFlhbmF5U3U3Ym5CT3BEQ2ttNwo5azZuTlBwZ1luRWU3U1QzL2l1N0xIUWI0bzdKSlFnVkZhNWhON3Bqa1R0QU11ZE5scVByNEp5SCs0dDU0RzVTCndjUk5vYjg1dVFFS0dldnRDYzlFcmROcEZJREpwREViWUhKZjVQWHZ0UTQ5bW9mc09GS2hxVzdyMCtJTWUwZmoKZUZPYUg5MXdOVEdyRnJpTEgyQ2s2Mk1ZYmpRcGVjRjEydkFRTzdvRitkd1BlOWVYLzJPNWFsOWdDcXhtdGFVTwpFbXBGWktCYUxIUHFSbVNzbEhVUW14R3A0RlJ5S21uVnE4ZHR3Rm00TGJ1dHhVckZYOWt4N0FENkFoZUJITTRCCkd2eEZ1YTV1YTlQdjVHWGdTaVE5aXp6Y2l2ZE5RMm45a1QxUGFBenV0cDhnbFZVYkFnTUJBQUdqUWpCQU1BNEcKQTFVZER3RUIvd1FFQXdJQ0JEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCUVJIMDV0OWtkcApueS9wWmlTams1bzZTajZ6eURBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQXpHR1NjVGt3c01IcVBhU3RONVJ2CnJBNmVRVENuS2lOdDdiQjVBYjJwZEdrMHc2V1Y1RkZVdDZZdFRZK1BjMlY1RXAyd0JwaUxST1EzQnVBS3JFRnoKYW10dWtsb1pSWFZHTm14emcwYUNkNGxpYVNhZTM2aTJaN0NZczZZWnZSMGhJNWtuTHd0WTNRbzBOM3UyUVQzYQo4blJ0bjZqNjhadlQxV2xWazVFTFJZTEVYQzFtK3JQcEZXU0xoQkJ2N3lZR29qd0tTUFV5ZXV4YmpBc3pUR3VrCnQ1anNybEpBcFhBUDkyNTRFdFFWUjltY24ySThQd0lTVnpFUzlDRUtXT0hGUWdwS1BpVFM2SFlOWWUwSzZxQmcKWlBwaHFPNUJKRnVNVnhXWlNEMkZPYUQybmNoMm80KzJiMDF1ZTRvaDRnMWkyRWlZZ1FjcjRmRDA5V3Q4Q0NsYwpIQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + }, + "private_key": { + "inline_bytes": "[redacted]" + } + } + }, + "last_updated": "2022-05-24T17:41:59.078Z" + } + ], + "dynamic_warming_secrets": [ + { + "name": "ROOTCA", + "last_updated": "2022-03-15T05:14:22.868Z", + "secret": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret", + "name": "ROOTCA", + "validation_context": { + "trusted_ca": { + "inline_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvVENDQWVXZ0F3SUJBZ0lSQUtiK3haaWRxNm0xRW5ZaU5ZbFVwVmN3RFFZSktvWklodmNOQVFFTEJRQXcKR0RFV01CUUdBMVVFQ2hNTlkyeDFjM1JsY2k1c2IyTmhiREFlRncweU1qQXpNVFV3TlRFeE1UWmFGdzB6TWpBegpNVEl3TlRFeE1UWmFNQmd4RmpBVUJnTlZCQW9URFdOc2RYTjBaWEl1Ykc5allXd3dnZ0VpTUEwR0NTcUdTSWIzCkRRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRFJZUTVaMDBpODFXZkMxeWZmenRZYW5heVN1N2JuQk9wRENrbTcKOWs2bk5QcGdZbkVlN1NUMy9pdTdMSFFiNG83SkpRZ1ZGYTVoTjdwamtUdEFNdWRObHFQcjRKeUgrNHQ1NEc1Uwp3Y1JOb2I4NXVRRUtHZXZ0Q2M5RXJkTnBGSURKcERFYllISmY1UFh2dFE0OW1vZnNPRktocVc3cjArSU1lMGZqCmVGT2FIOTF3TlRHckZyaUxIMkNrNjJNWWJqUXBlY0YxMnZBUU83b0YrZHdQZTllWC8yTzVhbDlnQ3F4bXRhVU8KRW1wRlpLQmFMSFBxUm1Tc2xIVVFteEdwNEZSeUttblZxOGR0d0ZtNExidXR4VXJGWDlreDdBRDZBaGVCSE00QgpHdnhGdWE1dWE5UHY1R1hnU2lROWl6emNpdmROUTJuOWtUMVBhQXp1dHA4Z2xWVWJBZ01CQUFHalFqQkFNQTRHCkExVWREd0VCL3dRRUF3SUNCREFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlFSSDA1dDlrZHAKbnkvcFppU2prNW82U2o2enlEQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUF6R0dTY1Rrd3NNSHFQYVN0TjVSdgpyQTZlUVRDbktpTnQ3YkI1QWIycGRHazB3NldWNUZGVXQ2WXRUWStQYzJWNUVwMndCcGlMUk9RM0J1QUtyRUZ6CmFtdHVrbG9aUlhWR05teHpnMGFDZDRsaWFTYWUzNmkyWjdDWXM2WVp2UjBoSTVrbkx3dFkzUW8wTjN1MlFUM2EKOG5SdG42ajY4WnZUMVdsVms1RUxSWUxFWEMxbStyUHBGV1NMaEJCdjd5WUdvandLU1BVeWV1eGJqQXN6VEd1awp0NWpzcmxKQXBYQVA5MjU0RXRRVlI5bWNuMkk4UHdJU1Z6RVM5Q0VLV09IRlFncEtQaVRTNkhZTlllMEs2cUJnClpQcGhxTzVCSkZ1TVZ4V1pTRDJGT2FEMm5jaDJvNCsyYjAxdWU0b2g0ZzFpMkVpWWdRY3I0ZkQwOVd0OENDbGMKSEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t" + } + } + } + } + ] + } + ] +} diff --git a/cli/cmd/status/status.go b/cli/cmd/status/status.go index 5c148e9921..926d8d790b 100644 --- a/cli/cmd/status/status.go +++ b/cli/cmd/status/status.go @@ -52,9 +52,6 @@ func (c *Command) init() { }) c.help = c.set.Help() - - // c.Init() calls the embedded BaseCommand's initialization function. - c.Init() } // Run checks the status of a Consul installation on Kubernetes. @@ -152,33 +149,10 @@ func (c *Command) checkHelmInstallation(settings *helmCLI.EnvSettings, uiLogger timezone, _ := rel.Info.LastDeployed.Zone() - tbl := terminal.NewTable([]string{"Name", "Namespace", "Status", "Chart Version", "AppVersion", "Revision", "Last Updated"}...) - trow := []terminal.TableEntry{ - { - Value: releaseName, - }, - { - Value: namespace, - }, - { - Value: string(rel.Info.Status), - }, - { - Value: rel.Chart.Metadata.Version, - }, - { - Value: rel.Chart.Metadata.AppVersion, - }, - { - Value: strconv.Itoa(rel.Version), - }, - { - Value: rel.Info.LastDeployed.Format("2006/01/02 15:04:05") + " " + timezone, - }, - } - tbl.Rows = [][]terminal.TableEntry{} - tbl.Rows = append(tbl.Rows, trow) - + tbl := terminal.NewTable("Name", "Namespace", "Status", "Chart Version", "AppVersion", "Revision", "Last Updated") + tbl.AddRow([]string{releaseName, namespace, string(rel.Info.Status), rel.Chart.Metadata.Version, + rel.Chart.Metadata.AppVersion, strconv.Itoa(rel.Version), + rel.Info.LastDeployed.Format("2006/01/02 15:04:05") + " " + timezone}, []string{}) c.UI.Table(tbl) valuesYaml, err := yaml.Marshal(rel.Config) diff --git a/cli/cmd/uninstall/uninstall.go b/cli/cmd/uninstall/uninstall.go index 288f97823b..01a442226d 100644 --- a/cli/cmd/uninstall/uninstall.go +++ b/cli/cmd/uninstall/uninstall.go @@ -105,9 +105,6 @@ func (c *Command) init() { }) c.help = c.set.Help() - - // c.Init() calls the embedded BaseCommand's initialization function. - c.Init() } func (c *Command) Run(args []string) int { diff --git a/cli/cmd/uninstall/uninstall_test.go b/cli/cmd/uninstall/uninstall_test.go index 8f1df8de80..7d36ce5d3d 100644 --- a/cli/cmd/uninstall/uninstall_test.go +++ b/cli/cmd/uninstall/uninstall_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" @@ -356,6 +357,7 @@ func getInitializedCommand(t *testing.T) *Command { baseCommand := &common.BaseCommand{ Log: log, + UI: terminal.NewBasicUI(context.TODO()), } c := &Command{ diff --git a/cli/cmd/upgrade/upgrade.go b/cli/cmd/upgrade/upgrade.go index 4ef26661e8..e145e238e1 100644 --- a/cli/cmd/upgrade/upgrade.go +++ b/cli/cmd/upgrade/upgrade.go @@ -157,9 +157,6 @@ func (c *Command) init() { }) c.help = c.set.Help() - - // c.Init() calls the embedded BaseCommand's initialization function. - c.Init() } func (c *Command) Run(args []string) int { diff --git a/cli/cmd/version/version.go b/cli/cmd/version/version.go index b8b2235f46..f31fa711f1 100644 --- a/cli/cmd/version/version.go +++ b/cli/cmd/version/version.go @@ -1,8 +1,6 @@ package version import ( - "sync" - "github.com/hashicorp/consul-k8s/cli/common" "github.com/hashicorp/consul-k8s/cli/common/terminal" ) @@ -12,17 +10,10 @@ type Command struct { // Version is the Consul on Kubernetes CLI version. Version string - - once sync.Once -} - -func (c *Command) init() { - c.Init() } // Run prints the version of the Consul on Kubernetes CLI. func (c *Command) Run(_ []string) int { - c.once.Do(c.init) c.UI.Output("consul-k8s %s", c.Version, terminal.WithInfoStyle()) return 0 } diff --git a/cli/commands.go b/cli/commands.go index 681421c502..d29b4a8ad7 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -4,11 +4,15 @@ import ( "context" "github.com/hashicorp/consul-k8s/cli/cmd/install" + "github.com/hashicorp/consul-k8s/cli/cmd/proxy" + "github.com/hashicorp/consul-k8s/cli/cmd/proxy/list" + "github.com/hashicorp/consul-k8s/cli/cmd/proxy/read" "github.com/hashicorp/consul-k8s/cli/cmd/status" "github.com/hashicorp/consul-k8s/cli/cmd/uninstall" "github.com/hashicorp/consul-k8s/cli/cmd/upgrade" cmdversion "github.com/hashicorp/consul-k8s/cli/cmd/version" "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/version" "github.com/hashicorp/go-hclog" "github.com/mitchellh/cli" @@ -19,6 +23,7 @@ func initializeCommands(ctx context.Context, log hclog.Logger) (*common.BaseComm baseCommand := &common.BaseCommand{ Ctx: ctx, Log: log, + UI: terminal.NewBasicUI(ctx), } commands := map[string]cli.CommandFactory{ @@ -48,6 +53,21 @@ func initializeCommands(ctx context.Context, log hclog.Logger) (*common.BaseComm Version: version.GetHumanVersion(), }, nil }, + "proxy": func() (cli.Command, error) { + return &proxy.ProxyCommand{ + BaseCommand: baseCommand, + }, nil + }, + "proxy list": func() (cli.Command, error) { + return &list.ListCommand{ + BaseCommand: baseCommand, + }, nil + }, + "proxy read": func() (cli.Command, error) { + return &read.ReadCommand{ + BaseCommand: baseCommand, + }, nil + }, } return baseCommand, commands diff --git a/cli/common/base.go b/cli/common/base.go index 81bb360267..2237b66f84 100644 --- a/cli/common/base.go +++ b/cli/common/base.go @@ -36,9 +36,3 @@ func (c *BaseCommand) Close() error { return nil } - -// Init should be called FIRST within the Run function implementation. -func (c *BaseCommand) Init() { - ui := terminal.NewBasicUI(c.Ctx) - c.UI = ui -} diff --git a/cli/common/flag/set.go b/cli/common/flag/set.go index df44e7031c..94c8e184c2 100644 --- a/cli/common/flag/set.go +++ b/cli/common/flag/set.go @@ -56,6 +56,22 @@ func (f *Sets) NewSet(name string) *Set { return flagSet } +// GetSetFlags returns a slice of flags for a given set. +// If the requested set does not exist, this will return an empty slice. +func (f *Sets) GetSetFlags(setName string) []string { + var setFlags []string + for _, set := range f.flagSets { + if set.name == setName { + set.flagSet.VisitAll(func(f *flag.Flag) { + setFlags = append(setFlags, fmt.Sprintf("-%s", f.Name)) + }) + return setFlags + } + } + + return setFlags +} + // Completions returns the completions for this flag set. func (f *Sets) Completions() complete.Flags { return f.completions diff --git a/cli/common/portforward.go b/cli/common/portforward.go new file mode 100644 index 0000000000..a2acd02d27 --- /dev/null +++ b/cli/common/portforward.go @@ -0,0 +1,141 @@ +package common + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "time" + + "k8s.io/apimachinery/pkg/util/httpstream" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" +) + +// PortForward represents a Kubernetes Pod port forwarding session which can be +// run as a background process. +type PortForward struct { + // Namespace is the Kubernetes Namespace where the Pod can be found. + Namespace string + // PodName is the name of the Pod to port forward. + PodName string + // RemotePort is the port on the Pod to forward to. + RemotePort int + + // KubeClient is the Kubernetes Client to use for port forwarding. + KubeClient kubernetes.Interface + // RestConfig is the REST client configuration to use for port forwarding. + RestConfig *rest.Config + + localPort int + stopChan chan struct{} + readyChan chan struct{} + + portForwardURL *url.URL + newForwarder func(httpstream.Dialer, []string, <-chan struct{}, chan struct{}, io.Writer, io.Writer) (forwarder, error) +} + +// PortForwarder enables a user to open and close a connection to a remote server. +type PortForwarder interface { + Open(context.Context) (string, error) + Close() +} + +// forwarder is an interface which can be used for opening a port forward session. +type forwarder interface { + ForwardPorts() error + Close() + GetPorts() ([]portforward.ForwardedPort, error) +} + +// Open opens a port forward session to a Kubernetes Pod. +func (pf *PortForward) Open(ctx context.Context) (string, error) { + // Get an open port on localhost. + if err := pf.allocateLocalPort(); err != nil { + return "", fmt.Errorf("failed to allocate local port: %v", err) + } + + // Configure the URL for starting the port forward. + if pf.portForwardURL == nil { + pf.portForwardURL = pf.KubeClient.CoreV1().RESTClient().Post().Resource("pods").Namespace(pf.Namespace). + Name(pf.PodName).SubResource("portforward").URL() + } + + // Create a dialer for the port forward target. + transport, upgrader, err := spdy.RoundTripperFor(pf.RestConfig) + if err != nil { + return "", fmt.Errorf("failed to create roundtripper: %v", err) + } + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", pf.portForwardURL) + + // Create channels for Goroutines to communicate. + pf.stopChan = make(chan struct{}, 1) + pf.readyChan = make(chan struct{}, 1) + errChan := make(chan error) + + // Use the default Kubernetes port forwarder if none is specified. + if pf.newForwarder == nil { + pf.newForwarder = newDefaultForwarder + } + + // Create a Kubernetes port forwarder. + ports := []string{fmt.Sprintf("%d:%d", pf.localPort, pf.RemotePort)} + portforwarder, err := pf.newForwarder(dialer, ports, pf.stopChan, pf.readyChan, nil, nil) + if err != nil { + return "", err + } + + // Start port forwarding. + go func() { + errChan <- portforwarder.ForwardPorts() + }() + + select { + case <-pf.readyChan: + return fmt.Sprintf("localhost:%d", pf.localPort), nil + case err := <-errChan: + return "", err + case <-ctx.Done(): + pf.Close() + return "", fmt.Errorf("port forward cancelled") + case <-time.After(time.Second * 5): + pf.Close() + return "", fmt.Errorf("port forward timed out") + } +} + +// Close closes the port forward connection. +func (pf *PortForward) Close() { + close(pf.stopChan) +} + +// allocateLocalPort looks for an open port on localhost and sets it to the +// localPort field. +func (pf *PortForward) allocateLocalPort() error { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return err + } + + _, port, err := net.SplitHostPort(listener.Addr().String()) + if err != nil { + return err + } + + if err := listener.Close(); err != nil { + return fmt.Errorf("unable to close listener %v", err) + } + + pf.localPort, err = strconv.Atoi(port) + return err +} + +// newDefaultForwarder creates a new Kubernetes port forwarder. +func newDefaultForwarder(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (forwarder, error) { + return portforward.New(dialer, ports, stopChan, readyChan, out, errOut) +} diff --git a/cli/common/portforward_test.go b/cli/common/portforward_test.go new file mode 100644 index 0000000000..9bf7cf8e9d --- /dev/null +++ b/cli/common/portforward_test.go @@ -0,0 +1,127 @@ +package common + +import ( + "context" + "fmt" + "io" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/httpstream" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/portforward" +) + +type mockForwarder struct { + forwardBehavior func() error +} + +func (m *mockForwarder) ForwardPorts() error { return m.forwardBehavior() } +func (m *mockForwarder) Close() {} +func (m *mockForwarder) GetPorts() ([]portforward.ForwardedPort, error) { return nil, nil } + +func TestPortForwardingSuccess(t *testing.T) { + mockForwarder := &mockForwarder{ + forwardBehavior: func() error { return nil }, + } + + newMockForwarder := func(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (forwarder, error) { + close(readyChan) + return mockForwarder, nil + } + + pf := &PortForward{ + KubeClient: fake.NewSimpleClientset(), + RestConfig: &rest.Config{}, + portForwardURL: &url.URL{}, + newForwarder: newMockForwarder, + } + + endpoint, err := pf.Open(context.Background()) + defer pf.Close() + + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("localhost:%d", pf.localPort), endpoint) +} + +func TestPortForwardingError(t *testing.T) { + mockForwarder := &mockForwarder{ + forwardBehavior: func() error { + return fmt.Errorf("error") + }, + } + + newMockForwarder := func(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (forwarder, error) { + return mockForwarder, nil + } + + pf := &PortForward{ + KubeClient: fake.NewSimpleClientset(), + RestConfig: &rest.Config{}, + portForwardURL: &url.URL{}, + newForwarder: newMockForwarder, + } + + endpoint, err := pf.Open(context.Background()) + defer pf.Close() + + require.Error(t, err) + require.Equal(t, "error", err.Error()) + require.Equal(t, "", endpoint) +} + +func TestPortForwardingContextCancel(t *testing.T) { + mockForwarder := &mockForwarder{ + forwardBehavior: func() error { + return nil + }, + } + + newMockForwarder := func(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (forwarder, error) { + return mockForwarder, nil + } + + pf := &PortForward{ + KubeClient: fake.NewSimpleClientset(), + RestConfig: &rest.Config{}, + portForwardURL: &url.URL{}, + newForwarder: newMockForwarder, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + endpoint, err := pf.Open(ctx) + + require.Error(t, err) + require.Equal(t, "port forward cancelled", err.Error()) + require.Equal(t, "", endpoint) +} + +func TestPortForwardingTimeout(t *testing.T) { + mockForwarder := &mockForwarder{ + forwardBehavior: func() error { + time.Sleep(time.Second * 10) + return nil + }, + } + + newMockForwarder := func(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (forwarder, error) { + return mockForwarder, nil + } + + pf := &PortForward{ + KubeClient: fake.NewSimpleClientset(), + RestConfig: &rest.Config{}, + portForwardURL: &url.URL{}, + newForwarder: newMockForwarder, + } + + endpoint, err := pf.Open(context.Background()) + + require.Error(t, err) + require.Equal(t, "port forward timed out", err.Error()) + require.Equal(t, "", endpoint) +} diff --git a/cli/common/terminal/basic.go b/cli/common/terminal/basic.go index d06815b8f5..e8411b9aec 100644 --- a/cli/common/terminal/basic.go +++ b/cli/common/terminal/basic.go @@ -15,15 +15,22 @@ import ( "github.com/mattn/go-isatty" ) -// basicUI. +// basicUI is a standard implementation of the UI interface for the terminal. type basicUI struct { - ctx context.Context + ctx context.Context + bufOut io.Writer } +// NewBasicUI creates a new instance of the basicUI struct for the terminal +// with color output. func NewBasicUI(ctx context.Context) *basicUI { - return &basicUI{ - ctx: ctx, - } + return NewUI(ctx, color.Output) +} + +// NewUI creates a new instance of the basicUI struct that outputs to +// the bufOut buffer. +func NewUI(ctx context.Context, bufOut io.Writer) *basicUI { + return &basicUI{ctx: ctx, bufOut: bufOut} } // Input implements UI. @@ -32,8 +39,8 @@ func (ui *basicUI) Input(input *Input) (string, error) { // Write the prompt, add a space. ui.Output(input.Prompt, WithStyle(input.Style), WithWriter(&buf)) - fmt.Fprint(color.Output, strings.TrimRight(buf.String(), "\r\n")) - fmt.Fprint(color.Output, " ") + fmt.Fprint(ui.bufOut, strings.TrimRight(buf.String(), "\r\n")) + fmt.Fprint(ui.bufOut, " ") // Ask for input in a go-routine so that we can ignore it. errCh := make(chan error, 1) @@ -62,7 +69,7 @@ func (ui *basicUI) Input(input *Input) (string, error) { return line, nil case <-ui.ctx.Done(): // Print newline so that any further output starts properly - fmt.Fprintln(color.Output) + fmt.Fprintln(ui.bufOut) return "", ui.ctx.Err() } } @@ -74,7 +81,7 @@ func (ui *basicUI) Interactive() bool { // Output implements UI. func (ui *basicUI) Output(msg string, raw ...interface{}) { - msg, style, w := Interpret(msg, raw...) + msg, style, w := ui.parse(msg, raw...) switch style { case HeaderStyle: @@ -114,7 +121,10 @@ func (ui *basicUI) Output(msg string, raw ...interface{}) { // NamedValues implements UI. func (ui *basicUI) NamedValues(rows []NamedValue, opts ...Option) { - cfg := &config{Writer: color.Output} + cfg := &config{ + Writer: ui.bufOut, + Style: "", + } for _, opt := range opts { opt(cfg) } @@ -147,3 +157,31 @@ func (ui *basicUI) NamedValues(rows []NamedValue, opts ...Option) { func (ui *basicUI) OutputWriters() (io.Writer, io.Writer, error) { return os.Stdout, os.Stderr, nil } + +// parse decomposes the msg and arguments into the message, style, and writer. +func (ui *basicUI) parse(msg string, raw ...interface{}) (string, string, io.Writer) { + // Build our args and options + var args []interface{} + var opts []Option + for _, r := range raw { + if opt, ok := r.(Option); ok { + opts = append(opts, opt) + } else { + args = append(args, r) + } + } + + // Build our message + msg = fmt.Sprintf(msg, args...) + + // Build our config and set our options + cfg := &config{ + Writer: ui.bufOut, + Style: "", + } + for _, opt := range opts { + opt(cfg) + } + + return msg, cfg.Style, cfg.Writer +} diff --git a/cli/common/terminal/table.go b/cli/common/terminal/table.go index c8e8c2c67b..67278931a4 100644 --- a/cli/common/terminal/table.go +++ b/cli/common/terminal/table.go @@ -1,14 +1,31 @@ package terminal import ( - "github.com/fatih/color" "github.com/olekukonko/tablewriter" ) +const ( + Yellow = "yellow" + Green = "green" + Red = "red" +) + +var colorMapping = map[string]int{ + Green: tablewriter.FgGreenColor, + Yellow: tablewriter.FgYellowColor, + Red: tablewriter.FgRedColor, +} + // Passed to UI.Table to provide a nicely formatted table. type Table struct { Headers []string - Rows [][]TableEntry + Rows [][]Cell +} + +// Cell is a single entry for a table. +type Cell struct { + Value string + Color string } // Table creates a new Table structure that can be used with UI.Table. @@ -18,21 +35,15 @@ func NewTable(headers ...string) *Table { } } -// TableEntry is a single entry for a table. -type TableEntry struct { - Value string - Color string -} - -// Rich adds a row to the table. -func (t *Table) Rich(cols []string, colors []string) { - var row []TableEntry +// AddRow adds a row to the table. +func (t *Table) AddRow(cols []string, colors []string) { + var row []Cell for i, col := range cols { if i < len(colors) { - row = append(row, TableEntry{Value: col, Color: colors[i]}) + row = append(row, Cell{Value: col, Color: colors[i]}) } else { - row = append(row, TableEntry{Value: col}) + row = append(row, Cell{Value: col}) } } @@ -42,7 +53,7 @@ func (t *Table) Rich(cols []string, colors []string) { // Table implements UI. func (u *basicUI) Table(tbl *Table, opts ...Option) { // Build our config and set our options - cfg := &config{Writer: color.Output} + cfg := &config{Writer: u.bufOut} for _, opt := range opts { opt(cfg) } @@ -52,20 +63,15 @@ func (u *basicUI) Table(tbl *Table, opts ...Option) { table.SetHeader(tbl.Headers) table.SetBorder(false) table.SetAutoWrapText(false) - - if cfg.Style == "Simple" { - // Format the table without borders, simple output - - table.SetAutoFormatHeaders(true) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetTablePadding("\t") // pad with tabs - table.SetNoWhiteSpace(true) - } + table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) for _, row := range tbl.Rows { colors := make([]tablewriter.Colors, len(row)) @@ -85,15 +91,3 @@ func (u *basicUI) Table(tbl *Table, opts ...Option) { table.Render() } - -const ( - Yellow = "yellow" - Green = "green" - Red = "red" -) - -var colorMapping = map[string]int{ - Green: tablewriter.FgGreenColor, - Yellow: tablewriter.FgYellowColor, - Red: tablewriter.FgRedColor, -} diff --git a/cli/common/terminal/ui.go b/cli/common/terminal/ui.go index a9baa7aa87..b90d05b197 100644 --- a/cli/common/terminal/ui.go +++ b/cli/common/terminal/ui.go @@ -2,7 +2,6 @@ package terminal import ( "errors" - "fmt" "io" "github.com/fatih/color" @@ -66,31 +65,6 @@ type Input struct { Secret bool } -// Interpret decomposes the msg and arguments into the message, style, and writer. -func Interpret(msg string, raw ...interface{}) (string, string, io.Writer) { - // Build our args and options - var args []interface{} - var opts []Option - for _, r := range raw { - if opt, ok := r.(Option); ok { - opts = append(opts, opt) - } else { - args = append(args, r) - } - } - - // Build our message - msg = fmt.Sprintf(msg, args...) - - // Build our config and set our options - cfg := &config{Writer: color.Output} - for _, opt := range opts { - opt(cfg) - } - - return msg, cfg.Style, cfg.Writer -} - const ( HeaderStyle = "header" ErrorStyle = "error" diff --git a/cli/go.mod b/cli/go.mod index c8807e4be5..00c718a139 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -20,6 +20,7 @@ require ( k8s.io/apimachinery v0.22.2 k8s.io/cli-runtime v0.21.0 k8s.io/client-go v0.22.2 + k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a sigs.k8s.io/yaml v1.2.0 ) @@ -140,8 +141,8 @@ require ( golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect - google.golang.org/grpc v1.33.1 // indirect - google.golang.org/protobuf v1.26.0 // indirect + google.golang.org/grpc v1.36.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/gorp.v1 v1.7.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -152,7 +153,6 @@ require ( k8s.io/klog/v2 v2.9.0 // indirect k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect k8s.io/kubectl v0.21.0 // indirect - k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect rsc.io/letsencrypt v0.0.3 // indirect sigs.k8s.io/kustomize/api v0.8.5 // indirect sigs.k8s.io/kustomize/kyaml v0.10.15 // indirect diff --git a/cli/go.sum b/cli/go.sum index c24ee30004..d65d9b4ff1 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -144,7 +144,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk= @@ -225,7 +225,7 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -1129,8 +1129,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1142,8 +1142,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=