diff --git a/etcdutl/etcdutl/backup_command.go b/etcdutl/etcdutl/backup_command.go index 89121a37e95c..68aed5eb0590 100644 --- a/etcdutl/etcdutl/backup_command.go +++ b/etcdutl/etcdutl/backup_command.go @@ -28,9 +28,9 @@ import ( "go.etcd.io/etcd/client/pkg/v3/types" "go.etcd.io/etcd/pkg/v3/idutil" "go.etcd.io/etcd/pkg/v3/pbutil" + "go.etcd.io/etcd/server/v3/etcdserver" "go.etcd.io/etcd/server/v3/etcdserver/api/membership" "go.etcd.io/etcd/server/v3/etcdserver/api/snap" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" "go.etcd.io/etcd/server/v3/storage/backend" "go.etcd.io/etcd/server/v3/storage/datadir" "go.etcd.io/etcd/server/v3/storage/schema" @@ -178,21 +178,10 @@ func saveSnap(lg *zap.Logger, destSnap, srcSnap string, desired *desiredCluster) // mustTranslateV2store processes storeData such that they match 'desiredCluster'. // In particular the method overrides membership information. func mustTranslateV2store(lg *zap.Logger, storeData []byte, desired *desiredCluster) []byte { - st := v2store.New() - if err := st.Recovery(storeData); err != nil { - lg.Panic("cannot translate v2store", zap.Error(err)) - } - raftCluster := membership.NewClusterFromMembers(lg, desired.clusterId, desired.members) raftCluster.SetID(desired.nodeId, desired.clusterId) - raftCluster.SetStore(st) - raftCluster.PushMembershipToStorage() - - outputData, err := st.Save() - if err != nil { - lg.Panic("cannot save v2store", zap.Error(err)) - } - return outputData + d := etcdserver.GetMembershipInfoInV2Format(lg, raftCluster) + return d } func translateWAL(lg *zap.Logger, srcWAL string, walsnap walpb.Snapshot) (etcdserverpb.Metadata, raftpb.HardState, []raftpb.Entry) { diff --git a/server/etcdserver/api/membership/cluster.go b/server/etcdserver/api/membership/cluster.go index 44ea53ea83ba..bd4213d7ab6b 100644 --- a/server/etcdserver/api/membership/cluster.go +++ b/server/etcdserver/api/membership/cluster.go @@ -854,3 +854,26 @@ func ValidateMaxLearnerConfig(maxLearners int, members []*Member, scaleUpLearner return nil } + +func (c *RaftCluster) Store(store v2store.Store) { + c.Lock() + defer c.Unlock() + for _, m := range c.members { + mustSaveMemberToStore(c.lg, store, m) + if m.ClientURLs != nil { + mustUpdateMemberAttrInStore(c.lg, store, m) + } + c.lg.Info( + "snapshot storing member", + zap.String("id", m.ID.String()), + zap.Strings("peer-urls", m.PeerURLs), + zap.Bool("is-learner", m.IsLearner), + ) + } + for id, _ := range c.removed { + mustDeleteMemberFromStore(c.lg, store, id) + } + if c.version != nil { + mustSaveClusterVersionToStore(c.lg, store, c.version) + } +} diff --git a/server/etcdserver/api/membership/cluster_test.go b/server/etcdserver/api/membership/cluster_test.go index ce98472df7b8..80bc7bf3d29f 100644 --- a/server/etcdserver/api/membership/cluster_test.go +++ b/server/etcdserver/api/membership/cluster_test.go @@ -21,6 +21,7 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" "go.uber.org/zap/zaptest" "go.etcd.io/etcd/client/pkg/v3/testutil" @@ -975,3 +976,29 @@ func TestIsReadyToPromoteMember(t *testing.T) { } } } + +func TestClusterStore(t *testing.T) { + name := "etcd" + clientURLs := []string{"http://127.0.0.1:4001"} + tests := []struct { + mems []*Member + removed map[types.ID]bool + }{ + { + []*Member{ + newTestMember(1, nil, name, clientURLs), + }, + map[types.ID]bool{types.ID(2): true}, + }, + } + for _, tt := range tests { + c := newTestCluster(t, tt.mems) + c.removed = tt.removed + + st := v2store.New("/0", "/1") + c.Store(st) + mst, rst := membersFromStore(c.lg, st) + assert.Equal(t, mst[types.ID(1)], tt.mems[0]) + assert.Equal(t, rst, tt.removed) + } +} diff --git a/server/etcdserver/api/membership/storev2.go b/server/etcdserver/api/membership/storev2.go index d428cb66e22c..9af5a8eb7e2c 100644 --- a/server/etcdserver/api/membership/storev2.go +++ b/server/etcdserver/api/membership/storev2.go @@ -94,7 +94,7 @@ func mustSaveMemberToStore(lg *zap.Logger, s v2store.Store, m *Member) { } func mustDeleteMemberFromStore(lg *zap.Logger, s v2store.Store, id types.ID) { - if _, err := s.Delete(MemberStoreKey(id), true, true); err != nil { + if _, err := s.Delete(MemberStoreKey(id), true, true); err != nil && !isKeyNotFound(err) { lg.Panic( "failed to delete member from store", zap.String("path", MemberStoreKey(id)), diff --git a/server/etcdserver/cluster_util.go b/server/etcdserver/cluster_util.go index 065283a58553..220045c0590f 100644 --- a/server/etcdserver/cluster_util.go +++ b/server/etcdserver/cluster_util.go @@ -28,6 +28,7 @@ import ( "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/types" "go.etcd.io/etcd/server/v3/etcdserver/api/membership" + "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" "go.etcd.io/etcd/server/v3/etcdserver/errors" "github.com/coreos/go-semver/semver" @@ -416,3 +417,14 @@ func convertToClusterVersion(v string) (*semver.Version, error) { ver = &semver.Version{Major: ver.Major, Minor: ver.Minor} return ver, nil } + +func GetMembershipInfoInV2Format(lg *zap.Logger, cl *membership.RaftCluster) []byte { + var st v2store.Store + st = v2store.New(StoreClusterPrefix, StoreKeysPrefix) + cl.Store(st) + d, err := st.SaveNoCopy() + if err != nil { + lg.Panic("failed to save v2 store", zap.Error(err)) + } + return d +}