一個 API 資源對象的 Schema 的唯一標識由 apiVersion 和 kind 組成,其中 apiVersion 又分爲 Group 和 Version。例如:

Group    Version    Kind
apps     v1         Deployment

kubernetes 的 apiVersion 有很多,可以通過 kubectl api-versions 命令查看當前支持的 API 版本:

# kubectl api-versions
admissionregistration.k8s.io/v1beta1
apiextensions.k8s.io/v1beta1
apiregistration.k8s.io/v1
apiregistration.k8s.io/v1beta1
apps/v1
apps/v1beta1
apps/v1beta2
authentication.k8s.io/v1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1
authorization.k8s.io/v1beta1
autoscaling/v1
autoscaling/v2beta1
autoscaling/v2beta2
batch/v1
batch/v1beta1
certificates.k8s.io/v1beta1
coordination.k8s.io/v1
coordination.k8s.io/v1beta1
crd.projectcalico.org/v1
events.k8s.io/v1beta1
extensions/v1beta1
networking.k8s.io/v1
networking.k8s.io/v1beta1
node.k8s.io/v1beta1
policy/v1beta1
rbac.authorization.k8s.io/v1
rbac.authorization.k8s.io/v1beta1
scheduling.k8s.io/v1
scheduling.k8s.io/v1beta1
storage.k8s.io/v1
storage.k8s.io/v1beta1
v1

通過 kubectl api-resources 還可以查看當前支持的 API 資源對象:

# kubectl api-resources | grep deploy
deployments                       deploy       apps                           true         Deployment
deployments                       deploy       extensions                     true         Deployment

一個 kind 可能出現在不同的 apiVersion 中,例如 Deployment 就出現在瞭如下多個 apiVersion 中:

  • k8s.io/api/apps/v1.Deployment
  • k8s.io/api/apps/v1beta1.Deployment
  • k8s.io/api/apps/v1beta2.Deployment
  • k8s.io/api/extensions/v1beta1.Deployment

對於不同的 apiVersion,在 kube-apiserver 中,其對應的 HTTP Handler 是什麼樣子的呢?

在 kube-apiserver 的啓動過程中,服務路由的安裝是通過如下調用實現的:

func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*Master, error) {

    ......

    restStorageProviders := []RESTStorageProvider{
        auditregistrationrest.RESTStorageProvider{},
        authenticationrest.RESTStorageProvider{Authenticator: c.GenericConfig.Authentication.Authenticator, APIAudiences: c.GenericConfig.Authentication.APIAudiences},
        authorizationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, RuleResolver: c.GenericConfig.RuleResolver},
        autoscalingrest.RESTStorageProvider{},
        batchrest.RESTStorageProvider{},
        certificatesrest.RESTStorageProvider{},
        coordinationrest.RESTStorageProvider{},
        extensionsrest.RESTStorageProvider{},
        networkingrest.RESTStorageProvider{},
        noderest.RESTStorageProvider{},
        policyrest.RESTStorageProvider{},
        rbacrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer},
        schedulingrest.RESTStorageProvider{},
        settingsrest.RESTStorageProvider{},
        storagerest.RESTStorageProvider{},
        // keep apps after extensions so legacy clients resolve the extensions versions of shared resource names.
        // See https://github.com/kubernetes/kubernetes/issues/42392
        appsrest.RESTStorageProvider{},
        admissionregistrationrest.RESTStorageProvider{},
        eventsrest.RESTStorageProvider{TTL: c.ExtraConfig.EventTTL},
    }
    m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...)

    ......

}

以 appsrest.RESTStorageProvider{} 爲例,從它的 NewRESTStorage 方法可以看出,它管理了 apps 這個 Group 下支持的 Version:

func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, bool) {
    apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apps.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs)
    // If you add a version here, be sure to add an entry in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go with specific priorities.
    // TODO refactor the plumbing to provide the information in the APIGroupInfo

    if apiResourceConfigSource.VersionEnabled(appsapiv1beta1.SchemeGroupVersion) {
        apiGroupInfo.VersionedResourcesStorageMap[appsapiv1beta1.SchemeGroupVersion.Version] = p.v1beta1Storage(apiResourceConfigSource, restOptionsGetter)
    }
    if apiResourceConfigSource.VersionEnabled(appsapiv1beta2.SchemeGroupVersion) {
        apiGroupInfo.VersionedResourcesStorageMap[appsapiv1beta2.SchemeGroupVersion.Version] = p.v1beta2Storage(apiResourceConfigSource, restOptionsGetter)
    }
    if apiResourceConfigSource.VersionEnabled(appsapiv1.SchemeGroupVersion) {
        apiGroupInfo.VersionedResourcesStorageMap[appsapiv1.SchemeGroupVersion.Version] = p.v1Storage(apiResourceConfigSource, restOptionsGetter)
    }

    return apiGroupInfo, true
}

而每個 Version 下面管理着支持的 Kind:

func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) map[string]rest.Storage {
    storage := map[string]rest.Storage{}

    // deployments
    deploymentStorage := deploymentstore.NewStorage(restOptionsGetter)
    storage["deployments"] = deploymentStorage.Deployment
    storage["deployments/status"] = deploymentStorage.Status
    storage["deployments/scale"] = deploymentStorage.Scale

    ......

    return storage
}

對比不同 Version,發現對於同一個 Kind,使用的都是相同的 Storage 實現,例如 Deployment 的 Storage 實現就是 pkg/registry/apps/deployment/storage.DeploymentStorage。爲何不同 apiVersion 的資源對象可以使用相同的 Storage 實現呢?

通過跟蹤請求 PATCH /apis/apps/v1/namespaces/default/deployments/nginx,發現在處理過程中對資源對象版本進行了轉換:

patcher.patchResource
    ...
    schemaReferenceObj, err := p.unsafeConvertor.ConvertToVersion(p.restPatcher.New(), p.kind.GroupVersion())
    // *v1.Deployment                                               *apps.Deployment        apps/v1
    ...

smpPatcher.applyPatchToCurrentObject
    ...
    versionedObjToUpdate, err := p.creater.New(p.kind) // runtime.Scheme
    // *v1.Deployment
    if err != nil {
        return nil, err
    }
    if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj); err != nil {
        return nil, err
    }
    newObj, err := p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, p.hubGroupVersion)
    // *apps.Deployment                                *v1.Deployment        apps/__internal

也就是 apps/v1/Deployment 會在處理的過程中被轉換爲內部對象 apps/Deployment。那麼這個轉換又是如何完成的呢?

通過調查 p.unsafeConvertor.ConvertToVersion,其實際上調用的是:

// 該方法會將 in 轉換爲 target 指定的類型。
k8s.io/apimachinery/pkg/runtime.Scheme.ConvertToVersion(in Object, target GroupVersioner)
k8s.io/apimachinery/pkg/conversion.Converter.Convert(src, dest interface{}, flags FieldMatchingFlags, meta *Meta)
// 該方法會被註冊到 conversion.Converter 中
pkg/apis/apps/v1.Convert_v1_Deployment_To_apps_Deployment(in *appsv1.Deployment, out *apps.Deployment, s conversion.Scope)

所以完成 apps/v1/Deployment 到 apps/Deployment 的轉換就是由 pkg/apis/apps/v1.Convert_v1_Deployment_To_apps_Deployment 實現的,對應的 apps/Deployment 到 apps/v1/Deployment 的轉換是由 pkg/apis/apps/v1.Convert_apps_Deployment_To_v1_Deployment 實現,用於從 Etcd 中讀取資源對象後,解碼出來的是 apps/Deployment,然後根據當前請求的 apiVersion 進行轉換。

相關文章