apimachinery 中的概念

Kubernetes 的 api 相关代码中有很多概念都是 k8s 独有的,需要专门理解一下,才方便研究 k8s 代码。

Kubebuilder 项目有一篇文章比较好的介绍了这些关键概念的理解,可以先阅读一下:https://book.kubebuilder.io/cronjob-tutorial/gvks.html。我这里写的是我个人的理解。

GVK: GroupVersionKind

k8s.io/apimachinery/pkg/runtime/schema/group_version.go

// GroupVersionKind unambiguously identifies a kind.  It doesn't anonymously include GroupVersion
// to avoid automatic coercion.  It doesn't use a GroupVersion to avoid custom marshalling
type GroupVersionKind struct {
	Group   string
	Version string
	Kind    string
}

这个结构体包含了 API 的 group, version 和 kind 信息。这里的 kind 是对应的 Go 结构体的 type 名称。比如 StatefulSet 就是:

GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}

GVR: GroupVersionResource

k8s.io/apimachinery/pkg/runtime/schema/group_version.go

// GroupVersionResource unambiguously identifies a resource.  It doesn't anonymously include GroupVersion
// to avoid automatic coercion.  It doesn't use a GroupVersion to avoid custom marshalling
type GroupVersionResource struct {
	Group    string
	Version  string
	Resource string
}

比如:

GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"}

这个结构体包含了 API 的 group, version 和 resource 信息。这里的 resource 对应的是 API 路径里的名字。很容易会搞混 resource 和 kind 的区别,我觉得可以这么理解:

  • Resource 是 API 侧的概念,是根据 API 路径推导出来的资源类型名称,例如 pods, deployments 等(下面会说单复数的问题)。
  • Kind 是 API 路径里得到这个资源类型名称所对应的 Go 的结构体的 type 名称。

在现有的代码中,GVR 在 apiserver 端是比较少使用的,反而是在 controller 和 client 中会用得多一些。

apiserver 中的使用

下面这个函数中会添加 API 请求的 handler。

-> k8s.io/apiserver/pkg/endpoints/installer.go: func (a *APIInstaller) registerResourceHandlers()

因为 APIInstaller 中已经包含了 APIGroupVersion,所以在添加的过程中,可以根据 GroupVersion 直接得到 GVK:

	fqKindToRegister, err := GetResourceKind(a.group.GroupVersion, storage, a.group.Typer)
	if err != nil {
		return nil, nil, err
	}

	...

	reqScope := handlers.RequestScope{
		# 这里也生成了 GVR
		Resource:    a.group.GroupVersion.WithResource(resource),
	}

RESTMapper

其他地方的使用更多的是依赖于 RESTMapper 来根据 GVR 获得 GVK。

有好几种 RESTMapper,默认的如下:

k8s.io/apimachinery/pkg/api/meta/restmapper.go

// DefaultRESTMapper exposes mappings between the types defined in a
// runtime.Scheme. It assumes that all types defined the provided scheme
// can be mapped with the provided MetadataAccessor and Codec interfaces.
//
// The resource name of a Kind is defined as the lowercase,
// English-plural version of the Kind string.
// When converting from resource to Kind, the singular version of the
// resource name is also accepted for convenience.
//
// TODO: Only accept plural for some operations for increased control?
// (`get pod bar` vs `get pods bar`)
type DefaultRESTMapper struct {
	defaultGroupVersions []schema.GroupVersion

	resourceToKind       map[schema.GroupVersionResource]schema.GroupVersionKind
	kindToPluralResource map[schema.GroupVersionKind]schema.GroupVersionResource
	kindToScope          map[schema.GroupVersionKind]RESTScope
	singularToPlural     map[schema.GroupVersionResource]schema.GroupVersionResource
	pluralToSingular     map[schema.GroupVersionResource]schema.GroupVersionResource
}

从它的内容可以看出,它是在 resource 和 kind 之间做映射的。同时,它还指出了,resource name 是根据 kind 来的,小写且是复数。不过,为了方便,也支持单数形式的 resource name。

DefaultRESTMapper 实现了 RESTMapper interface。这个 interface 定义了一些方法用来实现转换,比如 KindFor 根据 GVR 得到 GVK:

	// KindFor takes a partial resource and returns the single match.  Returns an error if there are multiple matches
	KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error)

根据使用场景补充,k8s 中还实现了好几个不同的 RESTMapper,比如 MultiRESTMapper, DefferedDiscoveryRESTMapper 等。

Scheme

k8s.io/apimachinery/pkg/runtime/scheme.go

Scheme 的主要工作就是保存 Go 类型和对应的 API 信息之间的关系。通过它的一些成员可以看出它的设计目标就是保存这种映射关系:

type Scheme struct {
	// gvkToType allows one to figure out the go type of an object with
	// the given version and name.
	gvkToType map[schema.GroupVersionKind]reflect.Type

	// typeToGVK allows one to find metadata for a given go object.
	// The reflect.Type we index by should *not* be a pointer.
	typeToGVK map[reflect.Type][]schema.GroupVersionKind

	...
}

一般来说,一大堆的 API 可以共用一个 Scheme,比如 legacy API 都是共用下面这个文件中的 Scheme 对象:pkg/api/legacyscheme/scheme.go

代码中一般是使用 Scheme 对象的 AddKnownTypes 方法把 Go 对象添加到 Scheme 中的。搜索这个方法可以找到 API 对象被添加的路径。以 rbac 为例:

pkg/apis/rbac/register.go

// GroupName is the name of this API group.
const GroupName = "rbac.authorization.k8s.io"

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}

// SchemeBuilder is a function that calls Register for you.
var (
	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
	AddToScheme   = SchemeBuilder.AddToScheme
)

// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&Role{},
		&RoleBinding{},
		&RoleBindingList{},
		&RoleList{},

		&ClusterRole{},
		&ClusterRoleBinding{},
		&ClusterRoleBindingList{},
		&ClusterRoleList{},
	)
	return nil
}

另外,你可以根据上面代码中的 AddToScheme 方法推导出:当这个方法被调用时,就会执行这些添加操作。因此,也可以在代码中搜索 rbac.*AddToScheme 来找到添加的地方:

pkg/apis/rbac/install/install.go

func init() {
	Install(legacyscheme.Scheme)
}

// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
	utilruntime.Must(rbac.AddToScheme(scheme))
	utilruntime.Must(v1.AddToScheme(scheme))
	utilruntime.Must(v1beta1.AddToScheme(scheme))
	utilruntime.Must(v1alpha1.AddToScheme(scheme))
	utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion))
}

所以,只要这个 pkg 被 import,rbac 的这些信息就会被注册到 legacy 的 Scheme 中。在这个 API Group 的 storage 被初始化的时候,这个 pkg 就会被 import:

-> pkg/registry/rbac/rest/storage_rest.go: func (p RESTStorageProvider) NewRESTStorage()

知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。