github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/scorecard/basic_tests.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 "math/rand" 23 "reflect" 24 "strings" 25 "time" 26 27 "github.com/operator-framework/operator-sdk/internal/util/fileutil" 28 29 log "github.com/sirupsen/logrus" 30 "github.com/spf13/viper" 31 appsv1 "k8s.io/api/apps/v1" 32 v1 "k8s.io/api/core/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/labels" 35 "k8s.io/apimachinery/pkg/types" 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/client-go/kubernetes" 38 "sigs.k8s.io/controller-runtime/pkg/client" 39 ) 40 41 // checkSpecAndStat checks that the spec and status blocks exist. If noStore is set to true, this function 42 // will not store the result of the test in scTests and will instead just wait until the spec and 43 // status blocks exist or return an error after the timeout. 44 func checkSpecAndStat(runtimeClient client.Client, obj *unstructured.Unstructured, noStore bool) error { 45 testSpec := scorecardTest{testType: basicOperator, name: "Spec Block Exists", maximumPoints: 1} 46 testStat := scorecardTest{testType: basicOperator, name: "Status Block Exist", maximumPoints: 1} 47 err := wait.Poll(time.Second*1, time.Second*time.Duration(viper.GetInt64(InitTimeoutOpt)), func() (bool, error) { 48 err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj) 49 if err != nil { 50 return false, fmt.Errorf("error getting custom resource: %v", err) 51 } 52 var specPass, statusPass bool 53 if obj.Object["spec"] != nil { 54 testSpec.earnedPoints = 1 55 specPass = true 56 } 57 58 if obj.Object["status"] != nil { 59 testStat.earnedPoints = 1 60 statusPass = true 61 } 62 return statusPass && specPass, nil 63 }) 64 if !noStore { 65 scTests = append(scTests, testSpec, testStat) 66 } 67 if err != nil && err != wait.ErrWaitTimeout { 68 return err 69 } 70 if testSpec.earnedPoints != 1 { 71 scSuggestions = append(scSuggestions, "Add a 'spec' field to your Custom Resource") 72 } 73 if testStat.earnedPoints != 1 { 74 scSuggestions = append(scSuggestions, "Add a 'status' field to your Custom Resource") 75 } 76 return nil 77 } 78 79 // TODO: user specified tests for operators 80 81 // checkStatusUpdate looks at all fields in the spec section of a custom resource and attempts to modify them and 82 // see if the status changes as a result. This is a bit prone to breakage as this is a black box test and we don't 83 // know much about how the operators we are testing actually work and may pass an invalid value. In the future, we 84 // should use user-specified tests 85 func checkStatusUpdate(runtimeClient client.Client, obj *unstructured.Unstructured) error { 86 test := scorecardTest{testType: basicOperator, name: "Operator actions are reflected in status", maximumPoints: 1} 87 err := runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj) 88 if err != nil { 89 return fmt.Errorf("error getting custom resource: %v", err) 90 } 91 if obj.Object["status"] == nil || obj.Object["spec"] == nil { 92 scTests = append(scTests, test) 93 return nil 94 } 95 statCopy := make(map[string]interface{}) 96 for k, v := range obj.Object["status"].(map[string]interface{}) { 97 statCopy[k] = v 98 } 99 specMap := obj.Object["spec"].(map[string]interface{}) 100 err = modifySpecAndCheck(specMap, obj) 101 if err != nil { 102 test.earnedPoints = 0 103 scSuggestions = append(scSuggestions, "Make sure that the 'status' block is always updated to reflect changes after the 'spec' block is changed") 104 scTests = append(scTests, test) 105 return nil 106 } 107 test.earnedPoints = 1 108 scTests = append(scTests, test) 109 return nil 110 } 111 112 // modifySpecAndCheck is a helper function for checkStatusUpdate 113 func modifySpecAndCheck(specMap map[string]interface{}, obj *unstructured.Unstructured) error { 114 statCopy := make(map[string]interface{}) 115 for k, v := range obj.Object["status"].(map[string]interface{}) { 116 statCopy[k] = v 117 } 118 var err error 119 for k, v := range specMap { 120 mapType := false 121 switch t := v.(type) { 122 case int64: 123 specMap[k] = specMap[k].(int64) + 1 124 case float64: 125 specMap[k] = specMap[k].(float64) + 1 126 case string: 127 // TODO: try and find out how to make this better 128 // Since strings may be very operator specific, this test may not work correctly in many cases 129 specMap[k] = fmt.Sprintf("operator sdk test value %f", rand.Float64()) 130 case bool: 131 specMap[k] = !specMap[k].(bool) 132 case map[string]interface{}: 133 mapType = true 134 err = modifySpecAndCheck(specMap[k].(map[string]interface{}), obj) 135 case []map[string]interface{}: 136 mapType = true 137 for _, item := range specMap[k].([]map[string]interface{}) { 138 err = modifySpecAndCheck(item, obj) 139 if err != nil { 140 break 141 } 142 } 143 case []interface{}: // TODO: Decide how this should be handled 144 default: 145 fmt.Printf("Unknown type for key (%s) in spec: (%v)\n", k, reflect.TypeOf(t)) 146 } 147 if !mapType { 148 if err := runtimeClient.Update(context.TODO(), obj); err != nil { 149 return fmt.Errorf("failed to update object: %v", err) 150 } 151 err = wait.Poll(time.Second*1, time.Second*15, func() (done bool, err error) { 152 err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj) 153 if err != nil { 154 return false, err 155 } 156 return !reflect.DeepEqual(statCopy, obj.Object["status"]), nil 157 }) 158 } 159 if err != nil { 160 return err 161 } 162 //reset stat copy to match 163 statCopy = make(map[string]interface{}) 164 for k, v := range obj.Object["status"].(map[string]interface{}) { 165 statCopy[k] = v 166 } 167 } 168 return nil 169 } 170 171 // wiritingIntoCRsHasEffect simply looks at the proxy logs and verifies that the operator is sending PUT 172 // and/or POST requests to the API server, which should mean that it is creating or modifying resources. 173 func writingIntoCRsHasEffect(obj *unstructured.Unstructured) (string, error) { 174 test := scorecardTest{testType: basicOperator, name: "Writing into CRs has an effect", maximumPoints: 1} 175 kubeclient, err := kubernetes.NewForConfig(kubeconfig) 176 if err != nil { 177 return "", fmt.Errorf("failed to create kubeclient: %v", err) 178 } 179 dep := &appsv1.Deployment{} 180 err = runtimeClient.Get(context.TODO(), types.NamespacedName{Namespace: obj.GetNamespace(), Name: deploymentName}, dep) 181 if err != nil { 182 return "", fmt.Errorf("failed to get newly created operator deployment: %v", err) 183 } 184 set := labels.Set(dep.Spec.Selector.MatchLabels) 185 pods := &v1.PodList{} 186 err = runtimeClient.List(context.TODO(), &client.ListOptions{LabelSelector: set.AsSelector()}, pods) 187 if err != nil { 188 return "", fmt.Errorf("failed to get list of pods in deployment: %v", err) 189 } 190 proxyPod = &pods.Items[0] 191 req := kubeclient.CoreV1().Pods(obj.GetNamespace()).GetLogs(proxyPod.GetName(), &v1.PodLogOptions{Container: "scorecard-proxy"}) 192 readCloser, err := req.Stream() 193 if err != nil { 194 return "", fmt.Errorf("failed to get logs: %v", err) 195 } 196 defer func() { 197 if err := readCloser.Close(); err != nil && !fileutil.IsClosedError(err) { 198 log.Errorf("Failed to close pod log reader: (%v)", err) 199 } 200 }() 201 buf := new(bytes.Buffer) 202 _, err = buf.ReadFrom(readCloser) 203 if err != nil { 204 return "", fmt.Errorf("test failed and failed to read pod logs: %v", err) 205 } 206 logs := buf.String() 207 msgMap := make(map[string]interface{}) 208 for _, msg := range strings.Split(logs, "\n") { 209 if err := json.Unmarshal([]byte(msg), &msgMap); err != nil { 210 continue 211 } 212 method, ok := msgMap["method"].(string) 213 if !ok { 214 continue 215 } 216 if method == "PUT" || method == "POST" { 217 test.earnedPoints = 1 218 break 219 } 220 } 221 scTests = append(scTests, test) 222 if test.earnedPoints != 1 { 223 scSuggestions = append(scSuggestions, "The operator should write into objects to update state. No PUT or POST requests from you operator were recorded by the scorecard.") 224 } 225 return buf.String(), nil 226 }