Skip to content

Commit baa0b92

Browse files
Merge pull request #1792 from dgrisonnet/check-etcd-endpoints
OCPBUGS-48673: targetconfigcontroller: check live etcd endpoints
2 parents 194f646 + e32cbb3 commit baa0b92

File tree

2 files changed

+202
-38
lines changed

2 files changed

+202
-38
lines changed

pkg/operator/targetconfigcontroller/targetconfigcontroller.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ import (
3939
corev1listers "k8s.io/client-go/listers/core/v1"
4040
)
4141

42+
const (
43+
etcdEndpointNamespace = "openshift-etcd"
44+
etcdEndpointName = "etcd-endpoints"
45+
)
46+
4247
type TargetConfigController struct {
4348
targetImagePullSpec string
4449
operatorImagePullSpec string
@@ -104,7 +109,7 @@ func (c TargetConfigController) sync(ctx context.Context, syncContext factory.Sy
104109

105110
// block until config is observed and specific paths are present
106111
isNotOnSingleReplicaTopology := c.notOnSingleReplicaTopologyFn()
107-
if err := isRequiredConfigPresent(operatorSpec.ObservedConfig.Raw, isNotOnSingleReplicaTopology); err != nil {
112+
if err := c.isRequiredConfigPresent(operatorSpec.ObservedConfig.Raw, isNotOnSingleReplicaTopology); err != nil {
108113
syncContext.Recorder().Warning("ConfigMissing", err.Error())
109114
return err
110115
}
@@ -120,7 +125,7 @@ func (c TargetConfigController) sync(ctx context.Context, syncContext factory.Sy
120125
return nil
121126
}
122127

123-
func isRequiredConfigPresent(config []byte, isNotSingleNode bool) error {
128+
func (c *TargetConfigController) isRequiredConfigPresent(config []byte, isNotSingleNode bool) error {
124129
if len(config) == 0 {
125130
return fmt.Errorf("no observedConfig")
126131
}
@@ -158,11 +163,45 @@ func isRequiredConfigPresent(config []byte, isNotSingleNode bool) error {
158163
if !ok {
159164
return fmt.Errorf("%v is not a slice", strings.Join(requiredPath, "."))
160165
}
161-
if len(configValSlice) < 3 {
162-
return fmt.Errorf("%v has less than three endpoints: %v", strings.Join(requiredPath, "."), configValSlice)
166+
167+
if err := etcdEndpointsPresent(c.configMapLister, configValSlice, requiredPath); err != nil {
168+
return err
169+
}
170+
}
171+
}
172+
return nil
173+
}
174+
175+
// etcdEndpointsPresent compares the list of endpoints exposed by
176+
// cluster-etcd-operator with the list of URLs in the etcd-servers config. If
177+
// less than two etcd live endpoints (not localhost and bootstrap) are present
178+
// in the config, it returns an error because the configuration does not
179+
// statisfy HA requirements.
180+
func etcdEndpointsPresent(configMapLister corev1listers.ConfigMapLister, config []interface{}, configPath []string) error {
181+
etcdEndpointsCM, err := configMapLister.ConfigMaps(etcdEndpointNamespace).Get(etcdEndpointName)
182+
if err != nil {
183+
return err
184+
}
185+
186+
var liveEndpoints []string
187+
for _, val := range config {
188+
url, ok := val.(string)
189+
if !ok {
190+
return fmt.Errorf("%v is not a string slice", strings.Join(configPath, "."))
191+
}
192+
193+
for _, etcdEndpoint := range etcdEndpointsCM.Data {
194+
if strings.Contains(url, etcdEndpoint) {
195+
liveEndpoints = append(liveEndpoints, url)
196+
break
163197
}
164198
}
165199
}
200+
201+
if len(liveEndpoints) < 2 {
202+
return fmt.Errorf("%v has less than two live etcd endpoints: %v", strings.Join(configPath, "."), liveEndpoints)
203+
}
204+
166205
return nil
167206
}
168207

pkg/operator/targetconfigcontroller/targetconfigcontroller_test.go

Lines changed: 159 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package targetconfigcontroller
22

33
import (
4+
"context"
45
"fmt"
6+
"strconv"
57
"strings"
68
"testing"
79

8-
"k8s.io/apimachinery/pkg/runtime"
9-
1010
operatorv1 "github.com/openshift/api/operator/v1"
11+
corev1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/labels"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/client-go/kubernetes/fake"
1116
"k8s.io/client-go/kubernetes/scheme"
17+
corev1listers "k8s.io/client-go/listers/core/v1"
1218
)
1319

1420
var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
@@ -109,9 +115,11 @@ func TestIsRequiredConfigPresent(t *testing.T) {
109115
},
110116
}
111117

118+
c := TargetConfigController{}
119+
112120
for _, test := range tests {
113121
t.Run(test.name, func(t *testing.T) {
114-
actual := isRequiredConfigPresent([]byte(test.config), false)
122+
actual := c.isRequiredConfigPresent([]byte(test.config), false)
115123
switch {
116124
case actual == nil && len(test.expectedError) == 0:
117125
case actual == nil && len(test.expectedError) != 0:
@@ -255,75 +263,147 @@ func TestIsRequiredConfigPresentEtcdEndpoints(t *testing.T) {
255263
}
256264
}
257265
`
266+
267+
zeroEtcdEndpoint := makeEtcdEndpointsCM()
268+
oneEtcdEndpoint := makeEtcdEndpointsCM("ip-10-0-0-1")
269+
twoEtcdEndpoints := makeEtcdEndpointsCM("ip-10-0-0-1", "ip-10-0-0-2")
270+
threeEtcdEndpoints := makeEtcdEndpointsCM("ip-10-0-0-1", "ip-10-0-0-2", "ip-10-0-0-3")
271+
258272
tests := []struct {
259273
name string
260274
etcdServers string
275+
etcdEndpointsCM *corev1.ConfigMap
261276
expectedError string
262277
isNotSingleNode bool
263278
}{
264279
{
265-
name: "nil-storage-urls",
266-
etcdServers: "null",
267-
expectedError: "apiServerArguments.etcd-servers null in config",
280+
name: "nil-storage-urls",
281+
etcdServers: "null",
282+
etcdEndpointsCM: zeroEtcdEndpoint,
283+
expectedError: "apiServerArguments.etcd-servers null in config",
268284
},
269285
{
270-
name: "missing-storage-urls",
271-
etcdServers: "[]",
272-
expectedError: "apiServerArguments.etcd-servers empty in config",
286+
name: "missing-storage-urls",
287+
etcdServers: "[]",
288+
etcdEndpointsCM: zeroEtcdEndpoint,
289+
expectedError: "apiServerArguments.etcd-servers empty in config",
273290
},
274291
{
275-
name: "empty-string-storage-urls",
276-
etcdServers: `""`,
277-
expectedError: "apiServerArguments.etcd-servers empty in config",
292+
name: "empty-string-storage-urls",
293+
etcdServers: `""`,
294+
etcdEndpointsCM: zeroEtcdEndpoint,
295+
expectedError: "apiServerArguments.etcd-servers empty in config",
278296
},
279297
{
280-
name: "one-etcd-server",
281-
etcdServers: `[ "val" ]`,
298+
name: "missing-etcd-endpoints-configmap",
299+
etcdServers: `[ "not-empty" ]`,
300+
etcdEndpointsCM: &corev1.ConfigMap{},
282301
isNotSingleNode: true,
283-
expectedError: "apiServerArguments.etcd-servers has less than three endpoints",
302+
expectedError: "configmaps \"etcd-endpoints\" not found",
284303
},
285304
{
286-
name: "one-etcd-server-sno",
287-
etcdServers: `[ "val" ]`,
288-
isNotSingleNode: false,
305+
name: "bootstrap",
306+
etcdServers: `[ "bootstrap" ]`,
307+
etcdEndpointsCM: zeroEtcdEndpoint,
308+
isNotSingleNode: true,
309+
expectedError: "apiServerArguments.etcd-servers has less than two live etcd endpoints: []",
289310
},
290311
{
291-
name: "two-etcd-servers",
292-
etcdServers: `[ "val1", "val2" ]`,
312+
name: "bootstrap-one-endpoint",
313+
etcdServers: `[ "bootstrap", "ip-10-0-0-1" ]`,
314+
etcdEndpointsCM: oneEtcdEndpoint,
293315
isNotSingleNode: true,
294-
expectedError: "apiServerArguments.etcd-servers has less than three endpoints",
316+
expectedError: "apiServerArguments.etcd-servers has less than two live etcd endpoints: [ip-10-0-0-1]",
295317
},
296318
{
297-
name: "two-etcd-servers-sno",
298-
etcdServers: `[ "val1", "val2" ]`,
299-
isNotSingleNode: false,
319+
name: "bootstrap-two-endpoints",
320+
etcdServers: `[ "bootstrap", "ip-10-0-0-1", "ip-10-0-0-2" ]`,
321+
etcdEndpointsCM: twoEtcdEndpoints,
322+
isNotSingleNode: true,
300323
},
301324
{
302-
name: "three-etcd-servers",
303-
etcdServers: `[ "val1", "val2", "val3" ]`,
325+
name: "bootstrap-three-endpoints",
326+
etcdServers: `[ "bootstrap", "ip-10-0-0-1", "ip-10-0-0-2", "ip-10-0-0-3" ]`,
327+
etcdEndpointsCM: threeEtcdEndpoints,
304328
isNotSingleNode: true,
305329
},
306330
{
307-
name: "three-etcd-servers-sno",
308-
etcdServers: `[ "val1", "val2", "val3" ]`,
309-
isNotSingleNode: false,
331+
name: "bootstrap-and-localhost",
332+
etcdServers: `[ "bootstrap", "localhost" ]`,
333+
etcdEndpointsCM: zeroEtcdEndpoint,
334+
isNotSingleNode: true,
335+
expectedError: "apiServerArguments.etcd-servers has less than two live etcd endpoints: []",
310336
},
311337
{
312-
name: "four-etcd-servers",
313-
etcdServers: `[ "val1", "val2", "val3", "val4" ]`,
338+
name: "bootstrap-localhost-one-endpoint",
339+
etcdServers: `[ "bootstrap", "localhost", "ip-10-0-0-1" ]`,
340+
etcdEndpointsCM: oneEtcdEndpoint,
314341
isNotSingleNode: true,
342+
expectedError: "apiServerArguments.etcd-servers has less than two live etcd endpoints: [ip-10-0-0-1]",
315343
},
316344
{
317-
name: "four-etcd-servers-sno",
318-
etcdServers: `[ "val1", "val2", "val3", "val4" ]`,
345+
name: "bootstrap-localhost-two-endpoints",
346+
etcdServers: `[ "bootstrap", "localhost", "ip-10-0-0-1", "ip-10-0-0-2" ]`,
347+
etcdEndpointsCM: twoEtcdEndpoints,
348+
isNotSingleNode: true,
349+
},
350+
{
351+
name: "bootstrap-localhost-three-endpoints",
352+
etcdServers: `[ "bootstrap", "localhost", "ip-10-0-0-1", "ip-10-0-0-2", "ip-10-0-0-3" ]`,
353+
etcdEndpointsCM: threeEtcdEndpoints,
354+
isNotSingleNode: true,
355+
},
356+
{
357+
name: "one-endpoint",
358+
etcdServers: `[ "ip-10-0-0-1" ]`,
359+
etcdEndpointsCM: oneEtcdEndpoint,
360+
isNotSingleNode: true,
361+
expectedError: "apiServerArguments.etcd-servers has less than two live etcd endpoints: [ip-10-0-0-1]",
362+
},
363+
{
364+
name: "two-endpoints",
365+
etcdServers: `[ "ip-10-0-0-1", "ip-10-0-0-2" ]`,
366+
etcdEndpointsCM: twoEtcdEndpoints,
367+
isNotSingleNode: true,
368+
},
369+
{
370+
name: "three-endpoints",
371+
etcdServers: `[ "ip-10-0-0-1", "ip-10-0-0-2", "ip-10-0-0-3" ]`,
372+
etcdEndpointsCM: threeEtcdEndpoints,
373+
isNotSingleNode: true,
374+
},
375+
{
376+
name: "bootstrap-sno",
377+
etcdServers: `[ "bootstrap" ]`,
378+
etcdEndpointsCM: zeroEtcdEndpoint,
379+
isNotSingleNode: false,
380+
},
381+
{
382+
name: "one-endpoint-sno",
383+
etcdServers: `[ "ip-10-0-0-1" ]`,
384+
etcdEndpointsCM: oneEtcdEndpoint,
385+
isNotSingleNode: false,
386+
},
387+
{
388+
name: "two-endpoints-sno",
389+
etcdServers: `[ "ip-10-0-0-1", "ip-10-0-0-2" ]`,
390+
etcdEndpointsCM: twoEtcdEndpoints,
391+
isNotSingleNode: false,
392+
},
393+
{
394+
name: "bootstrap-three-endpoints",
395+
etcdServers: `[ "ip-10-0-0-1", "ip-10-0-0-2", "ip-10-0-0-3" ]`,
396+
etcdEndpointsCM: threeEtcdEndpoints,
319397
isNotSingleNode: false,
320398
},
321399
}
322400

323401
for _, test := range tests {
324402
t.Run(test.name, func(t *testing.T) {
403+
kubeClient := fake.NewSimpleClientset(test.etcdEndpointsCM)
404+
c := TargetConfigController{configMapLister: &configMapLister{client: kubeClient, namespace: etcdEndpointNamespace}}
325405
config := fmt.Sprintf(configTemplate, test.etcdServers)
326-
actual := isRequiredConfigPresent([]byte(config), test.isNotSingleNode)
406+
actual := c.isRequiredConfigPresent([]byte(config), test.isNotSingleNode)
327407
switch {
328408
case actual == nil && len(test.expectedError) == 0:
329409
case actual == nil && len(test.expectedError) != 0:
@@ -336,3 +416,48 @@ func TestIsRequiredConfigPresentEtcdEndpoints(t *testing.T) {
336416
})
337417
}
338418
}
419+
420+
func makeEtcdEndpointsCM(endpoints ...string) *corev1.ConfigMap {
421+
cm := &corev1.ConfigMap{}
422+
cm.Name = etcdEndpointName
423+
cm.Namespace = etcdEndpointNamespace
424+
425+
cm.Data = make(map[string]string)
426+
for i, ep := range endpoints {
427+
cm.Data[strconv.Itoa(i)] = ep
428+
}
429+
430+
return cm
431+
}
432+
433+
type configMapLister struct {
434+
client *fake.Clientset
435+
namespace string
436+
}
437+
438+
var _ corev1listers.ConfigMapNamespaceLister = &configMapLister{}
439+
var _ corev1listers.ConfigMapLister = &configMapLister{}
440+
441+
func (l *configMapLister) List(selector labels.Selector) (ret []*corev1.ConfigMap, err error) {
442+
list, err := l.client.CoreV1().ConfigMaps(l.namespace).List(context.Background(), metav1.ListOptions{
443+
LabelSelector: selector.String(),
444+
})
445+
446+
var items []*corev1.ConfigMap
447+
for i := range list.Items {
448+
items = append(items, &list.Items[i])
449+
}
450+
451+
return items, err
452+
}
453+
454+
func (l *configMapLister) ConfigMaps(namespace string) corev1listers.ConfigMapNamespaceLister {
455+
return &configMapLister{
456+
client: l.client,
457+
namespace: namespace,
458+
}
459+
}
460+
461+
func (l *configMapLister) Get(name string) (*corev1.ConfigMap, error) {
462+
return l.client.CoreV1().ConfigMaps(l.namespace).Get(context.Background(), name, metav1.GetOptions{})
463+
}

0 commit comments

Comments
 (0)