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  }