From 0a2c9c74ab5e730c565eb84b21ffa72aacf1d139 Mon Sep 17 00:00:00 2001 From: Dexter Yan Date: Mon, 2 Sep 2024 09:42:40 +1200 Subject: [PATCH] feat(analyzer): allow templating for Node Resources Analyzer (#1605) * feat(analyzer): allow templating for Node Resources Analyzer --- config/crds/troubleshoot.sh_analyzers.yaml | 4 +- config/crds/troubleshoot.sh_preflights.yaml | 4 +- .../crds/troubleshoot.sh_supportbundles.yaml | 4 +- pkg/analyze/node_resources.go | 32 ++- pkg/analyze/node_resources_test.go | 199 +++++++++++++++++- .../troubleshoot/v1beta2/analyzer_shared.go | 2 +- schemas/analyzer-troubleshoot-v1beta2.json | 4 +- schemas/preflight-troubleshoot-v1beta2.json | 4 +- .../supportbundle-troubleshoot-v1beta2.json | 4 +- 9 files changed, 234 insertions(+), 23 deletions(-) diff --git a/config/crds/troubleshoot.sh_analyzers.yaml b/config/crds/troubleshoot.sh_analyzers.yaml index bd9d4ff9..a879627a 100644 --- a/config/crds/troubleshoot.sh_analyzers.yaml +++ b/config/crds/troubleshoot.sh_analyzers.yaml @@ -1166,10 +1166,10 @@ spec: type: BoolString filters: properties: - architecture: - type: string cpuAllocatable: type: string + cpuArchitecture: + type: string cpuCapacity: type: string ephemeralStorageAllocatable: diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml index 12b4e656..aa6a932b 100644 --- a/config/crds/troubleshoot.sh_preflights.yaml +++ b/config/crds/troubleshoot.sh_preflights.yaml @@ -1166,10 +1166,10 @@ spec: type: BoolString filters: properties: - architecture: - type: string cpuAllocatable: type: string + cpuArchitecture: + type: string cpuCapacity: type: string ephemeralStorageAllocatable: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 2b3ff1a3..eea7f3f0 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -1197,10 +1197,10 @@ spec: type: BoolString filters: properties: - architecture: - type: string cpuAllocatable: type: string + cpuArchitecture: + type: string cpuCapacity: type: string ephemeralStorageAllocatable: diff --git a/pkg/analyze/node_resources.go b/pkg/analyze/node_resources.go index 37618e6c..7698593d 100644 --- a/pkg/analyze/node_resources.go +++ b/pkg/analyze/node_resources.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/pkg/errors" + util "github.com/replicatedhq/troubleshoot/internal/util" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/constants" corev1 "k8s.io/api/core/v1" @@ -18,12 +19,16 @@ type AnalyzeNodeResources struct { analyzer *troubleshootv1beta2.NodeResources } +type NodeResourceMsg struct { + *troubleshootv1beta2.NodeResourceFilters + NodeCount int +} + func (a *AnalyzeNodeResources) Title() string { title := a.analyzer.CheckName if title == "" { title = "Node Resources" } - return title } @@ -41,6 +46,7 @@ func (a *AnalyzeNodeResources) Analyze(getFile getCollectedFileContents, findFil } func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta2.NodeResources, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + collected, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NODES)) if err != nil { return nil, errors.Wrap(err, "failed to get contents of nodes.json") @@ -70,6 +76,10 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", } + nodeMsg := NodeResourceMsg{ + analyzer.Filters, len(matchingNodes), + } + for _, outcome := range analyzer.Outcomes { if outcome.Fail != nil { isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodes) @@ -79,9 +89,11 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta if isWhenMatch { result.IsFail = true - result.Message = outcome.Fail.Message + result.Message, err = util.RenderTemplate(outcome.Fail.Message, nodeMsg) + if err != nil { + return nil, errors.Wrap(err, "failed to render message template") + } result.URI = outcome.Fail.URI - return result, nil } } else if outcome.Warn != nil { @@ -92,7 +104,10 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta if isWhenMatch { result.IsWarn = true - result.Message = outcome.Warn.Message + result.Message, err = util.RenderTemplate(outcome.Warn.Message, nodeMsg) + if err != nil { + return nil, errors.Wrap(err, "failed to render message template") + } result.URI = outcome.Warn.URI return result, nil @@ -105,7 +120,10 @@ func (a *AnalyzeNodeResources) analyzeNodeResources(analyzer *troubleshootv1beta if isWhenMatch { result.IsPass = true - result.Message = outcome.Pass.Message + result.Message, err = util.RenderTemplate(outcome.Pass.Message, nodeMsg) + if err != nil { + return nil, errors.Wrap(err, "failed to render message template") + } result.URI = outcome.Pass.URI return result, nil @@ -373,8 +391,8 @@ func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta2.NodeResou } } - if filters.Architecture != "" { - parsed := filters.Architecture + if filters.CPUArchitecture != "" { + parsed := filters.CPUArchitecture if !strings.EqualFold(node.Status.NodeInfo.Architecture, parsed) { return false, nil diff --git a/pkg/analyze/node_resources_test.go b/pkg/analyze/node_resources_test.go index ac8b1c0f..87e37327 100644 --- a/pkg/analyze/node_resources_test.go +++ b/pkg/analyze/node_resources_test.go @@ -445,7 +445,7 @@ func Test_nodeMatchesFilters(t *testing.T) { name: "true when cpu arch is amd64", node: node, filters: &troubleshootv1beta2.NodeResourceFilters{ - Architecture: "amd64", + CPUArchitecture: "amd64", }, expectResult: true, }, @@ -453,7 +453,7 @@ func Test_nodeMatchesFilters(t *testing.T) { name: "false when cpu arch is not amd64", node: node, filters: &troubleshootv1beta2.NodeResourceFilters{ - Architecture: "armhf", + CPUArchitecture: "armhf", }, expectResult: false, }, @@ -751,7 +751,44 @@ func Test_analyzeNodeResources(t *testing.T) { }, }, Filters: &troubleshootv1beta2.NodeResourceFilters{ - Architecture: "amd64", + CPUArchitecture: "amd64", + }, + }, + want: &AnalyzeResult{ + IsPass: true, + IsFail: false, + IsWarn: false, + Title: "amd64-exists", + Message: "There is a node with at least 8 cores on amd64 arch", + URI: "", + IconKey: "kubernetes_node_resources", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", + }, + }, + { + name: "at least 8 cores on amd64 with message templating", // filter for a node with enough amd64 cores with message templating + analyzer: &troubleshootv1beta2.NodeResources{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "amd64-exists", + }, + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "max(cpuCapacity) < 8", + Message: "There isn't a node with 8 or more cores on {{ .CPUArchitecture }} arch", + URI: "", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "max(cpuCapacity) >= 8", + Message: "There is a node with at least 8 cores on {{ .CPUArchitecture }} arch", + URI: "", + }, + }, + }, + Filters: &troubleshootv1beta2.NodeResourceFilters{ + CPUArchitecture: "amd64", }, }, want: &AnalyzeResult{ @@ -898,6 +935,162 @@ func Test_analyzeNodeResources(t *testing.T) { IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", }, }, + { + name: "8 cores in nodes with at least 8gb of ram with message templating", // validate that filtering based on memory capacity works with message templating + analyzer: &troubleshootv1beta2.NodeResources{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "memory filter", + }, + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "sum(cpuCapacity) < 8", + Message: "less than 8 CPUs in nodes with {{ .MemoryCapacity }} of ram", + URI: "", + }, + }, + { + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "sum(cpuCapacity) = 8", + Message: "exactly 8 CPUs total in nodes with {{ .MemoryCapacity }} of ram", + URI: "", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "sum(cpuCapacity) > 8", + Message: "more than 8 CPUs in nodes with {{ .MemoryCapacity }} of ram", + URI: "", + }, + }, + }, + Filters: &troubleshootv1beta2.NodeResourceFilters{ + MemoryCapacity: "8Gi", + }, + }, + want: &AnalyzeResult{ + IsPass: true, + IsFail: false, + IsWarn: false, + Title: "memory filter", + Message: "more than 8 CPUs in nodes with 8Gi of ram", + URI: "", + IconKey: "kubernetes_node_resources", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", + }, + }, + { + name: "at least 1 node on arm64 with message templating", // filter for arm64 nodes with message templating + analyzer: &troubleshootv1beta2.NodeResources{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "arm64-exists", + }, + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "count() < 3", + Message: "This application requires at least 3 nodes. {{ .CPUArchitecture }}, it should only return the {{ .NodeCount }} nodes that match that filter", + URI: "", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "count() >= 3", + Message: "There are {{ .NodeCount }} nodes that match that filter", + URI: "", + }, + }, + }, + Filters: &troubleshootv1beta2.NodeResourceFilters{ + CPUArchitecture: "arm64", + }, + }, + want: &AnalyzeResult{ + IsPass: false, + IsFail: true, + IsWarn: false, + Title: "arm64-exists", + Message: "This application requires at least 3 nodes. arm64, it should only return the 0 nodes that match that filter", + URI: "", + IconKey: "kubernetes_node_resources", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", + }, + }, + { + name: "at least 1 node on amd64 with message templating", // filter for amd64 nodes with message templating + analyzer: &troubleshootv1beta2.NodeResources{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "amd64-exists", + }, + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "count() < 3", + Message: "This application requires at least 3 nodes. {{ .CPUArchitecture }}, it should only return the {{ .NodeCount }} nodes that match that filter", + URI: "", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "count() >= 3", + Message: "There are {{ .NodeCount }} nodes that match that filter", + URI: "", + }, + }, + }, + Filters: &troubleshootv1beta2.NodeResourceFilters{ + CPUArchitecture: "amd64", + }, + }, + want: &AnalyzeResult{ + IsPass: true, + IsFail: false, + IsWarn: false, + Title: "amd64-exists", + Message: "There are 6 nodes that match that filter", + URI: "", + IconKey: "kubernetes_node_resources", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", + }, + }, + { + name: "Only 5 Nodes with amd64 and 2 CPU with message templating", // filter for amd64 and 2 CPU nodes with message templating + analyzer: &troubleshootv1beta2.NodeResources{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "amd64-exists", + }, + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "count() < 3", + Message: "This application requires at least 3 nodes. {{ .CPUArchitecture }}, it should only return the {{ .NodeCount }} nodes that match that filter", + URI: "", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "count() >= 3", + Message: "There are {{ .NodeCount }} nodes that match that filter {{ .CPUArchitecture }} and {{ .CPUCapacity }} CPU cores", + URI: "", + }, + }, + }, + Filters: &troubleshootv1beta2.NodeResourceFilters{ + CPUArchitecture: "amd64", + CPUCapacity: "2", + }, + }, + want: &AnalyzeResult{ + IsPass: true, + IsFail: false, + IsWarn: false, + Title: "amd64-exists", + Message: "There are 5 nodes that match that filter amd64 and 2 CPU cores", + URI: "", + IconKey: "kubernetes_node_resources", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/node-resources.svg?w=16&h=18", + }, + }, { name: "no pass or fail", // validate that the pass message is not always shown diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go index 216e4553..297a4163 100644 --- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go @@ -120,7 +120,7 @@ type NodeResources struct { } type NodeResourceFilters struct { - Architecture string `json:"architecture,omitempty" yaml:"cpuArchitecture,omitempty"` + CPUArchitecture string `json:"cpuArchitecture,omitempty" yaml:"cpuArchitecture,omitempty"` CPUCapacity string `json:"cpuCapacity,omitempty" yaml:"cpuCapacity,omitempty"` CPUAllocatable string `json:"cpuAllocatable,omitempty" yaml:"cpuAllocatable,omitempty"` MemoryCapacity string `json:"memoryCapacity,omitempty" yaml:"memoryCapacity,omitempty"` diff --git a/schemas/analyzer-troubleshoot-v1beta2.json b/schemas/analyzer-troubleshoot-v1beta2.json index d11fa867..06fc51e1 100644 --- a/schemas/analyzer-troubleshoot-v1beta2.json +++ b/schemas/analyzer-troubleshoot-v1beta2.json @@ -1755,10 +1755,10 @@ "filters": { "type": "object", "properties": { - "architecture": { + "cpuAllocatable": { "type": "string" }, - "cpuAllocatable": { + "cpuArchitecture": { "type": "string" }, "cpuCapacity": { diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json index 8676d0e2..0dde4749 100644 --- a/schemas/preflight-troubleshoot-v1beta2.json +++ b/schemas/preflight-troubleshoot-v1beta2.json @@ -1755,10 +1755,10 @@ "filters": { "type": "object", "properties": { - "architecture": { + "cpuAllocatable": { "type": "string" }, - "cpuAllocatable": { + "cpuArchitecture": { "type": "string" }, "cpuCapacity": { diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 10c9f989..60791f11 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -1801,10 +1801,10 @@ "filters": { "type": "object", "properties": { - "architecture": { + "cpuAllocatable": { "type": "string" }, - "cpuAllocatable": { + "cpuArchitecture": { "type": "string" }, "cpuCapacity": {