github.com/openebs/api@v1.12.0/pkg/kubernetes/leaderelection/leader_election.go (about)

     1  /*
     2  Copyright 2020 The OpenEBS Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package leaderelection
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"regexp"
    25  	"strings"
    26  	"time"
    27  
    28  	v1 "k8s.io/api/core/v1"
    29  	"k8s.io/client-go/kubernetes"
    30  	"k8s.io/client-go/kubernetes/scheme"
    31  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    32  	"k8s.io/client-go/tools/leaderelection"
    33  	"k8s.io/client-go/tools/leaderelection/resourcelock"
    34  	"k8s.io/client-go/tools/record"
    35  	"k8s.io/klog"
    36  )
    37  
    38  const (
    39  	openEBS              = "openebs"
    40  	defaultLeaseDuration = 15 * time.Second
    41  	defaultRenewDeadline = 10 * time.Second
    42  	defaultRetryPeriod   = 5 * time.Second
    43  )
    44  
    45  // LeaderElection is a convenience wrapper around client-go's leader election library.
    46  type LeaderElection struct {
    47  	runFunc func(ctx context.Context)
    48  
    49  	// the lockName identifies the leader election config and should be shared across all members
    50  	lockName string
    51  	// the identity is the unique identity of the currently running member
    52  	identity string
    53  	// the namespace to store the lock resource
    54  	namespace string
    55  	// resourceLock defines the type of leaderelection that should be used
    56  	// valid options are resourcelock.LeasesResourceLock, resourcelock.EndpointsResourceLock,
    57  	// and resourcelock.ConfigMapsResourceLock
    58  	resourceLock string
    59  
    60  	leaseDuration time.Duration
    61  	renewDeadline time.Duration
    62  	retryPeriod   time.Duration
    63  
    64  	clientset kubernetes.Interface
    65  }
    66  
    67  // NewLeaderElection returns the default & preferred leader election type
    68  func NewLeaderElection(clientset kubernetes.Interface, lockName string, runFunc func(ctx context.Context)) *LeaderElection {
    69  	return NewLeaderElectionWithLeases(clientset, lockName, runFunc)
    70  }
    71  
    72  // NewLeaderElectionWithLeases returns an implementation of leader election using Leases
    73  func NewLeaderElectionWithLeases(clientset kubernetes.Interface, lockName string, runFunc func(ctx context.Context)) *LeaderElection {
    74  	return &LeaderElection{
    75  		runFunc:       runFunc,
    76  		lockName:      lockName,
    77  		resourceLock:  resourcelock.LeasesResourceLock,
    78  		leaseDuration: defaultLeaseDuration,
    79  		renewDeadline: defaultRenewDeadline,
    80  		retryPeriod:   defaultRetryPeriod,
    81  		clientset:     clientset,
    82  	}
    83  }
    84  
    85  // NewLeaderElectionWithEndpoints returns an implementation of leader election using Endpoints
    86  func NewLeaderElectionWithEndpoints(clientset kubernetes.Interface, lockName string, runFunc func(ctx context.Context)) *LeaderElection {
    87  	return &LeaderElection{
    88  		runFunc:       runFunc,
    89  		lockName:      lockName,
    90  		resourceLock:  resourcelock.EndpointsResourceLock,
    91  		leaseDuration: defaultLeaseDuration,
    92  		renewDeadline: defaultRenewDeadline,
    93  		retryPeriod:   defaultRetryPeriod,
    94  		clientset:     clientset,
    95  	}
    96  }
    97  
    98  // NewLeaderElectionWithConfigMaps returns an implementation of leader election using ConfigMaps
    99  func NewLeaderElectionWithConfigMaps(clientset kubernetes.Interface, lockName string, runFunc func(ctx context.Context)) *LeaderElection {
   100  	return &LeaderElection{
   101  		runFunc:       runFunc,
   102  		lockName:      lockName,
   103  		resourceLock:  resourcelock.ConfigMapsResourceLock,
   104  		leaseDuration: defaultLeaseDuration,
   105  		renewDeadline: defaultRenewDeadline,
   106  		retryPeriod:   defaultRetryPeriod,
   107  		clientset:     clientset,
   108  	}
   109  }
   110  
   111  // WithIdentity ...
   112  func (l *LeaderElection) WithIdentity(identity string) {
   113  	l.identity = identity
   114  }
   115  
   116  // WithNamespace ...
   117  func (l *LeaderElection) WithNamespace(namespace string) {
   118  	l.namespace = namespace
   119  }
   120  
   121  // WithLeaseDuration ...
   122  func (l *LeaderElection) WithLeaseDuration(leaseDuration time.Duration) {
   123  	l.leaseDuration = leaseDuration
   124  }
   125  
   126  // WithRenewDeadline ...
   127  func (l *LeaderElection) WithRenewDeadline(renewDeadline time.Duration) {
   128  	l.renewDeadline = renewDeadline
   129  }
   130  
   131  // WithRetryPeriod ...
   132  func (l *LeaderElection) WithRetryPeriod(retryPeriod time.Duration) {
   133  	l.retryPeriod = retryPeriod
   134  }
   135  
   136  // Run starts the leader loop
   137  func (l *LeaderElection) Run() error {
   138  	if l.identity == "" {
   139  		id, err := defaultLeaderElectionIdentity()
   140  		if err != nil {
   141  			return fmt.Errorf("error getting the default leader identity: %v", err)
   142  		}
   143  
   144  		l.identity = id
   145  	}
   146  
   147  	if l.namespace == "" {
   148  		l.namespace = inClusterNamespace()
   149  	}
   150  
   151  	broadcaster := record.NewBroadcaster()
   152  	broadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: l.clientset.CoreV1().Events(l.namespace)})
   153  	eventRecorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: fmt.Sprintf("%s/%s", l.lockName, string(l.identity))})
   154  
   155  	rlConfig := resourcelock.ResourceLockConfig{
   156  		Identity:      sanitizeName(l.identity),
   157  		EventRecorder: eventRecorder,
   158  	}
   159  
   160  	lock, err := resourcelock.New(l.resourceLock, l.namespace, sanitizeName(l.lockName), l.clientset.CoreV1(), l.clientset.CoordinationV1(), rlConfig)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	leaderConfig := leaderelection.LeaderElectionConfig{
   166  		Lock:          lock,
   167  		LeaseDuration: l.leaseDuration,
   168  		RenewDeadline: l.renewDeadline,
   169  		RetryPeriod:   l.retryPeriod,
   170  		Callbacks: leaderelection.LeaderCallbacks{
   171  			OnStartedLeading: func(ctx context.Context) {
   172  				klog.V(2).Info("became leader, starting")
   173  				l.runFunc(ctx)
   174  			},
   175  			OnStoppedLeading: func() {
   176  				klog.Fatal("stopped leading")
   177  			},
   178  			OnNewLeader: func(identity string) {
   179  				klog.V(3).Infof("new leader detected, current leader: %s", identity)
   180  			},
   181  		},
   182  	}
   183  	// RunOrDie starts a client with the provided config or panics if the
   184  	// leaderconfig fails to validate.
   185  	leaderelection.RunOrDie(context.TODO(), leaderConfig)
   186  	return nil // should never reach here
   187  }
   188  
   189  func defaultLeaderElectionIdentity() (string, error) {
   190  	return os.Hostname()
   191  }
   192  
   193  // sanitizeName sanitizes the provided string so it can be consumed by leader election library
   194  func sanitizeName(name string) string {
   195  	re := regexp.MustCompile("[^a-zA-Z0-9-]")
   196  	name = re.ReplaceAllString(name, "-")
   197  	if name[len(name)-1] == '-' {
   198  		// name must not end with '-'
   199  		name = name + "X"
   200  	}
   201  	return name
   202  }
   203  
   204  // inClusterNamespace returns the namespace in which the pod is running in by checking
   205  // the env var POD_NAMESPACE, then the file /var/run/secrets/kubernetes.io/serviceaccount/namespace.
   206  // if neither returns a valid namespace, the "openebs" namespace is returned
   207  func inClusterNamespace() string {
   208  	if ns := os.Getenv("POD_NAMESPACE"); ns != "" {
   209  		return ns
   210  	}
   211  
   212  	if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
   213  		if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
   214  			return ns
   215  		}
   216  	}
   217  	return openEBS
   218  }