When using the Kubernetes Fake Client to write unit tests, I noticed that it fails to create two identical objects which have their ObjectMeta.GenerateName
field set to some string. A real cluster accepts this specification and generates a unique name for each object.
Running the following test code:
package main
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestFake(t *testing.T) {
ctx := context.Background()
client := fake.NewSimpleClientset()
_, err := client.CoreV1().Secrets("default").Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "generated",
},
StringData: map[string]string{"foo": "bar"},
}, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = client.CoreV1().Secrets("default").Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "generated",
},
StringData: map[string]string{"foo": "bar"},
}, metav1.CreateOptions{})
assert.NoError(t, err)
}
fails with
--- FAIL: TestFake (0.00s)
/Users/mihaitodor/Projects/kubernetes/main_test.go:44:
Error Trace: main_test.go:44
Error: Received unexpected error:
secrets "" already exists
Test: TestFake
FAIL
FAIL kubernetes 0.401s
FAIL
According to this GitHub issue comment:
the fake clientset doesn't attempt to duplicate server-side behavior like validation, name generation, uid assignment, etc. if you want to test things like that, you can add reactors to mock that behavior.
To add the required reactor, we can insert the following code before creating the corev1.Secret
objects:
client.PrependReactor(
"create", "*",
func(action k8sTesting.Action) (handled bool, ret runtime.Object, err error) {
ret = action.(k8sTesting.CreateAction).GetObject()
meta, ok := ret.(metav1.Object)
if !ok {
return
}
if meta.GetName() == "" && meta.GetGenerateName() != "" {
meta.SetName(names.SimpleNameGenerator.GenerateName(meta.GetGenerateName()))
}
return
},
)
There are a few gotchas in there:
Clientset
contains an embedded Fake
structure which has the PrependReactor
method we need to call for this use case (there are a few others). This code here is invoked when creating such objects.PrependReactor
method has 3 parameters: verb
, resource
and reaction
. For verb
, resource
, I couldn't find any named constants, so, in this case, "create" and "secrets" (strange that it's not "secret") seem to be the correct values for them if we want to be super-specific, but setting resource
to "*" should be acceptable in this case.reaction
parameter is of type ReactionFunc, which takes an Action
as a parameter and returns handled
, ret
and err
. After some digging, I noticed that the action
parameter will be cast to CreateAction
, which has the GetObject()
method that returns a runtime.Object
instance, which can be cast to metav1.Object
. This interface allows us to get and set the various metadata fields of the underlying object. After setting the object Name
field as needed, we have to return handled = false
, ret = mutatedObject
and err = nil
to instruct the calling code to execute the remaining reactors.apiserver
code, I noticed that the ObjectMeta.Name
field is generated from the ObjectMeta.GenerateName
field using the names.SimpleNameGenerator.GenerateName
utility.