diff --git a/pkg/analyze/node_resources.go b/pkg/analyze/node_resources.go index fbae50f0c..3f9904c74 100644 --- a/pkg/analyze/node_resources.go +++ b/pkg/analyze/node_resources.go @@ -3,9 +3,9 @@ package analyzer import ( "encoding/json" "fmt" + "regexp" "strconv" "strings" - "regexp" "github.com/pkg/errors" troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" @@ -19,12 +19,12 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec return nil, errors.Wrap(err, "failed to get contents of nodes.json") } - var nodes []corev1.Node + nodes := []corev1.Node{} if err := json.Unmarshal(collected, &nodes); err != nil { return nil, errors.Wrap(err, "failed to unmarshal node list") } - matchingNodeCount := 0 + matchingNodes := []corev1.Node{} for _, node := range nodes { isMatch, err := nodeMatchesFilters(node, analyzer.Filters) @@ -33,7 +33,7 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec } if isMatch { - matchingNodeCount++ + matchingNodes = append(matchingNodes, node) } } @@ -108,42 +108,167 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes [] } operator := parts[1] + var desiredValue interface{} desiredValue = parts[2] parsedDesiredValue, err := strconv.Atoi(parts[2]) - if err != nil { + if err == nil { desiredValue = parsedDesiredValue } + reg := regexp.MustCompile(`(?P.*)\((?P.*)\)`) + match := reg.FindStringSubmatch(parts[0]) + + if match == nil { + // We support this as equivalent to the count() function + match = reg.FindStringSubmatch(fmt.Sprintf("count() == %s", parts[0])) + } + + if match == nil || len(match) != 3 { + return false, errors.New("conditional does not match pattern of function(property?)") + } + + function := match[1] + property := match[2] + var actualValue interface{} - actualValue = len(matchingNodes) - reg := regexp.MustCompile("(?P.*)\((?P.*)\)") - match := reg.FindStringSubmatch(parts[0]) + switch function { + case "count": + actualValue = len(matchingNodes) + break + case "min": + av, err := findMin(matchingNodes, property) + if err != nil { + return false, errors.Wrap(err, "failed to find min") + } + actualValue = av + } - fmt.Printf("reg = %#v\n", reg) - // result := make(map[string]string) + switch operator { + case "=", "==", "===": + return desiredValue == actualValue, nil + case "<": + if _, ok := actualValue.(int); ok { + if _, ok := desiredValue.(int); ok { + return actualValue.(int) < desiredValue.(int), nil + } + } + return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == -1, nil + case ">": + if _, ok := actualValue.(int); ok { + if _, ok := desiredValue.(int); ok { + return actualValue.(int) > desiredValue.(int), nil + } + } + return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 1, nil - // switch operator { - // case "=", "==", "===": - // return desiredValue == actualValue, nil - // case "<": - // return actualValue < desiredValue, nil - // case "<=": - // return actualValue <= desiredValue, nil - // case ">": - // return actualValue > desiredValue, nil - // case ">=": - // return actualValue >= desiredValue, nil - // } + case "<=": + if _, ok := actualValue.(int); ok { + if _, ok := desiredValue.(int); ok { + return actualValue.(int) <= desiredValue.(int), nil + } + } + return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == -1 || + actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 0, nil + + case ">=": + if _, ok := actualValue.(int); ok { + if _, ok := desiredValue.(int); ok { + return actualValue.(int) >= desiredValue.(int), nil + } + } + return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 1 || + actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 0, nil + } return false, errors.New("unexpected conditional in nodeResources") } -func findMin(nodes []codev1.Node, property string) (string, error) { - return "", errors.New("not implemented") +func findMin(nodes []corev1.Node, property string) (*resource.Quantity, error) { + var min *resource.Quantity + + for _, node := range nodes { + switch property { + case "cpuCapacity": + if min == nil { + min = node.Status.Capacity.Cpu() + } else { + if node.Status.Capacity.Cpu().Cmp(*min) == -1 { + min = node.Status.Capacity.Cpu() + } + } + break + case "cpuAllocatable": + if min == nil { + min = node.Status.Allocatable.Cpu() + } else { + if node.Status.Allocatable.Cpu().Cmp(*min) == -1 { + min = node.Status.Allocatable.Cpu() + } + } + break + case "memoryCapacity": + if min == nil { + min = node.Status.Capacity.Memory() + } else { + if node.Status.Capacity.Memory().Cmp(*min) == -1 { + min = node.Status.Capacity.Memory() + } + } + break + case "memoryAllocatable": + if min == nil { + min = node.Status.Allocatable.Memory() + } else { + if node.Status.Allocatable.Memory().Cmp(*min) == -1 { + min = node.Status.Allocatable.Memory() + } + } + break + case "podCapacity": + if min == nil { + min = node.Status.Capacity.Pods() + } else { + if node.Status.Capacity.Pods().Cmp(*min) == -1 { + min = node.Status.Capacity.Pods() + } + } + break + case "podAllocatable": + if min == nil { + min = node.Status.Allocatable.Pods() + } else { + if node.Status.Allocatable.Pods().Cmp(*min) == -1 { + min = node.Status.Allocatable.Pods() + } + } + break + case "ephemeralStorageCapacity": + if min == nil { + min = node.Status.Capacity.StorageEphemeral() + } else { + if node.Status.Capacity.StorageEphemeral().Cmp(*min) == -1 { + min = node.Status.Capacity.StorageEphemeral() + } + } + break + case "ephemeralStorageAllocatable": + if min == nil { + min = node.Status.Allocatable.StorageEphemeral() + } else { + if node.Status.Allocatable.StorageEphemeral().Cmp(*min) == -1 { + min = node.Status.Allocatable.StorageEphemeral() + } + } + break + + } + } + + return min, nil } func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta1.NodeResourceFilters) (bool, error) { diff --git a/pkg/analyze/node_resources_test.go b/pkg/analyze/node_resources_test.go index d74fdf270..8c6b9ed67 100644 --- a/pkg/analyze/node_resources_test.go +++ b/pkg/analyze/node_resources_test.go @@ -8,64 +8,358 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func Test_compareNodeResourceConditionalToActual(t *testing.T) { tests := []struct { - name string - conditional string - matchingNodeCount int - totalNodeCount int - expected bool + name string + conditional string + totalNodeCount int + matchingNodes []corev1.Node + expected bool }{ { - name: "=", - conditional: "= 5", - matchingNodeCount: 5, - totalNodeCount: 1, - expected: true, + name: "=", + conditional: "= 2", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, }, { - name: "<= (pass)", - conditional: "<= 5", - matchingNodeCount: 4, - totalNodeCount: 1, - expected: true, + name: "count()", + conditional: "count() == 2", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, }, { - name: "<= (fail)", - conditional: "<= 5", - matchingNodeCount: 6, - totalNodeCount: 1, - expected: false, + name: "<", + conditional: "< 3", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, }, { - name: "> (pass)", - conditional: "> 5", - matchingNodeCount: 6, - totalNodeCount: 1, - expected: true, + name: "count() <", + conditional: "count() < 3", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, }, { - name: ">= (fail)", - conditional: ">= 5", - matchingNodeCount: 4, - totalNodeCount: 1, - expected: false, + name: ">", + conditional: "> 2", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: false, }, { - name: "min(memoryCapacity) <= 16Gi (pass)", - conditional: "min(memoryCapacity) <= 16Gi", - matchingNodeCount: 2, - totalNodeCount: 2, - expected: true, + name: "count() >", + conditional: "count() > 1", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, }, { - name: "min(memoryCapacity) <= 16Gi", - conditional: "min(memoryCapacity) <= 16Gi", - matchingNodeCount: 1, - totalNodeCount: 2, - expected: false, + name: "count() >= 1 (true)", + conditional: "count() > 1", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, + }, + { + name: "count() <= 2 (true)", + conditional: "count() <= 2", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: true, + }, + { + name: "count() <= 1 (false)", + conditional: "count() <= 1", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, + totalNodeCount: 2, + expected: false, + }, + { + name: "min(memoryCapacity) <= 4Gi (true)", + conditional: "min(memoryCapacity) <= 4Gi", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: corev1.NodeStatus{ + Capacity: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "memory": resource.MustParse("3999Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "memory": resource.MustParse("16Ki"), + "pods": resource.MustParse("29"), + }, + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Status: corev1.NodeStatus{ + Capacity: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "memory": resource.MustParse("7951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, + }, + }, + }, + totalNodeCount: 2, + expected: true, + }, + { + name: "min(memoryCapacity) <= 4Gi (false)", + conditional: "min(memoryCapacity) <= 4Gi", + matchingNodes: []corev1.Node{ + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: corev1.NodeStatus{ + Capacity: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "memory": resource.MustParse("17951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, + }, + }, + corev1.Node{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Node", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Status: corev1.NodeStatus{ + Capacity: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("20959212Ki"), + "memory": resource.MustParse("7951376Ki"), + "pods": resource.MustParse("29"), + }, + Allocatable: corev1.ResourceList{ + "cpu": resource.MustParse("2"), + "ephemeral-storage": resource.MustParse("19316009748"), + "memory": resource.MustParse("7848976Ki"), + "pods": resource.MustParse("29"), + }, + }, + }, + }, + totalNodeCount: 2, + expected: false, }, } @@ -73,7 +367,7 @@ func Test_compareNodeResourceConditionalToActual(t *testing.T) { t.Run(test.name, func(t *testing.T) { req := require.New(t) - actual, err := compareNodeResourceConditionalToActual(test.conditional, test.matchingNodeCount, test.totalNodeCount) + actual, err := compareNodeResourceConditionalToActual(test.conditional, test.matchingNodes, test.totalNodeCount) req.NoError(err) assert.Equal(t, test.expected, actual)