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 }