Kubernetes:kube-apiserver 之 scheme(一)

2023-10-18 06:00:29

0. 前言

在進入 kube-apiserver 原始碼分析前,有一個非常重要的概念需要了解甚至熟悉的:資源登入檔(scheme)。

Kubernetes 中一切皆資源,管理的是資源,建立、更新、刪除的是資源。如何對龐雜的資源進行管理就成了一件大事。Kubernetes 通過引入 scheme 資源登入檔,將資源資訊註冊到資源登入檔,各個元件通過索引資源登入檔實現資源的管理。

1. 介紹

直接開始閱讀 kube-apiserver scheme 部分的原始碼是比較困難的,這裡舉程式碼範例如下:

package main

import (
	"fmt"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

func main() {
	// KnownType external
	coreGV := schema.GroupVersion{Group: "", Version: "v1"}
	extensionsGV := schema.GroupVersion{Group: "extensions", Version: "v1beta1"}

	// KnownType internal
	coreInternalGV := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal}

	// UnversionedType
	Unversioned := schema.GroupVersion{Group: "", Version: "v1"}

	schema := runtime.NewScheme()
	schema.AddKnownTypes(coreGV, &corev1.Pod{})
	schema.AddKnownTypes(extensionsGV, &appsv1.DaemonSet{})
	schema.AddKnownTypes(coreInternalGV, &corev1.Pod{})
	schema.AddUnversionedTypes(Unversioned, &metav1.Status{})

	fmt.Println(*schema)
    fmt.Println(schema.KnownTypes(coreGV))
}

範例中有幾點需要關注。

  1. 通過 runtime.NewScheme 建立 scheme 範例:
func NewScheme() *Scheme {
	s := &Scheme{
		gvkToType:                 map[schema.GroupVersionKind]reflect.Type{},
		typeToGVK:                 map[reflect.Type][]schema.GroupVersionKind{},
		unversionedTypes:          map[reflect.Type]schema.GroupVersionKind{},
		unversionedKinds:          map[string]reflect.Type{},
		fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
		defaulterFuncs:            map[reflect.Type]func(interface{}){},
		versionPriority:           map[string][]string{},
		schemeName:                naming.GetNameFromCallsite(internalPackages...),
	}
	s.converter = conversion.NewConverter(nil)

	...
	return s
}

Scheme 的結構體有四個 field 需要重點關注:

  • gvkToType: 記錄的是 schema.GroupVersionKind 和資源型別的對映關係。
  • typeToGVK: 記錄的是資源型別和 schema.GroupVersionKind 的對映關係。
  • unversionedTypes: 記錄的是無版本資源型別和 schema.GroupVersionKind 的對映關係。
  • unversionedKinds: 記錄的是資源種類 Kind 和無版本資源型別的對映關係。

還有一個 Scheme.converter 也挺重要,這裡先跳過不講。

  1. 呼叫 SchemeAddKnownTypesAddUnversionedTypes 方法建立資源 Group/Version/Kind 和資源型別的相互對映關係。

AddKnownTypes 為例,函數通過反射 reflect.TypeOf(obj) 獲得物件 runtime.Object 的資源型別。這裡,物件是資源的介面,可實現資源和物件的相互轉換。

func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
	s.addObservedVersion(gv)
	for _, obj := range types {
		t := reflect.TypeOf(obj)
		if t.Kind() != reflect.Pointer {
			panic("All types must be pointers to structs.")
		}
		t = t.Elem()
		s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
	}
}

func (gv GroupVersion) WithKind(kind string) GroupVersionKind {
	return GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind}
}

func (s *Scheme) AddKnownTypeWithName(gvk schema.GroupVersionKind, obj Object) {
	...
	t := reflect.TypeOf(obj)
	s.gvkToType[gvk] = t
}

然後呼叫 gv.WithKind(t.Name()) 轉換為 GroupVersionKind,可以看出上面反射的資源型別作為 kind 賦給 GroupVersionKind

最後呼叫 AddKnownTypeWithNameschema.GroupVersionKind 和資源型別寫入 Scheme.gvkToType

執行程式碼結果如下:

{
	map[
		{ __internal Pod}:0xb46180 
		{ v1 Pod}:0xb46180 
		{ v1 Status}:0xb56d40 
		{extensions v1beta1 DaemonSet}:0xb44e40
	] 
	map[
		0xb44e40:[{extensions v1beta1 DaemonSet}] 
		0xb46180:[{ v1 Pod} { __internal Pod}] 
		0xb56d40:[{ v1 Status}]
	] 
	map[
		0xb56d40:{ v1 Status}
	] 
	map[
		Status:0xb56d40
	] 
	map[] 
	map[] 
	0xc000124888 
	map[] 
	[{ v1} {extensions v1beta1}] 
	pkg/runtime/scheme.go:100
}
map[Pod:v1.Pod Status:v1.Status]
  1. Kubernetes 資源分為外部資源和內部資源。外部資源是對外可存取的,其版本為 v1/v1beta1 等,內部資源是 Kubernetes 內部存取的資源,其版本為 __internal

為什麼會有內部版本呢?
為了適配版本間的 convert

試想,如果 v1v1apha1v1beta1v1beta2 要相互轉換需要建立十二種轉換關係:

v1 		 ---->  v1alpha1  
v1 		 ---->  v1beta1 
v1 		 ---->  v1beta2  
v1alpha1 ---->  v1  
v1alpha1 ---->  v1beta1  
v1alpha1 ---->  v1beta2
v1beta1  ---->  v1  
v1beta1  ---->  v1alpha1  
v1beta1  ---->  v1beta2
v1beta2  ---->  v1  
v1beta2  ---->  v1alpha1  
v1beta2  ---->  v1beta1

可以看到這種轉換已經比較複雜了,隨著資源增多,這種資源版本之間的轉換會越來越複雜且難以維護。Kubernetes 通過引入內部版本 __internl 建立每種資源到 __internal 的轉換,從而實現不同資源版本的相互轉換。

v1 			---->  __internal  
__internal  ---->  v1 
v1alpha1 	---->  __internal 
__internal  ---->  v1alpha1 
v1beta1 	---->  __internal  
__internal  ---->  v1beta1
v1beta2 	---->  __internal
__internal  ---->  v1beta2

轉換示意圖如下:

可以看到,相當乾淨,清爽。

2. kube-apiserver scheme

第一節介紹了 scheme 結構及部分特性。繼續看 kube-apiserverscheme 的應用。

2.1 註冊資源

kube-apiserver 中資源的註冊通過匯入包的形式實現。Kubernetes 將資源拆分為三類,並且由三種 HTTP Server 負責處理這三類資源,架構如下。

關於這幅圖,這裡不多講,後續會逐層介紹。
要知道的是,三類資源分別註冊到 extensionsapiserver.Schemelegacyscheme.Schemeaggregatorscheme.Scheme 三種資源登入檔中。

legacyscheme.Scheme 資源登入檔為例。
kube-apiserver 的啟動 app 包中匯入 "k8s.io/kubernetes/pkg/api/legacyscheme""k8s.io/kubernetes/pkg/controlplane"包:

package app

import (
	"k8s.io/kubernetes/pkg/api/legacyscheme"
	"k8s.io/kubernetes/pkg/controlplane"
)

legacyscheme 包中建立了 Scheme 資源登入檔,Codecs 編碼器和 ParameterCodec 引數編碼器。

# kubernetes/pkg/api/legacyscheme/scheme.go

package legacyscheme

var (
	Scheme = runtime.NewScheme()

	Codecs = serializer.NewCodecFactory(Scheme)

	ParameterCodec = runtime.NewParameterCodec(Scheme)
)

controlplane 包中匯入需要註冊的資源組:

# kubernetes/pkg/controlplane/import_known_versions.go

package controlplane

import (
	_ "k8s.io/kubernetes/pkg/apis/apps/install"
)

以註冊 apps 資源組為例:

# kubernetes/pkg/apis/apps/install/install.go

package install

import (
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/kubernetes/pkg/api/legacyscheme"
	"k8s.io/kubernetes/pkg/apis/apps"
	"k8s.io/kubernetes/pkg/apis/apps/v1"
	"k8s.io/kubernetes/pkg/apis/apps/v1beta1"
	"k8s.io/kubernetes/pkg/apis/apps/v1beta2"
)

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

func Install(scheme *runtime.Scheme) {
	utilruntime.Must(apps.AddToScheme(scheme))
	utilruntime.Must(v1beta1.AddToScheme(scheme))
	utilruntime.Must(v1beta2.AddToScheme(scheme))
	utilruntime.Must(v1.AddToScheme(scheme))
	utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta2.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
}

資源組中呼叫 apps.AddToSchemev1beta1.AddToScheme,v1beta2.AddToSchemev1.AddToScheme 函數將同一資源組不同版本的資源註冊到 legacyscheme.Scheme 資源登入檔中。

其中 apps.AddToScheme 註冊的是內部版本的資源:

package apps

var (
	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
	AddToScheme = SchemeBuilder.AddToScheme
)

const GroupName = "apps"

var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}

func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&DaemonSet{},
		&DaemonSetList{},
		&Deployment{},
		&DeploymentList{},
		&DeploymentRollback{},
		&autoscaling.Scale{},
		&StatefulSet{},
		&StatefulSetList{},
		&ControllerRevision{},
		&ControllerRevisionList{},
		&ReplicaSet{},
		&ReplicaSetList{},
	)
	return nil
}

v1beta1.AddToScheme,v1beta2.AddToSchemev1.AddToScheme 註冊的是外部版本的資源,以 v1.AddToScheme 為例:

package v1

const GroupName = "apps"

var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}

var (
	SchemeBuilder      = runtime.NewSchemeBuilder(addKnownTypes)
	localSchemeBuilder = &SchemeBuilder
	AddToScheme        = localSchemeBuilder.AddToScheme
)

func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&Deployment{},
		&DeploymentList{},
		&StatefulSet{},
		&StatefulSetList{},
		&DaemonSet{},
		&DaemonSetList{},
		&ReplicaSet{},
		&ReplicaSetList{},
		&ControllerRevision{},
		&ControllerRevisionList{},
	)
	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
	return nil
}

需要注意的是,內部版本和外部版本註冊的資源並不一樣。比如 Deployment 資源,外部版本參照的是 kubernetes/vendor/k8s.io/api/apps/v1/types.go 中的資源定義:

type Deployment struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
	Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
	Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

資源宣告了 jsonprotobuf tag,以便於外部存取的序列化,反序列化操作。

內部版本參照的是 kubernetes/pkg/apis/apps/types.go 中的資源定義:

type Deployment struct {
	metav1.TypeMeta
	metav1.ObjectMeta
	Spec DeploymentSpec
	Status DeploymentStatus
}

內部資源不需要被外部存取,沒有 tag 宣告。

到這裡,kube-apiserver 關於資源的註冊就差不多了。下一篇將進一步介紹 schemeconvertkube-apiserver 中如何使用 schemeschemeobject 相互轉換等內容。