k8s.io/apiserver@v0.31.1/pkg/storage/feature/feature_support_checker.go (about) 1 /* 2 Copyright 2024 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package feature 18 19 import ( 20 "context" 21 "fmt" 22 "sync" 23 "time" 24 25 clientv3 "go.etcd.io/etcd/client/v3" 26 "k8s.io/apimachinery/pkg/util/runtime" 27 "k8s.io/apimachinery/pkg/util/version" 28 "k8s.io/apimachinery/pkg/util/wait" 29 "k8s.io/apiserver/pkg/storage" 30 "k8s.io/klog/v2" 31 "k8s.io/utils/ptr" 32 ) 33 34 var ( 35 // Define these static versions to use for checking version of etcd, issue on kubernetes #123192 36 v3_4_31 = version.MustParseSemantic("3.4.31") 37 v3_5_0 = version.MustParseSemantic("3.5.0") 38 v3_5_13 = version.MustParseSemantic("3.5.13") 39 40 // DefaultFeatureSupportChecker is a shared global etcd FeatureSupportChecker. 41 DefaultFeatureSupportChecker FeatureSupportChecker = newDefaultFeatureSupportChecker() 42 ) 43 44 // FeatureSupportChecker to define Supports functions. 45 type FeatureSupportChecker interface { 46 // Supports check if the feature is supported or not by checking internal cache. 47 // By default all calls to this function before calling CheckClient returns false. 48 // Returns true if all endpoints in etcd clients are supporting the feature. 49 // If client A supports and client B doesn't support the feature, the `Supports` will 50 // first return true at client A initializtion and then return false on client B 51 // initialzation, it can flip the support at runtime. 52 Supports(feature storage.Feature) bool 53 // CheckClient works with etcd client to recalcualte feature support and cache it internally. 54 // All etcd clients should support feature to cause `Supports` return true. 55 // If client A supports and client B doesn't support the feature, the `Supports` will 56 // first return true at client A initializtion and then return false on client B 57 // initialzation, it can flip the support at runtime. 58 CheckClient(ctx context.Context, c client, feature storage.Feature) 59 } 60 61 type defaultFeatureSupportChecker struct { 62 lock sync.Mutex 63 progressNotifySupported *bool 64 checkingEndpoint map[string]struct{} 65 } 66 67 func newDefaultFeatureSupportChecker() *defaultFeatureSupportChecker { 68 return &defaultFeatureSupportChecker{ 69 checkingEndpoint: make(map[string]struct{}), 70 } 71 } 72 73 // Supports can check the featue from anywhere without storage if it was cached before. 74 func (f *defaultFeatureSupportChecker) Supports(feature storage.Feature) bool { 75 switch feature { 76 case storage.RequestWatchProgress: 77 f.lock.Lock() 78 defer f.lock.Unlock() 79 80 return ptr.Deref(f.progressNotifySupported, false) 81 default: 82 runtime.HandleError(fmt.Errorf("feature %q is not implemented in DefaultFeatureSupportChecker", feature)) 83 return false 84 } 85 } 86 87 // CheckClient accepts client and calculate the support per endpoint and caches it. 88 func (f *defaultFeatureSupportChecker) CheckClient(ctx context.Context, c client, feature storage.Feature) { 89 switch feature { 90 case storage.RequestWatchProgress: 91 f.checkClient(ctx, c) 92 default: 93 runtime.HandleError(fmt.Errorf("feature %q is not implemented in DefaultFeatureSupportChecker", feature)) 94 } 95 } 96 97 func (f *defaultFeatureSupportChecker) checkClient(ctx context.Context, c client) { 98 // start with 10 ms, multiply by 2 each step, until 15 s and stays on 15 seconds. 99 delayFunc := wait.Backoff{ 100 Duration: 10 * time.Millisecond, 101 Cap: 15 * time.Second, 102 Factor: 2.0, 103 Steps: 11}.DelayFunc() 104 f.lock.Lock() 105 defer f.lock.Unlock() 106 for _, ep := range c.Endpoints() { 107 if _, found := f.checkingEndpoint[ep]; found { 108 continue 109 } 110 f.checkingEndpoint[ep] = struct{}{} 111 go func(ep string) { 112 defer runtime.HandleCrash() 113 err := delayFunc.Until(ctx, true, true, func(ctx context.Context) (done bool, err error) { 114 internalErr := f.clientSupportsRequestWatchProgress(ctx, c, ep) 115 return internalErr == nil, nil 116 }) 117 if err != nil { 118 klog.ErrorS(err, "Failed to check if RequestWatchProgress is supported by etcd after retrying") 119 } 120 }(ep) 121 } 122 } 123 124 func (f *defaultFeatureSupportChecker) clientSupportsRequestWatchProgress(ctx context.Context, c client, ep string) error { 125 supported, err := endpointSupportsRequestWatchProgress(ctx, c, ep) 126 if err != nil { 127 return err 128 } 129 f.lock.Lock() 130 defer f.lock.Unlock() 131 132 if !supported { 133 klog.Infof("RequestWatchProgress feature is not supported by %q endpoint", ep) 134 f.progressNotifySupported = ptr.To(false) 135 return nil 136 } 137 if f.progressNotifySupported == nil { 138 f.progressNotifySupported = ptr.To(true) 139 } 140 return nil 141 } 142 143 // Sub interface of etcd client. 144 type client interface { 145 // Endpoints returns list of endpoints in etcd client. 146 Endpoints() []string 147 // Status retrieves the status information from the etcd client connected to the specified endpoint. 148 // It takes a context.Context parameter for cancellation or timeout control. 149 // It returns a clientv3.StatusResponse containing the status information or an error if the operation fails. 150 Status(ctx context.Context, endpoint string) (*clientv3.StatusResponse, error) 151 } 152 153 // endpointSupportsRequestWatchProgress evaluates whether RequestWatchProgress supported by current version of etcd endpoint. 154 // Based on this issues: 155 // - https://github.com/etcd-io/etcd/issues/15220 - Fixed in etcd v3.4.25+ and v3.5.8+ 156 // - https://github.com/etcd-io/etcd/issues/17507 - Fixed in etcd v3.4.31+ and v3.5.13+ 157 func endpointSupportsRequestWatchProgress(ctx context.Context, c client, endpoint string) (bool, error) { 158 resp, err := c.Status(ctx, endpoint) 159 if err != nil { 160 return false, fmt.Errorf("failed checking etcd version, endpoint: %q: %w", endpoint, err) 161 } 162 ver, err := version.ParseSemantic(resp.Version) 163 if err != nil { 164 // Assume feature is not supported if etcd version cannot be parsed. 165 klog.ErrorS(err, "Failed to parse etcd version", "version", resp.Version) 166 return false, nil 167 } 168 if ver.LessThan(v3_4_31) || ver.AtLeast(v3_5_0) && ver.LessThan(v3_5_13) { 169 return false, nil 170 } 171 return true, nil 172 }