Add impl for uvip
This commit is contained in:
		@@ -37,6 +37,7 @@ import (
 | 
				
			|||||||
	genericapiserver "k8s.io/apiserver/pkg/server"
 | 
						genericapiserver "k8s.io/apiserver/pkg/server"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/healthz"
 | 
						"k8s.io/apiserver/pkg/server/healthz"
 | 
				
			||||||
	utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
 | 
						utilpeerproxy "k8s.io/apiserver/pkg/util/peerproxy"
 | 
				
			||||||
	kubeexternalinformers "k8s.io/client-go/informers"
 | 
						kubeexternalinformers "k8s.io/client-go/informers"
 | 
				
			||||||
	"k8s.io/client-go/tools/cache"
 | 
						"k8s.io/client-go/tools/cache"
 | 
				
			||||||
	v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
 | 
						v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
 | 
				
			||||||
@@ -57,6 +58,7 @@ func createAggregatorConfig(
 | 
				
			|||||||
	externalInformers kubeexternalinformers.SharedInformerFactory,
 | 
						externalInformers kubeexternalinformers.SharedInformerFactory,
 | 
				
			||||||
	serviceResolver aggregatorapiserver.ServiceResolver,
 | 
						serviceResolver aggregatorapiserver.ServiceResolver,
 | 
				
			||||||
	proxyTransport *http.Transport,
 | 
						proxyTransport *http.Transport,
 | 
				
			||||||
 | 
						peerProxy utilpeerproxy.Interface,
 | 
				
			||||||
	pluginInitializers []admission.PluginInitializer,
 | 
						pluginInitializers []admission.PluginInitializer,
 | 
				
			||||||
) (*aggregatorapiserver.Config, error) {
 | 
					) (*aggregatorapiserver.Config, error) {
 | 
				
			||||||
	// make a shallow copy to let us twiddle a few things
 | 
						// make a shallow copy to let us twiddle a few things
 | 
				
			||||||
@@ -76,6 +78,16 @@ func createAggregatorConfig(
 | 
				
			|||||||
		genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition
 | 
							genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if peerProxy != nil {
 | 
				
			||||||
 | 
							originalHandlerChainBuilder := genericConfig.BuildHandlerChainFunc
 | 
				
			||||||
 | 
							genericConfig.BuildHandlerChainFunc = func(apiHandler http.Handler, c *genericapiserver.Config) http.Handler {
 | 
				
			||||||
 | 
								// Add peer proxy handler to aggregator-apiserver.
 | 
				
			||||||
 | 
								// wrap the peer proxy handler first.
 | 
				
			||||||
 | 
								apiHandler = peerProxy.WrapHandler(apiHandler)
 | 
				
			||||||
 | 
								return originalHandlerChainBuilder(apiHandler, c)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// copy the etcd options so we don't mutate originals.
 | 
						// copy the etcd options so we don't mutate originals.
 | 
				
			||||||
	// we assume that the etcd options have been completed already.  avoid messing with anything outside
 | 
						// we assume that the etcd options have been completed already.  avoid messing with anything outside
 | 
				
			||||||
	// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
 | 
						// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
 | 
				
			||||||
@@ -104,6 +116,8 @@ func createAggregatorConfig(
 | 
				
			|||||||
		ExtraConfig: aggregatorapiserver.ExtraConfig{
 | 
							ExtraConfig: aggregatorapiserver.ExtraConfig{
 | 
				
			||||||
			ProxyClientCertFile:       commandOptions.ProxyClientCertFile,
 | 
								ProxyClientCertFile:       commandOptions.ProxyClientCertFile,
 | 
				
			||||||
			ProxyClientKeyFile:        commandOptions.ProxyClientKeyFile,
 | 
								ProxyClientKeyFile:        commandOptions.ProxyClientKeyFile,
 | 
				
			||||||
 | 
								PeerCAFile:                commandOptions.PeerCAFile,
 | 
				
			||||||
 | 
								PeerAdvertiseAddress:      commandOptions.PeerAdvertiseAddress,
 | 
				
			||||||
			ServiceResolver:           serviceResolver,
 | 
								ServiceResolver:           serviceResolver,
 | 
				
			||||||
			ProxyTransport:            proxyTransport,
 | 
								ProxyTransport:            proxyTransport,
 | 
				
			||||||
			RejectForwardingRedirects: commandOptions.AggregatorRejectForwardingRedirects,
 | 
								RejectForwardingRedirects: commandOptions.AggregatorRejectForwardingRedirects,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -84,7 +84,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	c.ApiExtensions = apiExtensions
 | 
						c.ApiExtensions = apiExtensions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	aggregator, err := createAggregatorConfig(*controlPlane.GenericConfig, opts.CompletedOptions, controlPlane.ExtraConfig.VersionedInformers, serviceResolver, controlPlane.ExtraConfig.ProxyTransport, pluginInitializer)
 | 
						aggregator, err := createAggregatorConfig(*controlPlane.GenericConfig, opts.CompletedOptions, controlPlane.ExtraConfig.VersionedInformers, serviceResolver, controlPlane.ExtraConfig.ProxyTransport, controlPlane.ExtraConfig.PeerProxy, pluginInitializer)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,6 +57,7 @@ import (
 | 
				
			|||||||
	"k8s.io/klog/v2"
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
	aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
 | 
						aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
 | 
				
			||||||
	aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
 | 
						aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/features"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
 | 
						"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/api/legacyscheme"
 | 
						"k8s.io/kubernetes/pkg/api/legacyscheme"
 | 
				
			||||||
@@ -258,6 +259,21 @@ func CreateKubeAPIServerConfig(opts options.CompletedOptions) (
 | 
				
			|||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
 | 
				
			||||||
 | 
							config.ExtraConfig.PeerEndpointLeaseReconciler, err = controlplaneapiserver.CreatePeerEndpointLeaseReconciler(*genericConfig, storageFactory)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, nil, nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// build peer proxy config only if peer ca file exists
 | 
				
			||||||
 | 
							if opts.PeerCAFile != "" {
 | 
				
			||||||
 | 
								config.ExtraConfig.PeerProxy, err = controlplaneapiserver.BuildPeerProxy(versionedInformers, genericConfig.StorageVersionManager, opts.ProxyClientCertFile,
 | 
				
			||||||
 | 
									opts.ProxyClientKeyFile, opts.PeerCAFile, opts.PeerAdvertiseAddress, genericConfig.APIServerID, config.ExtraConfig.PeerEndpointLeaseReconciler, config.GenericConfig.Serializer)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, nil, nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	clientCAProvider, err := opts.Authentication.ClientCert.GetClientCAContentProvider()
 | 
						clientCAProvider, err := opts.Authentication.ClientCert.GetClientCAContentProvider()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, nil, nil, err
 | 
							return nil, nil, nil, err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,19 +28,25 @@ import (
 | 
				
			|||||||
	"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
 | 
						"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
 | 
				
			||||||
	openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
 | 
						openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
 | 
				
			||||||
	genericfeatures "k8s.io/apiserver/pkg/features"
 | 
						genericfeatures "k8s.io/apiserver/pkg/features"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
	genericapiserver "k8s.io/apiserver/pkg/server"
 | 
						genericapiserver "k8s.io/apiserver/pkg/server"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/egressselector"
 | 
						"k8s.io/apiserver/pkg/server/egressselector"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/filters"
 | 
						"k8s.io/apiserver/pkg/server/filters"
 | 
				
			||||||
	serverstorage "k8s.io/apiserver/pkg/server/storage"
 | 
						serverstorage "k8s.io/apiserver/pkg/server/storage"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storageversion"
 | 
				
			||||||
	utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
	utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
 | 
						utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/util/openapi"
 | 
						"k8s.io/apiserver/pkg/util/openapi"
 | 
				
			||||||
 | 
						utilpeerproxy "k8s.io/apiserver/pkg/util/peerproxy"
 | 
				
			||||||
	clientgoinformers "k8s.io/client-go/informers"
 | 
						clientgoinformers "k8s.io/client-go/informers"
 | 
				
			||||||
	clientgoclientset "k8s.io/client-go/kubernetes"
 | 
						clientgoclientset "k8s.io/client-go/kubernetes"
 | 
				
			||||||
 | 
						"k8s.io/client-go/transport"
 | 
				
			||||||
	"k8s.io/component-base/version"
 | 
						"k8s.io/component-base/version"
 | 
				
			||||||
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
	openapicommon "k8s.io/kube-openapi/pkg/common"
 | 
						openapicommon "k8s.io/kube-openapi/pkg/common"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/api/legacyscheme"
 | 
						"k8s.io/kubernetes/pkg/api/legacyscheme"
 | 
				
			||||||
 | 
						api "k8s.io/kubernetes/pkg/apis/core"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/controlplane"
 | 
						"k8s.io/kubernetes/pkg/controlplane"
 | 
				
			||||||
	controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver/options"
 | 
						controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver/options"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubeapiserver"
 | 
						"k8s.io/kubernetes/pkg/kubeapiserver"
 | 
				
			||||||
@@ -193,3 +199,50 @@ func BuildPriorityAndFairness(s controlplaneapiserver.CompletedOptions, extclien
 | 
				
			|||||||
		s.GenericServerRunOptions.RequestTimeout/4,
 | 
							s.GenericServerRunOptions.RequestTimeout/4,
 | 
				
			||||||
	), nil
 | 
						), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePeerEndpointLeaseReconciler creates a apiserver endpoint lease reconciliation loop
 | 
				
			||||||
 | 
					// The peer endpoint leases are used to find network locations of apiservers for peer proxy
 | 
				
			||||||
 | 
					func CreatePeerEndpointLeaseReconciler(c genericapiserver.Config, storageFactory serverstorage.StorageFactory) (reconcilers.PeerEndpointLeaseReconciler, error) {
 | 
				
			||||||
 | 
						ttl := controlplane.DefaultEndpointReconcilerTTL
 | 
				
			||||||
 | 
						config, err := storageFactory.NewConfig(api.Resource("apiServerPeerIPInfo"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error creating storage factory config: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						reconciler, err := reconcilers.NewPeerEndpointLeaseReconciler(config, "/peerserverleases/", ttl)
 | 
				
			||||||
 | 
						return reconciler, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BuildPeerProxy(versionedInformer clientgoinformers.SharedInformerFactory, svm storageversion.Manager,
 | 
				
			||||||
 | 
						proxyClientCertFile string, proxyClientKeyFile string, peerCAFile string, peerAdvertiseAddress reconcilers.PeerAdvertiseAddress,
 | 
				
			||||||
 | 
						apiServerID string, reconciler reconcilers.PeerEndpointLeaseReconciler, serializer runtime.NegotiatedSerializer) (utilpeerproxy.Interface, error) {
 | 
				
			||||||
 | 
						if proxyClientCertFile == "" {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error building peer proxy handler, proxy-cert-file not specified")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if proxyClientKeyFile == "" {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error building peer proxy handler, proxy-key-file not specified")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// create proxy client config
 | 
				
			||||||
 | 
						clientConfig := &transport.Config{
 | 
				
			||||||
 | 
							TLS: transport.TLSConfig{
 | 
				
			||||||
 | 
								Insecure:   false,
 | 
				
			||||||
 | 
								CertFile:   proxyClientCertFile,
 | 
				
			||||||
 | 
								KeyFile:    proxyClientKeyFile,
 | 
				
			||||||
 | 
								CAFile:     peerCAFile,
 | 
				
			||||||
 | 
								ServerName: "kubernetes.default.svc",
 | 
				
			||||||
 | 
							}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// build proxy transport
 | 
				
			||||||
 | 
						proxyRoundTripper, transportBuildingError := transport.New(clientConfig)
 | 
				
			||||||
 | 
						if transportBuildingError != nil {
 | 
				
			||||||
 | 
							klog.Error(transportBuildingError.Error())
 | 
				
			||||||
 | 
							return nil, transportBuildingError
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return utilpeerproxy.NewPeerProxyHandler(
 | 
				
			||||||
 | 
							versionedInformer,
 | 
				
			||||||
 | 
							svm,
 | 
				
			||||||
 | 
							proxyRoundTripper,
 | 
				
			||||||
 | 
							apiServerID,
 | 
				
			||||||
 | 
							reconciler,
 | 
				
			||||||
 | 
							serializer,
 | 
				
			||||||
 | 
						), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peerreconcilers "k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
	genericoptions "k8s.io/apiserver/pkg/server/options"
 | 
						genericoptions "k8s.io/apiserver/pkg/server/options"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/storage/storagebackend"
 | 
						"k8s.io/apiserver/pkg/storage/storagebackend"
 | 
				
			||||||
	"k8s.io/client-go/util/keyutil"
 | 
						"k8s.io/client-go/util/keyutil"
 | 
				
			||||||
@@ -63,6 +64,16 @@ type Options struct {
 | 
				
			|||||||
	ProxyClientCertFile string
 | 
						ProxyClientCertFile string
 | 
				
			||||||
	ProxyClientKeyFile  string
 | 
						ProxyClientKeyFile  string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerCAFile is the ca bundle used by this kube-apiserver to verify peer apiservers'
 | 
				
			||||||
 | 
						// serving certs when routing a request to the peer in the case the request can not be served
 | 
				
			||||||
 | 
						// locally due to version skew.
 | 
				
			||||||
 | 
						PeerCAFile string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerAdvertiseAddress is the IP for this kube-apiserver which is used by peer apiservers to route a request
 | 
				
			||||||
 | 
						// to this apiserver. This happens in cases where the peer is not able to serve the request due to
 | 
				
			||||||
 | 
						// version skew.
 | 
				
			||||||
 | 
						PeerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	EnableAggregatorRouting             bool
 | 
						EnableAggregatorRouting             bool
 | 
				
			||||||
	AggregatorRejectForwardingRedirects bool
 | 
						AggregatorRejectForwardingRedirects bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,6 +165,20 @@ func (s *Options) AddFlags(fss *cliflag.NamedFlagSets) {
 | 
				
			|||||||
		"when it must call out during a request. This includes proxying requests to a user "+
 | 
							"when it must call out during a request. This includes proxying requests to a user "+
 | 
				
			||||||
		"api-server and calling out to webhook admission plugins.")
 | 
							"api-server and calling out to webhook admission plugins.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fs.StringVar(&s.PeerCAFile, "peer-ca-file", s.PeerCAFile,
 | 
				
			||||||
 | 
							"If set and the UnknownVersionInteroperabilityProxy feature gate is enabled, this file will be used to verify serving certificates of peer kube-apiservers. "+
 | 
				
			||||||
 | 
								"This flag is only used in clusters configured with multiple kube-apiservers for high availability.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fs.StringVar(&s.PeerAdvertiseAddress.PeerAdvertiseIP, "peer-advertise-ip", s.PeerAdvertiseAddress.PeerAdvertiseIP,
 | 
				
			||||||
 | 
							"If set and the UnknownVersionInteroperabilityProxy feature gate is enabled, this IP will be used by peer kube-apiservers to proxy requests to this kube-apiserver "+
 | 
				
			||||||
 | 
								"when the request cannot be handled by the peer due to version skew between the kube-apiservers. "+
 | 
				
			||||||
 | 
								"This flag is only used in clusters configured with multiple kube-apiservers for high availability. ")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fs.StringVar(&s.PeerAdvertiseAddress.PeerAdvertisePort, "peer-advertise-port", s.PeerAdvertiseAddress.PeerAdvertisePort,
 | 
				
			||||||
 | 
							"If set and the UnknownVersionInteroperabilityProxy feature gate is enabled, this port will be used by peer kube-apiservers to proxy requests to this kube-apiserver "+
 | 
				
			||||||
 | 
								"when the request cannot be handled by the peer due to version skew between the kube-apiservers. "+
 | 
				
			||||||
 | 
								"This flag is only used in clusters configured with multiple kube-apiservers for high availability. ")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fs.BoolVar(&s.EnableAggregatorRouting, "enable-aggregator-routing", s.EnableAggregatorRouting,
 | 
						fs.BoolVar(&s.EnableAggregatorRouting, "enable-aggregator-routing", s.EnableAggregatorRouting,
 | 
				
			||||||
		"Turns on aggregator routing requests to endpoints IP rather than cluster IP.")
 | 
							"Turns on aggregator routing requests to endpoints IP rather than cluster IP.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,7 @@ import (
 | 
				
			|||||||
	genericfeatures "k8s.io/apiserver/pkg/features"
 | 
						genericfeatures "k8s.io/apiserver/pkg/features"
 | 
				
			||||||
	utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
	aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
 | 
						aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/features"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/api/legacyscheme"
 | 
						"k8s.io/kubernetes/pkg/api/legacyscheme"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -69,6 +70,32 @@ func validateAPIPriorityAndFairness(options *Options) []error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func validateUnknownVersionInteroperabilityProxyFeature() []error {
 | 
				
			||||||
 | 
						if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
 | 
				
			||||||
 | 
							if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return []error{fmt.Errorf("UnknownVersionInteroperabilityProxy feature requires StorageVersionAPI feature flag to be enabled")}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func validateUnknownVersionInteroperabilityProxyFlags(options *Options) []error {
 | 
				
			||||||
 | 
						err := []error{}
 | 
				
			||||||
 | 
						if !utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
 | 
				
			||||||
 | 
							if options.PeerCAFile != "" {
 | 
				
			||||||
 | 
								err = append(err, fmt.Errorf("--peer-ca-file requires UnknownVersionInteroperabilityProxy feature to be turned on"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if options.PeerAdvertiseAddress.PeerAdvertiseIP != "" {
 | 
				
			||||||
 | 
								err = append(err, fmt.Errorf("--peer-advertise-ip requires UnknownVersionInteroperabilityProxy feature to be turned on"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if options.PeerAdvertiseAddress.PeerAdvertisePort != "" {
 | 
				
			||||||
 | 
								err = append(err, fmt.Errorf("--peer-advertise-port requires UnknownVersionInteroperabilityProxy feature to be turned on"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Validate checks Options and return a slice of found errs.
 | 
					// Validate checks Options and return a slice of found errs.
 | 
				
			||||||
func (s *Options) Validate() []error {
 | 
					func (s *Options) Validate() []error {
 | 
				
			||||||
	var errs []error
 | 
						var errs []error
 | 
				
			||||||
@@ -83,6 +110,8 @@ func (s *Options) Validate() []error {
 | 
				
			|||||||
	errs = append(errs, s.APIEnablement.Validate(legacyscheme.Scheme, apiextensionsapiserver.Scheme, aggregatorscheme.Scheme)...)
 | 
						errs = append(errs, s.APIEnablement.Validate(legacyscheme.Scheme, apiextensionsapiserver.Scheme, aggregatorscheme.Scheme)...)
 | 
				
			||||||
	errs = append(errs, validateTokenRequest(s)...)
 | 
						errs = append(errs, validateTokenRequest(s)...)
 | 
				
			||||||
	errs = append(errs, s.Metrics.Validate()...)
 | 
						errs = append(errs, s.Metrics.Validate()...)
 | 
				
			||||||
 | 
						errs = append(errs, validateUnknownVersionInteroperabilityProxyFeature()...)
 | 
				
			||||||
 | 
						errs = append(errs, validateUnknownVersionInteroperabilityProxyFlags(s)...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return errs
 | 
						return errs
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,8 +22,13 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	kubeapiserveradmission "k8s.io/apiserver/pkg/admission"
 | 
						kubeapiserveradmission "k8s.io/apiserver/pkg/admission"
 | 
				
			||||||
	genericoptions "k8s.io/apiserver/pkg/server/options"
 | 
						genericoptions "k8s.io/apiserver/pkg/server/options"
 | 
				
			||||||
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
 | 
						"k8s.io/component-base/featuregate"
 | 
				
			||||||
	basemetrics "k8s.io/component-base/metrics"
 | 
						basemetrics "k8s.io/component-base/metrics"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/features"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peerreconcilers "k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
 | 
						featuregatetesting "k8s.io/component-base/featuregate/testing"
 | 
				
			||||||
	kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
 | 
						kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -80,6 +85,83 @@ func TestValidateAPIPriorityAndFairness(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestValidateUnknownVersionInteroperabilityProxy(t *testing.T) {
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name                 string
 | 
				
			||||||
 | 
							featureEnabled       bool
 | 
				
			||||||
 | 
							errShouldContain     string
 | 
				
			||||||
 | 
							peerCAFile           string
 | 
				
			||||||
 | 
							peerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:             "feature disabled but peerCAFile set",
 | 
				
			||||||
 | 
								featureEnabled:   false,
 | 
				
			||||||
 | 
								peerCAFile:       "foo",
 | 
				
			||||||
 | 
								errShouldContain: "--peer-ca-file requires UnknownVersionInteroperabilityProxy feature to be turned on",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:                 "feature disabled but peerAdvertiseIP set",
 | 
				
			||||||
 | 
								featureEnabled:       false,
 | 
				
			||||||
 | 
								peerAdvertiseAddress: peerreconcilers.PeerAdvertiseAddress{PeerAdvertiseIP: "1.2.3.4"},
 | 
				
			||||||
 | 
								errShouldContain:     "--peer-advertise-ip requires UnknownVersionInteroperabilityProxy feature to be turned on",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:                 "feature disabled but peerAdvertisePort set",
 | 
				
			||||||
 | 
								featureEnabled:       false,
 | 
				
			||||||
 | 
								peerAdvertiseAddress: peerreconcilers.PeerAdvertiseAddress{PeerAdvertisePort: "1"},
 | 
				
			||||||
 | 
								errShouldContain:     "--peer-advertise-port requires UnknownVersionInteroperabilityProxy feature to be turned on",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							t.Run(test.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								options := &Options{
 | 
				
			||||||
 | 
									PeerCAFile:           test.peerCAFile,
 | 
				
			||||||
 | 
									PeerAdvertiseAddress: test.peerAdvertiseAddress,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if test.featureEnabled {
 | 
				
			||||||
 | 
									defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.UnknownVersionInteroperabilityProxy, true)()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var errMessageGot string
 | 
				
			||||||
 | 
								if errs := validateUnknownVersionInteroperabilityProxyFlags(options); len(errs) > 0 {
 | 
				
			||||||
 | 
									errMessageGot = errs[0].Error()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !strings.Contains(errMessageGot, test.errShouldContain) {
 | 
				
			||||||
 | 
									t.Errorf("Expected error message to contain: %q, but got: %q", test.errShouldContain, errMessageGot)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestValidateUnknownVersionInteroperabilityProxyFeature(t *testing.T) {
 | 
				
			||||||
 | 
						const conflict = "UnknownVersionInteroperabilityProxy feature requires StorageVersionAPI feature flag to be enabled"
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name            string
 | 
				
			||||||
 | 
							featuresEnabled []featuregate.Feature
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:            "enabled: UnknownVersionInteroperabilityProxy, disabled: StorageVersionAPI",
 | 
				
			||||||
 | 
								featuresEnabled: []featuregate.Feature{features.UnknownVersionInteroperabilityProxy},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							t.Run(test.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								for _, feature := range test.featuresEnabled {
 | 
				
			||||||
 | 
									defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, feature, true)()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var errMessageGot string
 | 
				
			||||||
 | 
								if errs := validateUnknownVersionInteroperabilityProxyFeature(); len(errs) > 0 {
 | 
				
			||||||
 | 
									errMessageGot = errs[0].Error()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !strings.Contains(errMessageGot, conflict) {
 | 
				
			||||||
 | 
									t.Errorf("Expected error message to contain: %q, but got: %q", conflict, errMessageGot)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestValidateOptions(t *testing.T) {
 | 
					func TestValidateOptions(t *testing.T) {
 | 
				
			||||||
	testCases := []struct {
 | 
						testCases := []struct {
 | 
				
			||||||
		name         string
 | 
							name         string
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,11 +61,13 @@ import (
 | 
				
			|||||||
	"k8s.io/apimachinery/pkg/util/wait"
 | 
						"k8s.io/apimachinery/pkg/util/wait"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/endpoints/discovery"
 | 
						"k8s.io/apiserver/pkg/endpoints/discovery"
 | 
				
			||||||
	apiserverfeatures "k8s.io/apiserver/pkg/features"
 | 
						apiserverfeatures "k8s.io/apiserver/pkg/features"
 | 
				
			||||||
 | 
						peerreconcilers "k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/registry/generic"
 | 
						"k8s.io/apiserver/pkg/registry/generic"
 | 
				
			||||||
	genericapiserver "k8s.io/apiserver/pkg/server"
 | 
						genericapiserver "k8s.io/apiserver/pkg/server"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/dynamiccertificates"
 | 
						"k8s.io/apiserver/pkg/server/dynamiccertificates"
 | 
				
			||||||
	serverstorage "k8s.io/apiserver/pkg/server/storage"
 | 
						serverstorage "k8s.io/apiserver/pkg/server/storage"
 | 
				
			||||||
	utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
 | 
						utilpeerproxy "k8s.io/apiserver/pkg/util/peerproxy"
 | 
				
			||||||
	"k8s.io/client-go/informers"
 | 
						"k8s.io/client-go/informers"
 | 
				
			||||||
	"k8s.io/client-go/kubernetes"
 | 
						"k8s.io/client-go/kubernetes"
 | 
				
			||||||
	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 | 
						corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 | 
				
			||||||
@@ -83,6 +85,7 @@ import (
 | 
				
			|||||||
	"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
 | 
						"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/controlplane/controller/systemnamespaces"
 | 
						"k8s.io/kubernetes/pkg/controlplane/controller/systemnamespaces"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/controlplane/reconcilers"
 | 
						"k8s.io/kubernetes/pkg/controlplane/reconcilers"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/features"
 | 
				
			||||||
	kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
 | 
						kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
 | 
				
			||||||
	kubeletclient "k8s.io/kubernetes/pkg/kubelet/client"
 | 
						kubeletclient "k8s.io/kubernetes/pkg/kubelet/client"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/routes"
 | 
						"k8s.io/kubernetes/pkg/routes"
 | 
				
			||||||
@@ -157,6 +160,23 @@ type ExtraConfig struct {
 | 
				
			|||||||
	EnableLogsSupport bool
 | 
						EnableLogsSupport bool
 | 
				
			||||||
	ProxyTransport    *http.Transport
 | 
						ProxyTransport    *http.Transport
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerProxy, if not nil, sets proxy transport between kube-apiserver peers for requests
 | 
				
			||||||
 | 
						// that can not be served locally
 | 
				
			||||||
 | 
						PeerProxy utilpeerproxy.Interface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerEndpointLeaseReconciler updates the peer endpoint leases
 | 
				
			||||||
 | 
						PeerEndpointLeaseReconciler peerreconcilers.PeerEndpointLeaseReconciler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerCAFile is the ca bundle used by this kube-apiserver to verify peer apiservers'
 | 
				
			||||||
 | 
						// serving certs when routing a request to the peer in the case the request can not be served
 | 
				
			||||||
 | 
						// locally due to version skew.
 | 
				
			||||||
 | 
						PeerCAFile string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerAdvertiseAddress is the IP for this kube-apiserver which is used by peer apiservers to route a request
 | 
				
			||||||
 | 
						// to this apiserver. This happens in cases where the peer is not able to serve the request due to
 | 
				
			||||||
 | 
						// version skew. If unset, AdvertiseAddress/BindAddress will be used.
 | 
				
			||||||
 | 
						PeerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Values to build the IP addresses used by discovery
 | 
						// Values to build the IP addresses used by discovery
 | 
				
			||||||
	// The range of IPs to be assigned to services with type=ClusterIP or greater
 | 
						// The range of IPs to be assigned to services with type=ClusterIP or greater
 | 
				
			||||||
	ServiceIPRange net.IPNet
 | 
						ServiceIPRange net.IPNet
 | 
				
			||||||
@@ -492,6 +512,36 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
 | 
				
			|||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
 | 
				
			||||||
 | 
							peeraddress := getPeerAddress(c.ExtraConfig.PeerAdvertiseAddress, c.GenericConfig.PublicAddress, publicServicePort)
 | 
				
			||||||
 | 
							peerEndpointCtrl := peerreconcilers.New(
 | 
				
			||||||
 | 
								c.GenericConfig.APIServerID,
 | 
				
			||||||
 | 
								peeraddress,
 | 
				
			||||||
 | 
								c.ExtraConfig.PeerEndpointLeaseReconciler,
 | 
				
			||||||
 | 
								c.ExtraConfig.EndpointReconcilerConfig.Interval,
 | 
				
			||||||
 | 
								clientset)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to create peer endpoint lease controller: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							m.GenericAPIServer.AddPostStartHookOrDie("peer-endpoint-reconciler-controller",
 | 
				
			||||||
 | 
								func(hookContext genericapiserver.PostStartHookContext) error {
 | 
				
			||||||
 | 
									peerEndpointCtrl.Start(hookContext.StopCh)
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							m.GenericAPIServer.AddPreShutdownHookOrDie("peer-endpoint-reconciler-controller",
 | 
				
			||||||
 | 
								func() error {
 | 
				
			||||||
 | 
									peerEndpointCtrl.Stop()
 | 
				
			||||||
 | 
									return nil
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							// Add PostStartHooks for Unknown Version Proxy filter.
 | 
				
			||||||
 | 
							if c.ExtraConfig.PeerProxy != nil {
 | 
				
			||||||
 | 
								m.GenericAPIServer.AddPostStartHookOrDie("unknown-version-proxy-filter", func(context genericapiserver.PostStartHookContext) error {
 | 
				
			||||||
 | 
									err := c.ExtraConfig.PeerProxy.WaitForCacheSync(context.StopCh)
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	m.GenericAPIServer.AddPostStartHookOrDie("start-cluster-authentication-info-controller", func(hookContext genericapiserver.PostStartHookContext) error {
 | 
						m.GenericAPIServer.AddPostStartHookOrDie("start-cluster-authentication-info-controller", func(hookContext genericapiserver.PostStartHookContext) error {
 | 
				
			||||||
		controller := clusterauthenticationtrust.NewClusterAuthenticationTrustController(m.ClusterAuthenticationInfo, clientset)
 | 
							controller := clusterauthenticationtrust.NewClusterAuthenticationTrustController(m.ClusterAuthenticationInfo, clientset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -539,6 +589,8 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
 | 
				
			|||||||
			leaseName := m.GenericAPIServer.APIServerID
 | 
								leaseName := m.GenericAPIServer.APIServerID
 | 
				
			||||||
			holderIdentity := m.GenericAPIServer.APIServerID + "_" + string(uuid.NewUUID())
 | 
								holderIdentity := m.GenericAPIServer.APIServerID + "_" + string(uuid.NewUUID())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								peeraddress := getPeerAddress(c.ExtraConfig.PeerAdvertiseAddress, c.GenericConfig.PublicAddress, publicServicePort)
 | 
				
			||||||
 | 
								// must replace ':,[]' in [ip:port] to be able to store this as a valid label value
 | 
				
			||||||
			controller := lease.NewController(
 | 
								controller := lease.NewController(
 | 
				
			||||||
				clock.RealClock{},
 | 
									clock.RealClock{},
 | 
				
			||||||
				kubeClient,
 | 
									kubeClient,
 | 
				
			||||||
@@ -549,7 +601,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
 | 
				
			|||||||
				leaseName,
 | 
									leaseName,
 | 
				
			||||||
				metav1.NamespaceSystem,
 | 
									metav1.NamespaceSystem,
 | 
				
			||||||
				// TODO: receive identity label value as a parameter when post start hook is moved to generic apiserver.
 | 
									// TODO: receive identity label value as a parameter when post start hook is moved to generic apiserver.
 | 
				
			||||||
				labelAPIServerHeartbeatFunc(KubeAPIServer))
 | 
									labelAPIServerHeartbeatFunc(KubeAPIServer, peeraddress))
 | 
				
			||||||
			go controller.Run(ctx)
 | 
								go controller.Run(ctx)
 | 
				
			||||||
			return nil
 | 
								return nil
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
@@ -597,12 +649,16 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
 | 
				
			|||||||
	return m, nil
 | 
						return m, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func labelAPIServerHeartbeatFunc(identity string) lease.ProcessLeaseFunc {
 | 
					func labelAPIServerHeartbeatFunc(identity string, peeraddress string) lease.ProcessLeaseFunc {
 | 
				
			||||||
	return func(lease *coordinationapiv1.Lease) error {
 | 
						return func(lease *coordinationapiv1.Lease) error {
 | 
				
			||||||
		if lease.Labels == nil {
 | 
							if lease.Labels == nil {
 | 
				
			||||||
			lease.Labels = map[string]string{}
 | 
								lease.Labels = map[string]string{}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if lease.Annotations == nil {
 | 
				
			||||||
 | 
								lease.Annotations = map[string]string{}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// This label indiciates the identity of the lease object.
 | 
							// This label indiciates the identity of the lease object.
 | 
				
			||||||
		lease.Labels[IdentityLeaseComponentLabelKey] = identity
 | 
							lease.Labels[IdentityLeaseComponentLabelKey] = identity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -613,6 +669,13 @@ func labelAPIServerHeartbeatFunc(identity string) lease.ProcessLeaseFunc {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// convenience label to easily map a lease object to a specific apiserver
 | 
							// convenience label to easily map a lease object to a specific apiserver
 | 
				
			||||||
		lease.Labels[apiv1.LabelHostname] = hostname
 | 
							lease.Labels[apiv1.LabelHostname] = hostname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Include apiserver network location <ip_port> used by peers to proxy requests between kube-apiservers
 | 
				
			||||||
 | 
							if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
 | 
				
			||||||
 | 
								if peeraddress != "" {
 | 
				
			||||||
 | 
									lease.Annotations[apiv1.AnnotationPeerAdvertiseAddress] = peeraddress
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -752,3 +815,13 @@ func DefaultAPIResourceConfigSource() *serverstorage.ResourceConfig {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return ret
 | 
						return ret
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// utility function to get the apiserver address that is used by peer apiservers to proxy
 | 
				
			||||||
 | 
					// requests to this apiserver in case the peer is incapable of serving the request
 | 
				
			||||||
 | 
					func getPeerAddress(peerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress, publicAddress net.IP, publicServicePort int) string {
 | 
				
			||||||
 | 
						if peerAdvertiseAddress.PeerAdvertiseIP != "" && peerAdvertiseAddress.PeerAdvertisePort != "" {
 | 
				
			||||||
 | 
							return net.JoinHostPort(peerAdvertiseAddress.PeerAdvertiseIP, peerAdvertiseAddress.PeerAdvertisePort)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return net.JoinHostPort(publicAddress.String(), strconv.Itoa(publicServicePort))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -859,6 +859,12 @@ const (
 | 
				
			|||||||
	// Allow the usage of options to fine-tune the topology manager policies.
 | 
						// Allow the usage of options to fine-tune the topology manager policies.
 | 
				
			||||||
	TopologyManagerPolicyOptions featuregate.Feature = "TopologyManagerPolicyOptions"
 | 
						TopologyManagerPolicyOptions featuregate.Feature = "TopologyManagerPolicyOptions"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// owner: @richabanker
 | 
				
			||||||
 | 
						// alpha: v1.28
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Proxies client to an apiserver capable of serving the request in the event of version skew.
 | 
				
			||||||
 | 
						UnknownVersionInteroperabilityProxy featuregate.Feature = "UnknownVersionInteroperabilityProxy"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// owner: @rata, @giuseppe
 | 
						// owner: @rata, @giuseppe
 | 
				
			||||||
	// kep: https://kep.k8s.io/127
 | 
						// kep: https://kep.k8s.io/127
 | 
				
			||||||
	// alpha: v1.25
 | 
						// alpha: v1.25
 | 
				
			||||||
@@ -1147,6 +1153,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	TopologyManagerPolicyOptions: {Default: true, PreRelease: featuregate.Beta},
 | 
						TopologyManagerPolicyOptions: {Default: true, PreRelease: featuregate.Beta},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						UnknownVersionInteroperabilityProxy: {Default: false, PreRelease: featuregate.Alpha},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	VolumeCapacityPriority: {Default: false, PreRelease: featuregate.Alpha},
 | 
						VolumeCapacityPriority: {Default: false, PreRelease: featuregate.Alpha},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	UserNamespacesSupport: {Default: false, PreRelease: featuregate.Alpha},
 | 
						UserNamespacesSupport: {Default: false, PreRelease: featuregate.Alpha},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,10 @@ package v1
 | 
				
			|||||||
const (
 | 
					const (
 | 
				
			||||||
	LabelHostname = "kubernetes.io/hostname"
 | 
						LabelHostname = "kubernetes.io/hostname"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Label value is the network location of kube-apiserver stored as <ip:port>
 | 
				
			||||||
 | 
						// Stored in APIServer Identity lease objects to view what address is used for peer proxy
 | 
				
			||||||
 | 
						AnnotationPeerAdvertiseAddress = "kubernetes.io/peer-advertise-address"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	LabelTopologyZone   = "topology.kubernetes.io/zone"
 | 
						LabelTopologyZone   = "topology.kubernetes.io/zone"
 | 
				
			||||||
	LabelTopologyRegion = "topology.kubernetes.io/region"
 | 
						LabelTopologyRegion = "topology.kubernetes.io/region"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,6 +89,7 @@ require (
 | 
				
			|||||||
	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 | 
						github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 | 
				
			||||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
						github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
				
			||||||
	github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
						github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
				
			||||||
 | 
						github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 | 
				
			||||||
	github.com/pkg/errors v0.9.1 // indirect
 | 
						github.com/pkg/errors v0.9.1 // indirect
 | 
				
			||||||
	github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
						github.com/pmezard/go-difflib v1.0.0 // indirect
 | 
				
			||||||
	github.com/pquerna/cachecontrol v0.1.0 // indirect
 | 
						github.com/pquerna/cachecontrol v0.1.0 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								staging/src/k8s.io/apiserver/go.sum
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								staging/src/k8s.io/apiserver/go.sum
									
									
									
										generated
									
									
									
								
							@@ -382,6 +382,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
 | 
				
			|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 | 
					github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 | 
				
			||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 | 
					github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 | 
				
			||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
					github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
				
			||||||
 | 
					github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
 | 
				
			||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 | 
					github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 | 
				
			||||||
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
 | 
					github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
 | 
				
			||||||
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
 | 
					github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,364 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2023 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package reconcilers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/client-go/kubernetes"
 | 
				
			||||||
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						corev1 "k8s.io/api/core/v1"
 | 
				
			||||||
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						kruntime "k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/wait"
 | 
				
			||||||
 | 
						apirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/registry/rest"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storage"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storage/storagebackend"
 | 
				
			||||||
 | 
						storagefactory "k8s.io/apiserver/pkg/storage/storagebackend/factory"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						APIServerIdentityLabel = "apiserverIdentity"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PeerAdvertiseAddress struct {
 | 
				
			||||||
 | 
						PeerAdvertiseIP   string
 | 
				
			||||||
 | 
						PeerAdvertisePort string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type peerEndpointLeases struct {
 | 
				
			||||||
 | 
						storage   storage.Interface
 | 
				
			||||||
 | 
						destroyFn func()
 | 
				
			||||||
 | 
						baseKey   string
 | 
				
			||||||
 | 
						leaseTime time.Duration
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PeerEndpointLeaseReconciler interface {
 | 
				
			||||||
 | 
						// GetEndpoint retrieves the endpoint for a given apiserverId
 | 
				
			||||||
 | 
						GetEndpoint(serverId string) (string, error)
 | 
				
			||||||
 | 
						// UpdateLease updates the ip and port of peer servers
 | 
				
			||||||
 | 
						UpdateLease(serverId string, ip string, endpointPorts []corev1.EndpointPort) error
 | 
				
			||||||
 | 
						// RemoveEndpoints removes this apiserver's peer endpoint lease.
 | 
				
			||||||
 | 
						RemoveLease(serverId string) error
 | 
				
			||||||
 | 
						// Destroy cleans up everything on shutdown.
 | 
				
			||||||
 | 
						Destroy()
 | 
				
			||||||
 | 
						// StopReconciling turns any later ReconcileEndpoints call into a noop.
 | 
				
			||||||
 | 
						StopReconciling()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type peerEndpointLeaseReconciler struct {
 | 
				
			||||||
 | 
						serverLeases          *peerEndpointLeases
 | 
				
			||||||
 | 
						stopReconcilingCalled atomic.Bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewPeerEndpointLeaseReconciler creates a new peer endpoint lease reconciler
 | 
				
			||||||
 | 
					func NewPeerEndpointLeaseReconciler(config *storagebackend.ConfigForResource, baseKey string, leaseTime time.Duration) (PeerEndpointLeaseReconciler, error) {
 | 
				
			||||||
 | 
						leaseStorage, destroyFn, err := storagefactory.Create(*config, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error creating storage factory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var once sync.Once
 | 
				
			||||||
 | 
						return &peerEndpointLeaseReconciler{
 | 
				
			||||||
 | 
							serverLeases: &peerEndpointLeases{
 | 
				
			||||||
 | 
								storage:   leaseStorage,
 | 
				
			||||||
 | 
								destroyFn: func() { once.Do(destroyFn) },
 | 
				
			||||||
 | 
								baseKey:   baseKey,
 | 
				
			||||||
 | 
								leaseTime: leaseTime,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PeerEndpointController is the controller manager for updating the peer endpoint leases.
 | 
				
			||||||
 | 
					// This provides a separate independent reconciliation loop for peer endpoint leases
 | 
				
			||||||
 | 
					// which ensures that the peer kube-apiservers are fetching the updated endpoint info for a given apiserver
 | 
				
			||||||
 | 
					// in the case when the peer wants to proxy the request to the given apiserver because it can not serve the
 | 
				
			||||||
 | 
					// request itself due to version mismatch.
 | 
				
			||||||
 | 
					type PeerEndpointLeaseController struct {
 | 
				
			||||||
 | 
						reconciler       PeerEndpointLeaseReconciler
 | 
				
			||||||
 | 
						endpointInterval time.Duration
 | 
				
			||||||
 | 
						serverId         string
 | 
				
			||||||
 | 
						// peeraddress stores the IP and port of this kube-apiserver. Used by peer kube-apiservers to
 | 
				
			||||||
 | 
						// route request to this apiserver in case of a version skew.
 | 
				
			||||||
 | 
						peeraddress string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client kubernetes.Interface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lock   sync.Mutex
 | 
				
			||||||
 | 
						stopCh chan struct{} // closed by Stop()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New(serverId string, peeraddress string,
 | 
				
			||||||
 | 
						reconciler PeerEndpointLeaseReconciler, endpointInterval time.Duration, client kubernetes.Interface) *PeerEndpointLeaseController {
 | 
				
			||||||
 | 
						return &PeerEndpointLeaseController{
 | 
				
			||||||
 | 
							reconciler: reconciler,
 | 
				
			||||||
 | 
							serverId:   serverId,
 | 
				
			||||||
 | 
							// peeraddress stores the IP and port of this kube-apiserver. Used by peer kube-apiservers to
 | 
				
			||||||
 | 
							// route request to this apiserver in case of a version skew.
 | 
				
			||||||
 | 
							peeraddress:      peeraddress,
 | 
				
			||||||
 | 
							endpointInterval: endpointInterval,
 | 
				
			||||||
 | 
							client:           client,
 | 
				
			||||||
 | 
							stopCh:           make(chan struct{}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Start begins the peer endpoint lease reconciler loop that must exist for bootstrapping
 | 
				
			||||||
 | 
					// a cluster.
 | 
				
			||||||
 | 
					func (c *PeerEndpointLeaseController) Start(stopCh <-chan struct{}) {
 | 
				
			||||||
 | 
						localStopCh := make(chan struct{})
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							defer close(localStopCh)
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-stopCh: // from Start
 | 
				
			||||||
 | 
							case <-c.stopCh: // from Stop
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						go c.Run(localStopCh)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RunPeerEndpointReconciler periodically updates the peer endpoint leases
 | 
				
			||||||
 | 
					func (c *PeerEndpointLeaseController) Run(stopCh <-chan struct{}) {
 | 
				
			||||||
 | 
						// wait until process is ready
 | 
				
			||||||
 | 
						wait.PollImmediateUntil(100*time.Millisecond, func() (bool, error) {
 | 
				
			||||||
 | 
							var code int
 | 
				
			||||||
 | 
							c.client.CoreV1().RESTClient().Get().AbsPath("/readyz").Do(context.TODO()).StatusCode(&code)
 | 
				
			||||||
 | 
							return code == http.StatusOK, nil
 | 
				
			||||||
 | 
						}, stopCh)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						wait.NonSlidingUntil(func() {
 | 
				
			||||||
 | 
							if err := c.UpdatePeerEndpointLeases(); err != nil {
 | 
				
			||||||
 | 
								runtime.HandleError(fmt.Errorf("unable to update peer endpoint leases: %v", err))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, c.endpointInterval, stopCh)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Stop cleans up this apiserver's peer endpoint leases.
 | 
				
			||||||
 | 
					func (c *PeerEndpointLeaseController) Stop() {
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						defer c.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case <-c.stopCh:
 | 
				
			||||||
 | 
							return // only close once
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							close(c.stopCh)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						finishedReconciling := make(chan struct{})
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							defer close(finishedReconciling)
 | 
				
			||||||
 | 
							klog.Infof("Shutting down peer endpoint lease reconciler")
 | 
				
			||||||
 | 
							// stop reconciliation
 | 
				
			||||||
 | 
							c.reconciler.StopReconciling()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Ensure that there will be no race condition with the ReconcileEndpointLeases.
 | 
				
			||||||
 | 
							if err := c.reconciler.RemoveLease(c.serverId); err != nil {
 | 
				
			||||||
 | 
								klog.Errorf("Unable to remove peer endpoint leases: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.reconciler.Destroy()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case <-finishedReconciling:
 | 
				
			||||||
 | 
							// done
 | 
				
			||||||
 | 
						case <-time.After(2 * c.endpointInterval):
 | 
				
			||||||
 | 
							// don't block server shutdown forever if we can't reach etcd to remove ourselves
 | 
				
			||||||
 | 
							klog.Warning("peer_endpoint_controller's RemoveEndpoints() timed out")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdatePeerEndpointLeases attempts to update the peer endpoint leases.
 | 
				
			||||||
 | 
					func (c *PeerEndpointLeaseController) UpdatePeerEndpointLeases() error {
 | 
				
			||||||
 | 
						host, port, err := net.SplitHostPort(c.peeraddress)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						p, err := strconv.Atoi(port)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						endpointPorts := createEndpointPortSpec(p, "https")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure that there will be no race condition with the RemoveEndpointLeases.
 | 
				
			||||||
 | 
						c.lock.Lock()
 | 
				
			||||||
 | 
						defer c.lock.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Refresh the TTL on our key, independently of whether any error or
 | 
				
			||||||
 | 
						// update conflict happens below. This makes sure that at least some of
 | 
				
			||||||
 | 
						// the servers will add our endpoint lease.
 | 
				
			||||||
 | 
						if err := c.reconciler.UpdateLease(c.serverId, host, endpointPorts); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateLease resets the TTL on a server IP in storage
 | 
				
			||||||
 | 
					// UpdateLease will create a new key if it doesn't exist.
 | 
				
			||||||
 | 
					// We use the first element in endpointPorts as a part of the lease's base key
 | 
				
			||||||
 | 
					// This is done to support out tests that simulate 2 apiservers running on the same ip but
 | 
				
			||||||
 | 
					// different ports
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// It will also do the following if UnknownVersionInteroperabilityProxy feature is enabled
 | 
				
			||||||
 | 
					// 1. store the apiserverId as a label
 | 
				
			||||||
 | 
					// 2. store the values passed to --peer-advertise-ip and --peer-advertise-port flags to kube-apiserver as an annotation
 | 
				
			||||||
 | 
					// with value of format <ip:port>
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) UpdateLease(serverId string, ip string, endpointPorts []corev1.EndpointPort) error {
 | 
				
			||||||
 | 
						// reconcile endpoints only if apiserver was not shutdown
 | 
				
			||||||
 | 
						if r.stopReconcilingCalled.Load() {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we use the serverID as the key to avoid using the server IP, port as the key.
 | 
				
			||||||
 | 
						// note: this means that this lease doesn't enforce mutual exclusion of ip/port usage between apiserver.
 | 
				
			||||||
 | 
						key := path.Join(r.serverLeases.baseKey, serverId)
 | 
				
			||||||
 | 
						return r.serverLeases.storage.GuaranteedUpdate(apirequest.NewDefaultContext(), key, &corev1.Endpoints{}, true, nil, func(input kruntime.Object, respMeta storage.ResponseMeta) (kruntime.Object, *uint64, error) {
 | 
				
			||||||
 | 
							existing := input.(*corev1.Endpoints)
 | 
				
			||||||
 | 
							existing.Subsets = []corev1.EndpointSubset{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									Addresses: []corev1.EndpointAddress{{IP: ip}},
 | 
				
			||||||
 | 
									Ports:     endpointPorts,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// store this server's identity (serverId) as a label. This will be used by
 | 
				
			||||||
 | 
							// peers to find the IP of this server when the peer can not serve a request
 | 
				
			||||||
 | 
							// due to version skew.
 | 
				
			||||||
 | 
							if existing.Labels == nil {
 | 
				
			||||||
 | 
								existing.Labels = map[string]string{}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							existing.Labels[APIServerIdentityLabel] = serverId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// leaseTime needs to be in seconds
 | 
				
			||||||
 | 
							leaseTime := uint64(r.serverLeases.leaseTime / time.Second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// NB: GuaranteedUpdate does not perform the store operation unless
 | 
				
			||||||
 | 
							// something changed between load and store (not including resource
 | 
				
			||||||
 | 
							// version), meaning we can't refresh the TTL without actually
 | 
				
			||||||
 | 
							// changing a field.
 | 
				
			||||||
 | 
							existing.Generation++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							klog.V(6).Infof("Resetting TTL on server IP %q listed in storage to %v", ip, leaseTime)
 | 
				
			||||||
 | 
							return existing, &leaseTime, nil
 | 
				
			||||||
 | 
						}, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListLeases retrieves a list of the current server IPs from storage
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) ListLeases() ([]string, error) {
 | 
				
			||||||
 | 
						storageOpts := storage.ListOptions{
 | 
				
			||||||
 | 
							ResourceVersion:      "0",
 | 
				
			||||||
 | 
							ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
 | 
				
			||||||
 | 
							Predicate:            storage.Everything,
 | 
				
			||||||
 | 
							Recursive:            true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ipInfoList, err := r.getIpInfoList(storageOpts)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ipList := make([]string, 0, len(ipInfoList.Items))
 | 
				
			||||||
 | 
						for _, ip := range ipInfoList.Items {
 | 
				
			||||||
 | 
							if len(ip.Subsets) > 0 && len(ip.Subsets[0].Addresses) > 0 && len(ip.Subsets[0].Addresses[0].IP) > 0 {
 | 
				
			||||||
 | 
								ipList = append(ipList, ip.Subsets[0].Addresses[0].IP)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						klog.V(6).Infof("Current server IPs listed in storage are %v", ipList)
 | 
				
			||||||
 | 
						return ipList, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLease retrieves the server IP and port for a specific server id
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) GetLease(serverId string) (string, error) {
 | 
				
			||||||
 | 
						var fullAddr string
 | 
				
			||||||
 | 
						if serverId == "" {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("error getting endpoint for serverId: empty serverId")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						storageOpts := storage.ListOptions{
 | 
				
			||||||
 | 
							ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
 | 
				
			||||||
 | 
							Predicate:            storage.Everything,
 | 
				
			||||||
 | 
							Recursive:            true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ipInfoList, err := r.getIpInfoList(storageOpts)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, ip := range ipInfoList.Items {
 | 
				
			||||||
 | 
							if ip.Labels[APIServerIdentityLabel] == serverId {
 | 
				
			||||||
 | 
								if len(ip.Subsets) > 0 {
 | 
				
			||||||
 | 
									var ipStr, portStr string
 | 
				
			||||||
 | 
									if len(ip.Subsets[0].Addresses) > 0 {
 | 
				
			||||||
 | 
										if len(ip.Subsets[0].Addresses[0].IP) > 0 {
 | 
				
			||||||
 | 
											ipStr = ip.Subsets[0].Addresses[0].IP
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if len(ip.Subsets[0].Ports) > 0 {
 | 
				
			||||||
 | 
										portStr = fmt.Sprint(ip.Subsets[0].Ports[0].Port)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									fullAddr = net.JoinHostPort(ipStr, portStr)
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						klog.V(6).Infof("Fetched this server IP for the specified apiserverId %v, %v", serverId, fullAddr)
 | 
				
			||||||
 | 
						return fullAddr, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) StopReconciling() {
 | 
				
			||||||
 | 
						r.stopReconcilingCalled.Store(true)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveLease removes the lease on a server IP in storage
 | 
				
			||||||
 | 
					// We use the first element in endpointPorts as a part of the lease's base key
 | 
				
			||||||
 | 
					// This is done to support out tests that simulate 2 apiservers running on the same ip but
 | 
				
			||||||
 | 
					// different ports
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) RemoveLease(serverId string) error {
 | 
				
			||||||
 | 
						key := path.Join(r.serverLeases.baseKey, serverId)
 | 
				
			||||||
 | 
						return r.serverLeases.storage.Delete(apirequest.NewDefaultContext(), key, &corev1.Endpoints{}, nil, rest.ValidateAllObjectFunc, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) Destroy() {
 | 
				
			||||||
 | 
						r.serverLeases.destroyFn()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) GetEndpoint(serverId string) (string, error) {
 | 
				
			||||||
 | 
						return r.GetLease(serverId)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *peerEndpointLeaseReconciler) getIpInfoList(storageOpts storage.ListOptions) (*corev1.EndpointsList, error) {
 | 
				
			||||||
 | 
						ipInfoList := &corev1.EndpointsList{}
 | 
				
			||||||
 | 
						if err := r.serverLeases.storage.GetList(apirequest.NewDefaultContext(), r.serverLeases.baseKey, storageOpts, ipInfoList); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ipInfoList, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// createEndpointPortSpec creates the endpoint ports
 | 
				
			||||||
 | 
					func createEndpointPortSpec(endpointPort int, endpointPortName string) []corev1.EndpointPort {
 | 
				
			||||||
 | 
						return []corev1.EndpointPort{{
 | 
				
			||||||
 | 
							Protocol: corev1.ProtocolTCP,
 | 
				
			||||||
 | 
							Port:     int32(endpointPort),
 | 
				
			||||||
 | 
							Name:     endpointPortName,
 | 
				
			||||||
 | 
						}}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,278 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2017 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package reconcilers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						corev1 "k8s.io/api/core/v1"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/api/apitesting"
 | 
				
			||||||
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime/serializer"
 | 
				
			||||||
 | 
						utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/features"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storage"
 | 
				
			||||||
 | 
						etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storage/storagebackend/factory"
 | 
				
			||||||
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
 | 
						featuregatetesting "k8s.io/component-base/featuregate/testing"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						var scheme = runtime.NewScheme()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
 | 
				
			||||||
 | 
						utilruntime.Must(corev1.AddToScheme(scheme))
 | 
				
			||||||
 | 
						utilruntime.Must(scheme.SetVersionPriority(corev1.SchemeGroupVersion))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						codecs = serializer.NewCodecFactory(scheme)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var codecs serializer.CodecFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type serverInfo struct {
 | 
				
			||||||
 | 
						existingIP     string
 | 
				
			||||||
 | 
						id             string
 | 
				
			||||||
 | 
						ports          []corev1.EndpointPort
 | 
				
			||||||
 | 
						newIP          string
 | 
				
			||||||
 | 
						removeLease    bool
 | 
				
			||||||
 | 
						expectEndpoint string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewFakePeerEndpointReconciler(t *testing.T, s storage.Interface) peerEndpointLeaseReconciler {
 | 
				
			||||||
 | 
						// use the same base key used by the controlplane, but add a random
 | 
				
			||||||
 | 
						// prefix so we can reuse the etcd instance for subtests independently.
 | 
				
			||||||
 | 
						base := "/" + uuid.New().String() + "/peerserverleases/"
 | 
				
			||||||
 | 
						return peerEndpointLeaseReconciler{serverLeases: &peerEndpointLeases{
 | 
				
			||||||
 | 
							storage:   s,
 | 
				
			||||||
 | 
							destroyFn: func() {},
 | 
				
			||||||
 | 
							baseKey:   base,
 | 
				
			||||||
 | 
							leaseTime: 1 * time.Minute, // avoid the lease to timeout on tests
 | 
				
			||||||
 | 
						}}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (f *peerEndpointLeaseReconciler) SetKeys(servers []serverInfo) error {
 | 
				
			||||||
 | 
						for _, server := range servers {
 | 
				
			||||||
 | 
							if err := f.UpdateLease(server.id, server.existingIP, server.ports); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPeerEndpointLeaseReconciler(t *testing.T) {
 | 
				
			||||||
 | 
						// enable feature flags
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIServerIdentity, true)()
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StorageVersionAPI, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
 | 
				
			||||||
 | 
						t.Cleanup(func() { server.Terminate(t) })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newFunc := func() runtime.Object { return &corev1.Endpoints{} }
 | 
				
			||||||
 | 
						sc.Codec = apitesting.TestStorageCodec(codecs, corev1.SchemeGroupVersion)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s, dFunc, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "endpoints"}), newFunc)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error creating storage: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Cleanup(dFunc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							testName     string
 | 
				
			||||||
 | 
							servers      []serverInfo
 | 
				
			||||||
 | 
							expectLeases []string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName: "existing IP satisfy",
 | 
				
			||||||
 | 
								servers: []serverInfo{{
 | 
				
			||||||
 | 
									existingIP:     "4.3.2.1",
 | 
				
			||||||
 | 
									id:             "server-1",
 | 
				
			||||||
 | 
									ports:          []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
									expectEndpoint: "4.3.2.1:8080",
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									existingIP:     "1.2.3.4",
 | 
				
			||||||
 | 
									id:             "server-2",
 | 
				
			||||||
 | 
									ports:          []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
									expectEndpoint: "1.2.3.4:8080",
 | 
				
			||||||
 | 
								}},
 | 
				
			||||||
 | 
								expectLeases: []string{"4.3.2.1", "1.2.3.4"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName: "existing IP + new IP = should return the new IP",
 | 
				
			||||||
 | 
								servers: []serverInfo{{
 | 
				
			||||||
 | 
									existingIP:     "4.3.2.2",
 | 
				
			||||||
 | 
									id:             "server-1",
 | 
				
			||||||
 | 
									ports:          []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
									newIP:          "4.3.2.1",
 | 
				
			||||||
 | 
									expectEndpoint: "4.3.2.1:8080",
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									existingIP:     "1.2.3.4",
 | 
				
			||||||
 | 
									id:             "server-2",
 | 
				
			||||||
 | 
									ports:          []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
									newIP:          "1.1.1.1",
 | 
				
			||||||
 | 
									expectEndpoint: "1.1.1.1:8080",
 | 
				
			||||||
 | 
								}},
 | 
				
			||||||
 | 
								expectLeases: []string{"4.3.2.1", "1.1.1.1"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName: "no existing IP, should return new IP",
 | 
				
			||||||
 | 
								servers: []serverInfo{{
 | 
				
			||||||
 | 
									id:             "server-1",
 | 
				
			||||||
 | 
									ports:          []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
									newIP:          "1.2.3.4",
 | 
				
			||||||
 | 
									expectEndpoint: "1.2.3.4:8080",
 | 
				
			||||||
 | 
								}},
 | 
				
			||||||
 | 
								expectLeases: []string{"1.2.3.4"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							t.Run(test.testName, func(t *testing.T) {
 | 
				
			||||||
 | 
								fakeReconciler := NewFakePeerEndpointReconciler(t, s)
 | 
				
			||||||
 | 
								err := fakeReconciler.SetKeys(test.servers)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Errorf("unexpected error creating keys: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for _, server := range test.servers {
 | 
				
			||||||
 | 
									if server.newIP != "" {
 | 
				
			||||||
 | 
										err = fakeReconciler.UpdateLease(server.id, server.newIP, server.ports)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											t.Errorf("unexpected error reconciling: %v", err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								leases, err := fakeReconciler.ListLeases()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Errorf("unexpected error: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// sort for comparison
 | 
				
			||||||
 | 
								sort.Strings(leases)
 | 
				
			||||||
 | 
								sort.Strings(test.expectLeases)
 | 
				
			||||||
 | 
								if !reflect.DeepEqual(leases, test.expectLeases) {
 | 
				
			||||||
 | 
									t.Errorf("expected %v got: %v", test.expectLeases, leases)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for _, server := range test.servers {
 | 
				
			||||||
 | 
									endpoint, err := fakeReconciler.GetLease(server.id)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										t.Errorf("unexpected error: %v", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if endpoint != server.expectEndpoint {
 | 
				
			||||||
 | 
										t.Errorf("expected %v got: %v", server.expectEndpoint, endpoint)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPeerLeaseRemoveEndpoints(t *testing.T) {
 | 
				
			||||||
 | 
						// enable feature flags
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIServerIdentity, true)()
 | 
				
			||||||
 | 
						defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StorageVersionAPI, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
 | 
				
			||||||
 | 
						t.Cleanup(func() { server.Terminate(t) })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newFunc := func() runtime.Object { return &corev1.Endpoints{} }
 | 
				
			||||||
 | 
						sc.Codec = apitesting.TestStorageCodec(codecs, corev1.SchemeGroupVersion)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s, dFunc, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "pods"}), newFunc)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error creating storage: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Cleanup(dFunc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stopTests := []struct {
 | 
				
			||||||
 | 
							testName         string
 | 
				
			||||||
 | 
							servers          []serverInfo
 | 
				
			||||||
 | 
							expectLeases     []string
 | 
				
			||||||
 | 
							apiServerStartup bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName: "successful remove previous endpoints before apiserver starts",
 | 
				
			||||||
 | 
								servers: []serverInfo{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										existingIP:  "1.2.3.4",
 | 
				
			||||||
 | 
										id:          "test-server-1",
 | 
				
			||||||
 | 
										ports:       []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
										removeLease: true,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										existingIP: "2.4.6.8",
 | 
				
			||||||
 | 
										id:         "test-server-2",
 | 
				
			||||||
 | 
										ports:      []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
									}},
 | 
				
			||||||
 | 
								expectLeases:     []string{"2.4.6.8"},
 | 
				
			||||||
 | 
								apiServerStartup: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName: "stop reconciling with new IP not in existing ip list",
 | 
				
			||||||
 | 
								servers: []serverInfo{{
 | 
				
			||||||
 | 
									existingIP: "1.2.3.4",
 | 
				
			||||||
 | 
									newIP:      "4.6.8.9",
 | 
				
			||||||
 | 
									id:         "test-server-1",
 | 
				
			||||||
 | 
									ports:      []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										existingIP:  "2.4.6.8",
 | 
				
			||||||
 | 
										id:          "test-server-2",
 | 
				
			||||||
 | 
										ports:       []corev1.EndpointPort{{Name: "foo", Port: 8080, Protocol: "TCP"}},
 | 
				
			||||||
 | 
										removeLease: true,
 | 
				
			||||||
 | 
									}},
 | 
				
			||||||
 | 
								expectLeases: []string{"1.2.3.4"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, test := range stopTests {
 | 
				
			||||||
 | 
							t.Run(test.testName, func(t *testing.T) {
 | 
				
			||||||
 | 
								fakeReconciler := NewFakePeerEndpointReconciler(t, s)
 | 
				
			||||||
 | 
								err := fakeReconciler.SetKeys(test.servers)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Errorf("unexpected error creating keys: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !test.apiServerStartup {
 | 
				
			||||||
 | 
									fakeReconciler.StopReconciling()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, server := range test.servers {
 | 
				
			||||||
 | 
									if server.removeLease {
 | 
				
			||||||
 | 
										err = fakeReconciler.RemoveLease(server.id)
 | 
				
			||||||
 | 
										// if the ip is not on the endpoints, it must return an storage error and stop reconciling
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											t.Errorf("unexpected error reconciling: %v", err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								leases, err := fakeReconciler.ListLeases()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Errorf("unexpected error: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// sort for comparison
 | 
				
			||||||
 | 
								sort.Strings(leases)
 | 
				
			||||||
 | 
								sort.Strings(test.expectLeases)
 | 
				
			||||||
 | 
								if !reflect.DeepEqual(leases, test.expectLeases) {
 | 
				
			||||||
 | 
									t.Errorf("expected %v got: %v", test.expectLeases, leases)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -86,6 +86,13 @@ import (
 | 
				
			|||||||
	_ "k8s.io/apiserver/pkg/apis/apiserver/install"
 | 
						_ "k8s.io/apiserver/pkg/apis/apiserver/install"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// hostnameFunc is a function to set the hostnameFunc of this apiserver.
 | 
				
			||||||
 | 
					// To be used for testing purpose only, to simulate scenarios where multiple apiservers
 | 
				
			||||||
 | 
					// exist. In such cases we want to ensure unique apiserver IDs which are a hash of hostnameFunc.
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						hostnameFunc = os.Hostname
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	// DefaultLegacyAPIPrefix is where the legacy APIs will be located.
 | 
						// DefaultLegacyAPIPrefix is where the legacy APIs will be located.
 | 
				
			||||||
	DefaultLegacyAPIPrefix = "/api"
 | 
						DefaultLegacyAPIPrefix = "/api"
 | 
				
			||||||
@@ -367,7 +374,7 @@ func NewConfig(codecs serializer.CodecFactory) *Config {
 | 
				
			|||||||
	defaultHealthChecks := []healthz.HealthChecker{healthz.PingHealthz, healthz.LogHealthz}
 | 
						defaultHealthChecks := []healthz.HealthChecker{healthz.PingHealthz, healthz.LogHealthz}
 | 
				
			||||||
	var id string
 | 
						var id string
 | 
				
			||||||
	if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
 | 
						if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
 | 
				
			||||||
		hostname, err := os.Hostname()
 | 
							hostname, err := hostnameFunc()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			klog.Fatalf("error getting hostname for apiserver identity: %v", err)
 | 
								klog.Fatalf("error getting hostname for apiserver identity: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -897,7 +904,9 @@ func BuildHandlerChainWithStorageVersionPrecondition(apiHandler http.Handler, c
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
 | 
					func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
 | 
				
			||||||
	handler := filterlatency.TrackCompleted(apiHandler)
 | 
						handler := apiHandler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handler = filterlatency.TrackCompleted(handler)
 | 
				
			||||||
	handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
 | 
						handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
 | 
				
			||||||
	handler = filterlatency.TrackStarted(handler, c.TracerProvider, "authorization")
 | 
						handler = filterlatency.TrackStarted(handler, c.TracerProvider, "authorization")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1070,3 +1079,12 @@ func AuthorizeClientBearerToken(loopback *restclient.Config, authn *Authenticati
 | 
				
			|||||||
	tokenAuthenticator := authenticatorfactory.NewFromTokens(tokens, authn.APIAudiences)
 | 
						tokenAuthenticator := authenticatorfactory.NewFromTokens(tokens, authn.APIAudiences)
 | 
				
			||||||
	authn.Authenticator = authenticatorunion.New(tokenAuthenticator, authn.Authenticator)
 | 
						authn.Authenticator = authenticatorunion.New(tokenAuthenticator, authn.Authenticator)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// For testing purpose only
 | 
				
			||||||
 | 
					func SetHostnameFuncForTests(name string) {
 | 
				
			||||||
 | 
						hostnameFunc = func() (host string, err error) {
 | 
				
			||||||
 | 
							host = name
 | 
				
			||||||
 | 
							err = nil
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ import (
 | 
				
			|||||||
	cachermetrics "k8s.io/apiserver/pkg/storage/cacher/metrics"
 | 
						cachermetrics "k8s.io/apiserver/pkg/storage/cacher/metrics"
 | 
				
			||||||
	etcd3metrics "k8s.io/apiserver/pkg/storage/etcd3/metrics"
 | 
						etcd3metrics "k8s.io/apiserver/pkg/storage/etcd3/metrics"
 | 
				
			||||||
	flowcontrolmetrics "k8s.io/apiserver/pkg/util/flowcontrol/metrics"
 | 
						flowcontrolmetrics "k8s.io/apiserver/pkg/util/flowcontrol/metrics"
 | 
				
			||||||
 | 
						peerproxymetrics "k8s.io/apiserver/pkg/util/peerproxy/metrics"
 | 
				
			||||||
	"k8s.io/component-base/metrics/legacyregistry"
 | 
						"k8s.io/component-base/metrics/legacyregistry"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -50,4 +51,5 @@ func register() {
 | 
				
			|||||||
	cachermetrics.Register()
 | 
						cachermetrics.Register()
 | 
				
			||||||
	etcd3metrics.Register()
 | 
						etcd3metrics.Register()
 | 
				
			||||||
	flowcontrolmetrics.Register()
 | 
						flowcontrolmetrics.Register()
 | 
				
			||||||
 | 
						peerproxymetrics.Register()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2023 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package metrics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/component-base/metrics"
 | 
				
			||||||
 | 
						"k8s.io/component-base/metrics/legacyregistry"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						subsystem  = "apiserver"
 | 
				
			||||||
 | 
						statuscode = "code"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var registerMetricsOnce sync.Once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						// peerProxiedRequestsTotal counts the number of requests that were proxied to a peer kube-apiserver.
 | 
				
			||||||
 | 
						peerProxiedRequestsTotal = metrics.NewCounterVec(
 | 
				
			||||||
 | 
							&metrics.CounterOpts{
 | 
				
			||||||
 | 
								Subsystem:      subsystem,
 | 
				
			||||||
 | 
								Name:           "rerouted_request_total",
 | 
				
			||||||
 | 
								Help:           "Total number of requests that were proxied to a peer kube apiserver because the local apiserver was not capable of serving it",
 | 
				
			||||||
 | 
								StabilityLevel: metrics.ALPHA,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[]string{statuscode},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Register() {
 | 
				
			||||||
 | 
						registerMetricsOnce.Do(func() {
 | 
				
			||||||
 | 
							legacyregistry.MustRegister(peerProxiedRequestsTotal)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IncPeerProxiedRequest increments the # of proxied requests to peer kube-apiserver
 | 
				
			||||||
 | 
					func IncPeerProxiedRequest(ctx context.Context, status string) {
 | 
				
			||||||
 | 
						peerProxiedRequestsTotal.WithContext(ctx).WithLabelValues(status).Add(1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										67
									
								
								staging/src/k8s.io/apiserver/pkg/util/peerproxy/peerproxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								staging/src/k8s.io/apiserver/pkg/util/peerproxy/peerproxy.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2023 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package peerproxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storageversion"
 | 
				
			||||||
 | 
						kubeinformers "k8s.io/client-go/informers"
 | 
				
			||||||
 | 
						"k8s.io/client-go/tools/cache"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Interface defines how the Unknown Version Proxy filter interacts with the underlying system.
 | 
				
			||||||
 | 
					type Interface interface {
 | 
				
			||||||
 | 
						WrapHandler(handler http.Handler) http.Handler
 | 
				
			||||||
 | 
						WaitForCacheSync(stopCh <-chan struct{}) error
 | 
				
			||||||
 | 
						HasFinishedSync() bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New creates a new instance to implement unknown version proxy
 | 
				
			||||||
 | 
					func NewPeerProxyHandler(informerFactory kubeinformers.SharedInformerFactory,
 | 
				
			||||||
 | 
						svm storageversion.Manager,
 | 
				
			||||||
 | 
						proxyTransport http.RoundTripper,
 | 
				
			||||||
 | 
						serverId string,
 | 
				
			||||||
 | 
						reconciler reconcilers.PeerEndpointLeaseReconciler,
 | 
				
			||||||
 | 
						serializer runtime.NegotiatedSerializer) *peerProxyHandler {
 | 
				
			||||||
 | 
						h := &peerProxyHandler{
 | 
				
			||||||
 | 
							name:                  "PeerProxyHandler",
 | 
				
			||||||
 | 
							storageversionManager: svm,
 | 
				
			||||||
 | 
							proxyTransport:        proxyTransport,
 | 
				
			||||||
 | 
							svMap:                 sync.Map{},
 | 
				
			||||||
 | 
							serverId:              serverId,
 | 
				
			||||||
 | 
							reconciler:            reconciler,
 | 
				
			||||||
 | 
							serializer:            serializer,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						svi := informerFactory.Internal().V1alpha1().StorageVersions()
 | 
				
			||||||
 | 
						h.storageversionInformer = svi.Informer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						svi.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 | 
				
			||||||
 | 
							AddFunc: func(obj interface{}) {
 | 
				
			||||||
 | 
								h.addSV(obj)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							UpdateFunc: func(oldObj, newObj interface{}) {
 | 
				
			||||||
 | 
								h.updateSV(oldObj, newObj)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							DeleteFunc: func(obj interface{}) {
 | 
				
			||||||
 | 
								h.deleteSV(obj)
 | 
				
			||||||
 | 
							}})
 | 
				
			||||||
 | 
						return h
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,357 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2023 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package peerproxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"math/rand"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"k8s.io/api/apiserverinternal/v1alpha1"
 | 
				
			||||||
 | 
						apierrors "k8s.io/apimachinery/pkg/api/errors"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						schema "k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/proxy"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
 | 
				
			||||||
 | 
						epmetrics "k8s.io/apiserver/pkg/endpoints/metrics"
 | 
				
			||||||
 | 
						apirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/endpoints/responsewriter"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storageversion"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/util/peerproxy/metrics"
 | 
				
			||||||
 | 
						apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy"
 | 
				
			||||||
 | 
						"k8s.io/client-go/tools/cache"
 | 
				
			||||||
 | 
						"k8s.io/client-go/transport"
 | 
				
			||||||
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						PeerProxiedHeader = "x-kubernetes-peer-proxied"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type peerProxyHandler struct {
 | 
				
			||||||
 | 
						name string
 | 
				
			||||||
 | 
						// StorageVersion informer used to fetch apiserver ids than can serve a resource
 | 
				
			||||||
 | 
						storageversionInformer cache.SharedIndexInformer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// StorageVersion manager used to ensure it has finished updating storageversions before
 | 
				
			||||||
 | 
						// we start handling external requests
 | 
				
			||||||
 | 
						storageversionManager storageversion.Manager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// proxy transport
 | 
				
			||||||
 | 
						proxyTransport http.RoundTripper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// identity for this server
 | 
				
			||||||
 | 
						serverId string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// reconciler that is used to fetch host port of peer apiserver when proxying request to a peer
 | 
				
			||||||
 | 
						reconciler reconcilers.PeerEndpointLeaseReconciler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						serializer runtime.NegotiatedSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SyncMap for storing an up to date copy of the storageversions and apiservers that can serve them
 | 
				
			||||||
 | 
						// This map is populated using the StorageVersion informer
 | 
				
			||||||
 | 
						// This map has key set to GVR and value being another SyncMap
 | 
				
			||||||
 | 
						// The nested SyncMap has key set to apiserver id and value set to boolean
 | 
				
			||||||
 | 
						// The nested maps are created to have a "Set" like structure to store unique apiserver ids
 | 
				
			||||||
 | 
						// for a given GVR
 | 
				
			||||||
 | 
						svMap sync.Map
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						finishedSync atomic.Bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type serviceableByResponse struct {
 | 
				
			||||||
 | 
						locallyServiceable            bool
 | 
				
			||||||
 | 
						errorFetchingAddressFromLease bool
 | 
				
			||||||
 | 
						peerEndpoints                 []string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// responder implements rest.Responder for assisting a connector in writing objects or errors.
 | 
				
			||||||
 | 
					type responder struct {
 | 
				
			||||||
 | 
						w   http.ResponseWriter
 | 
				
			||||||
 | 
						ctx context.Context
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) HasFinishedSync() bool {
 | 
				
			||||||
 | 
						return h.finishedSync.Load()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) WaitForCacheSync(stopCh <-chan struct{}) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ok := cache.WaitForNamedCacheSync("unknown-version-proxy", stopCh, h.storageversionInformer.HasSynced, h.storageversionManager.Completed)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return fmt.Errorf("error while waiting for initial cache sync")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						klog.V(3).Infof("setting finishedSync to true")
 | 
				
			||||||
 | 
						h.finishedSync.Store(true)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// WrapHandler will fetch the apiservers that can serve the request and either serve it locally
 | 
				
			||||||
 | 
					// or route it to a peer
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) WrapHandler(handler http.Handler) http.Handler {
 | 
				
			||||||
 | 
						return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							ctx := r.Context()
 | 
				
			||||||
 | 
							requestInfo, ok := apirequest.RequestInfoFrom(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								responsewriters.InternalError(w, r, errors.New("no RequestInfo found in the context"))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Allow non-resource requests
 | 
				
			||||||
 | 
							if !requestInfo.IsResourceRequest {
 | 
				
			||||||
 | 
								klog.V(3).Infof("Not a resource request skipping proxying")
 | 
				
			||||||
 | 
								handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Request has already been proxied once, it must be served locally
 | 
				
			||||||
 | 
							if r.Header.Get(PeerProxiedHeader) == "true" {
 | 
				
			||||||
 | 
								klog.V(3).Infof("Already rerouted once, skipping proxying to peer")
 | 
				
			||||||
 | 
								handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// StorageVersion Informers and/or StorageVersionManager is not synced yet, pass request to next handler
 | 
				
			||||||
 | 
							// This will happen for self requests from the kube-apiserver because we have a poststarthook
 | 
				
			||||||
 | 
							// to ensure that external requests are not served until the StorageVersion Informer and
 | 
				
			||||||
 | 
							// StorageVersionManager has synced
 | 
				
			||||||
 | 
							if !h.HasFinishedSync() {
 | 
				
			||||||
 | 
								handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							gvr := schema.GroupVersionResource{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion, Resource: requestInfo.Resource}
 | 
				
			||||||
 | 
							if requestInfo.APIGroup == "" {
 | 
				
			||||||
 | 
								gvr.Group = "core"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// find servers that are capable of serving this request
 | 
				
			||||||
 | 
							serviceableByResp, err := h.findServiceableByServers(gvr, h.serverId, h.reconciler)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								// this means that resource is an aggregated API or a CR since it wasn't found in SV informer cache, pass as it is
 | 
				
			||||||
 | 
								handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// found the gvr locally, pass request to the next handler in local apiserver
 | 
				
			||||||
 | 
							if serviceableByResp.locallyServiceable {
 | 
				
			||||||
 | 
								handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							gv := schema.GroupVersion{Group: gvr.Group, Version: gvr.Version}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if serviceableByResp.errorFetchingAddressFromLease {
 | 
				
			||||||
 | 
								klog.ErrorS(err, "error fetching ip and port of remote server while proxying")
 | 
				
			||||||
 | 
								responsewriters.ErrorNegotiated(apierrors.NewServiceUnavailable("Error getting ip and port info of the remote server while proxying"), h.serializer, gv, w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// no apiservers were found that could serve the request, pass request to
 | 
				
			||||||
 | 
							// next handler, that should eventually serve 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: maintain locally serviceable GVRs somewhere so that we dont have to
 | 
				
			||||||
 | 
							// consult the storageversion-informed map for those
 | 
				
			||||||
 | 
							if len(serviceableByResp.peerEndpoints) == 0 {
 | 
				
			||||||
 | 
								klog.Errorf(fmt.Sprintf("GVR %v is not served by anything in this cluster", gvr))
 | 
				
			||||||
 | 
								handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// otherwise, randomly select an apiserver and proxy request to it
 | 
				
			||||||
 | 
							rand := rand.Intn(len(serviceableByResp.peerEndpoints))
 | 
				
			||||||
 | 
							destServerHostPort := serviceableByResp.peerEndpoints[rand]
 | 
				
			||||||
 | 
							h.proxyRequestToDestinationAPIServer(r, w, destServerHostPort)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) findServiceableByServers(gvr schema.GroupVersionResource, localAPIServerId string, reconciler reconcilers.PeerEndpointLeaseReconciler) (serviceableByResponse, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiserversi, ok := h.svMap.Load(gvr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// no value found for the requested gvr in svMap
 | 
				
			||||||
 | 
						if !ok || apiserversi == nil {
 | 
				
			||||||
 | 
							return serviceableByResponse{}, fmt.Errorf("no StorageVersions found for the GVR: %v", gvr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						apiservers := apiserversi.(*sync.Map)
 | 
				
			||||||
 | 
						response := serviceableByResponse{}
 | 
				
			||||||
 | 
						var peerServerEndpoints []string
 | 
				
			||||||
 | 
						apiservers.Range(func(key, value interface{}) bool {
 | 
				
			||||||
 | 
							apiserverKey := key.(string)
 | 
				
			||||||
 | 
							if apiserverKey == localAPIServerId {
 | 
				
			||||||
 | 
								response.errorFetchingAddressFromLease = true
 | 
				
			||||||
 | 
								response.locallyServiceable = true
 | 
				
			||||||
 | 
								// stop iteration
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							hostPort, err := reconciler.GetEndpoint(apiserverKey)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								response.errorFetchingAddressFromLease = true
 | 
				
			||||||
 | 
								klog.Errorf("failed to get peer ip from storage lease for server %s", apiserverKey)
 | 
				
			||||||
 | 
								// continue with iteration
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// check ip format
 | 
				
			||||||
 | 
							_, _, err = net.SplitHostPort(hostPort)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								response.errorFetchingAddressFromLease = true
 | 
				
			||||||
 | 
								klog.Errorf("invalid address found for server %s", apiserverKey)
 | 
				
			||||||
 | 
								// continue with iteration
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							peerServerEndpoints = append(peerServerEndpoints, hostPort)
 | 
				
			||||||
 | 
							// continue with iteration
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						response.peerEndpoints = peerServerEndpoints
 | 
				
			||||||
 | 
						return response, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) proxyRequestToDestinationAPIServer(req *http.Request, rw http.ResponseWriter, host string) {
 | 
				
			||||||
 | 
						user, ok := apirequest.UserFrom(req.Context())
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							klog.Errorf("failed to get user info from request")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// write a new location based on the existing request pointed at the target service
 | 
				
			||||||
 | 
						location := &url.URL{}
 | 
				
			||||||
 | 
						location.Scheme = "https"
 | 
				
			||||||
 | 
						location.Host = host
 | 
				
			||||||
 | 
						location.Path = req.URL.Path
 | 
				
			||||||
 | 
						location.RawQuery = req.URL.Query().Encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newReq, cancelFn := apiserverproxyutil.NewRequestForProxy(location, req)
 | 
				
			||||||
 | 
						newReq.Header.Add(PeerProxiedHeader, "true")
 | 
				
			||||||
 | 
						defer cancelFn()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						proxyRoundTripper := transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), h.proxyTransport)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						delegate := &epmetrics.ResponseWriterDelegator{ResponseWriter: rw}
 | 
				
			||||||
 | 
						w := responsewriter.WrapForHTTP1Or2(delegate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, false, &responder{w: w, ctx: req.Context()})
 | 
				
			||||||
 | 
						handler.ServeHTTP(w, newReq)
 | 
				
			||||||
 | 
						// Increment the count of proxied requests
 | 
				
			||||||
 | 
						metrics.IncPeerProxiedRequest(req.Context(), strconv.Itoa(delegate.Status()))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *responder) Error(w http.ResponseWriter, req *http.Request, err error) {
 | 
				
			||||||
 | 
						klog.Errorf("Error while proxying request to destination apiserver: %v", err)
 | 
				
			||||||
 | 
						http.Error(w, err.Error(), http.StatusServiceUnavailable)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Adds a storageversion object to SVMap
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) addSV(obj interface{}) {
 | 
				
			||||||
 | 
						sv, ok := obj.(*v1alpha1.StorageVersion)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							klog.Errorf("Invalid StorageVersion provided to addSV()")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h.updateSVMap(nil, sv)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Updates the SVMap to delete old storageversion and add new storageversion
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) updateSV(oldObj interface{}, newObj interface{}) {
 | 
				
			||||||
 | 
						oldSV, ok := oldObj.(*v1alpha1.StorageVersion)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							klog.Errorf("Invalid StorageVersion provided to updateSV()")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						newSV, ok := newObj.(*v1alpha1.StorageVersion)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							klog.Errorf("Invalid StorageVersion provided to updateSV()")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h.updateSVMap(oldSV, newSV)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Deletes a storageversion object from SVMap
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) deleteSV(obj interface{}) {
 | 
				
			||||||
 | 
						sv, ok := obj.(*v1alpha1.StorageVersion)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							klog.Errorf("Invalid StorageVersion provided to deleteSV()")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h.updateSVMap(sv, nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Delete old storageversion, add new storagversion
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) updateSVMap(oldSV *v1alpha1.StorageVersion, newSV *v1alpha1.StorageVersion) {
 | 
				
			||||||
 | 
						if oldSV != nil {
 | 
				
			||||||
 | 
							// delete old SV entries
 | 
				
			||||||
 | 
							h.deleteSVFromMap(oldSV)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if newSV != nil {
 | 
				
			||||||
 | 
							// add new SV entries
 | 
				
			||||||
 | 
							h.addSVToMap(newSV)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) deleteSVFromMap(sv *v1alpha1.StorageVersion) {
 | 
				
			||||||
 | 
						// The name of storageversion is <group>.<resource>
 | 
				
			||||||
 | 
						splitInd := strings.LastIndex(sv.Name, ".")
 | 
				
			||||||
 | 
						group := sv.Name[:splitInd]
 | 
				
			||||||
 | 
						resource := sv.Name[splitInd+1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gvr := schema.GroupVersionResource{Group: group, Resource: resource}
 | 
				
			||||||
 | 
						for _, gr := range sv.Status.StorageVersions {
 | 
				
			||||||
 | 
							for _, version := range gr.ServedVersions {
 | 
				
			||||||
 | 
								versionSplit := strings.Split(version, "/")
 | 
				
			||||||
 | 
								if len(versionSplit) == 2 {
 | 
				
			||||||
 | 
									version = versionSplit[1]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								gvr.Version = version
 | 
				
			||||||
 | 
								h.svMap.Delete(gvr)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) addSVToMap(sv *v1alpha1.StorageVersion) {
 | 
				
			||||||
 | 
						// The name of storageversion is <group>.<resource>
 | 
				
			||||||
 | 
						splitInd := strings.LastIndex(sv.Name, ".")
 | 
				
			||||||
 | 
						group := sv.Name[:splitInd]
 | 
				
			||||||
 | 
						resource := sv.Name[splitInd+1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gvr := schema.GroupVersionResource{Group: group, Resource: resource}
 | 
				
			||||||
 | 
						for _, gr := range sv.Status.StorageVersions {
 | 
				
			||||||
 | 
							for _, version := range gr.ServedVersions {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// some versions have groups included in them, so get rid of the groups
 | 
				
			||||||
 | 
								versionSplit := strings.Split(version, "/")
 | 
				
			||||||
 | 
								if len(versionSplit) == 2 {
 | 
				
			||||||
 | 
									version = versionSplit[1]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								gvr.Version = version
 | 
				
			||||||
 | 
								apiserversi, _ := h.svMap.LoadOrStore(gvr, &sync.Map{})
 | 
				
			||||||
 | 
								apiservers := apiserversi.(*sync.Map)
 | 
				
			||||||
 | 
								apiservers.Store(gr.APIServerID, true)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,329 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2023 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package peerproxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						corev1 "k8s.io/api/core/v1"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/api/apitesting"
 | 
				
			||||||
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime/serializer"
 | 
				
			||||||
 | 
						utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/sets"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/authentication/user"
 | 
				
			||||||
 | 
						apifilters "k8s.io/apiserver/pkg/endpoints/filters"
 | 
				
			||||||
 | 
						apirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/features"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
 | 
						etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/storageversion"
 | 
				
			||||||
 | 
						utilfeature "k8s.io/apiserver/pkg/util/feature"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/util/peerproxy/metrics"
 | 
				
			||||||
 | 
						"k8s.io/client-go/informers"
 | 
				
			||||||
 | 
						"k8s.io/client-go/kubernetes/fake"
 | 
				
			||||||
 | 
						"k8s.io/client-go/transport"
 | 
				
			||||||
 | 
						featuregatetesting "k8s.io/component-base/featuregate/testing"
 | 
				
			||||||
 | 
						"k8s.io/component-base/metrics/legacyregistry"
 | 
				
			||||||
 | 
						"k8s.io/component-base/metrics/testutil"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						requestTimeout = 30 * time.Second
 | 
				
			||||||
 | 
						localServerId  = "local-apiserver"
 | 
				
			||||||
 | 
						remoteServerId = "remote-apiserver"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FakeSVMapData struct {
 | 
				
			||||||
 | 
						gvr      schema.GroupVersionResource
 | 
				
			||||||
 | 
						serverId string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type reconciler struct {
 | 
				
			||||||
 | 
						do       bool
 | 
				
			||||||
 | 
						publicIP string
 | 
				
			||||||
 | 
						serverId string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPeerProxy(t *testing.T) {
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							desc                 string
 | 
				
			||||||
 | 
							svdata               FakeSVMapData
 | 
				
			||||||
 | 
							informerFinishedSync bool
 | 
				
			||||||
 | 
							requestPath          string
 | 
				
			||||||
 | 
							peerproxiedHeader    string
 | 
				
			||||||
 | 
							expectedStatus       int
 | 
				
			||||||
 | 
							metrics              []string
 | 
				
			||||||
 | 
							want                 string
 | 
				
			||||||
 | 
							reconcilerConfig     reconciler
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:           "allow non resource requests",
 | 
				
			||||||
 | 
								requestPath:    "/foo/bar/baz",
 | 
				
			||||||
 | 
								expectedStatus: http.StatusOK,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:              "allow if already proxied once",
 | 
				
			||||||
 | 
								requestPath:       "/api/bar/baz",
 | 
				
			||||||
 | 
								expectedStatus:    http.StatusOK,
 | 
				
			||||||
 | 
								peerproxiedHeader: "true",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:                 "allow if unsynced informers",
 | 
				
			||||||
 | 
								requestPath:          "/api/bar/baz",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusOK,
 | 
				
			||||||
 | 
								informerFinishedSync: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:                 "allow if no storage version found",
 | 
				
			||||||
 | 
								requestPath:          "/api/bar/baz",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusOK,
 | 
				
			||||||
 | 
								informerFinishedSync: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// since if no server id is found, we pass request to next handler
 | 
				
			||||||
 | 
								//, and our last handler in local chain is an http ok handler
 | 
				
			||||||
 | 
								desc:                 "200 if no serverid found",
 | 
				
			||||||
 | 
								requestPath:          "/api/bar/baz",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusOK,
 | 
				
			||||||
 | 
								informerFinishedSync: true,
 | 
				
			||||||
 | 
								svdata: FakeSVMapData{
 | 
				
			||||||
 | 
									gvr: schema.GroupVersionResource{
 | 
				
			||||||
 | 
										Group:    "core",
 | 
				
			||||||
 | 
										Version:  "bar",
 | 
				
			||||||
 | 
										Resource: "baz"},
 | 
				
			||||||
 | 
									serverId: ""},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:                 "503 if no endpoint fetched from lease",
 | 
				
			||||||
 | 
								requestPath:          "/api/foo/bar",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusServiceUnavailable,
 | 
				
			||||||
 | 
								informerFinishedSync: true,
 | 
				
			||||||
 | 
								svdata: FakeSVMapData{
 | 
				
			||||||
 | 
									gvr: schema.GroupVersionResource{
 | 
				
			||||||
 | 
										Group:    "core",
 | 
				
			||||||
 | 
										Version:  "foo",
 | 
				
			||||||
 | 
										Resource: "bar"},
 | 
				
			||||||
 | 
									serverId: remoteServerId},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:                 "200 if locally serviceable",
 | 
				
			||||||
 | 
								requestPath:          "/api/foo/bar",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusOK,
 | 
				
			||||||
 | 
								informerFinishedSync: true,
 | 
				
			||||||
 | 
								svdata: FakeSVMapData{
 | 
				
			||||||
 | 
									gvr: schema.GroupVersionResource{
 | 
				
			||||||
 | 
										Group:    "core",
 | 
				
			||||||
 | 
										Version:  "foo",
 | 
				
			||||||
 | 
										Resource: "bar"},
 | 
				
			||||||
 | 
									serverId: localServerId},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:                 "503 unreachable peer bind address",
 | 
				
			||||||
 | 
								requestPath:          "/api/foo/bar",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusServiceUnavailable,
 | 
				
			||||||
 | 
								informerFinishedSync: true,
 | 
				
			||||||
 | 
								svdata: FakeSVMapData{
 | 
				
			||||||
 | 
									gvr: schema.GroupVersionResource{
 | 
				
			||||||
 | 
										Group:    "core",
 | 
				
			||||||
 | 
										Version:  "foo",
 | 
				
			||||||
 | 
										Resource: "bar"},
 | 
				
			||||||
 | 
									serverId: remoteServerId},
 | 
				
			||||||
 | 
								reconcilerConfig: reconciler{
 | 
				
			||||||
 | 
									do:       true,
 | 
				
			||||||
 | 
									publicIP: "1.2.3.4",
 | 
				
			||||||
 | 
									serverId: remoteServerId,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								metrics: []string{
 | 
				
			||||||
 | 
									"apiserver_rerouted_request_total",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP apiserver_rerouted_request_total [ALPHA] Total number of requests that were proxied to a peer kube apiserver because the local apiserver was not capable of serving it
 | 
				
			||||||
 | 
								# TYPE apiserver_rerouted_request_total counter
 | 
				
			||||||
 | 
								apiserver_rerouted_request_total{code="503"} 1
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								desc:                 "503 unreachable peer public address",
 | 
				
			||||||
 | 
								requestPath:          "/api/foo/bar",
 | 
				
			||||||
 | 
								expectedStatus:       http.StatusServiceUnavailable,
 | 
				
			||||||
 | 
								informerFinishedSync: true,
 | 
				
			||||||
 | 
								svdata: FakeSVMapData{
 | 
				
			||||||
 | 
									gvr: schema.GroupVersionResource{
 | 
				
			||||||
 | 
										Group:    "core",
 | 
				
			||||||
 | 
										Version:  "foo",
 | 
				
			||||||
 | 
										Resource: "bar"},
 | 
				
			||||||
 | 
									serverId: remoteServerId},
 | 
				
			||||||
 | 
								reconcilerConfig: reconciler{
 | 
				
			||||||
 | 
									do:       true,
 | 
				
			||||||
 | 
									publicIP: "1.2.3.4",
 | 
				
			||||||
 | 
									serverId: remoteServerId,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								metrics: []string{
 | 
				
			||||||
 | 
									"apiserver_rerouted_request_total",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: `
 | 
				
			||||||
 | 
								# HELP apiserver_rerouted_request_total [ALPHA] Total number of requests that were proxied to a peer kube apiserver because the local apiserver was not capable of serving it
 | 
				
			||||||
 | 
								# TYPE apiserver_rerouted_request_total counter
 | 
				
			||||||
 | 
								apiserver_rerouted_request_total{code="503"} 2
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metrics.Register()
 | 
				
			||||||
 | 
						for _, tt := range testCases {
 | 
				
			||||||
 | 
							t.Run(tt.desc, func(t *testing.T) {
 | 
				
			||||||
 | 
								lastHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
									w.Write([]byte("OK"))
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								reconciler := newFakePeerEndpointReconciler(t)
 | 
				
			||||||
 | 
								handler := newHandlerChain(t, lastHandler, reconciler, tt.informerFinishedSync, tt.svdata)
 | 
				
			||||||
 | 
								server, requestGetter := createHTTP2ServerWithClient(handler, requestTimeout*2)
 | 
				
			||||||
 | 
								defer server.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if tt.reconcilerConfig.do {
 | 
				
			||||||
 | 
									// need to enable feature flags first
 | 
				
			||||||
 | 
									defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIServerIdentity, true)()
 | 
				
			||||||
 | 
									defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StorageVersionAPI, true)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									reconciler.UpdateLease(tt.reconcilerConfig.serverId,
 | 
				
			||||||
 | 
										tt.reconcilerConfig.publicIP,
 | 
				
			||||||
 | 
										[]corev1.EndpointPort{{Name: "foo",
 | 
				
			||||||
 | 
											Port: 8080, Protocol: "TCP"}})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								req, err := http.NewRequest(http.MethodGet, server.URL+tt.requestPath, nil)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("failed to create new http request - %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								req.Header.Set(PeerProxiedHeader, tt.peerproxiedHeader)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								resp, _ := requestGetter(req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// compare response
 | 
				
			||||||
 | 
								assert.Equal(t, tt.expectedStatus, resp.StatusCode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// compare metric
 | 
				
			||||||
 | 
								if tt.want != "" {
 | 
				
			||||||
 | 
									if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.want), tt.metrics...); err != nil {
 | 
				
			||||||
 | 
										t.Fatal(err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newFakePeerEndpointReconciler(t *testing.T) reconcilers.PeerEndpointLeaseReconciler {
 | 
				
			||||||
 | 
						server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
 | 
				
			||||||
 | 
						t.Cleanup(func() { server.Terminate(t) })
 | 
				
			||||||
 | 
						scheme := runtime.NewScheme()
 | 
				
			||||||
 | 
						metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
 | 
				
			||||||
 | 
						//utilruntime.Must(core.AddToScheme(scheme))
 | 
				
			||||||
 | 
						utilruntime.Must(corev1.AddToScheme(scheme))
 | 
				
			||||||
 | 
						utilruntime.Must(scheme.SetVersionPriority(corev1.SchemeGroupVersion))
 | 
				
			||||||
 | 
						codecs := serializer.NewCodecFactory(scheme)
 | 
				
			||||||
 | 
						sc.Codec = apitesting.TestStorageCodec(codecs, corev1.SchemeGroupVersion)
 | 
				
			||||||
 | 
						config := *sc.ForResource(schema.GroupResource{Resource: "endpoints"})
 | 
				
			||||||
 | 
						baseKey := "/" + uuid.New().String() + "/peer-testleases/"
 | 
				
			||||||
 | 
						leaseTime := 1 * time.Minute
 | 
				
			||||||
 | 
						reconciler, err := reconcilers.NewPeerEndpointLeaseReconciler(&config, baseKey, leaseTime)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error creating storage: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return reconciler
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newHandlerChain(t *testing.T, handler http.Handler, reconciler reconcilers.PeerEndpointLeaseReconciler, informerFinishedSync bool, svdata FakeSVMapData) http.Handler {
 | 
				
			||||||
 | 
						// Add peerproxy handler
 | 
				
			||||||
 | 
						s := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion()
 | 
				
			||||||
 | 
						peerProxyHandler, err := newFakePeerProxyHandler(informerFinishedSync, reconciler, svdata, localServerId, s)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error creating peer proxy handler: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						peerProxyHandler.finishedSync.Store(informerFinishedSync)
 | 
				
			||||||
 | 
						handler = peerProxyHandler.WrapHandler(handler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add user info
 | 
				
			||||||
 | 
						handler = withFakeUser(handler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add requestInfo handler
 | 
				
			||||||
 | 
						requestInfoFactory := &apirequest.RequestInfoFactory{APIPrefixes: sets.NewString("apis", "api"), GrouplessAPIPrefixes: sets.NewString("api")}
 | 
				
			||||||
 | 
						handler = apifilters.WithRequestInfo(handler, requestInfoFactory)
 | 
				
			||||||
 | 
						return handler
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newFakePeerProxyHandler(informerFinishedSync bool, reconciler reconcilers.PeerEndpointLeaseReconciler, svdata FakeSVMapData, id string, s runtime.NegotiatedSerializer) (*peerProxyHandler, error) {
 | 
				
			||||||
 | 
						clientset := fake.NewSimpleClientset()
 | 
				
			||||||
 | 
						informerFactory := informers.NewSharedInformerFactory(clientset, 0)
 | 
				
			||||||
 | 
						clientConfig := &transport.Config{
 | 
				
			||||||
 | 
							TLS: transport.TLSConfig{
 | 
				
			||||||
 | 
								Insecure: false,
 | 
				
			||||||
 | 
							}}
 | 
				
			||||||
 | 
						proxyRoundTripper, err := transport.New(clientConfig)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ppI := NewPeerProxyHandler(informerFactory, storageversion.NewDefaultManager(), proxyRoundTripper, id, reconciler, s)
 | 
				
			||||||
 | 
						if testDataExists(svdata.gvr) {
 | 
				
			||||||
 | 
							ppI.addToStorageVersionMap(svdata.gvr, svdata.serverId)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ppI, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *peerProxyHandler) addToStorageVersionMap(gvr schema.GroupVersionResource, serverId string) {
 | 
				
			||||||
 | 
						apiserversi, _ := h.svMap.LoadOrStore(gvr, &sync.Map{})
 | 
				
			||||||
 | 
						apiservers := apiserversi.(*sync.Map)
 | 
				
			||||||
 | 
						if serverId != "" {
 | 
				
			||||||
 | 
							apiservers.Store(serverId, true)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func testDataExists(gvr schema.GroupVersionResource) bool {
 | 
				
			||||||
 | 
						return gvr.Group != "" && gvr.Version != "" && gvr.Resource != ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func withFakeUser(handler http.Handler) http.Handler {
 | 
				
			||||||
 | 
						return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							r = r.WithContext(apirequest.WithUser(r.Context(), &user.DefaultInfo{
 | 
				
			||||||
 | 
								Groups: r.Header["Groups"],
 | 
				
			||||||
 | 
							}))
 | 
				
			||||||
 | 
							handler.ServeHTTP(w, r)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// returns a started http2 server, with a client function to send request to the server.
 | 
				
			||||||
 | 
					func createHTTP2ServerWithClient(handler http.Handler, clientTimeout time.Duration) (*httptest.Server, func(req *http.Request) (*http.Response, error)) {
 | 
				
			||||||
 | 
						server := httptest.NewUnstartedServer(handler)
 | 
				
			||||||
 | 
						server.EnableHTTP2 = true
 | 
				
			||||||
 | 
						server.StartTLS()
 | 
				
			||||||
 | 
						cli := server.Client()
 | 
				
			||||||
 | 
						cli.Timeout = clientTimeout
 | 
				
			||||||
 | 
						return server, func(req *http.Request) (*http.Response, error) {
 | 
				
			||||||
 | 
							return cli.Do(req)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -17,17 +17,30 @@ limitations under the License.
 | 
				
			|||||||
package proxy
 | 
					package proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"math/rand"
 | 
						"math/rand"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/api/core/v1"
 | 
						"k8s.io/api/core/v1"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/api/errors"
 | 
						"k8s.io/apimachinery/pkg/api/errors"
 | 
				
			||||||
 | 
						utilnet "k8s.io/apimachinery/pkg/util/net"
 | 
				
			||||||
 | 
						auditinternal "k8s.io/apiserver/pkg/apis/audit"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/audit"
 | 
				
			||||||
 | 
						genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
				
			||||||
	listersv1 "k8s.io/client-go/listers/core/v1"
 | 
						listersv1 "k8s.io/client-go/listers/core/v1"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// taken from https://github.com/kubernetes/kubernetes/blob/release-1.27/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go#L47
 | 
				
			||||||
 | 
						aggregatedDiscoveryTimeout = 5 * time.Second
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// findServicePort finds the service port by name or numerically.
 | 
					// findServicePort finds the service port by name or numerically.
 | 
				
			||||||
func findServicePort(svc *v1.Service, port int32) (*v1.ServicePort, error) {
 | 
					func findServicePort(svc *v1.Service, port int32) (*v1.ServicePort, error) {
 | 
				
			||||||
	for _, svcPort := range svc.Spec.Ports {
 | 
						for _, svcPort := range svc.Spec.Ports {
 | 
				
			||||||
@@ -117,3 +130,34 @@ func ResolveCluster(services listersv1.ServiceLister, namespace, id string, port
 | 
				
			|||||||
		return nil, fmt.Errorf("unsupported service type %q", svc.Spec.Type)
 | 
							return nil, fmt.Errorf("unsupported service type %q", svc.Spec.Type)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewRequestForProxy returns a shallow copy of the original request with a context that may include a timeout for discovery requests
 | 
				
			||||||
 | 
					func NewRequestForProxy(location *url.URL, req *http.Request) (*http.Request, context.CancelFunc) {
 | 
				
			||||||
 | 
						newCtx := req.Context()
 | 
				
			||||||
 | 
						cancelFn := func() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if requestInfo, ok := genericapirequest.RequestInfoFrom(req.Context()); ok {
 | 
				
			||||||
 | 
							// trim leading and trailing slashes. Then "/apis/group/version" requests are for discovery, so if we have exactly three
 | 
				
			||||||
 | 
							// segments that we are going to proxy, we have a discovery request.
 | 
				
			||||||
 | 
							if !requestInfo.IsResourceRequest && len(strings.Split(strings.Trim(requestInfo.Path, "/"), "/")) == 3 {
 | 
				
			||||||
 | 
								// discovery requests are used by kubectl and others to determine which resources a server has.  This is a cheap call that
 | 
				
			||||||
 | 
								// should be fast for every aggregated apiserver.  Latency for aggregation is expected to be low (as for all extensions)
 | 
				
			||||||
 | 
								// so forcing a short timeout here helps responsiveness of all clients.
 | 
				
			||||||
 | 
								newCtx, cancelFn = context.WithTimeout(newCtx, aggregatedDiscoveryTimeout)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// WithContext creates a shallow clone of the request with the same context.
 | 
				
			||||||
 | 
						newReq := req.WithContext(newCtx)
 | 
				
			||||||
 | 
						newReq.Header = utilnet.CloneHeader(req.Header)
 | 
				
			||||||
 | 
						newReq.URL = location
 | 
				
			||||||
 | 
						newReq.Host = location.Host
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If the original request has an audit ID, let's make sure we propagate this
 | 
				
			||||||
 | 
						// to the aggregated server.
 | 
				
			||||||
 | 
						if auditID, found := audit.AuditIDFrom(req.Context()); found {
 | 
				
			||||||
 | 
							newReq.Header.Set(auditinternal.HeaderAuditID, string(auditID))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return newReq, cancelFn
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,7 @@ import (
 | 
				
			|||||||
	"k8s.io/apimachinery/pkg/util/wait"
 | 
						"k8s.io/apimachinery/pkg/util/wait"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
 | 
						"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
 | 
				
			||||||
	genericfeatures "k8s.io/apiserver/pkg/features"
 | 
						genericfeatures "k8s.io/apiserver/pkg/features"
 | 
				
			||||||
 | 
						peerreconcilers "k8s.io/apiserver/pkg/reconcilers"
 | 
				
			||||||
	genericapiserver "k8s.io/apiserver/pkg/server"
 | 
						genericapiserver "k8s.io/apiserver/pkg/server"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/dynamiccertificates"
 | 
						"k8s.io/apiserver/pkg/server/dynamiccertificates"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/egressselector"
 | 
						"k8s.io/apiserver/pkg/server/egressselector"
 | 
				
			||||||
@@ -76,6 +77,16 @@ const (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ExtraConfig represents APIServices-specific configuration
 | 
					// ExtraConfig represents APIServices-specific configuration
 | 
				
			||||||
type ExtraConfig struct {
 | 
					type ExtraConfig struct {
 | 
				
			||||||
 | 
						// PeerCAFile is the ca bundle used by this kube-apiserver to verify peer apiservers'
 | 
				
			||||||
 | 
						// serving certs when routing a request to the peer in the case the request can not be served
 | 
				
			||||||
 | 
						// locally due to version skew.
 | 
				
			||||||
 | 
						PeerCAFile string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PeerAdvertiseAddress is the IP for this kube-apiserver which is used by peer apiservers to route a request
 | 
				
			||||||
 | 
						// to this apiserver. This happens in cases where the peer is not able to serve the request due to
 | 
				
			||||||
 | 
						// version skew. If unset, AdvertiseAddress/BindAddress will be used.
 | 
				
			||||||
 | 
						PeerAdvertiseAddress peerreconcilers.PeerAdvertiseAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ProxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
 | 
						// ProxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
 | 
				
			||||||
	// this to confirm the proxy's identity
 | 
						// this to confirm the proxy's identity
 | 
				
			||||||
	ProxyClientCertFile string
 | 
						ProxyClientCertFile string
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,23 +17,18 @@ limitations under the License.
 | 
				
			|||||||
package apiserver
 | 
					package apiserver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/httpstream"
 | 
						"k8s.io/apimachinery/pkg/util/httpstream"
 | 
				
			||||||
	utilnet "k8s.io/apimachinery/pkg/util/net"
 | 
					 | 
				
			||||||
	"k8s.io/apimachinery/pkg/util/proxy"
 | 
						"k8s.io/apimachinery/pkg/util/proxy"
 | 
				
			||||||
	auditinternal "k8s.io/apiserver/pkg/apis/audit"
 | 
					 | 
				
			||||||
	"k8s.io/apiserver/pkg/audit"
 | 
					 | 
				
			||||||
	"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
 | 
						"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
 | 
				
			||||||
	endpointmetrics "k8s.io/apiserver/pkg/endpoints/metrics"
 | 
						endpointmetrics "k8s.io/apiserver/pkg/endpoints/metrics"
 | 
				
			||||||
	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
						genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
				
			||||||
	utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
 | 
						utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
 | 
				
			||||||
 | 
						apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/util/x509metrics"
 | 
						"k8s.io/apiserver/pkg/util/x509metrics"
 | 
				
			||||||
	"k8s.io/client-go/transport"
 | 
						"k8s.io/client-go/transport"
 | 
				
			||||||
	"k8s.io/klog/v2"
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
@@ -43,8 +38,6 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	aggregatorComponent string = "aggregator"
 | 
						aggregatorComponent string = "aggregator"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	aggregatedDiscoveryTimeout = 5 * time.Second
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type certKeyFunc func() ([]byte, []byte)
 | 
					type certKeyFunc func() ([]byte, []byte)
 | 
				
			||||||
@@ -149,7 +142,7 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
	location.Path = req.URL.Path
 | 
						location.Path = req.URL.Path
 | 
				
			||||||
	location.RawQuery = req.URL.Query().Encode()
 | 
						location.RawQuery = req.URL.Query().Encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	newReq, cancelFn := newRequestForProxy(location, req)
 | 
						newReq, cancelFn := apiserverproxyutil.NewRequestForProxy(location, req)
 | 
				
			||||||
	defer cancelFn()
 | 
						defer cancelFn()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if handlingInfo.proxyRoundTripper == nil {
 | 
						if handlingInfo.proxyRoundTripper == nil {
 | 
				
			||||||
@@ -177,37 +170,6 @@ func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
	handler.ServeHTTP(w, newReq)
 | 
						handler.ServeHTTP(w, newReq)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// newRequestForProxy returns a shallow copy of the original request with a context that may include a timeout for discovery requests
 | 
					 | 
				
			||||||
func newRequestForProxy(location *url.URL, req *http.Request) (*http.Request, context.CancelFunc) {
 | 
					 | 
				
			||||||
	newCtx := req.Context()
 | 
					 | 
				
			||||||
	cancelFn := func() {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if requestInfo, ok := genericapirequest.RequestInfoFrom(req.Context()); ok {
 | 
					 | 
				
			||||||
		// trim leading and trailing slashes. Then "/apis/group/version" requests are for discovery, so if we have exactly three
 | 
					 | 
				
			||||||
		// segments that we are going to proxy, we have a discovery request.
 | 
					 | 
				
			||||||
		if !requestInfo.IsResourceRequest && len(strings.Split(strings.Trim(requestInfo.Path, "/"), "/")) == 3 {
 | 
					 | 
				
			||||||
			// discovery requests are used by kubectl and others to determine which resources a server has.  This is a cheap call that
 | 
					 | 
				
			||||||
			// should be fast for every aggregated apiserver.  Latency for aggregation is expected to be low (as for all extensions)
 | 
					 | 
				
			||||||
			// so forcing a short timeout here helps responsiveness of all clients.
 | 
					 | 
				
			||||||
			newCtx, cancelFn = context.WithTimeout(newCtx, aggregatedDiscoveryTimeout)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// WithContext creates a shallow clone of the request with the same context.
 | 
					 | 
				
			||||||
	newReq := req.WithContext(newCtx)
 | 
					 | 
				
			||||||
	newReq.Header = utilnet.CloneHeader(req.Header)
 | 
					 | 
				
			||||||
	newReq.URL = location
 | 
					 | 
				
			||||||
	newReq.Host = location.Host
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If the original request has an audit ID, let's make sure we propagate this
 | 
					 | 
				
			||||||
	// to the aggregated server.
 | 
					 | 
				
			||||||
	if auditID, found := audit.AuditIDFrom(req.Context()); found {
 | 
					 | 
				
			||||||
		newReq.Header.Set(auditinternal.HeaderAuditID, string(auditID))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return newReq, cancelFn
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// responder implements rest.Responder for assisting a connector in writing objects or errors.
 | 
					// responder implements rest.Responder for assisting a connector in writing objects or errors.
 | 
				
			||||||
type responder struct {
 | 
					type responder struct {
 | 
				
			||||||
	w http.ResponseWriter
 | 
						w http.ResponseWriter
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,7 @@ import (
 | 
				
			|||||||
	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
						genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/server/egressselector"
 | 
						"k8s.io/apiserver/pkg/server/egressselector"
 | 
				
			||||||
	utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
 | 
						utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol"
 | 
				
			||||||
 | 
						apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy"
 | 
				
			||||||
	"k8s.io/component-base/metrics"
 | 
						"k8s.io/component-base/metrics"
 | 
				
			||||||
	"k8s.io/component-base/metrics/legacyregistry"
 | 
						"k8s.io/component-base/metrics/legacyregistry"
 | 
				
			||||||
	apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
 | 
						apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
 | 
				
			||||||
@@ -747,7 +748,7 @@ func TestGetContextForNewRequest(t *testing.T) {
 | 
				
			|||||||
		location.Path = req.URL.Path
 | 
							location.Path = req.URL.Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		nestedReq := req.WithContext(genericapirequest.WithRequestInfo(req.Context(), &genericapirequest.RequestInfo{Path: req.URL.Path}))
 | 
							nestedReq := req.WithContext(genericapirequest.WithRequestInfo(req.Context(), &genericapirequest.RequestInfo{Path: req.URL.Path}))
 | 
				
			||||||
		newReq, cancelFn := newRequestForProxy(location, nestedReq)
 | 
							newReq, cancelFn := apiserverproxyutil.NewRequestForProxy(location, nestedReq)
 | 
				
			||||||
		defer cancelFn()
 | 
							defer cancelFn()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		theproxy := proxy.NewUpgradeAwareHandler(location, server.Client().Transport, true, false, &responder{w: w})
 | 
							theproxy := proxy.NewUpgradeAwareHandler(location, server.Client().Transport, true, false, &responder{w: w})
 | 
				
			||||||
@@ -802,7 +803,7 @@ func TestNewRequestForProxyWithAuditID(t *testing.T) {
 | 
				
			|||||||
				req = req.WithContext(ctx)
 | 
									req = req.WithContext(ctx)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			newReq, _ := newRequestForProxy(req.URL, req)
 | 
								newReq, _ := apiserverproxyutil.NewRequestForProxy(req.URL, req)
 | 
				
			||||||
			if newReq == nil {
 | 
								if newReq == nil {
 | 
				
			||||||
				t.Fatal("expected a non nil Request object")
 | 
									t.Fatal("expected a non nil Request object")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -598,6 +598,7 @@ resources:
 | 
				
			|||||||
			// the following resources are not encrypted as they are not REST APIs and hence are not expected
 | 
								// the following resources are not encrypted as they are not REST APIs and hence are not expected
 | 
				
			||||||
			// to be encrypted because it would be impossible to perform a storage migration on them
 | 
								// to be encrypted because it would be impossible to perform a storage migration on them
 | 
				
			||||||
			if strings.Contains(kv.String(), "masterleases") ||
 | 
								if strings.Contains(kv.String(), "masterleases") ||
 | 
				
			||||||
 | 
									strings.Contains(kv.String(), "peerserverleases") ||
 | 
				
			||||||
				strings.Contains(kv.String(), "serviceips") ||
 | 
									strings.Contains(kv.String(), "serviceips") ||
 | 
				
			||||||
				strings.Contains(kv.String(), "servicenodeports") {
 | 
									strings.Contains(kv.String(), "servicenodeports") {
 | 
				
			||||||
				// assert that these resources are not encrypted with any provider
 | 
									// assert that these resources are not encrypted with any provider
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							@@ -1514,6 +1514,7 @@ k8s.io/apiserver/pkg/endpoints/warning
 | 
				
			|||||||
k8s.io/apiserver/pkg/features
 | 
					k8s.io/apiserver/pkg/features
 | 
				
			||||||
k8s.io/apiserver/pkg/quota/v1
 | 
					k8s.io/apiserver/pkg/quota/v1
 | 
				
			||||||
k8s.io/apiserver/pkg/quota/v1/generic
 | 
					k8s.io/apiserver/pkg/quota/v1/generic
 | 
				
			||||||
 | 
					k8s.io/apiserver/pkg/reconcilers
 | 
				
			||||||
k8s.io/apiserver/pkg/registry/generic
 | 
					k8s.io/apiserver/pkg/registry/generic
 | 
				
			||||||
k8s.io/apiserver/pkg/registry/generic/registry
 | 
					k8s.io/apiserver/pkg/registry/generic/registry
 | 
				
			||||||
k8s.io/apiserver/pkg/registry/generic/rest
 | 
					k8s.io/apiserver/pkg/registry/generic/rest
 | 
				
			||||||
@@ -1574,6 +1575,8 @@ k8s.io/apiserver/pkg/util/flowcontrol/request
 | 
				
			|||||||
k8s.io/apiserver/pkg/util/flushwriter
 | 
					k8s.io/apiserver/pkg/util/flushwriter
 | 
				
			||||||
k8s.io/apiserver/pkg/util/notfoundhandler
 | 
					k8s.io/apiserver/pkg/util/notfoundhandler
 | 
				
			||||||
k8s.io/apiserver/pkg/util/openapi
 | 
					k8s.io/apiserver/pkg/util/openapi
 | 
				
			||||||
 | 
					k8s.io/apiserver/pkg/util/peerproxy
 | 
				
			||||||
 | 
					k8s.io/apiserver/pkg/util/peerproxy/metrics
 | 
				
			||||||
k8s.io/apiserver/pkg/util/proxy
 | 
					k8s.io/apiserver/pkg/util/proxy
 | 
				
			||||||
k8s.io/apiserver/pkg/util/shufflesharding
 | 
					k8s.io/apiserver/pkg/util/shufflesharding
 | 
				
			||||||
k8s.io/apiserver/pkg/util/webhook
 | 
					k8s.io/apiserver/pkg/util/webhook
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user