Add ability for Cluster resources analyzer to do number and size comparison (#1210)

* make CR analyzer compare ints and sizes

* schemas

* typo

* more comments

* tests for cluster resources analyzer doing number/size comparisons

* switch from humanize to k8s size parsing

* schemas

---------

Co-authored-by: Nathan Sullivan <nathans@replicated.com>
This commit is contained in:
danj-replicated
2023-06-08 07:51:11 -04:00
committed by GitHub
co-authored by Nathan Sullivan
parent 70f0e071dd
commit 0d1e23651a
3 changed files with 210 additions and 5 deletions
+4 -5
View File
@@ -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")
+138
View File
@@ -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{
+68
View File
@@ -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 {