Repository: replicatedhq/troubleshoot
Pull request #100 (2019-12-19 00:50:09)

Regex analyzer
(unit tests coming before this one can possibly be merged)


Patch:
diff --git a/cmd/preflight/cli/run.go b/cmd/preflight/cli/run.go
index a5f9f89a..a653d7be 100644
--- a/cmd/preflight/cli/run.go
+++ b/cmd/preflight/cli/run.go
@@ -60,7 +60,7 @@ func runPreflights(v *viper.Viper, arg string) error {
 
 	preflight := troubleshootv1beta1.Preflight{}
 	if err := yaml.Unmarshal([]byte(preflightContent), &preflight); err != nil {
-		return fmt.Errorf("unable to parse %s as a preflight", arg)
+		return fmt.Errorf("unable to parse %s as a preflight, error: %s", arg, err.Error())
 	}
 
 	s := spin.New()
diff --git a/examples/preflight/postgres-connection-valid.yaml b/examples/preflight/postgres-connection-valid.yaml
new file mode 100644
index 00000000..5a95796c
--- /dev/null
+++ b/examples/preflight/postgres-connection-valid.yaml
@@ -0,0 +1,24 @@
+apiVersion: troubleshoot.replicated.com/v1beta1
+kind: Preflight
+metadata:
+  name: postgres-connection-valid
+spec:
+  collectors:
+    - run:
+        collectorName: "run-ping"
+        image: busybox:1
+        namespace: default
+        command: [ping]
+        args: [-w, "5", www.google.com]
+        imagePullPolicy: IfNotPresent
+  analyzers:
+    - regex:
+        collectorName: "run-ping"
+        checkName: "Ping google with low packet loss?"
+        expression: "(?P<Transmitted>\\d+) packets? transmitted, (?P<Received>\\d+) packets? received, (?P<Loss>\\d+\\.?\\d+)% packet loss"
+        outcomes:
+          - pass:
+              when: "Loss < 20"
+              message: "Solid connection to google.com"
+          - fail:
+              message: "Really high packet loss"
diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go
index bb71c660..95503c3c 100644
--- a/pkg/analyze/analyzer.go
+++ b/pkg/analyze/analyzer.go
@@ -79,6 +79,12 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont
 		}
 		return analyzeDistribution(analyzer.Distribution, getFile)
 	}
+	if analyzer.RegEx != nil {
+		if analyzer.RegEx.Exclude {
+			return nil, nil
+		}
+		return analyzeRegex(analyzer.RegEx, getFile)
+	}
 
 	return nil, errors.New("invalid analyzer")
 }
diff --git a/pkg/analyze/regex.go b/pkg/analyze/regex.go
new file mode 100644
index 00000000..50d6c81e
--- /dev/null
+++ b/pkg/analyze/regex.go
@@ -0,0 +1,166 @@
+package analyzer
+
+import (
+	"fmt"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
+)
+
+func analyzeRegex(analyzer *troubleshootv1beta1.RegEx, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	contents, err := getCollectedFileContents(path.Join(analyzer.CollectorName, analyzer.CollectorName+".txt"))
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get file contents for regex")
+	}
+
+	expression := regexp.MustCompile(analyzer.Expression)
+	match := expression.FindStringSubmatch(string(contents))
+
+	result := &AnalyzeResult{
+		Title: analyzer.CheckName,
+	}
+	// to avoid empty strings in the UI..,.
+	if result.Title == "" {
+		result.Title = analyzer.CollectorName
+	}
+
+	foundMatches := map[string]string{}
+	for i, name := range expression.SubexpNames() {
+		if i != 0 && name != "" {
+			foundMatches[name] = match[i]
+		}
+	}
+
+	// allow fallthrough
+	for _, outcome := range analyzer.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
+			}
+
+			isMatch, err := compareRegex(outcome.Fail.When, foundMatches)
+			if err != nil {
+				return result, errors.Wrap(err, "failed to compare regex fail conditional")
+			}
+
+			if isMatch {
+				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
+			}
+
+			isMatch, err := compareRegex(outcome.Warn.When, foundMatches)
+			if err != nil {
+				return result, errors.Wrap(err, "failed to compare regex warn conditional")
+			}
+
+			if isMatch {
+				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
+			}
+
+			isMatch, err := compareRegex(outcome.Pass.When, foundMatches)
+			if err != nil {
+				return result, errors.Wrap(err, "failed to compare regex pass conditional")
+			}
+
+			if isMatch {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return result, nil
+			}
+		}
+	}
+
+	return result, nil
+}
+
+func compareRegex(conditional string, foundMatches map[string]string) (bool, error) {
+	parts := strings.Split(strings.TrimSpace(conditional), " ")
+
+	if len(parts) != 3 {
+		return false, errors.New("unable to parse regex conditional")
+	}
+
+	lookForMatchName := parts[0]
+	operator := parts[1]
+	lookForValue := parts[2]
+
+	foundValue, ok := foundMatches[lookForMatchName]
+	if !ok {
+		// not an error, just wasn't matched
+		return false, nil
+	}
+
+	// if the value side of the conditional is an int, we assume it's an int
+	lookForValueInt, err := strconv.Atoi(lookForValue)
+	if err == nil {
+		fmt.Printf("look for = %s, found = %s\n", lookForValue, foundValue)
+		foundValueInt, err := strconv.Atoi(foundValue)
+		if err != nil {
+			// not an error but maybe it should be...
+			return false, nil
+		}
+
+		switch operator {
+		case "=":
+			fallthrough
+		case "==":
+			fallthrough
+		case "===":
+			return lookForValueInt == foundValueInt, nil
+
+		case "<":
+			return lookForValueInt < foundValueInt, nil
+
+		case ">":
+			return lookForValueInt > foundValueInt, nil
+
+		case "<=":
+			return lookForValueInt <= foundValueInt, nil
+
+		case ">=":
+			return lookForValueInt >= foundValueInt, nil
+		}
+	} else {
+		// all we can support is "=" and "==" and "===" for now
+		if operator != "=" && operator != "==" && operator != "===" {
+			return false, errors.New("unexpected operator in regex comparator")
+		}
+
+		return foundValue == lookForValue, nil
+	}
+
+	return false, nil
+}
diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go
index 29b6b955..3389bc03 100644
--- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go
+++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go
@@ -74,6 +74,13 @@ type Distribution struct {
 	Outcomes    []*Outcome `json:"outcomes" yaml:"outcomes"`
 }
 
+type RegEx struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName" yaml:"collectorName"`
+	Expression    string     `json:"expression" yaml:"expression"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
 type AnalyzeMeta struct {
 	CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"`
 	Exclude   bool   `json:"exclude,omitempty" yaml:"exclude,omitempty"`
@@ -90,4 +97,5 @@ type Analyze struct {
 	StatefulsetStatus        *StatefulsetStatus        `json:"statefulsetStatus,omitempty" yaml:"statefulsetStatus,omitempty"`
 	ContainerRuntime         *ContainerRuntime         `json:"containerRuntime,omitempty" yaml:"containerRuntime,omitempty"`
 	Distribution             *Distribution             `json:"distribution,omitempty" yaml:"distribution,omitempty"`
+	RegEx                    *RegEx                    `json:"regex,omitempty" yaml:"regex,omitempty"`
 }
diff --git a/pkg/apis/troubleshoot/v1beta1/collector_shared.go b/pkg/apis/troubleshoot/v1beta1/collector_shared.go
index e437a056..568c3b5f 100644
--- a/pkg/apis/troubleshoot/v1beta1/collector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta1/collector_shared.go
@@ -43,7 +43,6 @@ type Data struct {
 
 type Run struct {
 	CollectorMeta   `json:",inline" yaml:",inline"`
-	Name            string   `json:"name,omitempty" yaml:"name,omitempty"`
 	Namespace       string   `json:"namespace" yaml:"namespace"`
 	Image           string   `json:"image" yaml:"image"`
 	Command         []string `json:"command,omitempty" yaml:"command,omitempty"`
@@ -54,7 +53,6 @@ type Run struct {
 
 type Exec struct {
 	CollectorMeta `json:",inline" yaml:",inline"`
-	Name          string   `json:"name,omitempty" yaml:"name,omitempty"`
 	Selector      []string `json:"selector" yaml:"selector"`
 	Namespace     string   `json:"namespace" yaml:"namespace"`
 	ContainerName string   `json:"containerName,omitempty" yaml:"containerName,omitempty"`
@@ -65,7 +63,6 @@ type Exec struct {
 
 type Copy struct {
 	CollectorMeta `json:",inline" yaml:",inline"`
-	Name          string   `json:"name,omitempty" yaml:"name,omitempty"`
 	Selector      []string `json:"selector" yaml:"selector"`
 	Namespace     string   `json:"namespace" yaml:"namespace"`
 	ContainerPath string   `json:"containerPath" yaml:"containerPath"`
diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go
index 6d6455ce..04c281da 100644
--- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go
@@ -102,6 +102,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
 		*out = new(Distribution)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.RegEx != nil {
+		in, out := &in.RegEx, &out.RegEx
+		*out = new(RegEx)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
@@ -1359,6 +1364,33 @@ func (in *Put) DeepCopy() *Put {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RegEx) DeepCopyInto(out *RegEx) {
+	*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 RegEx.
+func (in *RegEx) DeepCopy() *RegEx {
+	if in == nil {
+		return nil
+	}
+	out := new(RegEx)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ResultRequest) DeepCopyInto(out *ResultRequest) {
 	*out = *in
diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go
index c42816c5..dbd04681 100644
--- a/pkg/collect/cluster_resources.go
+++ b/pkg/collect/cluster_resources.go
@@ -7,9 +7,7 @@ import (
 	"strings"
 
 	"github.com/replicatedhq/troubleshoot/pkg/redact"
-	authorizationv1 "k8s.io/api/authorization/v1"
 	corev1 "k8s.io/api/core/v1"
-	rbacv1 "k8s.io/api/rbac/v1"
 	apiextensionsv1beta1clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -34,10 +32,6 @@ type ClusterResourcesOutput struct {
 	ImagePullSecretsErrors          []byte            `json:"cluster-resources/image-pull-secrets-errors.json,omitempty"`
 	Nodes                           []byte            `json:"cluster-resources/nodes.json,omitempty"`
 	NodesErrors                     []byte            `json:"cluster-resources/nodes-errors.json,omitempty"`
-
-	// TODO these should be considered for relocation to an rbac or auth package.  cluster resources might not be the right place
-	AuthCanI       map[string][]byte `json:"cluster-resources/auth-cani-list,omitempty"`
-	AuthCanIErrors []byte            `json:"cluster-resources/auth-cani-list-errors.json,omitempty"`
 }
 
 func ClusterResources(ctx *Context) ([]byte, error) {
@@ -137,14 +131,6 @@ func ClusterResources(ctx *Context) ([]byte, error) {
 		return nil, err
 	}
 
-	// auth cani
-	authCanI, authCanIErrors := authCanI(client, namespaceNames)
-	clusterResourcesOutput.AuthCanI = authCanI
-	clusterResourcesOutput.AuthCanIErrors, err = marshalNonNil(authCanIErrors)
-	if err != nil {
-		return nil, err
-	}
-
 	if ctx.Redact {
 		clusterResourcesOutput, err = clusterResourcesOutput.Redact()
 		if err != nil {
@@ -374,63 +360,6 @@ func nodes(client *kubernetes.Clientset) ([]byte, []string) {
 	return b, nil
 }
 
-func authCanI(client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) {
-	// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/auth/cani.go
-
-	authListByNamespace := make(map[string][]byte)
-	errorsByNamespace := make(map[string]string)
-
-	for _, namespace := range namespaces {
-		sar := &authorizationv1.SelfSubjectRulesReview{
-			Spec: authorizationv1.SelfSubjectRulesReviewSpec{
-				Namespace: namespace,
-			},
-		}
-		response, err := client.AuthorizationV1().SelfSubjectRulesReviews().Create(sar)
-		if err != nil {
-			errorsByNamespace[namespace] = err.Error()
-			continue
-		}
-
-		rules := []rbacv1.PolicyRule{}
-		for _, rule := range convertToPolicyRule(response.Status) {
-			rules = append(rules, rule)
-		}
-
-		b, err := json.MarshalIndent(rules, "", "  ")
-		if err != nil {
-			errorsByNamespace[namespace] = err.Error()
-			continue
-		}
-
-		authListByNamespace[namespace+".json"] = b
-	}
-
-	return authListByNamespace, errorsByNamespace
-}
-
-// not exprted from: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/auth/cani.go#L339
-func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule {
-	ret := []rbacv1.PolicyRule{}
-	for _, resource := range status.ResourceRules {
-		ret = append(ret, rbacv1.PolicyRule{
-			Verbs:         resource.Verbs,
-			APIGroups:     resource.APIGroups,
-			Resources:     resource.Resources,
-			ResourceNames: resource.ResourceNames,
-		})
-	}
-
-	for _, nonResource := range status.NonResourceRules {
-		ret = append(ret, rbacv1.PolicyRule{
-			Verbs:           nonResource.Verbs,
-			NonResourceURLs: nonResource.NonResourceURLs,
-		})
-	}
-
-	return ret
-}
-
 func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
 	namespaces, err := redact.Redact(c.Namespaces)
 	if err != nil {
@@ -464,7 +393,6 @@ func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
 	if err != nil {
 		return nil, err
 	}
-
 	return &ClusterResourcesOutput{
 		Namespaces:                      namespaces,
 		NamespacesErrors:                c.NamespacesErrors,
@@ -484,7 +412,5 @@ func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
 		CustomResourceDefinitionsErrors: c.CustomResourceDefinitionsErrors,
 		ImagePullSecrets:                c.ImagePullSecrets,
 		ImagePullSecretsErrors:          c.ImagePullSecretsErrors,
-		AuthCanI:                        c.AuthCanI,
-		AuthCanIErrors:                  c.AuthCanIErrors,
 	}, nil
 }
diff --git a/pkg/collect/copy.go b/pkg/collect/copy.go
index f3967899..f5734d37 100644
--- a/pkg/collect/copy.go
+++ b/pkg/collect/copy.go
@@ -34,7 +34,7 @@ func Copy(ctx *Context, copyCollector *troubleshootv1beta1.Copy) ([]byte, error)
 
 	if len(pods) > 0 {
 		for _, pod := range pods {
-			bundlePath := filepath.Join(copyCollector.Name, pod.Namespace, pod.Name)
+			bundlePath := filepath.Join(copyCollector.CollectorName, pod.Namespace, pod.Name)
 
 			files, copyErrors := copyFiles(ctx, client, pod, copyCollector)
 			if len(copyErrors) > 0 {
@@ -136,9 +136,6 @@ func (c CopyOutput) Redact() (CopyOutput, error) {
 }
 
 func getCopyErrosFileName(copyCollector *troubleshootv1beta1.Copy) string {
-	if len(copyCollector.Name) > 0 {
-		return fmt.Sprintf("%s-errors.json", copyCollector.Name)
-	}
 	if len(copyCollector.CollectorName) > 0 {
 		return fmt.Sprintf("%s-errors.json", copyCollector.CollectorName)
 	}
diff --git a/pkg/collect/exec.go b/pkg/collect/exec.go
index a375973a..27565383 100644
--- a/pkg/collect/exec.go
+++ b/pkg/collect/exec.go
@@ -70,7 +70,7 @@ func execWithoutTimeout(ctx *Context, execCollector *troubleshootv1beta1.Exec) (
 		for _, pod := range pods {
 			stdout, stderr, execErrors := getExecOutputs(ctx, client, pod, execCollector)
 
-			bundlePath := filepath.Join(execCollector.Name, pod.Namespace, pod.Name)
+			bundlePath := filepath.Join(execCollector.CollectorName, pod.Namespace, pod.Name)
 			if len(stdout) > 0 {
 				execOutput[filepath.Join(bundlePath, execCollector.CollectorName+"-stdout.txt")] = stdout
 			}
@@ -158,9 +158,6 @@ func (r ExecOutput) Redact() (ExecOutput, error) {
 }
 
 func getExecErrosFileName(execCollector *troubleshootv1beta1.Exec) string {
-	if len(execCollector.Name) > 0 {
-		return fmt.Sprintf("%s-errors.json", execCollector.Name)
-	}
 	if len(execCollector.CollectorName) > 0 {
 		return fmt.Sprintf("%s-errors.json", execCollector.CollectorName)
 	}
diff --git a/pkg/collect/run.go b/pkg/collect/run.go
index 6c341fb0..ecc78102 100644
--- a/pkg/collect/run.go
+++ b/pkg/collect/run.go
@@ -85,7 +85,7 @@ func runWithoutTimeout(ctx *Context, pod *corev1.Pod, runCollector *troubleshoot
 	limits := troubleshootv1beta1.LogLimits{
 		MaxLines: 10000,
 	}
-	podLogs, err := getPodLogs(client, *pod, runCollector.Name, "", &limits, true)
+	podLogs, err := getPodLogs(client, *pod, runCollector.CollectorName, "", &limits, true)
 
 	for k, v := range podLogs {
 		runOutput[k] = v


Test patch:
diff --git a/pkg/analyze/regex_test.go b/pkg/analyze/regex_test.go
new file mode 100644
index 00000000..48cae730
--- /dev/null
+++ b/pkg/analyze/regex_test.go
@@ -0,0 +1,57 @@
+package analyzer
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_compareRegex(t *testing.T) {
+	tests := []struct {
+		name         string
+		conditional  string
+		foundMatches map[string]string
+		expected     bool
+	}{
+		{
+			name:        "Loss < 5",
+			conditional: "Loss < 5",
+			foundMatches: map[string]string{
+				"Transmitted": "5",
+				"Received":    "4",
+				"Loss":        "20",
+			},
+			expected: true,
+		},
+		{
+			name:        "Hostname = icecream",
+			conditional: "Hostname = icecream",
+			foundMatches: map[string]string{
+				"Something": "5",
+				"Hostname":  "icecream",
+			},
+			expected: true,
+		},
+		{
+			name:        "Day >= 23",
+			conditional: "Day >= 23",
+			foundMatches: map[string]string{
+				"day": "5",
+				"Day": "24",
+			},
+			expected: false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			actual, err := compareRegex(test.conditional, test.foundMatches)
+			req.NoError(err)
+
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+
+}