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  }