英文:
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
Clientset
contains an embeddedFake
structure which has thePrependReactor
method we need to call for this use case (there are a few others). This code here is invoked when creating such objects. -
The
PrependReactor
method has 3 parameters:verb
,resource
andreaction
. 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 settingresource
to "*" should be acceptable in this case. -
The
reaction
parameter is of type ReactionFunc, which takes anAction
as a parameter and returnshandled
,ret
anderr
. After some digging, I noticed that theaction
parameter will be cast toCreateAction
, which has theGetObject()
method that returns aruntime.Object
instance, 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 objectName
field as needed, we have to returnhandled = false
,ret = mutatedObject
anderr = nil
to instruct the calling code to execute the remaining reactors. -
Digging through the
apiserver
code, I noticed that theObjectMeta.Name
field is generated from theObjectMeta.GenerateName
field using thenames.SimpleNameGenerator.GenerateName
utility.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论