Skip to content

Commit 50791ad

Browse files
authoredOct 10, 2022
Merge pull request #341 from avestuk/leadership-election
Leadership election
2 parents e0fcc3b + 1c71908 commit 50791ad

File tree

13 files changed

+480
-12
lines changed

13 files changed

+480
-12
lines changed
 

‎README.md

+8
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ You can enable to scrape Reloader's Prometheus metrics by setting `serviceMonito
245245
| isArgoRollouts | Enable Argo Rollouts. Valid value are either `true` or `false` | boolean |
246246
| reloadOnCreate | Enable reload on create events. Valid value are either `true` or `false` | boolean |
247247

248+
**ReloadOnCreate** reloadOnCreate controls how Reloader handles secrets being added to the cache for the first time. If reloadOnCreate is set to true:
249+
* Configmaps/secrets being added to the cache will cause Reloader to perform a rolling update of the associated workload.
250+
* When applications are deployed for the first time, Reloader will perform a rolling update of the associated workload.
251+
* If you are running Reloader in HA mode all workloads will have a rolling update performed when a new leader is elected.
252+
253+
If ReloadOnCreate is set to false:
254+
* Updates to configMaps/Secrets that occur while there is no leader will not be picked up by the new leader until a subsequent update of the configmap/secret occurs. In the worst case the window in which there can be no leader is 15s as this is the LeaseDuration.
255+
248256
## Help
249257

250258
### Documentation

‎deployments/kubernetes/chart/reloader/templates/_helpers.tpl

+18-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ heritage: {{ .Release.Service | quote }}
2828
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
2929
{{- end -}}
3030

31+
{{/*
32+
Create pod anti affinity labels
33+
*/}}
34+
{{- define "reloader-podAntiAffinity" -}}
35+
podAntiAffinity:
36+
preferredDuringSchedulingIgnoredDuringExecution:
37+
- weight: 100
38+
podAffinityTerm:
39+
labelSelector:
40+
matchExpressions:
41+
- key: app
42+
operator: In
43+
values:
44+
- {{ template "reloader-fullname" . }}
45+
topologyKey: "kubernetes.io/hostname"
46+
{{- end -}}
47+
3148
{{/*
3249
Create the name of the service account to use
3350
*/}}
@@ -45,4 +62,4 @@ Create the annotations to support helm3
4562
{{- define "reloader-helm3.annotations" -}}
4663
meta.helm.sh/release-namespace: {{ .Release.Namespace | quote }}
4764
meta.helm.sh/release-name: {{ .Release.Name | quote }}
48-
{{- end -}}
65+
{{- end -}}

‎deployments/kubernetes/chart/reloader/templates/clusterrole.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ rules:
7777
- get
7878
- update
7979
- patch
80+
{{- if .Values.reloader.enableHA }}
81+
- apiGroups:
82+
- "coordination.k8s.io"
83+
resources:
84+
- leases
85+
verbs:
86+
- create
87+
- get
88+
- update
89+
{{- end}}
8090
- apiGroups:
8191
- ""
8292
resources:

‎deployments/kubernetes/chart/reloader/templates/deployment.yaml

+27-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ metadata:
1717
name: {{ template "reloader-fullname" . }}
1818
namespace: {{ .Release.Namespace }}
1919
spec:
20+
{{- if not (.Values.reloader.enableHA) }}
21+
replicas: 1
22+
{{- else }}
2023
replicas: {{ .Values.reloader.deployment.replicas }}
24+
{{- end}}
2125
revisionHistoryLimit: 2
2226
selector:
2327
matchLabels:
@@ -45,9 +49,12 @@ spec:
4549
nodeSelector:
4650
{{ toYaml .Values.reloader.deployment.nodeSelector | indent 8 }}
4751
{{- end }}
48-
{{- if .Values.reloader.deployment.affinity }}
52+
{{- if or (.Values.reloader.deployment.affinity) (.Values.reloader.enableHA) }}
4953
affinity:
54+
{{- if .Values.reloader.deployment.affinity }}
5055
{{ toYaml .Values.reloader.deployment.affinity | indent 8 }}
56+
{{- end}}
57+
{{ include "reloader-podAntiAffinity" . | indent 8 }}
5158
{{- end }}
5259
{{- if .Values.reloader.deployment.tolerations }}
5360
tolerations:
@@ -60,7 +67,7 @@ spec:
6067
- image: "{{ .Values.reloader.deployment.image.name }}:{{ .Values.reloader.deployment.image.tag }}"
6168
imagePullPolicy: {{ .Values.reloader.deployment.image.pullPolicy }}
6269
name: {{ template "reloader-fullname" . }}
63-
{{- if or (.Values.reloader.deployment.env.open) (.Values.reloader.deployment.env.secret) (.Values.reloader.deployment.env.field) (eq .Values.reloader.watchGlobally false) }}
70+
{{- if or (.Values.reloader.deployment.env.open) (.Values.reloader.deployment.env.secret) (.Values.reloader.deployment.env.field) (eq .Values.reloader.watchGlobally false) (.Values.reloader.enableHA)}}
6471
env:
6572
{{- range $name, $value := .Values.reloader.deployment.env.open }}
6673
{{- if not (empty $value) }}
@@ -92,14 +99,26 @@ spec:
9299
fieldRef:
93100
fieldPath: metadata.namespace
94101
{{- end }}
102+
{{- if .Values.reloader.enableHA }}
103+
- name: POD_NAME
104+
valueFrom:
105+
fieldRef:
106+
fieldPath: metadata.name
107+
- name: POD_NAMESPACE
108+
valueFrom:
109+
fieldRef:
110+
fieldPath: metadata.namespace
111+
{{- end }}
95112
{{- end }}
96113

97114
ports:
98115
- name: http
116+
containerPort: 9091
117+
- name: metrics
99118
containerPort: 9090
100119
livenessProbe:
101120
httpGet:
102-
path: /metrics
121+
path: /live
103122
port: http
104123
timeoutSeconds: {{ .Values.reloader.deployment.livenessProbe.timeoutSeconds | default "5" }}
105124
failureThreshold: {{ .Values.reloader.deployment.livenessProbe.failureThreshold | default "5" }}
@@ -108,7 +127,7 @@ spec:
108127
readinessProbe:
109128
httpGet:
110129
path: /metrics
111-
port: http
130+
port: metrics
112131
timeoutSeconds: {{ .Values.reloader.deployment.readinessProbe.timeoutSeconds | default "5" }}
113132
failureThreshold: {{ .Values.reloader.deployment.readinessProbe.failureThreshold | default "5" }}
114133
periodSeconds: {{ .Values.reloader.deployment.readinessProbe.periodSeconds | default "10" }}
@@ -123,7 +142,7 @@ spec:
123142
- mountPath: /tmp/
124143
name: tmp-volume
125144
{{- end }}
126-
{{- if or (.Values.reloader.logFormat) (.Values.reloader.ignoreSecrets) (.Values.reloader.ignoreNamespaces) (.Values.reloader.ignoreConfigMaps) (.Values.reloader.custom_annotations) (eq .Values.reloader.isArgoRollouts true) (eq .Values.reloader.reloadOnCreate true) (ne .Values.reloader.reloadStrategy "default")}}
145+
{{- if or (.Values.reloader.logFormat) (.Values.reloader.ignoreSecrets) (.Values.reloader.ignoreNamespaces) (.Values.reloader.ignoreConfigMaps) (.Values.reloader.custom_annotations) (eq .Values.reloader.isArgoRollouts true) (eq .Values.reloader.reloadOnCreate true) (ne .Values.reloader.reloadStrategy "default") (.Values.reloader.enableHA)}}
127146
args:
128147
{{- if .Values.reloader.logFormat }}
129148
- "--log-format={{ .Values.reloader.logFormat }}"
@@ -169,6 +188,9 @@ spec:
169188
{{- if ne .Values.reloader.reloadStrategy "default" }}
170189
- "--reload-strategy={{ .Values.reloader.reloadStrategy }}"
171190
{{- end }}
191+
{{- if or (gt .Values.reloader.deployment.replicas 1.0) (.Values.reloader.enableHA) }}
192+
- "--enable-ha=true"
193+
{{- end}}
172194
{{- end }}
173195
{{- if .Values.reloader.deployment.resources }}
174196
resources:

‎deployments/kubernetes/chart/reloader/templates/role.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ rules:
7777
- get
7878
- update
7979
- patch
80+
{{- if .Values.reloader.enableHA }}
81+
- apiGroups:
82+
- "coordination.k8s.io"
83+
resources:
84+
- leases
85+
verbs:
86+
- create
87+
- get
88+
- update
89+
{{- end}}
8090
- apiGroups:
8191
- ""
8292
resources:

‎deployments/kubernetes/chart/reloader/values.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ reloader:
1818
ignoreNamespaces: "" # Comma separated list of namespaces to ignore
1919
logFormat: "" #json
2020
watchGlobally: true
21+
# Set to true to enable leadership election allowing you to run multiple replicas
22+
enableHA: false
2123
# Set to true if you have a pod security policy that enforces readOnlyRootFilesystem
2224
readOnlyRootFileSystem: false
2325
legacy:
2426
rbac: false
2527
matchLabels: {}
2628
deployment:
29+
# If you wish to run multiple replicas set reloader.enableHA = true
2730
replicas: 1
2831
nodeSelector:
2932
# cloud.google.com/gke-nodepool: default-pool

‎internal/pkg/cmd/reloader.go

+56-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package cmd
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
6-
"github.com/stakater/Reloader/internal/pkg/constants"
77
"os"
88
"strings"
99

10+
"github.com/stakater/Reloader/internal/pkg/constants"
11+
"github.com/stakater/Reloader/internal/pkg/leadership"
12+
1013
"github.com/sirupsen/logrus"
1114
"github.com/spf13/cobra"
1215
"github.com/stakater/Reloader/internal/pkg/controller"
@@ -38,21 +41,34 @@ func NewReloaderCommand() *cobra.Command {
3841
cmd.PersistentFlags().StringVar(&options.IsArgoRollouts, "is-Argo-Rollouts", "false", "Add support for argo rollouts")
3942
cmd.PersistentFlags().StringVar(&options.ReloadStrategy, constants.ReloadStrategyFlag, constants.EnvVarsReloadStrategy, "Specifies the desired reload strategy")
4043
cmd.PersistentFlags().StringVar(&options.ReloadOnCreate, "reload-on-create", "false", "Add support to watch create events")
44+
cmd.PersistentFlags().BoolVar(&options.EnableHA, "enable-ha", false, "Adds support for running multiple replicas via leadership election")
4145

4246
return cmd
4347
}
4448

4549
func validateFlags(*cobra.Command, []string) error {
4650
// Ensure the reload strategy is one of the following...
51+
var validReloadStrategy bool
4752
valid := []string{constants.EnvVarsReloadStrategy, constants.AnnotationsReloadStrategy}
4853
for _, s := range valid {
4954
if s == options.ReloadStrategy {
50-
return nil
55+
validReloadStrategy = true
56+
}
57+
}
58+
59+
if !validReloadStrategy {
60+
err := fmt.Sprintf("%s must be one of: %s", constants.ReloadStrategyFlag, strings.Join(valid, ", "))
61+
return errors.New(err)
62+
}
63+
64+
// Validate that HA options are correct
65+
if options.EnableHA {
66+
if err := validateHAEnvs(); err != nil {
67+
return err
5168
}
5269
}
5370

54-
err := fmt.Sprintf("%s must be one of: %s", constants.ReloadStrategyFlag, strings.Join(valid, ", "))
55-
return errors.New(err)
71+
return nil
5672
}
5773

5874
func configureLogging(logFormat string) error {
@@ -68,6 +84,25 @@ func configureLogging(logFormat string) error {
6884
return nil
6985
}
7086

87+
func validateHAEnvs() error {
88+
podName, podNamespace := getHAEnvs()
89+
90+
if podName == "" {
91+
return fmt.Errorf("%s not set, cannot run in HA mode without %s set", constants.PodNameEnv, constants.PodNameEnv)
92+
}
93+
if podNamespace == "" {
94+
return fmt.Errorf("%s not set, cannot run in HA mode without %s set", constants.PodNamespaceEnv, constants.PodNamespaceEnv)
95+
}
96+
return nil
97+
}
98+
99+
func getHAEnvs() (string, string) {
100+
podName := os.Getenv(constants.PodNameEnv)
101+
podNamespace := os.Getenv(constants.PodNamespaceEnv)
102+
103+
return podName, podNamespace
104+
}
105+
71106
func startReloader(cmd *cobra.Command, args []string) {
72107
err := configureLogging(options.LogFormat)
73108
if err != nil {
@@ -99,6 +134,7 @@ func startReloader(cmd *cobra.Command, args []string) {
99134

100135
collectors := metrics.SetupPrometheusEndpoint()
101136

137+
var controllers []*controller.Controller
102138
for k := range kube.ResourceMap {
103139
if ignoredResourcesList.Contains(k) {
104140
continue
@@ -109,15 +145,29 @@ func startReloader(cmd *cobra.Command, args []string) {
109145
logrus.Fatalf("%s", err)
110146
}
111147

148+
controllers = append(controllers, c)
149+
150+
// If HA is enabled we only run the controller when
151+
if options.EnableHA {
152+
continue
153+
}
112154
// Now let's start the controller
113155
stop := make(chan struct{})
114156
defer close(stop)
115157
logrus.Infof("Starting Controller to watch resource type: %s", k)
116158
go c.Run(1, stop)
117159
}
118160

119-
// Wait forever
120-
select {}
161+
// Run leadership election
162+
if options.EnableHA {
163+
podName, podNamespace := getHAEnvs()
164+
lock := leadership.GetNewLock(clientset.CoordinationV1(), constants.LockName, podName, podNamespace)
165+
ctx, cancel := context.WithCancel(context.Background())
166+
defer cancel()
167+
go leadership.RunLeaderElection(lock, ctx, cancel, podName, controllers)
168+
}
169+
170+
logrus.Fatal(leadership.Healthz())
121171
}
122172

123173
func getIgnoredNamespacesList(cmd *cobra.Command) (util.List, error) {

‎internal/pkg/constants/constants.go

+7
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,10 @@ const (
2020
// AnnotationsReloadStrategy instructs Reloader to add pod template annotations to facilitate a restart
2121
AnnotationsReloadStrategy = "annotations"
2222
)
23+
24+
// Leadership election related consts
25+
const (
26+
LockName string = "stakaer-reloader-lock"
27+
PodNameEnv string = "POD_NAME"
28+
PodNamespaceEnv string = "POD_NAMESPACE"
29+
)

‎internal/pkg/controller/controller.go

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ func NewController(
6565
c.queue = queue
6666
c.collectors = collectors
6767
c.recorder = recorder
68+
69+
logrus.Infof("created controller for: %s", resource)
6870
return &c, nil
6971
}
7072

‎internal/pkg/leadership/leadership.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package leadership
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"sync"
7+
"time"
8+
9+
"github.com/sirupsen/logrus"
10+
"github.com/stakater/Reloader/internal/pkg/controller"
11+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/client-go/tools/leaderelection"
13+
"k8s.io/client-go/tools/leaderelection/resourcelock"
14+
15+
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
16+
)
17+
18+
const healthPort string = ":9091"
19+
20+
var (
21+
// Used for liveness probe
22+
m sync.Mutex
23+
healthy bool = true
24+
)
25+
26+
func GetNewLock(client coordinationv1.CoordinationV1Interface, lockName, podname, namespace string) *resourcelock.LeaseLock {
27+
return &resourcelock.LeaseLock{
28+
LeaseMeta: v1.ObjectMeta{
29+
Name: lockName,
30+
Namespace: namespace,
31+
},
32+
Client: client,
33+
LockConfig: resourcelock.ResourceLockConfig{
34+
Identity: podname,
35+
},
36+
}
37+
}
38+
39+
// runLeaderElection runs leadership election. If an instance of the controller is the leader and stops leading it will shutdown.
40+
func RunLeaderElection(lock *resourcelock.LeaseLock, ctx context.Context, cancel context.CancelFunc, id string, controllers []*controller.Controller) {
41+
// Construct channels for the controllers to use
42+
var stopChannels []chan struct{}
43+
for i := 0; i < len(controllers); i++ {
44+
stop := make(chan struct{})
45+
stopChannels = append(stopChannels, stop)
46+
}
47+
48+
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
49+
Lock: lock,
50+
ReleaseOnCancel: true,
51+
LeaseDuration: 15 * time.Second,
52+
RenewDeadline: 10 * time.Second,
53+
RetryPeriod: 2 * time.Second,
54+
Callbacks: leaderelection.LeaderCallbacks{
55+
OnStartedLeading: func(c context.Context) {
56+
logrus.Info("became leader, starting controllers")
57+
runControllers(controllers, stopChannels)
58+
},
59+
OnStoppedLeading: func() {
60+
logrus.Info("no longer leader, shutting down")
61+
stopControllers(stopChannels)
62+
cancel()
63+
m.Lock()
64+
defer m.Unlock()
65+
healthy = false
66+
},
67+
OnNewLeader: func(current_id string) {
68+
if current_id == id {
69+
logrus.Info("still the leader!")
70+
return
71+
}
72+
logrus.Infof("new leader is %s", current_id)
73+
},
74+
},
75+
})
76+
}
77+
78+
func runControllers(controllers []*controller.Controller, stopChannels []chan struct{}) {
79+
for i, c := range controllers {
80+
c := c
81+
go c.Run(1, stopChannels[i])
82+
}
83+
}
84+
85+
func stopControllers(stopChannels []chan struct{}) {
86+
for _, c := range stopChannels {
87+
close(c)
88+
}
89+
}
90+
91+
// Healthz serves the liveness probe endpoint. If leadership election is
92+
// enabled and a replica stops leading the liveness probe will fail and the
93+
// kubelet will restart the container.
94+
func Healthz() error {
95+
http.HandleFunc("/live", healthz)
96+
return http.ListenAndServe(healthPort, nil)
97+
}
98+
99+
func healthz(w http.ResponseWriter, req *http.Request) {
100+
m.Lock()
101+
defer m.Unlock()
102+
if healthy {
103+
if i, err := w.Write([]byte("alive")); err != nil {
104+
logrus.Infof("failed to write liveness response, wrote: %d bytes, got err: %s", i, err)
105+
}
106+
return
107+
}
108+
109+
w.WriteHeader(http.StatusInternalServerError)
110+
}
+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package leadership
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"os"
9+
"testing"
10+
"time"
11+
12+
"github.com/sirupsen/logrus"
13+
"github.com/stakater/Reloader/internal/pkg/constants"
14+
"github.com/stakater/Reloader/internal/pkg/controller"
15+
"github.com/stakater/Reloader/internal/pkg/handler"
16+
"github.com/stakater/Reloader/internal/pkg/metrics"
17+
"github.com/stakater/Reloader/internal/pkg/options"
18+
"github.com/stakater/Reloader/internal/pkg/testutil"
19+
"github.com/stakater/Reloader/internal/pkg/util"
20+
"github.com/stakater/Reloader/pkg/kube"
21+
)
22+
23+
func TestMain(m *testing.M) {
24+
25+
testutil.CreateNamespace(testutil.Namespace, testutil.Clients.KubernetesClient)
26+
27+
logrus.Infof("Running Testcases")
28+
retCode := m.Run()
29+
30+
testutil.DeleteNamespace(testutil.Namespace, testutil.Clients.KubernetesClient)
31+
32+
os.Exit(retCode)
33+
}
34+
35+
func TestHealthz(t *testing.T) {
36+
request, err := http.NewRequest(http.MethodGet, "/live", nil)
37+
if err != nil {
38+
t.Fatalf(("failed to create request"))
39+
}
40+
41+
response := httptest.NewRecorder()
42+
43+
healthz(response, request)
44+
got := response.Code
45+
want := 200
46+
47+
if got != want {
48+
t.Fatalf("got: %q, want: %q", got, want)
49+
}
50+
51+
// Have the liveness probe serve a 500
52+
healthy = false
53+
54+
request, err = http.NewRequest(http.MethodGet, "/live", nil)
55+
if err != nil {
56+
t.Fatalf(("failed to create request"))
57+
}
58+
59+
response = httptest.NewRecorder()
60+
61+
healthz(response, request)
62+
got = response.Code
63+
want = 500
64+
65+
if got != want {
66+
t.Fatalf("got: %q, want: %q", got, want)
67+
}
68+
}
69+
70+
// TestRunLeaderElection validates that the liveness endpoint serves 500 when
71+
// leadership election fails
72+
func TestRunLeaderElection(t *testing.T) {
73+
ctx, cancel := context.WithCancel(context.TODO())
74+
75+
lock := GetNewLock(testutil.Clients.KubernetesClient.CoordinationV1(), constants.LockName, testutil.Pod, testutil.Namespace)
76+
77+
go RunLeaderElection(lock, ctx, cancel, testutil.Pod, []*controller.Controller{})
78+
79+
// Liveness probe should be serving OK
80+
request, err := http.NewRequest(http.MethodGet, "/live", nil)
81+
if err != nil {
82+
t.Fatalf(("failed to create request"))
83+
}
84+
85+
response := httptest.NewRecorder()
86+
87+
healthz(response, request)
88+
got := response.Code
89+
want := 500
90+
91+
if got != want {
92+
t.Fatalf("got: %q, want: %q", got, want)
93+
}
94+
95+
// Cancel the leader election context, so leadership is released and
96+
// live endpoint serves 500
97+
cancel()
98+
99+
request, err = http.NewRequest(http.MethodGet, "/live", nil)
100+
if err != nil {
101+
t.Fatalf(("failed to create request"))
102+
}
103+
104+
response = httptest.NewRecorder()
105+
106+
healthz(response, request)
107+
got = response.Code
108+
want = 500
109+
110+
if got != want {
111+
t.Fatalf("got: %q, want: %q", got, want)
112+
}
113+
}
114+
115+
// TestRunLeaderElectionWithControllers tests that leadership election works
116+
// wiht real controllers and that on context cancellation the controllers stop
117+
// running.
118+
func TestRunLeaderElectionWithControllers(t *testing.T) {
119+
t.Logf("Creating controller")
120+
var controllers []*controller.Controller
121+
for k := range kube.ResourceMap {
122+
c, err := controller.NewController(testutil.Clients.KubernetesClient, k, testutil.Namespace, []string{}, metrics.NewCollectors())
123+
if err != nil {
124+
logrus.Fatalf("%s", err)
125+
}
126+
127+
controllers = append(controllers, c)
128+
}
129+
time.Sleep(3 * time.Second)
130+
131+
lock := GetNewLock(testutil.Clients.KubernetesClient.CoordinationV1(), fmt.Sprintf("%s-%d", constants.LockName, 1), testutil.Pod, testutil.Namespace)
132+
133+
ctx, cancel := context.WithCancel(context.TODO())
134+
135+
// Start running leadership election, this also starts the controllers
136+
go RunLeaderElection(lock, ctx, cancel, testutil.Pod, controllers)
137+
time.Sleep(3 * time.Second)
138+
139+
// Create some stuff and do a thing
140+
configmapName := testutil.ConfigmapNamePrefix + "-update-" + testutil.RandSeq(5)
141+
configmapClient, err := testutil.CreateConfigMap(testutil.Clients.KubernetesClient, testutil.Namespace, configmapName, "www.google.com")
142+
if err != nil {
143+
t.Fatalf("Error while creating the configmap %v", err)
144+
}
145+
146+
// Creating deployment
147+
_, err = testutil.CreateDeployment(testutil.Clients.KubernetesClient, configmapName, testutil.Namespace, true)
148+
if err != nil {
149+
t.Fatalf("Error in deployment creation: %v", err)
150+
}
151+
152+
// Updating configmap for first time
153+
updateErr := testutil.UpdateConfigMap(configmapClient, testutil.Namespace, configmapName, "", "www.stakater.com")
154+
if updateErr != nil {
155+
t.Fatalf("Configmap was not updated")
156+
}
157+
time.Sleep(3 * time.Second)
158+
159+
// Verifying deployment update
160+
logrus.Infof("Verifying pod envvars has been created")
161+
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, testutil.Namespace, configmapName, "www.stakater.com")
162+
config := util.Config{
163+
Namespace: testutil.Namespace,
164+
ResourceName: configmapName,
165+
SHAValue: shaData,
166+
Annotation: options.ConfigmapUpdateOnChangeAnnotation,
167+
}
168+
deploymentFuncs := handler.GetDeploymentRollingUpgradeFuncs()
169+
updated := testutil.VerifyResourceEnvVarUpdate(testutil.Clients, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs)
170+
if !updated {
171+
t.Fatalf("Deployment was not updated")
172+
}
173+
time.Sleep(testutil.SleepDuration)
174+
175+
// Cancel the leader election context, so leadership is released
176+
logrus.Info("shutting down controller from test")
177+
cancel()
178+
time.Sleep(5 * time.Second)
179+
180+
// Updating configmap again
181+
updateErr = testutil.UpdateConfigMap(configmapClient, testutil.Namespace, configmapName, "", "www.stakater.com/new")
182+
if updateErr != nil {
183+
t.Fatalf("Configmap was not updated")
184+
}
185+
186+
// Verifying that the deployment was not updated as leadership has been lost
187+
logrus.Infof("Verifying pod envvars has not been updated")
188+
shaData = testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, testutil.Namespace, configmapName, "www.stakater.com/new")
189+
config = util.Config{
190+
Namespace: testutil.Namespace,
191+
ResourceName: configmapName,
192+
SHAValue: shaData,
193+
Annotation: options.ConfigmapUpdateOnChangeAnnotation,
194+
}
195+
deploymentFuncs = handler.GetDeploymentRollingUpgradeFuncs()
196+
updated = testutil.VerifyResourceEnvVarUpdate(testutil.Clients, config, constants.ConfigmapEnvVarPostfix, deploymentFuncs)
197+
if updated {
198+
t.Fatalf("Deployment was updated")
199+
}
200+
201+
// Deleting deployment
202+
err = testutil.DeleteDeployment(testutil.Clients.KubernetesClient, testutil.Namespace, configmapName)
203+
if err != nil {
204+
logrus.Errorf("Error while deleting the deployment %v", err)
205+
}
206+
207+
// Deleting configmap
208+
err = testutil.DeleteConfigMap(testutil.Clients.KubernetesClient, testutil.Namespace, configmapName)
209+
if err != nil {
210+
logrus.Errorf("Error while deleting the configmap %v", err)
211+
}
212+
time.Sleep(testutil.SleepDuration)
213+
}

‎internal/pkg/options/flags.go

+2
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ var (
2525
ReloadStrategy = constants.EnvVarsReloadStrategy
2626
// ReloadOnCreate Adds support to watch create events
2727
ReloadOnCreate = "false"
28+
// EnableHA adds support for running multiple replicas via leadership election
29+
EnableHA = false
2830
)

‎internal/pkg/testutil/kube.go

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/stakater/Reloader/internal/pkg/callbacks"
1717
"github.com/stakater/Reloader/internal/pkg/constants"
1818
"github.com/stakater/Reloader/internal/pkg/crypto"
19+
"github.com/stakater/Reloader/internal/pkg/metrics"
1920
"github.com/stakater/Reloader/internal/pkg/options"
2021
"github.com/stakater/Reloader/internal/pkg/util"
2122
"github.com/stakater/Reloader/pkg/kube"
@@ -35,6 +36,19 @@ var (
3536
SecretResourceType = "secrets"
3637
)
3738

39+
var (
40+
Clients = kube.GetClients()
41+
Pod = "test-reloader-" + RandSeq(5)
42+
Namespace = "test-reloader-" + RandSeq(5)
43+
ConfigmapNamePrefix = "testconfigmap-reloader"
44+
SecretNamePrefix = "testsecret-reloader"
45+
Data = "dGVzdFNlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI="
46+
NewData = "dGVzdE5ld1NlY3JldEVuY29kaW5nRm9yUmVsb2FkZXI="
47+
UpdatedData = "dGVzdFVwZGF0ZWRTZWNyZXRFbmNvZGluZ0ZvclJlbG9hZGVy"
48+
Collectors = metrics.NewCollectors()
49+
SleepDuration = 3 * time.Second
50+
)
51+
3852
// CreateNamespace creates namespace for testing
3953
func CreateNamespace(namespace string, client kubernetes.Interface) {
4054
_, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{})

0 commit comments

Comments
 (0)
Please sign in to comment.