Kubernetes Fake Client 不处理 ObjectMeta 中的 GenerateName

Kubernetes Fake Client doesn't handle GenerateName in ObjectMeta

当使用 Kubernetes Fake Client to write unit tests, I noticed that it fails to create two identical objects which have their 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

根据thisGitHub问题评论:

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.

要添加所需的反应器,我们可以在创建 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 structure which has the PrependReactor method we need to call for this use case (there are a few others). This code here 在创建此类对象时被调用。
  • PrependReactor方法有3个参数:verbresourcereaction。对于 verbresource,我找不到任何命名常量,因此,在这种情况下,“create”和“secrets”(奇怪的是它不是“secret”)似乎是正确的值对于他们来说,如果我们想要超级具体,但是在这种情况下将 resource 设置为“*”应该是可以接受的。
  • reaction 参数的类型为 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。该接口允许我们获取和设置底层对象的各种元数据字段。根据需要设置对象 Name 字段后,我们必须 return handled = falseret = mutatedObjecterr = nil 来指示调用代码执行剩余的反应器。
  • 深入研究 apiserver 代码,我注意到 ObjectMeta.Name 字段 is generated from the ObjectMeta.GenerateName field using the names.SimpleNameGenerator.GenerateName 实用程序。