Skip to content

Commit e32aada

Browse files
authored
commands/.../scorecard: modularize tests and implement weights (#994)
* commands/.../scorecard: modularize tests and implement weights
1 parent 45fbe73 commit e32aada

File tree

6 files changed

+506
-312
lines changed

6 files changed

+506
-312
lines changed

commands/operator-sdk/cmd/scorecard/basic_tests.go

Lines changed: 33 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -18,155 +18,52 @@ import (
1818
"context"
1919
"encoding/json"
2020
"fmt"
21-
"math/rand"
22-
"reflect"
2321
"strings"
24-
"time"
2522

26-
"github.com/spf13/viper"
27-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2823
"k8s.io/apimachinery/pkg/types"
29-
"k8s.io/apimachinery/pkg/util/wait"
30-
"sigs.k8s.io/controller-runtime/pkg/client"
3124
)
3225

33-
// checkSpecAndStat checks that the spec and status blocks exist. If noStore is set to true, this function
34-
// will not store the result of the test in scTests and will instead just wait until the spec and
35-
// status blocks exist or return an error after the timeout.
36-
func checkSpecAndStat(runtimeClient client.Client, obj *unstructured.Unstructured, noStore bool) error {
37-
testSpec := scorecardTest{testType: basicOperator, name: "Spec Block Exists", maximumPoints: 1}
38-
testStat := scorecardTest{testType: basicOperator, name: "Status Block Exists", maximumPoints: 1}
39-
err := wait.Poll(time.Second*1, time.Second*time.Duration(viper.GetInt64(InitTimeoutOpt)), func() (bool, error) {
40-
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
41-
if err != nil {
42-
return false, fmt.Errorf("error getting custom resource: %v", err)
43-
}
44-
var specPass, statusPass bool
45-
if obj.Object["spec"] != nil {
46-
testSpec.earnedPoints = 1
47-
specPass = true
48-
}
49-
50-
if obj.Object["status"] != nil {
51-
testStat.earnedPoints = 1
52-
statusPass = true
53-
}
54-
return statusPass && specPass, nil
55-
})
56-
if !noStore {
57-
scTests = append(scTests, testSpec, testStat)
58-
}
59-
if err != nil && err != wait.ErrWaitTimeout {
60-
return err
26+
// Run - implements Test interface
27+
func (t *CheckSpecTest) Run(ctx context.Context) *TestResult {
28+
res := &TestResult{Test: t, MaximumPoints: 1}
29+
err := t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR)
30+
if err != nil {
31+
res.Errors = append(res.Errors, fmt.Errorf("error getting custom resource: %v", err))
32+
return res
6133
}
62-
if testSpec.earnedPoints != 1 {
63-
scSuggestions = append(scSuggestions, "Add a 'spec' field to your Custom Resource")
34+
if t.CR.Object["spec"] != nil {
35+
res.EarnedPoints++
6436
}
65-
if testStat.earnedPoints != 1 {
66-
scSuggestions = append(scSuggestions, "Add a 'status' field to your Custom Resource")
37+
if res.EarnedPoints != 1 {
38+
res.Suggestions = append(res.Suggestions, "Add a 'spec' field to your Custom Resource")
6739
}
68-
return nil
40+
return res
6941
}
7042

71-
// TODO: user specified tests for operators
72-
73-
// checkStatusUpdate looks at all fields in the spec section of a custom resource and attempts to modify them and
74-
// see if the status changes as a result. This is a bit prone to breakage as this is a black box test and we don't
75-
// know much about how the operators we are testing actually work and may pass an invalid value. In the future, we
76-
// should use user-specified tests
77-
func checkStatusUpdate(runtimeClient client.Client, obj *unstructured.Unstructured) error {
78-
test := scorecardTest{testType: basicOperator, name: "Operator actions are reflected in status", maximumPoints: 1}
79-
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
43+
// Run - implements Test interface
44+
func (t *CheckStatusTest) Run(ctx context.Context) *TestResult {
45+
res := &TestResult{Test: t, MaximumPoints: 1}
46+
err := t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR)
8047
if err != nil {
81-
return fmt.Errorf("error getting custom resource: %v", err)
48+
res.Errors = append(res.Errors, fmt.Errorf("error getting custom resource: %v", err))
49+
return res
8250
}
83-
if obj.Object["status"] == nil || obj.Object["spec"] == nil {
84-
scTests = append(scTests, test)
85-
return nil
51+
if t.CR.Object["status"] != nil {
52+
res.EarnedPoints++
8653
}
87-
statCopy := make(map[string]interface{})
88-
for k, v := range obj.Object["status"].(map[string]interface{}) {
89-
statCopy[k] = v
90-
}
91-
specMap := obj.Object["spec"].(map[string]interface{})
92-
err = modifySpecAndCheck(specMap, obj)
93-
if err != nil {
94-
test.earnedPoints = 0
95-
scSuggestions = append(scSuggestions, "Make sure that the 'status' block is always updated to reflect changes after the 'spec' block is changed")
96-
scTests = append(scTests, test)
97-
return nil
98-
}
99-
test.earnedPoints = 1
100-
scTests = append(scTests, test)
101-
return nil
102-
}
103-
104-
// modifySpecAndCheck is a helper function for checkStatusUpdate
105-
func modifySpecAndCheck(specMap map[string]interface{}, obj *unstructured.Unstructured) error {
106-
statCopy := make(map[string]interface{})
107-
for k, v := range obj.Object["status"].(map[string]interface{}) {
108-
statCopy[k] = v
109-
}
110-
var err error
111-
for k, v := range specMap {
112-
mapType := false
113-
switch t := v.(type) {
114-
case int64:
115-
specMap[k] = specMap[k].(int64) + 1
116-
case float64:
117-
specMap[k] = specMap[k].(float64) + 1
118-
case string:
119-
// TODO: try and find out how to make this better
120-
// Since strings may be very operator specific, this test may not work correctly in many cases
121-
specMap[k] = fmt.Sprintf("operator sdk test value %f", rand.Float64())
122-
case bool:
123-
specMap[k] = !specMap[k].(bool)
124-
case map[string]interface{}:
125-
mapType = true
126-
err = modifySpecAndCheck(specMap[k].(map[string]interface{}), obj)
127-
case []map[string]interface{}:
128-
mapType = true
129-
for _, item := range specMap[k].([]map[string]interface{}) {
130-
err = modifySpecAndCheck(item, obj)
131-
if err != nil {
132-
break
133-
}
134-
}
135-
case []interface{}: // TODO: Decide how this should be handled
136-
default:
137-
fmt.Printf("Unknown type for key (%s) in spec: (%v)\n", k, reflect.TypeOf(t))
138-
}
139-
if !mapType {
140-
if err := runtimeClient.Update(context.TODO(), obj); err != nil {
141-
return fmt.Errorf("failed to update object: %v", err)
142-
}
143-
err = wait.Poll(time.Second*1, time.Second*15, func() (done bool, err error) {
144-
err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
145-
if err != nil {
146-
return false, err
147-
}
148-
return !reflect.DeepEqual(statCopy, obj.Object["status"]), nil
149-
})
150-
}
151-
if err != nil {
152-
return err
153-
}
154-
//reset stat copy to match
155-
statCopy = make(map[string]interface{})
156-
for k, v := range obj.Object["status"].(map[string]interface{}) {
157-
statCopy[k] = v
158-
}
54+
if res.EarnedPoints != 1 {
55+
res.Suggestions = append(res.Suggestions, "Add a 'status' field to your Custom Resource")
15956
}
160-
return nil
57+
return res
16158
}
16259

163-
// wiritingIntoCRsHasEffect simply looks at the proxy logs and verifies that the operator is sending PUT
164-
// and/or POST requests to the API server, which should mean that it is creating or modifying resources.
165-
func writingIntoCRsHasEffect(obj *unstructured.Unstructured) (string, error) {
166-
test := scorecardTest{testType: basicOperator, name: "Writing into CRs has an effect", maximumPoints: 1}
167-
logs, err := getProxyLogs()
60+
// Run - implements Test interface
61+
func (t *WritingIntoCRsHasEffectTest) Run(ctx context.Context) *TestResult {
62+
res := &TestResult{Test: t, MaximumPoints: 1}
63+
logs, err := getProxyLogs(t.ProxyPod)
16864
if err != nil {
169-
return "", err
65+
res.Errors = append(res.Errors, fmt.Errorf("error getting proxy logs: %v", err))
66+
return res
17067
}
17168
msgMap := make(map[string]interface{})
17269
for _, msg := range strings.Split(logs, "\n") {
@@ -178,13 +75,12 @@ func writingIntoCRsHasEffect(obj *unstructured.Unstructured) (string, error) {
17875
continue
17976
}
18077
if method == "PUT" || method == "POST" {
181-
test.earnedPoints = 1
78+
res.EarnedPoints = 1
18279
break
18380
}
18481
}
185-
scTests = append(scTests, test)
186-
if test.earnedPoints != 1 {
187-
scSuggestions = append(scSuggestions, "The operator should write into objects to update state. No PUT or POST requests from you operator were recorded by the scorecard.")
82+
if res.EarnedPoints != 1 {
83+
res.Suggestions = append(res.Suggestions, "The operator should write into objects to update state. No PUT or POST requests from the operator were recorded by the scorecard.")
18884
}
189-
return logs, nil
85+
return res
19086
}

0 commit comments

Comments
 (0)