Skip to content

Commit dca3fec

Browse files
authored
Leader remove runlocal (#7)
* Remove runlocal bits from leader election * Add ginkgo tests for leader election * Export ErrNoNamespace * Added options to Become to make it more testable * unexport setDefaults * Ensure Become will return ErrNoNamespace
1 parent 525cb9a commit dca3fec

File tree

3 files changed

+262
-44
lines changed

3 files changed

+262
-44
lines changed

leader/leader.go

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -31,72 +31,89 @@ import (
3131
logf "sigs.k8s.io/controller-runtime/pkg/log"
3232
)
3333

34-
type runModeType string
35-
36-
const (
37-
localRunMode runModeType = "local"
38-
)
39-
40-
// forceRunModeEnv indicates if the operator should be forced to run in either local
41-
// or cluster mode (currently only used for local mode)
42-
var forceRunModeEnv = "OSDK_FORCE_RUN_MODE"
43-
44-
// errNoNamespace indicates that a namespace could not be found for the current
34+
// ErrNoNamespace indicates that a namespace could not be found for the current
4535
// environment
46-
var errNoNamespace = fmt.Errorf("namespace not found for current environment")
47-
48-
// errRunLocal indicates that the operator is set to run in local mode (this error
49-
// is returned by functions that only work on operators running in cluster mode)
50-
var errRunLocal = fmt.Errorf("operator run mode forced to local")
36+
var ErrNoNamespace = fmt.Errorf("namespace not found for current environment")
5137

5238
// podNameEnvVar is the constant for env variable POD_NAME
5339
// which is the name of the current pod.
5440
const podNameEnvVar = "POD_NAME"
5541

42+
var readNamespace = func() ([]byte, error) {
43+
return ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
44+
}
45+
5646
var log = logf.Log.WithName("leader")
5747

5848
// maxBackoffInterval defines the maximum amount of time to wait between
5949
// attempts to become the leader.
6050
const maxBackoffInterval = time.Second * 16
6151

52+
type Option func(*Config) error
53+
54+
type Config struct {
55+
Client crclient.Client
56+
}
57+
58+
func (c *Config) setDefaults() error {
59+
if c.Client == nil {
60+
config, err := config.GetConfig()
61+
if err != nil {
62+
return err
63+
}
64+
65+
client, err := crclient.New(config, crclient.Options{})
66+
if err != nil {
67+
return err
68+
}
69+
c.Client = client
70+
}
71+
return nil
72+
}
73+
74+
func WithClient(cl crclient.Client) Option {
75+
return func(c *Config) error {
76+
c.Client = cl
77+
return nil
78+
}
79+
}
80+
6281
// Become ensures that the current pod is the leader within its namespace. If
6382
// run outside a cluster, it will skip leader election and return nil. It
6483
// continuously tries to create a ConfigMap with the provided name and the
6584
// current pod set as the owner reference. Only one can exist at a time with
6685
// the same name, so the pod that successfully creates the ConfigMap is the
6786
// leader. Upon termination of that pod, the garbage collector will delete the
6887
// ConfigMap, enabling a different pod to become the leader.
69-
func Become(ctx context.Context, lockName string) error {
88+
func Become(ctx context.Context, lockName string, opts ...Option) error {
7089
log.Info("Trying to become the leader.")
7190

72-
ns, err := getOperatorNamespace()
73-
if err != nil {
74-
if err == errNoNamespace || err == errRunLocal {
75-
log.Info("Skipping leader election; not running in a cluster.")
76-
return nil
91+
config := Config{}
92+
93+
for _, opt := range opts {
94+
if err := opt(&config); err != nil {
95+
return err
7796
}
78-
return err
7997
}
8098

81-
config, err := config.GetConfig()
82-
if err != nil {
99+
if err := config.setDefaults(); err != nil {
83100
return err
84101
}
85102

86-
client, err := crclient.New(config, crclient.Options{})
103+
ns, err := getOperatorNamespace()
87104
if err != nil {
88105
return err
89106
}
90107

91-
owner, err := myOwnerRef(ctx, client, ns)
108+
owner, err := myOwnerRef(ctx, config.Client, ns)
92109
if err != nil {
93110
return err
94111
}
95112

96113
// check for existing lock from this pod, in case we got restarted
97114
existing := &corev1.ConfigMap{}
98115
key := crclient.ObjectKey{Namespace: ns, Name: lockName}
99-
err = client.Get(ctx, key, existing)
116+
err = config.Client.Get(ctx, key, existing)
100117

101118
switch {
102119
case err == nil:
@@ -126,15 +143,15 @@ func Become(ctx context.Context, lockName string) error {
126143
// try to create a lock
127144
backoff := time.Second
128145
for {
129-
err := client.Create(ctx, cm)
146+
err := config.Client.Create(ctx, cm)
130147
switch {
131148
case err == nil:
132149
log.Info("Became the leader.")
133150
return nil
134151
case apierrors.IsAlreadyExists(err):
135152
// refresh the lock so we use current leader
136153
key := crclient.ObjectKey{Namespace: ns, Name: lockName}
137-
if err := client.Get(ctx, key, existing); err != nil {
154+
if err := config.Client.Get(ctx, key, existing); err != nil {
138155
log.Info("Leader lock configmap not found.")
139156
continue // configmap got lost ... just wait a bit
140157
}
@@ -148,7 +165,7 @@ func Become(ctx context.Context, lockName string) error {
148165
default:
149166
leaderPod := &corev1.Pod{}
150167
key = crclient.ObjectKey{Namespace: ns, Name: existingOwners[0].Name}
151-
err = client.Get(ctx, key, leaderPod)
168+
err = config.Client.Get(ctx, key, leaderPod)
152169
switch {
153170
case apierrors.IsNotFound(err):
154171
log.Info("Leader pod has been deleted, waiting for garbage collection to remove the lock.")
@@ -158,7 +175,7 @@ func Become(ctx context.Context, lockName string) error {
158175
log.Info("Operator pod with leader lock has been evicted.", "leader", leaderPod.Name)
159176
log.Info("Deleting evicted leader.")
160177
// Pod may not delete immediately, continue with backoff
161-
err := client.Delete(ctx, leaderPod)
178+
err := config.Client.Delete(ctx, leaderPod)
162179
if err != nil {
163180
log.Error(err, "Leader pod could not be deleted.")
164181
}
@@ -210,13 +227,10 @@ func isPodEvicted(pod corev1.Pod) bool {
210227

211228
// getOperatorNamespace returns the namespace the operator should be running in.
212229
func getOperatorNamespace() (string, error) {
213-
if isRunModeLocal() {
214-
return "", errRunLocal
215-
}
216-
nsBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
230+
nsBytes, err := readNamespace()
217231
if err != nil {
218232
if os.IsNotExist(err) {
219-
return "", errNoNamespace
233+
return "", ErrNoNamespace
220234
}
221235
return "", err
222236
}
@@ -225,17 +239,10 @@ func getOperatorNamespace() (string, error) {
225239
return ns, nil
226240
}
227241

228-
func isRunModeLocal() bool {
229-
return os.Getenv(forceRunModeEnv) == string(localRunMode)
230-
}
231-
232242
// getPod returns a Pod object that corresponds to the pod in which the code
233243
// is currently running.
234244
// It expects the environment variable POD_NAME to be set by the downwards API.
235245
func getPod(ctx context.Context, client crclient.Client, ns string) (*corev1.Pod, error) {
236-
if isRunModeLocal() {
237-
return nil, errRunLocal
238-
}
239246
podName := os.Getenv(podNameEnvVar)
240247
if podName == "" {
241248
return nil, fmt.Errorf("required env %s not set, please configure downward API", podNameEnvVar)

leader/leader_suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package leader
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestLeader(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Leader Suite")
13+
}

0 commit comments

Comments
 (0)