k8s.io/apiserver@v0.31.1/pkg/reconcilers/peer_endpoint_lease.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package reconcilers 18 19 import ( 20 "context" 21 "fmt" 22 "net" 23 "net/http" 24 "path" 25 "strconv" 26 "sync" 27 "sync/atomic" 28 "time" 29 30 "k8s.io/client-go/kubernetes" 31 "k8s.io/klog/v2" 32 33 corev1 "k8s.io/api/core/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 kruntime "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/util/runtime" 37 "k8s.io/apimachinery/pkg/util/wait" 38 apirequest "k8s.io/apiserver/pkg/endpoints/request" 39 "k8s.io/apiserver/pkg/registry/rest" 40 "k8s.io/apiserver/pkg/storage" 41 "k8s.io/apiserver/pkg/storage/storagebackend" 42 storagefactory "k8s.io/apiserver/pkg/storage/storagebackend/factory" 43 ) 44 45 const ( 46 APIServerIdentityLabel = "apiserverIdentity" 47 ) 48 49 type PeerAdvertiseAddress struct { 50 PeerAdvertiseIP string 51 PeerAdvertisePort string 52 } 53 54 type peerEndpointLeases struct { 55 storage storage.Interface 56 destroyFn func() 57 baseKey string 58 leaseTime time.Duration 59 } 60 61 type PeerEndpointLeaseReconciler interface { 62 // GetEndpoint retrieves the endpoint for a given apiserverId 63 GetEndpoint(serverId string) (string, error) 64 // UpdateLease updates the ip and port of peer servers 65 UpdateLease(serverId string, ip string, endpointPorts []corev1.EndpointPort) error 66 // RemoveEndpoints removes this apiserver's peer endpoint lease. 67 RemoveLease(serverId string) error 68 // Destroy cleans up everything on shutdown. 69 Destroy() 70 // StopReconciling turns any later ReconcileEndpoints call into a noop. 71 StopReconciling() 72 } 73 74 type peerEndpointLeaseReconciler struct { 75 serverLeases *peerEndpointLeases 76 stopReconcilingCalled atomic.Bool 77 } 78 79 // NewPeerEndpointLeaseReconciler creates a new peer endpoint lease reconciler 80 func NewPeerEndpointLeaseReconciler(config *storagebackend.ConfigForResource, baseKey string, leaseTime time.Duration) (PeerEndpointLeaseReconciler, error) { 81 // note that newFunc, newListFunc and resourcePrefix 82 // can be left blank unless the storage.Watch method is used 83 leaseStorage, destroyFn, err := storagefactory.Create(*config, nil, nil, "") 84 if err != nil { 85 return nil, fmt.Errorf("error creating storage factory: %v", err) 86 } 87 var once sync.Once 88 return &peerEndpointLeaseReconciler{ 89 serverLeases: &peerEndpointLeases{ 90 storage: leaseStorage, 91 destroyFn: func() { once.Do(destroyFn) }, 92 baseKey: baseKey, 93 leaseTime: leaseTime, 94 }, 95 }, nil 96 } 97 98 // PeerEndpointController is the controller manager for updating the peer endpoint leases. 99 // This provides a separate independent reconciliation loop for peer endpoint leases 100 // which ensures that the peer kube-apiservers are fetching the updated endpoint info for a given apiserver 101 // in the case when the peer wants to proxy the request to the given apiserver because it can not serve the 102 // request itself due to version mismatch. 103 type PeerEndpointLeaseController struct { 104 reconciler PeerEndpointLeaseReconciler 105 endpointInterval time.Duration 106 serverId string 107 // peeraddress stores the IP and port of this kube-apiserver. Used by peer kube-apiservers to 108 // route request to this apiserver in case of a version skew. 109 peeraddress string 110 111 client kubernetes.Interface 112 113 lock sync.Mutex 114 stopCh chan struct{} // closed by Stop() 115 } 116 117 func New(serverId string, peeraddress string, 118 reconciler PeerEndpointLeaseReconciler, endpointInterval time.Duration, client kubernetes.Interface) *PeerEndpointLeaseController { 119 return &PeerEndpointLeaseController{ 120 reconciler: reconciler, 121 serverId: serverId, 122 // peeraddress stores the IP and port of this kube-apiserver. Used by peer kube-apiservers to 123 // route request to this apiserver in case of a version skew. 124 peeraddress: peeraddress, 125 endpointInterval: endpointInterval, 126 client: client, 127 stopCh: make(chan struct{}), 128 } 129 } 130 131 // Start begins the peer endpoint lease reconciler loop that must exist for bootstrapping 132 // a cluster. 133 func (c *PeerEndpointLeaseController) Start(stopCh <-chan struct{}) { 134 localStopCh := make(chan struct{}) 135 go func() { 136 defer close(localStopCh) 137 select { 138 case <-stopCh: // from Start 139 case <-c.stopCh: // from Stop 140 } 141 }() 142 go c.Run(localStopCh) 143 } 144 145 // RunPeerEndpointReconciler periodically updates the peer endpoint leases 146 func (c *PeerEndpointLeaseController) Run(stopCh <-chan struct{}) { 147 // wait until process is ready 148 wait.PollImmediateUntil(100*time.Millisecond, func() (bool, error) { 149 var code int 150 c.client.CoreV1().RESTClient().Get().AbsPath("/readyz").Do(context.TODO()).StatusCode(&code) 151 return code == http.StatusOK, nil 152 }, stopCh) 153 154 wait.NonSlidingUntil(func() { 155 if err := c.UpdatePeerEndpointLeases(); err != nil { 156 runtime.HandleError(fmt.Errorf("unable to update peer endpoint leases: %v", err)) 157 } 158 }, c.endpointInterval, stopCh) 159 } 160 161 // Stop cleans up this apiserver's peer endpoint leases. 162 func (c *PeerEndpointLeaseController) Stop() { 163 c.lock.Lock() 164 defer c.lock.Unlock() 165 166 select { 167 case <-c.stopCh: 168 return // only close once 169 default: 170 close(c.stopCh) 171 } 172 finishedReconciling := make(chan struct{}) 173 go func() { 174 defer close(finishedReconciling) 175 klog.Infof("Shutting down peer endpoint lease reconciler") 176 // stop reconciliation 177 c.reconciler.StopReconciling() 178 179 // Ensure that there will be no race condition with the ReconcileEndpointLeases. 180 if err := c.reconciler.RemoveLease(c.serverId); err != nil { 181 klog.Errorf("Unable to remove peer endpoint leases: %v", err) 182 } 183 c.reconciler.Destroy() 184 }() 185 186 select { 187 case <-finishedReconciling: 188 // done 189 case <-time.After(2 * c.endpointInterval): 190 // don't block server shutdown forever if we can't reach etcd to remove ourselves 191 klog.Warning("peer_endpoint_controller's RemoveEndpoints() timed out") 192 } 193 } 194 195 // UpdatePeerEndpointLeases attempts to update the peer endpoint leases. 196 func (c *PeerEndpointLeaseController) UpdatePeerEndpointLeases() error { 197 host, port, err := net.SplitHostPort(c.peeraddress) 198 if err != nil { 199 return err 200 } 201 202 p, err := strconv.Atoi(port) 203 if err != nil { 204 return err 205 } 206 endpointPorts := createEndpointPortSpec(p, "https") 207 208 // Ensure that there will be no race condition with the RemoveEndpointLeases. 209 c.lock.Lock() 210 defer c.lock.Unlock() 211 212 // Refresh the TTL on our key, independently of whether any error or 213 // update conflict happens below. This makes sure that at least some of 214 // the servers will add our endpoint lease. 215 if err := c.reconciler.UpdateLease(c.serverId, host, endpointPorts); err != nil { 216 return err 217 } 218 return nil 219 } 220 221 // UpdateLease resets the TTL on a server IP in storage 222 // UpdateLease will create a new key if it doesn't exist. 223 // We use the first element in endpointPorts as a part of the lease's base key 224 // This is done to support out tests that simulate 2 apiservers running on the same ip but 225 // different ports 226 227 // It will also do the following if UnknownVersionInteroperabilityProxy feature is enabled 228 // 1. store the apiserverId as a label 229 // 2. store the values passed to --peer-advertise-ip and --peer-advertise-port flags to kube-apiserver as an annotation 230 // with value of format <ip:port> 231 func (r *peerEndpointLeaseReconciler) UpdateLease(serverId string, ip string, endpointPorts []corev1.EndpointPort) error { 232 // reconcile endpoints only if apiserver was not shutdown 233 if r.stopReconcilingCalled.Load() { 234 return nil 235 } 236 237 // we use the serverID as the key to avoid using the server IP, port as the key. 238 // note: this means that this lease doesn't enforce mutual exclusion of ip/port usage between apiserver. 239 key := path.Join(r.serverLeases.baseKey, serverId) 240 return r.serverLeases.storage.GuaranteedUpdate(apirequest.NewDefaultContext(), key, &corev1.Endpoints{}, true, nil, func(input kruntime.Object, respMeta storage.ResponseMeta) (kruntime.Object, *uint64, error) { 241 existing := input.(*corev1.Endpoints) 242 existing.Subsets = []corev1.EndpointSubset{ 243 { 244 Addresses: []corev1.EndpointAddress{{IP: ip}}, 245 Ports: endpointPorts, 246 }, 247 } 248 249 // store this server's identity (serverId) as a label. This will be used by 250 // peers to find the IP of this server when the peer can not serve a request 251 // due to version skew. 252 if existing.Labels == nil { 253 existing.Labels = map[string]string{} 254 } 255 existing.Labels[APIServerIdentityLabel] = serverId 256 257 // leaseTime needs to be in seconds 258 leaseTime := uint64(r.serverLeases.leaseTime / time.Second) 259 260 // NB: GuaranteedUpdate does not perform the store operation unless 261 // something changed between load and store (not including resource 262 // version), meaning we can't refresh the TTL without actually 263 // changing a field. 264 existing.Generation++ 265 266 klog.V(6).Infof("Resetting TTL on server IP %q listed in storage to %v", ip, leaseTime) 267 return existing, &leaseTime, nil 268 }, nil) 269 } 270 271 // ListLeases retrieves a list of the current server IPs from storage 272 func (r *peerEndpointLeaseReconciler) ListLeases() ([]string, error) { 273 storageOpts := storage.ListOptions{ 274 ResourceVersion: "0", 275 ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, 276 Predicate: storage.Everything, 277 Recursive: true, 278 } 279 ipInfoList, err := r.getIpInfoList(storageOpts) 280 if err != nil { 281 return nil, err 282 } 283 ipList := make([]string, 0, len(ipInfoList.Items)) 284 for _, ip := range ipInfoList.Items { 285 if len(ip.Subsets) > 0 && len(ip.Subsets[0].Addresses) > 0 && len(ip.Subsets[0].Addresses[0].IP) > 0 { 286 ipList = append(ipList, ip.Subsets[0].Addresses[0].IP) 287 } 288 } 289 klog.V(6).Infof("Current server IPs listed in storage are %v", ipList) 290 return ipList, nil 291 } 292 293 // GetLease retrieves the server IP and port for a specific server id 294 func (r *peerEndpointLeaseReconciler) GetLease(serverId string) (string, error) { 295 var fullAddr string 296 if serverId == "" { 297 return "", fmt.Errorf("error getting endpoint for serverId: empty serverId") 298 } 299 storageOpts := storage.ListOptions{ 300 ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, 301 Predicate: storage.Everything, 302 Recursive: true, 303 } 304 ipInfoList, err := r.getIpInfoList(storageOpts) 305 if err != nil { 306 return "", err 307 } 308 309 for _, ip := range ipInfoList.Items { 310 if ip.Labels[APIServerIdentityLabel] == serverId { 311 if len(ip.Subsets) > 0 { 312 var ipStr, portStr string 313 if len(ip.Subsets[0].Addresses) > 0 { 314 if len(ip.Subsets[0].Addresses[0].IP) > 0 { 315 ipStr = ip.Subsets[0].Addresses[0].IP 316 } 317 } 318 if len(ip.Subsets[0].Ports) > 0 { 319 portStr = fmt.Sprint(ip.Subsets[0].Ports[0].Port) 320 } 321 fullAddr = net.JoinHostPort(ipStr, portStr) 322 break 323 } 324 } 325 } 326 klog.V(6).Infof("Fetched this server IP for the specified apiserverId %v, %v", serverId, fullAddr) 327 return fullAddr, nil 328 } 329 330 func (r *peerEndpointLeaseReconciler) StopReconciling() { 331 r.stopReconcilingCalled.Store(true) 332 } 333 334 // RemoveLease removes the lease on a server IP in storage 335 // We use the first element in endpointPorts as a part of the lease's base key 336 // This is done to support out tests that simulate 2 apiservers running on the same ip but 337 // different ports 338 func (r *peerEndpointLeaseReconciler) RemoveLease(serverId string) error { 339 key := path.Join(r.serverLeases.baseKey, serverId) 340 return r.serverLeases.storage.Delete(apirequest.NewDefaultContext(), key, &corev1.Endpoints{}, nil, rest.ValidateAllObjectFunc, nil) 341 } 342 343 func (r *peerEndpointLeaseReconciler) Destroy() { 344 r.serverLeases.destroyFn() 345 } 346 347 func (r *peerEndpointLeaseReconciler) GetEndpoint(serverId string) (string, error) { 348 return r.GetLease(serverId) 349 } 350 351 func (r *peerEndpointLeaseReconciler) getIpInfoList(storageOpts storage.ListOptions) (*corev1.EndpointsList, error) { 352 ipInfoList := &corev1.EndpointsList{} 353 if err := r.serverLeases.storage.GetList(apirequest.NewDefaultContext(), r.serverLeases.baseKey, storageOpts, ipInfoList); err != nil { 354 return nil, err 355 } 356 return ipInfoList, nil 357 } 358 359 // createEndpointPortSpec creates the endpoint ports 360 func createEndpointPortSpec(endpointPort int, endpointPortName string) []corev1.EndpointPort { 361 return []corev1.EndpointPort{{ 362 Protocol: corev1.ProtocolTCP, 363 Port: int32(endpointPort), 364 Name: endpointPortName, 365 }} 366 }