Skip to content

Operator SDK Test Framework #377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 49 commits into from
Aug 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e589d19
pkg/test: add initial e2e framework and `operator-sdk test` subcommand
AlexNPavel Jul 31, 2018
4b455f4
pkg/test/resource_creator.go: create InitializeClusterResources function
AlexNPavel Jul 31, 2018
9c32234
pkg/test,commands/.../test.go: make namespace settable and global
AlexNPavel Aug 1, 2018
f6524e6
pkg/test: add license header to files
AlexNPavel Aug 1, 2018
7f13e2b
pkg/util/e2eutil: change DeploymentReplicaCheck to WaitForDeployment
AlexNPavel Aug 1, 2018
e0d06ba
pkg/util/e2eutil: rename check_util to wait_util
AlexNPavel Aug 1, 2018
267bed5
commands/operator-sdk/cmd/test.go: add cli args for manifest paths
AlexNPavel Aug 1, 2018
476f951
pkg/test: handle new cli args
AlexNPavel Aug 1, 2018
6f85f67
pkg/test/resource_creator.go: add UpdateCR
AlexNPavel Aug 1, 2018
3c0c94c
pkg/test/resource_creator.go: fix bug in GetCRClient
AlexNPavel Aug 1, 2018
438ddeb
test/test-framework: add tests for the test framework
AlexNPavel Aug 1, 2018
6198721
pkg/test,commands/.../test.go: add project root env var
AlexNPavel Aug 1, 2018
0d83f5b
pkg/test/framework.go: use `!ok` instead of `ok != true`
AlexNPavel Aug 1, 2018
9b174ba
commands/.../test.go: don't specify flag defaults
AlexNPavel Aug 1, 2018
2ce24f7
pkg/test/resource_creator.go: delete just the necessary CR, not all
AlexNPavel Aug 2, 2018
a0960ea
pkg/test: add simulator file to simulate events in a test
AlexNPavel Aug 2, 2018
336cfbb
test/test-framework/memcached_test.go: update framework example
AlexNPavel Aug 2, 2018
2bfbfbd
pkg/test,commands/.../test.go: improve error messages
AlexNPavel Aug 2, 2018
0e435c3
pkg/util/e2eutil/wait_util.go: add godoc for WaitForDeployment
AlexNPavel Aug 2, 2018
228f327
Revert "test/test-framework/memcached_test.go: update framework example"
AlexNPavel Aug 2, 2018
1fcc59a
Revert "pkg/test: add simulator file to simulate events in a test"
AlexNPavel Aug 2, 2018
8f92207
pkg/test,commands/.../test.go: use flags instead of env vars
AlexNPavel Aug 3, 2018
dc60192
pkg/test: make namespace flags work correctly
AlexNPavel Aug 3, 2018
e4f7b93
test/test-framework/*.go: add license headers
AlexNPavel Aug 3, 2018
512d60c
test/test-framework/memcached_test.go: put license header in correct …
AlexNPavel Aug 3, 2018
95c4f2b
test/test-framework/memcached_test.go: fix typo
AlexNPavel Aug 3, 2018
7ab8388
pkg/test/resource_creator.go: add TODO regarding dynamic client
AlexNPavel Aug 3, 2018
8d27fea
commands/.../test.go: update usage message
AlexNPavel Aug 6, 2018
2f9f83f
commands/.../test.go,pkg/test/main_entry.go: make flag names constants
AlexNPavel Aug 6, 2018
3a4ba63
pkg/test/main_entry.go: check error of os.Chdir()
AlexNPavel Aug 6, 2018
c64041b
pkg/test/resource_creator.go: remove unused variable
AlexNPavel Aug 6, 2018
6736e32
pkg/test/main_entry.go: improve usage message for namespace flag
AlexNPavel Aug 6, 2018
1832022
pkg/test: use separate namespaces for each test
AlexNPavel Aug 6, 2018
64b431a
pkg/test,commands/.../test.go: remove namespace flags
AlexNPavel Aug 6, 2018
ff4a96f
commands/.../test.go: remove namespace variable
AlexNPavel Aug 6, 2018
2cbf475
pkg/test/resource_creator.go: return error if namespace already exists
AlexNPavel Aug 6, 2018
46300e7
pkg/test/framework.go: add dynamic client from controller-runtime
AlexNPavel Aug 6, 2018
145e6a2
pkg/test: add dynamic client using controller-runtime
AlexNPavel Aug 7, 2018
b3ecd38
Gopkg.lock: correct controller-runtime revision in lock
AlexNPavel Aug 7, 2018
491f339
Gpokg.*: change controller-runtime name
AlexNPavel Aug 7, 2018
1cc1a6c
pkg/test/resource_creator.go: support multispec files
AlexNPavel Aug 7, 2018
61e7943
pkg/test/resource_creator.go: add finalizer funcs in CreateFromYAML
AlexNPavel Aug 7, 2018
5b85f1e
pkg/test/resource_creator.go: add TODO
AlexNPavel Aug 7, 2018
2c8d7f1
commands/.../test.go: simplify testFunc with existing helper funcs
AlexNPavel Aug 7, 2018
0a930dd
pkg/test/context.go: remove unused function
AlexNPavel Aug 7, 2018
90b8170
pkg/test: separate NewTestCtx from the framework struct
AlexNPavel Aug 7, 2018
b74f40c
commands/.../test.go: add todo
AlexNPavel Aug 8, 2018
a792207
pkg/util/e2eutil: use apimachinery's wail util
AlexNPavel Aug 8, 2018
3066a44
pkg/util/e2eutil: make wait_util stateless
AlexNPavel Aug 8, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Gopkg.lock

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

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
[[constraint]]
name = "github.com/sergi/go-diff"
version = "1.0.0"

[[constraint]]
name = "sigs.k8s.io/controller-runtime"
revision = "60bb251ad86f9b313653618aad0c2c53f41a6625"
1 change: 1 addition & 0 deletions commands/operator-sdk/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func NewRootCmd() *cobra.Command {
cmd.AddCommand(NewGenerateCmd())
cmd.AddCommand(NewUpCmd())
cmd.AddCommand(NewCompletionCmd())
cmd.AddCommand(NewTestCmd())

return cmd
}
68 changes: 68 additions & 0 deletions commands/operator-sdk/cmd/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2018 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"os"

"github.com/operator-framework/operator-sdk/pkg/test"

"github.com/spf13/cobra"
)

var (
testLocation string
kubeconfig string
crdManifestPath string
opManifestPath string
rbacManifestPath string
verbose bool
)

// TODO: allow users to pass flags through to `go test`
func NewTestCmd() *cobra.Command {
testCmd := &cobra.Command{
Use: "test --test-location <path to tests directory> [flags]",
Short: "Run End-To-End tests",
Run: testFunc,
}
defaultKubeConfig := ""
homedir, ok := os.LookupEnv("HOME")
if ok {
defaultKubeConfig = homedir + "/.kube/config"
}
testCmd.Flags().StringVarP(&testLocation, "test-location", "t", "", "Location of test files (e.g. ./test/e2e/)")
testCmd.MarkFlagRequired("test-location")
testCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", defaultKubeConfig, "Kubeconfig path")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to that most of the flags are required as indicated in Framework.setup() down below. Maybe we should mark those as required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flags that are not marked as required have defaults that are used instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you are right.

testCmd.Flags().StringVarP(&crdManifestPath, "crd", "c", "deploy/crd.yaml", "Path to CRD manifest")
testCmd.Flags().StringVarP(&opManifestPath, "operator", "o", "deploy/operator.yaml", "Path to operator manifest")
testCmd.Flags().StringVarP(&rbacManifestPath, "rbac", "r", "deploy/rbac.yaml", "Path to RBAC manifest")
testCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose go test")

return testCmd
}

func testFunc(cmd *cobra.Command, args []string) {
testArgs := []string{"test", testLocation + "/..."}
if verbose {
testArgs = append(testArgs, "-v")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider allowing arbitrary passthrough of test flags, perhaps following the typical convention (e.g. go test itself) of "everything after [--|-args] is passed through".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like that would be quite difficult to do; I'm currently thinking of ways to implement that in a simple manner. The way go test does it is quite complicated and they don't use the flag package for go test, instead opting for separate code: https://github.com/golang/go/blob/master/src/cmd/go/internal/test/testflag.go#L21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the test framework is implemented as a library and doesn't impose a dependency on this wrapper command (which I THINK is true) then maybe it's not a big deal. For example, it looks like I should be able to do this if I wanted:

go test <go-test-args...> ./... -args <framework-args...>

Which would be fine for me in special/advanced cases. That would also be consistent with the way the build/run cycle of the SDK currently works for me (e.g. I compile with the standard go build toolchain during iteration, use the SDK to do iterative testing, and sometimes run my binary without the SDK in very special cases).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add that as a TODO for now. It would be nice to be able to do it with cobra, but if that's too complicated, we may just implement it as a string that the user passes in (e.g. operator-sdk test -t ./test/e2e/ --passthrough "-v -parallel=2".

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure... it may be useful to help me understand the design goals. For example, if a goal is to provide a simple wrapper which works for most cases but consistently provides an escape hatch for advanced uses (via the standard Go toolchain) then just keeping what you have here seems reasonable to me. If the wrapper is intended to be the only reasonable way to use the test framework, then things get a little trickier in deciding how much of the standard toolchain to re-expose, and how.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ironcladlou It's the former. The wrapper is meant to be a simple way for users to run their e2e tests in an SDK based project. I don't think there's any restriction for it to be the only way.
For more advanced use cases go test could be used.

}
testArgs = append(testArgs, "-"+test.KubeConfigFlag, kubeconfig)
testArgs = append(testArgs, "-"+test.CrdManPathFlag, crdManifestPath)
testArgs = append(testArgs, "-"+test.OpManPathFlag, opManifestPath)
testArgs = append(testArgs, "-"+test.RbacManPathFlag, rbacManifestPath)
testArgs = append(testArgs, "-"+test.ProjRootFlag, mustGetwd())
execCmd(os.Stdout, "go", testArgs...)
}
90 changes: 90 additions & 0 deletions pkg/test/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2018 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package test

import (
"log"
"strconv"
"strings"
"testing"
"time"

"k8s.io/client-go/rest"
)

type TestCtx struct {
ID string
CleanUpFns []finalizerFn
Namespace string
CRClient *rest.RESTClient
}

type finalizerFn func() error

func NewTestCtx(t *testing.T) TestCtx {
var prefix string
if t != nil {
// TestCtx is used among others for namespace names where '/' is forbidden
prefix = strings.TrimPrefix(
strings.Replace(
strings.ToLower(t.Name()),
"/",
"-",
-1,
),
"test",
)
} else {
prefix = "main"
}

id := prefix + "-" + strconv.FormatInt(time.Now().Unix(), 10)
return TestCtx{
ID: id,
}
}

func (ctx *TestCtx) GetID() string {
return ctx.ID
}

func (ctx *TestCtx) Cleanup(t *testing.T) {
for i := len(ctx.CleanUpFns) - 1; i >= 0; i-- {
err := ctx.CleanUpFns[i]()
if err != nil {
t.Errorf("a cleanup function failed with error: %v\n", err)
}
}
}

// CleanupNoT is a modified version of Cleanup; does not use t for logging, instead uses log
// intended for use by MainEntry, which does not have a testing.T
func (ctx *TestCtx) CleanupNoT() {
failed := false
for i := len(ctx.CleanUpFns) - 1; i >= 0; i-- {
err := ctx.CleanUpFns[i]()
if err != nil {
failed = true
log.Printf("a cleanup function failed with error: %v\n", err)
}
}
if failed {
log.Fatal("a cleanup function failed")
}
}

func (ctx *TestCtx) AddFinalizerFn(fn finalizerFn) {
ctx.CleanUpFns = append(ctx.CleanUpFns, fn)
}
76 changes: 76 additions & 0 deletions pkg/test/framework.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2018 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package test

import (
"fmt"

extensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
cgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
dynclient "sigs.k8s.io/controller-runtime/pkg/client"
)

var Global *Framework

type Framework struct {
KubeConfig *rest.Config
KubeClient kubernetes.Interface
ExtensionsClient *extensions.Clientset
DynamicClient dynclient.Client
DynamicDecoder runtime.Decoder
CrdManPath *string
OpManPath *string
RbacManPath *string
}

func setup(kubeconfigPath, crdManPath, opManPath, rbacManPath *string) error {
kubeconfig, err := clientcmd.BuildConfigFromFlags("", *kubeconfigPath)
if err != nil {
return fmt.Errorf("failed to build the kubeconfig: %v", err)
}
kubeclient, err := kubernetes.NewForConfig(kubeconfig)
if err != nil {
return fmt.Errorf("failed to build the kubeclient: %v", err)
}
extensionsClient, err := extensions.NewForConfig(kubeconfig)
if err != nil {
return fmt.Errorf("failed to build the extensionsClient: %v", err)
}
scheme := runtime.NewScheme()
cgoscheme.AddToScheme(scheme)
extscheme.AddToScheme(scheme)
dynClient, err := dynclient.New(kubeconfig, dynclient.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to build the dynamic client: %v", err)
}
dynDec := serializer.NewCodecFactory(scheme).UniversalDeserializer()
Global = &Framework{
KubeConfig: kubeconfig,
KubeClient: kubeclient,
ExtensionsClient: extensionsClient,
DynamicClient: dynClient,
DynamicDecoder: dynDec,
CrdManPath: crdManPath,
OpManPath: opManPath,
RbacManPath: rbacManPath,
}
return nil
}
66 changes: 66 additions & 0 deletions pkg/test/main_entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2018 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package test

import (
"flag"
"io/ioutil"
"log"
"os"
"testing"
)

const (
ProjRootFlag = "root"
KubeConfigFlag = "kubeconfig"
CrdManPathFlag = "crd"
OpManPathFlag = "op"
RbacManPathFlag = "rbac"
)

func MainEntry(m *testing.M) {
projRoot := flag.String("root", "", "path to project root")
kubeconfigPath := flag.String("kubeconfig", "", "path to kubeconfig")
crdManPath := flag.String("crd", "", "path to crd manifest")
opManPath := flag.String("op", "", "path to operator manifest")
rbacManPath := flag.String("rbac", "", "path to rbac manifest")
flag.Parse()
// go test always runs from the test directory; change to project root
err := os.Chdir(*projRoot)
if err != nil {
log.Fatalf("failed to change directory to project root: %v", err)
}
if err := setup(kubeconfigPath, crdManPath, opManPath, rbacManPath); err != nil {
log.Fatalf("failed to set up framework: %v", err)
}
// setup context to use when setting up crd
ctx := NewTestCtx(nil)
// os.Exit stops the program before the deferred functions run
// to fix this, we put the exit in the defer as well
defer func() {
exitCode := m.Run()
ctx.CleanupNoT()
os.Exit(exitCode)
}()
// create crd
crdYAML, err := ioutil.ReadFile(*Global.CrdManPath)
if err != nil {
log.Fatalf("failed to read crd file: %v", err)
}
err = ctx.CreateFromYAML(crdYAML)
if err != nil {
log.Fatalf("failed to create crd resource: %v", err)
}
}
Loading