From 40af0f8a9ceb804d25dc0df4f59d1b63389a62d5 Mon Sep 17 00:00:00 2001 From: Andrew Reed Date: Thu, 11 Feb 2021 23:11:29 +0000 Subject: [PATCH] Analyze TCP connection --- examples/preflight/host-tcp-connect.yaml | 25 +++++ pkg/analyze/analyzer.go | 8 +- pkg/analyze/host_tcp_connect.go | 86 +++++++++++++++++ pkg/analyze/host_tcp_connect_test.go | 92 +++++++++++++++++++ .../v1beta2/hostanalyzer_shared.go | 8 ++ .../v1beta2/hostcollector_shared.go | 7 ++ .../v1beta2/zz_generated.deepcopy.go | 53 +++++++++++ pkg/collect/host_collector.go | 2 + pkg/collect/host_tcp_connect.go | 56 +++++++++++ 9 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 examples/preflight/host-tcp-connect.yaml create mode 100644 pkg/analyze/host_tcp_connect.go create mode 100644 pkg/analyze/host_tcp_connect_test.go create mode 100644 pkg/collect/host_tcp_connect.go diff --git a/examples/preflight/host-tcp-connect.yaml b/examples/preflight/host-tcp-connect.yaml new file mode 100644 index 00000000..2f2572de --- /dev/null +++ b/examples/preflight/host-tcp-connect.yaml @@ -0,0 +1,25 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: connect +spec: + collectors: + - tcpConnect: + collectorName: weave host 1 + address: 10.128.0.2:6783 + analyzers: + - tcpConnect: + collectorName: weave host 1 + outcomes: + - fail: + when: "connection-refused" + message: Connection to weave on host 1 was refused + - fail: + when: "connection-timeout" + message: Timed out connecting to weave on host 1 + - fail: + when: "error" + message: Unexpected error connecting to weave on host 1 + - pass: + when: "connected" + message: Successfully connected to weave on host 1 diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index 60d907da..6f632981 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -97,7 +97,13 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle } return []*AnalyzeResult{result}, nil } - + if hostAnalyzer.TCPConnect != nil { + result, err := analyzeHostTCPConnect(hostAnalyzer.TCPConnect, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } return nil, errors.New("invalid analyzer") } diff --git a/pkg/analyze/host_tcp_connect.go b/pkg/analyze/host_tcp_connect.go new file mode 100644 index 00000000..327dc0db --- /dev/null +++ b/pkg/analyze/host_tcp_connect.go @@ -0,0 +1,86 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +func analyzeHostTCPConnect(hostAnalyzer *troubleshootv1beta2.TCPConnectAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + fullPath := path.Join("connect", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName)) + + collected, err := getCollectedFileContents(fullPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath) + } + actual := collect.NetworkStatusResult{} + if err := json.Unmarshal(collected, &actual); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal collected") + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = "TCP Connection Attempt" + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Fail.When { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Warn.When { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Pass.When { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} diff --git a/pkg/analyze/host_tcp_connect_test.go b/pkg/analyze/host_tcp_connect_test.go new file mode 100644 index 00000000..6cc8064c --- /dev/null +++ b/pkg/analyze/host_tcp_connect_test.go @@ -0,0 +1,92 @@ +package analyzer + +import ( + "encoding/json" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAnalyzeTCPConnect(t *testing.T) { + tests := []struct { + name string + info *collect.NetworkStatusResult + hostAnalyzer *troubleshootv1beta2.TCPConnectAnalyze + result *AnalyzeResult + expectErr bool + }{ + { + name: "connection refused, fail", + info: &collect.NetworkStatusResult{ + Status: collect.NetworkStatusConnectionRefused, + }, + hostAnalyzer: &troubleshootv1beta2.TCPConnectAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "connection-refused", + Message: "Connection was refused", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "TCP Connection Attempt", + IsFail: true, + Message: "Connection was refused", + }, + }, + { + name: "connected, fail", + info: &collect.NetworkStatusResult{ + Status: collect.NetworkStatusConnected, + }, + hostAnalyzer: &troubleshootv1beta2.TCPConnectAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "connection-refused", + Message: "Connection was refused", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "connected", + Message: "Connection was successful", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "TCP Connection Attempt", + IsPass: true, + Message: "Connection was successful", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + b, err := json.Marshal(test.info) + if err != nil { + t.Fatal(err) + } + + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + + result, err := analyzeHostTCPConnect(test.hostAnalyzer, getCollectedFileContents) + if test.expectErr { + req.Error(err) + } else { + req.NoError(err) + } + + assert.Equal(t, test.result, result) + }) + } +} diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go index 915a1d8d..5ec7c9f0 100644 --- a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go @@ -44,6 +44,12 @@ type BlockDevicesAnalyze struct { Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` } +type TCPConnectAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + type HostAnalyze struct { CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"` // @@ -60,4 +66,6 @@ type HostAnalyze struct { Time *TimeAnalyze `json:"time" yaml:"time"` BlockDevices *BlockDevicesAnalyze `json:"blockDevices" yaml:"blockDevices"` + + TCPConnect *TCPConnectAnalyze `json:"tcpConnect" yaml:"tcpConnect"` } diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go index b912724a..81d9295b 100644 --- a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go @@ -67,6 +67,12 @@ type HostBlockDevices struct { HostCollectorMeta `json:",inline" yaml:",inline"` } +type TCPConnect struct { + HostCollectorMeta `json:",inline" yaml:",inline"` + Address string `json:"address"` + Timeout string `json:"timeout,omitempty"` +} + type HostCollect struct { CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"` Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"` @@ -79,6 +85,7 @@ type HostCollect struct { HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"` Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"` BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"` + TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"` } func (c *HostCollect) GetName() string { diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 50a68dc6..59a84302 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -1140,6 +1140,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) { *out = new(BlockDevicesAnalyze) (*in).DeepCopyInto(*out) } + if in.TCPConnect != nil { + in, out := &in.TCPConnect, &out.TCPConnect + *out = new(TCPConnectAnalyze) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze. @@ -1226,6 +1231,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) { *out = new(HostBlockDevices) **out = **in } + if in.TCPConnect != nil { + in, out := &in.TCPConnect, &out.TCPConnect + *out = new(TCPConnect) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect. @@ -2315,6 +2325,49 @@ func (in *SupportBundleVersionSpec) DeepCopy() *SupportBundleVersionSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPConnect) DeepCopyInto(out *TCPConnect) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPConnect. +func (in *TCPConnect) DeepCopy() *TCPConnect { + if in == nil { + return nil + } + out := new(TCPConnect) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPConnectAnalyze) DeepCopyInto(out *TCPConnectAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPConnectAnalyze. +func (in *TCPConnectAnalyze) DeepCopy() *TCPConnectAnalyze { + if in == nil { + return nil + } + out := new(TCPConnectAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TCPLoadBalancer) DeepCopyInto(out *TCPLoadBalancer) { *out = *in diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go index 37dbd25a..544512a8 100644 --- a/pkg/collect/host_collector.go +++ b/pkg/collect/host_collector.go @@ -34,6 +34,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error) result, err = HostTime(c) } else if c.Collect.BlockDevices != nil { result, err = HostBlockDevices(c) + } else if c.Collect.TCPConnect != nil { + result, err = HostTCPConnect(c) } else { err = errors.New("no spec found to run") return diff --git a/pkg/collect/host_tcp_connect.go b/pkg/collect/host_tcp_connect.go new file mode 100644 index 00000000..1e1ce03a --- /dev/null +++ b/pkg/collect/host_tcp_connect.go @@ -0,0 +1,56 @@ +package collect + +import ( + "encoding/json" + "fmt" + "net" + "path" + "strings" + "time" + + "github.com/pkg/errors" +) + +func HostTCPConnect(c *HostCollector) (map[string][]byte, error) { + address := c.Collect.TCPConnect.Address + + timeout := 10 * time.Second + if c.Collect.TCPConnect.Timeout != "" { + var err error + timeout, err = time.ParseDuration(c.Collect.TCPConnect.Timeout) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse timeout %q", c.Collect.TCPConnect.Timeout) + } + } + + result := NetworkStatusResult{ + Status: attemptConnect(address, timeout), + } + + b, err := json.Marshal(result) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal result") + } + + name := path.Join("connect", fmt.Sprintf("%s.json", c.Collect.TCPConnect.CollectorName)) + + return map[string][]byte{ + name: b, + }, nil +} + +func attemptConnect(address string, timeout time.Duration) NetworkStatus { + conn, err := net.DialTimeout("tcp", address, timeout) + if err != nil { + if strings.Contains(err.Error(), "i/o timeout") { + return NetworkStatusConnectionTimeout + } + if strings.Contains(err.Error(), "connection refused") { + return NetworkStatusConnectionRefused + } + return NetworkStatusErrorOther + } + + conn.Close() + return NetworkStatusConnected +}