Please, open an issue for us. That will really help us improving the Operator and it would benefit other users such as yourself. There are templates ready for bug reporting and feature request to make things easier for you.
We're happy to answer! Either open an issue or send an email to our mailing list: [email protected].
Before sending a PR, consider opening an issue first. This way we can discuss your approach and the motivations behind it while also taking into account other development efforts.
Regarding your local development environment:
- We use golint-ci to check the code. Consider integrating it in your favorite IDE to avoid failing in the CI
- Always run
make test
before sending a PR to make sure the license headers and the manifests are updated (and of course the unit tests are passing) - Consider adding a new end-to-end test case covering your scenario and make sure to run
make test-e2e
before sending the PR - Make sure to always keep your version of
Go
and theoperator-sdk
on par with the project. The current version information can be found at the go.mod file
To run all unit tests, build the image, run the E2E tests with that image and push, all in one go, you may run make pr-prep
. If any tests fail or if the build fails, the process will be terminated so that you make the necessary adjustments. If they are all successful, you'll be prompted to push your commited changes.
$ make pr-prep
# (output omitted)
All tests were successful!
Do you wish to push? (y/n) y
Insert the remote name: [origin]
Insert branch: [pr-prep]
Pushing to origin/pr-prep
# (output omitted)
If you don't inform remote name and branch, it will use "origin" as the remote and your current branch (the defaults, which appear between "[]"). Double check if the information is correct.
If you don't want to go over the interactive prompt every time, you can push with the defaults using the PUSH_WITH_DEFAULTS
environment variable:
$ PUSH_WITH_DEFAULTS=TRUE make pr-prep
# (output omitted)
All tests were successful!
Pushing to origin/pr-prep
# (output omitted)
If you added a new functionality and are willing to add some end-to-end (E2E) testing of your own, please add a test case to test/e2e/nexus_test.go
.
The test case structure allows you to name your test appropriately (try naming it in a way it's clear what it's testing), provide a Nexus CR that the Operator will use to generate the other resources, provide additional checks your feature may require and provide a custom cleanup function if necessary.
Then each test case is submitted to a series of checks which should make sure everything on the cluster is as it should, based on the Nexus CR that has been defined.
Let's take our smoke test as an example to go over the test cases structure:
testCases := []struct {
name string (1)
input *v1alpha1.Nexus (2)
cleanup func() error (3)
additionalChecks []func(nexus *v1alpha1.Nexus) error (4)
}{
{
name: "Smoke test: no persistence, nodeport exposure", (1)
input: &v1alpha1.Nexus{ (2)
ObjectMeta: metav1.ObjectMeta{
Name: nexusName,
Namespace: namespace,
},
Spec: defaultNexusSpec, (5)
},
cleanup: tester.defaultCleanup, (3)
additionalChecks: nil, (4)
},
(1): the test case's name. In this scenario we're testing a deployment with all default values, no persistence and exposed via Node Port.
(2): the Nexus CR which the Operator will use to orchestrate and maintain your Nexus3 deployment
(3): a cleanup function which should be ran after the test has been completed
(4): additional checks your test case may need
(5): the base, default Nexus CR specification which should be used for testing. Modify this to test your own features
Important: although the operator will set the defaults on the Nexus CR you provide it with, the tests will use your original CR for comparison, so be sure to make a completely valid Nexus CR for your test case as it will not be modified to insert default values.
If your test requires modifications to the default Nexus CR, you can do so directly and concisely when defining the test case by making use of anonymous functions.
For example:
{
name: "Networking: ingress with no TLS",
input: &v1alpha1.Nexus{
ObjectMeta: metav1.ObjectMeta{
Name: nexusName,
Namespace: namespace,
},
Spec: func() v1alpha1.NexusSpec {
spec := *defaultNexusSpec.DeepCopy()
spec.Networking = v1alpha1.NexusNetworking{Expose: true, ExposeAs: v1alpha1.IngressExposeType, Host: "test-example.com"}
return spec
}(),
},
cleanup: tester.defaultCleanup,
additionalChecks: nil,
},
When defining the Nexus's specification in this case we're actually calling an anonymous function that acquires the default spec, modifies the required fields and then returns that spec, thus making the necessary changes for the test.
Our test cases make use of functions first-class citizenship in Go by declaring the cleanup function as a field from the test case. This way it's possible to specify our own custom cleanup function for a test.
In previous examples, tester.defaultCleanup
was used, which simply deletes all Nexus CRs in the namespace, but you may want to do some additional computation when cleaning up, such as counting to 5 (intentionally useless to promote simplicity in this example):
{
name: "Test Example: this counts to 5 during cleanup and uses the default cleanup once done",
input: &v1alpha1.Nexus{
ObjectMeta: metav1.ObjectMeta{
Name: nexusName,
Namespace: namespace,
},
Spec: defaultNexusSpec,
},
cleanup: func() error {
for i := 0; i < 5; i++ {
tester.t.Logf("Count: %d", i)
}
return tester.defaultCleanup()
},
additionalChecks: nil,
},
It's possible, of course, to not use the default cleanup function at all, but be sure to actually delete the resources you created if they conflict with other test cases (the framework itself will delete the whole namespace once the tests are done):
{
name: "Test Example: this only counts to 5 during cleanup and does not delete anything",
input: &v1alpha1.Nexus{
ObjectMeta: metav1.ObjectMeta{
Name: nexusName,
Namespace: namespace,
},
Spec: defaultNexusSpec,
},
cleanup: func() error {
for i := 0; i < 5; i++ {
tester.t.Logf("Count: %d", i)
}
return nil
},
additionalChecks: nil,
},
If your testing needs to check something that isn't already checked by default you may add functions to perform these checks as the function that is responsible for running the default checks will receive them as variadic arguments.
In another useless yet simple example, let's also make sure that 5 is greater than 4 when performing our checks:
{
name: "Test Example: this will also check if 5 > 4",
input: &v1alpha1.Nexus{
ObjectMeta: metav1.ObjectMeta{
Name: nexusName,
Namespace: namespace,
},
Spec: defaultNexusSpec,
},
cleanup: tester.defaultCleanup,
additionalChecks: []func(nexus *v1alpha1.Nexus)error{
func(nexus *v1alpha1.Nexus) error {
assert.Greater(tester.t, 5, 4)
return nil
},
},
},