Kubernetes-apiServer源码深度分析(八)Extension API Server

550 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

记得看代码中的注释哈,理解都在里面 源码基于v1.24

本篇文章先从处理CRD的Extension API Server开始介绍Server的细节,从server的配置到创建再到处理过程慢慢深入

Extension API Server 的引入

Extension API Server主要处理的是CR相关资源对象的server,通过CRD去注册一个CR,进行拓展apiServer的原生资源对象 image.png 关于CRD(Custom Resource Definition)

image.png

ExtensionServer的配置

续用了上一个MasterServer的部分通用配置

func createAPIExtensionsConfig(
   kubeAPIServerConfig genericapiserver.Config,
   externalInformers kubeexternalinformers.SharedInformerFactory,
   pluginInitializers []admission.PluginInitializer,
   commandOptions *options.ServerRunOptions,
   masterCount int,
   serviceResolver webhook.ServiceResolver,
   authResolverWrapper webhook.AuthenticationInfoResolverWrapper,
) (*apiextensionsapiserver.Config, error) {
   // make a shallow copy to let us twiddle a few things
   // most of the config actually remains the same.  We only need to mess with a couple items related to the particulars of the apiextensions
   genericConfig := kubeAPIServerConfig
   genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
   genericConfig.RESTOptionsGetter = nil

   // override genericConfig.AdmissionControl with apiextensions' scheme,
   // because apiextensions apiserver should use its own scheme to convert resources.
   err := commandOptions.Admission.ApplyTo(
      &genericConfig,
      externalInformers,
      genericConfig.LoopbackClientConfig,
      utilfeature.DefaultFeatureGate,
      pluginInitializers...)
   if err != nil {
      return nil, err
   }

   // copy the etcd options so we don't mutate originals.
   etcdOptions := *commandOptions.Etcd
   etcdOptions.StorageConfig.Paging = utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
   // this is where the true decodable levels come from.
   etcdOptions.StorageConfig.Codec = apiextensionsapiserver.Codecs.LegacyCodec(v1beta1.SchemeGroupVersion, v1.SchemeGroupVersion)
   // prefer the more compact serialization (v1beta1) for storage until http://issue.k8s.io/82292 is resolved for objects whose v1 serialization is too big but whose v1beta1 serialization can be stored
   etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(v1beta1.SchemeGroupVersion, schema.GroupKind{Group: v1beta1.GroupName})
   genericConfig.RESTOptionsGetter = &genericoptions.SimpleRestOptionsFactory{Options: etcdOptions}

   // override MergedResourceConfig with apiextensions defaults and registry
   if err := commandOptions.APIEnablement.ApplyTo(
      &genericConfig,
      apiextensionsapiserver.DefaultAPIResourceConfigSource(),
      apiextensionsapiserver.Scheme); err != nil {
      return nil, err
   }
/*
    NewCRDRESTOptionsGetter去做处理CR的请求处理相关
*/

   apiextensionsConfig := &apiextensionsapiserver.Config{
      GenericConfig: &genericapiserver.RecommendedConfig{
         Config:                genericConfig,
         SharedInformerFactory: externalInformers,
      },
      ExtraConfig: apiextensionsapiserver.ExtraConfig{
         CRDRESTOptionsGetter: apiextensionsoptions.NewCRDRESTOptionsGetter(etcdOptions),
         MasterCount:          masterCount,
         AuthResolverWrapper:  authResolverWrapper,
         ServiceResolver:      serviceResolver,
      },
   }

   // we need to clear the poststarthooks so we don't add them multiple times to all the servers (that fails)
   apiextensionsConfig.GenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}

   return apiextensionsConfig, nil
}

流程解析如下图 image.png

ExtensionServer 的创建

func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
   genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
   if err != nil {
      return nil, err
   }

   // hasCRDInformerSyncedSignal is closed when the CRD informer this server uses has been fully synchronized.
   // It ensures that requests to potential custom resource endpoints while the server hasn't installed all known HTTP paths get a 503 error instead of a 404
   hasCRDInformerSyncedSignal := make(chan struct{})
   if err := genericServer.RegisterMuxAndDiscoveryCompleteSignal("CRDInformerHasNotSynced", hasCRDInformerSyncedSignal); err != nil {
      return nil, err
   }

   s := &CustomResourceDefinitions{
      GenericAPIServer: genericServer,
   }
/*
    CustomResource 经过apiGroupinfo 然后install到GenericServer ,GenericServer就能处理CustomResource相应的处理了(CRUD)了
*/
   apiResourceConfig := c.GenericConfig.MergedResourceConfig
   apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
   storage := map[string]rest.Storage{}
   // customresourcedefinitions
   if resource := "customresourcedefinitions"; apiResourceConfig.ResourceEnabled(v1.SchemeGroupVersion.WithResource(resource)) {
      customResourceDefinitionStorage, err := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
      if err != nil {
         return nil, err
      }
      storage[resource] = customResourceDefinitionStorage
      storage[resource+"/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefinitionStorage)
   }
   if len(storage) > 0 {
      apiGroupInfo.VersionedResourcesStorageMap[v1.SchemeGroupVersion.Version] = storage
   }

   if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
      return nil, err
   }

   crdClient, err := clientset.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
   if err != nil {
      // it's really bad that this is leaking here, but until we can fix the test (which I'm pretty sure isn't even testing what it wants to test),
      // we need to be able to move forward
      return nil, fmt.Errorf("failed to create clientset: %v", err)
   }
   s.Informers = externalinformers.NewSharedInformerFactory(crdClient, 5*time.Minute)
/*
    根据请求的不同类型交给不同的对象去处理
*/
   delegateHandler := delegationTarget.UnprotectedHandler()
   if delegateHandler == nil {
      delegateHandler = http.NotFoundHandler()
   }

   versionDiscoveryHandler := &versionDiscoveryHandler{
      discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{},
      delegate:  delegateHandler,
   }
   groupDiscoveryHandler := &groupDiscoveryHandler{
      discovery: map[string]*discovery.APIGroupHandler{},
      delegate:  delegateHandler,
   }
   /*
       构建crdHandler
   */
   establishingController := establish.NewEstablishingController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
   crdHandler, err := NewCustomResourceDefinitionHandler(
      versionDiscoveryHandler,
      groupDiscoveryHandler,
      s.Informers.Apiextensions().V1().CustomResourceDefinitions(),
      delegateHandler,
      c.ExtraConfig.CRDRESTOptionsGetter,
      c.GenericConfig.AdmissionControl,
      establishingController,
      c.ExtraConfig.ServiceResolver,
      c.ExtraConfig.AuthResolverWrapper,
      c.ExtraConfig.MasterCount,
      s.GenericAPIServer.Authorizer,
      c.GenericConfig.RequestTimeout,
      time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
      apiGroupInfo.StaticOpenAPISpec,
      c.GenericConfig.MaxRequestBodyBytes,
   )
   if err != nil {
      return nil, err
   }
   s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
   s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
   s.GenericAPIServer.RegisterDestroyFunc(crdHandler.destroy)
/*
    一系列相关的Controller,保证正常的运行
*/
   discoveryController := NewDiscoveryController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler)
   namingController := status.NewNamingConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
   nonStructuralSchemaController := nonstructuralschema.NewConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
   apiApprovalController := apiapproval.NewKubernetesAPIApprovalPolicyConformantConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
   finalizingController := finalizer.NewCRDFinalizer(
      s.Informers.Apiextensions().V1().CustomResourceDefinitions(),
      crdClient.ApiextensionsV1(),
      crdHandler,
   )

   s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
      s.Informers.Start(context.StopCh)
      return nil
   })
   s.GenericAPIServer.AddPostStartHookOrDie("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error {
      // OpenAPIVersionedService and StaticOpenAPISpec are populated in generic apiserver PrepareRun().
      // Together they serve the /openapi/v2 endpoint on a generic apiserver. A generic apiserver may
      // choose to not enable OpenAPI by having null openAPIConfig, and thus OpenAPIVersionedService
      // and StaticOpenAPISpec are both null. In that case we don't run the CRD OpenAPI controller.
      if s.GenericAPIServer.StaticOpenAPISpec != nil {
         if s.GenericAPIServer.OpenAPIVersionedService != nil {
            openapiController := openapicontroller.NewController(s.Informers.Apiextensions().V1().CustomResourceDefinitions())
            go openapiController.Run(s.GenericAPIServer.StaticOpenAPISpec, s.GenericAPIServer.OpenAPIVersionedService, context.StopCh)
         }

         if s.GenericAPIServer.OpenAPIV3VersionedService != nil && utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
            openapiv3Controller := openapiv3controller.NewController(s.Informers.Apiextensions().V1().CustomResourceDefinitions())
            go openapiv3Controller.Run(s.GenericAPIServer.OpenAPIV3VersionedService, context.StopCh)
         }
      }
/*
    启动Controller
*/
      go namingController.Run(context.StopCh)
      go establishingController.Run(context.StopCh)
      go nonStructuralSchemaController.Run(5, context.StopCh)
      go apiApprovalController.Run(5, context.StopCh)
      go finalizingController.Run(5, context.StopCh)

      discoverySyncedCh := make(chan struct{})
      go discoveryController.Run(context.StopCh, discoverySyncedCh)
      select {
      case <-context.StopCh:
      case <-discoverySyncedCh:
      }

      return nil
   })
   // we don't want to report healthy until we can handle all CRDs that have already been registered.  Waiting for the informer
   // to sync makes sure that the lister will be valid before we begin.  There may still be races for CRDs added after startup,
   // but we won't go healthy until we can handle the ones already present.
   s.GenericAPIServer.AddPostStartHookOrDie("crd-informer-synced", func(context genericapiserver.PostStartHookContext) error {
      return wait.PollImmediateUntil(100*time.Millisecond, func() (bool, error) {
         if s.Informers.Apiextensions().V1().CustomResourceDefinitions().Informer().HasSynced() {
            close(hasCRDInformerSyncedSignal)
            return true, nil
         }
         return false, nil
      }, context.StopCh)
   })

   return s, nil
}

流程解析如下图 image.png

CRDHandler怎么处理请求的呢?

func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   ctx := req.Context()
   requestInfo, ok := apirequest.RequestInfoFrom(ctx)
   if !ok {
      responsewriters.ErrorNegotiated(
         apierrors.NewInternalError(fmt.Errorf("no RequestInfo found in the context")),
         Codecs, schema.GroupVersion{}, w, req,
      )
      return
   }
   /*
       是不是要version,group呢?交给相应的Handler去处理
   */
   if !requestInfo.IsResourceRequest {
      pathParts := splitPath(requestInfo.Path)
      // only match /apis/<group>/<version>
      // only registered under /apis
      if len(pathParts) == 3 {
         r.versionDiscoveryHandler.ServeHTTP(w, req)
         return
      }
      // only match /apis/<group>
      if len(pathParts) == 2 {
         r.groupDiscoveryHandler.ServeHTTP(w, req)
         return
      }

      r.delegate.ServeHTTP(w, req)
      return
   }

   crdName := requestInfo.Resource + "." + requestInfo.APIGroup
   crd, err := r.crdLister.Get(crdName)
   if apierrors.IsNotFound(err) {
      r.delegate.ServeHTTP(w, req)
      return
   }
   if err != nil {
      utilruntime.HandleError(err)
      responsewriters.ErrorNegotiated(
         apierrors.NewInternalError(fmt.Errorf("error resolving resource")),
         Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
      )
      return
   }
/*
    校验正确  如果对应不是 就交给r.delegate.ServeHTTP 也就是Notfound啦
*/
   // if the scope in the CRD and the scope in request differ (with exception of the verbs in possiblyAcrossAllNamespacesVerbs
   // for namespaced resources), pass request to the delegate, which is supposed to lead to a 404.
   namespacedCRD, namespacedReq := crd.Spec.Scope == apiextensionsv1.NamespaceScoped, len(requestInfo.Namespace) > 0
   if !namespacedCRD && namespacedReq {
      r.delegate.ServeHTTP(w, req)
      return
   }
   if namespacedCRD && !namespacedReq && !possiblyAcrossAllNamespacesVerbs.Has(requestInfo.Verb) {
      r.delegate.ServeHTTP(w, req)
      return
   }

   if !apiextensionshelpers.HasServedCRDVersion(crd, requestInfo.APIVersion) {
      r.delegate.ServeHTTP(w, req)
      return
   }

   // There is a small chance that a CRD is being served because NamesAccepted condition is true,
   // but it becomes "unserved" because another names update leads to a conflict
   // and EstablishingController wasn't fast enough to put the CRD into the Established condition.
   // We accept this as the problem is small and self-healing.
   if !apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.NamesAccepted) &&
      !apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Established) {
      r.delegate.ServeHTTP(w, req)
      return
   }

   terminating := apiextensionshelpers.IsCRDConditionTrue(crd, apiextensionsv1.Terminating)

   crdInfo, err := r.getOrCreateServingInfoFor(crd.UID, crd.Name)
   if apierrors.IsNotFound(err) {
      r.delegate.ServeHTTP(w, req)
      return
   }
   if err != nil {
      utilruntime.HandleError(err)
      responsewriters.ErrorNegotiated(
         apierrors.NewInternalError(fmt.Errorf("error resolving resource")),
         Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
      )
      return
   }
   if !hasServedCRDVersion(crdInfo.spec, requestInfo.APIVersion) {
      r.delegate.ServeHTTP(w, req)
      return
   }

   deprecated := crdInfo.deprecated[requestInfo.APIVersion]
   for _, w := range crdInfo.warnings[requestInfo.APIVersion] {
      warning.AddWarning(req.Context(), "", w)
   }

   verb := strings.ToUpper(requestInfo.Verb)
   resource := requestInfo.Resource
   subresource := requestInfo.Subresource
   scope := metrics.CleanScope(requestInfo)
   supportedTypes := []string{
      string(types.JSONPatchType),
      string(types.MergePatchType),
   }
   if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
      supportedTypes = append(supportedTypes, string(types.ApplyPatchType))
   }

   var handlerFunc http.HandlerFunc
   subresources, err := apiextensionshelpers.GetSubresourcesForVersion(crd, requestInfo.APIVersion)
   if err != nil {
      utilruntime.HandleError(err)
      responsewriters.ErrorNegotiated(
         apierrors.NewInternalError(fmt.Errorf("could not properly serve the subresource")),
         Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
      )
      return
   }
   /*
       子对象去调用对应的方法
   */
   switch {
   case subresource == "status" && subresources != nil && subresources.Status != nil:
      handlerFunc = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
   case subresource == "scale" && subresources != nil && subresources.Scale != nil:
      handlerFunc = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
   case len(subresource) == 0:
      handlerFunc = r.serveResource(w, req, requestInfo, crdInfo, crd, terminating, supportedTypes)
   default:
      responsewriters.ErrorNegotiated(
         apierrors.NewNotFound(schema.GroupResource{Group: requestInfo.APIGroup, Resource: requestInfo.Resource}, requestInfo.Name),
         Codecs, schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}, w, req,
      )
   }

   if handlerFunc != nil {
      handlerFunc = metrics.InstrumentHandlerFunc(verb, requestInfo.APIGroup, requestInfo.APIVersion, resource, subresource, scope, metrics.APIServerComponent, deprecated, "", handlerFunc)
      handler := genericfilters.WithWaitGroup(handlerFunc, longRunningFilter, crdInfo.waitGroup)
      /*
          真正响应
      */
      handler.ServeHTTP(w, req)
      return
   }
}

流程解析如下图 image.png

Summary

  1. ExtensionServer的配置 image.png
  2. ExtensionServer的创建 image.png
  3. crdHandler处理 image.png