Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a535dc994b | ||
![]() |
46d347812b | ||
![]() |
1d12212e60 | ||
![]() |
1f17d7204e | ||
![]() |
198664e49c | ||
![]() |
ee872bb7ca | ||
![]() |
8c9a3c55bd | ||
![]() |
6f1ceee9a3 | ||
![]() |
f47375af89 |
@@ -135,7 +135,7 @@ The data directory contains all the data to recover a member to its point-in-tim
|
||||
|
||||
* Stop the member process.
|
||||
* Copy the data directory of the now-idle member to the new machine.
|
||||
* Update the peer URLs for the replaced member to reflect the new machine according to the [runtime reconfiguration instructions][update-member].
|
||||
* Update the peer URLs for the replaced member to reflect the new machine according to the [runtime reconfiguration instructions][update-a-member].
|
||||
* Start etcd on the new machine, using the same configuration and the copy of the data directory.
|
||||
|
||||
This example will walk you through the process of migrating the infra1 member to a new machine:
|
||||
@@ -259,7 +259,7 @@ Once you have verified that etcd has started successfully, shut it down and move
|
||||
|
||||
#### Restoring the cluster
|
||||
|
||||
Now that the node is running successfully, [change its advertised peer URLs][update-member], as the `--force-new-cluster` option has set the peer URL to the default listening on localhost.
|
||||
Now that the node is running successfully, [change its advertised peer URLs][update-a-member], as the `--force-new-cluster` option has set the peer URL to the default listening on localhost.
|
||||
|
||||
You can then add more nodes to the cluster and restore resiliency. See the [add a new member][add-a-member] guide for more details. **NB:** If you are trying to restore your cluster using old failed etcd nodes, please make sure you have stopped old etcd instances and removed their old data directories specified by the data-dir configuration parameter.
|
||||
|
||||
|
@@ -145,8 +145,8 @@ GET/HEAD /v2/auth/users
|
||||
"role": "root",
|
||||
"permissions": {
|
||||
"kv": {
|
||||
"read": ["*"],
|
||||
"write": ["*"]
|
||||
"read": ["/*"],
|
||||
"write": ["/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,8 +159,8 @@ GET/HEAD /v2/auth/users
|
||||
"role": "guest",
|
||||
"permissions": {
|
||||
"kv": {
|
||||
"read": ["*"],
|
||||
"write": ["*"]
|
||||
"read": ["/*"],
|
||||
"write": ["/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,8 +198,8 @@ GET/HEAD /v2/auth/users/alice
|
||||
"role": "etcd",
|
||||
"permissions" : {
|
||||
"kv" : {
|
||||
"read": [ "*" ],
|
||||
"write": [ "*" ]
|
||||
"read": [ "/*" ],
|
||||
"write": [ "/*" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,8 +311,8 @@ GET/HEAD /v2/auth/roles
|
||||
"role": "etcd",
|
||||
"permissions": {
|
||||
"kv": {
|
||||
"read": ["*"],
|
||||
"write": ["*"]
|
||||
"read": ["/*"],
|
||||
"write": ["/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -320,8 +320,8 @@ GET/HEAD /v2/auth/roles
|
||||
"role": "quay",
|
||||
"permissions": {
|
||||
"kv": {
|
||||
"read": ["*"],
|
||||
"write": ["*"]
|
||||
"read": ["/*"],
|
||||
"write": ["/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,7 +393,7 @@ PUT /v2/auth/roles/guest
|
||||
"revoke" : {
|
||||
"kv" : {
|
||||
"write": [
|
||||
"*"
|
||||
"/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -285,6 +285,10 @@ type userWithRoles struct {
|
||||
Roles []auth.Role `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
type usersCollections struct {
|
||||
Users []userWithRoles `json:"users"`
|
||||
}
|
||||
|
||||
func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
@@ -311,9 +315,7 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var usersCollections struct {
|
||||
Users []userWithRoles `json:"users"`
|
||||
}
|
||||
ucs := usersCollections{}
|
||||
for _, userName := range users {
|
||||
var user auth.User
|
||||
user, err = sh.sec.GetUser(userName)
|
||||
@@ -327,15 +329,14 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var role auth.Role
|
||||
role, err = sh.sec.GetRole(roleName)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
continue
|
||||
}
|
||||
uwr.Roles = append(uwr.Roles, role)
|
||||
}
|
||||
|
||||
usersCollections.Users = append(usersCollections.Users, uwr)
|
||||
ucs.Users = append(ucs.Users, uwr)
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(usersCollections)
|
||||
err = json.NewEncoder(w).Encode(ucs)
|
||||
|
||||
if err != nil {
|
||||
plog.Warningf("baseUsers error encoding on %s", r.URL)
|
||||
|
@@ -15,10 +15,14 @@
|
||||
package etcdhttp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -43,7 +47,14 @@ type mockAuthStore struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (s *mockAuthStore) AllUsers() ([]string, error) { return []string{"alice", "bob", "root"}, s.err }
|
||||
func (s *mockAuthStore) AllUsers() ([]string, error) {
|
||||
var us []string
|
||||
for u := range s.users {
|
||||
us = append(us, u)
|
||||
}
|
||||
sort.Strings(us)
|
||||
return us, s.err
|
||||
}
|
||||
func (s *mockAuthStore) GetUser(name string) (auth.User, error) {
|
||||
u, ok := s.users[name]
|
||||
if !ok {
|
||||
@@ -67,7 +78,13 @@ func (s *mockAuthStore) UpdateUser(user auth.User) (auth.User, error) {
|
||||
func (s *mockAuthStore) AllRoles() ([]string, error) {
|
||||
return []string{"awesome", "guest", "root"}, s.err
|
||||
}
|
||||
func (s *mockAuthStore) GetRole(name string) (auth.Role, error) { return *s.roles[name], s.err }
|
||||
func (s *mockAuthStore) GetRole(name string) (auth.Role, error) {
|
||||
r, ok := s.roles[name]
|
||||
if ok {
|
||||
return *r, s.err
|
||||
}
|
||||
return auth.Role{}, fmt.Errorf("%q does not exist (%v)", name, s.err)
|
||||
}
|
||||
func (s *mockAuthStore) CreateRole(role auth.Role) error { return s.err }
|
||||
func (s *mockAuthStore) DeleteRole(name string) error { return s.err }
|
||||
func (s *mockAuthStore) UpdateRole(role auth.Role) (auth.Role, error) {
|
||||
@@ -361,6 +378,61 @@ func TestAuthFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserGrantedWithNonexistingRole(t *testing.T) {
|
||||
sh := &authHandler{
|
||||
sec: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"root": {
|
||||
User: "root",
|
||||
Roles: []string{"root", "foo"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
cluster: &fakeCluster{id: 1},
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(sh.baseUsers))
|
||||
defer srv.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.URL, err = url.Parse(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
cli := http.DefaultClient
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var uc usersCollections
|
||||
if err := json.NewDecoder(resp.Body).Decode(&uc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(uc.Users) != 1 {
|
||||
t.Fatalf("expected 1 user, got %+v", uc.Users)
|
||||
}
|
||||
if uc.Users[0].User != "root" {
|
||||
t.Fatalf("expected 'root', got %q", uc.Users[0].User)
|
||||
}
|
||||
if len(uc.Users[0].Roles) != 1 {
|
||||
t.Fatalf("expected 1 role, got %+v", uc.Users[0].Roles)
|
||||
}
|
||||
if uc.Users[0].Roles[0].Role != "root" {
|
||||
t.Fatalf("expected 'root', got %q", uc.Users[0].Roles[0].Role)
|
||||
}
|
||||
}
|
||||
|
||||
func mustAuthRequest(method, username, password string) *http.Request {
|
||||
req, err := http.NewRequest(method, "path", strings.NewReader(""))
|
||||
if err != nil {
|
||||
|
@@ -203,6 +203,10 @@ type EtcdServer struct {
|
||||
// count the number of inflight snapshots.
|
||||
// MUST use atomic operation to access this field.
|
||||
inflightSnapshots int64
|
||||
|
||||
// wg is used to wait for the go routines that depends on the server state
|
||||
// to exit when stopping the server.
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewServer creates a new EtcdServer from the supplied configuration. The
|
||||
@@ -515,9 +519,15 @@ func (s *EtcdServer) run() {
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.r.stop()
|
||||
sched.Stop()
|
||||
|
||||
// wait for snapshots before closing raft so wal stays open
|
||||
s.wg.Wait()
|
||||
|
||||
// must stop raft after scheduler-- etcdserver can leak rafthttp pipelines
|
||||
// by adding a peer after raft stops the transport
|
||||
s.r.stop()
|
||||
|
||||
// kv, lessor and backend can be nil if running without v3 enabled
|
||||
// or running unit tests.
|
||||
if s.lessor != nil {
|
||||
@@ -1162,7 +1172,10 @@ func (s *EtcdServer) applyConfChange(cc raftpb.ConfChange, confState *raftpb.Con
|
||||
func (s *EtcdServer) snapshot(snapi uint64, confState raftpb.ConfState) {
|
||||
clone := s.store.Clone()
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
|
||||
d, err := clone.SaveNoCopy()
|
||||
// TODO: current store will never fail to do a snapshot
|
||||
// what should we do if the store might fail?
|
||||
|
@@ -231,9 +231,14 @@ func TestIssue2681(t *testing.T) {
|
||||
}
|
||||
|
||||
// Ensure we can remove a member after a snapshot then add a new one back.
|
||||
func TestIssue2746(t *testing.T) {
|
||||
func TestIssue2746(t *testing.T) { testIssue2746(t, 5) }
|
||||
|
||||
// With 3 nodes TestIssue2476 sometimes had a shutdown with an inflight snapshot.
|
||||
func TestIssue2746WithThree(t *testing.T) { testIssue2746(t, 3) }
|
||||
|
||||
func testIssue2746(t *testing.T, members int) {
|
||||
defer testutil.AfterTest(t)
|
||||
c := NewCluster(t, 5)
|
||||
c := NewCluster(t, members)
|
||||
|
||||
for _, m := range c.Members {
|
||||
m.SnapCount = 10
|
||||
@@ -247,7 +252,7 @@ func TestIssue2746(t *testing.T) {
|
||||
clusterMustProgress(t, c.Members)
|
||||
}
|
||||
|
||||
c.RemoveMember(t, uint64(c.Members[4].s.ID()))
|
||||
c.RemoveMember(t, uint64(c.Members[members-1].s.ID()))
|
||||
c.waitLeader(t, c.Members)
|
||||
|
||||
c.AddMember(t)
|
||||
|
@@ -837,6 +837,12 @@ func (r *raft) addNode(id uint64) {
|
||||
func (r *raft) removeNode(id uint64) {
|
||||
r.delProgress(id)
|
||||
r.pendingConf = false
|
||||
|
||||
// do not try to commit or abort transferring if there is no nodes in the cluster.
|
||||
if len(r.prs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// The quorum size is now smaller, so see if any pending entries can
|
||||
// be committed.
|
||||
if r.maybeCommit() {
|
||||
|
@@ -1780,6 +1780,13 @@ func TestRemoveNode(t *testing.T) {
|
||||
if g := r.nodes(); !reflect.DeepEqual(g, w) {
|
||||
t.Errorf("nodes = %v, want %v", g, w)
|
||||
}
|
||||
|
||||
// remove all nodes from cluster
|
||||
r.removeNode(1)
|
||||
w = []uint64{}
|
||||
if g := r.nodes(); !reflect.DeepEqual(g, w) {
|
||||
t.Errorf("nodes = %v, want %v", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromotable(t *testing.T) {
|
||||
|
@@ -29,7 +29,7 @@ import (
|
||||
var (
|
||||
// MinClusterVersion is the min cluster version this etcd binary is compatible with.
|
||||
MinClusterVersion = "2.2.0"
|
||||
Version = "2.3.4"
|
||||
Version = "2.3.5"
|
||||
|
||||
// Git SHA Value will be set during build
|
||||
GitSHA = "Not provided (use ./build instead of go build)"
|
||||
|
Reference in New Issue
Block a user