Skip to content

Commit

Permalink
Implementation of framework test harness (KEP-0008)
Browse files Browse the repository at this point in the history
Rename Test to Case, rename Stage to Step.

Fix race if a resource is updated externally while the test harness is updating the resource.

Migrate InstallManifests into utils.
  • Loading branch information
jbarrick-mesosphere committed Jun 10, 2019
1 parent 61aa3db commit 1e69cf6
Show file tree
Hide file tree
Showing 32 changed files with 2,315 additions and 56 deletions.
17 changes: 17 additions & 0 deletions cmd/test/test_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// +build build_harness

package tools

import (
"testing"

"github.com/kudobuilder/kudo/pkg/test"
)

func init() {
test.RegisterFlags()
}

func TestKudoFrameworks(t *testing.T) {
test.HarnessFromFlags(t).Run()
}
56 changes: 56 additions & 0 deletions config/samples/test-framework/test-framework.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
apiVersion: kudo.k8s.io/v1alpha1
kind: Framework
metadata:
name: test-framework
namespace: default
---
apiVersion: kudo.k8s.io/v1alpha1
kind: FrameworkVersion
metadata:
name: test-framework-1.0
namespace: default
spec:
framework:
name: test-framework
kind: Framework
version: "1.0"
parameters:
- name: REPLICAS
description: "Number of nginx replicas"
default: "3"
displayName: "Replica count"
templates:
deploy.yaml: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: {{REPLICAS}}
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
tasks:
deploy:
resources:
- deploy.yaml
plans:
deploy:
strategy: serial
phases:
- name: deploy
strategy: parallel
steps:
- name: deploy
tasks:
- deploy
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2
github.com/appscode/jsonpatch v0.0.0-20190108182946-7c0e3b262f30 // indirect
github.com/dustinkirkland/golang-petname v0.0.0-20170921220637-d3c2ba80e75e
github.com/emicklei/go-restful v2.9.0+incompatible // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v0.1.0 // indirect
Expand Down Expand Up @@ -43,6 +44,7 @@ require (
github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v0.9.2 // indirect
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
github.com/prometheus/common v0.2.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustinkirkland/golang-petname v0.0.0-20170921220637-d3c2ba80e75e h1:bRcq7ruHMqCVB/ugLbBylx+LrccNACFDEaqAD/aZ80Q=
github.com/dustinkirkland/golang-petname v0.0.0-20170921220637-d3c2ba80e75e/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
github.com/emicklei/go-restful v2.9.0+incompatible h1:YKhDcF/NL19iSAQcyCATL1MkFXCzxfdaTiuJKr18Ank=
github.com/emicklei/go-restful v2.9.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/evanphx/json-patch v4.0.0+incompatible h1:xregGRMLBeuRcwiOTHRCsPPuzCQlqhxUPbqdw+zNkLc=
Expand Down Expand Up @@ -162,6 +164,7 @@ github.com/rogpeppe/go-internal v1.2.2 h1:J7U/N7eRtzjhs26d6GqMh2HBuXP8/Z64Densii
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
Expand Down
108 changes: 52 additions & 56 deletions keps/0008-framework-testing.md

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions pkg/apis/kudo/v1alpha1/test_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TestStep settings to apply to a test step.
type TestStep struct {
// The type meta object, should always be a GVK of kudo.k8s.io/v1alpha1/TestStep.
metav1.TypeMeta `json:",inline"`
// Override the default metadata. Set labels or override the test step name.
metav1.ObjectMeta `json:"metadata,omitempty"`

Index int `json:"index,omitempty"`
// Objects to delete at the beginning of the test step.
Delete []corev1.ObjectReference `json:"delete,omitempty"`

// Indicates that this is a unit test - safe to run without a real Kubernetes cluster.
UnitTest bool `json:"unitTest"`

// Allowed environment labels
// Disallowed environment labels
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TestAssert represents the settings needed to verify the result of a test step.
type TestAssert struct {
// The type meta object, should always be a GVK of kudo.k8s.io/v1alpha1/TestAssert.
metav1.TypeMeta `json:",inline"`
// Override the default timeout of 300 seconds (in seconds).
Timeout int `json:"timeout"`
}
57 changes: 57 additions & 0 deletions pkg/apis/kudo/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

171 changes: 171 additions & 0 deletions pkg/test/case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package test

import (
"context"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"sort"
"strconv"
"testing"

petname "github.com/dustinkirkland/golang-petname"
testutils "github.com/kudobuilder/kudo/pkg/test/utils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var testStepRegex = regexp.MustCompile(`^(\d+)-([^.]+)(.yaml)?$`)

// Case contains all of the test steps and the Kubernetes client and other global configuration
// for a test.
type Case struct {
Steps []*Step
Name string
Dir string

Client client.Client
DiscoveryClient discovery.DiscoveryInterface
Logger testutils.Logger
}

// DeleteNamespace deletes a namespace in Kubernetes after we are done using it.
func (t *Case) DeleteNamespace(namespace string) error {
t.Logger.Log("Deleting namespace:", namespace)
return t.Client.Delete(context.TODO(), &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "Namespace",
},
})
}

// CreateNamespace creates a namespace in Kubernetes to use for a test.
func (t *Case) CreateNamespace(namespace string) error {
t.Logger.Log("Creating namespace:", namespace)
return t.Client.Create(context.TODO(), &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "Namespace",
},
})
}

// TestCaseFactory creates a new Go test that runs a set of test steps.
func (t *Case) TestCaseFactory() func(*testing.T) {
return func(test *testing.T) {
t.Logger = testutils.NewTestLogger(test, t.Name)

test.Parallel()

ns := fmt.Sprintf("kudo-test-%s", petname.Generate(2, "-"))

if err := t.CreateNamespace(ns); err != nil {
test.Fatal(err)
}

defer t.DeleteNamespace(ns)

for _, testStep := range t.Steps {
testStep.Client = t.Client
testStep.DiscoveryClient = t.DiscoveryClient
testStep.Logger = t.Logger.WithPrefix(testStep.String())

defer testStep.Clean(ns)

if errs := testStep.Run(ns); len(errs) > 0 {
for _, err := range errs {
test.Error(err)
}

test.Error(fmt.Errorf("failed in step %s", testStep.String()))
break
}
}
}
}

// CollectTestStepFiles collects a map of test steps and their associated files
// from a directory.
func (t *Case) CollectTestStepFiles() (map[int64][]string, error) {
testStepFiles := map[int64][]string{}

files, err := ioutil.ReadDir(t.Dir)
if err != nil {
return nil, err
}

for _, file := range files {
matches := testStepRegex.FindStringSubmatch(file.Name())

index, err := strconv.ParseInt(matches[1], 10, 32)
if err != nil {
return nil, err
}

if testStepFiles[index] == nil {
testStepFiles[index] = []string{}
}

testStepPath := filepath.Join(t.Dir, file.Name())

if file.IsDir() {
testStepDir, err := ioutil.ReadDir(testStepPath)
if err != nil {
return nil, err
}

for _, testStepFile := range testStepDir {
testStepFiles[index] = append(testStepFiles[index], filepath.Join(
testStepPath, testStepFile.Name(),
))
}
} else {
testStepFiles[index] = append(testStepFiles[index], testStepPath)
}
}

return testStepFiles, nil
}

// LoadTestSteps loads all of the test steps for a test case.
func (t *Case) LoadTestSteps() error {
testStepFiles, err := t.CollectTestStepFiles()
if err != nil {
return err
}

testSteps := []*Step{}

for index, files := range testStepFiles {
testStep := &Step{
Index: int(index),
Asserts: []runtime.Object{},
Apply: []runtime.Object{},
Errors: []runtime.Object{},
}

for _, file := range files {
if err := testStep.LoadYAML(file); err != nil {
return err
}
}

testSteps = append(testSteps, testStep)
}

sort.Slice(testSteps, func(i, j int) bool {
return testSteps[i].Index < testSteps[j].Index
})

t.Steps = testSteps
return nil
}
Loading

0 comments on commit 1e69cf6

Please sign in to comment.