Skip to content

Commit 784918e

Browse files
authored
feat(preflight): adding warning message when validating the content of preflight and host preflight spec (#1250)
1 parent 819dd5f commit 784918e

14 files changed

+491
-0
lines changed

pkg/preflight/run.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ func RunPreflights(interactive bool, output string, format string, args []string
4747
return err
4848
}
4949

50+
warning := validatePreflight(specs)
51+
52+
if warning != nil {
53+
fmt.Println(warning.Warning())
54+
return nil
55+
}
56+
5057
var collectResults []CollectResult
5158
var uploadCollectResults []CollectResult
5259
preflightSpecName := ""

pkg/preflight/validate_specs.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package preflight
2+
3+
import (
4+
"reflect"
5+
6+
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
7+
"github.com/replicatedhq/troubleshoot/pkg/multitype"
8+
"github.com/replicatedhq/troubleshoot/pkg/types"
9+
)
10+
11+
// validatePreflight validates the preflight spec and returns a warning if there is any
12+
func validatePreflight(specs PreflightSpecs) *types.ExitCodeWarning {
13+
14+
if specs.PreflightSpec == nil && specs.HostPreflightSpec == nil {
15+
return types.NewExitCodeWarning("no preflight or host preflight spec was found")
16+
}
17+
18+
if specs.PreflightSpec != nil {
19+
warning := validatePreflightSpecItems(specs.PreflightSpec.Spec.Collectors, specs.PreflightSpec.Spec.Analyzers)
20+
if warning != nil {
21+
return warning
22+
}
23+
}
24+
25+
if specs.HostPreflightSpec != nil {
26+
warning := validateHostPreflightSpecItems(specs.HostPreflightSpec.Spec.Collectors, specs.HostPreflightSpec.Spec.Analyzers)
27+
if warning != nil {
28+
return warning
29+
}
30+
}
31+
32+
return nil
33+
}
34+
35+
// validatePreflightSpecItems validates the preflight spec items and returns a warning if there is any
36+
// clusterResources and clusterInfo collectors are added automatically to the preflight spec, cannot be excluded
37+
func validatePreflightSpecItems(collectors []*v1beta2.Collect, analyzers []*v1beta2.Analyze) *types.ExitCodeWarning {
38+
var numberOfExcludedCollectors, numberOfExcludedAnalyzers int
39+
var numberOfExcludedDefaultCollectors int
40+
numberOfCollectors := len(collectors) + 2
41+
numberOfAnalyzers := len(analyzers)
42+
43+
if numberOfCollectors >= 0 {
44+
collectorsInterface := make([]interface{}, len(collectors))
45+
for i, v := range collectors {
46+
if v.ClusterInfo != nil || v.ClusterResources != nil {
47+
numberOfExcludedDefaultCollectors++
48+
}
49+
collectorsInterface[i] = v
50+
}
51+
52+
numberOfExcludedCollectors += countExcludedItems(collectorsInterface)
53+
54+
if numberOfExcludedCollectors+numberOfExcludedDefaultCollectors == numberOfCollectors {
55+
return types.NewExitCodeWarning("All collectors were excluded by the applied values")
56+
}
57+
}
58+
59+
// if there are no analyzers, return a warning
60+
// else check if all analyzers are excluded
61+
if numberOfAnalyzers == 0 {
62+
return types.NewExitCodeWarning("No analyzers found")
63+
} else {
64+
analyzersInterface := make([]interface{}, len(analyzers))
65+
for i, v := range analyzers {
66+
analyzersInterface[i] = v
67+
}
68+
69+
numberOfExcludedAnalyzers = countExcludedItems(analyzersInterface)
70+
71+
if numberOfExcludedAnalyzers == numberOfAnalyzers {
72+
return types.NewExitCodeWarning("All analyzers were excluded by the applied values")
73+
}
74+
}
75+
return nil
76+
}
77+
78+
// validateHostPreflightSpecItems validates the host preflight spec items and returns a warning if there is any
79+
// no collectors are added or excluded automatically to the host preflight spec
80+
func validateHostPreflightSpecItems(collectors []*v1beta2.HostCollect, analyzers []*v1beta2.HostAnalyze) *types.ExitCodeWarning {
81+
var numberOfExcludedCollectors, numberOfExcludedAnalyzers int
82+
numberOfCollectors := len(collectors)
83+
numberOfAnalyzers := len(analyzers)
84+
85+
// if there are no collectors, return a warning
86+
if numberOfCollectors == 0 {
87+
return types.NewExitCodeWarning("No collectors found")
88+
}
89+
90+
// if there are no analyzers, return a warning
91+
if numberOfAnalyzers == 0 {
92+
return types.NewExitCodeWarning("No analyzers found")
93+
}
94+
95+
collectorsInterface := make([]interface{}, len(collectors))
96+
for i, v := range collectors {
97+
collectorsInterface[i] = v
98+
}
99+
100+
analyzersInterface := make([]interface{}, len(analyzers))
101+
for i, v := range analyzers {
102+
analyzersInterface[i] = v
103+
}
104+
105+
numberOfExcludedCollectors = countExcludedItems(collectorsInterface)
106+
numberOfExcludedAnalyzers = countExcludedItems(analyzersInterface)
107+
108+
if numberOfExcludedCollectors == numberOfCollectors {
109+
return types.NewExitCodeWarning("All collectors were excluded by the applied values")
110+
}
111+
112+
if numberOfExcludedAnalyzers == numberOfAnalyzers {
113+
return types.NewExitCodeWarning("All analyzers were excluded by the applied values")
114+
}
115+
116+
return nil
117+
}
118+
119+
// countExcludedItems counts and returns the number of excluded items in the given items slice.
120+
// Items are assumed to be structures that may have an "Exclude" field as bool
121+
// If the "Exclude" field is true, the item is considered excluded.
122+
func countExcludedItems(items []interface{}) int {
123+
numberOfExcludedItems := 0
124+
for _, item := range items {
125+
itemElem := reflect.ValueOf(item).Elem()
126+
127+
// Loop over all fields of the current item.
128+
for i := 0; i < itemElem.NumField(); i++ {
129+
// Get the value of the current field.
130+
itemValue := itemElem.Field(i)
131+
// If the current field is a pointer to a struct, check if it has an "Exclude" field.
132+
if !itemValue.IsNil() {
133+
elem := itemValue.Elem()
134+
if elem.Kind() == reflect.Struct {
135+
// Look for a field named "Exclude" in the struct.
136+
excludeField := elem.FieldByName("Exclude")
137+
if excludeField.IsValid() {
138+
// Try to get the field's value as a *multitype.BoolOrString.
139+
excludeValue, ok := excludeField.Interface().(*multitype.BoolOrString)
140+
// If the field's value was successfully obtained and is not nil, and the value is true
141+
if ok && excludeValue != nil {
142+
if excludeValue.BoolOrDefaultFalse() {
143+
numberOfExcludedItems++
144+
}
145+
}
146+
}
147+
}
148+
}
149+
}
150+
}
151+
return numberOfExcludedItems
152+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package preflight
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/replicatedhq/troubleshoot/internal/testutils"
8+
"github.com/replicatedhq/troubleshoot/pkg/types"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestValidatePreflight(t *testing.T) {
13+
testingFiles := map[string]string{
14+
"noCollectorsPreflightFile": "troubleshoot_v1beta2_preflight_validate_empty_collectors_gotest.yaml",
15+
"noAnalyzersPreflightFile": "troubleshoot_v1beta2_preflight_validate_empty_analyzers_gotest.yaml",
16+
"excludedAllDefaultCollectorsPreflightFile": "troubleshoot_v1beta2_preflight_validate_excluded_all_default_collectors_gotest.yaml",
17+
"excludedOneDefaultCollectorsPreflightFile": "troubleshoot_v1beta2_preflight_validate_excluded_one_default_collectors_gotest.yaml",
18+
"excludedAllNonCollectorsPreflightFile": "troubleshoot_v1beta2_preflight_validate_excluded_all_non_default_collectors_gotest.yaml",
19+
"excludedAnalyzersPreflightFile": "troubleshoot_v1beta2_preflight_validate_excluded_analyzers_gotest.yaml",
20+
"noCollectorsHostPreflightFile": "troubleshoot_v1beta2_host_preflight_validate_empty_collectors_gotest.yaml",
21+
"noAnalyzersHostPreflightFile": "troubleshoot_v1beta2_host_preflight_validate_empty_analyzers_gotest.yaml",
22+
"excludedHostCollectorsPreflightFile": "troubleshoot_v1beta2_host_preflight_validate_excluded_collectors_gotest.yaml",
23+
"excludedHostAnalyzersPreflightFile": "troubleshoot_v1beta2_host_preflight_validate_excluded_analyzers_gotest.yaml",
24+
}
25+
26+
tests := []struct {
27+
name string
28+
preflightSpec string
29+
wantWarning *types.ExitCodeWarning
30+
}{
31+
{
32+
name: "empty-preflight",
33+
preflightSpec: "",
34+
wantWarning: types.NewExitCodeWarning("no preflight or host preflight spec was found"),
35+
},
36+
{
37+
name: "no-collectores",
38+
preflightSpec: testingFiles["noCollectorsPreflightFile"],
39+
wantWarning: nil,
40+
},
41+
{
42+
name: "no-analyzers",
43+
preflightSpec: testingFiles["noAnalyzersPreflightFile"],
44+
wantWarning: types.NewExitCodeWarning("No analyzers found"),
45+
},
46+
{
47+
name: "excluded-all-default-collectors",
48+
preflightSpec: testingFiles["excludedAllDefaultCollectorsPreflightFile"],
49+
wantWarning: types.NewExitCodeWarning("All collectors were excluded by the applied values"),
50+
},
51+
{
52+
name: "excluded-one-default-collectors",
53+
preflightSpec: testingFiles["excludedOneDefaultCollectorsPreflightFile"],
54+
wantWarning: nil,
55+
},
56+
{
57+
name: "excluded-all-non-default-collectors",
58+
preflightSpec: testingFiles["excludedAllNonCollectorsPreflightFile"],
59+
wantWarning: nil,
60+
},
61+
{
62+
name: "excluded-analyzers",
63+
preflightSpec: testingFiles["excludedAnalyzersPreflightFile"],
64+
wantWarning: types.NewExitCodeWarning("All analyzers were excluded by the applied values"),
65+
},
66+
{
67+
name: "no-host-preflight-collectores",
68+
preflightSpec: testingFiles["noCollectorsHostPreflightFile"],
69+
wantWarning: types.NewExitCodeWarning("No collectors found"),
70+
},
71+
{
72+
name: "no-host-preflight-analyzers",
73+
preflightSpec: testingFiles["noAnalyzersHostPreflightFile"],
74+
wantWarning: types.NewExitCodeWarning("No analyzers found"),
75+
},
76+
{
77+
name: "excluded-host-preflight-collectors",
78+
preflightSpec: testingFiles["excludedHostCollectorsPreflightFile"],
79+
wantWarning: types.NewExitCodeWarning("All collectors were excluded by the applied values"),
80+
},
81+
{
82+
name: "excluded-host-preflight-analyzers",
83+
preflightSpec: testingFiles["excludedHostAnalyzersPreflightFile"],
84+
wantWarning: types.NewExitCodeWarning("All analyzers were excluded by the applied values"),
85+
},
86+
}
87+
88+
for _, tt := range tests {
89+
t.Run(tt.name, func(t *testing.T) {
90+
testFilePath := filepath.Join(testutils.FileDir(), "../../testdata/preflightspec/"+tt.preflightSpec)
91+
specs := PreflightSpecs{}
92+
specs.Read([]string{testFilePath})
93+
gotWarning := validatePreflight(specs)
94+
assert.Equal(t, tt.wantWarning, gotWarning)
95+
})
96+
}
97+
}

pkg/types/types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package types
22

3+
import "fmt"
4+
35
type NotFoundError struct {
46
Name string
57
}
@@ -18,6 +20,10 @@ type ExitCodeError struct {
1820
Code int
1921
}
2022

23+
type ExitCodeWarning struct {
24+
Msg string
25+
}
26+
2127
func (e *ExitCodeError) Error() string {
2228
return e.Msg
2329
}
@@ -33,3 +39,11 @@ func NewExitCodeError(exitCode int, theErr error) *ExitCodeError {
3339
}
3440
return &ExitCodeError{Msg: useErr, Code: exitCode}
3541
}
42+
43+
func NewExitCodeWarning(theErrMsg string) *ExitCodeWarning {
44+
return &ExitCodeWarning{Msg: theErrMsg}
45+
}
46+
47+
func (e *ExitCodeWarning) Warning() string {
48+
return fmt.Sprintf("Warning: %s", e.Msg)
49+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: troubleshoot.sh/v1beta2
2+
kind: HostPreflight
3+
metadata:
4+
name: sample
5+
spec:
6+
collectors:
7+
- cpu: {}
8+
analyzers: []
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: troubleshoot.sh/v1beta2
2+
kind: HostPreflight
3+
metadata:
4+
name: sample
5+
spec:
6+
analyzers:
7+
- hostOS:
8+
outcomes:
9+
- fail:
10+
when: "ubuntu-16.04-kernel < 4.15"
11+
message: unsupported distribution
12+
- pass:
13+
when: "ubuntu-18.04-kernel >= 4.15"
14+
message: supported distribution
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
apiVersion: troubleshoot.sh/v1beta2
2+
kind: HostPreflight
3+
metadata:
4+
name: sample
5+
spec:
6+
collectors:
7+
- cpu: {}
8+
- diskUsage:
9+
collectorName: ephemeral
10+
path: /var/lib/kubelet
11+
analyzers:
12+
- cpu:
13+
exclude: true
14+
outcomes:
15+
- fail:
16+
when: "physical < 4"
17+
message: At least 4 physical CPU cores are required
18+
- fail:
19+
when: "logical < 8"
20+
message: At least 8 CPU cores are required
21+
- warn:
22+
when: "count < 16"
23+
message: At least 16 CPU cores preferred
24+
- pass:
25+
message: This server has sufficient CPU cores
26+
- diskUsage:
27+
exclude: true
28+
collectorName: ephemeral
29+
outcomes:
30+
- fail:
31+
when: "total < 20Gi"
32+
message: /var/lib/kubelet has less than 20Gi of total space
33+
- fail:
34+
when: "available < 10Gi"
35+
message: /var/lib/kubelet has less than 10Gi of disk space available
36+
- fail:
37+
when: "used/total > 70%"
38+
message: /var/lib/kubelet is more than 70% full
39+
- warn:
40+
when: "total < 40Gi"
41+
message: /var/lib/kubelet has less than 40Gi of total space
42+
- warn:
43+
when: "used/total > 60%"
44+
message: /var/lib/kubelet is more than 60% full
45+
- pass:
46+
when: "available/total >= 90%"
47+
message: /var/lib/kubelet has more than 90% available
48+
- pass:
49+
message: /var/lib/kubelet has sufficient disk space available
50+

0 commit comments

Comments
 (0)