From 3665d25abf87705b640aafcbf8028f771000a636 Mon Sep 17 00:00:00 2001 From: Johannes Tuchscherer Date: Thu, 6 Mar 2025 14:40:47 -0700 Subject: [PATCH] Http comperators (#1753) * Allowing more comperators for the http analyzer * test * Update pkg/analyze/host_http.go Co-authored-by: Andrew Lavery --------- Co-authored-by: Andrew Lavery --- go.mod | 1 + go.sum | 2 + pkg/analyze/host_http.go | 39 ++++--- pkg/analyze/host_http_test.go | 192 ++++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index d3614547..a7b49d6c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 github.com/apparentlymart/go-cidr v1.1.0 github.com/blang/semver/v4 v4.0.0 + github.com/casbin/govaluate v1.3.0 github.com/cilium/ebpf v0.17.3 github.com/containerd/cgroups/v3 v3.0.5 github.com/containers/image/v5 v5.34.1 diff --git a/go.sum b/go.sum index 14a2da31..604e5b9d 100644 --- a/go.sum +++ b/go.sum @@ -707,6 +707,8 @@ github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuP github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f h1:tRk+aBit+q3oqnj/1mF5HHhP2yxJM2lSa0afOJxQ3nE= github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/pkg/analyze/host_http.go b/pkg/analyze/host_http.go index 264661c6..9c97750d 100644 --- a/pkg/analyze/host_http.go +++ b/pkg/analyze/host_http.go @@ -3,9 +3,9 @@ package analyzer import ( "encoding/json" "fmt" - "strconv" "strings" + "github.com/casbin/govaluate" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/collect" @@ -64,28 +64,33 @@ func compareHostHTTPConditionalToActual(conditional string, result *httpResult) return result.Error != nil, nil } - parts := strings.Split(conditional, " ") - if len(parts) != 3 { - return false, fmt.Errorf("Failed to parse conditional: got %d parts", len(parts)) - } + conditional = strings.ReplaceAll(conditional, " = ", " == ") + conditional = strings.ReplaceAll(conditional, " === ", " == ") - if parts[0] != "statusCode" { - return false, errors.New(`Conditional must begin with keyword "statusCode"`) - } - - if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" { - return false, errors.New(`Only supported operator is "=="`) - } - - i, err := strconv.Atoi(parts[2]) + expression, err := govaluate.NewEvaluableExpression(conditional) if err != nil { - return false, err + return false, errors.Wrap(err, "failed to create evaluable expression") } if result.Response == nil { - return false, err + return false, nil } - return result.Response.Status == i, nil + + parameters := make(map[string]interface{}, 8) + parameters["statusCode"] = result.Response.Status + + comparisonResult, err := expression.Evaluate(parameters) + + if err != nil { + return false, errors.Wrap(err, "failed to evaluate expression") + } + + boolResult, ok := comparisonResult.(bool) + if !ok { + return false, fmt.Errorf("expression did not evaluate to a boolean value") + } + + return boolResult, nil } func analyzeHTTPResult(analyzer *troubleshootv1beta2.HTTPAnalyze, fileName string, getCollectedFileContents getCollectedFileContents, title string) ([]*AnalyzeResult, error) { diff --git a/pkg/analyze/host_http_test.go b/pkg/analyze/host_http_test.go index 517a769f..f8e0df43 100644 --- a/pkg/analyze/host_http_test.go +++ b/pkg/analyze/host_http_test.go @@ -113,6 +113,124 @@ func TestAnalyzeHostHTTP(t *testing.T) { }, }, }, + { + name: "invalid compare operator", + expectErr: true, + httpResult: &httpResult{ + Response: &collect.HTTPResponse{ + Status: 200, + }, + }, + hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{ + CollectorName: "collector", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "statusCode #$ 200", + Message: "passed", + }, + }, + { + Warn: &troubleshootv1beta2.SingleOutcome{ + Message: "default", + }, + }, + }, + }, + }, + { + name: "!= compare operator", + httpResult: &httpResult{ + Response: &collect.HTTPResponse{ + Status: 201, + }, + }, + hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{ + CollectorName: "collector", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "statusCode != 200", + Message: "passed", + }, + }, + { + Warn: &troubleshootv1beta2.SingleOutcome{ + Message: "default", + }, + }, + }, + }, + result: []*AnalyzeResult{ + { + Title: "HTTP Request", + IsPass: true, + Message: "passed", + }, + }, + }, + { + name: "Looking for 2xx status codes", + httpResult: &httpResult{ + Response: &collect.HTTPResponse{ + Status: 201, + }, + }, + hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{ + CollectorName: "collector", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "statusCode >= 300 || statusCode < 200", + Message: "expected 2xx status code", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "default", + }, + }, + }, + }, + result: []*AnalyzeResult{ + { + Title: "HTTP Request", + IsPass: true, + Message: "default", + }, + }, + }, + { + name: "Looking for 2xx status codes does not match", + httpResult: &httpResult{ + Response: &collect.HTTPResponse{ + Status: 300, + }, + }, + hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{ + CollectorName: "collector", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "statusCode >= 300 || statusCode < 200", + Message: "expected 2xx status code", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "default", + }, + }, + }, + }, + result: []*AnalyzeResult{ + { + Title: "HTTP Request", + IsFail: true, + Message: "expected 2xx status code", + }, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -137,3 +255,77 @@ func TestAnalyzeHostHTTP(t *testing.T) { }) } } + +func TestAnalyzeHostHTTPHTTPCodesAndCompareOperators(t *testing.T) { + httpResult := &httpResult{ + Response: &collect.HTTPResponse{ + Status: 200, + }, + } + + tests := []struct { + name string + }{ + { + name: "statusCode == 200", + }, + { + name: "statusCode === 200", + }, + { + name: "statusCode = 200", + }, + { + name: "statusCode != 201", + }, + { + name: "statusCode >= 200", + }, + { + name: "statusCode > 199", + }, + { + name: "statusCode <= 200", + }, + { + name: "statusCode <= 201", + }, + { + name: "statusCode < 201", + }, + { + name: "statusCode < 201 && statusCode > 199", + }, + { + name: "statusCode < 201 || statusCode > 199 && statusCode == 200", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + hostAnalyzer := &troubleshootv1beta2.HTTPAnalyze{ + CollectorName: "registry", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: test.name}, + }, + }, + } + + req := require.New(t) + b, err := json.Marshal(httpResult) + if err != nil { + t.Fatal(err) + } + + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + + result, err := (&AnalyzeHostHTTP{hostAnalyzer}).Analyze(getCollectedFileContents, nil) + req.NoError(err) + req.Len(result, 1) + req.Equal(true, result[0].IsPass) + }) + } +}