-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expose more test framework functions #1205
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
fbeadf2
5df3a9e
bb52ce2
05535c9
f0fee70
06f4086
bd12703
ee6c9dc
b73cd05
c7b682b
3687a63
f0d12ce
2bb88b1
3ad807f
5a174b1
f09bb2b
a1faddf
735c154
17f3575
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,13 +23,15 @@ import ( | |
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// TestCtx contains the state of a test, which includes ID, namespace, and cleanup functions. | ||
type TestCtx struct { | ||
id string | ||
cleanupFns []cleanupFn | ||
namespace string | ||
t *testing.T | ||
} | ||
|
||
// CleanupOptions allows for configuration of resource cleanup functions. | ||
type CleanupOptions struct { | ||
TestContext *TestCtx | ||
Timeout time.Duration | ||
|
@@ -38,9 +40,11 @@ type CleanupOptions struct { | |
|
||
type cleanupFn func() error | ||
|
||
// NewTestCtx returns a new TestCtx object. | ||
func NewTestCtx(t *testing.T) *TestCtx { | ||
var prefix string | ||
if t != nil { | ||
// Use the name of the test as the prefix | ||
// TestCtx is used among others for namespace names where '/' is forbidden | ||
prefix = strings.TrimPrefix( | ||
strings.Replace( | ||
|
@@ -52,38 +56,37 @@ func NewTestCtx(t *testing.T) *TestCtx { | |
"test", | ||
) | ||
} else { | ||
prefix = "main" | ||
prefix = "operator-sdk" | ||
} | ||
|
||
id := prefix + "-" + strconv.FormatInt(time.Now().Unix(), 10) | ||
// add a creation timestamp to the ID | ||
id := prefix + "-" + strconv.FormatInt(time.Now().UnixNano(), 10) | ||
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. What is the reason for switching to 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. To prevent collisions. Collisions shouldn't have happened before due to the namespace being based on the name of the test, but now it could be more likely to happen if users run create multiple contexts in parallel since those would use the same name and potentially be created in the same second. Using nanoseconds should prevent collisions. |
||
return &TestCtx{ | ||
id: id, | ||
t: t, | ||
} | ||
} | ||
|
||
// GetID returns the ID of the TestCtx. | ||
func (ctx *TestCtx) GetID() string { | ||
return ctx.id | ||
} | ||
|
||
// Cleanup runs all the TestCtx's cleanup function in reverse order of their insertion. | ||
func (ctx *TestCtx) Cleanup() { | ||
failed := false | ||
for i := len(ctx.cleanupFns) - 1; i >= 0; i-- { | ||
err := ctx.cleanupFns[i]() | ||
if err != nil { | ||
failed = true | ||
if ctx.t != nil { | ||
ctx.t.Errorf("A cleanup function failed with error: (%v)\n", err) | ||
} else { | ||
log.Errorf("A cleanup function failed with error: (%v)", err) | ||
} | ||
} | ||
} | ||
if ctx.t == nil && failed { | ||
log.Fatal("A cleanup function failed") | ||
} | ||
} | ||
|
||
// AddCleanupFn adds a new cleanup function to the TestCtx. | ||
func (ctx *TestCtx) AddCleanupFn(fn cleanupFn) { | ||
ctx.cleanupFns = append(ctx.cleanupFns, fn) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,36 +40,45 @@ import ( | |
) | ||
|
||
var ( | ||
// Global framework struct | ||
// Global is a global framework struct that the test can use. | ||
Global *Framework | ||
// mutex for AddToFrameworkScheme | ||
mutex = sync.Mutex{} | ||
// whether to run tests in a single namespace | ||
singleNamespace *bool | ||
// singleNamespaceInternal determines whether tests are to be run in a single namespace or not. | ||
singleNamespaceInternal bool | ||
// decoder used by createFromYaml | ||
dynamicDecoder runtime.Decoder | ||
// restMapper for the dynamic client | ||
restMapper *restmapper.DeferredDiscoveryRESTMapper | ||
) | ||
|
||
// Framework contains all relevant variables needed for running tests with the operator-sdk. | ||
type Framework struct { | ||
Client *frameworkClient | ||
KubeConfig *rest.Config | ||
KubeClient kubernetes.Interface | ||
Scheme *runtime.Scheme | ||
NamespacedManPath *string | ||
NamespacedManPath string | ||
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. Was this done initially in case we could have a nil and do we not have that case anymore? 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'm not sure why it was a pointer. We don't have any nil checks anywhere, so this doesn't affect anything. Just makes more sense to have it as a normal string IMO 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. Makes sense, will this be a breaking change for the users, should we explicitly mention it in the changelog. 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 don't think it's something the users should have been accessing directly in the first place. It might make sense to unexport this field (and maybe a few others) 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. Hi @AlexNPavel, I think that namespace can be null, is not possible the user/dev use it as an lib/imported in their project? |
||
Namespace string | ||
LocalOperator bool | ||
} | ||
|
||
func setup(kubeconfigPath, namespacedManPath *string, localOperator bool) error { | ||
namespace := "" | ||
if *singleNamespace { | ||
namespace = os.Getenv(TestNamespaceEnv) | ||
// Setup initializes the Global.Framework variable and its fields. | ||
func Setup(kubeconfigPath, namespacedManPath, namespace string, singleNamespace, localOperator bool) error { | ||
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'm not sure if there are problematic implications, but can this function signature be refactored to something like: type FrameworkOptions struct {
KubeconfigPath string
NamespacedManPath string
Namespace string
SingleNamespace bool
LocalOperator bool
}
func NewFramework(opts FrameworkOptions) (*Framework, error) Then I realize this would be a breaking change, so that's worth some discussion. I think we should try to avoid having global mutable state as much as possible and favor returning and passing values around instead. 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. The biggest difficulty with not having the global 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. One way to avoid the global state in 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. Ok I think I'm understanding this a little bit better now having taken a look at https://github.com/operator-framework/operator-sdk/blob/master/test/test-framework/test/e2e/memcached_test.go. If I understand correctly, right now it's basically required for tests to call If getting rid of the exported |
||
if namespace != "" && !singleNamespace { | ||
return fmt.Errorf("oneNamespace must be set to true if namespace is set") | ||
} | ||
singleNamespaceInternal = singleNamespace | ||
var err error | ||
var kubeconfig *rest.Config | ||
if *kubeconfigPath == "incluster" { | ||
if kubeconfigPath == "incluster" { | ||
// when running with an InCluster config, we don't have permission to create new namespaces, so we must be in single namespace mode | ||
if singleNamespaceInternal != true { | ||
return fmt.Errorf("singleNamespace must be set to true for in cluster testing mode") | ||
} | ||
if len(namespace) == 0 { | ||
return fmt.Errorf("namespace must be set for in cluster testing mode") | ||
} | ||
// Work around https://github.com/kubernetes/kubernetes/issues/40973 | ||
if len(os.Getenv("KUBERNETES_SERVICE_HOST")) == 0 { | ||
addrs, err := net.LookupHost("kubernetes.default.svc") | ||
|
@@ -86,15 +95,10 @@ func setup(kubeconfigPath, namespacedManPath *string, localOperator bool) error | |
} | ||
} | ||
kubeconfig, err = rest.InClusterConfig() | ||
*singleNamespace = true | ||
namespace = os.Getenv(TestNamespaceEnv) | ||
if len(namespace) == 0 { | ||
return fmt.Errorf("test namespace env not set") | ||
} | ||
} else { | ||
var kcNamespace string | ||
kubeconfig, kcNamespace, err = k8sInternal.GetKubeconfigAndNamespace(*kubeconfigPath) | ||
if *singleNamespace && namespace == "" { | ||
kubeconfig, kcNamespace, err = k8sInternal.GetKubeconfigAndNamespace(kubeconfigPath) | ||
if singleNamespaceInternal && namespace == "" { | ||
namespace = kcNamespace | ||
} | ||
} | ||
|
@@ -147,34 +151,29 @@ type addToSchemeFunc func(*runtime.Scheme) error | |
// } | ||
// The List object is needed because the CRD has not always been fully registered | ||
// by the time this function is called. If the CRD takes more than 5 seconds to | ||
// become ready, this function throws an error | ||
// become ready, this function throws an error. | ||
func AddToFrameworkScheme(addToScheme addToSchemeFunc, obj runtime.Object) error { | ||
mutex.Lock() | ||
defer mutex.Unlock() | ||
err := addToScheme(Global.Scheme) | ||
if err != nil { | ||
return err | ||
return fmt.Errorf("failed to update global scheme: %v", err) | ||
} | ||
restMapper.Reset() | ||
dynClient, err := dynclient.New(Global.KubeConfig, dynclient.Options{Scheme: Global.Scheme, Mapper: restMapper}) | ||
if err != nil { | ||
return fmt.Errorf("failed to initialize new dynamic client: (%v)", err) | ||
} | ||
err = wait.PollImmediate(time.Second, time.Second*10, func() (done bool, err error) { | ||
if *singleNamespace { | ||
err = dynClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: Global.Namespace}, obj) | ||
if singleNamespaceInternal { | ||
err = Global.Client.List(goctx.TODO(), &dynclient.ListOptions{Namespace: Global.Namespace}, obj) | ||
} else { | ||
err = dynClient.List(goctx.TODO(), &dynclient.ListOptions{Namespace: "default"}, obj) | ||
err = Global.Client.List(goctx.TODO(), &dynclient.ListOptions{Namespace: "default"}, obj) | ||
} | ||
if err != nil { | ||
restMapper.Reset() | ||
return false, nil | ||
} | ||
Global.Client = &frameworkClient{Client: dynClient} | ||
return true, nil | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("failed to build the dynamic client: %v", err) | ||
return fmt.Errorf("failed to update the client restmapper: %v", err) | ||
} | ||
dynamicDecoder = serializer.NewCodecFactory(Global.Scheme).UniversalDeserializer() | ||
return nil | ||
|
Uh oh!
There was an error while loading. Please reload this page.