Add version upgrade endpoint.

This commit is contained in:
Ben Johnson
2013-10-29 15:21:22 -06:00
parent 576badd334
commit ddf527e092
10 changed files with 131 additions and 19 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/coreos/etcd/log" "github.com/coreos/etcd/log"
"github.com/coreos/etcd/store" "github.com/coreos/etcd/store"
"github.com/coreos/go-raft" "github.com/coreos/go-raft"
"github.com/gorilla/mux"
) )
type PeerServer struct { type PeerServer struct {
@ -236,25 +237,27 @@ func (s *PeerServer) startAsFollower(cluster []string) {
func (s *PeerServer) startTransport(scheme string, tlsConf tls.Config) error { func (s *PeerServer) startTransport(scheme string, tlsConf tls.Config) error {
log.Infof("raft server [name %s, listen on %s, advertised url %s]", s.name, s.listenHost, s.url) log.Infof("raft server [name %s, listen on %s, advertised url %s]", s.name, s.listenHost, s.url)
raftMux := http.NewServeMux() router := mux.NewRouter()
s.httpServer = &http.Server{ s.httpServer = &http.Server{
Handler: raftMux, Handler: router,
TLSConfig: &tlsConf, TLSConfig: &tlsConf,
Addr: s.listenHost, Addr: s.listenHost,
} }
// internal commands // internal commands
raftMux.HandleFunc("/name", s.NameHttpHandler) router.HandleFunc("/name", s.NameHttpHandler)
raftMux.HandleFunc("/version", s.VersionHttpHandler) router.HandleFunc("/version", s.VersionHttpHandler)
raftMux.HandleFunc("/join", s.JoinHttpHandler) router.HandleFunc("/version/{version:[0-9]+}/check", s.VersionCheckHttpHandler)
raftMux.HandleFunc("/remove/", s.RemoveHttpHandler) router.HandleFunc("/upgrade", s.UpgradeHttpHandler)
raftMux.HandleFunc("/vote", s.VoteHttpHandler) router.HandleFunc("/join", s.JoinHttpHandler)
raftMux.HandleFunc("/log", s.GetLogHttpHandler) router.HandleFunc("/remove/{name:.+}", s.RemoveHttpHandler)
raftMux.HandleFunc("/log/append", s.AppendEntriesHttpHandler) router.HandleFunc("/vote", s.VoteHttpHandler)
raftMux.HandleFunc("/snapshot", s.SnapshotHttpHandler) router.HandleFunc("/log", s.GetLogHttpHandler)
raftMux.HandleFunc("/snapshotRecovery", s.SnapshotRecoveryHttpHandler) router.HandleFunc("/log/append", s.AppendEntriesHttpHandler)
raftMux.HandleFunc("/etcdURL", s.EtcdURLHttpHandler) router.HandleFunc("/snapshot", s.SnapshotHttpHandler)
router.HandleFunc("/snapshotRecovery", s.SnapshotRecoveryHttpHandler)
router.HandleFunc("/etcdURL", s.EtcdURLHttpHandler)
if scheme == "http" { if scheme == "http" {
return s.listenAndServe() return s.listenAndServe()
@ -283,6 +286,29 @@ func getVersion(t *transporter, versionURL url.URL) (int, error) {
return version, nil return version, nil
} }
// Upgradable checks whether all peers in a cluster support an upgrade to the next store version.
func (s *PeerServer) Upgradable() error {
nextVersion := s.store.Version() + 1
for _, peerURL := range s.registry.PeerURLs(s.raftServer.Leader(), s.name) {
u, err := url.Parse(peerURL)
if err != nil {
return fmt.Errorf("PeerServer: Cannot parse URL: '%s' (%s)", peerURL, err)
}
t, _ := s.raftServer.Transporter().(*transporter)
checkURL := (&url.URL{Host: u.Host, Scheme: s.tlsConf.Scheme, Path: fmt.Sprintf("/version/%d/check", nextVersion)}).String()
resp, _, err := t.Get(checkURL)
if err != nil {
return fmt.Errorf("PeerServer: Cannot check version compatibility: %s", u.Host)
}
if resp.StatusCode != 200 {
return fmt.Errorf("PeerServer: Version %d is not compatible with peer: %s", nextVersion, u.Host)
}
}
return nil
}
func (s *PeerServer) joinCluster(cluster []string) bool { func (s *PeerServer) joinCluster(cluster []string) bool {
for _, machine := range cluster { for _, machine := range cluster {
if len(machine) == 0 { if len(machine) == 0 {

View File

@ -7,7 +7,9 @@ import (
etcdErr "github.com/coreos/etcd/error" etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/log" "github.com/coreos/etcd/log"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft" "github.com/coreos/go-raft"
"github.com/gorilla/mux"
) )
// Get all the current logs // Get all the current logs
@ -134,9 +136,9 @@ func (ps *PeerServer) RemoveHttpHandler(w http.ResponseWriter, req *http.Request
return return
} }
nodeName := req.URL.Path[len("/remove/"):] vars := mux.Vars(req)
command := &RemoveCommand{ command := &RemoveCommand{
Name: nodeName, Name: vars["name"],
} }
log.Debugf("[recv] Remove Request [%s]", command.Name) log.Debugf("[recv] Remove Request [%s]", command.Name)
@ -157,3 +159,35 @@ func (ps *PeerServer) VersionHttpHandler(w http.ResponseWriter, req *http.Reques
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(strconv.Itoa(ps.store.Version()))) w.Write([]byte(strconv.Itoa(ps.store.Version())))
} }
// Checks whether a given version is supported.
func (ps *PeerServer) VersionCheckHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s%s ", ps.url, req.URL.Path)
vars := mux.Vars(req)
version, _ := strconv.Atoi(vars["version"])
if version >= store.MinVersion() && version <= store.MaxVersion() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusForbidden)
}
}
// Upgrades the current store version to the next version.
func (ps *PeerServer) UpgradeHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s/version", ps.url)
// Check if upgrade is possible for all nodes.
if err := ps.Upgradable(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create an upgrade command from the current version.
c := ps.store.CommandFactory().CreateUpgradeCommand()
if err := ps.server.Dispatch(c, w, req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@ -15,6 +15,7 @@ var minVersion, maxVersion int
// depending on the current version of the store. // depending on the current version of the store.
type CommandFactory interface { type CommandFactory interface {
Version() int Version() int
CreateUpgradeCommand() raft.Command
CreateSetCommand(key string, value string, expireTime time.Time) raft.Command CreateSetCommand(key string, value string, expireTime time.Time) raft.Command
CreateCreateCommand(key string, value string, expireTime time.Time, unique bool) raft.Command CreateCreateCommand(key string, value string, expireTime time.Time, unique bool) raft.Command
CreateUpdateCommand(key string, value string, expireTime time.Time) raft.Command CreateUpdateCommand(key string, value string, expireTime time.Time) raft.Command

View File

@ -20,6 +20,11 @@ func (f *CommandFactory) Version() int {
return 2 return 2
} }
// CreateUpgradeCommand is a no-op since version 2 is the first version to support store versioning.
func (f *CommandFactory) CreateUpgradeCommand() raft.Command {
return &raft.NOPCommand{}
}
// CreateSetCommand creates a version 2 command to set a key to a given value in the store. // CreateSetCommand creates a version 2 command to set a key to a given value in the store.
func (f *CommandFactory) CreateSetCommand(key string, value string, expireTime time.Time) raft.Command { func (f *CommandFactory) CreateSetCommand(key string, value string, expireTime time.Time) raft.Command {
return &SetCommand{ return &SetCommand{

View File

@ -23,7 +23,7 @@ type CompareAndSwapCommand struct {
// The name of the testAndSet command in the log // The name of the testAndSet command in the log
func (c *CompareAndSwapCommand) CommandName() string { func (c *CompareAndSwapCommand) CommandName() string {
return "etcd:v2:compareAndSwap" return "etcd:compareAndSwap"
} }
// Set the key-value pair if the current value of the key equals to the given prevValue // Set the key-value pair if the current value of the key equals to the given prevValue

View File

@ -22,7 +22,7 @@ type CreateCommand struct {
// The name of the create command in the log // The name of the create command in the log
func (c *CreateCommand) CommandName() string { func (c *CreateCommand) CommandName() string {
return "etcd:v2:create" return "etcd:create"
} }
// Create node // Create node

View File

@ -18,7 +18,7 @@ type DeleteCommand struct {
// The name of the delete command in the log // The name of the delete command in the log
func (c *DeleteCommand) CommandName() string { func (c *DeleteCommand) CommandName() string {
return "etcd:v2:delete" return "etcd:delete"
} }
// Delete the key // Delete the key

View File

@ -21,7 +21,7 @@ type SetCommand struct {
// The name of the create command in the log // The name of the create command in the log
func (c *SetCommand) CommandName() string { func (c *SetCommand) CommandName() string {
return "etcd:v2:set" return "etcd:set"
} }
// Create node // Create node

View File

@ -20,7 +20,7 @@ type UpdateCommand struct {
// The name of the update command in the log // The name of the update command in the log
func (c *UpdateCommand) CommandName() string { func (c *UpdateCommand) CommandName() string {
return "etcd:v2:update" return "etcd:update"
} }
// Create node // Create node

View File

@ -0,0 +1,46 @@
package test
import (
"net/http"
"os"
"testing"
"time"
)
// Ensure that a node can reply to a version check appropriately.
func TestVersionCheck(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
args := []string{"etcd", "-n=node1", "-f", "-d=/tmp/version_check"}
process, err := os.StartProcess(EtcdBinPath, args, procAttr)
if err != nil {
t.Fatal("start process failed:" + err.Error())
return
}
defer process.Kill()
time.Sleep(time.Second)
// Check a version too small.
resp, _ := http.Get("http://localhost:7001/version/1/check")
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatal("Invalid version check: ", resp.StatusCode)
}
// Check a version too large.
resp, _ = http.Get("http://localhost:7001/version/3/check")
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatal("Invalid version check: ", resp.StatusCode)
}
// Check a version that's just right.
resp, _ = http.Get("http://localhost:7001/version/2/check")
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Invalid version check: ", resp.StatusCode)
}
}