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 }