Kubernetes:kube-apiserver 之 scheme(二)

2023-10-19 12:01:03

Kubernetes:kube-apiserver 之 scheme(一)

2.2 資源 convert

上篇說到資源版本之間通過內部版本 __internal 進行資源轉換。這裡進一步擴充套件介紹資源轉換內容,以加深理解。

同樣以例子開始,通過 kubectlapps/v1beta1/Deployment 轉換為 apps/v1/Deployment

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:1.0.0
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
        ports:
        - containerPort: 80

執行 kubectl convert -f v1beta1Deployment.yaml --output-version=apps/v1,輸出 apps/v1/Deployment 資源設定:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: myapp
  name: myapp
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 2
  ...
status: {}

這條命令背後,kubectl 將轉換命令組成 client-go 識別的 Restful API 訊息,通過 client-go 發給 kube-apiserverkube-apiserver 根據事先註冊的轉換函數 convertapps/v1beta1/Deployment 轉換為 apps/__internal/Deployment,接著將 apps/__internal/Deployment 轉換為 apps/v1/Deployment

程式碼範例如下:

v1beta1Deployment := &appsv1beta1.Deployment{
    TypeMeta: metav1.TypeMeta{
        Kind:       "Deployment",
        APIVersion: "apps/v1beta1",
    },
}

// v1beta1 -> __internal
targetVersion := schema.GroupVersion{Group: "apps", Version: "__internal"}
objInternal, err := scheme.ConvertToVersion(v1beta1Deployment, targetVersion)
if err != nil {
    panic(err)
}

// __internal -> v1
objV1, err := scheme.ConvertToVersion(objInternal, appsv1.SchemeGroupVersion)
if err != nil {
    panic(err)
}

scheme.ConvertToVersion 函數轉換物件到指定資源版本。

2.2.1 kube-apiserver 資源版本 convert

資源版本的轉換首先需要註冊版本轉換函數到 scheme,只有註冊過的版本才能進行資源版本轉換。

kube-apiserver 通過匯入包的方式註冊轉換函數。

# kubernetes/cmd/kube-apiserver/app/server.go
package app

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

# kubernetes/pkg/controlplane/import_known_versions.go
package controlplane

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

kube-apiserver 啟動的 app 包匯入 controlplane 包,controlplane 繼續匯入資源組的安裝包。以 apps 資源組為例,檢視資源組下資源轉換函數是怎麼註冊的。

資源組下的每個外部資源版本都有 zz_generated.conversion.go 檔案,該檔案由 conversion-go 自動生成,檔案中定義了外部資源到內部資源的相互轉換。

# kubernetes/pkg/apis/apps/
apps/
    /v1
        /zz_generated.conversion.go
    /v1beta1
        /zz_generated.conversion.go
    /v1beta2
        /zz_generated.conversion.go

v1 版本為例,zz_generated.conversion.go 中註冊資源轉換函數:

package v1

func init() {
	localSchemeBuilder.Register(RegisterConversions)
}

func RegisterConversions(s *runtime.Scheme) error {
    if err := s.AddGeneratedConversionFunc((*v1.DeploymentSpec)(nil), (*apps.DeploymentSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
		return Convert_v1_DeploymentSpec_To_apps_DeploymentSpec(a.(*v1.DeploymentSpec), b.(*apps.DeploymentSpec), scope)
	}); err != nil {
		return err
	}
    if err := s.AddConversionFunc((*apps.DeploymentSpec)(nil), (*v1.DeploymentSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
		return Convert_apps_DeploymentSpec_To_v1_DeploymentSpec(a.(*apps.DeploymentSpec), b.(*v1.DeploymentSpec), scope)
	}); err != nil {
		return err
	}
    ...
}

可以看到,函數 Convert_v1_DeploymentSpec_To_apps_DeploymentSpec 註冊了 v1/DeploymentSpec__internal/DeploymentSpec 的資源轉換,Convert_apps_DeploymentSpec_To_v1_DeploymentSpec 註冊了 __internal/DeploymentSpecv1/DeploymentSpec

關於資源版本轉換基本告一段落。下面進一步介紹,在 kube-apiserver 是怎麼使用 scheme 資源登入檔的。

2.3 使用資源登入檔 scheme

這一節會進入 kube-apiserverscheme 是如何使用的。需要說明的是,kube-apiserver 非常複雜,這裡並不介紹啟動流程,建立 Restful API 等內容,僅從 scheme 的視角看 kube-apiserver

# kubernetes/cmd/kube-apiserver/app/server.go

func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
	notFoundHandler := notfoundhandler.New(config.ControlPlane.GenericConfig.Serializer, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
	apiExtensionsServer, err := config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
	if err != nil {
		return nil, err
	}
  ...
}

CreateServerChain 函數中建立 API 擴充套件服務(APIExtensionServer)API 核心服務(KubeAPIServer)API 聚合服務(AggregatorServer)。這裡以 API 擴充套件服務(APIExtensionServer)scheme 是如何使用的。

進入 config.ApiExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)) 方法。

# kubernetes/vendor/k8s.io/apiserver/pkg/server/genericapiserver.go
// APIGroupInfo 中儲存資源登入檔 Scheme
func NewDefaultAPIGroupInfo(group string, scheme *runtime.Scheme, parameterCodec runtime.ParameterCodec, codecs serializer.CodecFactory) APIGroupInfo {
	return APIGroupInfo{
		PrioritizedVersions:          scheme.PrioritizedVersionsForGroup(group),
		VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
		OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
		Scheme:                 scheme,
		ParameterCodec:         parameterCodec,
		NegotiatedSerializer:   codecs,
	}
}

# kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go
// 將 apiserver.Scheme 傳入 NewDefaultAPIGroupInfo
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
	...
	apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
  ...
  if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
		return nil, err
	}
}

# kubernetes/vendor/k8s.io/apiserver/pkg/server/genericapiserver.go
func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
	return s.InstallAPIGroups(apiGroupInfo)
}

func (s *GenericAPIServer) InstallAPIGroups(apiGroupInfos ...*APIGroupInfo) error {
  ...
  for _, apiGroupInfo := range apiGroupInfos {
    if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels); err != nil {
      return fmt.Errorf("unable to install api resources: %v", err)
    }
  }
}

// 在 GenericAPIServer.getAPIGroupVersion 方法中將 apiGroupInfo 轉換解析為 apiGroupVersion
// apiGroupVersion 中儲存資源登入檔 scheme
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, typeConverter managedfields.TypeConverter) error {
	for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
		apiGroupVersion, err := s.getAPIGroupVersion(apiGroupInfo, groupVersion, apiPrefix)
		if err != nil {
			return err
		}

    ...
		discoveryAPIResources, r, err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)
  }
}

# kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/groupversion.go
func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]apidiscoveryv2beta1.APIResourceDiscovery, []*storageversion.ResourceInfo, error) {
	installer := &APIInstaller{
		group:             g,
		prefix:            prefix,
		minRequestTimeout: g.MinRequestTimeout,
	}

	apiResources, resourceInfos, ws, registrationErrors := installer.Install()
	...
}

# kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/installer.go
func (a *APIInstaller) Install() ([]metav1.APIResource, []*storageversion.ResourceInfo, *restful.WebService, []error) {
	for _, path := range paths {
		apiResource, resourceInfo, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
		...
	}
	...
}

func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, *storageversion.ResourceInfo, error) {
  fqKindToRegister, err := GetResourceKind(a.group.GroupVersion, storage, a.group.Typer)
	if err != nil {
		return nil, nil, err
	}
}

func GetResourceKind(groupVersion schema.GroupVersion, storage rest.Storage, typer runtime.ObjectTyper) (schema.GroupVersionKind, error) {
	object := storage.New()
	fqKinds, _, err := typer.ObjectKinds(object)
	if err != nil {
		return schema.GroupVersionKind{}, err
	}
  ...
}

函數鏈很長,最後在函數 GetResourceKind 中呼叫 typer.ObjectKinds(object) 獲取物件 object 的型別。其中,typer 是一個獲取物件型別的介面,對應的範例是 scheme,實際是呼叫 scheme.ObjectKinds 方法獲取物件型別。

注意,能獲取物件型別是因為該物件提前註冊在資源登入檔 scheme 中,否則將會報 no kind is registered for the type xxx 錯誤。

範例程式碼如下。

package main

import (
	"fmt"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/kubernetes/pkg/apis/core"
)

func main() {
	pod := &core.Pod{
		TypeMeta: metav1.TypeMeta{
			Kind: "Pod",
		},
		ObjectMeta: metav1.ObjectMeta{
			Labels: map[string]string{"name": "foo"},
		},
	}

	coreGV := schema.GroupVersion{Group: "", Version: "v1"}
	schema := runtime.NewScheme()
	schema.AddKnownTypes(coreGV, &core.Pod{})

	gvk, _, err := schema.ObjectKinds(pod)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(gvk)
}

2.3.1 資源物件 runtime.Object

上面說到資源物件,這裡有必要在擴充套件下資源物件 runtime.Objectruntime.Object 是一個介面,資源需要實現該介面,通過介面方法可以實現資源和介面的相互轉換。示意圖如下。

示意程式碼如下:

package main

import (
	"reflect"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/kubernetes/pkg/apis/core"
)

func main() {
	pod := &core.Pod{
		TypeMeta: metav1.TypeMeta{
			Kind: "Pod",
		},
		ObjectMeta: metav1.ObjectMeta{
			Labels: map[string]string{"name": "foo"},
		},
	}

	obj := runtime.Object(pod)

	pod2, ok := obj.(*core.Pod)
	if !ok {
		panic("unexpected runtime object")
	}

	if !reflect.DeepEqual(pod, pod2) {
		panic("unexpected")
	}
}

介紹完資源登入檔 scheme,後續將繼續介紹 kube-apiserver 是怎麼啟動,註冊 RESTful API,如何實現認證,鑑權等操作,未完待續...