istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/leaderelection/leaderelection_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package leaderelection 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 "time" 22 23 "go.uber.org/atomic" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/runtime" 26 "k8s.io/client-go/kubernetes" 27 "k8s.io/client-go/kubernetes/fake" 28 k8stesting "k8s.io/client-go/testing" 29 30 "istio.io/istio/pkg/revisions" 31 "istio.io/istio/pkg/test/util/retry" 32 ) 33 34 const testLock = "test-lock" 35 36 func createElection(t *testing.T, 37 name string, revision string, 38 watcher revisions.DefaultWatcher, 39 expectLeader bool, 40 client kubernetes.Interface, fns ...func(stop <-chan struct{}), 41 ) (*LeaderElection, chan struct{}) { 42 t.Helper() 43 return createElectionMulticluster(t, name, revision, false, false, watcher, expectLeader, client, fns...) 44 } 45 46 func createPerRevisionElection(t *testing.T, 47 name string, revision string, 48 watcher revisions.DefaultWatcher, 49 expectLeader bool, 50 client kubernetes.Interface, 51 ) (*LeaderElection, chan struct{}) { 52 t.Helper() 53 return createElectionMulticluster(t, name, revision, false, true, watcher, expectLeader, client) 54 } 55 56 func createElectionMulticluster(t *testing.T, 57 name, revision string, 58 remote, perRevision bool, 59 watcher revisions.DefaultWatcher, 60 expectLeader bool, 61 client kubernetes.Interface, fns ...func(stop <-chan struct{}), 62 ) (*LeaderElection, chan struct{}) { 63 t.Helper() 64 lockName := testLock 65 if perRevision { 66 lockName += "-" + revision 67 } 68 l := &LeaderElection{ 69 namespace: "ns", 70 name: name, 71 electionID: lockName, 72 client: client, 73 revision: revision, 74 remote: remote, 75 defaultWatcher: watcher, 76 perRevision: perRevision, 77 ttl: time.Second, 78 cycle: atomic.NewInt32(0), 79 enabled: true, 80 } 81 l.AddRunFunction(func(stop <-chan struct{}) { 82 <-stop 83 }) 84 for _, fn := range fns { 85 l.AddRunFunction(fn) 86 } 87 stop := make(chan struct{}) 88 go l.Run(stop) 89 90 retry.UntilOrFail(t, func() bool { 91 return l.isLeader() == expectLeader 92 }, retry.Converge(5), retry.Delay(time.Millisecond*100), retry.Timeout(time.Second*10)) 93 return l, stop 94 } 95 96 type fakeDefaultWatcher struct { 97 defaultRevision string 98 } 99 100 func (w *fakeDefaultWatcher) Run(stop <-chan struct{}) { 101 } 102 103 func (w *fakeDefaultWatcher) HasSynced() bool { 104 return true 105 } 106 107 func (w *fakeDefaultWatcher) GetDefault() string { 108 return w.defaultRevision 109 } 110 111 func (w *fakeDefaultWatcher) AddHandler(handler revisions.DefaultHandler) { 112 panic("unimplemented") 113 } 114 115 func TestLeaderElection(t *testing.T) { 116 client := fake.NewSimpleClientset() 117 watcher := &fakeDefaultWatcher{} 118 // First pod becomes the leader 119 _, stop := createElection(t, "pod1", "", watcher, true, client) 120 // A new pod is not the leader 121 _, stop2 := createElection(t, "pod2", "", watcher, false, client) 122 close(stop2) 123 close(stop) 124 } 125 126 func TestPerRevisionElection(t *testing.T) { 127 client := fake.NewSimpleClientset() 128 watcher := &fakeDefaultWatcher{"foo"} 129 // First pod becomes the leader 130 _, stop := createPerRevisionElection(t, "pod1", "foo", watcher, true, client) 131 // A new pod is not the leader 132 _, stop2 := createPerRevisionElection(t, "pod2", "foo", watcher, false, client) 133 close(stop2) 134 close(stop) 135 t.Log("drop") 136 // After leader is lost, we can take over 137 _, stop3 := createPerRevisionElection(t, "pod2", "foo", watcher, true, client) 138 // Other revisions are independent 139 _, stop4 := createPerRevisionElection(t, "pod4", "not-foo", watcher, true, client) 140 close(stop3) 141 close(stop4) 142 } 143 144 func TestPrioritizedLeaderElection(t *testing.T) { 145 client := fake.NewSimpleClientset() 146 watcher := &fakeDefaultWatcher{defaultRevision: "red"} 147 148 // First pod, revision "green" becomes the leader, but is not the default revision 149 _, stop := createElection(t, "pod1", "green", watcher, true, client) 150 // Second pod, revision "red", steals the leader lock from "green" since it is the default revision 151 _, stop2 := createElection(t, "pod2", "red", watcher, true, client) 152 // Third pod with revision "red" comes in and cannot take the lock since another revision with "red" has it 153 _, stop3 := createElection(t, "pod3", "red", watcher, false, client) 154 // Fourth pod with revision "green" cannot take the lock since a revision with "red" has it. 155 _, stop4 := createElection(t, "pod4", "green", watcher, false, client) 156 close(stop2) 157 close(stop3) 158 close(stop4) 159 // Now that revision "green" has stopped acting as leader, revision "red" should be able to claim lock. 160 _, stop5 := createElection(t, "pod2", "red", watcher, true, client) 161 close(stop5) 162 close(stop) 163 // Revision "green" can reclaim once "red" releases. 164 _, stop6 := createElection(t, "pod4", "green", watcher, true, client) 165 close(stop6) 166 } 167 168 func TestMulticlusterLeaderElection(t *testing.T) { 169 client := fake.NewSimpleClientset() 170 watcher := &fakeDefaultWatcher{} 171 // First remote pod becomes the leader 172 _, stop := createElectionMulticluster(t, "pod1", "", true, false, watcher, true, client) 173 // A new local pod should become leader 174 _, stop2 := createElectionMulticluster(t, "pod2", "", false, false, watcher, true, client) 175 // A new remote pod cannot become leader 176 _, stop3 := createElectionMulticluster(t, "pod3", "", true, false, watcher, false, client) 177 close(stop3) 178 close(stop2) 179 close(stop) 180 } 181 182 func TestPrioritizedMulticlusterLeaderElection(t *testing.T) { 183 client := fake.NewSimpleClientset() 184 watcher := &fakeDefaultWatcher{defaultRevision: "red"} 185 186 // First pod, revision "green" becomes the remote leader 187 _, stop := createElectionMulticluster(t, "pod1", "green", true, false, watcher, true, client) 188 // Second pod, revision "red", steals the leader lock from "green" since it is the default revision 189 _, stop2 := createElectionMulticluster(t, "pod2", "red", true, false, watcher, true, client) 190 // Third pod with revision "red" comes in and can take the lock since it is a local revision "red" 191 _, stop3 := createElectionMulticluster(t, "pod3", "red", false, false, watcher, true, client) 192 // Fourth pod with revision "red" cannot take the lock since it is remote 193 _, stop4 := createElectionMulticluster(t, "pod4", "red", true, false, watcher, false, client) 194 close(stop4) 195 close(stop3) 196 close(stop2) 197 close(stop) 198 } 199 200 func SimpleRevisionComparison(currentLeaderRevision string, l *LeaderElection) bool { 201 // Old key comparison impl for interoperablilty testing 202 defaultRevision := l.defaultWatcher.GetDefault() 203 return l.revision != currentLeaderRevision && 204 // empty default revision indicates that there is no default set 205 defaultRevision != "" && defaultRevision == l.revision 206 } 207 208 type LeaderComparison func(string, *LeaderElection) bool 209 210 type instance struct { 211 revision string 212 remote bool 213 comp string 214 } 215 216 func (i instance) GetComp() (LeaderComparison, string) { 217 key := i.revision 218 switch i.comp { 219 case "location": 220 if i.remote { 221 key = remoteIstiodPrefix + key 222 } 223 return LocationPrioritizedComparison, key 224 case "simple": 225 return SimpleRevisionComparison, key 226 default: 227 panic("unknown comparison type") 228 } 229 } 230 231 // TestPrioritizationCycles 232 func TestPrioritizationCycles(t *testing.T) { 233 cases := []instance{} 234 for _, rev := range []string{"", "default", "not-default"} { 235 for _, loc := range []bool{false, true} { 236 for _, comp := range []string{"location", "simple"} { 237 cases = append(cases, instance{ 238 revision: rev, 239 remote: loc, 240 comp: comp, 241 }) 242 } 243 } 244 } 245 246 for _, start := range cases { 247 t.Run(fmt.Sprint(start), func(t *testing.T) { 248 checkCycles(t, start, cases, nil) 249 }) 250 } 251 } 252 253 func alreadyHit(cur instance, chain []instance) bool { 254 for _, cc := range chain { 255 if cur == cc { 256 return true 257 } 258 } 259 return false 260 } 261 262 func checkCycles(t *testing.T, start instance, cases []instance, chain []instance) { 263 if alreadyHit(start, chain) { 264 t.Fatalf("cycle on leader election: cur %v, chain %v", start, chain) 265 } 266 for _, nextHop := range cases { 267 next := LeaderElection{ 268 remote: nextHop.remote, 269 defaultWatcher: &fakeDefaultWatcher{defaultRevision: "default"}, 270 revision: nextHop.revision, 271 } 272 cmpFunc, key := start.GetComp() 273 if cmpFunc(key, &next) { 274 nc := append([]instance{}, chain...) 275 nc = append(nc, start) 276 checkCycles(t, nextHop, cases, nc) 277 } 278 } 279 } 280 281 func TestLeaderElectionConfigMapRemoved(t *testing.T) { 282 client := fake.NewSimpleClientset() 283 watcher := &fakeDefaultWatcher{} 284 _, stop := createElection(t, "pod1", "", watcher, true, client) 285 if err := client.CoreV1().ConfigMaps("ns").Delete(context.TODO(), testLock, metav1.DeleteOptions{}); err != nil { 286 t.Fatal(err) 287 } 288 retry.UntilSuccessOrFail(t, func() error { 289 l, err := client.CoreV1().ConfigMaps("ns").List(context.TODO(), metav1.ListOptions{}) 290 if err != nil { 291 return err 292 } 293 if len(l.Items) != 1 { 294 return fmt.Errorf("got unexpected config map entry: %v", l.Items) 295 } 296 return nil 297 }) 298 close(stop) 299 } 300 301 func TestLeaderElectionNoPermission(t *testing.T) { 302 client := fake.NewSimpleClientset() 303 watcher := &fakeDefaultWatcher{} 304 allowRbac := atomic.NewBool(true) 305 client.Fake.PrependReactor("update", "*", func(action k8stesting.Action) (bool, runtime.Object, error) { 306 if allowRbac.Load() { 307 return false, nil, nil 308 } 309 return true, nil, fmt.Errorf("nope, out of luck") 310 }) 311 312 completions := atomic.NewInt32(0) 313 l, stop := createElection(t, "pod1", "", watcher, true, client, func(stop <-chan struct{}) { 314 completions.Add(1) 315 }) 316 // Expect to run once 317 expectInt(t, completions.Load, 1) 318 319 // drop RBAC permissions to update the configmap 320 // This simulates loosing an active lease 321 allowRbac.Store(false) 322 323 // We should start a new cycle at this point 324 expectInt(t, l.cycle.Load, 2) 325 326 // Add configmap permission back 327 allowRbac.Store(true) 328 329 // We should get the leader lock back 330 expectInt(t, completions.Load, 2) 331 332 close(stop) 333 } 334 335 func expectInt(t *testing.T, f func() int32, expected int32) { 336 t.Helper() 337 retry.UntilSuccessOrFail(t, func() error { 338 got := f() 339 if got != expected { 340 return fmt.Errorf("unexpected count: %v, want %v", got, expected) 341 } 342 return nil 343 }, retry.Timeout(time.Second)) 344 } 345 346 func TestLeaderElectionDisabled(t *testing.T) { 347 client := fake.NewSimpleClientset() 348 watcher := &fakeDefaultWatcher{} 349 // Prevent LeaderElection from creating a lease, so that the runFn only runs 350 // if leader election is disabled. 351 client.Fake.PrependReactor("*", "*", func(action k8stesting.Action) (bool, runtime.Object, error) { 352 return true, nil, fmt.Errorf("nope, out of luck") 353 }) 354 355 l := &LeaderElection{ 356 namespace: "ns", 357 name: "disabled", 358 enabled: false, 359 electionID: testLock, 360 client: client, 361 revision: "", 362 defaultWatcher: watcher, 363 ttl: time.Second, 364 cycle: atomic.NewInt32(0), 365 } 366 gotLeader := atomic.NewBool(false) 367 l.AddRunFunction(func(stop <-chan struct{}) { 368 gotLeader.Store(true) 369 }) 370 stop := make(chan struct{}) 371 go l.Run(stop) 372 t.Cleanup(func() { 373 close(stop) 374 }) 375 376 // Need to retry until Run() starts to execute in the goroutine. 377 retry.UntilOrFail(t, gotLeader.Load, retry.Converge(5), retry.Delay(time.Millisecond*100), retry.Timeout(time.Second*10)) 378 if !l.isLeader() { 379 t.Errorf("isLeader()=false, want true") 380 } 381 }