diff --git a/pkg/analyze/common_status.go b/pkg/analyze/common_status.go index 316b2073..23af59a8 100644 --- a/pkg/analyze/common_status.go +++ b/pkg/analyze/common_status.go @@ -47,7 +47,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey } } - match, err := compareActualToWhen(outcome.Fail.When, readyReplicas, exists) + match, err := compareActualToWhen(outcome.Fail.When, readyReplicas) if err != nil { return nil, errors.Wrap(err, "failed to parse fail range") } @@ -87,7 +87,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey } } - match, err := compareActualToWhen(outcome.Warn.When, readyReplicas, exists) + match, err := compareActualToWhen(outcome.Warn.When, readyReplicas) if err != nil { return nil, errors.Wrap(err, "failed to parse warn range") } @@ -127,7 +127,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey } } - match, err := compareActualToWhen(outcome.Pass.When, readyReplicas, exists) + match, err := compareActualToWhen(outcome.Pass.When, readyReplicas) if err != nil { return nil, errors.Wrap(err, "failed to parse pass range") } @@ -145,9 +145,8 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey return result, nil } -func compareActualToWhen(when string, actual int, exists bool) (bool, error) { +func compareActualToWhen(when string, actual int) (bool, error) { parts := strings.Split(strings.TrimSpace(when), " ") - // we can make this a lot more flexible if len(parts) != 2 { return false, errors.New("unable to parse when range") diff --git a/pkg/analyze/kube_resource.go b/pkg/analyze/kube_resource.go index 06c92525..992f1b19 100644 --- a/pkg/analyze/kube_resource.go +++ b/pkg/analyze/kube_resource.go @@ -5,12 +5,14 @@ import ( "path/filepath" "reflect" "strconv" + "strings" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/constants" iutils "github.com/replicatedhq/troubleshoot/pkg/interfaceutils" "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/klog/v2" ) @@ -108,6 +110,133 @@ func FindResource(kind string, clusterScoped bool, namespace string, name string } +func compareWhentoResource(w string, actual interface{}) (bool, error) { + + // check our "when" has operators + var whenSplit []string + if strings.ContainsAny(w, "!=<>") { + whenSplit = strings.Split(strings.TrimSpace(w), " ") + } else { + return false, errors.New("no operators found") + } + + // let's first check if we can cast "actual" as an int, that should inform us what comparison we're doing + actualAsInt, ok := actual.(int) + if ok { + // it's an int! we can do integer comparison here + // we're going to re-use an ill-fitting bit of code from the deployment analyzer for now + return compareActualToWhen(w, actualAsInt) + } + + // if we've fallen through here we're going to have to try a bit harder to work out what we're comparing + // let's try making it a string + actualAsString, ok := actual.(string) + if !ok { + return false, errors.New("could not cast found value as string") + } + + // now we can try checking if it's a "quantity" + actualASQuantity, err := resource.ParseQuantity(actualAsString) + if err == nil { + // it's probably a size, we can do some comparison here + // but I'm being lazy here so we'll convert our last argument to an int and throw it back at our existing int comparison function + whenAsQuantity, err := resource.ParseQuantity(whenSplit[1]) + if err != nil { + // our when wasn't a size! naughty user + return false, errors.New("Cannot compare size with not size") + } + whenIntAsString := strconv.FormatInt(whenAsQuantity.Value(), 10) + // re-use that same compare function from earlier, might as well + return compareActualToWhen(whenSplit[0]+" "+whenIntAsString, int(actualASQuantity.Value())) + + } + + return false, errors.New("could not match comparison method for result") +} + +func analyzeWhenField(actual interface{}, outcomes []*troubleshootv1beta2.Outcome, checkName string) (*AnalyzeResult, error) { + + result := &AnalyzeResult{ + Title: checkName, + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16", + } + + for _, outcome := range outcomes { + if outcome.Fail != nil { + if outcome.Fail.When != "" { + compareResult, err := compareWhentoResource(outcome.Fail.When, actual) + if err != nil { + return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Fail.When) + } + if compareResult { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + return result, nil + } + } else { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return result, nil + } + } + if outcome.Warn != nil { + + if outcome.Warn.When != "" { + compareResult, err := compareWhentoResource(outcome.Fail.When, actual) + if err != nil { + return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Warn.When) + } + if compareResult { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + return result, nil + } + } else { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return result, nil + } + + } + if outcome.Pass != nil { + + if outcome.Pass.When != "" { + compareResult, err := compareWhentoResource(outcome.Pass.When, actual) + if err != nil { + return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Pass.When) + } + if compareResult { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + return result, nil + } + } else { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return result, nil + } + } + } + + return &AnalyzeResult{ + Title: checkName, + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg", + IsFail: true, + Message: "Invalid analyzer", + }, nil +} + func (a *AnalyzeClusterResource) analyzeResource(analyzer *troubleshootv1beta2.ClusterResource, getFileContents getCollectedFileContents) (*AnalyzeResult, error) { selected, err := FindResource(analyzer.Kind, analyzer.ClusterScoped, analyzer.Namespace, analyzer.Name, getFileContents) if err != nil { @@ -168,6 +297,15 @@ func (a *AnalyzeClusterResource) analyzeResource(analyzer *troubleshootv1beta2.C if result != nil { return result, nil } + } else { + // fall through to comparing from the when key + result, err := analyzeWhenField(actual, analyzer.Outcomes, a.Title()) + if err != nil { + return nil, err + } + if result != nil { + return result, nil + } } return &AnalyzeResult{ diff --git a/pkg/analyze/kube_resource_test.go b/pkg/analyze/kube_resource_test.go index a75d13f8..b12e85d0 100644 --- a/pkg/analyze/kube_resource_test.go +++ b/pkg/analyze/kube_resource_test.go @@ -257,6 +257,74 @@ func Test_analyzeResource(t *testing.T) { IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16", }, }, + { + name: "pass-when-pvc-exists-and-is-at-least-4Gi", + analyzer: troubleshootv1beta2.ClusterResource{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "check-pvc-is-at-least-4Gi", + }, + Kind: "PersistentVolumeClaim", + Name: "data-postgresql-0", + Namespace: "default", + YamlPath: "spec.resources.requests.storage", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: ">= 4Gi", + Message: "pass", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "fail", + }, + }, + }, + }, + expectResult: AnalyzeResult{ + IsPass: true, + IsWarn: false, + IsFail: false, + Title: "check-pvc-is-at-least-4Gi", + Message: "pass", + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16", + }, + }, + { + name: "fail-when-pvc-exists-and-is-not-at-least-16Gi", + analyzer: troubleshootv1beta2.ClusterResource{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "check-pvc-is-at-least-16Gi", + }, + Kind: "PersistentVolumeClaim", + Name: "data-postgresql-0", + Namespace: "default", + YamlPath: "spec.resources.requests.storage", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: ">= 16Gi", + Message: "pass", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "fail", + }, + }, + }, + }, + expectResult: AnalyzeResult{ + IsPass: false, + IsWarn: false, + IsFail: true, + Title: "check-pvc-is-at-least-16Gi", + Message: "fail", + IconKey: "kubernetes_text_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16", + }, + }, } { for _, test := range tests {