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  }