diff --git a/internal/controller/observables.go b/internal/controller/observables.go index adbaba3f..59ceadd4 100644 --- a/internal/controller/observables.go +++ b/internal/controller/observables.go @@ -4,6 +4,7 @@ import ( "context" "sync" + "github.com/aenix-io/etcd-operator/pkg/set" clientv3 "go.etcd.io/etcd/client/v3" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -24,6 +25,7 @@ type etcdStatus struct { type observables struct { statefulSet appsv1.StatefulSet stsExists bool + endpoints corev1.Endpoints endpointsFound bool etcdStatuses []etcdStatus clusterID uint64 @@ -43,15 +45,43 @@ func (o *observables) setClusterID() { // inSplitbrain compares clusterID field with clusterIDs in etcdStatuses. // If more than one unique ID is reported, cluster is in splitbrain. +// Also if members have different opinions on the list of members, this is +// also a splitbrain. func (o *observables) inSplitbrain() bool { + return o.clusterIDsAllEqual() && o.memberListsAllEqual() +} + +func (o *observables) clusterIDsAllEqual() bool { + ids := set.New[uint64]() for i := range o.etcdStatuses { if o.etcdStatuses[i].endpointStatus != nil { - if o.clusterID != o.etcdStatuses[i].endpointStatus.Header.ClusterId { - return true + ids.Add(o.etcdStatuses[i].endpointStatus.Header.ClusterId) + } + } + return len(ids) <= 1 +} + +func (o *observables) memberListsAllEqual() bool { + type m struct { + Name string + ID uint64 + } + memberLists := make([]set.Set[m], 0, len(o.etcdStatuses)) + for i := range o.etcdStatuses { + if o.etcdStatuses[i].memberList != nil { + memberSet := set.New[m]() + for _, member := range o.etcdStatuses[i].memberList.Members { + memberSet.Add(m{member.Name, member.ID}) } + memberLists = append(memberLists, memberSet) + } + } + for i := range memberLists { + if !memberLists[0].Equals(memberLists[i]) { + return false } } - return false + return true } // fill takes a single-endpoint client and populates the fields of etcdStatus @@ -67,7 +97,11 @@ func (s *etcdStatus) fill(ctx context.Context, c *clientv3.Client) { wg.Wait() } -// TODO: make a real function +// TODO: make a real function to determine the right number of replicas. +// Hint: if ClientURL in the member list is absent, the member has not yet +// started, but if the name field is populated, this is a member of the +// initial cluster. If the name field is empty, this member has just been +// added with etcdctl member add (or equivalent API call). func (o *observables) _() int { if o.etcdStatuses != nil { for i := range o.etcdStatuses { diff --git a/pkg/set/set.go b/pkg/set/set.go new file mode 100644 index 00000000..a1b5131c --- /dev/null +++ b/pkg/set/set.go @@ -0,0 +1,29 @@ +package set + +type Set[E comparable] map[E]struct{} + +func New[E comparable](vals ...E) Set[E] { + s := Set[E]{} + for _, v := range vals { + s[v] = struct{}{} + } + return s +} + +func (s Set[E]) Add(vals ...E) { + for _, v := range vals { + s[v] = struct{}{} + } +} + +func (s Set[E]) Equals(other Set[E]) bool { + if len(s) != len(other) { + return false + } + for k := range s { + if _, has := other[k]; !has { + return false + } + } + return true +}