github.com/fabianvf/ocp-release-operator-sdk@v0.0.0-20190426141702-57620ee2f090/cmd/operator-sdk/scorecard/test_definitions.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  
    20  	olmapiv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
    21  	v1 "k8s.io/api/core/v1"
    22  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    23  	"sigs.k8s.io/controller-runtime/pkg/client"
    24  )
    25  
    26  // Type Definitions
    27  
    28  // Test provides methods for running scorecard tests
    29  type Test interface {
    30  	GetName() string
    31  	GetDescription() string
    32  	IsCumulative() bool
    33  	Run(context.Context) *TestResult
    34  }
    35  
    36  // TestResult contains a test's points, suggestions, and errors
    37  type TestResult struct {
    38  	Test          Test
    39  	EarnedPoints  int
    40  	MaximumPoints int
    41  	Suggestions   []string
    42  	Errors        []error
    43  }
    44  
    45  // TestInfo contains information about the scorecard test
    46  type TestInfo struct {
    47  	Name        string
    48  	Description string
    49  	// If a test is set to cumulative, the scores of multiple runs of the same test on separate CRs are added together for the total score.
    50  	// If cumulative is false, if any test failed, the total score is 0/1. Otherwise 1/1.
    51  	Cumulative bool
    52  }
    53  
    54  // GetName return the test name
    55  func (i TestInfo) GetName() string { return i.Name }
    56  
    57  // GetDescription returns the test description
    58  func (i TestInfo) GetDescription() string { return i.Description }
    59  
    60  // IsCumulative returns true if the test's scores are intended to be cumulative
    61  func (i TestInfo) IsCumulative() bool { return i.Cumulative }
    62  
    63  // BasicTestConfig contains all variables required by the BasicTest TestSuite
    64  type BasicTestConfig struct {
    65  	Client   client.Client
    66  	CR       *unstructured.Unstructured
    67  	ProxyPod *v1.Pod
    68  }
    69  
    70  // OLMTestConfig contains all variables required by the OLMTest TestSuite
    71  type OLMTestConfig struct {
    72  	Client   client.Client
    73  	CR       *unstructured.Unstructured
    74  	CSV      *olmapiv1alpha1.ClusterServiceVersion
    75  	CRDsDir  string
    76  	ProxyPod *v1.Pod
    77  }
    78  
    79  // TestSuite contains a list of tests and results, along with the relative weights of each test
    80  type TestSuite struct {
    81  	TestInfo
    82  	Tests       []Test
    83  	TestResults []*TestResult
    84  	Weights     map[string]float64
    85  }
    86  
    87  // Test definitions
    88  
    89  // CheckSpecTest is a scorecard test that verifies that the CR has a spec block
    90  type CheckSpecTest struct {
    91  	TestInfo
    92  	BasicTestConfig
    93  }
    94  
    95  // NewCheckSpecTest returns a new CheckSpecTest object
    96  func NewCheckSpecTest(conf BasicTestConfig) *CheckSpecTest {
    97  	return &CheckSpecTest{
    98  		BasicTestConfig: conf,
    99  		TestInfo: TestInfo{
   100  			Name:        "Spec Block Exists",
   101  			Description: "Custom Resource has a Spec Block",
   102  			Cumulative:  false,
   103  		},
   104  	}
   105  }
   106  
   107  // CheckStatusTest is a scorecard test that verifies that the CR has a status block
   108  type CheckStatusTest struct {
   109  	TestInfo
   110  	BasicTestConfig
   111  }
   112  
   113  // NewCheckStatusTest returns a new CheckStatusTest object
   114  func NewCheckStatusTest(conf BasicTestConfig) *CheckStatusTest {
   115  	return &CheckStatusTest{
   116  		BasicTestConfig: conf,
   117  		TestInfo: TestInfo{
   118  			Name:        "Status Block Exists",
   119  			Description: "Custom Resource has a Status Block",
   120  			Cumulative:  false,
   121  		},
   122  	}
   123  }
   124  
   125  // WritingIntoCRsHasEffectTest is a scorecard test that verifies that the operator is making PUT and/or POST requests to the API server
   126  type WritingIntoCRsHasEffectTest struct {
   127  	TestInfo
   128  	BasicTestConfig
   129  }
   130  
   131  // NewWritingIntoCRsHasEffectTest returns a new WritingIntoCRsHasEffectTest object
   132  func NewWritingIntoCRsHasEffectTest(conf BasicTestConfig) *WritingIntoCRsHasEffectTest {
   133  	return &WritingIntoCRsHasEffectTest{
   134  		BasicTestConfig: conf,
   135  		TestInfo: TestInfo{
   136  			Name:        "Writing into CRs has an effect",
   137  			Description: "A CR sends PUT/POST requests to the API server to modify resources in response to spec block changes",
   138  			Cumulative:  false,
   139  		},
   140  	}
   141  }
   142  
   143  // CRDsHaveValidationTest is a scorecard test that verifies that all CRDs have a validation section
   144  type CRDsHaveValidationTest struct {
   145  	TestInfo
   146  	OLMTestConfig
   147  }
   148  
   149  // NewCRDsHaveValidationTest returns a new CRDsHaveValidationTest object
   150  func NewCRDsHaveValidationTest(conf OLMTestConfig) *CRDsHaveValidationTest {
   151  	return &CRDsHaveValidationTest{
   152  		OLMTestConfig: conf,
   153  		TestInfo: TestInfo{
   154  			Name:        "Provided APIs have validation",
   155  			Description: "All CRDs have an OpenAPI validation subsection",
   156  			Cumulative:  true,
   157  		},
   158  	}
   159  }
   160  
   161  // CRDsHaveResourcesTest is a scorecard test that verifies that the CSV lists used resources in its owned CRDs secyion
   162  type CRDsHaveResourcesTest struct {
   163  	TestInfo
   164  	OLMTestConfig
   165  }
   166  
   167  // NewCRDsHaveResourcesTest returns a new CRDsHaveResourcesTest object
   168  func NewCRDsHaveResourcesTest(conf OLMTestConfig) *CRDsHaveResourcesTest {
   169  	return &CRDsHaveResourcesTest{
   170  		OLMTestConfig: conf,
   171  		TestInfo: TestInfo{
   172  			Name:        "Owned CRDs have resources listed",
   173  			Description: "All Owned CRDs contain a resources subsection",
   174  			Cumulative:  true,
   175  		},
   176  	}
   177  }
   178  
   179  // AnnotationsContainExamplesTest is a scorecard test that verifies that the CSV contains examples via the alm-examples annotation
   180  type AnnotationsContainExamplesTest struct {
   181  	TestInfo
   182  	OLMTestConfig
   183  }
   184  
   185  // NewAnnotationsContainExamplesTest returns a new AnnotationsContainExamplesTest object
   186  func NewAnnotationsContainExamplesTest(conf OLMTestConfig) *AnnotationsContainExamplesTest {
   187  	return &AnnotationsContainExamplesTest{
   188  		OLMTestConfig: conf,
   189  		TestInfo: TestInfo{
   190  			Name:        "CRs have at least 1 example",
   191  			Description: "The CSV's metadata contains an alm-examples section",
   192  			Cumulative:  true,
   193  		},
   194  	}
   195  }
   196  
   197  // SpecDescriptorsTest is a scorecard test that verifies that all spec fields have descriptors
   198  type SpecDescriptorsTest struct {
   199  	TestInfo
   200  	OLMTestConfig
   201  }
   202  
   203  // NewSpecDescriptorsTest returns a new SpecDescriptorsTest object
   204  func NewSpecDescriptorsTest(conf OLMTestConfig) *SpecDescriptorsTest {
   205  	return &SpecDescriptorsTest{
   206  		OLMTestConfig: conf,
   207  		TestInfo: TestInfo{
   208  			Name:        "Spec fields with descriptors",
   209  			Description: "All spec fields have matching descriptors in the CSV",
   210  			Cumulative:  true,
   211  		},
   212  	}
   213  }
   214  
   215  // StatusDescriptorsTest is a scorecard test that verifies that all status fields have descriptors
   216  type StatusDescriptorsTest struct {
   217  	TestInfo
   218  	OLMTestConfig
   219  }
   220  
   221  // NewStatusDescriptorsTest returns a new StatusDescriptorsTest object
   222  func NewStatusDescriptorsTest(conf OLMTestConfig) *StatusDescriptorsTest {
   223  	return &StatusDescriptorsTest{
   224  		OLMTestConfig: conf,
   225  		TestInfo: TestInfo{
   226  			Name:        "Status fields with descriptors",
   227  			Description: "All status fields have matching descriptors in the CSV",
   228  			Cumulative:  true,
   229  		},
   230  	}
   231  }
   232  
   233  // Test Suite Declarations
   234  
   235  // NewBasicTestSuite returns a new TestSuite object containing basic, functional operator tests
   236  func NewBasicTestSuite(conf BasicTestConfig) *TestSuite {
   237  	ts := NewTestSuite(
   238  		"Basic Tests",
   239  		"Test suite that runs basic, functional operator tests",
   240  	)
   241  	ts.AddTest(NewCheckSpecTest(conf), 1.5)
   242  	ts.AddTest(NewCheckStatusTest(conf), 1)
   243  	ts.AddTest(NewWritingIntoCRsHasEffectTest(conf), 1)
   244  
   245  	return ts
   246  }
   247  
   248  // NewOLMTestSuite returns a new TestSuite object containing CSV best practice checks
   249  func NewOLMTestSuite(conf OLMTestConfig) *TestSuite {
   250  	ts := NewTestSuite(
   251  		"OLM Tests",
   252  		"Test suite checks if an operator's CSV follows best practices",
   253  	)
   254  
   255  	ts.AddTest(NewCRDsHaveValidationTest(conf), 1.25)
   256  	ts.AddTest(NewCRDsHaveResourcesTest(conf), 1)
   257  	ts.AddTest(NewAnnotationsContainExamplesTest(conf), 1)
   258  	ts.AddTest(NewSpecDescriptorsTest(conf), 1)
   259  	ts.AddTest(NewStatusDescriptorsTest(conf), 1)
   260  
   261  	return ts
   262  }
   263  
   264  // Helper functions
   265  
   266  // ResultsPassFail will be used when multiple CRs are supported
   267  func ResultsPassFail(results []TestResult) (earned, max int) {
   268  	for _, result := range results {
   269  		if result.EarnedPoints != result.MaximumPoints {
   270  			return 0, 1
   271  		}
   272  	}
   273  	return 1, 1
   274  }
   275  
   276  // ResultsCumulative will be used when multiple CRs are supported
   277  func ResultsCumulative(results []TestResult) (earned, max int) {
   278  	for _, result := range results {
   279  		earned += result.EarnedPoints
   280  		max += result.MaximumPoints
   281  	}
   282  	return earned, max
   283  }
   284  
   285  // AddTest adds a new Test to a TestSuite along with a relative weight for the new Test
   286  func (ts *TestSuite) AddTest(t Test, weight float64) {
   287  	ts.Tests = append(ts.Tests, t)
   288  	ts.Weights[t.GetName()] = weight
   289  }
   290  
   291  // TotalScore calculates and returns the total score of all run Tests in a TestSuite
   292  func (ts *TestSuite) TotalScore() (score int) {
   293  	floatScore := 0.0
   294  	for _, result := range ts.TestResults {
   295  		if result.MaximumPoints != 0 {
   296  			floatScore += (float64(result.EarnedPoints) / float64(result.MaximumPoints)) * ts.Weights[result.Test.GetName()]
   297  		}
   298  	}
   299  	// scale to a percentage
   300  	addedWeights := 0.0
   301  	for _, weight := range ts.Weights {
   302  		addedWeights += weight
   303  	}
   304  	floatScore = floatScore * (100 / addedWeights)
   305  	return int(floatScore)
   306  }
   307  
   308  // Run runs all Tests in a TestSuite
   309  func (ts *TestSuite) Run(ctx context.Context) {
   310  	for _, test := range ts.Tests {
   311  		ts.TestResults = append(ts.TestResults, test.Run(ctx))
   312  	}
   313  }
   314  
   315  // NewTestSuite returns a new TestSuite with a given name and description
   316  func NewTestSuite(name, description string) *TestSuite {
   317  	return &TestSuite{
   318  		TestInfo: TestInfo{
   319  			Name:        name,
   320  			Description: description,
   321  		},
   322  		Weights: make(map[string]float64),
   323  	}
   324  }