github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/cmd/operator-sdk/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 "encoding/json" 20 "fmt" 21 "strings" 22 23 "github.com/operator-framework/operator-sdk/internal/util/k8sutil" 24 25 olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" 26 log "github.com/sirupsen/logrus" 27 v1 "k8s.io/api/core/v1" 28 apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/apimachinery/pkg/types" 31 ) 32 33 func matchKind(kind1, kind2 string) bool { 34 singularKind1, err := restMapper.ResourceSingularizer(kind1) 35 if err != nil { 36 singularKind1 = kind1 37 log.Warningf("could not find singular version of %s", kind1) 38 } 39 singularKind2, err := restMapper.ResourceSingularizer(kind2) 40 if err != nil { 41 singularKind2 = kind2 42 log.Warningf("could not find singular version of %s", kind2) 43 } 44 return strings.EqualFold(singularKind1, singularKind2) 45 } 46 47 // matchVersion checks if a CRD contains a specified version in a case insensitive manner 48 func matchVersion(version string, crd *apiextv1beta1.CustomResourceDefinition) bool { 49 if strings.EqualFold(version, crd.Spec.Version) { 50 return true 51 } 52 // crd.Spec.Version is deprecated, so check in crd.Spec.Versions as well 53 for _, currVer := range crd.Spec.Versions { 54 if strings.EqualFold(version, currVer.Name) { 55 return true 56 } 57 } 58 return false 59 } 60 61 // Run - implements Test interface 62 func (t *CRDsHaveValidationTest) Run(ctx context.Context) *TestResult { 63 res := &TestResult{Test: t} 64 crds, err := k8sutil.GetCRDs(t.CRDsDir) 65 if err != nil { 66 res.Errors = append(res.Errors, fmt.Errorf("failed to get CRDs in %s directory: %v", t.CRDsDir, err)) 67 return res 68 } 69 err = t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR) 70 if err != nil { 71 res.Errors = append(res.Errors, err) 72 return res 73 } 74 // TODO: we need to make this handle multiple CRs better/correctly 75 for _, crd := range crds { 76 res.MaximumPoints++ 77 if crd.Spec.Validation == nil { 78 res.Suggestions = append(res.Suggestions, fmt.Sprintf("Add CRD validation for %s/%s", crd.Spec.Names.Kind, crd.Spec.Version)) 79 continue 80 } 81 // check if the CRD matches the testing CR 82 gvk := t.CR.GroupVersionKind() 83 // Only check the validation block if the CRD and CR have the same Kind and Version 84 if !(matchVersion(gvk.Version, crd) && matchKind(gvk.Kind, crd.Spec.Names.Kind)) { 85 res.EarnedPoints++ 86 continue 87 } 88 failed := false 89 if t.CR.Object["spec"] != nil { 90 spec := t.CR.Object["spec"].(map[string]interface{}) 91 for key := range spec { 92 if _, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["spec"].Properties[key]; !ok { 93 failed = true 94 res.Suggestions = append(res.Suggestions, fmt.Sprintf("Add CRD validation for spec field `%s` in %s/%s", key, gvk.Kind, gvk.Version)) 95 } 96 } 97 } 98 if t.CR.Object["status"] != nil { 99 status := t.CR.Object["status"].(map[string]interface{}) 100 for key := range status { 101 if _, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"].Properties[key]; !ok { 102 failed = true 103 res.Suggestions = append(res.Suggestions, fmt.Sprintf("Add CRD validation for status field `%s` in %s/%s", key, gvk.Kind, gvk.Version)) 104 } 105 } 106 } 107 if !failed { 108 res.EarnedPoints++ 109 } 110 } 111 return res 112 } 113 114 // Run - implements Test interface 115 func (t *CRDsHaveResourcesTest) Run(ctx context.Context) *TestResult { 116 res := &TestResult{Test: t} 117 for _, crd := range t.CSV.Spec.CustomResourceDefinitions.Owned { 118 res.MaximumPoints++ 119 gvk := t.CR.GroupVersionKind() 120 if strings.EqualFold(crd.Version, gvk.Version) && matchKind(gvk.Kind, crd.Kind) { 121 resources, err := getUsedResources(t.ProxyPod) 122 if err != nil { 123 log.Warningf("getUsedResource failed: %v", err) 124 } 125 allResourcesListed := true 126 for _, resource := range resources { 127 foundResource := false 128 for _, listedResource := range crd.Resources { 129 if matchKind(resource.Kind, listedResource.Kind) && strings.EqualFold(resource.Version, listedResource.Version) { 130 foundResource = true 131 } 132 } 133 if foundResource == false { 134 allResourcesListed = false 135 } 136 } 137 if allResourcesListed { 138 res.EarnedPoints++ 139 } 140 } else { 141 if len(crd.Resources) > 0 { 142 res.EarnedPoints++ 143 } 144 } 145 } 146 if res.EarnedPoints < res.MaximumPoints { 147 res.Suggestions = append(res.Suggestions, "Add resources to owned CRDs") 148 } 149 return res 150 } 151 152 func getUsedResources(proxyPod *v1.Pod) ([]schema.GroupVersionKind, error) { 153 logs, err := getProxyLogs(proxyPod) 154 if err != nil { 155 return nil, err 156 } 157 resources := map[schema.GroupVersionKind]bool{} 158 for _, line := range strings.Split(logs, "\n") { 159 logMap := make(map[string]interface{}) 160 err := json.Unmarshal([]byte(line), &logMap) 161 if err != nil { 162 // it is very common to get "unexpected end of JSON input", so we'll leave this at the debug level 163 log.Debugf("could not unmarshal line: %v", err) 164 continue 165 } 166 /* 167 There are 6 formats a resource uri can have: 168 Cluster-Scoped: 169 Collection: /apis/GROUP/VERSION/KIND 170 Individual: /apis/GROUP/VERSION/KIND/NAME 171 Core: /api/v1/KIND 172 Namespaces: 173 All Namespaces: /apis/GROUP/VERSION/KIND (same as cluster collection) 174 Collection in Namespace: /apis/GROUP/VERSION/namespaces/NAMESPACE/KIND 175 Individual: /apis/GROUP/VERSION/namespaces/NAMESPACE/KIND/NAME 176 Core: /api/v1/namespaces/NAMESPACE/KIND 177 178 These urls are also often appended with options, which are denoted by the '?' symbol 179 */ 180 if msg, ok := logMap["msg"].(string); !ok || msg != "Request Info" { 181 continue 182 } 183 uri, ok := logMap["uri"].(string) 184 if !ok { 185 log.Warn("URI type is not string") 186 continue 187 } 188 removedOptions := strings.Split(uri, "?")[0] 189 splitURI := strings.Split(removedOptions, "/") 190 // first string is empty string "" 191 if len(splitURI) < 2 { 192 log.Warnf("Invalid URI: \"%s\"", uri) 193 continue 194 } 195 splitURI = splitURI[1:] 196 switch len(splitURI) { 197 case 3: 198 if splitURI[0] == "api" { 199 resources[schema.GroupVersionKind{Version: splitURI[1], Kind: splitURI[2]}] = true 200 break 201 } else if splitURI[0] == "apis" { 202 // this situation happens when the client enumerates the available resources of the server 203 // Example: "/apis/apps/v1?timeout=32s" 204 break 205 } 206 log.Warnf("Invalid URI: \"%s\"", uri) 207 case 4: 208 if splitURI[0] == "apis" { 209 resources[schema.GroupVersionKind{Group: splitURI[1], Version: splitURI[2], Kind: splitURI[3]}] = true 210 break 211 } 212 log.Warnf("Invalid URI: \"%s\"", uri) 213 case 5: 214 if splitURI[0] == "api" { 215 resources[schema.GroupVersionKind{Version: splitURI[1], Kind: splitURI[4]}] = true 216 break 217 } else if splitURI[0] == "apis" { 218 resources[schema.GroupVersionKind{Group: splitURI[1], Version: splitURI[2], Kind: splitURI[3]}] = true 219 break 220 } 221 log.Warnf("Invalid URI: \"%s\"", uri) 222 case 6, 7: 223 if splitURI[0] == "apis" { 224 resources[schema.GroupVersionKind{Group: splitURI[1], Version: splitURI[2], Kind: splitURI[5]}] = true 225 break 226 } 227 log.Warnf("Invalid URI: \"%s\"", uri) 228 } 229 } 230 var resourcesArr []schema.GroupVersionKind 231 for gvk := range resources { 232 resourcesArr = append(resourcesArr, gvk) 233 } 234 return resourcesArr, nil 235 } 236 237 // Run - implements Test interface 238 func (t *AnnotationsContainExamplesTest) Run(ctx context.Context) *TestResult { 239 res := &TestResult{Test: t, MaximumPoints: 1} 240 if t.CSV.Annotations != nil && t.CSV.Annotations["alm-examples"] != "" { 241 res.EarnedPoints = 1 242 } 243 if res.EarnedPoints == 0 { 244 res.Suggestions = append(res.Suggestions, fmt.Sprintf("Add an alm-examples annotation to your CSV to pass the %s test", t.GetName())) 245 } 246 return res 247 } 248 249 // Run - implements Test interface 250 func (t *StatusDescriptorsTest) Run(ctx context.Context) *TestResult { 251 res := &TestResult{Test: t} 252 err := t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR) 253 if err != nil { 254 res.Errors = append(res.Errors, err) 255 return res 256 } 257 if t.CR.Object["status"] == nil { 258 return res 259 } 260 statusBlock := t.CR.Object["status"].(map[string]interface{}) 261 res.MaximumPoints = len(statusBlock) 262 var crd *olmapiv1alpha1.CRDDescription 263 for _, owned := range t.CSV.Spec.CustomResourceDefinitions.Owned { 264 if owned.Kind == t.CR.GetKind() { 265 crd = &owned 266 break 267 } 268 } 269 if crd == nil { 270 return res 271 } 272 for key := range statusBlock { 273 for _, statDesc := range crd.StatusDescriptors { 274 if statDesc.Path == key { 275 res.EarnedPoints++ 276 delete(statusBlock, key) 277 break 278 } 279 } 280 } 281 for key := range statusBlock { 282 res.Suggestions = append(res.Suggestions, "Add a status descriptor for "+key) 283 } 284 return res 285 } 286 287 // Run - implements Test interface 288 func (t *SpecDescriptorsTest) Run(ctx context.Context) *TestResult { 289 res := &TestResult{Test: t} 290 err := t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR) 291 if err != nil { 292 res.Errors = append(res.Errors, err) 293 return res 294 } 295 if t.CR.Object["spec"] == nil { 296 return res 297 } 298 specBlock := t.CR.Object["spec"].(map[string]interface{}) 299 res.MaximumPoints = len(specBlock) 300 var crd *olmapiv1alpha1.CRDDescription 301 for _, owned := range t.CSV.Spec.CustomResourceDefinitions.Owned { 302 if owned.Kind == t.CR.GetKind() { 303 crd = &owned 304 break 305 } 306 } 307 if crd == nil { 308 return res 309 } 310 for key := range specBlock { 311 for _, statDesc := range crd.SpecDescriptors { 312 if statDesc.Path == key { 313 res.EarnedPoints++ 314 delete(specBlock, key) 315 break 316 } 317 } 318 } 319 for key := range specBlock { 320 res.Suggestions = append(res.Suggestions, "Add a spec descriptor for "+key) 321 } 322 return res 323 }