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  }