mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
CLE controller and client changes
This commit is contained in:
parent
b5a62f14cd
commit
c47ff1e1a9
@ -28,8 +28,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/blang/semver/v4"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
v1 "k8s.io/api/coordination/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
@ -78,7 +79,9 @@ import (
|
|||||||
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
||||||
garbagecollector "k8s.io/kubernetes/pkg/controller/garbagecollector"
|
garbagecollector "k8s.io/kubernetes/pkg/controller/garbagecollector"
|
||||||
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
||||||
|
kubefeatures "k8s.io/kubernetes/pkg/features"
|
||||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -289,6 +292,30 @@ func Run(ctx context.Context, c *config.CompletedConfig) error {
|
|||||||
return startSATokenControllerInit(ctx, controllerContext, controllerName)
|
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
|
// Start the main lock
|
||||||
go leaderElectAndRun(ctx, c, id, electionChecker,
|
go leaderElectAndRun(ctx, c, id, electionChecker,
|
||||||
@ -886,6 +913,7 @@ func leaderElectAndRun(ctx context.Context, c *config.CompletedConfig, lockIdent
|
|||||||
Callbacks: callbacks,
|
Callbacks: callbacks,
|
||||||
WatchDog: electionChecker,
|
WatchDog: electionChecker,
|
||||||
Name: leaseName,
|
Name: leaseName,
|
||||||
|
Coordinated: utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection),
|
||||||
})
|
})
|
||||||
|
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
|
@ -24,8 +24,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
goruntime "runtime"
|
goruntime "runtime"
|
||||||
|
|
||||||
|
"github.com/blang/semver/v4"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
coordinationv1 "k8s.io/api/coordination/v1"
|
||||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
@ -56,8 +57,11 @@ import (
|
|||||||
"k8s.io/component-base/version"
|
"k8s.io/component-base/version"
|
||||||
"k8s.io/component-base/version/verflag"
|
"k8s.io/component-base/version/verflag"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
schedulerserverconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
|
schedulerserverconfig "k8s.io/kubernetes/cmd/kube-scheduler/app/config"
|
||||||
"k8s.io/kubernetes/cmd/kube-scheduler/app/options"
|
"k8s.io/kubernetes/cmd/kube-scheduler/app/options"
|
||||||
|
kubefeatures "k8s.io/kubernetes/pkg/features"
|
||||||
"k8s.io/kubernetes/pkg/scheduler"
|
"k8s.io/kubernetes/pkg/scheduler"
|
||||||
kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config"
|
kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config"
|
||||||
"k8s.io/kubernetes/pkg/scheduler/apis/config/latest"
|
"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)
|
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.
|
// Start up the healthz server.
|
||||||
if cc.SecureServing != nil {
|
if cc.SecureServing != nil {
|
||||||
handler := buildHandlerChain(newHealthEndpointsAndMetricsHandler(&cc.ComponentConfig, cc.InformerFactory, isLeader, checks, readyzChecks), cc.Authentication.Authenticator, cc.Authorization.Authorizer)
|
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 leader election is enabled, runCommand via LeaderElector until done and exit.
|
||||||
if cc.LeaderElection != nil {
|
if cc.LeaderElection != nil {
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
|
||||||
|
cc.LeaderElection.Coordinated = true
|
||||||
|
}
|
||||||
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
|
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
|
||||||
OnStartedLeading: func(ctx context.Context) {
|
OnStartedLeading: func(ctx context.Context) {
|
||||||
close(waitingForLeader)
|
close(waitingForLeader)
|
||||||
|
@ -1027,6 +1027,7 @@ EOF
|
|||||||
--feature-gates="${FEATURE_GATES}" \
|
--feature-gates="${FEATURE_GATES}" \
|
||||||
--authentication-kubeconfig "${CERT_DIR}"/scheduler.kubeconfig \
|
--authentication-kubeconfig "${CERT_DIR}"/scheduler.kubeconfig \
|
||||||
--authorization-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 &
|
--master="https://${API_HOST}:${API_SECURE_PORT}" >"${SCHEDULER_LOG}" 2>&1 &
|
||||||
SCHEDULER_PID=$!
|
SCHEDULER_PID=$!
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@ -41,6 +42,7 @@ import (
|
|||||||
|
|
||||||
"k8s.io/kubernetes/pkg/controlplane/controller/apiserverleasegc"
|
"k8s.io/kubernetes/pkg/controlplane/controller/apiserverleasegc"
|
||||||
"k8s.io/kubernetes/pkg/controlplane/controller/clusterauthenticationtrust"
|
"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/legacytokentracking"
|
||||||
"k8s.io/kubernetes/pkg/controlplane/controller/systemnamespaces"
|
"k8s.io/kubernetes/pkg/controlplane/controller/systemnamespaces"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"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)
|
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) {
|
if utilfeature.DefaultFeatureGate.Enabled(features.UnknownVersionInteroperabilityProxy) {
|
||||||
peeraddress := getPeerAddress(c.Extra.PeerAdvertiseAddress, c.Generic.PublicAddress, publicServicePort)
|
peeraddress := getPeerAddress(c.Extra.PeerAdvertiseAddress, c.Generic.PublicAddress, publicServicePort)
|
||||||
peerEndpointCtrl := peerreconcilers.New(
|
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:
|
ignoredSubTrees:
|
||||||
- "./staging/src/k8s.io/client-go/tools/cache/testing"
|
- "./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/resourcelock"
|
||||||
|
- "./staging/src/k8s.io/client-go/tools/leaderelection"
|
||||||
- "./staging/src/k8s.io/client-go/tools/portforward"
|
- "./staging/src/k8s.io/client-go/tools/portforward"
|
||||||
- "./staging/src/k8s.io/client-go/tools/record"
|
- "./staging/src/k8s.io/client-go/tools/record"
|
||||||
- "./staging/src/k8s.io/client-go/tools/events"
|
- "./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 is the name of the resource lock for debugging
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
// Coordinated will use the Coordinated Leader Election feature
|
||||||
|
Coordinated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeaderCallbacks are callbacks that are triggered during certain
|
// 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()
|
desc := le.config.Lock.Describe()
|
||||||
klog.Infof("attempting to acquire leader lease %v...", desc)
|
klog.Infof("attempting to acquire leader lease %v...", desc)
|
||||||
wait.JitterUntil(func() {
|
wait.JitterUntil(func() {
|
||||||
|
if !le.config.Coordinated {
|
||||||
succeeded = le.tryAcquireOrRenew(ctx)
|
succeeded = le.tryAcquireOrRenew(ctx)
|
||||||
|
} else {
|
||||||
|
succeeded = le.tryCoordinatedRenew(ctx)
|
||||||
|
}
|
||||||
le.maybeReportTransition()
|
le.maybeReportTransition()
|
||||||
if !succeeded {
|
if !succeeded {
|
||||||
klog.V(4).Infof("failed to acquire lease %v", desc)
|
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)
|
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, le.config.RenewDeadline)
|
||||||
defer timeoutCancel()
|
defer timeoutCancel()
|
||||||
err := wait.PollImmediateUntil(le.config.RetryPeriod, func() (bool, error) {
|
err := wait.PollImmediateUntil(le.config.RetryPeriod, func() (bool, error) {
|
||||||
|
if !le.config.Coordinated {
|
||||||
return le.tryAcquireOrRenew(timeoutCtx), nil
|
return le.tryAcquireOrRenew(timeoutCtx), nil
|
||||||
|
} else {
|
||||||
|
return le.tryCoordinatedRenew(timeoutCtx), nil
|
||||||
|
}
|
||||||
}, timeoutCtx.Done())
|
}, timeoutCtx.Done())
|
||||||
|
|
||||||
le.maybeReportTransition()
|
le.maybeReportTransition()
|
||||||
@ -282,7 +293,6 @@ func (le *LeaderElector) renew(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
le.metrics.leaderOff(le.config.Name)
|
le.metrics.leaderOff(le.config.Name)
|
||||||
klog.Infof("failed to renew lease %v: %v", desc, err)
|
|
||||||
cancel()
|
cancel()
|
||||||
}, le.config.RetryPeriod, ctx.Done())
|
}, le.config.RetryPeriod, ctx.Done())
|
||||||
|
|
||||||
@ -315,6 +325,81 @@ func (le *LeaderElector) release() bool {
|
|||||||
return true
|
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,
|
// 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
|
// else it tries to renew the lease if it has already been acquired. Returns true
|
||||||
// on success else returns false.
|
// on success else returns false.
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
coordinationv1 "k8s.io/api/coordination/v1"
|
coordinationv1 "k8s.io/api/coordination/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/equality"
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
@ -37,8 +38,6 @@ import (
|
|||||||
rl "k8s.io/client-go/tools/leaderelection/resourcelock"
|
rl "k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||||
"k8s.io/client-go/tools/record"
|
"k8s.io/client-go/tools/record"
|
||||||
"k8s.io/utils/clock"
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func createLockObject(t *testing.T, objectType, namespace, name string, record *rl.LeaderElectionRecord) (obj runtime.Object) {
|
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
|
// Will test leader election using lease as the resource
|
||||||
func TestTryAcquireOrRenewLeases(t *testing.T) {
|
func TestTryAcquireOrRenewLeases(t *testing.T) {
|
||||||
testTryAcquireOrRenew(t, "leases")
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/coordination/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
|
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
|
||||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -119,6 +120,8 @@ type LeaderElectionRecord struct {
|
|||||||
AcquireTime metav1.Time `json:"acquireTime"`
|
AcquireTime metav1.Time `json:"acquireTime"`
|
||||||
RenewTime metav1.Time `json:"renewTime"`
|
RenewTime metav1.Time `json:"renewTime"`
|
||||||
LeaderTransitions int `json:"leaderTransitions"`
|
LeaderTransitions int `json:"leaderTransitions"`
|
||||||
|
Strategy v1.CoordinatedLeaseStrategy `json:"strategy"`
|
||||||
|
PreferredHolder string `json:"preferredHolder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventRecorder records a change in the ResourceLock.
|
// EventRecorder records a change in the ResourceLock.
|
||||||
|
@ -122,6 +122,12 @@ func LeaseSpecToLeaderElectionRecord(spec *coordinationv1.LeaseSpec) *LeaderElec
|
|||||||
if spec.RenewTime != nil {
|
if spec.RenewTime != nil {
|
||||||
r.RenewTime = metav1.Time{Time: spec.RenewTime.Time}
|
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
|
return &r
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -129,11 +135,18 @@ func LeaseSpecToLeaderElectionRecord(spec *coordinationv1.LeaseSpec) *LeaderElec
|
|||||||
func LeaderElectionRecordToLeaseSpec(ler *LeaderElectionRecord) coordinationv1.LeaseSpec {
|
func LeaderElectionRecordToLeaseSpec(ler *LeaderElectionRecord) coordinationv1.LeaseSpec {
|
||||||
leaseDurationSeconds := int32(ler.LeaseDurationSeconds)
|
leaseDurationSeconds := int32(ler.LeaseDurationSeconds)
|
||||||
leaseTransitions := int32(ler.LeaderTransitions)
|
leaseTransitions := int32(ler.LeaderTransitions)
|
||||||
return coordinationv1.LeaseSpec{
|
spec := coordinationv1.LeaseSpec{
|
||||||
HolderIdentity: &ler.HolderIdentity,
|
HolderIdentity: &ler.HolderIdentity,
|
||||||
LeaseDurationSeconds: &leaseDurationSeconds,
|
LeaseDurationSeconds: &leaseDurationSeconds,
|
||||||
AcquireTime: &metav1.MicroTime{Time: ler.AcquireTime.Time},
|
AcquireTime: &metav1.MicroTime{Time: ler.AcquireTime.Time},
|
||||||
RenewTime: &metav1.MicroTime{Time: ler.RenewTime.Time},
|
RenewTime: &metav1.MicroTime{Time: ler.RenewTime.Time},
|
||||||
LeaseTransitions: &leaseTransitions,
|
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