Skip to content

Commit 2156077

Browse files
authored
commands/.../scorecard,Gopkg.lock: add crdsHaveValidation func (#979)
* commands/.../scorecard: add crdsHaveValidation func
1 parent d6325bc commit 2156077

File tree

7 files changed

+173
-19
lines changed

7 files changed

+173
-19
lines changed

commands/operator-sdk/cmd/scorecard.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"fmt"
1919
"strings"
2020

21+
"github.com/operator-framework/operator-sdk/pkg/scaffold"
22+
2123
log "github.com/sirupsen/logrus"
2224
"github.com/spf13/cobra"
2325
"github.com/spf13/viper"
@@ -40,6 +42,7 @@ type scorecardConfig struct {
4042
crManifest string
4143
proxyImage string
4244
proxyPullPolicy string
45+
crdsDir string
4346
verbose bool
4447
}
4548

@@ -67,6 +70,7 @@ func NewScorecardCmd() *cobra.Command {
6770
scorecardCmd.Flags().StringVar(&scConf.crManifest, scorecard.CRManifestOpt, "", "Path to manifest for Custom Resource (required)")
6871
scorecardCmd.Flags().StringVar(&scConf.proxyImage, scorecard.ProxyImageOpt, fmt.Sprintf("quay.io/operator-framework/scorecard-proxy:%s", strings.TrimSuffix(version.Version, "+git")), "Image name for scorecard proxy")
6972
scorecardCmd.Flags().StringVar(&scConf.proxyPullPolicy, scorecard.ProxyPullPolicyOpt, "Always", "Pull policy for scorecard proxy image")
73+
scorecardCmd.Flags().StringVar(&scConf.crdsDir, "crds-dir", scaffold.CRDsDir, "Directory containing CRDs (all CRD manifest filenames must have the suffix 'crd.yaml')")
7074
scorecardCmd.Flags().BoolVar(&scConf.verbose, scorecard.VerboseOpt, false, "Enable verbose logging")
7175

7276
if err := viper.BindPFlags(scorecardCmd.Flags()); err != nil {

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,123 @@ package scorecard
1616

1717
import (
1818
"context"
19+
"fmt"
20+
"io/ioutil"
21+
"path/filepath"
22+
"strings"
23+
24+
"github.com/operator-framework/operator-sdk/pkg/scaffold"
1925

2026
olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
27+
log "github.com/sirupsen/logrus"
28+
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
2129
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2230
"k8s.io/apimachinery/pkg/types"
2331
"sigs.k8s.io/controller-runtime/pkg/client"
2432
)
2533

34+
func getCRDs(crdsDir string) ([]apiextv1beta1.CustomResourceDefinition, error) {
35+
files, err := ioutil.ReadDir(crdsDir)
36+
if err != nil {
37+
return nil, fmt.Errorf("could not read deploy directory: (%v)", err)
38+
}
39+
crds := []apiextv1beta1.CustomResourceDefinition{}
40+
for _, file := range files {
41+
if strings.HasSuffix(file.Name(), "crd.yaml") {
42+
obj, err := yamlToUnstructured(filepath.Join(scaffold.CRDsDir, file.Name()))
43+
if err != nil {
44+
return nil, err
45+
}
46+
crd, err := unstructuredToCRD(obj)
47+
if err != nil {
48+
return nil, err
49+
}
50+
crds = append(crds, *crd)
51+
}
52+
}
53+
return crds, nil
54+
}
55+
56+
func matchKind(kind1, kind2 string) bool {
57+
singularKind1, err := restMapper.ResourceSingularizer(kind1)
58+
if err != nil {
59+
singularKind1 = kind1
60+
log.Warningf("could not find singular version of %s", kind1)
61+
}
62+
singularKind2, err := restMapper.ResourceSingularizer(kind2)
63+
if err != nil {
64+
singularKind2 = kind2
65+
log.Warningf("could not find singular version of %s", kind2)
66+
}
67+
return strings.EqualFold(singularKind1, singularKind2)
68+
}
69+
70+
// matchVersion checks if a CRD contains a specified version in a case insensitive manner
71+
func matchVersion(version string, crd apiextv1beta1.CustomResourceDefinition) bool {
72+
if strings.EqualFold(version, crd.Spec.Version) {
73+
return true
74+
}
75+
// crd.Spec.Version is deprecated, so check in crd.Spec.Versions as well
76+
for _, currVer := range crd.Spec.Versions {
77+
if strings.EqualFold(version, currVer.Name) {
78+
return true
79+
}
80+
}
81+
return false
82+
}
83+
84+
// crdsHaveValidation makes sure that all CRDs have a validation block
85+
func crdsHaveValidation(crdsDir string, runtimeClient client.Client, obj *unstructured.Unstructured) error {
86+
test := scorecardTest{testType: olmIntegration, name: "Provided APIs have validation"}
87+
crds, err := getCRDs(crdsDir)
88+
if err != nil {
89+
return fmt.Errorf("failed to get CRDs in %s directory: %v", crdsDir, err)
90+
}
91+
err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj)
92+
if err != nil {
93+
return err
94+
}
95+
// TODO: we need to make this handle multiple CRs better/correctly
96+
for _, crd := range crds {
97+
test.maximumPoints++
98+
if crd.Spec.Validation == nil {
99+
scSuggestions = append(scSuggestions, fmt.Sprintf("Add CRD validation for %s/%s", crd.Spec.Names.Kind, crd.Spec.Version))
100+
continue
101+
}
102+
// check if the CRD matches the testing CR
103+
gvk := obj.GroupVersionKind()
104+
// Only check the validation block if the CRD and CR have the same Kind and Version
105+
if !(matchVersion(gvk.Version, crd) && matchKind(gvk.Kind, crd.Spec.Names.Kind)) {
106+
test.earnedPoints++
107+
continue
108+
}
109+
failed := false
110+
if obj.Object["spec"] != nil {
111+
spec := obj.Object["spec"].(map[string]interface{})
112+
for key := range spec {
113+
if _, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["spec"].Properties[key]; !ok {
114+
failed = true
115+
scSuggestions = append(scSuggestions, fmt.Sprintf("Add CRD validation for spec field `%s` in %s/%s", key, gvk.Kind, gvk.Version))
116+
}
117+
}
118+
}
119+
if obj.Object["status"] != nil {
120+
status := obj.Object["status"].(map[string]interface{})
121+
for key := range status {
122+
if _, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"].Properties[key]; !ok {
123+
failed = true
124+
scSuggestions = append(scSuggestions, fmt.Sprintf("Add CRD validation for status field `%s` in %s/%s", key, gvk.Kind, gvk.Version))
125+
}
126+
}
127+
}
128+
if !failed {
129+
test.earnedPoints++
130+
}
131+
}
132+
scTests = append(scTests, test)
133+
return nil
134+
}
135+
26136
// crdsHaveResources checks to make sure that all owned CRDs have resources listed
27137
func crdsHaveResources(csv *olmapiv1alpha1.ClusterServiceVersion) {
28138
test := scorecardTest{testType: olmIntegration, name: "Owned CRDs have resources listed"}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
log "github.com/sirupsen/logrus"
3232
appsv1 "k8s.io/api/apps/v1"
3333
v1 "k8s.io/api/core/v1"
34+
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
3435
apierrors "k8s.io/apimachinery/pkg/api/errors"
3536
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3637
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -225,6 +226,24 @@ func addProxyContainer(dep *appsv1.Deployment) {
225226
})
226227
}
227228

229+
// unstructuredToCRD converts an unstructured object to a CRD
230+
func unstructuredToCRD(obj *unstructured.Unstructured) (*apiextv1beta1.CustomResourceDefinition, error) {
231+
jsonByte, err := obj.MarshalJSON()
232+
if err != nil {
233+
return nil, fmt.Errorf("failed to convert CRD to json: %v", err)
234+
}
235+
crdObj, _, err := dynamicDecoder.Decode(jsonByte, nil, nil)
236+
if err != nil {
237+
return nil, fmt.Errorf("failed to decode CRD object: %v", err)
238+
}
239+
switch o := crdObj.(type) {
240+
case *apiextv1beta1.CustomResourceDefinition:
241+
return o, nil
242+
default:
243+
return nil, fmt.Errorf("conversion of runtime object to CRD failed (resulting runtime object not CRD type)")
244+
}
245+
}
246+
228247
// unstructuredToDeployment converts an unstructured object to a deployment
229248
func unstructuredToDeployment(obj *unstructured.Unstructured) (*appsv1.Deployment, error) {
230249
jsonByte, err := obj.MarshalJSON()

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const (
5555
CRManifestOpt = "cr-manifest"
5656
ProxyImageOpt = "proxy-image"
5757
ProxyPullPolicyOpt = "proxy-pull-policy"
58+
CRDsDirOpt = "crds-dir"
5859
VerboseOpt = "verbose"
5960
)
6061

@@ -234,6 +235,10 @@ func ScorecardTests(cmd *cobra.Command, args []string) error {
234235
default:
235236
return fmt.Errorf("provided yaml file not of ClusterServiceVersion type")
236237
}
238+
fmt.Println("Checking if all CRDs have validation")
239+
if err := crdsHaveValidation(viper.GetString(CRDsDirOpt), runtimeClient, obj); err != nil {
240+
return err
241+
}
237242
fmt.Println("Checking for CRD resources")
238243
crdsHaveResources(csv)
239244
fmt.Println("Checking for existence of example CRs")

doc/test-framework/scorecard.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ API server, indicating that it is modifying resources. This test has a maximum s
7777

7878
### OLM Integration
7979

80+
#### Provided APIs have validation
81+
82+
This test verifies that all the CRDs in the CRDs folder contain a validation section. If the CRD matches the kind and version of the
83+
CR currently being tested, it will also verify that there is a validation for each spec and status field in that CR. This test has a
84+
maximum score of 1.
85+
8086
#### Owned CRDs Have Resources Listed
8187

8288
This test makes sure that the CRDs listed in the [`owned` CRDs section][owned-crds] of the CSV have a `resources` subsection. This

hack/tests/scorecard-subcommand.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ set -ex
1010
# the test framework directory has all the manifests needed to run the cluster
1111
pushd test/test-framework
1212
commandoutput="$(operator-sdk scorecard --cr-manifest deploy/crds/cache_v1alpha1_memcached_cr.yaml --init-timeout 60 --csv-path deploy/memcachedoperator.0.0.2.csv.yaml --verbose --proxy-image $DEST_IMAGE --proxy-pull-policy Never 2>&1)"
13-
echo $commandoutput | grep "Total Score: 5/7 points"
13+
echo $commandoutput | grep "Total Score: 6/8 points"
1414
# test config file
1515
commandoutput2="$(operator-sdk scorecard --proxy-image $DEST_IMAGE)"
16-
echo $commandoutput2 | grep "Total Score: 5/7 points"
16+
echo $commandoutput2 | grep "Total Score: 6/8 points"
1717
popd

test/test-framework/deploy/namespace-init.yaml

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ apiVersion: v1
22
kind: ServiceAccount
33
metadata:
44
name: memcached-operator
5+
56
---
7+
68
apiVersion: rbac.authorization.k8s.io/v1
79
kind: Role
810
metadata:
@@ -36,7 +38,9 @@ rules:
3638
- '*'
3739
verbs:
3840
- '*'
41+
3942
---
43+
4044
kind: RoleBinding
4145
apiVersion: rbac.authorization.k8s.io/v1
4246
metadata:
@@ -48,36 +52,42 @@ roleRef:
4852
kind: Role
4953
name: memcached-operator
5054
apiGroup: rbac.authorization.k8s.io
55+
5156
---
57+
5258
apiVersion: apps/v1
5359
kind: Deployment
5460
metadata:
61+
creationTimestamp: null
5562
name: memcached-operator
5663
spec:
5764
replicas: 1
5865
selector:
5966
matchLabels:
6067
name: memcached-operator
68+
strategy: {}
6169
template:
6270
metadata:
71+
creationTimestamp: null
6372
labels:
6473
name: memcached-operator
6574
spec:
66-
serviceAccountName: memcached-operator
6775
containers:
68-
- name: memcached-operator
69-
# Replace this with the built image name
70-
image: quay.io/coreos/operator-sdk-dev:test-framework-operator-runtime
71-
ports:
72-
- containerPort: 60000
73-
name: metrics
74-
command:
75-
- memcached-operator
76-
imagePullPolicy: Always
77-
env:
78-
- name: WATCH_NAMESPACE
79-
valueFrom:
80-
fieldRef:
81-
fieldPath: metadata.namespace
82-
- name: OPERATOR_NAME
83-
value: "memcached-operator"
76+
- command:
77+
- memcached-operator
78+
env:
79+
- name: WATCH_NAMESPACE
80+
valueFrom:
81+
fieldRef:
82+
fieldPath: metadata.namespace
83+
- name: OPERATOR_NAME
84+
value: memcached-operator
85+
image: quay.io/coreos/operator-sdk-dev:test-framework-operator-runtime
86+
imagePullPolicy: Always
87+
name: memcached-operator
88+
ports:
89+
- containerPort: 60000
90+
name: metrics
91+
resources: {}
92+
serviceAccountName: memcached-operator
93+
status: {}

0 commit comments

Comments
 (0)