Skip to content

Commit 4472f10

Browse files
authored
Merge pull request #127 from replicatedhq/node-analyzers
Node analyzers
2 parents 3982e20 + 879c3a6 commit 4472f10

File tree

9 files changed

+545
-7
lines changed

9 files changed

+545
-7
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
apiVersion: troubleshoot.replicated.com/v1beta1
2+
kind: Preflight
3+
metadata:
4+
name: sample
5+
spec:
6+
analyzers:
7+
- nodeResources:
8+
checkName: Must have at least 3 nodes in the cluster
9+
outcomes:
10+
- fail:
11+
when: "< 3"
12+
message: This application requires at least 3 nodes
13+
- warn:
14+
when: "< 5"
15+
message: This application recommends at last 5 nodes.
16+
- pass:
17+
message: This cluster has enough nodes.
18+
- nodeResources:
19+
checkName: Must have 3 nodes with at least 6 cores
20+
filters:
21+
cpuCapacity: "6"
22+
outcomes:
23+
- fail:
24+
when: "< 3"
25+
message: This application requires at least 3 nodes with 6 cores each
26+
- pass:
27+
message: This cluster has enough nodes with enough codes
28+
- nodeResources:
29+
checkName: Must have 1 node with 16 GB (available) memory and 5 cores (on a single node)
30+
filters:
31+
allocatableMemory: 16Gi
32+
cpuCapacity: "5"
33+
outcomes:
34+
- fail:
35+
when: "< 1"
36+
message: This application requires at least 1 node with 16GB available memory
37+
- pass:
38+
message: This cluster has a node with enough memory.

ffi/main.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ package main
33
import "C"
44

55
import (
6-
"fmt"
76
"encoding/json"
8-
9-
"gopkg.in/yaml.v2"
7+
"fmt"
8+
109
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
11-
"github.com/replicatedhq/troubleshoot/pkg/logger"
1210
"github.com/replicatedhq/troubleshoot/pkg/convert"
13-
11+
"github.com/replicatedhq/troubleshoot/pkg/logger"
12+
"gopkg.in/yaml.v2"
1413
)
1514

1615
//export Analyze
17-
func Analyze(bundleURL string, analyzers string, outputFormat string, compatibility string) *C.char {
16+
func Analyze(bundleURL string, analyzers string, outputFormat string, compatibility string) *C.char {
1817
logger.SetQuiet(true)
1918

2019
result, err := analyzer.DownloadAndAnalyze(bundleURL, analyzers)

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ require (
1818
github.com/hashicorp/go-getter v1.3.1-0.20190627223108-da0323b9545e
1919
github.com/hashicorp/go-multierror v1.0.0
2020
github.com/imdario/mergo v0.3.7 // indirect
21-
github.com/mattn/go-colorable v0.1.2 // indirect
2221
github.com/mholt/archiver v3.1.1+incompatible
2322
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
2423
github.com/nwaples/rardecode v1.0.0 // indirect
@@ -36,7 +35,9 @@ require (
3635
k8s.io/apimachinery v0.17.0
3736
k8s.io/cli-runtime v0.17.0
3837
k8s.io/client-go v0.17.0
38+
k8s.io/code-generator v0.16.5-beta.1 // indirect
3939
sigs.k8s.io/controller-runtime v0.4.0
40+
sigs.k8s.io/controller-tools v0.2.4 // indirect
4041
)
4142

4243
replace github.com/appscode/jsonpatch => github.com/gomodules/jsonpatch v2.0.1+incompatible

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
163163
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
164164
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
165165
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
166+
github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo=
167+
github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
166168
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
167169
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
168170
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
@@ -564,6 +566,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
564566
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
565567
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
566568
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
569+
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI=
567570
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
568571
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
569572
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -625,6 +628,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
625628
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
626629
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
627630
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
631+
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6CtBzjclw4pgm9gg8Ys0=
632+
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
628633
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
629634
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
630635
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -646,8 +651,11 @@ k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53
646651
k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg=
647652
k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k=
648653
k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE=
654+
k8s.io/code-generator v0.16.5-beta.1 h1:+zWxMQH3a6fd8lZe6utWyW/V7nmG2ZMXwtovSJI2p+0=
655+
k8s.io/code-generator v0.16.5-beta.1/go.mod h1:mJUgkl06XV4kstAnLHAIzJPVCOzVR+ZcfPIv4fUsFCY=
649656
k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA=
650657
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
658+
k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM=
651659
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
652660
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
653661
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
@@ -667,6 +675,8 @@ modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs
667675
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
668676
sigs.k8s.io/controller-runtime v0.4.0 h1:wATM6/m+3w8lj8FXNaO6Fs/rq/vqoOjO1Q116Z9NPsg=
669677
sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns=
678+
sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA=
679+
sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA=
670680
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
671681
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
672682
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=

pkg/analyze/analyzer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont
7979
}
8080
return analyzeDistribution(analyzer.Distribution, getFile)
8181
}
82+
if analyzer.NodeResources != nil {
83+
if analyzer.NodeResources.Exclude {
84+
return nil, nil
85+
}
86+
return analyzeNodeResources(analyzer.NodeResources, getFile)
87+
}
8288
if analyzer.TextAnalyze != nil {
8389
return analyzeTextAnalyze(analyzer.TextAnalyze, getFile)
8490
}

pkg/analyze/node_resources.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"strconv"
6+
"strings"
7+
8+
"github.com/pkg/errors"
9+
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
10+
corev1 "k8s.io/api/core/v1"
11+
"k8s.io/apimachinery/pkg/api/resource"
12+
)
13+
14+
func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
15+
collected, err := getCollectedFileContents("cluster-resources/nodes.json")
16+
if err != nil {
17+
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
18+
}
19+
20+
var nodes []corev1.Node
21+
if err := json.Unmarshal(collected, &nodes); err != nil {
22+
return nil, errors.Wrap(err, "failed to unmarshal node list")
23+
}
24+
25+
matchingNodeCount := 0
26+
27+
for _, node := range nodes {
28+
isMatch, err := nodeMatchesFilters(node, analyzer.Filters)
29+
if err != nil {
30+
return nil, errors.Wrap(err, "failed to check if node matches filter")
31+
}
32+
33+
if isMatch {
34+
matchingNodeCount++
35+
}
36+
}
37+
38+
title := analyzer.CheckName
39+
if title == "" {
40+
title = "Node Resources"
41+
}
42+
43+
result := &AnalyzeResult{
44+
Title: title,
45+
}
46+
47+
for _, outcome := range analyzer.Outcomes {
48+
if outcome.Fail != nil {
49+
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodeCount)
50+
if err != nil {
51+
return nil, errors.Wrap(err, "failed to parse when")
52+
}
53+
54+
if isWhenMatch {
55+
result.IsFail = true
56+
result.Message = outcome.Fail.Message
57+
result.URI = outcome.Fail.URI
58+
59+
return result, nil
60+
}
61+
} else if outcome.Warn != nil {
62+
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodeCount)
63+
if err != nil {
64+
return nil, errors.Wrap(err, "failed to parse when")
65+
}
66+
67+
if isWhenMatch {
68+
result.IsWarn = true
69+
result.Message = outcome.Warn.Message
70+
result.URI = outcome.Warn.URI
71+
72+
return result, nil
73+
}
74+
} else if outcome.Pass != nil {
75+
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodeCount)
76+
if err != nil {
77+
return nil, errors.Wrap(err, "failed to parse when")
78+
}
79+
80+
if isWhenMatch {
81+
result.IsPass = true
82+
result.Message = outcome.Pass.Message
83+
result.URI = outcome.Pass.URI
84+
85+
return result, nil
86+
}
87+
}
88+
}
89+
90+
return result, nil
91+
}
92+
93+
func compareNodeResourceConditionalToActual(conditional string, actual int) (bool, error) {
94+
if conditional == "" {
95+
return true, nil
96+
}
97+
98+
parts := strings.Split(strings.TrimSpace(conditional), " ")
99+
100+
if len(parts) != 2 {
101+
return false, errors.New("unable to parse nodeResources conditional")
102+
}
103+
104+
operator := parts[0]
105+
desiredValue, err := strconv.Atoi(parts[1])
106+
if err != nil {
107+
return false, errors.Wrap(err, "failed to parse nodeResource value")
108+
}
109+
110+
switch operator {
111+
case "=", "==", "===":
112+
return desiredValue == actual, nil
113+
case "<":
114+
return actual < desiredValue, nil
115+
case "<=":
116+
return actual <= desiredValue, nil
117+
case ">":
118+
return actual > desiredValue, nil
119+
case ">=":
120+
return actual >= desiredValue, nil
121+
}
122+
123+
return false, errors.New("unexpected conditional in nodeResources")
124+
}
125+
126+
func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta1.NodeResourceFilters) (bool, error) {
127+
if filters == nil {
128+
return true, nil
129+
}
130+
131+
// all filters must pass for this to pass
132+
133+
if filters.CPUCapacity != "" {
134+
parsed, err := resource.ParseQuantity(filters.CPUCapacity)
135+
if err != nil {
136+
return false, errors.Wrap(err, "failed to parse cpu capacity")
137+
}
138+
139+
if node.Status.Capacity.Cpu().Cmp(parsed) == -1 {
140+
return false, nil
141+
}
142+
}
143+
if filters.CPUAllocatable != "" {
144+
parsed, err := resource.ParseQuantity(filters.CPUAllocatable)
145+
if err != nil {
146+
return false, errors.Wrap(err, "failed to parse cpu allocatable")
147+
}
148+
149+
if node.Status.Allocatable.Cpu().Cmp(parsed) == -1 {
150+
return false, nil
151+
}
152+
}
153+
154+
if filters.MemoryCapacity != "" {
155+
parsed, err := resource.ParseQuantity(filters.MemoryCapacity)
156+
if err != nil {
157+
return false, errors.Wrap(err, "failed to parse memory capacity")
158+
}
159+
160+
if node.Status.Capacity.Memory().Cmp(parsed) == -1 {
161+
return false, nil
162+
}
163+
}
164+
if filters.MemoryAllocatable != "" {
165+
parsed, err := resource.ParseQuantity(filters.MemoryAllocatable)
166+
if err != nil {
167+
return false, errors.Wrap(err, "failed to parse memory allocatable")
168+
}
169+
170+
if node.Status.Allocatable.Memory().Cmp(parsed) == -1 {
171+
return false, nil
172+
}
173+
}
174+
175+
if filters.PodCapacity != "" {
176+
parsed, err := resource.ParseQuantity(filters.PodCapacity)
177+
if err != nil {
178+
return false, errors.Wrap(err, "failed to parse pod capacity")
179+
}
180+
181+
if node.Status.Capacity.Pods().Cmp(parsed) == -1 {
182+
return false, nil
183+
}
184+
}
185+
if filters.PodAllocatable != "" {
186+
parsed, err := resource.ParseQuantity(filters.PodAllocatable)
187+
if err != nil {
188+
return false, errors.Wrap(err, "failed to parse pod allocatable")
189+
}
190+
191+
if node.Status.Allocatable.Pods().Cmp(parsed) == -1 {
192+
return false, nil
193+
}
194+
}
195+
196+
if filters.EphemeralStorageCapacity != "" {
197+
parsed, err := resource.ParseQuantity(filters.EphemeralStorageCapacity)
198+
if err != nil {
199+
return false, errors.Wrap(err, "failed to parse ephemeralstorage capacity")
200+
}
201+
202+
if node.Status.Capacity.StorageEphemeral().Cmp(parsed) == -1 {
203+
return false, nil
204+
}
205+
}
206+
if filters.EphemeralStorageAllocatable != "" {
207+
parsed, err := resource.ParseQuantity(filters.EphemeralStorageAllocatable)
208+
if err != nil {
209+
return false, errors.Wrap(err, "failed to parse ephemeralstorage allocatable")
210+
}
211+
212+
if node.Status.Allocatable.StorageEphemeral().Cmp(parsed) == -1 {
213+
return false, nil
214+
}
215+
}
216+
217+
return true, nil
218+
}

0 commit comments

Comments
 (0)