Skip to content

Commit 0d1e236

Browse files
danj-replicatedNathan Sullivan
andauthored
Add ability for Cluster resources analyzer to do number and size comparison (#1210)
* make CR analyzer compare ints and sizes * schemas * typo * more comments * tests for cluster resources analyzer doing number/size comparisons * switch from humanize to k8s size parsing * schemas --------- Co-authored-by: Nathan Sullivan <[email protected]>
1 parent 70f0e07 commit 0d1e236

File tree

3 files changed

+210
-5
lines changed

3 files changed

+210
-5
lines changed

pkg/analyze/common_status.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey
4747
}
4848
}
4949

50-
match, err := compareActualToWhen(outcome.Fail.When, readyReplicas, exists)
50+
match, err := compareActualToWhen(outcome.Fail.When, readyReplicas)
5151
if err != nil {
5252
return nil, errors.Wrap(err, "failed to parse fail range")
5353
}
@@ -87,7 +87,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey
8787
}
8888
}
8989

90-
match, err := compareActualToWhen(outcome.Warn.When, readyReplicas, exists)
90+
match, err := compareActualToWhen(outcome.Warn.When, readyReplicas)
9191
if err != nil {
9292
return nil, errors.Wrap(err, "failed to parse warn range")
9393
}
@@ -127,7 +127,7 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey
127127
}
128128
}
129129

130-
match, err := compareActualToWhen(outcome.Pass.When, readyReplicas, exists)
130+
match, err := compareActualToWhen(outcome.Pass.When, readyReplicas)
131131
if err != nil {
132132
return nil, errors.Wrap(err, "failed to parse pass range")
133133
}
@@ -145,9 +145,8 @@ func commonStatus(outcomes []*troubleshootv1beta2.Outcome, name string, iconKey
145145
return result, nil
146146
}
147147

148-
func compareActualToWhen(when string, actual int, exists bool) (bool, error) {
148+
func compareActualToWhen(when string, actual int) (bool, error) {
149149
parts := strings.Split(strings.TrimSpace(when), " ")
150-
151150
// we can make this a lot more flexible
152151
if len(parts) != 2 {
153152
return false, errors.New("unable to parse when range")

pkg/analyze/kube_resource.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"path/filepath"
66
"reflect"
77
"strconv"
8+
"strings"
89

910
"github.com/pkg/errors"
1011
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
1112
"github.com/replicatedhq/troubleshoot/pkg/constants"
1213
iutils "github.com/replicatedhq/troubleshoot/pkg/interfaceutils"
1314
"gopkg.in/yaml.v2"
15+
"k8s.io/apimachinery/pkg/api/resource"
1416
"k8s.io/klog/v2"
1517
)
1618

@@ -108,6 +110,133 @@ func FindResource(kind string, clusterScoped bool, namespace string, name string
108110

109111
}
110112

113+
func compareWhentoResource(w string, actual interface{}) (bool, error) {
114+
115+
// check our "when" has operators
116+
var whenSplit []string
117+
if strings.ContainsAny(w, "!=<>") {
118+
whenSplit = strings.Split(strings.TrimSpace(w), " ")
119+
} else {
120+
return false, errors.New("no operators found")
121+
}
122+
123+
// let's first check if we can cast "actual" as an int, that should inform us what comparison we're doing
124+
actualAsInt, ok := actual.(int)
125+
if ok {
126+
// it's an int! we can do integer comparison here
127+
// we're going to re-use an ill-fitting bit of code from the deployment analyzer for now
128+
return compareActualToWhen(w, actualAsInt)
129+
}
130+
131+
// if we've fallen through here we're going to have to try a bit harder to work out what we're comparing
132+
// let's try making it a string
133+
actualAsString, ok := actual.(string)
134+
if !ok {
135+
return false, errors.New("could not cast found value as string")
136+
}
137+
138+
// now we can try checking if it's a "quantity"
139+
actualASQuantity, err := resource.ParseQuantity(actualAsString)
140+
if err == nil {
141+
// it's probably a size, we can do some comparison here
142+
// but I'm being lazy here so we'll convert our last argument to an int and throw it back at our existing int comparison function
143+
whenAsQuantity, err := resource.ParseQuantity(whenSplit[1])
144+
if err != nil {
145+
// our when wasn't a size! naughty user
146+
return false, errors.New("Cannot compare size with not size")
147+
}
148+
whenIntAsString := strconv.FormatInt(whenAsQuantity.Value(), 10)
149+
// re-use that same compare function from earlier, might as well
150+
return compareActualToWhen(whenSplit[0]+" "+whenIntAsString, int(actualASQuantity.Value()))
151+
152+
}
153+
154+
return false, errors.New("could not match comparison method for result")
155+
}
156+
157+
func analyzeWhenField(actual interface{}, outcomes []*troubleshootv1beta2.Outcome, checkName string) (*AnalyzeResult, error) {
158+
159+
result := &AnalyzeResult{
160+
Title: checkName,
161+
IconKey: "kubernetes_text_analyze",
162+
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16",
163+
}
164+
165+
for _, outcome := range outcomes {
166+
if outcome.Fail != nil {
167+
if outcome.Fail.When != "" {
168+
compareResult, err := compareWhentoResource(outcome.Fail.When, actual)
169+
if err != nil {
170+
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Fail.When)
171+
}
172+
if compareResult {
173+
result.IsFail = true
174+
result.Message = outcome.Fail.Message
175+
result.URI = outcome.Fail.URI
176+
return result, nil
177+
}
178+
} else {
179+
result.IsFail = true
180+
result.Message = outcome.Fail.Message
181+
result.URI = outcome.Fail.URI
182+
183+
return result, nil
184+
}
185+
}
186+
if outcome.Warn != nil {
187+
188+
if outcome.Warn.When != "" {
189+
compareResult, err := compareWhentoResource(outcome.Fail.When, actual)
190+
if err != nil {
191+
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Warn.When)
192+
}
193+
if compareResult {
194+
result.IsWarn = true
195+
result.Message = outcome.Warn.Message
196+
result.URI = outcome.Warn.URI
197+
return result, nil
198+
}
199+
} else {
200+
result.IsWarn = true
201+
result.Message = outcome.Warn.Message
202+
result.URI = outcome.Warn.URI
203+
204+
return result, nil
205+
}
206+
207+
}
208+
if outcome.Pass != nil {
209+
210+
if outcome.Pass.When != "" {
211+
compareResult, err := compareWhentoResource(outcome.Pass.When, actual)
212+
if err != nil {
213+
return nil, errors.Wrapf(err, "failed to process when statement: %s", outcome.Pass.When)
214+
}
215+
if compareResult {
216+
result.IsPass = true
217+
result.Message = outcome.Pass.Message
218+
result.URI = outcome.Pass.URI
219+
return result, nil
220+
}
221+
} else {
222+
result.IsPass = true
223+
result.Message = outcome.Pass.Message
224+
result.URI = outcome.Pass.URI
225+
226+
return result, nil
227+
}
228+
}
229+
}
230+
231+
return &AnalyzeResult{
232+
Title: checkName,
233+
IconKey: "kubernetes_text_analyze",
234+
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
235+
IsFail: true,
236+
Message: "Invalid analyzer",
237+
}, nil
238+
}
239+
111240
func (a *AnalyzeClusterResource) analyzeResource(analyzer *troubleshootv1beta2.ClusterResource, getFileContents getCollectedFileContents) (*AnalyzeResult, error) {
112241
selected, err := FindResource(analyzer.Kind, analyzer.ClusterScoped, analyzer.Namespace, analyzer.Name, getFileContents)
113242
if err != nil {
@@ -168,6 +297,15 @@ func (a *AnalyzeClusterResource) analyzeResource(analyzer *troubleshootv1beta2.C
168297
if result != nil {
169298
return result, nil
170299
}
300+
} else {
301+
// fall through to comparing from the when key
302+
result, err := analyzeWhenField(actual, analyzer.Outcomes, a.Title())
303+
if err != nil {
304+
return nil, err
305+
}
306+
if result != nil {
307+
return result, nil
308+
}
171309
}
172310

173311
return &AnalyzeResult{

pkg/analyze/kube_resource_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,74 @@ func Test_analyzeResource(t *testing.T) {
257257
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16",
258258
},
259259
},
260+
{
261+
name: "pass-when-pvc-exists-and-is-at-least-4Gi",
262+
analyzer: troubleshootv1beta2.ClusterResource{
263+
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
264+
CheckName: "check-pvc-is-at-least-4Gi",
265+
},
266+
Kind: "PersistentVolumeClaim",
267+
Name: "data-postgresql-0",
268+
Namespace: "default",
269+
YamlPath: "spec.resources.requests.storage",
270+
Outcomes: []*troubleshootv1beta2.Outcome{
271+
{
272+
Pass: &troubleshootv1beta2.SingleOutcome{
273+
When: ">= 4Gi",
274+
Message: "pass",
275+
},
276+
},
277+
{
278+
Fail: &troubleshootv1beta2.SingleOutcome{
279+
Message: "fail",
280+
},
281+
},
282+
},
283+
},
284+
expectResult: AnalyzeResult{
285+
IsPass: true,
286+
IsWarn: false,
287+
IsFail: false,
288+
Title: "check-pvc-is-at-least-4Gi",
289+
Message: "pass",
290+
IconKey: "kubernetes_text_analyze",
291+
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16",
292+
},
293+
},
294+
{
295+
name: "fail-when-pvc-exists-and-is-not-at-least-16Gi",
296+
analyzer: troubleshootv1beta2.ClusterResource{
297+
AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
298+
CheckName: "check-pvc-is-at-least-16Gi",
299+
},
300+
Kind: "PersistentVolumeClaim",
301+
Name: "data-postgresql-0",
302+
Namespace: "default",
303+
YamlPath: "spec.resources.requests.storage",
304+
Outcomes: []*troubleshootv1beta2.Outcome{
305+
{
306+
Pass: &troubleshootv1beta2.SingleOutcome{
307+
When: ">= 16Gi",
308+
Message: "pass",
309+
},
310+
},
311+
{
312+
Fail: &troubleshootv1beta2.SingleOutcome{
313+
Message: "fail",
314+
},
315+
},
316+
},
317+
},
318+
expectResult: AnalyzeResult{
319+
IsPass: false,
320+
IsWarn: false,
321+
IsFail: true,
322+
Title: "check-pvc-is-at-least-16Gi",
323+
Message: "fail",
324+
IconKey: "kubernetes_text_analyze",
325+
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg?w=13&h=16",
326+
},
327+
},
260328
}
261329
{
262330
for _, test := range tests {

0 commit comments

Comments
 (0)