mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 06:54:01 +00:00
CLE controller and client changes
This commit is contained in:
parent
b5a62f14cd
commit
c47ff1e1a9
@ -28,8 +28,9 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@ -78,7 +79,9 @@ import (
|
||||
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
||||
garbagecollector "k8s.io/kubernetes/pkg/controller/garbagecollector"
|
||||
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
||||
kubefeatures "k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -289,6 +292,30 @@ func Run(ctx context.Context, c *config.CompletedConfig) error {
|
||||
return startSATokenControllerInit(ctx, controllerContext, controllerName)
|
||||
}
|
||||
}
|
||||
ver, err := semver.ParseTolerant(version.Get().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
|
||||
// Start component identity lease management
|
||||
leaseCandidate, err := leaderelection.NewCandidate(
|
||||
c.Client,
|
||||
id,
|
||||
"kube-system",
|
||||
"kube-controller-manager",
|
||||
clock.RealClock{},
|
||||
ver.FinalizeVersion(),
|
||||
ver.FinalizeVersion(), // TODO: Use compatibility version when it's available
|
||||
[]v1.CoordinatedLeaseStrategy{"OldestEmulationVersion"},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
healthzHandler.AddHealthChecker(healthz.NewInformerSyncHealthz(leaseCandidate.InformerFactory))
|
||||
|
||||
go leaseCandidate.Run(ctx)
|
||||
}
|
||||
|
||||
// Start the main lock
|
||||
go leaderElectAndRun(ctx, c, id, electionChecker,
|
||||
@ -886,6 +913,7 @@ func leaderElectAndRun(ctx context.Context, c *config.CompletedConfig, lockIdent
|
||||
Callbacks: callbacks,
|
||||
WatchDog: electionChecker,
|
||||
Name: leaseName,
|
||||
Coordinated: utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection),
|
||||
})
|
||||
|
||||
panic("unreachable")
|
||||
|
@ -24,8 +24,9 @@ import (
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
coordinationv1 "k8s.io/api/coordination/v1"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
@ -56,8 +57,11 @@ import (
|
||||
"k8s.io/component-base/version"
|
||||
"k8s.io/component-base/version/verflag"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/clock"
|
||||
|
||||
schedulerserverconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
|
||||
"k8s.io/kubernetes/cmd/kube-scheduler/app/options"
|
||||
kubefeatures "k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/scheduler"
|
||||
kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config"
|
||||
"k8s.io/kubernetes/pkg/scheduler/apis/config/latest"
|
||||
@ -207,6 +211,34 @@ func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *
|
||||
})
|
||||
readyzChecks = append(readyzChecks, handlerSyncCheck)
|
||||
|
||||
if cc.LeaderElection != nil && utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
|
||||
binaryVersion, err := semver.ParseTolerant(utilversion.DefaultComponentGlobalsRegistry.EffectiveVersionFor(utilversion.DefaultKubeComponent).BinaryVersion().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emulationVersion, err := semver.ParseTolerant(utilversion.DefaultComponentGlobalsRegistry.EffectiveVersionFor(utilversion.DefaultKubeComponent).EmulationVersion().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start component identity lease management
|
||||
leaseCandidate, err := leaderelection.NewCandidate(
|
||||
cc.Client,
|
||||
cc.LeaderElection.Lock.Identity(),
|
||||
"kube-system",
|
||||
"kube-scheduler",
|
||||
clock.RealClock{},
|
||||
binaryVersion.FinalizeVersion(),
|
||||
emulationVersion.FinalizeVersion(),
|
||||
[]coordinationv1.CoordinatedLeaseStrategy{"OldestEmulationVersion"},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readyzChecks = append(readyzChecks, healthz.NewInformerSyncHealthz(leaseCandidate.InformerFactory))
|
||||
go leaseCandidate.Run(ctx)
|
||||
}
|
||||
|
||||
// Start up the healthz server.
|
||||
if cc.SecureServing != nil {
|
||||
handler := buildHandlerChain(newHealthEndpointsAndMetricsHandler(&cc.ComponentConfig, cc.InformerFactory, isLeader, checks, readyzChecks), cc.Authentication.Authenticator, cc.Authorization.Authorizer)
|
||||
@ -245,6 +277,9 @@ func Run(ctx context.Context, cc *schedulerserverconfig.CompletedConfig, sched *
|
||||
}
|
||||
// If leader election is enabled, runCommand via LeaderElector until done and exit.
|
||||
if cc.LeaderElection != nil {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
|
||||
cc.LeaderElection.Coordinated = true
|
||||
}
|
||||
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
close(waitingForLeader)
|
||||
|
@ -1027,6 +1027,7 @@ EOF
|
||||
--feature-gates="${FEATURE_GATES}" \
|
||||
--authentication-kubeconfig "${CERT_DIR}"/scheduler.kubeconfig \
|
||||
--authorization-kubeconfig "${CERT_DIR}"/scheduler.kubeconfig \
|
||||
--leader-elect=false \
|
||||
--master="https://${API_HOST}:${API_SECURE_PORT}" >"${SCHEDULER_LOG}" 2>&1 &
|
||||
SCHEDULER_PID=$!
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
@ -41,6 +42,7 @@ import (
|
||||
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/apiserverleasegc"
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/clusterauthenticationtrust"
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/leaderelection"
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/systemnamespaces"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
@ -145,6 +147,27 @@ func (c completedConfig) New(name string, delegationTarget genericapiserver.Dele
|
||||
return nil, fmt.Errorf("failed to get listener address: %w", err)
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(apiserverfeatures.CoordinatedLeaderElection) {
|
||||
leaseInformer := s.VersionedInformers.Coordination().V1().Leases()
|
||||
lcInformer := s.VersionedInformers.Coordination().V1alpha1().LeaseCandidates()
|
||||
// Ensure that informers are registered before starting. Coordinated Leader Election leader-elected
|
||||
// and may register informer handlers after they are started.
|
||||
_ = leaseInformer.Informer()
|
||||
_ = lcInformer.Informer()
|
||||
s.GenericAPIServer.AddPostStartHookOrDie("start-kube-apiserver-coordinated-leader-election-controller", func(hookContext genericapiserver.PostStartHookContext) error {
|
||||
go leaderelection.RunWithLeaderElection(hookContext, s.GenericAPIServer.LoopbackClientConfig, func() (func(ctx context.Context, workers int), error) {
|
||||
controller, err := leaderelection.NewController(
|
||||
leaseInformer,
|
||||
lcInformer,
|
||||
client.CoordinationV1(),
|
||||
client.CoordinationV1alpha1(),
|
||||
)
|
||||
return controller.Run, err
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
|
||||
peeraddress := getPeerAddress(c.Extra.PeerAdvertiseAddress, c.Generic.PublicAddress, publicServicePort)
|
||||
peerEndpointCtrl := peerreconcilers.New(
|
||||
|
135
pkg/controlplane/controller/leaderelection/election.go
Normal file
135
pkg/controlplane/controller/leaderelection/election.go
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
v1alpha1 "k8s.io/api/coordination/v1alpha1"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func pickBestLeaderOldestEmulationVersion(candidates []*v1alpha1.LeaseCandidate) *v1alpha1.LeaseCandidate {
|
||||
var electee *v1alpha1.LeaseCandidate
|
||||
for _, c := range candidates {
|
||||
if !validLeaseCandidateForOldestEmulationVersion(c) {
|
||||
continue
|
||||
}
|
||||
if electee == nil || compare(electee, c) > 0 {
|
||||
electee = c
|
||||
}
|
||||
}
|
||||
if electee == nil {
|
||||
klog.Infof("pickBestLeader: none found")
|
||||
} else {
|
||||
klog.Infof("pickBestLeader: %s %s", electee.Namespace, electee.Name)
|
||||
}
|
||||
return electee
|
||||
}
|
||||
|
||||
func shouldReelect(candidates []*v1alpha1.LeaseCandidate, currentLeader *v1alpha1.LeaseCandidate) bool {
|
||||
klog.Infof("shouldReelect for candidates: %+v", candidates)
|
||||
pickedLeader := pickBestLeaderOldestEmulationVersion(candidates)
|
||||
if pickedLeader == nil {
|
||||
return false
|
||||
}
|
||||
return compare(currentLeader, pickedLeader) > 0
|
||||
}
|
||||
|
||||
func pickBestStrategy(candidates []*v1alpha1.LeaseCandidate) v1.CoordinatedLeaseStrategy {
|
||||
// TODO: This doesn't account for cycles within the preference graph
|
||||
// We may have to do a topological sort to verify that the preference ordering is valid
|
||||
var bestStrategy *v1.CoordinatedLeaseStrategy
|
||||
for _, c := range candidates {
|
||||
if len(c.Spec.PreferredStrategies) > 0 {
|
||||
if bestStrategy == nil {
|
||||
bestStrategy = &c.Spec.PreferredStrategies[0]
|
||||
continue
|
||||
}
|
||||
if *bestStrategy != c.Spec.PreferredStrategies[0] {
|
||||
if idx := slices.Index(c.Spec.PreferredStrategies, *bestStrategy); idx > 0 {
|
||||
bestStrategy = &c.Spec.PreferredStrategies[0]
|
||||
} else {
|
||||
klog.Infof("Error: bad strategy ordering")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (*bestStrategy)
|
||||
}
|
||||
|
||||
func validLeaseCandidateForOldestEmulationVersion(l *v1alpha1.LeaseCandidate) bool {
|
||||
_, err := semver.ParseTolerant(l.Spec.EmulationVersion)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = semver.ParseTolerant(l.Spec.BinaryVersion)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func getEmulationVersion(l *v1alpha1.LeaseCandidate) semver.Version {
|
||||
value := l.Spec.EmulationVersion
|
||||
v, err := semver.ParseTolerant(value)
|
||||
if err != nil {
|
||||
return semver.Version{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func getBinaryVersion(l *v1alpha1.LeaseCandidate) semver.Version {
|
||||
value := l.Spec.BinaryVersion
|
||||
v, err := semver.ParseTolerant(value)
|
||||
if err != nil {
|
||||
return semver.Version{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// -1: lhs better, 1: rhs better
|
||||
func compare(lhs, rhs *v1alpha1.LeaseCandidate) int {
|
||||
lhsVersion := getEmulationVersion(lhs)
|
||||
rhsVersion := getEmulationVersion(rhs)
|
||||
result := lhsVersion.Compare(rhsVersion)
|
||||
if result == 0 {
|
||||
lhsVersion := getBinaryVersion(lhs)
|
||||
rhsVersion := getBinaryVersion(rhs)
|
||||
result = lhsVersion.Compare(rhsVersion)
|
||||
}
|
||||
if result == 0 {
|
||||
if lhs.CreationTimestamp.After(rhs.CreationTimestamp.Time) {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isLeaseExpired(lease *v1.Lease) bool {
|
||||
currentTime := time.Now()
|
||||
return lease.Spec.RenewTime == nil ||
|
||||
lease.Spec.LeaseDurationSeconds == nil ||
|
||||
lease.Spec.RenewTime.Add(time.Duration(*lease.Spec.LeaseDurationSeconds)*time.Second).Before(currentTime)
|
||||
}
|
||||
|
||||
func isLeaseCandidateExpired(lease *v1alpha1.LeaseCandidate) bool {
|
||||
currentTime := time.Now()
|
||||
return lease.Spec.RenewTime == nil ||
|
||||
lease.Spec.RenewTime.Add(leaseCandidateValidDuration).Before(currentTime)
|
||||
}
|
522
pkg/controlplane/controller/leaderelection/election_test.go
Normal file
522
pkg/controlplane/controller/leaderelection/election_test.go
Normal file
@ -0,0 +1,522 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
v1alpha1 "k8s.io/api/coordination/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestPickBestLeaderOldestEmulationVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidates []*v1alpha1.LeaseCandidate
|
||||
want *v1alpha1.LeaseCandidate
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
candidates: []*v1alpha1.LeaseCandidate{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single candidate",
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now()},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple candidates, different emulation versions",
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate2",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now()},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.2.0",
|
||||
BinaryVersion: "0.2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "v1",
|
||||
BinaryVersion: "v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple candidates, same emulation versions, different binary versions",
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate2",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now()},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple candidates, same emulation versions, same binary versions, different creation timestamps",
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate2",
|
||||
Namespace: "default",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now()},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "candidate1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := pickBestLeaderOldestEmulationVersion(tt.candidates)
|
||||
if got != nil && tt.want != nil {
|
||||
if got.Name != tt.want.Name || got.Namespace != tt.want.Namespace {
|
||||
t.Errorf("pickBestLeaderOldestEmulationVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
} else if got != tt.want {
|
||||
t.Errorf("pickBestLeaderOldestEmulationVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidLeaseCandidateForOldestEmulationVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidate *v1alpha1.LeaseCandidate
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid emulation and binary versions",
|
||||
candidate: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid emulation version",
|
||||
candidate: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "invalid",
|
||||
BinaryVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invalid binary version",
|
||||
candidate: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
BinaryVersion: "invalid",
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := validLeaseCandidateForOldestEmulationVersion(tt.candidate)
|
||||
if got != tt.want {
|
||||
t.Errorf("validLeaseCandidateForOldestEmulationVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEmulationVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidate *v1alpha1.LeaseCandidate
|
||||
want semver.Version
|
||||
}{
|
||||
{
|
||||
name: "valid emulation version",
|
||||
candidate: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "0.1.0",
|
||||
},
|
||||
},
|
||||
want: semver.MustParse("0.1.0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getEmulationVersion(tt.candidate)
|
||||
if got.FinalizeVersion() != tt.want.FinalizeVersion() {
|
||||
t.Errorf("getEmulationVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBinaryVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidate *v1alpha1.LeaseCandidate
|
||||
want semver.Version
|
||||
}{
|
||||
{
|
||||
name: "valid binary version",
|
||||
candidate: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
BinaryVersion: "0.3.0",
|
||||
},
|
||||
},
|
||||
want: semver.MustParse("0.3.0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getBinaryVersion(tt.candidate)
|
||||
if got.FinalizeVersion() != tt.want.FinalizeVersion() {
|
||||
t.Errorf("getBinaryVersion() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
nowTime := time.Now()
|
||||
cases := []struct {
|
||||
name string
|
||||
lhs *v1alpha1.LeaseCandidate
|
||||
rhs *v1alpha1.LeaseCandidate
|
||||
expectedResult int
|
||||
}{
|
||||
{
|
||||
name: "identical versions earlier timestamp",
|
||||
lhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.21.0",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Time{Time: nowTime.Add(time.Duration(1))},
|
||||
},
|
||||
},
|
||||
rhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.21.0",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Time{Time: nowTime},
|
||||
},
|
||||
},
|
||||
expectedResult: 1,
|
||||
},
|
||||
{
|
||||
name: "no lhs version",
|
||||
lhs: &v1alpha1.LeaseCandidate{},
|
||||
rhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.21.0",
|
||||
},
|
||||
},
|
||||
expectedResult: -1,
|
||||
},
|
||||
{
|
||||
name: "no rhs version",
|
||||
lhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.21.0",
|
||||
},
|
||||
},
|
||||
rhs: &v1alpha1.LeaseCandidate{},
|
||||
expectedResult: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid lhs version",
|
||||
lhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "xyz",
|
||||
BinaryVersion: "xyz",
|
||||
},
|
||||
},
|
||||
rhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.21.0",
|
||||
},
|
||||
},
|
||||
expectedResult: -1,
|
||||
},
|
||||
{
|
||||
name: "invalid rhs version",
|
||||
lhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.21.0",
|
||||
},
|
||||
},
|
||||
rhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "xyz",
|
||||
BinaryVersion: "xyz",
|
||||
},
|
||||
},
|
||||
expectedResult: 1,
|
||||
},
|
||||
{
|
||||
name: "lhs less than rhs",
|
||||
lhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
},
|
||||
},
|
||||
rhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
},
|
||||
},
|
||||
expectedResult: -1,
|
||||
},
|
||||
{
|
||||
name: "rhs less than lhs",
|
||||
lhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
},
|
||||
},
|
||||
rhs: &v1alpha1.LeaseCandidate{
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
},
|
||||
},
|
||||
expectedResult: 1,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := compare(tc.lhs, tc.rhs)
|
||||
if result != tc.expectedResult {
|
||||
t.Errorf("Expected comparison result of %d but got %d", tc.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldReelect(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
candidates []*v1alpha1.LeaseCandidate
|
||||
currentLeader *v1alpha1.LeaseCandidate
|
||||
expectResult bool
|
||||
}{
|
||||
{
|
||||
name: "candidate with newer binary version",
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-2",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
currentLeader: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "no newer candidates",
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-2",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
currentLeader: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "no candidates",
|
||||
candidates: []*v1alpha1.LeaseCandidate{},
|
||||
currentLeader: &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
expectResult: false,
|
||||
},
|
||||
// TODO: Add test cases where candidates have invalid version numbers
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := shouldReelect(tc.candidates, tc.currentLeader)
|
||||
if tc.expectResult != result {
|
||||
t.Errorf("Expected %t but got %t", tc.expectResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,399 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
v1alpha1 "k8s.io/api/coordination/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
coordinationv1informers "k8s.io/client-go/informers/coordination/v1"
|
||||
coordinationv1alpha1 "k8s.io/client-go/informers/coordination/v1alpha1"
|
||||
coordinationv1client "k8s.io/client-go/kubernetes/typed/coordination/v1"
|
||||
coordinationv1alpha1client "k8s.io/client-go/kubernetes/typed/coordination/v1alpha1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
controllerName = "leader-election-controller"
|
||||
ElectedByAnnotationName = "coordination.k8s.io/elected-by" // Value should be set to controllerName
|
||||
|
||||
// Requeue interval is the interval at which a Lease is requeued to verify that it is being renewed properly.
|
||||
requeueInterval = 5 * time.Second
|
||||
defaultLeaseDurationSeconds int32 = 5
|
||||
|
||||
electionDuration = 5 * time.Second
|
||||
|
||||
leaseCandidateValidDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
// Controller is the leader election controller, which observes component identity leases for
|
||||
// components that have self-nominated as candidate leaders for leases and elects leaders
|
||||
// for those leases, favoring candidates with higher versions.
|
||||
type Controller struct {
|
||||
leaseInformer coordinationv1informers.LeaseInformer
|
||||
leaseClient coordinationv1client.CoordinationV1Interface
|
||||
leaseRegistration cache.ResourceEventHandlerRegistration
|
||||
|
||||
leaseCandidateInformer coordinationv1alpha1.LeaseCandidateInformer
|
||||
leaseCandidateClient coordinationv1alpha1client.CoordinationV1alpha1Interface
|
||||
leaseCandidateRegistration cache.ResourceEventHandlerRegistration
|
||||
|
||||
queue workqueue.TypedRateLimitingInterface[types.NamespacedName]
|
||||
}
|
||||
|
||||
func (c *Controller) Run(ctx context.Context, workers int) {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
defer func() {
|
||||
err := c.leaseInformer.Informer().RemoveEventHandler(c.leaseRegistration)
|
||||
if err != nil {
|
||||
klog.Warning("error removing leaseInformer eventhandler")
|
||||
}
|
||||
err = c.leaseCandidateInformer.Informer().RemoveEventHandler(c.leaseCandidateRegistration)
|
||||
if err != nil {
|
||||
klog.Warning("error removing leaseCandidateInformer eventhandler")
|
||||
}
|
||||
}()
|
||||
|
||||
if !cache.WaitForNamedCacheSync(controllerName, ctx.Done(), c.leaseRegistration.HasSynced, c.leaseCandidateRegistration.HasSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
// This controller is leader elected and may start after informers have already started. List on startup.
|
||||
lcs, err := c.leaseCandidateInformer.Lister().List(labels.Everything())
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return
|
||||
}
|
||||
for _, lc := range lcs {
|
||||
c.processCandidate(lc)
|
||||
}
|
||||
|
||||
klog.Infof("Workers: %d", workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
klog.Infof("Starting worker")
|
||||
go wait.UntilWithContext(ctx, c.runElectionWorker, time.Second)
|
||||
}
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func NewController(leaseInformer coordinationv1informers.LeaseInformer, leaseCandidateInformer coordinationv1alpha1.LeaseCandidateInformer, leaseClient coordinationv1client.CoordinationV1Interface, leaseCandidateClient coordinationv1alpha1client.CoordinationV1alpha1Interface) (*Controller, error) {
|
||||
c := &Controller{
|
||||
leaseInformer: leaseInformer,
|
||||
leaseCandidateInformer: leaseCandidateInformer,
|
||||
leaseClient: leaseClient,
|
||||
leaseCandidateClient: leaseCandidateClient,
|
||||
|
||||
queue: workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[types.NamespacedName](), workqueue.TypedRateLimitingQueueConfig[types.NamespacedName]{Name: controllerName}),
|
||||
}
|
||||
leaseSynced, err := leaseInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
c.processLease(obj)
|
||||
},
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
c.processLease(newObj)
|
||||
},
|
||||
DeleteFunc: func(oldObj interface{}) {
|
||||
c.processLease(oldObj)
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
leaseCandidateSynced, err := leaseCandidateInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
c.processCandidate(obj)
|
||||
},
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
c.processCandidate(newObj)
|
||||
},
|
||||
DeleteFunc: func(oldObj interface{}) {
|
||||
c.processCandidate(oldObj)
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.leaseRegistration = leaseSynced
|
||||
c.leaseCandidateRegistration = leaseCandidateSynced
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Controller) runElectionWorker(ctx context.Context) {
|
||||
for c.processNextElectionItem(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) processNextElectionItem(ctx context.Context) bool {
|
||||
key, shutdown := c.queue.Get()
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
|
||||
completed, err := c.reconcileElectionStep(ctx, key)
|
||||
utilruntime.HandleError(err)
|
||||
if completed {
|
||||
defer c.queue.AddAfter(key, requeueInterval)
|
||||
}
|
||||
c.queue.Done(key)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Controller) processCandidate(obj any) {
|
||||
lc, ok := obj.(*v1alpha1.LeaseCandidate)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if lc == nil {
|
||||
return
|
||||
}
|
||||
// Ignore candidates that transitioned to Pending because reelection is already in progress
|
||||
if lc.Spec.PingTime != nil {
|
||||
return
|
||||
}
|
||||
c.queue.Add(types.NamespacedName{Namespace: lc.Namespace, Name: lc.Spec.LeaseName})
|
||||
}
|
||||
|
||||
func (c *Controller) processLease(obj any) {
|
||||
lease, ok := obj.(*v1.Lease)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.queue.Add(types.NamespacedName{Namespace: lease.Namespace, Name: lease.Name})
|
||||
}
|
||||
|
||||
func (c *Controller) electionNeeded(candidates []*v1alpha1.LeaseCandidate, leaseNN types.NamespacedName) (bool, error) {
|
||||
lease, err := c.leaseInformer.Lister().Leases(leaseNN.Namespace).Get(leaseNN.Name)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return false, fmt.Errorf("error reading lease")
|
||||
} else if apierrors.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if isLeaseExpired(lease) || lease.Spec.HolderIdentity == nil || *lease.Spec.HolderIdentity == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
prelimStrategy := pickBestStrategy(candidates)
|
||||
if prelimStrategy != v1.OldestEmulationVersion {
|
||||
klog.V(2).Infof("strategy %s is not recognized by CLE.", prelimStrategy)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
prelimElectee := pickBestLeaderOldestEmulationVersion(candidates)
|
||||
if prelimElectee == nil {
|
||||
return false, nil
|
||||
} else if lease != nil && lease.Spec.HolderIdentity != nil && prelimElectee.Name == *lease.Spec.HolderIdentity {
|
||||
klog.V(2).Infof("Leader %s is already most optimal for lease %s %s", prelimElectee.Name, lease.Namespace, lease.Name)
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// reconcileElectionStep steps through a step in an election.
|
||||
// A step looks at the current state of Lease and LeaseCandidates and takes one of the following action
|
||||
// - do nothing (because leader is already optimal or still waiting for an event)
|
||||
// - request ack from candidates (update LeaseCandidate PingTime)
|
||||
// - finds the most optimal candidate and elect (update the Lease object)
|
||||
// Instead of keeping a map and lock on election, the state is
|
||||
// calculated every time by looking at the lease, and set of available candidates.
|
||||
// PingTime + electionDuration > time.Now: We just asked all candidates to ack and are still waiting for response
|
||||
// PingTime + electionDuration < time.Now: Candidate has not responded within the appropriate PingTime. Continue the election.
|
||||
// RenewTime + 5 seconds > time.Now: All candidates acked in the last 5 seconds, continue the election.
|
||||
func (c *Controller) reconcileElectionStep(ctx context.Context, leaseNN types.NamespacedName) (requeue bool, err error) {
|
||||
now := time.Now()
|
||||
|
||||
candidates, err := c.listAdmissableCandidates(leaseNN)
|
||||
if err != nil {
|
||||
return true, err
|
||||
} else if len(candidates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
klog.V(4).Infof("reconcileElectionStep %q %q, candidates: %d", leaseNN.Namespace, leaseNN.Name, len(candidates))
|
||||
|
||||
// Check if an election is really needed by looking at the current lease
|
||||
// and set of candidates
|
||||
needElection, err := c.electionNeeded(candidates, leaseNN)
|
||||
if !needElection || err != nil {
|
||||
return needElection, err
|
||||
}
|
||||
|
||||
fastTrackElection := false
|
||||
|
||||
for _, candidate := range candidates {
|
||||
// If a candidate has a PingTime within the election duration, they have not acked
|
||||
// and we should wait until we receive their response
|
||||
if candidate.Spec.PingTime != nil {
|
||||
if candidate.Spec.PingTime.Add(electionDuration).After(now) {
|
||||
// continue waiting for the election to timeout
|
||||
return false, nil
|
||||
} else {
|
||||
// election timed out without ack. Clear and start election.
|
||||
fastTrackElection = true
|
||||
clone := candidate.DeepCopy()
|
||||
clone.Spec.PingTime = nil
|
||||
_, err := c.leaseCandidateClient.LeaseCandidates(clone.Namespace).Update(ctx, clone, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !fastTrackElection {
|
||||
continueElection := true
|
||||
for _, candidate := range candidates {
|
||||
// if renewTime of a candidate is longer than electionDuration old, we have to ping.
|
||||
if candidate.Spec.RenewTime != nil && candidate.Spec.RenewTime.Add(electionDuration).Before(now) {
|
||||
continueElection = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !continueElection {
|
||||
// Send an "are you alive" signal to all candidates
|
||||
for _, candidate := range candidates {
|
||||
clone := candidate.DeepCopy()
|
||||
clone.Spec.PingTime = &metav1.MicroTime{Time: time.Now()}
|
||||
_, err := c.leaseCandidateClient.LeaseCandidates(clone.Namespace).Update(ctx, clone, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
var ackedCandidates []*v1alpha1.LeaseCandidate
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Spec.RenewTime.Add(electionDuration).After(now) {
|
||||
ackedCandidates = append(ackedCandidates, candidate)
|
||||
}
|
||||
}
|
||||
if len(ackedCandidates) == 0 {
|
||||
return false, fmt.Errorf("no available candidates")
|
||||
}
|
||||
|
||||
strategy := pickBestStrategy(ackedCandidates)
|
||||
if strategy != v1.OldestEmulationVersion {
|
||||
klog.V(2).Infof("strategy %s is not recognized by CLE.", strategy)
|
||||
return false, nil
|
||||
}
|
||||
electee := pickBestLeaderOldestEmulationVersion(ackedCandidates)
|
||||
|
||||
if electee == nil {
|
||||
return false, fmt.Errorf("should not happen, could not find suitable electee")
|
||||
}
|
||||
|
||||
electeeName := electee.Name
|
||||
// create the leader election lease
|
||||
leaderLease := &v1.Lease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: leaseNN.Namespace,
|
||||
Name: leaseNN.Name,
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: &electeeName,
|
||||
Strategy: &strategy,
|
||||
LeaseDurationSeconds: ptr.To(defaultLeaseDurationSeconds),
|
||||
RenewTime: &metav1.MicroTime{Time: time.Now()},
|
||||
},
|
||||
}
|
||||
_, err = c.leaseClient.Leases(leaseNN.Namespace).Create(ctx, leaderLease, metav1.CreateOptions{})
|
||||
// If the create was successful, then we can return here.
|
||||
if err == nil {
|
||||
klog.Infof("Created lease %q %q for %q", leaseNN.Namespace, leaseNN.Name, electee.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If there was an error, return
|
||||
if !apierrors.IsAlreadyExists(err) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
existingLease, err := c.leaseClient.Leases(leaseNN.Namespace).Get(ctx, leaseNN.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
leaseClone := existingLease.DeepCopy()
|
||||
|
||||
// Update the Lease if it either does not have a holder or is expired
|
||||
isExpired := isLeaseExpired(existingLease)
|
||||
if leaseClone.Spec.HolderIdentity == nil || *leaseClone.Spec.HolderIdentity == "" || (isExpired && *leaseClone.Spec.HolderIdentity != electeeName) {
|
||||
klog.Infof("lease %q %q is expired, resetting it and setting holder to %q", leaseNN.Namespace, leaseNN.Name, electee.Name)
|
||||
leaseClone.Spec.Strategy = &strategy
|
||||
leaseClone.Spec.PreferredHolder = nil
|
||||
if leaseClone.ObjectMeta.Annotations == nil {
|
||||
leaseClone.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
leaseClone.ObjectMeta.Annotations[ElectedByAnnotationName] = controllerName
|
||||
leaseClone.Spec.HolderIdentity = &electeeName
|
||||
|
||||
leaseClone.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()}
|
||||
leaseClone.Spec.LeaseDurationSeconds = ptr.To(defaultLeaseDurationSeconds)
|
||||
leaseClone.Spec.AcquireTime = nil
|
||||
_, err = c.leaseClient.Leases(leaseNN.Namespace).Update(ctx, leaseClone, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else if leaseClone.Spec.HolderIdentity != nil && *leaseClone.Spec.HolderIdentity != electeeName {
|
||||
klog.Infof("lease %q %q already exists for holder %q but should be held by %q, marking preferredHolder", leaseNN.Namespace, leaseNN.Name, *leaseClone.Spec.HolderIdentity, electee.Name)
|
||||
leaseClone.Spec.PreferredHolder = &electeeName
|
||||
leaseClone.Spec.Strategy = &strategy
|
||||
_, err = c.leaseClient.Leases(leaseNN.Namespace).Update(ctx, leaseClone, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Controller) listAdmissableCandidates(leaseNN types.NamespacedName) ([]*v1alpha1.LeaseCandidate, error) {
|
||||
leases, err := c.leaseCandidateInformer.Lister().LeaseCandidates(leaseNN.Namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var results []*v1alpha1.LeaseCandidate
|
||||
for _, l := range leases {
|
||||
if l.Spec.LeaseName != leaseNN.Name {
|
||||
continue
|
||||
}
|
||||
if !isLeaseCandidateExpired(l) {
|
||||
results = append(results, l)
|
||||
} else {
|
||||
klog.Infof("LeaseCandidate %s is expired", l.Name)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
@ -0,0 +1,698 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
v1alpha1 "k8s.io/api/coordination/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func TestReconcileElectionStep(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
leaseNN types.NamespacedName
|
||||
candidates []*v1alpha1.LeaseCandidate
|
||||
existingLease *v1.Lease
|
||||
expectedHolderIdentity *string
|
||||
expectedPreferredHolder string
|
||||
expectedRequeue bool
|
||||
expectedError bool
|
||||
candidatesPinged bool
|
||||
}{
|
||||
{
|
||||
name: "no candidates, no lease, noop",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{},
|
||||
existingLease: nil,
|
||||
expectedHolderIdentity: nil,
|
||||
expectedRequeue: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "no candidates, lease exists. noop, not managed by CLE",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{},
|
||||
existingLease: &v1.Lease{},
|
||||
expectedHolderIdentity: nil,
|
||||
expectedRequeue: false,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, no existing lease should create lease",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: nil,
|
||||
expectedHolderIdentity: ptr.To("component-identity-1"),
|
||||
expectedRequeue: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, lease exists, unoptimal should set preferredHolder",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-2",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.18.0",
|
||||
BinaryVersion: "1.18.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: &v1.Lease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: ptr.To("component-identity-1"),
|
||||
LeaseDurationSeconds: ptr.To(int32(10)),
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
},
|
||||
},
|
||||
expectedHolderIdentity: ptr.To("component-identity-1"),
|
||||
expectedPreferredHolder: "component-identity-2",
|
||||
expectedRequeue: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, should only elect leader from acked candidates",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PingTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-2 * electionDuration))),
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-2 * electionDuration))),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-2",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: nil,
|
||||
expectedHolderIdentity: ptr.To("component-identity-2"),
|
||||
expectedRequeue: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, lease exists, lease expired",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: &v1.Lease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: ptr.To("component-identity-expired"),
|
||||
LeaseDurationSeconds: ptr.To(int32(10)),
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-1 * time.Minute))),
|
||||
},
|
||||
},
|
||||
expectedHolderIdentity: ptr.To("component-identity-1"),
|
||||
expectedRequeue: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, no acked candidates should return error",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PingTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-1 * time.Minute))),
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-1 * time.Minute))),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: nil,
|
||||
expectedHolderIdentity: nil,
|
||||
expectedRequeue: false,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, should ping on election",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-2 * electionDuration))),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: nil,
|
||||
expectedHolderIdentity: nil,
|
||||
expectedRequeue: true,
|
||||
expectedError: false,
|
||||
candidatesPinged: true,
|
||||
},
|
||||
{
|
||||
name: "candidates exist, ping within electionDuration should cause no state change",
|
||||
leaseNN: types.NamespacedName{Namespace: "default", Name: "component-A"},
|
||||
candidates: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
PingTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now().Add(-2 * electionDuration))),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingLease: nil,
|
||||
expectedHolderIdentity: nil,
|
||||
expectedRequeue: false,
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := fake.NewSimpleClientset()
|
||||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
_ = informerFactory.Coordination().V1alpha1().LeaseCandidates().Lister()
|
||||
controller, err := NewController(
|
||||
informerFactory.Coordination().V1().Leases(),
|
||||
informerFactory.Coordination().V1alpha1().LeaseCandidates(),
|
||||
client.CoordinationV1(),
|
||||
client.CoordinationV1alpha1(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go informerFactory.Start(ctx.Done())
|
||||
informerFactory.WaitForCacheSync(ctx.Done())
|
||||
// Set up the fake client with the existing lease
|
||||
if tc.existingLease != nil {
|
||||
_, err = client.CoordinationV1().Leases(tc.existingLease.Namespace).Create(ctx, tc.existingLease, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the fake client with the candidates
|
||||
for _, candidate := range tc.candidates {
|
||||
_, err = client.CoordinationV1alpha1().LeaseCandidates(candidate.Namespace).Create(ctx, candidate, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cache.WaitForCacheSync(ctx.Done(), controller.leaseCandidateInformer.Informer().HasSynced)
|
||||
requeue, err := controller.reconcileElectionStep(ctx, tc.leaseNN)
|
||||
|
||||
if requeue != tc.expectedRequeue {
|
||||
t.Errorf("reconcileElectionStep() requeue = %v, want %v", requeue, tc.expectedRequeue)
|
||||
}
|
||||
if tc.expectedError && err == nil {
|
||||
t.Errorf("reconcileElectionStep() error = %v, want error", err)
|
||||
} else if !tc.expectedError && err != nil {
|
||||
t.Errorf("reconcileElectionStep() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
// Check the lease holder identity
|
||||
if tc.expectedHolderIdentity != nil {
|
||||
lease, err := client.CoordinationV1().Leases(tc.leaseNN.Namespace).Get(ctx, tc.leaseNN.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lease.Spec.HolderIdentity == nil || *lease.Spec.HolderIdentity != *tc.expectedHolderIdentity {
|
||||
t.Errorf("reconcileElectionStep() holderIdentity = %v, want %v", *lease.Spec.HolderIdentity, *tc.expectedHolderIdentity)
|
||||
}
|
||||
if tc.expectedPreferredHolder != "" {
|
||||
if lease.Spec.PreferredHolder == nil || *lease.Spec.PreferredHolder != tc.expectedPreferredHolder {
|
||||
t.Errorf("reconcileElectionStep() preferredHolder = %v, want %v", lease.Spec.PreferredHolder, tc.expectedPreferredHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that ping to candidate was issued
|
||||
if tc.candidatesPinged {
|
||||
pinged := false
|
||||
candidatesList, err := client.CoordinationV1alpha1().LeaseCandidates(tc.leaseNN.Namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldCandidateMap := make(map[string]*v1alpha1.LeaseCandidate)
|
||||
for _, candidate := range tc.candidates {
|
||||
oldCandidateMap[candidate.Name] = candidate
|
||||
}
|
||||
for _, candidate := range candidatesList.Items {
|
||||
if candidate.Spec.PingTime != nil {
|
||||
if oldCandidateMap[candidate.Name].Spec.PingTime == nil {
|
||||
pinged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !pinged {
|
||||
t.Errorf("reconcileElectionStep() expected candidates to be pinged")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestController(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
leaseNN types.NamespacedName
|
||||
createAfterControllerStart []*v1alpha1.LeaseCandidate
|
||||
deleteAfterControllerStart []types.NamespacedName
|
||||
expectedLeaderLeases []*v1.Lease
|
||||
}{
|
||||
{
|
||||
name: "single candidate leader election",
|
||||
leaseNN: types.NamespacedName{Namespace: "kube-system", Name: "component-A"},
|
||||
createAfterControllerStart: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLeaderLeases: []*v1.Lease{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: ptr.To("component-identity-1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple candidate leader election",
|
||||
leaseNN: types.NamespacedName{Namespace: "kube-system", Name: "component-A"},
|
||||
createAfterControllerStart: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-2",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-3",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLeaderLeases: []*v1.Lease{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: ptr.To("component-identity-1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deletion of lease triggers reelection",
|
||||
leaseNN: types.NamespacedName{Namespace: "kube-system", Name: "component-A"},
|
||||
createAfterControllerStart: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
// Leader lease
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
deleteAfterControllerStart: []types.NamespacedName{
|
||||
{Namespace: "kube-system", Name: "component-A"},
|
||||
},
|
||||
expectedLeaderLeases: []*v1.Lease{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: ptr.To("component-identity-1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "better candidate triggers reelection",
|
||||
leaseNN: types.NamespacedName{Namespace: "kube-system", Name: "component-A"},
|
||||
createAfterControllerStart: []*v1alpha1.LeaseCandidate{
|
||||
{
|
||||
// Leader lease
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-1",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.20.0",
|
||||
BinaryVersion: "1.20.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-identity-2",
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: "component-A",
|
||||
EmulationVersion: "1.19.0",
|
||||
BinaryVersion: "1.19.0",
|
||||
RenewTime: ptr.To(metav1.NewMicroTime(time.Now())),
|
||||
PreferredStrategies: []v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLeaderLeases: []*v1.Lease{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "kube-system",
|
||||
Name: "component-A",
|
||||
Annotations: map[string]string{
|
||||
ElectedByAnnotationName: controllerName,
|
||||
},
|
||||
},
|
||||
Spec: v1.LeaseSpec{
|
||||
HolderIdentity: ptr.To("component-identity-2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
controller, err := NewController(
|
||||
informerFactory.Coordination().V1().Leases(),
|
||||
informerFactory.Coordination().V1alpha1().LeaseCandidates(),
|
||||
client.CoordinationV1(),
|
||||
client.CoordinationV1alpha1(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go informerFactory.Start(ctx.Done())
|
||||
go controller.Run(ctx, 1)
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
// Mock out the removal of preferredHolder leases.
|
||||
// When controllers are running, they are expected to do this voluntarily
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
for _, expectedLease := range tc.expectedLeaderLeases {
|
||||
lease, err := client.CoordinationV1().Leases(expectedLease.Namespace).Get(ctx, expectedLease.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if preferredHolder := lease.Spec.PreferredHolder; preferredHolder != nil {
|
||||
err = client.CoordinationV1().Leases(expectedLease.Namespace).Delete(ctx, expectedLease.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
runtime.HandleError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
// Mock out leasecandidate ack.
|
||||
// When controllers are running, they are expected to watch and ack
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
for _, lc := range tc.createAfterControllerStart {
|
||||
lease, err := client.CoordinationV1alpha1().LeaseCandidates(lc.Namespace).Get(ctx, lc.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if lease.Spec.PingTime != nil {
|
||||
c := lease.DeepCopy()
|
||||
c.Spec.PingTime = nil
|
||||
c.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()}
|
||||
_, err = client.CoordinationV1alpha1().LeaseCandidates(lc.Namespace).Update(ctx, c, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
runtime.HandleError(err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, obj := range tc.createAfterControllerStart {
|
||||
_, err := client.CoordinationV1alpha1().LeaseCandidates(obj.Namespace).Create(ctx, obj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, obj := range tc.deleteAfterControllerStart {
|
||||
err := client.CoordinationV1alpha1().LeaseCandidates(obj.Namespace).Delete(ctx, obj.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, expectedLease := range tc.expectedLeaderLeases {
|
||||
var lease *v1.Lease
|
||||
err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 600*time.Second, true, func(ctx context.Context) (done bool, err error) {
|
||||
lease, err = client.CoordinationV1().Leases(expectedLease.Namespace).Get(ctx, expectedLease.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
if expectedLease.Spec.HolderIdentity == nil || lease.Spec.HolderIdentity == nil {
|
||||
return expectedLease.Spec.HolderIdentity == nil && lease.Spec.HolderIdentity == nil, nil
|
||||
}
|
||||
if expectedLease.Spec.HolderIdentity != nil && lease.Spec.HolderIdentity != nil && *expectedLease.Spec.HolderIdentity != *lease.Spec.HolderIdentity {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lease.Spec.HolderIdentity == nil {
|
||||
t.Fatalf("Expected HolderIdentity of %s but got nil", expectedLease.Name)
|
||||
}
|
||||
if *lease.Spec.HolderIdentity != *expectedLease.Spec.HolderIdentity {
|
||||
t.Errorf("Expected HolderIdentity of %s but got %s", *expectedLease.Spec.HolderIdentity, *lease.Spec.HolderIdentity)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type NewRunner func() (func(ctx context.Context, workers int), error)
|
||||
|
||||
// RunWithLeaderElection runs the provided runner function with leader election.
|
||||
// newRunnerFn might be called multiple times, and it should return another
|
||||
// controller instance's Run method each time.
|
||||
// RunWithLeaderElection only returns when the context is done, or initial
|
||||
// leader election fails.
|
||||
func RunWithLeaderElection(ctx context.Context, config *rest.Config, newRunnerFn NewRunner) {
|
||||
var cancel context.CancelFunc
|
||||
|
||||
callbacks := leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
var err error
|
||||
run, err := newRunnerFn()
|
||||
if err != nil {
|
||||
klog.Infof("Error creating runner: %v", err)
|
||||
return
|
||||
}
|
||||
run(ctx, 1)
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
cancel()
|
||||
},
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
klog.Infof("Error parsing hostname: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rl, err := resourcelock.NewFromKubeconfig(
|
||||
"leases",
|
||||
"kube-system",
|
||||
controllerName,
|
||||
resourcelock.ResourceLockConfig{
|
||||
Identity: hostname + "_" + string(uuid.NewUUID()),
|
||||
},
|
||||
config,
|
||||
10,
|
||||
)
|
||||
if err != nil {
|
||||
klog.Infof("Error creating resourcelock: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
|
||||
Lock: rl,
|
||||
LeaseDuration: 15 * time.Second,
|
||||
RenewDeadline: 10 * time.Second,
|
||||
RetryPeriod: 2 * time.Second,
|
||||
Callbacks: callbacks,
|
||||
Name: controllerName,
|
||||
})
|
||||
if err != nil {
|
||||
klog.Infof("Error creating leader elector: %v", err)
|
||||
return
|
||||
}
|
||||
le.Run(ctx)
|
||||
}
|
@ -91,6 +91,7 @@
|
||||
ignoredSubTrees:
|
||||
- "./staging/src/k8s.io/client-go/tools/cache/testing"
|
||||
- "./staging/src/k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
- "./staging/src/k8s.io/client-go/tools/leaderelection"
|
||||
- "./staging/src/k8s.io/client-go/tools/portforward"
|
||||
- "./staging/src/k8s.io/client-go/tools/record"
|
||||
- "./staging/src/k8s.io/client-go/tools/events"
|
||||
|
@ -159,6 +159,9 @@ type LeaderElectionConfig struct {
|
||||
|
||||
// Name is the name of the resource lock for debugging
|
||||
Name string
|
||||
|
||||
// Coordinated will use the Coordinated Leader Election feature
|
||||
Coordinated bool
|
||||
}
|
||||
|
||||
// LeaderCallbacks are callbacks that are triggered during certain
|
||||
@ -249,7 +252,11 @@ func (le *LeaderElector) acquire(ctx context.Context) bool {
|
||||
desc := le.config.Lock.Describe()
|
||||
klog.Infof("attempting to acquire leader lease %v...", desc)
|
||||
wait.JitterUntil(func() {
|
||||
succeeded = le.tryAcquireOrRenew(ctx)
|
||||
if !le.config.Coordinated {
|
||||
succeeded = le.tryAcquireOrRenew(ctx)
|
||||
} else {
|
||||
succeeded = le.tryCoordinatedRenew(ctx)
|
||||
}
|
||||
le.maybeReportTransition()
|
||||
if !succeeded {
|
||||
klog.V(4).Infof("failed to acquire lease %v", desc)
|
||||
@ -272,7 +279,11 @@ func (le *LeaderElector) renew(ctx context.Context) {
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, le.config.RenewDeadline)
|
||||
defer timeoutCancel()
|
||||
err := wait.PollImmediateUntil(le.config.RetryPeriod, func() (bool, error) {
|
||||
return le.tryAcquireOrRenew(timeoutCtx), nil
|
||||
if !le.config.Coordinated {
|
||||
return le.tryAcquireOrRenew(timeoutCtx), nil
|
||||
} else {
|
||||
return le.tryCoordinatedRenew(timeoutCtx), nil
|
||||
}
|
||||
}, timeoutCtx.Done())
|
||||
|
||||
le.maybeReportTransition()
|
||||
@ -282,7 +293,6 @@ func (le *LeaderElector) renew(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
le.metrics.leaderOff(le.config.Name)
|
||||
klog.Infof("failed to renew lease %v: %v", desc, err)
|
||||
cancel()
|
||||
}, le.config.RetryPeriod, ctx.Done())
|
||||
|
||||
@ -315,6 +325,81 @@ func (le *LeaderElector) release() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// tryCoordinatedRenew checks if it acquired a lease and tries to renew the
|
||||
// lease if it has already been acquired. Returns true on success else returns
|
||||
// false.
|
||||
func (le *LeaderElector) tryCoordinatedRenew(ctx context.Context) bool {
|
||||
now := metav1.NewTime(le.clock.Now())
|
||||
leaderElectionRecord := rl.LeaderElectionRecord{
|
||||
HolderIdentity: le.config.Lock.Identity(),
|
||||
LeaseDurationSeconds: int(le.config.LeaseDuration / time.Second),
|
||||
RenewTime: now,
|
||||
AcquireTime: now,
|
||||
}
|
||||
|
||||
// 1. obtain the electionRecord
|
||||
oldLeaderElectionRecord, oldLeaderElectionRawRecord, err := le.config.Lock.Get(ctx)
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
klog.Errorf("error retrieving resource lock %v: %v", le.config.Lock.Describe(), err)
|
||||
return false
|
||||
}
|
||||
klog.Infof("lease lock not found: %v", le.config.Lock.Describe())
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. Record obtained, check the Identity & Time
|
||||
if !bytes.Equal(le.observedRawRecord, oldLeaderElectionRawRecord) {
|
||||
le.setObservedRecord(oldLeaderElectionRecord)
|
||||
|
||||
le.observedRawRecord = oldLeaderElectionRawRecord
|
||||
}
|
||||
hasExpired := le.observedTime.Add(time.Second * time.Duration(oldLeaderElectionRecord.LeaseDurationSeconds)).Before(now.Time)
|
||||
|
||||
if hasExpired {
|
||||
klog.Infof("lock has expired: %v", le.config.Lock.Describe())
|
||||
return false
|
||||
}
|
||||
|
||||
if !le.IsLeader() {
|
||||
klog.V(4).Infof("lock is held by %v and has not yet expired: %v", oldLeaderElectionRecord.HolderIdentity, le.config.Lock.Describe())
|
||||
return false
|
||||
}
|
||||
|
||||
// 2b. If the lease has been marked as "end of term", don't renew it
|
||||
if le.IsLeader() && oldLeaderElectionRecord.PreferredHolder != "" {
|
||||
klog.V(4).Infof("lock is marked as 'end of term': %v", le.config.Lock.Describe())
|
||||
// TODO: Instead of letting lease expire, the holder may deleted it directly
|
||||
// This will not be compatible with all controllers, so it needs to be opt-in behavior..
|
||||
// We must ensure all code guarded by this lease has successfully completed
|
||||
// prior to releasing or there may be two processes
|
||||
// simultaneously acting on the critical path.
|
||||
// Usually once this returns false, the process is terminated..
|
||||
// xref: OnStoppedLeading
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. We're going to try to update. The leaderElectionRecord is set to it's default
|
||||
// here. Let's correct it before updating.
|
||||
if le.IsLeader() {
|
||||
leaderElectionRecord.AcquireTime = oldLeaderElectionRecord.AcquireTime
|
||||
leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
|
||||
leaderElectionRecord.Strategy = oldLeaderElectionRecord.Strategy
|
||||
le.metrics.slowpathExercised(le.config.Name)
|
||||
} else {
|
||||
leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
|
||||
}
|
||||
|
||||
// update the lock itself
|
||||
if err = le.config.Lock.Update(ctx, leaderElectionRecord); err != nil {
|
||||
klog.Errorf("Failed to update lock: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
le.setObservedRecord(&leaderElectionRecord)
|
||||
return true
|
||||
}
|
||||
|
||||
// tryAcquireOrRenew tries to acquire a leader lease if it is not already acquired,
|
||||
// else it tries to renew the lease if it has already been acquired. Returns true
|
||||
// on success else returns false.
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
coordinationv1 "k8s.io/api/coordination/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
@ -37,8 +38,6 @@ import (
|
||||
rl "k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/clock"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createLockObject(t *testing.T, objectType, namespace, name string, record *rl.LeaderElectionRecord) (obj runtime.Object) {
|
||||
@ -353,6 +352,147 @@ func testTryAcquireOrRenew(t *testing.T, objectType string) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryCoordinatedRenew(t *testing.T) {
|
||||
objectType := "leases"
|
||||
clock := clock.RealClock{}
|
||||
future := clock.Now().Add(1000 * time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
observedRecord rl.LeaderElectionRecord
|
||||
observedTime time.Time
|
||||
retryAfter time.Duration
|
||||
reactors []Reactor
|
||||
expectedEvents []string
|
||||
|
||||
expectSuccess bool
|
||||
transitionLeader bool
|
||||
outHolder string
|
||||
}{
|
||||
{
|
||||
name: "don't acquire from led, acked object",
|
||||
reactors: []Reactor{
|
||||
{
|
||||
verb: "get",
|
||||
reaction: func(action fakeclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, createLockObject(t, objectType, action.GetNamespace(), action.(fakeclient.GetAction).GetName(), &rl.LeaderElectionRecord{HolderIdentity: "bing"}), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
observedTime: future,
|
||||
|
||||
expectSuccess: false,
|
||||
outHolder: "bing",
|
||||
},
|
||||
{
|
||||
name: "renew already acquired object",
|
||||
reactors: []Reactor{
|
||||
{
|
||||
verb: "get",
|
||||
reaction: func(action fakeclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, createLockObject(t, objectType, action.GetNamespace(), action.(fakeclient.GetAction).GetName(), &rl.LeaderElectionRecord{HolderIdentity: "baz"}), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
verb: "update",
|
||||
reaction: func(action fakeclient.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, action.(fakeclient.CreateAction).GetObject(), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
observedTime: future,
|
||||
observedRecord: rl.LeaderElectionRecord{HolderIdentity: "baz"},
|
||||
|
||||
expectSuccess: true,
|
||||
outHolder: "baz",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
test := &tests[i]
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// OnNewLeader is called async so we have to wait for it.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var reportedLeader string
|
||||
var lock rl.Interface
|
||||
|
||||
objectMeta := metav1.ObjectMeta{Namespace: "foo", Name: "bar"}
|
||||
recorder := record.NewFakeRecorder(100)
|
||||
resourceLockConfig := rl.ResourceLockConfig{
|
||||
Identity: "baz",
|
||||
EventRecorder: recorder,
|
||||
}
|
||||
c := &fake.Clientset{}
|
||||
for _, reactor := range test.reactors {
|
||||
c.AddReactor(reactor.verb, objectType, reactor.reaction)
|
||||
}
|
||||
c.AddReactor("*", "*", func(action fakeclient.Action) (bool, runtime.Object, error) {
|
||||
t.Errorf("unreachable action. testclient called too many times: %+v", action)
|
||||
return true, nil, fmt.Errorf("unreachable action")
|
||||
})
|
||||
|
||||
lock = &rl.LeaseLock{
|
||||
LeaseMeta: objectMeta,
|
||||
LockConfig: resourceLockConfig,
|
||||
Client: c.CoordinationV1(),
|
||||
}
|
||||
lec := LeaderElectionConfig{
|
||||
Lock: lock,
|
||||
LeaseDuration: 10 * time.Second,
|
||||
Callbacks: LeaderCallbacks{
|
||||
OnNewLeader: func(l string) {
|
||||
defer wg.Done()
|
||||
reportedLeader = l
|
||||
},
|
||||
},
|
||||
Coordinated: true,
|
||||
}
|
||||
observedRawRecord := GetRawRecordOrDie(t, objectType, test.observedRecord)
|
||||
le := &LeaderElector{
|
||||
config: lec,
|
||||
observedRecord: test.observedRecord,
|
||||
observedRawRecord: observedRawRecord,
|
||||
observedTime: test.observedTime,
|
||||
clock: clock,
|
||||
metrics: globalMetricsFactory.newLeaderMetrics(),
|
||||
}
|
||||
if test.expectSuccess != le.tryCoordinatedRenew(context.Background()) {
|
||||
if test.retryAfter != 0 {
|
||||
time.Sleep(test.retryAfter)
|
||||
if test.expectSuccess != le.tryCoordinatedRenew(context.Background()) {
|
||||
t.Errorf("unexpected result of tryCoordinatedRenew: [succeeded=%v]", !test.expectSuccess)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("unexpected result of gryCoordinatedRenew: [succeeded=%v]", !test.expectSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
le.observedRecord.AcquireTime = metav1.Time{}
|
||||
le.observedRecord.RenewTime = metav1.Time{}
|
||||
if le.observedRecord.HolderIdentity != test.outHolder {
|
||||
t.Errorf("expected holder:\n\t%+v\ngot:\n\t%+v", test.outHolder, le.observedRecord.HolderIdentity)
|
||||
}
|
||||
if len(test.reactors) != len(c.Actions()) {
|
||||
t.Errorf("wrong number of api interactions")
|
||||
}
|
||||
if test.transitionLeader && le.observedRecord.LeaderTransitions != 1 {
|
||||
t.Errorf("leader should have transitioned but did not")
|
||||
}
|
||||
if !test.transitionLeader && le.observedRecord.LeaderTransitions != 0 {
|
||||
t.Errorf("leader should not have transitioned but did")
|
||||
}
|
||||
|
||||
le.maybeReportTransition()
|
||||
wg.Wait()
|
||||
if reportedLeader != test.outHolder {
|
||||
t.Errorf("reported leader was not the new leader. expected %q, got %q", test.outHolder, reportedLeader)
|
||||
}
|
||||
assertEqualEvents(t, test.expectedEvents, recorder.Events)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Will test leader election using lease as the resource
|
||||
func TestTryAcquireOrRenewLeases(t *testing.T) {
|
||||
testTryAcquireOrRenew(t, "leases")
|
||||
|
@ -0,0 +1,196 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
v1alpha1 "k8s.io/api/coordination/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
coordinationv1alpha1client "k8s.io/client-go/kubernetes/typed/coordination/v1alpha1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
const requeueInterval = 5 * time.Minute
|
||||
|
||||
type LeaseCandidate struct {
|
||||
LeaseClient coordinationv1alpha1client.LeaseCandidateInterface
|
||||
LeaseCandidateInformer cache.SharedIndexInformer
|
||||
InformerFactory informers.SharedInformerFactory
|
||||
HasSynced cache.InformerSynced
|
||||
|
||||
// At most there will be one item in this Queue (since we only watch one item)
|
||||
queue workqueue.TypedRateLimitingInterface[int]
|
||||
|
||||
name string
|
||||
namespace string
|
||||
|
||||
// controller lease
|
||||
leaseName string
|
||||
|
||||
Clock clock.Clock
|
||||
|
||||
binaryVersion, emulationVersion string
|
||||
preferredStrategies []v1.CoordinatedLeaseStrategy
|
||||
}
|
||||
|
||||
func NewCandidate(clientset kubernetes.Interface,
|
||||
candidateName string,
|
||||
candidateNamespace string,
|
||||
targetLease string,
|
||||
clock clock.Clock,
|
||||
binaryVersion, emulationVersion string,
|
||||
preferredStrategies []v1.CoordinatedLeaseStrategy,
|
||||
) (*LeaseCandidate, error) {
|
||||
fieldSelector := fields.OneTermEqualSelector("metadata.name", candidateName).String()
|
||||
// A separate informer factory is required because this must start before informerFactories
|
||||
// are started for leader elected components
|
||||
informerFactory := informers.NewSharedInformerFactoryWithOptions(
|
||||
clientset, 5*time.Minute,
|
||||
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
|
||||
options.FieldSelector = fieldSelector
|
||||
}),
|
||||
)
|
||||
leaseCandidateInformer := informerFactory.Coordination().V1alpha1().LeaseCandidates().Informer()
|
||||
|
||||
lc := &LeaseCandidate{
|
||||
LeaseClient: clientset.CoordinationV1alpha1().LeaseCandidates(candidateNamespace),
|
||||
LeaseCandidateInformer: leaseCandidateInformer,
|
||||
InformerFactory: informerFactory,
|
||||
name: candidateName,
|
||||
namespace: candidateNamespace,
|
||||
leaseName: targetLease,
|
||||
Clock: clock,
|
||||
binaryVersion: binaryVersion,
|
||||
emulationVersion: emulationVersion,
|
||||
preferredStrategies: preferredStrategies,
|
||||
}
|
||||
lc.queue = workqueue.NewTypedRateLimitingQueueWithConfig(workqueue.DefaultTypedControllerRateLimiter[int](), workqueue.TypedRateLimitingQueueConfig[int]{Name: "leasecandidate"})
|
||||
|
||||
synced, err := leaseCandidateInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
if leasecandidate, ok := newObj.(*v1alpha1.LeaseCandidate); ok {
|
||||
if leasecandidate.Spec.PingTime != nil {
|
||||
lc.enqueueLease()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lc.HasSynced = synced.HasSynced
|
||||
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
func (c *LeaseCandidate) Run(ctx context.Context) {
|
||||
defer c.queue.ShutDown()
|
||||
|
||||
go c.InformerFactory.Start(ctx.Done())
|
||||
if !cache.WaitForNamedCacheSync("leasecandidateclient", ctx.Done(), c.HasSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
c.enqueueLease()
|
||||
go c.runWorker(ctx)
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (c *LeaseCandidate) runWorker(ctx context.Context) {
|
||||
for c.processNextWorkItem(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LeaseCandidate) processNextWorkItem(ctx context.Context) bool {
|
||||
key, shutdown := c.queue.Get()
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
defer c.queue.Done(key)
|
||||
|
||||
err := c.ensureLease(ctx)
|
||||
if err == nil {
|
||||
c.queue.AddAfter(key, requeueInterval)
|
||||
return true
|
||||
}
|
||||
|
||||
utilruntime.HandleError(err)
|
||||
klog.Infof("processNextWorkItem.AddRateLimited: %v", key)
|
||||
c.queue.AddRateLimited(key)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *LeaseCandidate) enqueueLease() {
|
||||
c.queue.Add(0)
|
||||
}
|
||||
|
||||
// ensureLease creates the lease if it does not exist and renew it if it exists. Returns the lease and
|
||||
// a bool (true if this call created the lease), or any error that occurs.
|
||||
func (c *LeaseCandidate) ensureLease(ctx context.Context) error {
|
||||
lease, err := c.LeaseClient.Get(ctx, c.name, metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
klog.V(2).Infof("Creating lease candidate")
|
||||
// lease does not exist, create it.
|
||||
leaseToCreate := c.newLease()
|
||||
_, err := c.LeaseClient.Create(ctx, leaseToCreate, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.V(2).Infof("Created lease candidate")
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.V(2).Infof("lease candidate exists.. renewing")
|
||||
clone := lease.DeepCopy()
|
||||
clone.Spec.RenewTime = &metav1.MicroTime{Time: c.Clock.Now()}
|
||||
clone.Spec.PingTime = nil
|
||||
_, err = c.LeaseClient.Update(ctx, clone, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LeaseCandidate) newLease() *v1alpha1.LeaseCandidate {
|
||||
lease := &v1alpha1.LeaseCandidate{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: c.name,
|
||||
Namespace: c.namespace,
|
||||
},
|
||||
Spec: v1alpha1.LeaseCandidateSpec{
|
||||
LeaseName: c.leaseName,
|
||||
BinaryVersion: c.binaryVersion,
|
||||
EmulationVersion: c.emulationVersion,
|
||||
PreferredStrategies: c.preferredStrategies,
|
||||
},
|
||||
}
|
||||
lease.Spec.RenewTime = &metav1.MicroTime{Time: c.Clock.Now()}
|
||||
return lease
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2024 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 leaderelection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
type testcase struct {
|
||||
candidateName, candidateNamespace, leaseName string
|
||||
binaryVersion, emulationVersion string
|
||||
}
|
||||
|
||||
func TestLeaseCandidateCreation(t *testing.T) {
|
||||
tc := testcase{
|
||||
candidateName: "foo",
|
||||
candidateNamespace: "default",
|
||||
leaseName: "lease",
|
||||
binaryVersion: "1.30.0",
|
||||
emulationVersion: "1.30.0",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
candidate, err := NewCandidate(
|
||||
client,
|
||||
tc.candidateName,
|
||||
tc.candidateNamespace,
|
||||
tc.leaseName,
|
||||
clock.RealClock{},
|
||||
tc.binaryVersion,
|
||||
tc.emulationVersion,
|
||||
[]v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go candidate.Run(ctx)
|
||||
err = pollForLease(ctx, tc, client, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeaseCandidateAck(t *testing.T) {
|
||||
tc := testcase{
|
||||
candidateName: "foo",
|
||||
candidateNamespace: "default",
|
||||
leaseName: "lease",
|
||||
binaryVersion: "1.30.0",
|
||||
emulationVersion: "1.30.0",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
candidate, err := NewCandidate(
|
||||
client,
|
||||
tc.candidateName,
|
||||
tc.candidateNamespace,
|
||||
tc.leaseName,
|
||||
clock.RealClock{},
|
||||
tc.binaryVersion,
|
||||
tc.emulationVersion,
|
||||
[]v1.CoordinatedLeaseStrategy{v1.OldestEmulationVersion},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go candidate.Run(ctx)
|
||||
err = pollForLease(ctx, tc, client, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Update PingTime and verify that the client renews
|
||||
ensureAfter := &metav1.MicroTime{Time: time.Now()}
|
||||
lc, err := client.CoordinationV1alpha1().LeaseCandidates(tc.candidateNamespace).Get(ctx, tc.candidateName, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if lc.Spec.PingTime == nil {
|
||||
c := lc.DeepCopy()
|
||||
c.Spec.PingTime = &metav1.MicroTime{Time: time.Now()}
|
||||
_, err = client.CoordinationV1alpha1().LeaseCandidates(tc.candidateNamespace).Update(ctx, c, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = pollForLease(ctx, tc, client, ensureAfter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func pollForLease(ctx context.Context, tc testcase, client *fake.Clientset, t *metav1.MicroTime) error {
|
||||
return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 10*time.Second, true, func(ctx context.Context) (done bool, err error) {
|
||||
lc, err := client.CoordinationV1alpha1().LeaseCandidates(tc.candidateNamespace).Get(ctx, tc.candidateName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
if lc.Spec.BinaryVersion == tc.binaryVersion &&
|
||||
lc.Spec.EmulationVersion == tc.emulationVersion &&
|
||||
lc.Spec.LeaseName == tc.leaseName &&
|
||||
lc.Spec.PingTime == nil &&
|
||||
lc.Spec.RenewTime != nil {
|
||||
// Ensure that if a time is provided, the renewTime occurred after the provided time.
|
||||
if t != nil && t.After(lc.Spec.RenewTime.Time) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
}
|
@ -19,14 +19,15 @@ package resourcelock
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -114,11 +115,13 @@ type LeaderElectionRecord struct {
|
||||
// attempt to acquire leases with empty identities and will wait for the full lease
|
||||
// interval to expire before attempting to reacquire. This value is set to empty when
|
||||
// a client voluntarily steps down.
|
||||
HolderIdentity string `json:"holderIdentity"`
|
||||
LeaseDurationSeconds int `json:"leaseDurationSeconds"`
|
||||
AcquireTime metav1.Time `json:"acquireTime"`
|
||||
RenewTime metav1.Time `json:"renewTime"`
|
||||
LeaderTransitions int `json:"leaderTransitions"`
|
||||
HolderIdentity string `json:"holderIdentity"`
|
||||
LeaseDurationSeconds int `json:"leaseDurationSeconds"`
|
||||
AcquireTime metav1.Time `json:"acquireTime"`
|
||||
RenewTime metav1.Time `json:"renewTime"`
|
||||
LeaderTransitions int `json:"leaderTransitions"`
|
||||
Strategy v1.CoordinatedLeaseStrategy `json:"strategy"`
|
||||
PreferredHolder string `json:"preferredHolder"`
|
||||
}
|
||||
|
||||
// EventRecorder records a change in the ResourceLock.
|
||||
|
@ -122,6 +122,12 @@ func LeaseSpecToLeaderElectionRecord(spec *coordinationv1.LeaseSpec) *LeaderElec
|
||||
if spec.RenewTime != nil {
|
||||
r.RenewTime = metav1.Time{Time: spec.RenewTime.Time}
|
||||
}
|
||||
if spec.PreferredHolder != nil {
|
||||
r.PreferredHolder = *spec.PreferredHolder
|
||||
}
|
||||
if spec.Strategy != nil {
|
||||
r.Strategy = *spec.Strategy
|
||||
}
|
||||
return &r
|
||||
|
||||
}
|
||||
@ -129,11 +135,18 @@ func LeaseSpecToLeaderElectionRecord(spec *coordinationv1.LeaseSpec) *LeaderElec
|
||||
func LeaderElectionRecordToLeaseSpec(ler *LeaderElectionRecord) coordinationv1.LeaseSpec {
|
||||
leaseDurationSeconds := int32(ler.LeaseDurationSeconds)
|
||||
leaseTransitions := int32(ler.LeaderTransitions)
|
||||
return coordinationv1.LeaseSpec{
|
||||
spec := coordinationv1.LeaseSpec{
|
||||
HolderIdentity: &ler.HolderIdentity,
|
||||
LeaseDurationSeconds: &leaseDurationSeconds,
|
||||
AcquireTime: &metav1.MicroTime{Time: ler.AcquireTime.Time},
|
||||
RenewTime: &metav1.MicroTime{Time: ler.RenewTime.Time},
|
||||
LeaseTransitions: &leaseTransitions,
|
||||
}
|
||||
if ler.PreferredHolder != "" {
|
||||
spec.PreferredHolder = &ler.PreferredHolder
|
||||
}
|
||||
if ler.Strategy != "" {
|
||||
spec.Strategy = &ler.Strategy
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
296
test/integration/apiserver/coordinated_leader_election_test.go
Normal file
296
test/integration/apiserver/coordinated_leader_election_test.go
Normal file
@ -0,0 +1,296 @@
|
||||
/*
|
||||
Copyright 2024 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 apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/coordination/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
kubernetes "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/clock"
|
||||
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestSingleLeaseCandidate(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CoordinatedLeaderElection, true)
|
||||
|
||||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
config := server.ClientConfig
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cletest := setupCLE(config, ctx, cancel, t)
|
||||
defer cletest.cleanup()
|
||||
go cletest.createAndRunFakeController("foo1", "default", "foo", "1.20.0", "1.20.0")
|
||||
cletest.pollForLease("foo", "default", "foo1")
|
||||
}
|
||||
|
||||
func TestMultipleLeaseCandidate(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CoordinatedLeaderElection, true)
|
||||
|
||||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
config := server.ClientConfig
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cletest := setupCLE(config, ctx, cancel, t)
|
||||
defer cletest.cleanup()
|
||||
go cletest.createAndRunFakeController("foo1", "default", "foo", "1.20.0", "1.20.0")
|
||||
go cletest.createAndRunFakeController("foo2", "default", "foo", "1.20.0", "1.19.0")
|
||||
go cletest.createAndRunFakeController("foo3", "default", "foo", "1.19.0", "1.19.0")
|
||||
go cletest.createAndRunFakeController("foo4", "default", "foo", "1.2.0", "1.19.0")
|
||||
go cletest.createAndRunFakeController("foo5", "default", "foo", "1.20.0", "1.19.0")
|
||||
cletest.pollForLease("foo", "default", "foo3")
|
||||
}
|
||||
|
||||
func TestLeaderDisappear(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CoordinatedLeaderElection, true)
|
||||
|
||||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
config := server.ClientConfig
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cletest := setupCLE(config, ctx, cancel, t)
|
||||
defer cletest.cleanup()
|
||||
|
||||
go cletest.createAndRunFakeController("foo1", "default", "foo", "1.20.0", "1.20.0")
|
||||
go cletest.createAndRunFakeController("foo2", "default", "foo", "1.20.0", "1.19.0")
|
||||
cletest.pollForLease("foo", "default", "foo2")
|
||||
cletest.cancelController("foo2", "default")
|
||||
cletest.deleteLC("foo2", "default")
|
||||
cletest.pollForLease("foo", "default", "foo1")
|
||||
}
|
||||
|
||||
func TestLeaseSwapIfBetterAvailable(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CoordinatedLeaderElection, true)
|
||||
|
||||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
config := server.ClientConfig
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cletest := setupCLE(config, ctx, cancel, t)
|
||||
defer cletest.cleanup()
|
||||
|
||||
go cletest.createAndRunFakeController("bar1", "default", "bar", "1.20.0", "1.20.0")
|
||||
cletest.pollForLease("bar", "default", "bar1")
|
||||
go cletest.createAndRunFakeController("bar2", "default", "bar", "1.19.0", "1.19.0")
|
||||
cletest.pollForLease("bar", "default", "bar2")
|
||||
}
|
||||
|
||||
// TestUpgradeSkew tests that a legacy client and a CLE aware client operating on the same lease do not cause errors
|
||||
func TestUpgradeSkew(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CoordinatedLeaderElection, true)
|
||||
|
||||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
config := server.ClientConfig
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cletest := setupCLE(config, ctx, cancel, t)
|
||||
defer cletest.cleanup()
|
||||
|
||||
go cletest.createAndRunFakeLegacyController("foo1-130", "default", "foo")
|
||||
cletest.pollForLease("foo", "default", "foo1-130")
|
||||
go cletest.createAndRunFakeController("foo1-131", "default", "foo", "1.31.0", "1.31.0")
|
||||
// running a new controller should not kick off old leader
|
||||
cletest.pollForLease("foo", "default", "foo1-130")
|
||||
cletest.cancelController("foo1-130", "default")
|
||||
cletest.pollForLease("foo", "default", "foo1-131")
|
||||
}
|
||||
|
||||
type ctxCancelPair struct {
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
}
|
||||
type cleTest struct {
|
||||
config *rest.Config
|
||||
clientset *kubernetes.Clientset
|
||||
t *testing.T
|
||||
ctxList map[string]ctxCancelPair
|
||||
}
|
||||
|
||||
func (t cleTest) createAndRunFakeLegacyController(name string, namespace string, targetLease string) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.ctxList[name+"/"+namespace] = ctxCancelPair{ctx, cancel}
|
||||
|
||||
electionChecker := leaderelection.NewLeaderHealthzAdaptor(time.Second * 20)
|
||||
go leaderElectAndRunUncoordinated(ctx, t.config, name, electionChecker,
|
||||
namespace,
|
||||
"leases",
|
||||
targetLease,
|
||||
leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
klog.Info("Elected leader, starting..")
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
klog.Errorf("%s Lost leadership, stopping", name)
|
||||
// klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
func (t cleTest) createAndRunFakeController(name string, namespace string, targetLease string, binaryVersion string, compatibilityVersion string) {
|
||||
identityLease, err := leaderelection.NewCandidate(
|
||||
t.clientset,
|
||||
name,
|
||||
namespace,
|
||||
targetLease,
|
||||
clock.RealClock{},
|
||||
binaryVersion,
|
||||
compatibilityVersion,
|
||||
[]v1.CoordinatedLeaseStrategy{"OldestEmulationVersion"},
|
||||
)
|
||||
if err != nil {
|
||||
t.t.Error(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.ctxList[name+"/"+namespace] = ctxCancelPair{ctx, cancel}
|
||||
go identityLease.Run(ctx)
|
||||
|
||||
electionChecker := leaderelection.NewLeaderHealthzAdaptor(time.Second * 20)
|
||||
go leaderElectAndRunCoordinated(ctx, t.config, name, electionChecker,
|
||||
namespace,
|
||||
"leases",
|
||||
targetLease,
|
||||
leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
klog.Info("Elected leader, starting..")
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
klog.Errorf("%s Lost leadership, stopping", name)
|
||||
// klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func leaderElectAndRunUncoordinated(ctx context.Context, kubeconfig *rest.Config, lockIdentity string, electionChecker *leaderelection.HealthzAdaptor, resourceNamespace, resourceLock, leaseName string, callbacks leaderelection.LeaderCallbacks) {
|
||||
leaderElectAndRun(ctx, kubeconfig, lockIdentity, electionChecker, resourceNamespace, resourceLock, leaseName, callbacks, false)
|
||||
}
|
||||
|
||||
func leaderElectAndRunCoordinated(ctx context.Context, kubeconfig *rest.Config, lockIdentity string, electionChecker *leaderelection.HealthzAdaptor, resourceNamespace, resourceLock, leaseName string, callbacks leaderelection.LeaderCallbacks) {
|
||||
leaderElectAndRun(ctx, kubeconfig, lockIdentity, electionChecker, resourceNamespace, resourceLock, leaseName, callbacks, true)
|
||||
}
|
||||
|
||||
func leaderElectAndRun(ctx context.Context, kubeconfig *rest.Config, lockIdentity string, electionChecker *leaderelection.HealthzAdaptor, resourceNamespace, resourceLock, leaseName string, callbacks leaderelection.LeaderCallbacks, coordinated bool) {
|
||||
logger := klog.FromContext(ctx)
|
||||
rl, err := resourcelock.NewFromKubeconfig(resourceLock,
|
||||
resourceNamespace,
|
||||
leaseName,
|
||||
resourcelock.ResourceLockConfig{
|
||||
Identity: lockIdentity,
|
||||
},
|
||||
kubeconfig,
|
||||
5*time.Second)
|
||||
if err != nil {
|
||||
logger.Error(err, "Error creating lock")
|
||||
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
||||
}
|
||||
|
||||
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
||||
Lock: rl,
|
||||
LeaseDuration: 5 * time.Second,
|
||||
RenewDeadline: 3 * time.Second,
|
||||
RetryPeriod: 2 * time.Second,
|
||||
Callbacks: callbacks,
|
||||
WatchDog: electionChecker,
|
||||
Name: leaseName,
|
||||
Coordinated: coordinated,
|
||||
})
|
||||
}
|
||||
|
||||
func (t cleTest) pollForLease(name, namespace, holder string) {
|
||||
err := wait.PollUntilContextTimeout(t.ctxList["main"].ctx, 1000*time.Millisecond, 15*time.Second, true, func(ctx context.Context) (done bool, err error) {
|
||||
lease, err := t.clientset.CoordinationV1().Leases(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false, nil
|
||||
}
|
||||
return lease.Spec.HolderIdentity != nil && *lease.Spec.HolderIdentity == holder, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.t.Fatalf("timeout awiting for Lease %s %s err: %v", name, namespace, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t cleTest) cancelController(name, namespace string) {
|
||||
t.ctxList[name+"/"+namespace].cancel()
|
||||
delete(t.ctxList, name+"/"+namespace)
|
||||
}
|
||||
|
||||
func (t cleTest) cleanup() {
|
||||
err := t.clientset.CoordinationV1().Leases("kube-system").Delete(context.TODO(), "leader-election-controller", metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.t.Error(err)
|
||||
}
|
||||
for _, c := range t.ctxList {
|
||||
c.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (t cleTest) deleteLC(name, namespace string) {
|
||||
err := t.clientset.CoordinationV1alpha1().LeaseCandidates(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupCLE(config *rest.Config, ctx context.Context, cancel func(), t *testing.T) cleTest {
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a := ctxCancelPair{ctx, cancel}
|
||||
return cleTest{
|
||||
config: config,
|
||||
clientset: clientset,
|
||||
ctxList: map[string]ctxCancelPair{"main": a},
|
||||
t: t,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user