github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/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  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"strings"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  )
    28  
    29  // BasicTestConfig contains all variables required by the BasicTest TestSuite
    30  type BasicTestConfig struct {
    31  	Client   client.Client
    32  	CR       *unstructured.Unstructured
    33  	ProxyPod *v1.Pod
    34  }
    35  
    36  // Test Defintions
    37  
    38  // CheckSpecTest is a scorecard test that verifies that the CR has a spec block
    39  type CheckSpecTest struct {
    40  	TestInfo
    41  	BasicTestConfig
    42  }
    43  
    44  // NewCheckSpecTest returns a new CheckSpecTest object
    45  func NewCheckSpecTest(conf BasicTestConfig) *CheckSpecTest {
    46  	return &CheckSpecTest{
    47  		BasicTestConfig: conf,
    48  		TestInfo: TestInfo{
    49  			Name:        "Spec Block Exists",
    50  			Description: "Custom Resource has a Spec Block",
    51  			Cumulative:  false,
    52  		},
    53  	}
    54  }
    55  
    56  // CheckStatusTest is a scorecard test that verifies that the CR has a status block
    57  type CheckStatusTest struct {
    58  	TestInfo
    59  	BasicTestConfig
    60  }
    61  
    62  // NewCheckStatusTest returns a new CheckStatusTest object
    63  func NewCheckStatusTest(conf BasicTestConfig) *CheckStatusTest {
    64  	return &CheckStatusTest{
    65  		BasicTestConfig: conf,
    66  		TestInfo: TestInfo{
    67  			Name:        "Status Block Exists",
    68  			Description: "Custom Resource has a Status Block",
    69  			Cumulative:  false,
    70  		},
    71  	}
    72  }
    73  
    74  // WritingIntoCRsHasEffectTest is a scorecard test that verifies that the operator is making PUT and/or POST requests to the API server
    75  type WritingIntoCRsHasEffectTest struct {
    76  	TestInfo
    77  	BasicTestConfig
    78  }
    79  
    80  // NewWritingIntoCRsHasEffectTest returns a new WritingIntoCRsHasEffectTest object
    81  func NewWritingIntoCRsHasEffectTest(conf BasicTestConfig) *WritingIntoCRsHasEffectTest {
    82  	return &WritingIntoCRsHasEffectTest{
    83  		BasicTestConfig: conf,
    84  		TestInfo: TestInfo{
    85  			Name:        "Writing into CRs has an effect",
    86  			Description: "A CR sends PUT/POST requests to the API server to modify resources in response to spec block changes",
    87  			Cumulative:  false,
    88  		},
    89  	}
    90  }
    91  
    92  // NewBasicTestSuite returns a new TestSuite object containing basic, functional operator tests
    93  func NewBasicTestSuite(conf BasicTestConfig) *TestSuite {
    94  	ts := NewTestSuite(
    95  		"Basic Tests",
    96  		"Test suite that runs basic, functional operator tests",
    97  	)
    98  	ts.AddTest(NewCheckSpecTest(conf), 1.5)
    99  	ts.AddTest(NewCheckStatusTest(conf), 1)
   100  	ts.AddTest(NewWritingIntoCRsHasEffectTest(conf), 1)
   101  
   102  	return ts
   103  }
   104  
   105  // Test Implementations
   106  
   107  // Run - implements Test interface
   108  func (t *CheckSpecTest) Run(ctx context.Context) *TestResult {
   109  	res := &TestResult{Test: t, MaximumPoints: 1}
   110  	err := t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR)
   111  	if err != nil {
   112  		res.Errors = append(res.Errors, fmt.Errorf("error getting custom resource: %v", err))
   113  		return res
   114  	}
   115  	if t.CR.Object["spec"] != nil {
   116  		res.EarnedPoints++
   117  	}
   118  	if res.EarnedPoints != 1 {
   119  		res.Suggestions = append(res.Suggestions, "Add a 'spec' field to your Custom Resource")
   120  	}
   121  	return res
   122  }
   123  
   124  // Run - implements Test interface
   125  func (t *CheckStatusTest) Run(ctx context.Context) *TestResult {
   126  	res := &TestResult{Test: t, MaximumPoints: 1}
   127  	err := t.Client.Get(ctx, types.NamespacedName{Namespace: t.CR.GetNamespace(), Name: t.CR.GetName()}, t.CR)
   128  	if err != nil {
   129  		res.Errors = append(res.Errors, fmt.Errorf("error getting custom resource: %v", err))
   130  		return res
   131  	}
   132  	if t.CR.Object["status"] != nil {
   133  		res.EarnedPoints++
   134  	}
   135  	if res.EarnedPoints != 1 {
   136  		res.Suggestions = append(res.Suggestions, "Add a 'status' field to your Custom Resource")
   137  	}
   138  	return res
   139  }
   140  
   141  // Run - implements Test interface
   142  func (t *WritingIntoCRsHasEffectTest) Run(ctx context.Context) *TestResult {
   143  	res := &TestResult{Test: t, MaximumPoints: 1}
   144  	logs, err := getProxyLogs(t.ProxyPod)
   145  	if err != nil {
   146  		res.Errors = append(res.Errors, fmt.Errorf("error getting proxy logs: %v", err))
   147  		return res
   148  	}
   149  	msgMap := make(map[string]interface{})
   150  	for _, msg := range strings.Split(logs, "\n") {
   151  		if err := json.Unmarshal([]byte(msg), &msgMap); err != nil {
   152  			continue
   153  		}
   154  		method, ok := msgMap["method"].(string)
   155  		if !ok {
   156  			continue
   157  		}
   158  		if method == "PUT" || method == "POST" {
   159  			res.EarnedPoints = 1
   160  			break
   161  		}
   162  	}
   163  	if res.EarnedPoints != 1 {
   164  		res.Suggestions = append(res.Suggestions, "The operator should write into objects to update state. No PUT or POST requests from the operator were recorded by the scorecard.")
   165  	}
   166  	return res
   167  }