英文:
Kubernetes Fake Client doesn't handle GenerateName in ObjectMeta
问题
在使用Kubernetes的Fake Client编写单元测试时,我注意到它无法创建两个具有相同ObjectMeta.GenerateName字段设置为某个字符串的相同对象。真实的集群接受此规范并为每个对象生成唯一名称。
运行以下测试代码:
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)
}
会失败并显示以下错误信息:
--- 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
英文:
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
答案1
得分: 2
根据这个GitHub问题的评论:
假的客户端集不会尝试复制服务器端的行为,如验证、名称生成、UID分配等。如果你想测试这样的行为,可以添加反应器来模拟这种行为。
要添加所需的反应器,我们可以在创建corev1.Secret对象之前插入以下代码:
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
},
)
其中有一些要注意的地方:
-
Clientset包含一个嵌入的Fake结构,其中包含我们需要调用的PrependReactor方法(还有其他几个)。当创建这样的对象时,会调用这里的代码。 -
PrependReactor方法有3个参数:verb、resource和reaction。对于verb和resource,我找不到任何命名常量,所以在这种情况下,"create"和"secrets"(奇怪的是不是"secret")似乎是正确的值,如果我们想要非常具体的话,但在这种情况下,将resource设置为"*"应该是可以接受的。 -
reaction参数的类型是ReactionFunc,它以Action作为参数,并返回handled、ret和err。经过一些挖掘,我注意到action参数将被转换为CreateAction,它具有返回runtime.Object实例的GetObject()方法,可以将其转换为metav1.Object。这个接口允许我们获取和设置底层对象的各种元数据字段。在设置完对象的Name字段后,我们必须返回handled = false、ret = mutatedObject和err = nil,以指示调用代码执行剩余的反应器。 -
在浏览
apiserver代码时,我注意到ObjectMeta.Name字段是使用names.SimpleNameGenerator.GenerateName工具从ObjectMeta.GenerateName字段生成的。
英文:
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:
-
The
Clientsetcontains an embeddedFakestructure which has thePrependReactormethod we need to call for this use case (there are a few others). This code here is invoked when creating such objects. -
The
PrependReactormethod has 3 parameters:verb,resourceandreaction. Forverb,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 settingresourceto "*" should be acceptable in this case. -
The
reactionparameter is of type ReactionFunc, which takes anActionas a parameter and returnshandled,retanderr. After some digging, I noticed that theactionparameter will be cast toCreateAction, which has theGetObject()method that returns aruntime.Objectinstance, which can be cast tometav1.Object. This interface allows us to get and set the various metadata fields of the underlying object. After setting the objectNamefield as needed, we have to returnhandled = false,ret = mutatedObjectanderr = nilto instruct the calling code to execute the remaining reactors. -
Digging through the
apiservercode, I noticed that theObjectMeta.Namefield is generated from theObjectMeta.GenerateNamefield using thenames.SimpleNameGenerator.GenerateNameutility.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论