github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/cmd/operator-sdk/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 ScorecardConf string 82 ) 83 84 const ( 85 scorecardPodName = "operator-scorecard-test" 86 scorecardContainerName = "scorecard-proxy" 87 ) 88 89 func ScorecardTests(cmd *cobra.Command, args []string) error { 90 if err := initConfig(); err != nil { 91 return err 92 } 93 if err := validateScorecardFlags(); err != nil { 94 return err 95 } 96 cmd.SilenceUsage = true 97 if viper.GetBool(VerboseOpt) { 98 log.SetLevel(log.DebugLevel) 99 } 100 defer func() { 101 if err := cleanupScorecard(); err != nil { 102 log.Errorf("Failed to clenup resources: (%v)", err) 103 } 104 }() 105 106 var ( 107 tmpNamespaceVar string 108 err error 109 ) 110 kubeconfig, tmpNamespaceVar, err = k8sInternal.GetKubeconfigAndNamespace(viper.GetString(KubeconfigOpt)) 111 if err != nil { 112 return fmt.Errorf("failed to build the kubeconfig: %v", err) 113 } 114 if viper.GetString(NamespaceOpt) == "" { 115 viper.Set(NamespaceOpt, tmpNamespaceVar) 116 } 117 scheme := runtime.NewScheme() 118 // scheme for client go 119 if err := cgoscheme.AddToScheme(scheme); err != nil { 120 return fmt.Errorf("failed to add client-go scheme to client: (%v)", err) 121 } 122 // api extensions scheme (CRDs) 123 if err := extscheme.AddToScheme(scheme); err != nil { 124 return fmt.Errorf("failed to add failed to add extensions api scheme to client: (%v)", err) 125 } 126 // olm api (CS 127 if err := olmapiv1alpha1.AddToScheme(scheme); err != nil { 128 return fmt.Errorf("failed to add failed to add oml api scheme (CSVs) to client: (%v)", err) 129 } 130 dynamicDecoder = serializer.NewCodecFactory(scheme).UniversalDeserializer() 131 // if a user creates a new CRD, we need to be able to reset the rest mapper 132 // temporary kubeclient to get a cached discovery 133 kubeclient, err := kubernetes.NewForConfig(kubeconfig) 134 if err != nil { 135 return fmt.Errorf("failed to get a kubeclient: %v", err) 136 } 137 cachedDiscoveryClient := cached.NewMemCacheClient(kubeclient.Discovery()) 138 restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient) 139 restMapper.Reset() 140 runtimeClient, _ = client.New(kubeconfig, client.Options{Scheme: scheme, Mapper: restMapper}) 141 142 csv := &olmapiv1alpha1.ClusterServiceVersion{} 143 if viper.GetBool(OLMTestsOpt) { 144 yamlSpec, err := ioutil.ReadFile(viper.GetString(CSVPathOpt)) 145 if err != nil { 146 return fmt.Errorf("failed to read csv: %v", err) 147 } 148 if err = yaml.Unmarshal(yamlSpec, csv); err != nil { 149 return fmt.Errorf("error getting ClusterServiceVersion: %v", err) 150 } 151 } 152 153 // Extract operator manifests from the CSV if olm-deployed is set. 154 if viper.GetBool(OlmDeployedOpt) { 155 // Get deploymentName from the deployment manifest within the CSV. 156 strat, err := (&olminstall.StrategyResolver{}).UnmarshalStrategy(csv.Spec.InstallStrategy) 157 if err != nil { 158 return err 159 } 160 stratDep, ok := strat.(*olminstall.StrategyDetailsDeployment) 161 if !ok { 162 return fmt.Errorf("expected StrategyDetailsDeployment, got strategy of type %T", strat) 163 } 164 deploymentName = stratDep.DeploymentSpecs[0].Name 165 // Get the proxy pod, which should have been created with the CSV. 166 proxyPodGlobal, err = getPodFromDeployment(deploymentName, viper.GetString(NamespaceOpt)) 167 if err != nil { 168 return err 169 } 170 171 // Create a temporary CR manifest from metadata if one is not provided. 172 crJSONStr, ok := csv.ObjectMeta.Annotations["alm-examples"] 173 if ok && viper.GetString(CRManifestOpt) == "" { 174 var crs []interface{} 175 if err = json.Unmarshal([]byte(crJSONStr), &crs); err != nil { 176 return err 177 } 178 // TODO: run scorecard against all CR's in CSV. 179 cr := crs[0] 180 crJSONBytes, err := json.Marshal(cr) 181 if err != nil { 182 return err 183 } 184 crYAMLBytes, err := yaml.JSONToYAML(crJSONBytes) 185 if err != nil { 186 return err 187 } 188 crFile, err := ioutil.TempFile("", "cr.yaml") 189 if err != nil { 190 return err 191 } 192 if _, err := crFile.Write(crYAMLBytes); err != nil { 193 return err 194 } 195 viper.Set(CRManifestOpt, crFile.Name()) 196 defer func() { 197 err := os.Remove(viper.GetString(CRManifestOpt)) 198 if err != nil { 199 log.Errorf("Could not delete temporary CR manifest file: (%v)", err) 200 } 201 }() 202 } 203 204 } else { 205 // If no namespaced manifest path is given, combine 206 // deploy/{service_account,role.yaml,role_binding,operator}.yaml. 207 if viper.GetString(NamespacedManifestOpt) == "" { 208 file, err := yamlutil.GenerateCombinedNamespacedManifest(scaffold.DeployDir) 209 if err != nil { 210 return err 211 } 212 viper.Set(NamespacedManifestOpt, file.Name()) 213 defer func() { 214 err := os.Remove(viper.GetString(NamespacedManifestOpt)) 215 if err != nil { 216 log.Errorf("Could not delete temporary namespace manifest file: (%v)", err) 217 } 218 }() 219 } 220 // If no global manifest is given, combine all CRD's in the given CRD's dir. 221 if viper.GetString(GlobalManifestOpt) == "" { 222 gMan, err := yamlutil.GenerateCombinedGlobalManifest(viper.GetString(CRDsDirOpt)) 223 if err != nil { 224 return err 225 } 226 viper.Set(GlobalManifestOpt, gMan.Name()) 227 defer func() { 228 err := os.Remove(viper.GetString(GlobalManifestOpt)) 229 if err != nil { 230 log.Errorf("Could not delete global manifest file: (%v)", err) 231 } 232 }() 233 } 234 if err := createFromYAMLFile(viper.GetString(GlobalManifestOpt)); err != nil { 235 return fmt.Errorf("failed to create global resources: %v", err) 236 } 237 if err := createFromYAMLFile(viper.GetString(NamespacedManifestOpt)); err != nil { 238 return fmt.Errorf("failed to create namespaced resources: %v", err) 239 } 240 } 241 242 if err := createFromYAMLFile(viper.GetString(CRManifestOpt)); err != nil { 243 return fmt.Errorf("failed to create cr resource: %v", err) 244 } 245 obj, err := yamlToUnstructured(viper.GetString(CRManifestOpt)) 246 if err != nil { 247 return fmt.Errorf("failed to decode custom resource manifest into object: %s", err) 248 } 249 if err := waitUntilCRStatusExists(obj); err != nil { 250 return fmt.Errorf("failed waiting to check if CR status exists: %v", err) 251 } 252 var suites []*TestSuite 253 254 // Run tests. 255 if viper.GetBool(BasicTestsOpt) { 256 conf := BasicTestConfig{ 257 Client: runtimeClient, 258 CR: obj, 259 ProxyPod: proxyPodGlobal, 260 } 261 basicTests := NewBasicTestSuite(conf) 262 basicTests.Run(context.TODO()) 263 suites = append(suites, basicTests) 264 } 265 if viper.GetBool(OLMTestsOpt) { 266 conf := OLMTestConfig{ 267 Client: runtimeClient, 268 CR: obj, 269 CSV: csv, 270 CRDsDir: viper.GetString(CRDsDirOpt), 271 ProxyPod: proxyPodGlobal, 272 } 273 olmTests := NewOLMTestSuite(conf) 274 olmTests.Run(context.TODO()) 275 suites = append(suites, olmTests) 276 } 277 totalScore := 0.0 278 for _, suite := range suites { 279 fmt.Printf("%s:\n", suite.GetName()) 280 for _, result := range suite.TestResults { 281 fmt.Printf("\t%s: %d/%d\n", result.Test.GetName(), result.EarnedPoints, result.MaximumPoints) 282 } 283 totalScore += float64(suite.TotalScore()) 284 } 285 totalScore = totalScore / float64(len(suites)) 286 fmt.Printf("\nTotal Score: %.0f%%\n", totalScore) 287 // Print suggestions 288 for _, suite := range suites { 289 for _, result := range suite.TestResults { 290 for _, suggestion := range result.Suggestions { 291 // 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings) 292 fmt.Printf("\x1b[%dmSUGGESTION:\x1b[0m %s\n", 33, suggestion) 293 } 294 } 295 } 296 // Print errors 297 for _, suite := range suites { 298 for _, result := range suite.TestResults { 299 for _, err := range result.Errors { 300 // 31 is red (specifically, the same shade of red that logrus uses for errors) 301 fmt.Printf("\x1b[%dmERROR:\x1b[0m %s\n", 31, err) 302 } 303 } 304 } 305 return nil 306 } 307 308 func initConfig() error { 309 if ScorecardConf != "" { 310 // Use config file from the flag. 311 viper.SetConfigFile(ScorecardConf) 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 }