github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/scorecard/scorecard.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  	"errors"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  
    23  	"github.com/operator-framework/operator-sdk/internal/util/projutil"
    24  
    25  	k8sInternal "github.com/operator-framework/operator-sdk/internal/util/k8sutil"
    26  	"github.com/operator-framework/operator-sdk/internal/util/yamlutil"
    27  
    28  	olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
    29  	log "github.com/sirupsen/logrus"
    30  	"github.com/spf13/cobra"
    31  	"github.com/spf13/viper"
    32  	v1 "k8s.io/api/core/v1"
    33  	extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/serializer"
    36  	"k8s.io/client-go/discovery/cached"
    37  	"k8s.io/client-go/kubernetes"
    38  	cgoscheme "k8s.io/client-go/kubernetes/scheme"
    39  	"k8s.io/client-go/rest"
    40  	"k8s.io/client-go/restmapper"
    41  	"sigs.k8s.io/controller-runtime/pkg/client"
    42  )
    43  
    44  const (
    45  	ConfigOpt             = "config"
    46  	NamespaceOpt          = "namespace"
    47  	KubeconfigOpt         = "kubeconfig"
    48  	InitTimeoutOpt        = "init-timeout"
    49  	CSVPathOpt            = "csv-path"
    50  	BasicTestsOpt         = "basic-tests"
    51  	OLMTestsOpt           = "olm-tests"
    52  	TenantTestsOpt        = "good-tenant-tests"
    53  	NamespacedManifestOpt = "namespace-manifest"
    54  	GlobalManifestOpt     = "global-manifest"
    55  	CRManifestOpt         = "cr-manifest"
    56  	ProxyImageOpt         = "proxy-image"
    57  	ProxyPullPolicyOpt    = "proxy-pull-policy"
    58  	CRDsDirOpt            = "crds-dir"
    59  	VerboseOpt            = "verbose"
    60  )
    61  
    62  const (
    63  	basicOperator  = "Basic Operator"
    64  	olmIntegration = "OLM Integration"
    65  	goodTenant     = "Good Tenant"
    66  )
    67  
    68  // TODO: add point weights to tests
    69  type scorecardTest struct {
    70  	testType      string
    71  	name          string
    72  	description   string
    73  	earnedPoints  int
    74  	maximumPoints int
    75  }
    76  
    77  type cleanupFn func() error
    78  
    79  var (
    80  	kubeconfig     *rest.Config
    81  	scTests        []scorecardTest
    82  	scSuggestions  []string
    83  	dynamicDecoder runtime.Decoder
    84  	runtimeClient  client.Client
    85  	restMapper     *restmapper.DeferredDiscoveryRESTMapper
    86  	deploymentName string
    87  	proxyPod       *v1.Pod
    88  	cleanupFns     []cleanupFn
    89  	ScorecardConf  string
    90  )
    91  
    92  const scorecardPodName = "operator-scorecard-test"
    93  
    94  func ScorecardTests(cmd *cobra.Command, args []string) error {
    95  	err := initConfig()
    96  	if err != nil {
    97  		return err
    98  	}
    99  	if viper.GetString(CRManifestOpt) == "" {
   100  		return errors.New("cr-manifest config option missing")
   101  	}
   102  	if !viper.GetBool(BasicTestsOpt) && !viper.GetBool(OLMTestsOpt) {
   103  		return errors.New("at least one test type is required")
   104  	}
   105  	if viper.GetBool(OLMTestsOpt) && viper.GetString(CSVPathOpt) == "" {
   106  		return fmt.Errorf("if olm-tests is enabled, the --csv-path flag must be set")
   107  	}
   108  	pullPolicy := viper.GetString(ProxyPullPolicyOpt)
   109  	if pullPolicy != "Always" && pullPolicy != "Never" && pullPolicy != "PullIfNotPresent" {
   110  		return fmt.Errorf("invalid proxy pull policy: (%s); valid values: Always, Never, PullIfNotPresent", pullPolicy)
   111  	}
   112  	cmd.SilenceUsage = true
   113  	if viper.GetBool(VerboseOpt) {
   114  		log.SetLevel(log.DebugLevel)
   115  	}
   116  	// if no namespaced manifest path is given, combine deploy/service_account.yaml, deploy/role.yaml, deploy/role_binding.yaml and deploy/operator.yaml
   117  	if viper.GetString(NamespacedManifestOpt) == "" {
   118  		file, err := yamlutil.GenerateCombinedNamespacedManifest()
   119  		if err != nil {
   120  			return err
   121  		}
   122  		viper.Set(NamespacedManifestOpt, file.Name())
   123  		defer func() {
   124  			err := os.Remove(viper.GetString(NamespacedManifestOpt))
   125  			if err != nil {
   126  				log.Errorf("Could not delete temporary namespace manifest file: (%v)", err)
   127  			}
   128  		}()
   129  	}
   130  	if viper.GetString(GlobalManifestOpt) == "" {
   131  		file, err := yamlutil.GenerateCombinedGlobalManifest()
   132  		if err != nil {
   133  			return err
   134  		}
   135  		viper.Set(GlobalManifestOpt, file.Name())
   136  		defer func() {
   137  			err := os.Remove(viper.GetString(GlobalManifestOpt))
   138  			if err != nil {
   139  				log.Errorf("Could not delete global manifest file: (%v)", err)
   140  			}
   141  		}()
   142  	}
   143  	defer func() {
   144  		if err := cleanupScorecard(); err != nil {
   145  			log.Errorf("Failed to clenup resources: (%v)", err)
   146  		}
   147  	}()
   148  	var tmpNamespaceVar string
   149  	kubeconfig, tmpNamespaceVar, err = k8sInternal.GetKubeconfigAndNamespace(viper.GetString(KubeconfigOpt))
   150  	if err != nil {
   151  		return fmt.Errorf("failed to build the kubeconfig: %v", err)
   152  	}
   153  	if viper.GetString(NamespaceOpt) == "" {
   154  		viper.Set(NamespaceOpt, tmpNamespaceVar)
   155  	}
   156  	scheme := runtime.NewScheme()
   157  	// scheme for client go
   158  	if err := cgoscheme.AddToScheme(scheme); err != nil {
   159  		return fmt.Errorf("failed to add client-go scheme to client: (%v)", err)
   160  	}
   161  	// api extensions scheme (CRDs)
   162  	if err := extscheme.AddToScheme(scheme); err != nil {
   163  		return fmt.Errorf("failed to add failed to add extensions api scheme to client: (%v)", err)
   164  	}
   165  	// olm api (CS
   166  	if err := olmapiv1alpha1.AddToScheme(scheme); err != nil {
   167  		return fmt.Errorf("failed to add failed to add oml api scheme (CSVs) to client: (%v)", err)
   168  	}
   169  	dynamicDecoder = serializer.NewCodecFactory(scheme).UniversalDeserializer()
   170  	// if a user creates a new CRD, we need to be able to reset the rest mapper
   171  	// temporary kubeclient to get a cached discovery
   172  	kubeclient, err := kubernetes.NewForConfig(kubeconfig)
   173  	if err != nil {
   174  		return fmt.Errorf("failed to get a kubeclient: %v", err)
   175  	}
   176  	cachedDiscoveryClient := cached.NewMemCacheClient(kubeclient.Discovery())
   177  	restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient)
   178  	restMapper.Reset()
   179  	runtimeClient, _ = client.New(kubeconfig, client.Options{Scheme: scheme, Mapper: restMapper})
   180  	if err := createFromYAMLFile(viper.GetString(GlobalManifestOpt)); err != nil {
   181  		return fmt.Errorf("failed to create global resources: %v", err)
   182  	}
   183  	if err := createFromYAMLFile(viper.GetString(NamespacedManifestOpt)); err != nil {
   184  		return fmt.Errorf("failed to create namespaced resources: %v", err)
   185  	}
   186  	if err := createFromYAMLFile(viper.GetString(CRManifestOpt)); err != nil {
   187  		return fmt.Errorf("failed to create cr resource: %v", err)
   188  	}
   189  	obj, err := yamlToUnstructured(viper.GetString(CRManifestOpt))
   190  	if err != nil {
   191  		return fmt.Errorf("failed to decode custom resource manifest into object: %s", err)
   192  	}
   193  	if viper.GetBool(BasicTestsOpt) {
   194  		fmt.Println("Checking for existence of spec and status blocks in CR")
   195  		err = checkSpecAndStat(runtimeClient, obj, false)
   196  		if err != nil {
   197  			return err
   198  		}
   199  		// This test is far too inconsistent and unreliable to be meaningful,
   200  		// so it has been disabled
   201  		/*
   202  			fmt.Println("Checking that operator actions are reflected in status")
   203  			err = checkStatusUpdate(runtimeClient, obj)
   204  			if err != nil {
   205  				return err
   206  			}
   207  		*/
   208  		fmt.Println("Checking that writing into CRs has an effect")
   209  		logs, err := writingIntoCRsHasEffect(obj)
   210  		if err != nil {
   211  			return err
   212  		}
   213  		log.Debugf("Scorecard Proxy Logs: %v\n", logs)
   214  	} else {
   215  		// checkSpecAndStat is used to make sure the operator is ready in this case
   216  		// the boolean argument set at the end tells the function not to add the result to scTests
   217  		err = checkSpecAndStat(runtimeClient, obj, true)
   218  		if err != nil {
   219  			return err
   220  		}
   221  	}
   222  	if viper.GetBool(OLMTestsOpt) {
   223  		yamlSpec, err := ioutil.ReadFile(viper.GetString(CSVPathOpt))
   224  		if err != nil {
   225  			return fmt.Errorf("failed to read csv: %v", err)
   226  		}
   227  		rawCSV, _, err := dynamicDecoder.Decode(yamlSpec, nil, nil)
   228  		if err != nil {
   229  			return err
   230  		}
   231  		csv := &olmapiv1alpha1.ClusterServiceVersion{}
   232  		switch o := rawCSV.(type) {
   233  		case *olmapiv1alpha1.ClusterServiceVersion:
   234  			csv = o
   235  		default:
   236  			return fmt.Errorf("provided yaml file not of ClusterServiceVersion type")
   237  		}
   238  		fmt.Println("Checking if all CRDs have validation")
   239  		if err := crdsHaveValidation(viper.GetString(CRDsDirOpt), runtimeClient, obj); err != nil {
   240  			return err
   241  		}
   242  		fmt.Println("Checking for CRD resources")
   243  		crdsHaveResources(csv)
   244  		fmt.Println("Checking for existence of example CRs")
   245  		annotationsContainExamples(csv)
   246  		fmt.Println("Checking spec descriptors")
   247  		err = specDescriptors(csv, runtimeClient, obj)
   248  		if err != nil {
   249  			return err
   250  		}
   251  		fmt.Println("Checking status descriptors")
   252  		err = statusDescriptors(csv, runtimeClient, obj)
   253  		if err != nil {
   254  			return err
   255  		}
   256  	}
   257  	var totalEarned, totalMax int
   258  	var enabledTestTypes []string
   259  	if viper.GetBool(BasicTestsOpt) {
   260  		enabledTestTypes = append(enabledTestTypes, basicOperator)
   261  	}
   262  	if viper.GetBool(OLMTestsOpt) {
   263  		enabledTestTypes = append(enabledTestTypes, olmIntegration)
   264  	}
   265  	if viper.GetBool(TenantTestsOpt) {
   266  		enabledTestTypes = append(enabledTestTypes, goodTenant)
   267  	}
   268  	for _, testType := range enabledTestTypes {
   269  		fmt.Printf("%s:\n", testType)
   270  		for _, test := range scTests {
   271  			if test.testType == testType {
   272  				if !(test.earnedPoints == 0 && test.maximumPoints == 0) {
   273  					fmt.Printf("\t%s: %d/%d points\n", test.name, test.earnedPoints, test.maximumPoints)
   274  				} else {
   275  					fmt.Printf("\t%s: N/A (depends on an earlier test that failed)\n", test.name)
   276  				}
   277  				totalEarned += test.earnedPoints
   278  				totalMax += test.maximumPoints
   279  			}
   280  		}
   281  	}
   282  	fmt.Printf("\nTotal Score: %d/%d points\n", totalEarned, totalMax)
   283  	for _, suggestion := range scSuggestions {
   284  		// 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings)
   285  		fmt.Printf("\x1b[%dmSUGGESTION:\x1b[0m %s\n", 33, suggestion)
   286  	}
   287  	return nil
   288  }
   289  
   290  func initConfig() error {
   291  	if ScorecardConf != "" {
   292  		// Use config file from the flag.
   293  		viper.SetConfigFile(ScorecardConf)
   294  	} else {
   295  		viper.AddConfigPath(projutil.MustGetwd())
   296  		// using SetConfigName allows users to use a .yaml, .json, or .toml file
   297  		viper.SetConfigName(".osdk-scorecard")
   298  	}
   299  
   300  	if err := viper.ReadInConfig(); err == nil {
   301  		log.Info("Using config file: ", viper.ConfigFileUsed())
   302  	} else {
   303  		log.Warn("Could not load config file; using flags")
   304  	}
   305  	return nil
   306  }