github.com/joelanford/operator-sdk@v0.8.2/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  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  	"os/exec"
    26  
    27  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
    28  	k8sInternal "github.com/operator-framework/operator-sdk/internal/util/k8sutil"
    29  	"github.com/operator-framework/operator-sdk/internal/util/projutil"
    30  	"github.com/operator-framework/operator-sdk/internal/util/yamlutil"
    31  	scapiv1alpha1 "github.com/operator-framework/operator-sdk/pkg/apis/scorecard/v1alpha1"
    32  
    33  	"github.com/ghodss/yaml"
    34  	olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
    35  	olminstall "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
    36  	"github.com/pkg/errors"
    37  	"github.com/sirupsen/logrus"
    38  	"github.com/spf13/cobra"
    39  	"github.com/spf13/viper"
    40  	v1 "k8s.io/api/core/v1"
    41  	extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
    42  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    43  	"k8s.io/apimachinery/pkg/runtime"
    44  	"k8s.io/apimachinery/pkg/runtime/schema"
    45  	"k8s.io/apimachinery/pkg/runtime/serializer"
    46  	"k8s.io/client-go/discovery/cached"
    47  	"k8s.io/client-go/kubernetes"
    48  	cgoscheme "k8s.io/client-go/kubernetes/scheme"
    49  	"k8s.io/client-go/rest"
    50  	"k8s.io/client-go/restmapper"
    51  	"sigs.k8s.io/controller-runtime/pkg/client"
    52  )
    53  
    54  const (
    55  	ConfigOpt                 = "config"
    56  	NamespaceOpt              = "namespace"
    57  	KubeconfigOpt             = "kubeconfig"
    58  	InitTimeoutOpt            = "init-timeout"
    59  	OlmDeployedOpt            = "olm-deployed"
    60  	CSVPathOpt                = "csv-path"
    61  	BasicTestsOpt             = "basic-tests"
    62  	OLMTestsOpt               = "olm-tests"
    63  	NamespacedManifestOpt     = "namespaced-manifest"
    64  	GlobalManifestOpt         = "global-manifest"
    65  	CRManifestOpt             = "cr-manifest"
    66  	ProxyImageOpt             = "proxy-image"
    67  	ProxyPullPolicyOpt        = "proxy-pull-policy"
    68  	CRDsDirOpt                = "crds-dir"
    69  	OutputFormatOpt           = "output"
    70  	PluginDirOpt              = "plugin-dir"
    71  	JSONOutputFormat          = "json"
    72  	HumanReadableOutputFormat = "human-readable"
    73  )
    74  
    75  const (
    76  	basicOperator  = "Basic Operator"
    77  	olmIntegration = "OLM Integration"
    78  )
    79  
    80  var (
    81  	kubeconfig     *rest.Config
    82  	dynamicDecoder runtime.Decoder
    83  	runtimeClient  client.Client
    84  	restMapper     *restmapper.DeferredDiscoveryRESTMapper
    85  	deploymentName string
    86  	proxyPodGlobal *v1.Pod
    87  	cleanupFns     []cleanupFn
    88  )
    89  
    90  const (
    91  	scorecardPodName       = "operator-scorecard-test"
    92  	scorecardContainerName = "scorecard-proxy"
    93  )
    94  
    95  // make a global logger for scorecard
    96  var (
    97  	logReadWriter io.ReadWriter
    98  	log           = logrus.New()
    99  )
   100  
   101  func runTests() ([]scapiv1alpha1.ScorecardOutput, error) {
   102  	defer func() {
   103  		if err := cleanupScorecard(); err != nil {
   104  			log.Errorf("Failed to cleanup resources: (%v)", err)
   105  		}
   106  	}()
   107  
   108  	var (
   109  		tmpNamespaceVar string
   110  		err             error
   111  	)
   112  	kubeconfig, tmpNamespaceVar, err = k8sInternal.GetKubeconfigAndNamespace(viper.GetString(KubeconfigOpt))
   113  	if err != nil {
   114  		return nil, fmt.Errorf("failed to build the kubeconfig: %v", err)
   115  	}
   116  	if viper.GetString(NamespaceOpt) == "" {
   117  		viper.Set(NamespaceOpt, tmpNamespaceVar)
   118  	}
   119  	scheme := runtime.NewScheme()
   120  	// scheme for client go
   121  	if err := cgoscheme.AddToScheme(scheme); err != nil {
   122  		return nil, fmt.Errorf("failed to add client-go scheme to client: (%v)", err)
   123  	}
   124  	// api extensions scheme (CRDs)
   125  	if err := extscheme.AddToScheme(scheme); err != nil {
   126  		return nil, fmt.Errorf("failed to add failed to add extensions api scheme to client: (%v)", err)
   127  	}
   128  	// olm api (CS
   129  	if err := olmapiv1alpha1.AddToScheme(scheme); err != nil {
   130  		return nil, fmt.Errorf("failed to add failed to add oml api scheme (CSVs) to client: (%v)", err)
   131  	}
   132  	dynamicDecoder = serializer.NewCodecFactory(scheme).UniversalDeserializer()
   133  	// if a user creates a new CRD, we need to be able to reset the rest mapper
   134  	// temporary kubeclient to get a cached discovery
   135  	kubeclient, err := kubernetes.NewForConfig(kubeconfig)
   136  	if err != nil {
   137  		return nil, fmt.Errorf("failed to get a kubeclient: %v", err)
   138  	}
   139  	cachedDiscoveryClient := cached.NewMemCacheClient(kubeclient.Discovery())
   140  	restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient)
   141  	restMapper.Reset()
   142  	runtimeClient, _ = client.New(kubeconfig, client.Options{Scheme: scheme, Mapper: restMapper})
   143  
   144  	csv := &olmapiv1alpha1.ClusterServiceVersion{}
   145  	if viper.GetBool(OLMTestsOpt) {
   146  		yamlSpec, err := ioutil.ReadFile(viper.GetString(CSVPathOpt))
   147  		if err != nil {
   148  			return nil, fmt.Errorf("failed to read csv: %v", err)
   149  		}
   150  		if err = yaml.Unmarshal(yamlSpec, csv); err != nil {
   151  			return nil, fmt.Errorf("error getting ClusterServiceVersion: %v", err)
   152  		}
   153  	}
   154  
   155  	// Extract operator manifests from the CSV if olm-deployed is set.
   156  	if viper.GetBool(OlmDeployedOpt) {
   157  		// Get deploymentName from the deployment manifest within the CSV.
   158  		strat, err := (&olminstall.StrategyResolver{}).UnmarshalStrategy(csv.Spec.InstallStrategy)
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  		stratDep, ok := strat.(*olminstall.StrategyDetailsDeployment)
   163  		if !ok {
   164  			return nil, fmt.Errorf("expected StrategyDetailsDeployment, got strategy of type %T", strat)
   165  		}
   166  		deploymentName = stratDep.DeploymentSpecs[0].Name
   167  		// Get the proxy pod, which should have been created with the CSV.
   168  		proxyPodGlobal, err = getPodFromDeployment(deploymentName, viper.GetString(NamespaceOpt))
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  
   173  		logCRMsg := false
   174  		if crMans := viper.GetStringSlice(CRManifestOpt); len(crMans) == 0 {
   175  			// Create a temporary CR manifest from metadata if one is not provided.
   176  			if crJSONStr, ok := csv.ObjectMeta.Annotations["alm-examples"]; ok {
   177  				var crs []interface{}
   178  				if err = json.Unmarshal([]byte(crJSONStr), &crs); err != nil {
   179  					return nil, err
   180  				}
   181  				// TODO: run scorecard against all CR's in CSV.
   182  				cr := crs[0]
   183  				logCRMsg = len(crs) > 1
   184  				crJSONBytes, err := json.Marshal(cr)
   185  				if err != nil {
   186  					return nil, err
   187  				}
   188  				crYAMLBytes, err := yaml.JSONToYAML(crJSONBytes)
   189  				if err != nil {
   190  					return nil, err
   191  				}
   192  				crFile, err := ioutil.TempFile("", "*.cr.yaml")
   193  				if err != nil {
   194  					return nil, err
   195  				}
   196  				if _, err := crFile.Write(crYAMLBytes); err != nil {
   197  					return nil, err
   198  				}
   199  				viper.Set(CRManifestOpt, []string{crFile.Name()})
   200  				defer func() {
   201  					for _, f := range viper.GetStringSlice(CRManifestOpt) {
   202  						if err := os.Remove(f); err != nil {
   203  							log.Errorf("Could not delete temporary CR manifest file: (%v)", err)
   204  						}
   205  					}
   206  				}()
   207  			} else {
   208  				return nil, errors.New("cr-manifest config option must be set if CSV has no metadata.annotations['alm-examples']")
   209  			}
   210  		} else {
   211  			// TODO: run scorecard against all CR's in CSV.
   212  			viper.Set(CRManifestOpt, []string{crMans[0]})
   213  			logCRMsg = len(crMans) > 1
   214  		}
   215  		// Let users know that only the first CR is being tested.
   216  		if logCRMsg {
   217  			log.Infof("The scorecard does not support testing multiple CR's at once when run with --olm-deployed. Testing the first CR %s", viper.GetStringSlice(CRManifestOpt)[0])
   218  		}
   219  
   220  	} else {
   221  		// If no namespaced manifest path is given, combine
   222  		// deploy/{service_account,role.yaml,role_binding,operator}.yaml.
   223  		if viper.GetString(NamespacedManifestOpt) == "" {
   224  			file, err := yamlutil.GenerateCombinedNamespacedManifest(scaffold.DeployDir)
   225  			if err != nil {
   226  				return nil, err
   227  			}
   228  			viper.Set(NamespacedManifestOpt, file.Name())
   229  			defer func() {
   230  				err := os.Remove(viper.GetString(NamespacedManifestOpt))
   231  				if err != nil {
   232  					log.Errorf("Could not delete temporary namespace manifest file: (%v)", err)
   233  				}
   234  			}()
   235  		}
   236  		// If no global manifest is given, combine all CRD's in the given CRD's dir.
   237  		if viper.GetString(GlobalManifestOpt) == "" {
   238  			gMan, err := yamlutil.GenerateCombinedGlobalManifest(viper.GetString(CRDsDirOpt))
   239  			if err != nil {
   240  				return nil, err
   241  			}
   242  			viper.Set(GlobalManifestOpt, gMan.Name())
   243  			defer func() {
   244  				err := os.Remove(viper.GetString(GlobalManifestOpt))
   245  				if err != nil {
   246  					log.Errorf("Could not delete global manifest file: (%v)", err)
   247  				}
   248  			}()
   249  		}
   250  	}
   251  
   252  	crs := viper.GetStringSlice(CRManifestOpt)
   253  	// check if there are duplicate CRs
   254  	gvks := []schema.GroupVersionKind{}
   255  	for _, cr := range crs {
   256  		file, err := ioutil.ReadFile(cr)
   257  		if err != nil {
   258  			return nil, fmt.Errorf("failed to read file: %s", cr)
   259  		}
   260  		newGVKs, err := getGVKs(file)
   261  		if err != nil {
   262  			return nil, fmt.Errorf("could not get GVKs for resource(s) in file: %s, due to error: (%v)", cr, err)
   263  		}
   264  		gvks = append(gvks, newGVKs...)
   265  	}
   266  	dupMap := make(map[schema.GroupVersionKind]bool)
   267  	for _, gvk := range gvks {
   268  		if _, ok := dupMap[gvk]; ok {
   269  			log.Warnf("Duplicate gvks in CR list detected (%s); results may be inaccurate", gvk)
   270  		}
   271  		dupMap[gvk] = true
   272  	}
   273  
   274  	var pluginResults []scapiv1alpha1.ScorecardOutput
   275  	var suites []TestSuite
   276  	for _, cr := range crs {
   277  		// TODO: Change built-in tests into plugins
   278  		// Run built-in tests.
   279  		fmt.Printf("Running for cr: %s\n", cr)
   280  		if !viper.GetBool(OlmDeployedOpt) {
   281  			if err := createFromYAMLFile(viper.GetString(GlobalManifestOpt)); err != nil {
   282  				return nil, fmt.Errorf("failed to create global resources: %v", err)
   283  			}
   284  			if err := createFromYAMLFile(viper.GetString(NamespacedManifestOpt)); err != nil {
   285  				return nil, fmt.Errorf("failed to create namespaced resources: %v", err)
   286  			}
   287  		}
   288  		if err := createFromYAMLFile(cr); err != nil {
   289  			return nil, fmt.Errorf("failed to create cr resource: %v", err)
   290  		}
   291  		obj, err := yamlToUnstructured(cr)
   292  		if err != nil {
   293  			return nil, fmt.Errorf("failed to decode custom resource manifest into object: %s", err)
   294  		}
   295  		if err := waitUntilCRStatusExists(obj); err != nil {
   296  			return nil, fmt.Errorf("failed waiting to check if CR status exists: %v", err)
   297  		}
   298  		if viper.GetBool(BasicTestsOpt) {
   299  			conf := BasicTestConfig{
   300  				Client:   runtimeClient,
   301  				CR:       obj,
   302  				ProxyPod: proxyPodGlobal,
   303  			}
   304  			basicTests := NewBasicTestSuite(conf)
   305  			basicTests.Run(context.TODO())
   306  			suites = append(suites, *basicTests)
   307  		}
   308  		if viper.GetBool(OLMTestsOpt) {
   309  			conf := OLMTestConfig{
   310  				Client:   runtimeClient,
   311  				CR:       obj,
   312  				CSV:      csv,
   313  				CRDsDir:  viper.GetString(CRDsDirOpt),
   314  				ProxyPod: proxyPodGlobal,
   315  			}
   316  			olmTests := NewOLMTestSuite(conf)
   317  			olmTests.Run(context.TODO())
   318  			suites = append(suites, *olmTests)
   319  		}
   320  		// set up clean environment for every CR
   321  		if err := cleanupScorecard(); err != nil {
   322  			log.Errorf("Failed to cleanup resources: (%v)", err)
   323  		}
   324  		// reset cleanup functions
   325  		cleanupFns = []cleanupFn{}
   326  		// clear name of operator deployment
   327  		deploymentName = ""
   328  	}
   329  	suites, err = MergeSuites(suites)
   330  	if err != nil {
   331  		return nil, fmt.Errorf("failed to merge test suite results: %v", err)
   332  	}
   333  	for _, suite := range suites {
   334  		// convert to ScorecardOutput format
   335  		// will add log when basic and olm tests are separated into plugins
   336  		pluginResults = append(pluginResults, TestSuitesToScorecardOutput([]TestSuite{suite}, ""))
   337  	}
   338  	// Run plugins
   339  	pluginDir := viper.GetString(PluginDirOpt)
   340  	if dir, err := os.Stat(pluginDir); err != nil || !dir.IsDir() {
   341  		log.Warnf("Plugin directory not found; skipping plugin tests: %v", err)
   342  		return pluginResults, nil
   343  	}
   344  	if err := os.Chdir(pluginDir); err != nil {
   345  		return nil, fmt.Errorf("failed to chdir into scorecard plugin directory: %v", err)
   346  	}
   347  	// executable files must be in "bin" subdirectory
   348  	files, err := ioutil.ReadDir("bin")
   349  	if err != nil {
   350  		return nil, fmt.Errorf("failed to list files in %s/bin: %v", pluginDir, err)
   351  	}
   352  	for _, file := range files {
   353  		cmd := exec.Command("./bin/" + file.Name())
   354  		stdout := &bytes.Buffer{}
   355  		cmd.Stdout = stdout
   356  		stderr := &bytes.Buffer{}
   357  		cmd.Stderr = stderr
   358  		err := cmd.Run()
   359  		if err != nil {
   360  			name := fmt.Sprintf("Failed Plugin: %s", file.Name())
   361  			description := fmt.Sprintf("Plugin with file name `%s` failed", file.Name())
   362  			logs := fmt.Sprintf("%s:\nStdout: %s\nStderr: %s", err, string(stdout.Bytes()), string(stderr.Bytes()))
   363  			pluginResults = append(pluginResults, failedPlugin(name, description, logs))
   364  			// output error to main logger as well for human-readable output
   365  			log.Errorf("Plugin `%s` failed with error (%v)", file.Name(), err)
   366  			continue
   367  		}
   368  		// parse output and add to suites
   369  		result := scapiv1alpha1.ScorecardOutput{}
   370  		err = json.Unmarshal(stdout.Bytes(), &result)
   371  		if err != nil {
   372  			name := fmt.Sprintf("Plugin output invalid: %s", file.Name())
   373  			description := fmt.Sprintf("Plugin with file name %s did not produce valid ScorecardOutput JSON", file.Name())
   374  			logs := fmt.Sprintf("Stdout: %s\nStderr: %s", string(stdout.Bytes()), string(stderr.Bytes()))
   375  			pluginResults = append(pluginResults, failedPlugin(name, description, logs))
   376  			log.Errorf("Output from plugin `%s` failed to unmarshal with error (%v)", file.Name(), err)
   377  			continue
   378  		}
   379  		stderrString := string(stderr.Bytes())
   380  		if len(stderrString) != 0 {
   381  			log.Warn(stderrString)
   382  		}
   383  		pluginResults = append(pluginResults, result)
   384  	}
   385  	return pluginResults, nil
   386  }
   387  
   388  func ScorecardTests(cmd *cobra.Command, args []string) error {
   389  	if err := initConfig(); err != nil {
   390  		return err
   391  	}
   392  	if err := validateScorecardFlags(); err != nil {
   393  		return err
   394  	}
   395  	cmd.SilenceUsage = true
   396  	pluginOutputs, err := runTests()
   397  	if err != nil {
   398  		return err
   399  	}
   400  	totalScore := 0.0
   401  	// Update the state for the tests
   402  	for _, suite := range pluginOutputs {
   403  		for idx, res := range suite.Results {
   404  			suite.Results[idx] = UpdateSuiteStates(res)
   405  		}
   406  	}
   407  	if viper.GetString(OutputFormatOpt) == HumanReadableOutputFormat {
   408  		numSuites := 0
   409  		for _, plugin := range pluginOutputs {
   410  			for _, suite := range plugin.Results {
   411  				fmt.Printf("%s:\n", suite.Name)
   412  				for _, result := range suite.Tests {
   413  					fmt.Printf("\t%s: %d/%d\n", result.Name, result.EarnedPoints, result.MaximumPoints)
   414  				}
   415  				totalScore += float64(suite.TotalScore)
   416  				numSuites++
   417  			}
   418  		}
   419  		totalScore = totalScore / float64(numSuites)
   420  		fmt.Printf("\nTotal Score: %.0f%%\n", totalScore)
   421  		// TODO: We can probably use some helper functions to clean up these quadruple nested loops
   422  		// Print suggestions
   423  		for _, plugin := range pluginOutputs {
   424  			for _, suite := range plugin.Results {
   425  				for _, result := range suite.Tests {
   426  					for _, suggestion := range result.Suggestions {
   427  						// 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings)
   428  						fmt.Printf("\x1b[%dmSUGGESTION:\x1b[0m %s\n", 33, suggestion)
   429  					}
   430  				}
   431  			}
   432  		}
   433  		// Print errors
   434  		for _, plugin := range pluginOutputs {
   435  			for _, suite := range plugin.Results {
   436  				for _, result := range suite.Tests {
   437  					for _, err := range result.Errors {
   438  						// 31 is red (specifically, the same shade of red that logrus uses for errors)
   439  						fmt.Printf("\x1b[%dmERROR:\x1b[0m %s\n", 31, err)
   440  					}
   441  				}
   442  			}
   443  		}
   444  	}
   445  	if viper.GetString(OutputFormatOpt) == JSONOutputFormat {
   446  		log, err := ioutil.ReadAll(logReadWriter)
   447  		if err != nil {
   448  			return fmt.Errorf("failed to read log buffer: %v", err)
   449  		}
   450  		scTest := CombineScorecardOutput(pluginOutputs, string(log))
   451  		// Pretty print so users can also read the json output
   452  		bytes, err := json.MarshalIndent(scTest, "", "  ")
   453  		if err != nil {
   454  			return err
   455  		}
   456  		fmt.Printf("%s\n", string(bytes))
   457  	}
   458  	return nil
   459  }
   460  
   461  func initConfig() error {
   462  	// viper/cobra already has flags parsed at this point; we can check if a config file flag is set
   463  	if viper.GetString(ConfigOpt) != "" {
   464  		// Use config file from the flag.
   465  		viper.SetConfigFile(viper.GetString(ConfigOpt))
   466  	} else {
   467  		viper.AddConfigPath(projutil.MustGetwd())
   468  		// using SetConfigName allows users to use a .yaml, .json, or .toml file
   469  		viper.SetConfigName(".osdk-scorecard")
   470  	}
   471  
   472  	if err := viper.ReadInConfig(); err == nil {
   473  		// configure logger output before logging anything
   474  		err := configureLogger()
   475  		if err != nil {
   476  			return err
   477  		}
   478  		log.Info("Using config file: ", viper.ConfigFileUsed())
   479  	} else {
   480  		err := configureLogger()
   481  		if err != nil {
   482  			return err
   483  		}
   484  		log.Warn("Could not load config file; using flags")
   485  	}
   486  	return nil
   487  }
   488  
   489  func configureLogger() error {
   490  	if viper.GetString(OutputFormatOpt) == HumanReadableOutputFormat {
   491  		logReadWriter = os.Stdout
   492  	} else if viper.GetString(OutputFormatOpt) == JSONOutputFormat {
   493  		logReadWriter = &bytes.Buffer{}
   494  	} else {
   495  		return fmt.Errorf("invalid output format: %s", viper.GetString(OutputFormatOpt))
   496  	}
   497  	log.SetOutput(logReadWriter)
   498  	return nil
   499  }
   500  
   501  func validateScorecardFlags() error {
   502  	if !viper.GetBool(OlmDeployedOpt) && len(viper.GetStringSlice(CRManifestOpt)) == 0 {
   503  		return errors.New("cr-manifest config option must be set")
   504  	}
   505  	if !viper.GetBool(BasicTestsOpt) && !viper.GetBool(OLMTestsOpt) {
   506  		return errors.New("at least one test type must be set")
   507  	}
   508  	if viper.GetBool(OLMTestsOpt) && viper.GetString(CSVPathOpt) == "" {
   509  		return fmt.Errorf("csv-path must be set if olm-tests is enabled")
   510  	}
   511  	if viper.GetBool(OlmDeployedOpt) && viper.GetString(CSVPathOpt) == "" {
   512  		return fmt.Errorf("csv-path must be set if olm-deployed is enabled")
   513  	}
   514  	pullPolicy := viper.GetString(ProxyPullPolicyOpt)
   515  	if pullPolicy != "Always" && pullPolicy != "Never" && pullPolicy != "PullIfNotPresent" {
   516  		return fmt.Errorf("invalid proxy pull policy: (%s); valid values: Always, Never, PullIfNotPresent", pullPolicy)
   517  	}
   518  	// this is already being checked in configure logger; may be unnecessary
   519  	outputFormat := viper.GetString(OutputFormatOpt)
   520  	if outputFormat != HumanReadableOutputFormat && outputFormat != JSONOutputFormat {
   521  		return fmt.Errorf("invalid output format (%s); valid values: %s, %s", outputFormat, HumanReadableOutputFormat, JSONOutputFormat)
   522  	}
   523  	return nil
   524  }
   525  
   526  func getGVKs(yamlFile []byte) ([]schema.GroupVersionKind, error) {
   527  	var gvks []schema.GroupVersionKind
   528  
   529  	scanner := yamlutil.NewYAMLScanner(yamlFile)
   530  	for scanner.Scan() {
   531  		yamlSpec := scanner.Bytes()
   532  
   533  		obj := &unstructured.Unstructured{}
   534  		jsonSpec, err := yaml.YAMLToJSON(yamlSpec)
   535  		if err != nil {
   536  			return nil, fmt.Errorf("could not convert yaml file to json: %v", err)
   537  		}
   538  		if err := obj.UnmarshalJSON(jsonSpec); err != nil {
   539  			return nil, fmt.Errorf("failed to unmarshal object spec: (%v)", err)
   540  		}
   541  		gvks = append(gvks, obj.GroupVersionKind())
   542  	}
   543  	return gvks, nil
   544  }
   545  
   546  func failedPlugin(name, desc, log string) scapiv1alpha1.ScorecardOutput {
   547  	return scapiv1alpha1.ScorecardOutput{
   548  		Results: []scapiv1alpha1.ScorecardSuiteResult{{
   549  			Name:        name,
   550  			Description: desc,
   551  			Error:       1,
   552  			Log:         log,
   553  		},
   554  		},
   555  	}
   556  }