-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Changes from all commits
e589d19
4b455f4
9c32234
f6524e6
7f13e2b
e0d06ba
267bed5
476f951
6f85f67
3c0c94c
438ddeb
6198721
0d83f5b
9b174ba
2ce24f7
a0960ea
336cfbb
2bfbfbd
0e435c3
228f327
1fcc59a
8f92207
dc60192
e4f7b93
512d60c
95c4f2b
7ab8388
8d27fea
2f9f83f
3a4ba63
c64041b
6736e32
1832022
64b431a
ff4a96f
2cbf475
46300e7
145e6a2
b3ecd38
491f339
1cc1a6c
61e7943
5b85f1e
2c8d7f1
0a930dd
90b8170
b74f40c
a792207
3066a44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
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...) | ||
} |
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) | ||
} |
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 | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, you are right.