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 }