Repository: replicatedhq/troubleshoot
Pull request #132 (2020-02-06 19:33:37)

Node aggregate analyzers
This adds support for node resource aggregate (min, max, count and sum) functions for cpu, memory, pods and ephemeralStorage: capacity and allocatable.

This operates on the filtered nodes, so you can filter based on a condition, and then aggregate and compare.

Examples:

```yaml
apiVersion: troubleshoot.replicated.com/v1beta1
kind: Preflight
metadata:
  name: sample
spec:
  analyzers:
    - nodeResources:
        checkName: Must have at least 3 nodes in the cluster
        outcomes:
          - fail:
              when: "count() < 3"
              message: This application requires at least 3 nodes
          - warn:
              when: "count() < 5"
              message: This application recommends at last 5 nodes.
          - pass:
              message: This cluster has enough nodes.
```

```yaml
    - nodeResources:
        checkName: Every node in the cluster must have at least 16Gi of memory
        filters:
          allocatableMemory: "16Gi"
        outcomes:
          - fail:
              when: "min(memoryCapacity) < 16Gi"
              message: All nodes must have at least 16 GB of memory
          - pass:
              message: All nodes have at least 16 GB of memory
```

```yaml
    - nodeResources:
        checkName: Total CPU Cores in the cluster is 20 or greater
        outcomes:
          - fail:
              when: "sum(cpuCapacity) < 20"
              message: The cluster must contain at least 20 cores
          - pass:
              message: There are at over 20 cores in the cluster
```

```yaml
    - nodeResources:
        checkName: Nodes that have 6 cores have at least 16 GB of memory also
        filters:
          cpuCapacity: "6"
          outcomes:
            - fail:
                when: "min(memoryCapacity) < 16Gi"
                message: All nodes that have 6 or more cores must have at least 16 GB of memory
            - pass:
                message:  All nodes with 6 or more cores have at least 16 GB of memory
```

```yaml
    - nodeResources:
        checkName: Must have 3 nodes with at least 6 cores
        filters:
          cpuCapacity: "6"
        outcomes:
          - fail:
              when: "count() < 3"
              message: This application requires at least 3 nodes with 6 cores each
          - pass:
              message: This cluster has enough nodes with enough codes
```

```yaml
    - nodeResources:
        checkName: Must have 1 node with 16 GB (available) memory and 5 cores (on a single node)
        filters:
          allocatableMemory: 16Gi
          cpuCapacity: "5"
        outcomes:
          - fail:
              when: "count() < 1"
              message: This application requires at least 1 node with 16GB available memory
          - pass:
              message: This cluster has a node with enough memory.
```



Patch:
diff --git a/pkg/analyze/node_resources.go b/pkg/analyze/node_resources.go
index f23afa3f..21167150 100644
--- a/pkg/analyze/node_resources.go
+++ b/pkg/analyze/node_resources.go
@@ -2,6 +2,8 @@ package analyzer
 
 import (
 	"encoding/json"
+	"fmt"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -17,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)
@@ -31,7 +33,7 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec
 		}
 
 		if isMatch {
-			matchingNodeCount++
+			matchingNodes = append(matchingNodes, node)
 		}
 	}
 
@@ -46,7 +48,7 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec
 
 	for _, outcome := range analyzer.Outcomes {
 		if outcome.Fail != nil {
-			isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodeCount)
+			isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodes, len(nodes))
 			if err != nil {
 				return nil, errors.Wrap(err, "failed to parse when")
 			}
@@ -59,7 +61,7 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec
 				return result, nil
 			}
 		} else if outcome.Warn != nil {
-			isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodeCount)
+			isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodes, len(nodes))
 			if err != nil {
 				return nil, errors.Wrap(err, "failed to parse when")
 			}
@@ -72,7 +74,7 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec
 				return result, nil
 			}
 		} else if outcome.Pass != nil {
-			isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodeCount)
+			isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodes, len(nodes))
 			if err != nil {
 				return nil, errors.Wrap(err, "failed to parse when")
 			}
@@ -90,39 +92,361 @@ func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollec
 	return result, nil
 }
 
-func compareNodeResourceConditionalToActual(conditional string, actual int) (bool, error) {
+func compareNodeResourceConditionalToActual(conditional string, matchingNodes []corev1.Node, totalNodeCount int) (bool, error) {
 	if conditional == "" {
 		return true, nil
 	}
 
-	parts := strings.Split(strings.TrimSpace(conditional), " ")
+	parts := strings.Fields(strings.TrimSpace(conditional))
 
-	if len(parts) != 2 {
+	if len(parts) == 2 {
+		parts = append([]string{"count"}, parts...)
+	}
+
+	if len(parts) != 3 {
 		return false, errors.New("unable to parse nodeResources conditional")
 	}
 
-	operator := parts[0]
-	desiredValue, err := strconv.Atoi(parts[1])
-	if err != nil {
-		return false, errors.Wrap(err, "failed to parse nodeResource value")
+	operator := parts[1]
+
+	var desiredValue interface{}
+	desiredValue = parts[2]
+
+	parsedDesiredValue, err := strconv.Atoi(parts[2])
+	if err == nil {
+		desiredValue = parsedDesiredValue
+	}
+
+	reg := regexp.MustCompile(`(?P<function>.*)\((?P<property>.*)\)`)
+	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{}
+
+	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
+	case "max":
+		av, err := findMax(matchingNodes, property)
+		if err != nil {
+			return false, errors.Wrap(err, "failed to find max")
+		}
+		actualValue = av
+	case "sum":
+		sum, err := findSum(matchingNodes, property)
+		if err != nil {
+			return false, errors.Wrap(err, "failed to find sum")
+		}
+		actualValue = sum
 	}
 
 	switch operator {
 	case "=", "==", "===":
-		return desiredValue == actual, nil
+		if _, ok := actualValue.(int); ok {
+			if _, ok := desiredValue.(int); ok {
+				return actualValue.(int) == desiredValue.(int), nil
+			}
+		}
+
+		if _, ok := desiredValue.(string); ok {
+			return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 0, nil
+		}
+
+		return actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == 0, nil
+
 	case "<":
-		return actual < desiredValue, nil
-	case "<=":
-		return actual <= desiredValue, nil
+		if _, ok := actualValue.(int); ok {
+			if _, ok := desiredValue.(int); ok {
+				return actualValue.(int) < desiredValue.(int), nil
+			}
+		}
+		if _, ok := desiredValue.(string); ok {
+			return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == -1, nil
+		}
+
+		return actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == -1, nil
+
 	case ">":
-		return actual > desiredValue, nil
+		if _, ok := actualValue.(int); ok {
+			if _, ok := desiredValue.(int); ok {
+				return actualValue.(int) > desiredValue.(int), nil
+			}
+		}
+		if _, ok := desiredValue.(string); ok {
+			return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 1, nil
+		}
+
+		return actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == 1, nil
+
+	case "<=":
+		if _, ok := actualValue.(int); ok {
+			if _, ok := desiredValue.(int); ok {
+				return actualValue.(int) <= desiredValue.(int), nil
+			}
+		}
+		if _, ok := desiredValue.(string); ok {
+			return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 0 ||
+				actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == -1, nil
+		}
+
+		return actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == 0 ||
+			actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == -1, nil
+
 	case ">=":
-		return actual >= desiredValue, nil
+		if _, ok := actualValue.(int); ok {
+			if _, ok := desiredValue.(int); ok {
+				return actualValue.(int) >= desiredValue.(int), nil
+			}
+		}
+		if _, ok := desiredValue.(string); ok {
+			return actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 0 ||
+				actualValue.(*resource.Quantity).Cmp(resource.MustParse(desiredValue.(string))) == 1, nil
+		}
+
+		return actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == 0 ||
+			actualValue.(*resource.Quantity).Cmp(resource.MustParse(strconv.Itoa(desiredValue.(int)))) == 1, nil
 	}
 
 	return false, errors.New("unexpected conditional in nodeResources")
 }
 
+func findSum(nodes []corev1.Node, property string) (*resource.Quantity, error) {
+	sum := resource.Quantity{}
+
+	for _, node := range nodes {
+		switch property {
+		case "cpuCapacity":
+			if node.Status.Capacity.Cpu() != nil {
+				sum.Add(*node.Status.Capacity.Cpu())
+			}
+			break
+		case "cpuAllocatable":
+			if node.Status.Allocatable.Cpu() != nil {
+				sum.Add(*node.Status.Allocatable.Cpu())
+			}
+			break
+		case "memoryCapacity":
+			if node.Status.Capacity.Memory() != nil {
+				sum.Add(*node.Status.Capacity.Memory())
+			}
+			break
+		case "memoryAllocatable":
+			if node.Status.Allocatable.Memory() != nil {
+				sum.Add(*node.Status.Allocatable.Cpu())
+			}
+			break
+		case "podCapacity":
+			if node.Status.Capacity.Pods() != nil {
+				sum.Add(*node.Status.Capacity.Pods())
+			}
+			break
+		case "podAllocatable":
+			if node.Status.Allocatable.Pods() != nil {
+				sum.Add(*node.Status.Allocatable.Cpu())
+			}
+			break
+		case "ephemeralStorageCapacity":
+			if node.Status.Capacity.StorageEphemeral() != nil {
+				sum.Add(*node.Status.Capacity.StorageEphemeral())
+			}
+			break
+		case "ephemeralStorageAllocatable":
+			if node.Status.Allocatable.StorageEphemeral() != nil {
+				sum.Add(*node.Status.Allocatable.StorageEphemeral())
+			}
+			break
+		}
+	}
+
+	return &sum, nil
+}
+
+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 findMax(nodes []corev1.Node, property string) (*resource.Quantity, error) {
+	var max *resource.Quantity
+
+	for _, node := range nodes {
+		switch property {
+		case "cpuCapacity":
+			if max == nil {
+				max = node.Status.Capacity.Cpu()
+			} else {
+				if node.Status.Capacity.Cpu().Cmp(*max) == 1 {
+					max = node.Status.Capacity.Cpu()
+				}
+			}
+			break
+		case "cpuAllocatable":
+			if max == nil {
+				max = node.Status.Allocatable.Cpu()
+			} else {
+				if node.Status.Allocatable.Cpu().Cmp(*max) == 1 {
+					max = node.Status.Allocatable.Cpu()
+				}
+			}
+			break
+		case "memoryCapacity":
+			if max == nil {
+				max = node.Status.Capacity.Memory()
+			} else {
+				if node.Status.Capacity.Memory().Cmp(*max) == 1 {
+					max = node.Status.Capacity.Memory()
+				}
+			}
+			break
+		case "memoryAllocatable":
+			if max == nil {
+				max = node.Status.Allocatable.Memory()
+			} else {
+				if node.Status.Allocatable.Memory().Cmp(*max) == 1 {
+					max = node.Status.Allocatable.Memory()
+				}
+			}
+			break
+		case "podCapacity":
+			if max == nil {
+				max = node.Status.Capacity.Pods()
+			} else {
+				if node.Status.Capacity.Pods().Cmp(*max) == 1 {
+					max = node.Status.Capacity.Pods()
+				}
+			}
+			break
+		case "podAllocatable":
+			if max == nil {
+				max = node.Status.Allocatable.Pods()
+			} else {
+				if node.Status.Allocatable.Pods().Cmp(*max) == 1 {
+					max = node.Status.Allocatable.Pods()
+				}
+			}
+			break
+		case "ephemeralStorageCapacity":
+			if max == nil {
+				max = node.Status.Capacity.StorageEphemeral()
+			} else {
+				if node.Status.Capacity.StorageEphemeral().Cmp(*max) == 1 {
+					max = node.Status.Capacity.StorageEphemeral()
+				}
+			}
+			break
+		case "ephemeralStorageAllocatable":
+			if max == nil {
+				max = node.Status.Allocatable.StorageEphemeral()
+			} else {
+				if node.Status.Allocatable.StorageEphemeral().Cmp(*max) == 1 {
+					max = node.Status.Allocatable.StorageEphemeral()
+				}
+			}
+			break
+
+		}
+	}
+
+	return max, nil
+}
+
 func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta1.NodeResourceFilters) (bool, error) {
 	if filters == nil {
 		return true, nil


Test patch:
diff --git a/pkg/analyze/node_resources_test.go b/pkg/analyze/node_resources_test.go
index 9353c271..166844e0 100644
--- a/pkg/analyze/node_resources_test.go
+++ b/pkg/analyze/node_resources_test.go
@@ -8,44 +8,589 @@ 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
-		actual      int
-		expected    bool
+		name           string
+		conditional    string
+		totalNodeCount int
+		matchingNodes  []corev1.Node
+		expected       bool
 	}{
 		{
 			name:        "=",
-			conditional: "= 5",
-			actual:      5,
-			expected:    true,
+			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:        "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:        "<",
+			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:        "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:        ">",
+			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:        "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:        "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:        "<= (pass)",
-			conditional: "<= 5",
-			actual:      4,
-			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:        "<= (fail)",
-			conditional: "<= 5",
-			actual:      6,
-			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:        "> (pass)",
-			conditional: "> 5",
-			actual:      6,
-			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,
 		},
 		{
-			name:        ">=(fail)",
-			conditional: ">= 5",
-			actual:      4,
-			expected:    false,
+			name:        "max(cpuCapacity) == 12 (false)",
+			conditional: "max(cpuCapacity) == 12",
+			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,
+		},
+		{
+			name:        "max(cpuCapacity) == 2 (true)",
+			conditional: "max(cpuCapacity) == 2",
+			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:       true,
+		},
+		{
+			name:        "sum(cpuCapacity) > 32 (true)",
+			conditional: "sum(cpuCapacity) > 32",
+			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("8"),
+							"ephemeral-storage": resource.MustParse("20959212Ki"),
+							"memory":            resource.MustParse("17951376Ki"),
+							"pods":              resource.MustParse("29"),
+						},
+						Allocatable: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"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("8"),
+							"ephemeral-storage": resource.MustParse("20959212Ki"),
+							"memory":            resource.MustParse("7951376Ki"),
+							"pods":              resource.MustParse("29"),
+						},
+						Allocatable: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"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: "node3",
+					},
+					Status: corev1.NodeStatus{
+						Capacity: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"ephemeral-storage": resource.MustParse("20959212Ki"),
+							"memory":            resource.MustParse("7951376Ki"),
+							"pods":              resource.MustParse("29"),
+						},
+						Allocatable: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"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: "node4",
+					},
+					Status: corev1.NodeStatus{
+						Capacity: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"ephemeral-storage": resource.MustParse("20959212Ki"),
+							"memory":            resource.MustParse("7951376Ki"),
+							"pods":              resource.MustParse("29"),
+						},
+						Allocatable: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"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: "node5",
+					},
+					Status: corev1.NodeStatus{
+						Capacity: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"ephemeral-storage": resource.MustParse("20959212Ki"),
+							"memory":            resource.MustParse("7951376Ki"),
+							"pods":              resource.MustParse("29"),
+						},
+						Allocatable: corev1.ResourceList{
+							"cpu":               resource.MustParse("8"),
+							"ephemeral-storage": resource.MustParse("19316009748"),
+							"memory":            resource.MustParse("7848976Ki"),
+							"pods":              resource.MustParse("29"),
+						},
+					},
+				},
+			},
+			totalNodeCount: 2,
+			expected:       true,
 		},
 	}
 
@@ -53,7 +598,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.actual)
+			actual, err := compareNodeResourceConditionalToActual(test.conditional, test.matchingNodes, test.totalNodeCount)
 			req.NoError(err)
 
 			assert.Equal(t, test.expected, actual)