diff --git a/command/troubleshoot/proxy/troubleshoot_proxy.go b/command/troubleshoot/proxy/troubleshoot_proxy.go index 8dde79300b..986a5d14c4 100644 --- a/command/troubleshoot/proxy/troubleshoot_proxy.go +++ b/command/troubleshoot/proxy/troubleshoot_proxy.go @@ -81,7 +81,10 @@ func (c *cmd) Run(args []string) int { c.UI.Error("error running the tests: " + err.Error()) return 1 } - c.UI.Output(output) + + for _, o := range output { + c.UI.Output(o) + } return 0 } @@ -97,6 +100,7 @@ const ( synopsis = "Troubleshoots service mesh issues from the current envoy instance" help = ` Usage: consul troubleshoot proxy [options] + Connects to local envoy proxy and troubleshoots service mesh communication issues. Requires an upstream service envoy identifier. Examples: diff --git a/troubleshoot/certs.go b/troubleshoot/certs.go new file mode 100644 index 0000000000..1056fd69fc --- /dev/null +++ b/troubleshoot/certs.go @@ -0,0 +1,54 @@ +package troubleshoot + +import ( + "fmt" + "time" + + envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" +) + +func (t *Troubleshoot) validateCerts(certs *envoy_admin_v3.Certificates) error { + + // TODO: we can probably warn if the expiration date is close + var resultErr error + now := time.Now() + + for _, cert := range certs.GetCertificates() { + for _, cacert := range cert.GetCaCert() { + if now.After(cacert.GetExpirationTime().AsTime()) { + resultErr = multierror.Append(resultErr, errors.New("Ca cert is expired")) + } + + } + for _, cc := range cert.GetCertChain() { + if now.After(cc.GetExpirationTime().AsTime()) { + resultErr = multierror.Append(resultErr, errors.New("cert chain is expired")) + } + } + } + return resultErr +} + +func (t *Troubleshoot) getEnvoyCerts() (*envoy_admin_v3.Certificates, error) { + + certsRaw, err := t.request("certs?format=json") + if err != nil { + return nil, fmt.Errorf("error in requesting Envoy Admin API /certs endpoint: %w", err) + } + + certs := &envoy_admin_v3.Certificates{} + + unmarshal := &protojson.UnmarshalOptions{ + DiscardUnknown: true, + } + err = unmarshal.Unmarshal(certsRaw, certs) + if err != nil { + return nil, fmt.Errorf("error in unmarshalling /certs response: %w", err) + } + + t.envoyCerts = certs + return certs, nil +} diff --git a/troubleshoot/stats.go b/troubleshoot/stats.go new file mode 100644 index 0000000000..1b00765f00 --- /dev/null +++ b/troubleshoot/stats.go @@ -0,0 +1,47 @@ +package troubleshoot + +import ( + "encoding/json" + "fmt" + + envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" +) + +type statsJson struct { + Stats []simpleMetric `json:"stats"` +} + +type simpleMetric struct { + Value int64 `json:"value,omitempty"` + Name string `json:"name,omitempty"` +} + +func (t *Troubleshoot) getEnvoyStats(filter string) ([]*envoy_admin_v3.SimpleMetric, error) { + + var resultErr error + + jsonRaw, err := t.request(fmt.Sprintf("stats?format=json&filter=%s&type=Counters", filter)) + if err != nil { + return nil, fmt.Errorf("error in requesting envoy Admin API /stats endpoint: %w", err) + } + + var rawStats statsJson + + err = json.Unmarshal(jsonRaw, &rawStats) + if err != nil { + return nil, fmt.Errorf("could not unmarshal /stats response: %w", err) + } + + stats := []*envoy_admin_v3.SimpleMetric{} + + for _, s := range rawStats.Stats { + stat := &envoy_admin_v3.SimpleMetric{ + Value: uint64(s.Value), + Name: s.Name, + } + stats = append(stats, stat) + } + + t.envoyStats = stats + return stats, resultErr +} diff --git a/troubleshoot/troubleshoot_proxy.go b/troubleshoot/troubleshoot_proxy.go index 1421bc8f06..68101e98f0 100644 --- a/troubleshoot/troubleshoot_proxy.go +++ b/troubleshoot/troubleshoot_proxy.go @@ -5,8 +5,10 @@ import ( "net" envoy_admin_v3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" + "github.com/pkg/errors" "github.com/hashicorp/consul/api" + "github.com/hashicorp/go-multierror" ) type Troubleshoot struct { @@ -21,11 +23,9 @@ type TroubleshootInfo struct { envoyClusters *envoy_admin_v3.Clusters envoyConfigDump *envoy_admin_v3.ConfigDump envoyCerts *envoy_admin_v3.Certificates - envoyStats EnvoyStats + envoyStats []*envoy_admin_v3.SimpleMetric } -type EnvoyStats []envoy_admin_v3.SimpleMetric - func NewTroubleshoot(envoyIP *net.IPAddr, envoyPort string) (*Troubleshoot, error) { cfg := api.DefaultConfig() c, err := api.NewClient(cfg) @@ -39,8 +39,35 @@ func NewTroubleshoot(envoyIP *net.IPAddr, envoyPort string) (*Troubleshoot, erro }, nil } -func (t *Troubleshoot) RunAllTests(upstreamSNI string) (string, error) { - return "", fmt.Errorf("not implemented") +func (t *Troubleshoot) RunAllTests(upstreamEnvoyID string) ([]string, error) { + var resultErr error + var output []string + + certs, err := t.getEnvoyCerts() + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("unable to get certs: %w", err)) + } + + if certs != nil && len(certs.GetCertificates()) != 0 { + err = t.validateCerts(certs) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("unable to validate certs: %w", err)) + } else { + output = append(output, "certs are valid") + } + + } else { + resultErr = multierror.Append(resultErr, errors.New("no certificate found")) + + } + + // getStats usage example + // rejectionStats, err := t.getEnvoyStats("update_rejected") + // if err != nil { + // resultErr = multierror.Append(resultErr, err) + // } + + return output, resultErr } func (t *Troubleshoot) GetUpstreams() ([]string, error) { diff --git a/troubleshoot/utils.go b/troubleshoot/utils.go new file mode 100644 index 0000000000..767d70ae4b --- /dev/null +++ b/troubleshoot/utils.go @@ -0,0 +1,36 @@ +package troubleshoot + +import ( + "context" + "fmt" + "io" + "net/http" +) + +func (t *Troubleshoot) request(path string) ([]byte, error) { + client := &http.Client{} + url := fmt.Sprintf("http://%v:%s/%s", t.envoyAddr.IP, t.envoyAdminPort, path) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req.WithContext(context.Background())) + if err != nil { + return nil, err + } + + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("ErrBackendNotMounted") + } + + rawConfig, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return rawConfig, nil +}