Repository: replicatedhq/troubleshoot
Pull request #317 (2021-02-11 23:14:13)

Analyze TCP connection


Patch:
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/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
+}


Test patch:
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)
+		})
+	}
+}