Skip to content

Commit 11b1270

Browse files
committed
commands/.../scorecard: modularize tests and implement weights
1 parent 3c97587 commit 11b1270

File tree

7 files changed

+368
-163
lines changed

7 files changed

+368
-163
lines changed

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

Lines changed: 49 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -37,39 +37,35 @@ import (
3737
"sigs.k8s.io/controller-runtime/pkg/client"
3838
)
3939

40-
// checkSpecAndStat checks that the spec and status blocks exist. If noStore is set to true, this function
41-
// will not store the result of the test in scTests and will instead just wait until the spec and
42-
// status blocks exist or return an error after the timeout.
43-
func checkSpecAndStat(runtimeClient client.Client, obj *unstructured.Unstructured, noStore bool) error {
44-
testSpec := scorecardTest{testType: basicOperator, name: "Spec Block Exists", maximumPoints: 1}
45-
testStat := scorecardTest{testType: basicOperator, name: "Status Block Exist", maximumPoints: 1}
46-
err := wait.Poll(time.Second*1, time.Second*time.Duration(SCConf.InitTimeout), func() (bool, error) {
47-
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
48-
if err != nil {
49-
return false, fmt.Errorf("error getting custom resource: %v", err)
50-
}
51-
var specPass, statusPass bool
52-
if obj.Object["spec"] != nil {
53-
testSpec.earnedPoints = 1
54-
specPass = true
55-
}
56-
57-
if obj.Object["status"] != nil {
58-
testStat.earnedPoints = 1
59-
statusPass = true
60-
}
61-
return statusPass && specPass, nil
62-
})
63-
if !noStore {
64-
scTests = append(scTests, testSpec, testStat)
40+
// checkSpec checks that the spec block exists
41+
func checkSpec(test *Test, vars ScorecardVars) error {
42+
score := Score{maximumPoints: 1}
43+
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: vars.crObj.GetNamespace(), Name: vars.crObj.GetName()}, vars.crObj)
44+
if err != nil {
45+
return fmt.Errorf("error getting custom resource: %v", err)
6546
}
66-
if err != nil && err != wait.ErrWaitTimeout {
67-
return err
47+
if vars.crObj.Object["spec"] != nil {
48+
score.earnedPoints = 1
6849
}
69-
if testSpec.earnedPoints != 1 {
50+
test.scores = append(test.scores, score)
51+
if score.earnedPoints != 1 {
7052
scSuggestions = append(scSuggestions, "Add a 'spec' field to your Custom Resource")
7153
}
72-
if testStat.earnedPoints != 1 {
54+
return nil
55+
}
56+
57+
// checkStat checks that the stat block exists
58+
func checkStat(test *Test, vars ScorecardVars) error {
59+
score := Score{maximumPoints: 1}
60+
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: vars.crObj.GetNamespace(), Name: vars.crObj.GetName()}, vars.crObj)
61+
if err != nil {
62+
return fmt.Errorf("error getting custom resource: %v", err)
63+
}
64+
if vars.crObj.Object["status"] != nil {
65+
score.earnedPoints = 1
66+
}
67+
test.scores = append(test.scores, score)
68+
if score.earnedPoints != 1 {
7369
scSuggestions = append(scSuggestions, "Add a 'status' field to your Custom Resource")
7470
}
7571
return nil
@@ -81,30 +77,29 @@ func checkSpecAndStat(runtimeClient client.Client, obj *unstructured.Unstructure
8177
// 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
8278
// know much about how the operators we are testing actually work and may pass an invalid value. In the future, we
8379
// should use user-specified tests
84-
func checkStatusUpdate(runtimeClient client.Client, obj *unstructured.Unstructured) error {
85-
test := scorecardTest{testType: basicOperator, name: "Operator actions are reflected in status", maximumPoints: 1}
86-
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
80+
func checkStatusUpdate(test *Test, vars ScorecardVars) error {
81+
score := Score{maximumPoints: 1}
82+
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: vars.crObj.GetNamespace(), Name: vars.crObj.GetName()}, vars.crObj)
8783
if err != nil {
8884
return fmt.Errorf("error getting custom resource: %v", err)
8985
}
90-
if obj.Object["status"] == nil || obj.Object["spec"] == nil {
91-
scTests = append(scTests, test)
86+
if vars.crObj.Object["status"] == nil || vars.crObj.Object["spec"] == nil {
9287
return nil
9388
}
9489
statCopy := make(map[string]interface{})
95-
for k, v := range obj.Object["status"].(map[string]interface{}) {
90+
for k, v := range vars.crObj.Object["status"].(map[string]interface{}) {
9691
statCopy[k] = v
9792
}
98-
specMap := obj.Object["spec"].(map[string]interface{})
99-
err = modifySpecAndCheck(specMap, obj)
93+
specMap := vars.crObj.Object["spec"].(map[string]interface{})
94+
err = modifySpecAndCheck(specMap, vars.crObj)
10095
if err != nil {
101-
test.earnedPoints = 0
96+
score.earnedPoints = 0
97+
test.scores = append(test.scores, score)
10298
scSuggestions = append(scSuggestions, "Make sure that the 'status' block is always updated to reflect changes after the 'spec' block is changed")
103-
scTests = append(scTests, test)
10499
return nil
105100
}
106-
test.earnedPoints = 1
107-
scTests = append(scTests, test)
101+
score.earnedPoints = 1
102+
test.scores = append(test.scores, score)
108103
return nil
109104
}
110105

@@ -169,28 +164,27 @@ func modifySpecAndCheck(specMap map[string]interface{}, obj *unstructured.Unstru
169164

170165
// wiritingIntoCRsHasEffect simply looks at the proxy logs and verifies that the operator is sending PUT
171166
// and/or POST requests to the API server, which should mean that it is creating or modifying resources.
172-
func writingIntoCRsHasEffect(obj *unstructured.Unstructured) (string, error) {
173-
test := scorecardTest{testType: basicOperator, name: "Writing into CRs has an effect", maximumPoints: 1}
167+
func writingIntoCRsHasEffect(test *Test, vars ScorecardVars) error {
174168
kubeclient, err := kubernetes.NewForConfig(kubeconfig)
175169
if err != nil {
176-
return "", fmt.Errorf("failed to create kubeclient: %v", err)
170+
return fmt.Errorf("failed to create kubeclient: %v", err)
177171
}
178172
dep := &appsv1.Deployment{}
179-
err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: deploymentName}, dep)
173+
err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: vars.crObj.GetNamespace(), Name: deploymentName}, dep)
180174
if err != nil {
181-
return "", fmt.Errorf("failed to get newly created operator deployment: %v", err)
175+
return fmt.Errorf("failed to get newly created operator deployment: %v", err)
182176
}
183177
set := labels.Set(dep.Spec.Selector.MatchLabels)
184178
pods := &v1.PodList{}
185179
err = runtimeClient.List(context.TODO(), &client.ListOptions{LabelSelector: set.AsSelector()}, pods)
186180
if err != nil {
187-
return "", fmt.Errorf("failed to get list of pods in deployment: %v", err)
181+
return fmt.Errorf("failed to get list of pods in deployment: %v", err)
188182
}
189183
proxyPod = &pods.Items[0]
190-
req := kubeclient.CoreV1().Pods(obj.GetNamespace()).GetLogs(proxyPod.GetName(), &v1.PodLogOptions{Container: "scorecard-proxy"})
184+
req := kubeclient.CoreV1().Pods(vars.crObj.GetNamespace()).GetLogs(proxyPod.GetName(), &v1.PodLogOptions{Container: "scorecard-proxy"})
191185
readCloser, err := req.Stream()
192186
if err != nil {
193-
return "", fmt.Errorf("failed to get logs: %v", err)
187+
return fmt.Errorf("failed to get logs: %v", err)
194188
}
195189
defer func() {
196190
if err := readCloser.Close(); err != nil && !fileutil.IsClosedError(err) {
@@ -200,10 +194,11 @@ func writingIntoCRsHasEffect(obj *unstructured.Unstructured) (string, error) {
200194
buf := new(bytes.Buffer)
201195
_, err = buf.ReadFrom(readCloser)
202196
if err != nil {
203-
return "", fmt.Errorf("test failed and failed to read pod logs: %v", err)
197+
return fmt.Errorf("test failed and failed to read pod logs: %v", err)
204198
}
205199
logs := buf.String()
206200
msgMap := make(map[string]interface{})
201+
score := Score{maximumPoints: 1}
207202
for _, msg := range strings.Split(logs, "\n") {
208203
if err := json.Unmarshal([]byte(msg), &msgMap); err != nil {
209204
continue
@@ -213,13 +208,13 @@ func writingIntoCRsHasEffect(obj *unstructured.Unstructured) (string, error) {
213208
continue
214209
}
215210
if method == "PUT" || method == "POST" {
216-
test.earnedPoints = 1
211+
score.earnedPoints = 1
217212
break
218213
}
219214
}
220-
scTests = append(scTests, test)
221-
if test.earnedPoints != 1 {
215+
test.scores = append(test.scores, score)
216+
if score.earnedPoints != 1 {
222217
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.")
223218
}
224-
return buf.String(), nil
219+
return nil
225220
}

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

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,116 +18,112 @@ import (
1818
"context"
1919

2020
olmAPI "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
21-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2221
"k8s.io/apimachinery/pkg/types"
23-
"sigs.k8s.io/controller-runtime/pkg/client"
2422
)
2523

2624
// crdsHaveResources checks to make sure that all owned CRDs have resources listed
27-
func crdsHaveResources(csv *olmAPI.ClusterServiceVersion) {
28-
test := scorecardTest{testType: olmIntegration, name: "Owned CRDs have resources listed"}
29-
for _, crd := range csv.Spec.CustomResourceDefinitions.Owned {
30-
test.maximumPoints++
25+
func crdsHaveResources(test *Test, vars ScorecardVars) error {
26+
score := Score{}
27+
for _, crd := range vars.csvObj.Spec.CustomResourceDefinitions.Owned {
28+
score.maximumPoints++
3129
if len(crd.Resources) > 0 {
32-
test.earnedPoints++
30+
score.earnedPoints++
3331
}
3432
}
35-
scTests = append(scTests, test)
36-
if test.earnedPoints == 0 {
33+
test.scores = append(test.scores, score)
34+
if score.earnedPoints < score.maximumPoints {
3735
scSuggestions = append(scSuggestions, "Add resources to owned CRDs")
3836
}
37+
return nil
3938
}
4039

4140
// annotationsContainExamples makes sure that the CSVs list at least 1 example for the CR
42-
func annotationsContainExamples(csv *olmAPI.ClusterServiceVersion) {
43-
test := scorecardTest{testType: olmIntegration, name: "CRs have at least 1 example", maximumPoints: 1}
44-
if csv.Annotations != nil && csv.Annotations["alm-examples"] != "" {
45-
test.earnedPoints = 1
41+
func annotationsContainExamples(test *Test, vars ScorecardVars) error {
42+
score := Score{maximumPoints: 1}
43+
if vars.csvObj.Annotations != nil && vars.csvObj.Annotations["alm-examples"] != "" {
44+
score.earnedPoints = 1
4645
}
47-
scTests = append(scTests, test)
48-
if test.earnedPoints == 0 {
49-
scSuggestions = append(scSuggestions, "Add an alm-examples annotation to your CSV to pass the " + test.name + " test")
46+
test.scores = append(test.scores, score)
47+
if score.earnedPoints == 0 {
48+
scSuggestions = append(scSuggestions, "Add an alm-examples annotation to your CSV to pass the "+test.name+" test")
5049
}
50+
return nil
5151
}
5252

5353
// statusDescriptors makes sure that all status fields found in the created CR has a matching descriptor in the CSV
54-
func statusDescriptors(csv *olmAPI.ClusterServiceVersion, runtimeClient client.Client, obj *unstructured.Unstructured) error {
55-
test := scorecardTest{testType: olmIntegration, name: "Status fields with descriptors"}
56-
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
54+
func statusDescriptors(test *Test, vars ScorecardVars) error {
55+
score := Score{}
56+
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: vars.crObj.GetNamespace(), Name: vars.crObj.GetName()}, vars.crObj)
5757
if err != nil {
5858
return err
5959
}
60-
if obj.Object["status"] == nil {
60+
if vars.crObj.Object["status"] == nil {
6161
// what should we do if there is no status block? Maybe some kind of N/A type output?
62-
scTests = append(scTests, test)
6362
return nil
6463
}
65-
statusBlock := obj.Object["status"].(map[string]interface{})
66-
test.maximumPoints = len(statusBlock)
64+
statusBlock := vars.crObj.Object["status"].(map[string]interface{})
65+
score.maximumPoints = len(statusBlock)
6766
var crd *olmAPI.CRDDescription
68-
for _, owned := range csv.Spec.CustomResourceDefinitions.Owned {
69-
if owned.Kind == obj.GetKind() {
67+
for _, owned := range vars.csvObj.Spec.CustomResourceDefinitions.Owned {
68+
if owned.Kind == vars.crObj.GetKind() {
7069
crd = &owned
7170
break
7271
}
7372
}
7473
if crd == nil {
75-
scTests = append(scTests, test)
7674
return nil
7775
}
7876
for key := range statusBlock {
7977
for _, statDesc := range crd.StatusDescriptors {
8078
if statDesc.Path == key {
81-
test.earnedPoints++
79+
score.earnedPoints++
8280
delete(statusBlock, key)
8381
break
8482
}
8583
}
8684
}
87-
scTests = append(scTests, test)
85+
test.scores = append(test.scores, score)
8886
for key := range statusBlock {
8987
scSuggestions = append(scSuggestions, "Add a status descriptor for "+key)
9088
}
9189
return nil
9290
}
9391

9492
// specDescriptors makes sure that all spec fields found in the created CR has a matching descriptor in the CSV
95-
func specDescriptors(csv *olmAPI.ClusterServiceVersion, runtimeClient client.Client, obj *unstructured.Unstructured) error {
96-
test := scorecardTest{testType: olmIntegration, name: "Spec fields with descriptors"}
97-
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
93+
func specDescriptors(test *Test, vars ScorecardVars) error {
94+
score := Score{}
95+
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: vars.crObj.GetNamespace(), Name: vars.crObj.GetName()}, vars.crObj)
9896
if err != nil {
9997
return err
10098
}
101-
if obj.Object["spec"] == nil {
99+
if vars.crObj.Object["spec"] == nil {
102100
// what should we do if there is no spec block? Maybe some kind of N/A type output?
103-
scTests = append(scTests, test)
104101
return nil
105102
}
106-
specBlock := obj.Object["spec"].(map[string]interface{})
107-
test.maximumPoints = len(specBlock)
103+
specBlock := vars.crObj.Object["spec"].(map[string]interface{})
104+
score.maximumPoints = len(specBlock)
108105
var crd *olmAPI.CRDDescription
109-
for _, owned := range csv.Spec.CustomResourceDefinitions.Owned {
110-
if owned.Kind == obj.GetKind() {
106+
for _, owned := range vars.csvObj.Spec.CustomResourceDefinitions.Owned {
107+
if owned.Kind == vars.crObj.GetKind() {
111108
crd = &owned
112109
break
113110
}
114111
}
115112
if crd == nil {
116-
scTests = append(scTests, test)
117113
return nil
118114
}
119115
for key := range specBlock {
120116
for _, specDesc := range crd.SpecDescriptors {
121117
if specDesc.Path == key {
122-
test.earnedPoints++
118+
score.earnedPoints++
123119
delete(specBlock, key)
124120
break
125121
}
126122
}
127123
}
128-
scTests = append(scTests, test)
124+
test.scores = append(test.scores, score)
129125
for key := range specBlock {
130-
scSuggestions = append(scSuggestions, "Add a spec descriptor for " + key)
126+
scSuggestions = append(scSuggestions, "Add a spec descriptor for "+key)
131127
}
132128
return nil
133129
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,27 @@ import (
3838
"k8s.io/apimachinery/pkg/util/wait"
3939
)
4040

41+
type cleanupFn func() error
42+
43+
// waitUntilReady waits until the status block of the CR currently being tested exists. If the timeout
44+
// is reached, it simply continues and assumes there is no status block
45+
func waitUntilReady(obj *unstructured.Unstructured) error {
46+
err := wait.Poll(time.Second*1, time.Second*time.Duration(SCConf.InitTimeout), func() (bool, error) {
47+
err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
48+
if err != nil {
49+
return false, fmt.Errorf("error getting custom resource: %v", err)
50+
}
51+
if obj.Object["status"] != nil {
52+
return true, nil
53+
}
54+
return false, nil
55+
})
56+
if err != nil && err != wait.ErrWaitTimeout {
57+
return err
58+
}
59+
return nil
60+
}
61+
4162
// yamlToUnstructured decodes a yaml file into an unstructured object
4263
func yamlToUnstructured(yamlPath string) (*unstructured.Unstructured, error) {
4364
yamlFile, err := ioutil.ReadFile(yamlPath)

0 commit comments

Comments
 (0)