k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controlplane/apiserver/aggregator.go (about)

     1  /*
     2  Copyright 2024 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apiserver
    18  
    19  import (
    20  	"fmt"
    21  	"net/http"
    22  	"strings"
    23  	"sync"
    24  
    25  	apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  	"k8s.io/apiserver/pkg/admission"
    31  	genericfeatures "k8s.io/apiserver/pkg/features"
    32  	genericapiserver "k8s.io/apiserver/pkg/server"
    33  	"k8s.io/apiserver/pkg/server/healthz"
    34  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    35  	utilpeerproxy "k8s.io/apiserver/pkg/util/peerproxy"
    36  	kubeexternalinformers "k8s.io/client-go/informers"
    37  	"k8s.io/client-go/tools/cache"
    38  	"k8s.io/klog/v2"
    39  	v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
    40  	v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper"
    41  	"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
    42  	aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
    43  	aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
    44  	apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
    45  	informers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1"
    46  	"k8s.io/kube-aggregator/pkg/controllers/autoregister"
    47  
    48  	"k8s.io/kubernetes/pkg/controlplane/apiserver/options"
    49  	"k8s.io/kubernetes/pkg/controlplane/controller/crdregistration"
    50  )
    51  
    52  func CreateAggregatorConfig(
    53  	kubeAPIServerConfig genericapiserver.Config,
    54  	commandOptions options.CompletedOptions,
    55  	externalInformers kubeexternalinformers.SharedInformerFactory,
    56  	serviceResolver aggregatorapiserver.ServiceResolver,
    57  	proxyTransport *http.Transport,
    58  	peerProxy utilpeerproxy.Interface,
    59  	pluginInitializers []admission.PluginInitializer,
    60  ) (*aggregatorapiserver.Config, error) {
    61  	// make a shallow copy to let us twiddle a few things
    62  	// most of the config actually remains the same.  We only need to mess with a couple items related to the particulars of the aggregator
    63  	genericConfig := kubeAPIServerConfig
    64  	genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
    65  	genericConfig.RESTOptionsGetter = nil
    66  	// prevent generic API server from installing the OpenAPI handler. Aggregator server
    67  	// has its own customized OpenAPI handler.
    68  	genericConfig.SkipOpenAPIInstallation = true
    69  
    70  	if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) &&
    71  		utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
    72  		// Add StorageVersionPrecondition handler to aggregator-apiserver.
    73  		// The handler will block write requests to built-in resources until the
    74  		// target resources' storage versions are up-to-date.
    75  		genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition
    76  	}
    77  
    78  	if peerProxy != nil {
    79  		originalHandlerChainBuilder := genericConfig.BuildHandlerChainFunc
    80  		genericConfig.BuildHandlerChainFunc = func(apiHandler http.Handler, c *genericapiserver.Config) http.Handler {
    81  			// Add peer proxy handler to aggregator-apiserver.
    82  			// wrap the peer proxy handler first.
    83  			apiHandler = peerProxy.WrapHandler(apiHandler)
    84  			return originalHandlerChainBuilder(apiHandler, c)
    85  		}
    86  	}
    87  
    88  	// copy the etcd options so we don't mutate originals.
    89  	// we assume that the etcd options have been completed already.  avoid messing with anything outside
    90  	// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
    91  	etcdOptions := *commandOptions.Etcd
    92  	etcdOptions.StorageConfig.Codec = aggregatorscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion)
    93  	etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(v1.SchemeGroupVersion, schema.GroupKind{Group: v1beta1.GroupName})
    94  	etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
    95  	if err := etcdOptions.ApplyTo(&genericConfig); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// override MergedResourceConfig with aggregator defaults and registry
   100  	if err := commandOptions.APIEnablement.ApplyTo(
   101  		&genericConfig,
   102  		aggregatorapiserver.DefaultAPIResourceConfigSource(),
   103  		aggregatorscheme.Scheme); err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	aggregatorConfig := &aggregatorapiserver.Config{
   108  		GenericConfig: &genericapiserver.RecommendedConfig{
   109  			Config:                genericConfig,
   110  			SharedInformerFactory: externalInformers,
   111  		},
   112  		ExtraConfig: aggregatorapiserver.ExtraConfig{
   113  			ProxyClientCertFile:       commandOptions.ProxyClientCertFile,
   114  			ProxyClientKeyFile:        commandOptions.ProxyClientKeyFile,
   115  			PeerAdvertiseAddress:      commandOptions.PeerAdvertiseAddress,
   116  			ServiceResolver:           serviceResolver,
   117  			ProxyTransport:            proxyTransport,
   118  			RejectForwardingRedirects: commandOptions.AggregatorRejectForwardingRedirects,
   119  		},
   120  	}
   121  
   122  	// we need to clear the poststarthooks so we don't add them multiple times to all the servers (that fails)
   123  	aggregatorConfig.GenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
   124  
   125  	return aggregatorConfig, nil
   126  }
   127  
   128  func CreateAggregatorServer(aggregatorConfig aggregatorapiserver.CompletedConfig, delegateAPIServer genericapiserver.DelegationTarget, crds apiextensionsinformers.CustomResourceDefinitionInformer, crdAPIEnabled bool, apiVersionPriorities map[schema.GroupVersion]APIServicePriority) (*aggregatorapiserver.APIAggregator, error) {
   129  	aggregatorServer, err := aggregatorConfig.NewWithDelegate(delegateAPIServer)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	// create controllers for auto-registration
   135  	apiRegistrationClient, err := apiregistrationclient.NewForConfig(aggregatorConfig.GenericConfig.LoopbackClientConfig)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	autoRegistrationController := autoregister.NewAutoRegisterController(aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), apiRegistrationClient)
   140  	apiServices := apiServicesToRegister(delegateAPIServer, autoRegistrationController, apiVersionPriorities)
   141  
   142  	type controller interface {
   143  		Run(workers int, stopCh <-chan struct{})
   144  		WaitForInitialSync()
   145  	}
   146  	var crdRegistrationController controller
   147  	if crdAPIEnabled {
   148  		crdRegistrationController = crdregistration.NewCRDRegistrationController(
   149  			crds,
   150  			autoRegistrationController)
   151  	}
   152  
   153  	// Imbue all builtin group-priorities onto the aggregated discovery
   154  	if aggregatorConfig.GenericConfig.AggregatedDiscoveryGroupManager != nil {
   155  		for gv, entry := range apiVersionPriorities {
   156  			aggregatorConfig.GenericConfig.AggregatedDiscoveryGroupManager.SetGroupVersionPriority(metav1.GroupVersion(gv), int(entry.Group), int(entry.Version))
   157  		}
   158  	}
   159  
   160  	err = aggregatorServer.GenericAPIServer.AddPostStartHook("kube-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error {
   161  		if crdAPIEnabled {
   162  			go crdRegistrationController.Run(5, context.Done())
   163  		}
   164  		go func() {
   165  			// let the CRD controller process the initial set of CRDs before starting the autoregistration controller.
   166  			// this prevents the autoregistration controller's initial sync from deleting APIServices for CRDs that still exist.
   167  			// we only need to do this if CRDs are enabled on this server.  We can't use discovery because we are the source for discovery.
   168  			if crdAPIEnabled {
   169  				klog.Infof("waiting for initial CRD sync...")
   170  				crdRegistrationController.WaitForInitialSync()
   171  				klog.Infof("initial CRD sync complete...")
   172  			} else {
   173  				klog.Infof("CRD API not enabled, starting APIService registration without waiting for initial CRD sync")
   174  			}
   175  			autoRegistrationController.Run(5, context.Done())
   176  		}()
   177  		return nil
   178  	})
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks(
   184  		makeAPIServiceAvailableHealthCheck(
   185  			"autoregister-completion",
   186  			apiServices,
   187  			aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(),
   188  		),
   189  	)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	return aggregatorServer, nil
   195  }
   196  
   197  func makeAPIService(gv schema.GroupVersion, apiVersionPriorities map[schema.GroupVersion]APIServicePriority) *v1.APIService {
   198  	apiServicePriority, ok := apiVersionPriorities[gv]
   199  	if !ok {
   200  		// if we aren't found, then we shouldn't register ourselves because it could result in a CRD group version
   201  		// being permanently stuck in the APIServices list.
   202  		klog.Infof("Skipping APIService creation for %v", gv)
   203  		return nil
   204  	}
   205  	return &v1.APIService{
   206  		ObjectMeta: metav1.ObjectMeta{Name: gv.Version + "." + gv.Group},
   207  		Spec: v1.APIServiceSpec{
   208  			Group:                gv.Group,
   209  			Version:              gv.Version,
   210  			GroupPriorityMinimum: apiServicePriority.Group,
   211  			VersionPriority:      apiServicePriority.Version,
   212  		},
   213  	}
   214  }
   215  
   216  // makeAPIServiceAvailableHealthCheck returns a healthz check that returns healthy
   217  // once all of the specified services have been observed to be available at least once.
   218  func makeAPIServiceAvailableHealthCheck(name string, apiServices []*v1.APIService, apiServiceInformer informers.APIServiceInformer) healthz.HealthChecker {
   219  	// Track the auto-registered API services that have not been observed to be available yet
   220  	pendingServiceNamesLock := &sync.RWMutex{}
   221  	pendingServiceNames := sets.NewString()
   222  	for _, service := range apiServices {
   223  		pendingServiceNames.Insert(service.Name)
   224  	}
   225  
   226  	// When an APIService in the list is seen as available, remove it from the pending list
   227  	handleAPIServiceChange := func(service *v1.APIService) {
   228  		pendingServiceNamesLock.Lock()
   229  		defer pendingServiceNamesLock.Unlock()
   230  		if !pendingServiceNames.Has(service.Name) {
   231  			return
   232  		}
   233  		if v1helper.IsAPIServiceConditionTrue(service, v1.Available) {
   234  			pendingServiceNames.Delete(service.Name)
   235  		}
   236  	}
   237  
   238  	// Watch add/update events for APIServices
   239  	apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ //nolint:errcheck // no way to return error
   240  		AddFunc:    func(obj interface{}) { handleAPIServiceChange(obj.(*v1.APIService)) },
   241  		UpdateFunc: func(old, new interface{}) { handleAPIServiceChange(new.(*v1.APIService)) },
   242  	})
   243  
   244  	// Don't return healthy until the pending list is empty
   245  	return healthz.NamedCheck(name, func(r *http.Request) error {
   246  		pendingServiceNamesLock.RLock()
   247  		defer pendingServiceNamesLock.RUnlock()
   248  		if pendingServiceNames.Len() > 0 {
   249  			return fmt.Errorf("missing APIService: %v", pendingServiceNames.List())
   250  		}
   251  		return nil
   252  	})
   253  }
   254  
   255  // APIServicePriority defines group priority that is used in discovery. This controls
   256  // group position in the kubectl output.
   257  type APIServicePriority struct {
   258  	// Group indicates the order of the group relative to other groups.
   259  	Group int32
   260  	// Version indicates the relative order of the Version inside of its group.
   261  	Version int32
   262  }
   263  
   264  // DefaultGenericAPIServicePriorities returns the APIService priorities for generic APIs
   265  func DefaultGenericAPIServicePriorities() map[schema.GroupVersion]APIServicePriority {
   266  	// The proper way to resolve this letting the aggregator know the desired group and version-within-group order of the underlying servers
   267  	// is to refactor the genericapiserver.DelegationTarget to include a list of priorities based on which APIs were installed.
   268  	// This requires the APIGroupInfo struct to evolve and include the concept of priorities and to avoid mistakes, the core storage map there needs to be updated.
   269  	// That ripples out every bit as far as you'd expect, so for 1.7 we'll include the list here instead of being built up during storage.
   270  	return map[schema.GroupVersion]APIServicePriority{
   271  		{Group: "", Version: "v1"}: {Group: 18000, Version: 1},
   272  		// to my knowledge, nothing below here collides
   273  		{Group: "events.k8s.io", Version: "v1"}:                      {Group: 17750, Version: 15},
   274  		{Group: "events.k8s.io", Version: "v1beta1"}:                 {Group: 17750, Version: 5},
   275  		{Group: "authentication.k8s.io", Version: "v1"}:              {Group: 17700, Version: 15},
   276  		{Group: "authentication.k8s.io", Version: "v1beta1"}:         {Group: 17700, Version: 9},
   277  		{Group: "authentication.k8s.io", Version: "v1alpha1"}:        {Group: 17700, Version: 1},
   278  		{Group: "authorization.k8s.io", Version: "v1"}:               {Group: 17600, Version: 15},
   279  		{Group: "certificates.k8s.io", Version: "v1"}:                {Group: 17300, Version: 15},
   280  		{Group: "certificates.k8s.io", Version: "v1alpha1"}:          {Group: 17300, Version: 1},
   281  		{Group: "rbac.authorization.k8s.io", Version: "v1"}:          {Group: 17000, Version: 15},
   282  		{Group: "apiextensions.k8s.io", Version: "v1"}:               {Group: 16700, Version: 15},
   283  		{Group: "admissionregistration.k8s.io", Version: "v1"}:       {Group: 16700, Version: 15},
   284  		{Group: "admissionregistration.k8s.io", Version: "v1beta1"}:  {Group: 16700, Version: 12},
   285  		{Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {Group: 16700, Version: 9},
   286  		{Group: "coordination.k8s.io", Version: "v1"}:                {Group: 16500, Version: 15},
   287  		{Group: "discovery.k8s.io", Version: "v1"}:                   {Group: 16200, Version: 15},
   288  		{Group: "discovery.k8s.io", Version: "v1beta1"}:              {Group: 16200, Version: 12},
   289  		{Group: "flowcontrol.apiserver.k8s.io", Version: "v1"}:       {Group: 16100, Version: 21},
   290  		{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta3"}:  {Group: 16100, Version: 18},
   291  		{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2"}:  {Group: 16100, Version: 15},
   292  		{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta1"}:  {Group: 16100, Version: 12},
   293  		{Group: "flowcontrol.apiserver.k8s.io", Version: "v1alpha1"}: {Group: 16100, Version: 9},
   294  		{Group: "internal.apiserver.k8s.io", Version: "v1alpha1"}:    {Group: 16000, Version: 9},
   295  		{Group: "resource.k8s.io", Version: "v1alpha2"}:              {Group: 15900, Version: 9},
   296  		{Group: "storagemigration.k8s.io", Version: "v1alpha1"}:      {Group: 15800, Version: 9},
   297  		// Append a new group to the end of the list if unsure.
   298  		// You can use min(existing group)-100 as the initial value for a group.
   299  		// Version can be set to 9 (to have space around) for a new group.
   300  	}
   301  }
   302  
   303  func apiServicesToRegister(delegateAPIServer genericapiserver.DelegationTarget, registration autoregister.AutoAPIServiceRegistration, apiVersionPriorities map[schema.GroupVersion]APIServicePriority) []*v1.APIService {
   304  	apiServices := []*v1.APIService{}
   305  
   306  	for _, curr := range delegateAPIServer.ListedPaths() {
   307  		if curr == "/api/v1" {
   308  			apiService := makeAPIService(schema.GroupVersion{Group: "", Version: "v1"}, apiVersionPriorities)
   309  			registration.AddAPIServiceToSyncOnStart(apiService)
   310  			apiServices = append(apiServices, apiService)
   311  			continue
   312  		}
   313  
   314  		if !strings.HasPrefix(curr, "/apis/") {
   315  			continue
   316  		}
   317  		// this comes back in a list that looks like /apis/rbac.authorization.k8s.io/v1alpha1
   318  		tokens := strings.Split(curr, "/")
   319  		if len(tokens) != 4 {
   320  			continue
   321  		}
   322  
   323  		apiService := makeAPIService(schema.GroupVersion{Group: tokens[2], Version: tokens[3]}, apiVersionPriorities)
   324  		if apiService == nil {
   325  			continue
   326  		}
   327  		registration.AddAPIServiceToSyncOnStart(apiService)
   328  		apiServices = append(apiServices, apiService)
   329  	}
   330  
   331  	return apiServices
   332  }