-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Operator SDK Test Framework #377
Conversation
commands/operator-sdk/cmd/test.go
Outdated
dc.Stderr = os.Stderr | ||
wd, err := os.Getwd() | ||
if err != nil { | ||
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("Could not determine working directory")) |
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.
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("Could not determine working directory"))
->
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("could not determine working directory: %v", err))
I noticed that we have mixed capitalization of the starting word of a error message. I'd suggest to keep it lower case. And we should have another pr to loop through all the code to make the error message to start with lower case.
pkg/test/framework.go
Outdated
} | ||
|
||
func setup() error { | ||
kubeconfigEnv, ok := os.LookupEnv("TEST_KUBECONFIG") |
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.
I think we should make "TEST_KUBECONFIG"
, "TEST_NAMESPACE"
, and etc into constants and save those under pkg/test/constant.go
.
pkg/test/framework.go
Outdated
} | ||
kubeconfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigEnv) | ||
if err != nil { | ||
return 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.
suggest to be more explicit what's the error is: e.g
return fmt.Errof("failed to build the kubeconfig: %v", err)
pkg/test/framework.go
Outdated
} | ||
kubeclient, err := kubernetes.NewForConfig(kubeconfig) | ||
if err != nil { | ||
return 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.
return fmt.Errof("failed to build the kubeclient: %v", err)
pkg/test/framework.go
Outdated
} | ||
extensionsClient, err := extensions.NewForConfig(kubeconfig) | ||
if err != nil { | ||
return 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.
return fmt.Errof("failed to build the extensionsClient: %v", err)
} | ||
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") |
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.
pkg/test/main_entry.go
Outdated
|
||
func MainEntry(m *testing.M) { | ||
// go test always runs from the test directory; change to project root | ||
root, ok := os.LookupEnv("TEST_PROJROOT") |
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.
use constant.go
pkg/test/main_entry.go
Outdated
// go test always runs from the test directory; change to project root | ||
root, ok := os.LookupEnv("TEST_PROJROOT") | ||
if !ok { | ||
log.Fatalf("Failed to get project root dir environment variable") |
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.
log.Fatalf
-> log.Fatal
? and Failed
-> failed
?
pkg/test/main_entry.go
Outdated
} | ||
os.Chdir(root) | ||
if err := setup(); err != nil { | ||
log.Fatalf("Failed to set up framework: %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.
Failed
-> failed
?
pkg/util/e2eutil/wait_util.go
Outdated
|
||
var retryInterval = time.Second * 5 | ||
|
||
func WaitForDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas, retries int) error { |
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.
doc string?
6086ce9
to
ce9e21b
Compare
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.
commands/operator-sdk/cmd/test.go
Outdated
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("could not determine working directory: %v", err)) | ||
} | ||
dc.Env = append(os.Environ(), | ||
fmt.Sprintf("%v=%v", "TEST_KUBECONFIG", kubeconfig), |
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.
If you make these values constants I think you may want to use them here
groupVersion := strings.Split(yamlMap["apiVersion"].(string), "/") | ||
crGV := schema.GroupVersion{Group: groupVersion[0], Version: groupVersion[1]} | ||
crConfig.GroupVersion = &crGV | ||
crConfig.APIPath = "/apis" |
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.
Do we need to handle the case when the Group is ""
. I believe that means the APIPath should be /api
.
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.
Custom resources should not be part of the core group (or group ""), so we shouldn't need to worry about that case. The GetCRClient function is only for custom resources.
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.
At the moment, that function gets called as a catch-all if it's something we did not specify in the CreateFromYAML function, so that could occur. I plan to support all resources in that function soon though.
commands/operator-sdk/cmd/test.go
Outdated
|
||
func NewTestCmd() *cobra.Command { | ||
testCmd := &cobra.Command{ | ||
Use: "test --kubeconfig <path to kubeconfig> --image <name of operator image>", |
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.
We don't need the operator --image flag anymore right?
Since we just use the input manifest to deploy the operator.
pkg/test/main_entry.go
Outdated
) | ||
|
||
func MainEntry(m *testing.M) { | ||
projRoot := flag.String("root", "", "path to project root") |
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.
We should probably declare the flag names as constants here and then import them in commands/operator-sdk/cmd/test.go
so we don't have to change them in two places.
pkg/test/main_entry.go
Outdated
rbacManPath := flag.String("rbac", "", "path to rbac manifest") | ||
flag.Parse() | ||
// go test always runs from the test directory; change to project root | ||
os.Chdir(*projRoot) |
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.
Check the returned error.
pkg/test/resource_creator.go
Outdated
) | ||
|
||
var ( | ||
filemode = int(0664) |
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.
This isn't being used anywhere.
pkg/test/main_entry.go
Outdated
func MainEntry(m *testing.M) { | ||
projRoot := flag.String("root", "", "path to project root") | ||
kubeconfigPath := flag.String("kubeconfig", "", "path to kubeconfig") | ||
namespace := flag.String("namespace", "", "namespace to tests to run in") |
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.
namespace to tests to run in
==> namespace where the tests will run
I will be moving the namespace and other global resources into the framework initialization |
We're moving all tests into their own namespaces again |
GetCRClient was not storing the CRClient it creates in ctx
Cobra prints the defaults in the help message for you
Was using github url instead of sig.k8s.io url
We are now using the controller-runtime's dynamic client to handle creation and deletion of all resources except for custom resources |
commands/operator-sdk/cmd/test.go
Outdated
} | ||
|
||
func testFunc(cmd *cobra.Command, args []string) { | ||
wd, err := os.Getwd() |
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.
we probably can use mustGetwd
from https://github.com/operator-framework/operator-sdk/blob/master/commands/operator-sdk/cmd/new.go#L148 here.
commands/operator-sdk/cmd/test.go
Outdated
if err != nil { | ||
cmdError.ExitWithError(cmdError.ExitError, fmt.Errorf("could not determine working directory: %v", err)) | ||
} | ||
testArgs := []string{"test", testLocation + "/..."} |
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.
we probably can use execCmd
from https://github.com/operator-framework/operator-sdk/blob/master/commands/operator-sdk/cmd/new.go#L156 to help us executing external command.
pkg/test/context.go
Outdated
// GetObjID returns an ascending ID based on the length of cleanUpFns. It is | ||
// based on the premise that every new object also appends a new finalizerFn on | ||
// cleanUpFns. | ||
func (ctx *TestCtx) GetObjID() string { |
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.
is this being used anywhere? if not, we probably don't need it.
pkg/test/context.go
Outdated
|
||
type finalizerFn func() error | ||
|
||
func (f *Framework) NewTestCtx(t *testing.T) TestCtx { |
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.
is there a reason to tie NewTestCtx
to Framework
object? it seems me that NewTestCtx
can just be a static factory function.
pkg/test/context.go
Outdated
return ctx.ID + "-" + strconv.Itoa(len(ctx.CleanUpFns)) | ||
} | ||
|
||
func (ctx *TestCtx) Cleanup(t *testing.T) { |
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.
Can you have make Cleanup
to take in f func(format string, v ...interface{})
?
then you can pass t.Errorf
or log.Fatalf
to it. So that we don't have to have a CleanupNoT
function.
e.g:
func (ctx *TestCtx) Cleanup(f func(format string, v ...interface{})) {
for i := len(ctx.CleanUpFns) - 1; i >= 0; i-- {
err := ctx.CleanUpFns[i]()
if err != nil {
f("a cleanup function failed with error: %v\n", err)
}
}
}
func TestBla(t *testing.T) {
ctx := Global.NewTestCtx(nil)
defer ctx.Cleanup(log.Fatalf)
ctx = Global.NewTestCtx(t)
defer ctx.Cleanup(t.Errof)
}
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.
@AlexNPavel any thought to the above approach?
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.
I like it. What I'm not sure about is how to handle failure. We don't want to use log.Fatalf
as our error reporting function, because that could cause the function to end before all clean up functions have finished. The way that CleanupNoT
handles this is with a failed
boolean that only runs log.Fatalf
after all functions have been executed. We could use that design again, but there's still the issue about what happens if a user give us something like log.Fatalf
. We could document the behavior, but it would still be easy for people to make this mistake.
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.
I see. Let's just keep it is at the moment. we can revisit this later.
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 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".
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.
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
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.
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).
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.
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"
.
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.
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 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.
@@ -0,0 +1,54 @@ | |||
// Copyright 2018 The Operator-SDK Authors |
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.
Not sure why the apimachinery utils for waiting/polling would be insufficient here, and using them would reduce the scope of changes/maintenance.
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.
@ironcladlou could you link those utils. I wasn't aware of those.
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.
Sure- check out the wait package.
pkg/util/e2eutil/wait_util.go
Outdated
// If the deployment does not have the required number of replicas after 5 * retries seconds, the function returns an error | ||
// This can be used in multiple ways, like verifying that a required resource is ready before trying to use it, or to test | ||
// failure handling, like simulated in SimulatePodFail. | ||
func WaitForDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas, retries int) error { |
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.
Let's just pass in the retry interval and total timeout and keep it the same as wait.Poll() expects it. Also that way we don't need to set the global vars for retryInterval and retries on lines 28-29
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.
In addition to taking the very good advice to make this stateless, I'd also recommend just keeping it a private function of the example test file until enough copypasta is observed in the wild to justify building up a util package (which tend to get gorpy very fast).
LGTM |
Should have squashed this |
@ironcladlou We used the squash-and-merge button on github to squash it here instead of doing it locally. So it'll just be one commit. |
@hasbro17 oh, nice! Didn't know about that- thanks! |
The PR is for the initial version of the Operator SDK test framework which can be used to test operators created with the SDK.
This library is based on the SDK's internal e2e testing framework and has several modifications to make it more applicable for use to test operators (whereas the SDK's tests test SDK functionality, not operators).