github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/scorecard/olm_tests.go (about) 1 // Copyright 2019 The Operator-SDK Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package scorecard 16 17 import ( 18 "context" 19 "fmt" 20 "io/ioutil" 21 "path/filepath" 22 "strings" 23 24 "github.com/operator-framework/operator-sdk/pkg/scaffold" 25 26 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" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/types" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 ) 33 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 136 // crdsHaveResources checks to make sure that all owned CRDs have resources listed 137 func crdsHaveResources(csv *olmapiv1alpha1.ClusterServiceVersion) { 138 test := scorecardTest{testType: olmIntegration, name: "Owned CRDs have resources listed"} 139 for _, crd := range csv.Spec.CustomResourceDefinitions.Owned { 140 test.maximumPoints++ 141 if len(crd.Resources) > 0 { 142 test.earnedPoints++ 143 } 144 } 145 scTests = append(scTests, test) 146 if test.earnedPoints == 0 { 147 scSuggestions = append(scSuggestions, "Add resources to owned CRDs") 148 } 149 } 150 151 // annotationsContainExamples makes sure that the CSVs list at least 1 example for the CR 152 func annotationsContainExamples(csv *olmapiv1alpha1.ClusterServiceVersion) { 153 test := scorecardTest{testType: olmIntegration, name: "CRs have at least 1 example", maximumPoints: 1} 154 if csv.Annotations != nil && csv.Annotations["alm-examples"] != "" { 155 test.earnedPoints = 1 156 } 157 scTests = append(scTests, test) 158 if test.earnedPoints == 0 { 159 scSuggestions = append(scSuggestions, "Add an alm-examples annotation to your CSV to pass the "+test.name+" test") 160 } 161 } 162 163 // statusDescriptors makes sure that all status fields found in the created CR has a matching descriptor in the CSV 164 func statusDescriptors(csv *olmapiv1alpha1.ClusterServiceVersion, runtimeClient client.Client, obj *unstructured.Unstructured) error { 165 test := scorecardTest{testType: olmIntegration, name: "Status fields with descriptors"} 166 err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj) 167 if err != nil { 168 return err 169 } 170 if obj.Object["status"] == nil { 171 // what should we do if there is no status block? Maybe some kind of N/A type output? 172 scTests = append(scTests, test) 173 return nil 174 } 175 statusBlock := obj.Object["status"].(map[string]interface{}) 176 test.maximumPoints = len(statusBlock) 177 var crd *olmapiv1alpha1.CRDDescription 178 for _, owned := range csv.Spec.CustomResourceDefinitions.Owned { 179 if owned.Kind == obj.GetKind() { 180 crd = &owned 181 break 182 } 183 } 184 if crd == nil { 185 scTests = append(scTests, test) 186 return nil 187 } 188 for key := range statusBlock { 189 for _, statDesc := range crd.StatusDescriptors { 190 if statDesc.Path == key { 191 test.earnedPoints++ 192 delete(statusBlock, key) 193 break 194 } 195 } 196 } 197 scTests = append(scTests, test) 198 for key := range statusBlock { 199 scSuggestions = append(scSuggestions, "Add a status descriptor for "+key) 200 } 201 return nil 202 } 203 204 // specDescriptors makes sure that all spec fields found in the created CR has a matching descriptor in the CSV 205 func specDescriptors(csv *olmapiv1alpha1.ClusterServiceVersion, runtimeClient client.Client, obj *unstructured.Unstructured) error { 206 test := scorecardTest{testType: olmIntegration, name: "Spec fields with descriptors"} 207 err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj) 208 if err != nil { 209 return err 210 } 211 if obj.Object["spec"] == nil { 212 // what should we do if there is no spec block? Maybe some kind of N/A type output? 213 scTests = append(scTests, test) 214 return nil 215 } 216 specBlock := obj.Object["spec"].(map[string]interface{}) 217 test.maximumPoints = len(specBlock) 218 var crd *olmapiv1alpha1.CRDDescription 219 for _, owned := range csv.Spec.CustomResourceDefinitions.Owned { 220 if owned.Kind == obj.GetKind() { 221 crd = &owned 222 break 223 } 224 } 225 if crd == nil { 226 scTests = append(scTests, test) 227 return nil 228 } 229 for key := range specBlock { 230 for _, specDesc := range crd.SpecDescriptors { 231 if specDesc.Path == key { 232 test.earnedPoints++ 233 delete(specBlock, key) 234 break 235 } 236 } 237 } 238 scTests = append(scTests, test) 239 for key := range specBlock { 240 scSuggestions = append(scSuggestions, "Add a spec descriptor for "+key) 241 } 242 return nil 243 }