mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
feat(analyze): add countDistinct() aggregate to nodeResources (#2079)
Add a countDistinct(<labelKey>) aggregate to the nodeResources analyzer "when" expression language. It counts distinct values of a node label across the filtered nodes and returns an int the existing comparison operators evaluate. Enables the AIR-238 3-AZ preflight: warn when Keeper-eligible nodes do not span 3 availability zones, e.g. "countDistinct(topology.kubernetes.io/zone) < 3". Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Xav Paice <xavpaice@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Xav Paice
parent
3c5bf74750
commit
3db6a99d3a
@@ -219,6 +219,8 @@ func compareNodeResourceConditionalToActual(conditional string, matchingNodes []
|
||||
switch function {
|
||||
case "count":
|
||||
actualValue = len(matchingNodes)
|
||||
case "countDistinct":
|
||||
actualValue = countDistinctLabelValues(matchingNodes, property)
|
||||
case "min":
|
||||
actualValue = findMin(matchingNodes, property, resourceName)
|
||||
case "max":
|
||||
@@ -368,6 +370,19 @@ func getQuantity(node corev1.Node, property string, resourceName string) *resour
|
||||
return nil
|
||||
}
|
||||
|
||||
// countDistinctLabelValues returns the number of distinct values of labelKey
|
||||
// across the given nodes. Nodes missing the label are ignored, so an absent
|
||||
// label yields 0.
|
||||
func countDistinctLabelValues(nodes []corev1.Node, labelKey string) int {
|
||||
seen := map[string]struct{}{}
|
||||
for _, node := range nodes {
|
||||
if v, ok := node.Labels[labelKey]; ok {
|
||||
seen[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
func findSum(nodes []corev1.Node, property string, resourceName string) *resource.Quantity {
|
||||
sum := resource.Quantity{}
|
||||
|
||||
|
||||
@@ -1666,6 +1666,116 @@ func Test_analyzeNodeResources(t *testing.T) {
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "countDistinct spans at least 3 instance types", // countDistinct pass path across all nodes
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "instance-type spread",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "countDistinct(node.kubernetes.io/instance-type) < 3",
|
||||
Message: "Fewer than 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "At least 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: true,
|
||||
IsFail: false,
|
||||
IsWarn: false,
|
||||
Title: "instance-type spread",
|
||||
Message: "At least 3 distinct instance types.",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "countDistinct only counts filtered nodes", // filtering to one pool leaves a single distinct value
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "filtered instance-type spread",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "countDistinct(node.kubernetes.io/instance-type) < 3",
|
||||
Message: "Fewer than 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "At least 3 distinct instance types.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: &troubleshootv1beta2.NodeResourceFilters{
|
||||
Selector: &troubleshootv1beta2.NodeResourceSelectors{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "node.kubernetes.io/instance-type",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"s-2vcpu-4gb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsFail: false,
|
||||
IsWarn: true,
|
||||
Title: "filtered instance-type spread",
|
||||
Message: "Fewer than 3 distinct instance types.",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "countDistinct is 0 when the label is absent", // AIR-238 zone syntax; fixture nodes carry no zone label
|
||||
analyzer: &troubleshootv1beta2.NodeResources{
|
||||
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
|
||||
CheckName: "zone spread",
|
||||
},
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "countDistinct(topology.kubernetes.io/zone) < 3",
|
||||
Message: "Nodes span fewer than 3 availability zones.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
Message: "Nodes span at least 3 availability zones.",
|
||||
URI: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsFail: false,
|
||||
IsWarn: true,
|
||||
Title: "zone spread",
|
||||
Message: "Nodes span fewer than 3 availability zones.",
|
||||
URI: "",
|
||||
IconKey: "kubernetes_node_resources",
|
||||
IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
getExampleNodeContents := func(nodeName string) ([]byte, error) {
|
||||
|
||||
Reference in New Issue
Block a user