Skip to content

Commit

Permalink
Operator SDK Test Framework (#377)
Browse files Browse the repository at this point in the history
Adds initial test framework and sample memcached tests that use the framework.
  • Loading branch information
AlexNPavel committed Aug 9, 2018
1 parent fa4ea18 commit b19e56f
Show file tree
Hide file tree
Showing 14 changed files with 802 additions and 1 deletion.
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")
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")
}
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

0 comments on commit b19e56f

Please sign in to comment.