github.com/mkimuram/operator-sdk@v0.7.1-0.20190410172100-52ad33a4bda0/internal/pkg/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  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  
    25  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
    26  	k8sInternal "github.com/operator-framework/operator-sdk/internal/util/k8sutil"
    27  	"github.com/operator-framework/operator-sdk/internal/util/projutil"
    28  	"github.com/operator-framework/operator-sdk/internal/util/yamlutil"
    29  
    30  	"github.com/ghodss/yaml"
    31  	olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
    32  	olminstall "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
    33  	log "github.com/sirupsen/logrus"
    34  	"github.com/spf13/cobra"
    35  	"github.com/spf13/viper"
    36  	v1 "k8s.io/api/core/v1"
    37  	extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
    38  	"k8s.io/apimachinery/pkg/runtime"
    39  	"k8s.io/apimachinery/pkg/runtime/serializer"
    40  	"k8s.io/client-go/discovery/cached"
    41  	"k8s.io/client-go/kubernetes"
    42  	cgoscheme "k8s.io/client-go/kubernetes/scheme"
    43  	"k8s.io/client-go/rest"
    44  	"k8s.io/client-go/restmapper"
    45  	"sigs.k8s.io/controller-runtime/pkg/client"
    46  )
    47  
    48  const (
    49  	ConfigOpt             = "config"
    50  	NamespaceOpt          = "namespace"
    51  	KubeconfigOpt         = "kubeconfig"
    52  	InitTimeoutOpt        = "init-timeout"
    53  	OlmDeployedOpt        = "olm-deployed"
    54  	CSVPathOpt            = "csv-path"
    55  	BasicTestsOpt         = "basic-tests"
    56  	OLMTestsOpt           = "olm-tests"
    57  	TenantTestsOpt        = "good-tenant-tests"
    58  	NamespacedManifestOpt = "namespaced-manifest"
    59  	GlobalManifestOpt     = "global-manifest"
    60  	CRManifestOpt         = "cr-manifest"
    61  	ProxyImageOpt         = "proxy-image"
    62  	ProxyPullPolicyOpt    = "proxy-pull-policy"
    63  	CRDsDirOpt            = "crds-dir"
    64  	VerboseOpt            = "verbose"
    65  )
    66  
    67  const (
    68  	basicOperator  = "Basic Operator"
    69  	olmIntegration = "OLM Integration"
    70  	goodTenant     = "Good Tenant"
    71  )
    72  
    73  var (
    74  	kubeconfig     *rest.Config
    75  	dynamicDecoder runtime.Decoder
    76  	runtimeClient  client.Client
    77  	restMapper     *restmapper.DeferredDiscoveryRESTMapper
    78  	deploymentName string
    79  	proxyPodGlobal *v1.Pod
    80  	cleanupFns     []cleanupFn
    81  )
    82  
    83  const (
    84  	scorecardPodName       = "operator-scorecard-test"
    85  	scorecardContainerName = "scorecard-proxy"
    86  )
    87  
    88  func ScorecardTests(cmd *cobra.Command, args []string) error {
    89  	if err := initConfig(); err != nil {
    90  		return err
    91  	}
    92  	if err := validateScorecardFlags(); err != nil {
    93  		return err
    94  	}
    95  	cmd.SilenceUsage = true
    96  	if viper.GetBool(VerboseOpt) {
    97  		log.SetLevel(log.DebugLevel)
    98  	}
    99  	defer func() {
   100  		if err := cleanupScorecard(); err != nil {
   101  			log.Errorf("Failed to clenup resources: (%v)", err)
   102  		}
   103  	}()
   104  
   105  	var (
   106  		tmpNamespaceVar string
   107  		err             error
   108  	)
   109  	kubeconfig, tmpNamespaceVar, err = k8sInternal.GetKubeconfigAndNamespace(viper.GetString(KubeconfigOpt))
   110  	if err != nil {
   111  		return fmt.Errorf("failed to build the kubeconfig: %v", err)
   112  	}
   113  	if viper.GetString(NamespaceOpt) == "" {
   114  		viper.Set(NamespaceOpt, tmpNamespaceVar)
   115  	}
   116  	scheme := runtime.NewScheme()
   117  	// scheme for client go
   118  	if err := cgoscheme.AddToScheme(scheme); err != nil {
   119  		return fmt.Errorf("failed to add client-go scheme to client: (%v)", err)
   120  	}
   121  	// api extensions scheme (CRDs)
   122  	if err := extscheme.AddToScheme(scheme); err != nil {
   123  		return fmt.Errorf("failed to add failed to add extensions api scheme to client: (%v)", err)
   124  	}
   125  	// olm api (CS
   126  	if err := olmapiv1alpha1.AddToScheme(scheme); err != nil {
   127  		return fmt.Errorf("failed to add failed to add oml api scheme (CSVs) to client: (%v)", err)
   128  	}
   129  	dynamicDecoder = serializer.NewCodecFactory(scheme).UniversalDeserializer()
   130  	// if a user creates a new CRD, we need to be able to reset the rest mapper
   131  	// temporary kubeclient to get a cached discovery
   132  	kubeclient, err := kubernetes.NewForConfig(kubeconfig)
   133  	if err != nil {
   134  		return fmt.Errorf("failed to get a kubeclient: %v", err)
   135  	}
   136  	cachedDiscoveryClient := cached.NewMemCacheClient(kubeclient.Discovery())
   137  	restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient)
   138  	restMapper.Reset()
   139  	runtimeClient, _ = client.New(kubeconfig, client.Options{Scheme: scheme, Mapper: restMapper})
   140  
   141  	csv := &olmapiv1alpha1.ClusterServiceVersion{}
   142  	if viper.GetBool(OLMTestsOpt) {
   143  		yamlSpec, err := ioutil.ReadFile(viper.GetString(CSVPathOpt))
   144  		if err != nil {
   145  			return fmt.Errorf("failed to read csv: %v", err)
   146  		}
   147  		if err = yaml.Unmarshal(yamlSpec, csv); err != nil {
   148  			return fmt.Errorf("error getting ClusterServiceVersion: %v", err)
   149  		}
   150  	}
   151  
   152  	// Extract operator manifests from the CSV if olm-deployed is set.
   153  	if viper.GetBool(OlmDeployedOpt) {
   154  		// Get deploymentName from the deployment manifest within the CSV.
   155  		strat, err := (&olminstall.StrategyResolver{}).UnmarshalStrategy(csv.Spec.InstallStrategy)
   156  		if err != nil {
   157  			return err
   158  		}
   159  		stratDep, ok := strat.(*olminstall.StrategyDetailsDeployment)
   160  		if !ok {
   161  			return fmt.Errorf("expected StrategyDetailsDeployment, got strategy of type %T", strat)
   162  		}
   163  		deploymentName = stratDep.DeploymentSpecs[0].Name
   164  		// Get the proxy pod, which should have been created with the CSV.
   165  		proxyPodGlobal, err = getPodFromDeployment(deploymentName, viper.GetString(NamespaceOpt))
   166  		if err != nil {
   167  			return err
   168  		}
   169  
   170  		// Create a temporary CR manifest from metadata if one is not provided.
   171  		crJSONStr, ok := csv.ObjectMeta.Annotations["alm-examples"]
   172  		if ok && viper.GetString(CRManifestOpt) == "" {
   173  			var crs []interface{}
   174  			if err = json.Unmarshal([]byte(crJSONStr), &crs); err != nil {
   175  				return err
   176  			}
   177  			// TODO: run scorecard against all CR's in CSV.
   178  			cr := crs[0]
   179  			crJSONBytes, err := json.Marshal(cr)
   180  			if err != nil {
   181  				return err
   182  			}
   183  			crYAMLBytes, err := yaml.JSONToYAML(crJSONBytes)
   184  			if err != nil {
   185  				return err
   186  			}
   187  			crFile, err := ioutil.TempFile("", "cr.yaml")
   188  			if err != nil {
   189  				return err
   190  			}
   191  			if _, err := crFile.Write(crYAMLBytes); err != nil {
   192  				return err
   193  			}
   194  			viper.Set(CRManifestOpt, crFile.Name())
   195  			defer func() {
   196  				err := os.Remove(viper.GetString(CRManifestOpt))
   197  				if err != nil {
   198  					log.Errorf("Could not delete temporary CR manifest file: (%v)", err)
   199  				}
   200  			}()
   201  		}
   202  
   203  	} else {
   204  		// If no namespaced manifest path is given, combine
   205  		// deploy/{service_account,role.yaml,role_binding,operator}.yaml.
   206  		if viper.GetString(NamespacedManifestOpt) == "" {
   207  			file, err := yamlutil.GenerateCombinedNamespacedManifest(scaffold.DeployDir)
   208  			if err != nil {
   209  				return err
   210  			}
   211  			viper.Set(NamespacedManifestOpt, file.Name())
   212  			defer func() {
   213  				err := os.Remove(viper.GetString(NamespacedManifestOpt))
   214  				if err != nil {
   215  					log.Errorf("Could not delete temporary namespace manifest file: (%v)", err)
   216  				}
   217  			}()
   218  		}
   219  		// If no global manifest is given, combine all CRD's in the given CRD's dir.
   220  		if viper.GetString(GlobalManifestOpt) == "" {
   221  			gMan, err := yamlutil.GenerateCombinedGlobalManifest(viper.GetString(CRDsDirOpt))
   222  			if err != nil {
   223  				return err
   224  			}
   225  			viper.Set(GlobalManifestOpt, gMan.Name())
   226  			defer func() {
   227  				err := os.Remove(viper.GetString(GlobalManifestOpt))
   228  				if err != nil {
   229  					log.Errorf("Could not delete global manifest file: (%v)", err)
   230  				}
   231  			}()
   232  		}
   233  		if err := createFromYAMLFile(viper.GetString(GlobalManifestOpt)); err != nil {
   234  			return fmt.Errorf("failed to create global resources: %v", err)
   235  		}
   236  		if err := createFromYAMLFile(viper.GetString(NamespacedManifestOpt)); err != nil {
   237  			return fmt.Errorf("failed to create namespaced resources: %v", err)
   238  		}
   239  	}
   240  
   241  	if err := createFromYAMLFile(viper.GetString(CRManifestOpt)); err != nil {
   242  		return fmt.Errorf("failed to create cr resource: %v", err)
   243  	}
   244  	obj, err := yamlToUnstructured(viper.GetString(CRManifestOpt))
   245  	if err != nil {
   246  		return fmt.Errorf("failed to decode custom resource manifest into object: %s", err)
   247  	}
   248  	if err := waitUntilCRStatusExists(obj); err != nil {
   249  		return fmt.Errorf("failed waiting to check if CR status exists: %v", err)
   250  	}
   251  	var suites []*TestSuite
   252  
   253  	// Run tests.
   254  	if viper.GetBool(BasicTestsOpt) {
   255  		conf := BasicTestConfig{
   256  			Client:   runtimeClient,
   257  			CR:       obj,
   258  			ProxyPod: proxyPodGlobal,
   259  		}
   260  		basicTests := NewBasicTestSuite(conf)
   261  		basicTests.Run(context.TODO())
   262  		suites = append(suites, basicTests)
   263  	}
   264  	if viper.GetBool(OLMTestsOpt) {
   265  		conf := OLMTestConfig{
   266  			Client:   runtimeClient,
   267  			CR:       obj,
   268  			CSV:      csv,
   269  			CRDsDir:  viper.GetString(CRDsDirOpt),
   270  			ProxyPod: proxyPodGlobal,
   271  		}
   272  		olmTests := NewOLMTestSuite(conf)
   273  		olmTests.Run(context.TODO())
   274  		suites = append(suites, olmTests)
   275  	}
   276  	totalScore := 0.0
   277  	for _, suite := range suites {
   278  		fmt.Printf("%s:\n", suite.GetName())
   279  		for _, result := range suite.TestResults {
   280  			fmt.Printf("\t%s: %d/%d\n", result.Test.GetName(), result.EarnedPoints, result.MaximumPoints)
   281  		}
   282  		totalScore += float64(suite.TotalScore())
   283  	}
   284  	totalScore = totalScore / float64(len(suites))
   285  	fmt.Printf("\nTotal Score: %.0f%%\n", totalScore)
   286  	// Print suggestions
   287  	for _, suite := range suites {
   288  		for _, result := range suite.TestResults {
   289  			for _, suggestion := range result.Suggestions {
   290  				// 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings)
   291  				fmt.Printf("\x1b[%dmSUGGESTION:\x1b[0m %s\n", 33, suggestion)
   292  			}
   293  		}
   294  	}
   295  	// Print errors
   296  	for _, suite := range suites {
   297  		for _, result := range suite.TestResults {
   298  			for _, err := range result.Errors {
   299  				// 31 is red (specifically, the same shade of red that logrus uses for errors)
   300  				fmt.Printf("\x1b[%dmERROR:\x1b[0m %s\n", 31, err)
   301  			}
   302  		}
   303  	}
   304  	return nil
   305  }
   306  
   307  func initConfig() error {
   308  	// viper/cobra already has flags parsed at this point; we can check if a config file flag is set
   309  	if viper.GetString(ConfigOpt) != "" {
   310  		// Use config file from the flag.
   311  		viper.SetConfigFile(viper.GetString(ConfigOpt))
   312  	} else {
   313  		viper.AddConfigPath(projutil.MustGetwd())
   314  		// using SetConfigName allows users to use a .yaml, .json, or .toml file
   315  		viper.SetConfigName(".osdk-scorecard")
   316  	}
   317  
   318  	if err := viper.ReadInConfig(); err == nil {
   319  		log.Info("Using config file: ", viper.ConfigFileUsed())
   320  	} else {
   321  		log.Warn("Could not load config file; using flags")
   322  	}
   323  	return nil
   324  }
   325  
   326  func validateScorecardFlags() error {
   327  	if !viper.GetBool(OlmDeployedOpt) && viper.GetString(CRManifestOpt) == "" {
   328  		return errors.New("cr-manifest config option must be set")
   329  	}
   330  	if !viper.GetBool(BasicTestsOpt) && !viper.GetBool(OLMTestsOpt) {
   331  		return errors.New("at least one test type must be set")
   332  	}
   333  	if viper.GetBool(OLMTestsOpt) && viper.GetString(CSVPathOpt) == "" {
   334  		return fmt.Errorf("csv-path must be set if olm-tests is enabled")
   335  	}
   336  	if viper.GetBool(OlmDeployedOpt) && viper.GetString(CSVPathOpt) == "" {
   337  		return fmt.Errorf("csv-path must be set if olm-deployed is enabled")
   338  	}
   339  	pullPolicy := viper.GetString(ProxyPullPolicyOpt)
   340  	if pullPolicy != "Always" && pullPolicy != "Never" && pullPolicy != "PullIfNotPresent" {
   341  		return fmt.Errorf("invalid proxy pull policy: (%s); valid values: Always, Never, PullIfNotPresent", pullPolicy)
   342  	}
   343  	return nil
   344  }