mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-06 17:57:51 +00:00
Add vendor folder to git
This commit is contained in:
parent
66cf5eaafb
commit
183585f56f
6916 changed files with 2629581 additions and 1 deletions
84
vendor/github.com/coreos/etcd/etcdserver/api/capability.go
generated
vendored
Normal file
84
vendor/github.com/coreos/etcd/etcdserver/api/capability.go
generated
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/version"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
AuthCapability Capability = "auth"
|
||||
V3rpcCapability Capability = "v3rpc"
|
||||
)
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/api")
|
||||
|
||||
// capabilityMaps is a static map of version to capability map.
|
||||
// the base capabilities is the set of capability 2.0 supports.
|
||||
capabilityMaps = map[string]map[Capability]bool{
|
||||
"2.3.0": {AuthCapability: true},
|
||||
"3.0.0": {AuthCapability: true, V3rpcCapability: true},
|
||||
"3.1.0": {AuthCapability: true, V3rpcCapability: true},
|
||||
}
|
||||
|
||||
enableMapMu sync.RWMutex
|
||||
// enabledMap points to a map in capabilityMaps
|
||||
enabledMap map[Capability]bool
|
||||
|
||||
curVersion *semver.Version
|
||||
)
|
||||
|
||||
func init() {
|
||||
enabledMap = make(map[Capability]bool)
|
||||
}
|
||||
|
||||
// UpdateCapability updates the enabledMap when the cluster version increases.
|
||||
func UpdateCapability(v *semver.Version) {
|
||||
if v == nil {
|
||||
// if recovered but version was never set by cluster
|
||||
return
|
||||
}
|
||||
enableMapMu.Lock()
|
||||
if curVersion != nil && !curVersion.LessThan(*v) {
|
||||
enableMapMu.Unlock()
|
||||
return
|
||||
}
|
||||
curVersion = v
|
||||
enabledMap = capabilityMaps[curVersion.String()]
|
||||
enableMapMu.Unlock()
|
||||
plog.Infof("enabled capabilities for version %s", version.Cluster(v.String()))
|
||||
}
|
||||
|
||||
func IsCapabilityEnabled(c Capability) bool {
|
||||
enableMapMu.RLock()
|
||||
defer enableMapMu.RUnlock()
|
||||
if enabledMap == nil {
|
||||
return false
|
||||
}
|
||||
return enabledMap[c]
|
||||
}
|
||||
|
||||
func EnableCapability(c Capability) {
|
||||
enableMapMu.Lock()
|
||||
defer enableMapMu.Unlock()
|
||||
enabledMap[c] = true
|
||||
}
|
||||
41
vendor/github.com/coreos/etcd/etcdserver/api/cluster.go
generated
vendored
Normal file
41
vendor/github.com/coreos/etcd/etcdserver/api/cluster.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
// Cluster is an interface representing a collection of members in one etcd cluster.
|
||||
type Cluster interface {
|
||||
// ID returns the cluster ID
|
||||
ID() types.ID
|
||||
// ClientURLs returns an aggregate set of all URLs on which this
|
||||
// cluster is listening for client requests
|
||||
ClientURLs() []string
|
||||
// Members returns a slice of members sorted by their ID
|
||||
Members() []*membership.Member
|
||||
// Member retrieves a particular member based on ID, or nil if the
|
||||
// member does not exist in the cluster
|
||||
Member(id types.ID) *membership.Member
|
||||
// IsIDRemoved checks whether the given ID has been removed from this
|
||||
// cluster at some point in the past
|
||||
IsIDRemoved(id types.ID) bool
|
||||
// Version is the cluster-wide minimum major.minor version.
|
||||
Version() *semver.Version
|
||||
}
|
||||
16
vendor/github.com/coreos/etcd/etcdserver/api/doc.go
generated
vendored
Normal file
16
vendor/github.com/coreos/etcd/etcdserver/api/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package api manages the capabilities and features that are exposed to clients by the etcd cluster.
|
||||
package api
|
||||
40
vendor/github.com/coreos/etcd/etcdserver/api/v2http/capability.go
generated
vendored
Normal file
40
vendor/github.com/coreos/etcd/etcdserver/api/v2http/capability.go
generated
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2http/httptypes"
|
||||
)
|
||||
|
||||
func capabilityHandler(c api.Capability, fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !api.IsCapabilityEnabled(c) {
|
||||
notCapable(w, r, c)
|
||||
return
|
||||
}
|
||||
fn(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func notCapable(w http.ResponseWriter, r *http.Request, c api.Capability) {
|
||||
herr := httptypes.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Not capable of accessing %s feature during rolling upgrades.", c))
|
||||
if err := herr.WriteTo(w); err != nil {
|
||||
plog.Debugf("error writing HTTPError (%v) to %s", err, r.RemoteAddr)
|
||||
}
|
||||
}
|
||||
822
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client.go
generated
vendored
Normal file
822
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client.go
generated
vendored
Normal file
|
|
@ -0,0 +1,822 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2http/httptypes"
|
||||
"github.com/coreos/etcd/etcdserver/auth"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/etcdserver/stats"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/etcd/version"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
authPrefix = "/v2/auth"
|
||||
keysPrefix = "/v2/keys"
|
||||
deprecatedMachinesPrefix = "/v2/machines"
|
||||
membersPrefix = "/v2/members"
|
||||
statsPrefix = "/v2/stats"
|
||||
varsPath = "/debug/vars"
|
||||
metricsPath = "/metrics"
|
||||
healthPath = "/health"
|
||||
versionPath = "/version"
|
||||
configPath = "/config"
|
||||
)
|
||||
|
||||
// NewClientHandler generates a muxed http.Handler with the given parameters to serve etcd client requests.
|
||||
func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http.Handler {
|
||||
sec := auth.NewStore(server, timeout)
|
||||
|
||||
kh := &keysHandler{
|
||||
sec: sec,
|
||||
server: server,
|
||||
cluster: server.Cluster(),
|
||||
timer: server,
|
||||
timeout: timeout,
|
||||
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
|
||||
}
|
||||
|
||||
sh := &statsHandler{
|
||||
stats: server,
|
||||
}
|
||||
|
||||
mh := &membersHandler{
|
||||
sec: sec,
|
||||
server: server,
|
||||
cluster: server.Cluster(),
|
||||
timeout: timeout,
|
||||
clock: clockwork.NewRealClock(),
|
||||
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
|
||||
}
|
||||
|
||||
dmh := &deprecatedMachinesHandler{
|
||||
cluster: server.Cluster(),
|
||||
}
|
||||
|
||||
sech := &authHandler{
|
||||
sec: sec,
|
||||
cluster: server.Cluster(),
|
||||
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", http.NotFound)
|
||||
mux.Handle(healthPath, healthHandler(server))
|
||||
mux.HandleFunc(versionPath, versionHandler(server.Cluster(), serveVersion))
|
||||
mux.Handle(keysPrefix, kh)
|
||||
mux.Handle(keysPrefix+"/", kh)
|
||||
mux.HandleFunc(statsPrefix+"/store", sh.serveStore)
|
||||
mux.HandleFunc(statsPrefix+"/self", sh.serveSelf)
|
||||
mux.HandleFunc(statsPrefix+"/leader", sh.serveLeader)
|
||||
mux.HandleFunc(varsPath, serveVars)
|
||||
mux.HandleFunc(configPath+"/local/log", logHandleFunc)
|
||||
mux.Handle(metricsPath, prometheus.Handler())
|
||||
mux.Handle(membersPrefix, mh)
|
||||
mux.Handle(membersPrefix+"/", mh)
|
||||
mux.Handle(deprecatedMachinesPrefix, dmh)
|
||||
handleAuth(mux, sech)
|
||||
|
||||
return requestLogger(mux)
|
||||
}
|
||||
|
||||
type keysHandler struct {
|
||||
sec auth.Store
|
||||
server etcdserver.Server
|
||||
cluster api.Cluster
|
||||
timer etcdserver.RaftTimer
|
||||
timeout time.Duration
|
||||
clientCertAuthEnabled bool
|
||||
}
|
||||
|
||||
func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "HEAD", "GET", "PUT", "POST", "DELETE") {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), h.timeout)
|
||||
defer cancel()
|
||||
clock := clockwork.NewRealClock()
|
||||
startTime := clock.Now()
|
||||
rr, noValueOnSuccess, err := parseKeyRequest(r, clock)
|
||||
if err != nil {
|
||||
writeKeyError(w, err)
|
||||
return
|
||||
}
|
||||
// The path must be valid at this point (we've parsed the request successfully).
|
||||
if !hasKeyPrefixAccess(h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive, h.clientCertAuthEnabled) {
|
||||
writeKeyNoAuth(w)
|
||||
return
|
||||
}
|
||||
if !rr.Wait {
|
||||
reportRequestReceived(rr)
|
||||
}
|
||||
resp, err := h.server.Do(ctx, rr)
|
||||
if err != nil {
|
||||
err = trimErrorPrefix(err, etcdserver.StoreKeysPrefix)
|
||||
writeKeyError(w, err)
|
||||
reportRequestFailed(rr, err)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case resp.Event != nil:
|
||||
if err := writeKeyEvent(w, resp.Event, noValueOnSuccess, h.timer); err != nil {
|
||||
// Should never be reached
|
||||
plog.Errorf("error writing event (%v)", err)
|
||||
}
|
||||
reportRequestCompleted(rr, resp, startTime)
|
||||
case resp.Watcher != nil:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultWatchTimeout)
|
||||
defer cancel()
|
||||
handleKeyWatch(ctx, w, resp.Watcher, rr.Stream, h.timer)
|
||||
default:
|
||||
writeKeyError(w, errors.New("received response with no Event/Watcher!"))
|
||||
}
|
||||
}
|
||||
|
||||
type deprecatedMachinesHandler struct {
|
||||
cluster api.Cluster
|
||||
}
|
||||
|
||||
func (h *deprecatedMachinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET", "HEAD") {
|
||||
return
|
||||
}
|
||||
endpoints := h.cluster.ClientURLs()
|
||||
w.Write([]byte(strings.Join(endpoints, ", ")))
|
||||
}
|
||||
|
||||
type membersHandler struct {
|
||||
sec auth.Store
|
||||
server etcdserver.Server
|
||||
cluster api.Cluster
|
||||
timeout time.Duration
|
||||
clock clockwork.Clock
|
||||
clientCertAuthEnabled bool
|
||||
}
|
||||
|
||||
func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") {
|
||||
return
|
||||
}
|
||||
if !hasWriteRootAccess(h.sec, r, h.clientCertAuthEnabled) {
|
||||
writeNoAuth(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), h.timeout)
|
||||
defer cancel()
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
switch trimPrefix(r.URL.Path, membersPrefix) {
|
||||
case "":
|
||||
mc := newMemberCollection(h.cluster.Members())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(mc); err != nil {
|
||||
plog.Warningf("failed to encode members response (%v)", err)
|
||||
}
|
||||
case "leader":
|
||||
id := h.server.Leader()
|
||||
if id == 0 {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusServiceUnavailable, "During election"))
|
||||
return
|
||||
}
|
||||
m := newMember(h.cluster.Member(id))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(m); err != nil {
|
||||
plog.Warningf("failed to encode members response (%v)", err)
|
||||
}
|
||||
default:
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusNotFound, "Not found"))
|
||||
}
|
||||
case "POST":
|
||||
req := httptypes.MemberCreateRequest{}
|
||||
if ok := unmarshalRequest(r, &req, w); !ok {
|
||||
return
|
||||
}
|
||||
now := h.clock.Now()
|
||||
m := membership.NewMember("", req.PeerURLs, "", &now)
|
||||
err := h.server.AddMember(ctx, *m)
|
||||
switch {
|
||||
case err == membership.ErrIDExists || err == membership.ErrPeerURLexists:
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusConflict, err.Error()))
|
||||
return
|
||||
case err != nil:
|
||||
plog.Errorf("error adding member %s (%v)", m.ID, err)
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
res := newMember(m)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
plog.Warningf("failed to encode members response (%v)", err)
|
||||
}
|
||||
case "DELETE":
|
||||
id, ok := getID(r.URL.Path, w)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
err := h.server.RemoveMember(ctx, uint64(id))
|
||||
switch {
|
||||
case err == membership.ErrIDRemoved:
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusGone, fmt.Sprintf("Member permanently removed: %s", id)))
|
||||
case err == membership.ErrIDNotFound:
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id)))
|
||||
case err != nil:
|
||||
plog.Errorf("error removing member %s (%v)", id, err)
|
||||
writeError(w, r, err)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
case "PUT":
|
||||
id, ok := getID(r.URL.Path, w)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
req := httptypes.MemberUpdateRequest{}
|
||||
if ok := unmarshalRequest(r, &req, w); !ok {
|
||||
return
|
||||
}
|
||||
m := membership.Member{
|
||||
ID: id,
|
||||
RaftAttributes: membership.RaftAttributes{PeerURLs: req.PeerURLs.StringSlice()},
|
||||
}
|
||||
err := h.server.UpdateMember(ctx, m)
|
||||
switch {
|
||||
case err == membership.ErrPeerURLexists:
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusConflict, err.Error()))
|
||||
case err == membership.ErrIDNotFound:
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", id)))
|
||||
case err != nil:
|
||||
plog.Errorf("error updating member %s (%v)", m.ID, err)
|
||||
writeError(w, r, err)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type statsHandler struct {
|
||||
stats stats.Stats
|
||||
}
|
||||
|
||||
func (h *statsHandler) serveStore(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(h.stats.StoreStats())
|
||||
}
|
||||
|
||||
func (h *statsHandler) serveSelf(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(h.stats.SelfStats())
|
||||
}
|
||||
|
||||
func (h *statsHandler) serveLeader(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
stats := h.stats.LeaderStats()
|
||||
if stats == nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusForbidden, "not current leader"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(stats)
|
||||
}
|
||||
|
||||
func serveVars(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprintf(w, "{\n")
|
||||
first := true
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
if !first {
|
||||
fmt.Fprintf(w, ",\n")
|
||||
}
|
||||
first = false
|
||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
||||
})
|
||||
fmt.Fprintf(w, "\n}\n")
|
||||
}
|
||||
|
||||
func healthHandler(server *etcdserver.EtcdServer) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
if uint64(server.Leader()) == raft.None {
|
||||
http.Error(w, `{"health": "false"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if _, err := server.Do(ctx, etcdserverpb.Request{Method: "QGET"}); err != nil {
|
||||
http.Error(w, `{"health": "false"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"health": "true"}`))
|
||||
}
|
||||
}
|
||||
|
||||
func versionHandler(c api.Cluster, fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
v := c.Version()
|
||||
if v != nil {
|
||||
fn(w, r, v.String())
|
||||
} else {
|
||||
fn(w, r, "not_decided")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveVersion(w http.ResponseWriter, r *http.Request, clusterV string) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
vs := version.Versions{
|
||||
Server: version.Version,
|
||||
Cluster: clusterV,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
b, err := json.Marshal(&vs)
|
||||
if err != nil {
|
||||
plog.Panicf("cannot marshal versions to json (%v)", err)
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func logHandleFunc(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "PUT") {
|
||||
return
|
||||
}
|
||||
|
||||
in := struct{ Level string }{}
|
||||
|
||||
d := json.NewDecoder(r.Body)
|
||||
if err := d.Decode(&in); err != nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid json body"))
|
||||
return
|
||||
}
|
||||
|
||||
logl, err := capnslog.ParseLevel(strings.ToUpper(in.Level))
|
||||
if err != nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid log level "+in.Level))
|
||||
return
|
||||
}
|
||||
|
||||
plog.Noticef("globalLogLevel set to %q", logl.String())
|
||||
capnslog.SetGlobalLogLevel(logl)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// parseKeyRequest converts a received http.Request on keysPrefix to
|
||||
// a server Request, performing validation of supplied fields as appropriate.
|
||||
// If any validation fails, an empty Request and non-nil error is returned.
|
||||
func parseKeyRequest(r *http.Request, clock clockwork.Clock) (etcdserverpb.Request, bool, error) {
|
||||
noValueOnSuccess := false
|
||||
emptyReq := etcdserverpb.Request{}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidForm,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(r.URL.Path, keysPrefix) {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidForm,
|
||||
"incorrect key prefix",
|
||||
)
|
||||
}
|
||||
p := path.Join(etcdserver.StoreKeysPrefix, r.URL.Path[len(keysPrefix):])
|
||||
|
||||
var pIdx, wIdx uint64
|
||||
if pIdx, err = getUint64(r.Form, "prevIndex"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeIndexNaN,
|
||||
`invalid value for "prevIndex"`,
|
||||
)
|
||||
}
|
||||
if wIdx, err = getUint64(r.Form, "waitIndex"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeIndexNaN,
|
||||
`invalid value for "waitIndex"`,
|
||||
)
|
||||
}
|
||||
|
||||
var rec, sort, wait, dir, quorum, stream bool
|
||||
if rec, err = getBool(r.Form, "recursive"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "recursive"`,
|
||||
)
|
||||
}
|
||||
if sort, err = getBool(r.Form, "sorted"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "sorted"`,
|
||||
)
|
||||
}
|
||||
if wait, err = getBool(r.Form, "wait"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "wait"`,
|
||||
)
|
||||
}
|
||||
// TODO(jonboulle): define what parameters dir is/isn't compatible with?
|
||||
if dir, err = getBool(r.Form, "dir"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "dir"`,
|
||||
)
|
||||
}
|
||||
if quorum, err = getBool(r.Form, "quorum"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "quorum"`,
|
||||
)
|
||||
}
|
||||
if stream, err = getBool(r.Form, "stream"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "stream"`,
|
||||
)
|
||||
}
|
||||
|
||||
if wait && r.Method != "GET" {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`"wait" can only be used with GET requests`,
|
||||
)
|
||||
}
|
||||
|
||||
pV := r.FormValue("prevValue")
|
||||
if _, ok := r.Form["prevValue"]; ok && pV == "" {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodePrevValueRequired,
|
||||
`"prevValue" cannot be empty`,
|
||||
)
|
||||
}
|
||||
|
||||
if noValueOnSuccess, err = getBool(r.Form, "noValueOnSuccess"); err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
`invalid value for "noValueOnSuccess"`,
|
||||
)
|
||||
}
|
||||
|
||||
// TTL is nullable, so leave it null if not specified
|
||||
// or an empty string
|
||||
var ttl *uint64
|
||||
if len(r.FormValue("ttl")) > 0 {
|
||||
i, err := getUint64(r.Form, "ttl")
|
||||
if err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeTTLNaN,
|
||||
`invalid value for "ttl"`,
|
||||
)
|
||||
}
|
||||
ttl = &i
|
||||
}
|
||||
|
||||
// prevExist is nullable, so leave it null if not specified
|
||||
var pe *bool
|
||||
if _, ok := r.Form["prevExist"]; ok {
|
||||
bv, err := getBool(r.Form, "prevExist")
|
||||
if err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
"invalid value for prevExist",
|
||||
)
|
||||
}
|
||||
pe = &bv
|
||||
}
|
||||
|
||||
// refresh is nullable, so leave it null if not specified
|
||||
var refresh *bool
|
||||
if _, ok := r.Form["refresh"]; ok {
|
||||
bv, err := getBool(r.Form, "refresh")
|
||||
if err != nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeInvalidField,
|
||||
"invalid value for refresh",
|
||||
)
|
||||
}
|
||||
refresh = &bv
|
||||
if refresh != nil && *refresh {
|
||||
val := r.FormValue("value")
|
||||
if _, ok := r.Form["value"]; ok && val != "" {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeRefreshValue,
|
||||
`A value was provided on a refresh`,
|
||||
)
|
||||
}
|
||||
if ttl == nil {
|
||||
return emptyReq, false, etcdErr.NewRequestError(
|
||||
etcdErr.EcodeRefreshTTLRequired,
|
||||
`No TTL value set`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rr := etcdserverpb.Request{
|
||||
Method: r.Method,
|
||||
Path: p,
|
||||
Val: r.FormValue("value"),
|
||||
Dir: dir,
|
||||
PrevValue: pV,
|
||||
PrevIndex: pIdx,
|
||||
PrevExist: pe,
|
||||
Wait: wait,
|
||||
Since: wIdx,
|
||||
Recursive: rec,
|
||||
Sorted: sort,
|
||||
Quorum: quorum,
|
||||
Stream: stream,
|
||||
}
|
||||
|
||||
if pe != nil {
|
||||
rr.PrevExist = pe
|
||||
}
|
||||
|
||||
if refresh != nil {
|
||||
rr.Refresh = refresh
|
||||
}
|
||||
|
||||
// Null TTL is equivalent to unset Expiration
|
||||
if ttl != nil {
|
||||
expr := time.Duration(*ttl) * time.Second
|
||||
rr.Expiration = clock.Now().Add(expr).UnixNano()
|
||||
}
|
||||
|
||||
return rr, noValueOnSuccess, nil
|
||||
}
|
||||
|
||||
// writeKeyEvent trims the prefix of key path in a single Event under
|
||||
// StoreKeysPrefix, serializes it and writes the resulting JSON to the given
|
||||
// ResponseWriter, along with the appropriate headers.
|
||||
func writeKeyEvent(w http.ResponseWriter, ev *store.Event, noValueOnSuccess bool, rt etcdserver.RaftTimer) error {
|
||||
if ev == nil {
|
||||
return errors.New("cannot write empty Event!")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("X-Etcd-Index", fmt.Sprint(ev.EtcdIndex))
|
||||
w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
|
||||
w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
|
||||
|
||||
if ev.IsCreated() {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
|
||||
if noValueOnSuccess &&
|
||||
(ev.Action == store.Set || ev.Action == store.CompareAndSwap ||
|
||||
ev.Action == store.Create || ev.Action == store.Update) {
|
||||
ev.Node = nil
|
||||
ev.PrevNode = nil
|
||||
}
|
||||
return json.NewEncoder(w).Encode(ev)
|
||||
}
|
||||
|
||||
func writeKeyNoAuth(w http.ResponseWriter) {
|
||||
e := etcdErr.NewError(etcdErr.EcodeUnauthorized, "Insufficient credentials", 0)
|
||||
e.WriteTo(w)
|
||||
}
|
||||
|
||||
// writeKeyError logs and writes the given Error to the ResponseWriter.
|
||||
// If Error is not an etcdErr, the error will be converted to an etcd error.
|
||||
func writeKeyError(w http.ResponseWriter, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
switch e := err.(type) {
|
||||
case *etcdErr.Error:
|
||||
e.WriteTo(w)
|
||||
default:
|
||||
switch err {
|
||||
case etcdserver.ErrTimeoutDueToLeaderFail, etcdserver.ErrTimeoutDueToConnectionLost:
|
||||
mlog.MergeError(err)
|
||||
default:
|
||||
mlog.MergeErrorf("got unexpected response error (%v)", err)
|
||||
}
|
||||
ee := etcdErr.NewError(etcdErr.EcodeRaftInternal, err.Error(), 0)
|
||||
ee.WriteTo(w)
|
||||
}
|
||||
}
|
||||
|
||||
func handleKeyWatch(ctx context.Context, w http.ResponseWriter, wa store.Watcher, stream bool, rt etcdserver.RaftTimer) {
|
||||
defer wa.Remove()
|
||||
ech := wa.EventChan()
|
||||
var nch <-chan bool
|
||||
if x, ok := w.(http.CloseNotifier); ok {
|
||||
nch = x.CloseNotify()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("X-Etcd-Index", fmt.Sprint(wa.StartIndex()))
|
||||
w.Header().Set("X-Raft-Index", fmt.Sprint(rt.Index()))
|
||||
w.Header().Set("X-Raft-Term", fmt.Sprint(rt.Term()))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// Ensure headers are flushed early, in case of long polling
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-nch:
|
||||
// Client closed connection. Nothing to do.
|
||||
return
|
||||
case <-ctx.Done():
|
||||
// Timed out. net/http will close the connection for us, so nothing to do.
|
||||
return
|
||||
case ev, ok := <-ech:
|
||||
if !ok {
|
||||
// If the channel is closed this may be an indication of
|
||||
// that notifications are much more than we are able to
|
||||
// send to the client in time. Then we simply end streaming.
|
||||
return
|
||||
}
|
||||
ev = trimEventPrefix(ev, etcdserver.StoreKeysPrefix)
|
||||
if err := json.NewEncoder(w).Encode(ev); err != nil {
|
||||
// Should never be reached
|
||||
plog.Warningf("error writing event (%v)", err)
|
||||
return
|
||||
}
|
||||
if !stream {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func trimEventPrefix(ev *store.Event, prefix string) *store.Event {
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
// Since the *Event may reference one in the store history
|
||||
// history, we must copy it before modifying
|
||||
e := ev.Clone()
|
||||
trimNodeExternPrefix(e.Node, prefix)
|
||||
trimNodeExternPrefix(e.PrevNode, prefix)
|
||||
return e
|
||||
}
|
||||
|
||||
func trimNodeExternPrefix(n *store.NodeExtern, prefix string) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
n.Key = strings.TrimPrefix(n.Key, prefix)
|
||||
for _, nn := range n.Nodes {
|
||||
trimNodeExternPrefix(nn, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func trimErrorPrefix(err error, prefix string) error {
|
||||
if e, ok := err.(*etcdErr.Error); ok {
|
||||
e.Cause = strings.TrimPrefix(e.Cause, prefix)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unmarshalRequest(r *http.Request, req json.Unmarshaler, w http.ResponseWriter) bool {
|
||||
ctype := r.Header.Get("Content-Type")
|
||||
semicolonPosition := strings.Index(ctype, ";")
|
||||
if semicolonPosition != -1 {
|
||||
ctype = strings.TrimSpace(strings.ToLower(ctype[0:semicolonPosition]))
|
||||
}
|
||||
if ctype != "application/json" {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusUnsupportedMediaType, fmt.Sprintf("Bad Content-Type %s, accept application/json", ctype)))
|
||||
return false
|
||||
}
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, err.Error()))
|
||||
return false
|
||||
}
|
||||
if err := req.UnmarshalJSON(b); err != nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, err.Error()))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getID(p string, w http.ResponseWriter) (types.ID, bool) {
|
||||
idStr := trimPrefix(p, membersPrefix)
|
||||
if idStr == "" {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return 0, false
|
||||
}
|
||||
id, err := types.IDFromString(idStr)
|
||||
if err != nil {
|
||||
writeError(w, nil, httptypes.NewHTTPError(http.StatusNotFound, fmt.Sprintf("No such member: %s", idStr)))
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
// getUint64 extracts a uint64 by the given key from a Form. If the key does
|
||||
// not exist in the form, 0 is returned. If the key exists but the value is
|
||||
// badly formed, an error is returned. If multiple values are present only the
|
||||
// first is considered.
|
||||
func getUint64(form url.Values, key string) (i uint64, err error) {
|
||||
if vals, ok := form[key]; ok {
|
||||
i, err = strconv.ParseUint(vals[0], 10, 64)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getBool extracts a bool by the given key from a Form. If the key does not
|
||||
// exist in the form, false is returned. If the key exists but the value is
|
||||
// badly formed, an error is returned. If multiple values are present only the
|
||||
// first is considered.
|
||||
func getBool(form url.Values, key string) (b bool, err error) {
|
||||
if vals, ok := form[key]; ok {
|
||||
b, err = strconv.ParseBool(vals[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// trimPrefix removes a given prefix and any slash following the prefix
|
||||
// e.g.: trimPrefix("foo", "foo") == trimPrefix("foo/", "foo") == ""
|
||||
func trimPrefix(p, prefix string) (s string) {
|
||||
s = strings.TrimPrefix(p, prefix)
|
||||
s = strings.TrimPrefix(s, "/")
|
||||
return
|
||||
}
|
||||
|
||||
func newMemberCollection(ms []*membership.Member) *httptypes.MemberCollection {
|
||||
c := httptypes.MemberCollection(make([]httptypes.Member, len(ms)))
|
||||
|
||||
for i, m := range ms {
|
||||
c[i] = newMember(m)
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func newMember(m *membership.Member) httptypes.Member {
|
||||
tm := httptypes.Member{
|
||||
ID: m.ID.String(),
|
||||
Name: m.Name,
|
||||
PeerURLs: make([]string, len(m.PeerURLs)),
|
||||
ClientURLs: make([]string, len(m.ClientURLs)),
|
||||
}
|
||||
|
||||
copy(tm.PeerURLs, m.PeerURLs)
|
||||
copy(tm.ClientURLs, m.ClientURLs)
|
||||
|
||||
return tm
|
||||
}
|
||||
543
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client_auth.go
generated
vendored
Normal file
543
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client_auth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2http/httptypes"
|
||||
"github.com/coreos/etcd/etcdserver/auth"
|
||||
)
|
||||
|
||||
type authHandler struct {
|
||||
sec auth.Store
|
||||
cluster api.Cluster
|
||||
clientCertAuthEnabled bool
|
||||
}
|
||||
|
||||
func hasWriteRootAccess(sec auth.Store, r *http.Request, clientCertAuthEnabled bool) bool {
|
||||
if r.Method == "GET" || r.Method == "HEAD" {
|
||||
return true
|
||||
}
|
||||
return hasRootAccess(sec, r, clientCertAuthEnabled)
|
||||
}
|
||||
|
||||
func userFromBasicAuth(sec auth.Store, r *http.Request) *auth.User {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
plog.Warningf("auth: malformed basic auth encoding")
|
||||
return nil
|
||||
}
|
||||
user, err := sec.GetUser(username)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ok = sec.CheckPassword(user, password)
|
||||
if !ok {
|
||||
plog.Warningf("auth: incorrect password for user: %s", username)
|
||||
return nil
|
||||
}
|
||||
return &user
|
||||
}
|
||||
|
||||
func userFromClientCertificate(sec auth.Store, r *http.Request) *auth.User {
|
||||
if r.TLS == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, chains := range r.TLS.VerifiedChains {
|
||||
for _, chain := range chains {
|
||||
plog.Debugf("auth: found common name %s.\n", chain.Subject.CommonName)
|
||||
user, err := sec.GetUser(chain.Subject.CommonName)
|
||||
if err == nil {
|
||||
plog.Debugf("auth: authenticated user %s by cert common name.", user.User)
|
||||
return &user
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasRootAccess(sec auth.Store, r *http.Request, clientCertAuthEnabled bool) bool {
|
||||
if sec == nil {
|
||||
// No store means no auth available, eg, tests.
|
||||
return true
|
||||
}
|
||||
if !sec.AuthEnabled() {
|
||||
return true
|
||||
}
|
||||
|
||||
var rootUser *auth.User
|
||||
if r.Header.Get("Authorization") == "" && clientCertAuthEnabled {
|
||||
rootUser = userFromClientCertificate(sec, r)
|
||||
if rootUser == nil {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
rootUser = userFromBasicAuth(sec, r)
|
||||
if rootUser == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range rootUser.Roles {
|
||||
if role == auth.RootRoleName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
plog.Warningf("auth: user %s does not have the %s role for resource %s.", rootUser.User, auth.RootRoleName, r.URL.Path)
|
||||
return false
|
||||
}
|
||||
|
||||
func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive, clientCertAuthEnabled bool) bool {
|
||||
if sec == nil {
|
||||
// No store means no auth available, eg, tests.
|
||||
return true
|
||||
}
|
||||
if !sec.AuthEnabled() {
|
||||
return true
|
||||
}
|
||||
|
||||
var user *auth.User
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
if clientCertAuthEnabled {
|
||||
user = userFromClientCertificate(sec, r)
|
||||
}
|
||||
if user == nil {
|
||||
return hasGuestAccess(sec, r, key)
|
||||
}
|
||||
} else {
|
||||
user = userFromBasicAuth(sec, r)
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
writeAccess := r.Method != "GET" && r.Method != "HEAD"
|
||||
for _, roleName := range user.Roles {
|
||||
role, err := sec.GetRole(roleName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if recursive {
|
||||
if role.HasRecursiveAccess(key, writeAccess) {
|
||||
return true
|
||||
}
|
||||
} else if role.HasKeyAccess(key, writeAccess) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
plog.Warningf("auth: invalid access for user %s on key %s.", user.User, key)
|
||||
return false
|
||||
}
|
||||
|
||||
func hasGuestAccess(sec auth.Store, r *http.Request, key string) bool {
|
||||
writeAccess := r.Method != "GET" && r.Method != "HEAD"
|
||||
role, err := sec.GetRole(auth.GuestRoleName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if role.HasKeyAccess(key, writeAccess) {
|
||||
return true
|
||||
}
|
||||
plog.Warningf("auth: invalid access for unauthenticated user on resource %s.", key)
|
||||
return false
|
||||
}
|
||||
|
||||
func writeNoAuth(w http.ResponseWriter, r *http.Request) {
|
||||
herr := httptypes.NewHTTPError(http.StatusUnauthorized, "Insufficient credentials")
|
||||
if err := herr.WriteTo(w); err != nil {
|
||||
plog.Debugf("error writing HTTPError (%v) to %s", err, r.RemoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAuth(mux *http.ServeMux, sh *authHandler) {
|
||||
mux.HandleFunc(authPrefix+"/roles", capabilityHandler(api.AuthCapability, sh.baseRoles))
|
||||
mux.HandleFunc(authPrefix+"/roles/", capabilityHandler(api.AuthCapability, sh.handleRoles))
|
||||
mux.HandleFunc(authPrefix+"/users", capabilityHandler(api.AuthCapability, sh.baseUsers))
|
||||
mux.HandleFunc(authPrefix+"/users/", capabilityHandler(api.AuthCapability, sh.handleUsers))
|
||||
mux.HandleFunc(authPrefix+"/enable", capabilityHandler(api.AuthCapability, sh.enableDisable))
|
||||
}
|
||||
|
||||
func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
|
||||
writeNoAuth(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
roles, err := sh.sec.AllRoles()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
if roles == nil {
|
||||
roles = make([]string, 0)
|
||||
}
|
||||
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var rolesCollections struct {
|
||||
Roles []auth.Role `json:"roles"`
|
||||
}
|
||||
for _, roleName := range roles {
|
||||
var role auth.Role
|
||||
role, err = sh.sec.GetRole(roleName)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
rolesCollections.Roles = append(rolesCollections.Roles, role)
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(rolesCollections)
|
||||
|
||||
if err != nil {
|
||||
plog.Warningf("baseRoles error encoding on %s", r.URL)
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (sh *authHandler) handleRoles(w http.ResponseWriter, r *http.Request) {
|
||||
subpath := path.Clean(r.URL.Path[len(authPrefix):])
|
||||
// Split "/roles/rolename/command".
|
||||
// First item is an empty string, second is "roles"
|
||||
pieces := strings.Split(subpath, "/")
|
||||
if len(pieces) == 2 {
|
||||
sh.baseRoles(w, r)
|
||||
return
|
||||
}
|
||||
if len(pieces) != 3 {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
|
||||
return
|
||||
}
|
||||
sh.forRole(w, r, pieces[2])
|
||||
}
|
||||
|
||||
func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role string) {
|
||||
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
|
||||
return
|
||||
}
|
||||
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
|
||||
writeNoAuth(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
data, err := sh.sec.GetRole(role)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
plog.Warningf("forRole error encoding on %s", r.URL)
|
||||
return
|
||||
}
|
||||
return
|
||||
case "PUT":
|
||||
var in auth.Role
|
||||
err := json.NewDecoder(r.Body).Decode(&in)
|
||||
if err != nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
|
||||
return
|
||||
}
|
||||
if in.Role != role {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON name does not match the name in the URL"))
|
||||
return
|
||||
}
|
||||
|
||||
var out auth.Role
|
||||
|
||||
// create
|
||||
if in.Grant.IsEmpty() && in.Revoke.IsEmpty() {
|
||||
err = sh.sec.CreateRole(in)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
out = in
|
||||
} else {
|
||||
if !in.Permissions.IsEmpty() {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON contains both permissions and grant/revoke"))
|
||||
return
|
||||
}
|
||||
out, err = sh.sec.UpdateRole(in)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(out)
|
||||
if err != nil {
|
||||
plog.Warningf("forRole error encoding on %s", r.URL)
|
||||
return
|
||||
}
|
||||
return
|
||||
case "DELETE":
|
||||
err := sh.sec.DeleteRole(role)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type userWithRoles struct {
|
||||
User string `json:"user"`
|
||||
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
|
||||
}
|
||||
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
|
||||
writeNoAuth(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
users, err := sh.sec.AllUsers()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
if users == nil {
|
||||
users = make([]string, 0)
|
||||
}
|
||||
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ucs := usersCollections{}
|
||||
for _, userName := range users {
|
||||
var user auth.User
|
||||
user, err = sh.sec.GetUser(userName)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
uwr := userWithRoles{User: user.User}
|
||||
for _, roleName := range user.Roles {
|
||||
var role auth.Role
|
||||
role, err = sh.sec.GetRole(roleName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
uwr.Roles = append(uwr.Roles, role)
|
||||
}
|
||||
|
||||
ucs.Users = append(ucs.Users, uwr)
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(ucs)
|
||||
|
||||
if err != nil {
|
||||
plog.Warningf("baseUsers error encoding on %s", r.URL)
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (sh *authHandler) handleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
subpath := path.Clean(r.URL.Path[len(authPrefix):])
|
||||
// Split "/users/username".
|
||||
// First item is an empty string, second is "users"
|
||||
pieces := strings.Split(subpath, "/")
|
||||
if len(pieces) == 2 {
|
||||
sh.baseUsers(w, r)
|
||||
return
|
||||
}
|
||||
if len(pieces) != 3 {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
|
||||
return
|
||||
}
|
||||
sh.forUser(w, r, pieces[2])
|
||||
}
|
||||
|
||||
func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user string) {
|
||||
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
|
||||
return
|
||||
}
|
||||
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
|
||||
writeNoAuth(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
u, err := sh.sec.GetUser(user)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
uwr := userWithRoles{User: u.User}
|
||||
for _, roleName := range u.Roles {
|
||||
var role auth.Role
|
||||
role, err = sh.sec.GetRole(roleName)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
uwr.Roles = append(uwr.Roles, role)
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(uwr)
|
||||
|
||||
if err != nil {
|
||||
plog.Warningf("forUser error encoding on %s", r.URL)
|
||||
return
|
||||
}
|
||||
return
|
||||
case "PUT":
|
||||
var u auth.User
|
||||
err := json.NewDecoder(r.Body).Decode(&u)
|
||||
if err != nil {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
|
||||
return
|
||||
}
|
||||
if u.User != user {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON name does not match the name in the URL"))
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
out auth.User
|
||||
created bool
|
||||
)
|
||||
|
||||
if len(u.Grant) == 0 && len(u.Revoke) == 0 {
|
||||
// create or update
|
||||
if len(u.Roles) != 0 {
|
||||
out, err = sh.sec.CreateUser(u)
|
||||
} else {
|
||||
// if user passes in both password and roles, we are unsure about his/her
|
||||
// intention.
|
||||
out, created, err = sh.sec.CreateOrUpdateUser(u)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// update case
|
||||
if len(u.Roles) != 0 {
|
||||
writeError(w, r, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON contains both roles and grant/revoke"))
|
||||
return
|
||||
}
|
||||
out, err = sh.sec.UpdateUser(u)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if created {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
out.Password = ""
|
||||
|
||||
err = json.NewEncoder(w).Encode(out)
|
||||
if err != nil {
|
||||
plog.Warningf("forUser error encoding on %s", r.URL)
|
||||
return
|
||||
}
|
||||
return
|
||||
case "DELETE":
|
||||
err := sh.sec.DeleteUser(user)
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type enabled struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (sh *authHandler) enableDisable(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
|
||||
return
|
||||
}
|
||||
if !hasWriteRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
|
||||
writeNoAuth(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
isEnabled := sh.sec.AuthEnabled()
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
jsonDict := enabled{isEnabled}
|
||||
err := json.NewEncoder(w).Encode(jsonDict)
|
||||
if err != nil {
|
||||
plog.Warningf("error encoding auth state on %s", r.URL)
|
||||
}
|
||||
case "PUT":
|
||||
err := sh.sec.EnableAuth()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
case "DELETE":
|
||||
err := sh.sec.DisableAuth()
|
||||
if err != nil {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
910
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client_auth_test.go
generated
vendored
Normal file
910
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client_auth_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,910 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/etcdserver/auth"
|
||||
)
|
||||
|
||||
const goodPassword = "good"
|
||||
|
||||
func mustJSONRequest(t *testing.T, method string, p string, body string) *http.Request {
|
||||
req, err := http.NewRequest(method, path.Join(authPrefix, p), strings.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("Error making JSON request: %s %s %s\n", method, p, body)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
type mockAuthStore struct {
|
||||
users map[string]*auth.User
|
||||
roles map[string]*auth.Role
|
||||
err error
|
||||
enabled bool
|
||||
}
|
||||
|
||||
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 {
|
||||
return auth.User{}, s.err
|
||||
}
|
||||
return *u, s.err
|
||||
}
|
||||
func (s *mockAuthStore) CreateOrUpdateUser(user auth.User) (out auth.User, created bool, err error) {
|
||||
if s.users == nil {
|
||||
out, err = s.CreateUser(user)
|
||||
return out, true, err
|
||||
}
|
||||
out, err = s.UpdateUser(user)
|
||||
return out, false, err
|
||||
}
|
||||
func (s *mockAuthStore) CreateUser(user auth.User) (auth.User, error) { return user, s.err }
|
||||
func (s *mockAuthStore) DeleteUser(name string) error { return s.err }
|
||||
func (s *mockAuthStore) UpdateUser(user auth.User) (auth.User, error) {
|
||||
return *s.users[user.User], s.err
|
||||
}
|
||||
func (s *mockAuthStore) AllRoles() ([]string, error) {
|
||||
return []string{"awesome", "guest", "root"}, 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) {
|
||||
return *s.roles[role.Role], s.err
|
||||
}
|
||||
func (s *mockAuthStore) AuthEnabled() bool { return s.enabled }
|
||||
func (s *mockAuthStore) EnableAuth() error { return s.err }
|
||||
func (s *mockAuthStore) DisableAuth() error { return s.err }
|
||||
|
||||
func (s *mockAuthStore) CheckPassword(user auth.User, password string) bool {
|
||||
return user.Password == password
|
||||
}
|
||||
|
||||
func (s *mockAuthStore) HashPassword(password string) (string, error) {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func TestAuthFlow(t *testing.T) {
|
||||
api.EnableCapability(api.AuthCapability)
|
||||
var testCases = []struct {
|
||||
req *http.Request
|
||||
store mockAuthStore
|
||||
|
||||
wcode int
|
||||
wbody string
|
||||
}{
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "users/alice", `{{{{{{{`),
|
||||
store: mockAuthStore{},
|
||||
wcode: http.StatusBadRequest,
|
||||
wbody: `{"message":"Invalid JSON in request body."}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
|
||||
store: mockAuthStore{enabled: true},
|
||||
wcode: http.StatusUnauthorized,
|
||||
wbody: `{"message":"Insufficient credentials"}`,
|
||||
},
|
||||
// Users
|
||||
{
|
||||
req: mustJSONRequest(t, "GET", "users", ""),
|
||||
store: mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"alice": {
|
||||
User: "alice",
|
||||
Roles: []string{"alicerole", "guest"},
|
||||
Password: "wheeee",
|
||||
},
|
||||
"bob": {
|
||||
User: "bob",
|
||||
Roles: []string{"guest"},
|
||||
Password: "wheeee",
|
||||
},
|
||||
"root": {
|
||||
User: "root",
|
||||
Roles: []string{"root"},
|
||||
Password: "wheeee",
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"alicerole": {
|
||||
Role: "alicerole",
|
||||
},
|
||||
"guest": {
|
||||
Role: "guest",
|
||||
},
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"users":[` +
|
||||
`{"user":"alice","roles":[` +
|
||||
`{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}},` +
|
||||
`{"role":"guest","permissions":{"kv":{"read":null,"write":null}}}` +
|
||||
`]},` +
|
||||
`{"user":"bob","roles":[{"role":"guest","permissions":{"kv":{"read":null,"write":null}}}]},` +
|
||||
`{"user":"root","roles":[{"role":"root","permissions":{"kv":{"read":null,"write":null}}}]}]}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "GET", "users/alice", ""),
|
||||
store: mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"alice": {
|
||||
User: "alice",
|
||||
Roles: []string{"alicerole"},
|
||||
Password: "wheeee",
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"alicerole": {
|
||||
Role: "alicerole",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"user":"alice","roles":[{"role":"alicerole","permissions":{"kv":{"read":null,"write":null}}}]}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
|
||||
store: mockAuthStore{},
|
||||
wcode: http.StatusCreated,
|
||||
wbody: `{"user":"alice","roles":null}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "DELETE", "users/alice", ``),
|
||||
store: mockAuthStore{},
|
||||
wcode: http.StatusOK,
|
||||
wbody: ``,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "password": "goodpassword"}`),
|
||||
store: mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"alice": {
|
||||
User: "alice",
|
||||
Roles: []string{"alicerole", "guest"},
|
||||
Password: "wheeee",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"user":"alice","roles":["alicerole","guest"]}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "users/alice", `{"user": "alice", "grant": ["alicerole"]}`),
|
||||
store: mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"alice": {
|
||||
User: "alice",
|
||||
Roles: []string{"alicerole", "guest"},
|
||||
Password: "wheeee",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"user":"alice","roles":["alicerole","guest"]}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "GET", "users/alice", ``),
|
||||
store: mockAuthStore{
|
||||
users: map[string]*auth.User{},
|
||||
err: auth.Error{Status: http.StatusNotFound, Errmsg: "auth: User alice doesn't exist."},
|
||||
},
|
||||
wcode: http.StatusNotFound,
|
||||
wbody: `{"message":"auth: User alice doesn't exist."}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "GET", "roles/manager", ""),
|
||||
store: mockAuthStore{
|
||||
roles: map[string]*auth.Role{
|
||||
"manager": {
|
||||
Role: "manager",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"role":"manager","permissions":{"kv":{"read":null,"write":null}}}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "DELETE", "roles/manager", ``),
|
||||
store: mockAuthStore{},
|
||||
wcode: http.StatusOK,
|
||||
wbody: ``,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "roles/manager", `{"role":"manager","permissions":{"kv":{"read":[],"write":[]}}}`),
|
||||
store: mockAuthStore{},
|
||||
wcode: http.StatusCreated,
|
||||
wbody: `{"role":"manager","permissions":{"kv":{"read":[],"write":[]}}}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "roles/manager", `{"role":"manager","revoke":{"kv":{"read":["foo"],"write":[]}}}`),
|
||||
store: mockAuthStore{
|
||||
roles: map[string]*auth.Role{
|
||||
"manager": {
|
||||
Role: "manager",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"role":"manager","permissions":{"kv":{"read":null,"write":null}}}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "GET", "roles", ""),
|
||||
store: mockAuthStore{
|
||||
roles: map[string]*auth.Role{
|
||||
"awesome": {
|
||||
Role: "awesome",
|
||||
},
|
||||
"guest": {
|
||||
Role: "guest",
|
||||
},
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"roles":[{"role":"awesome","permissions":{"kv":{"read":null,"write":null}}},` +
|
||||
`{"role":"guest","permissions":{"kv":{"read":null,"write":null}}},` +
|
||||
`{"role":"root","permissions":{"kv":{"read":null,"write":null}}}]}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "GET", "enable", ""),
|
||||
store: mockAuthStore{
|
||||
enabled: true,
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: `{"enabled":true}`,
|
||||
},
|
||||
{
|
||||
req: mustJSONRequest(t, "PUT", "enable", ""),
|
||||
store: mockAuthStore{
|
||||
enabled: false,
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: ``,
|
||||
},
|
||||
{
|
||||
req: (func() *http.Request {
|
||||
req := mustJSONRequest(t, "DELETE", "enable", "")
|
||||
req.SetBasicAuth("root", "good")
|
||||
return req
|
||||
})(),
|
||||
store: mockAuthStore{
|
||||
enabled: true,
|
||||
users: map[string]*auth.User{
|
||||
"root": {
|
||||
User: "root",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"root"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusOK,
|
||||
wbody: ``,
|
||||
},
|
||||
{
|
||||
req: (func() *http.Request {
|
||||
req := mustJSONRequest(t, "DELETE", "enable", "")
|
||||
req.SetBasicAuth("root", "bad")
|
||||
return req
|
||||
})(),
|
||||
store: mockAuthStore{
|
||||
enabled: true,
|
||||
users: map[string]*auth.User{
|
||||
"root": {
|
||||
User: "root",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"root"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "guest",
|
||||
},
|
||||
},
|
||||
},
|
||||
wcode: http.StatusUnauthorized,
|
||||
wbody: `{"message":"Insufficient credentials"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range testCases {
|
||||
mux := http.NewServeMux()
|
||||
h := &authHandler{
|
||||
sec: &tt.store,
|
||||
cluster: &fakeCluster{id: 1},
|
||||
}
|
||||
handleAuth(mux, h)
|
||||
rw := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rw, tt.req)
|
||||
if rw.Code != tt.wcode {
|
||||
t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode)
|
||||
}
|
||||
g := rw.Body.String()
|
||||
g = strings.TrimSpace(g)
|
||||
if g != tt.wbody {
|
||||
t.Errorf("#%d: got body=%s, want %s", i, g, tt.wbody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
panic("Cannot make auth request: " + err.Error())
|
||||
}
|
||||
req.SetBasicAuth(username, password)
|
||||
return req
|
||||
}
|
||||
|
||||
func unauthedRequest(method string) *http.Request {
|
||||
req, err := http.NewRequest(method, "path", strings.NewReader(""))
|
||||
if err != nil {
|
||||
panic("Cannot make request: " + err.Error())
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func tlsAuthedRequest(req *http.Request, certname string) *http.Request {
|
||||
bytes, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.pem", certname))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(bytes)
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req.TLS = &tls.ConnectionState{
|
||||
VerifiedChains: [][]*x509.Certificate{{cert}},
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func TestPrefixAccess(t *testing.T) {
|
||||
var table = []struct {
|
||||
key string
|
||||
req *http.Request
|
||||
store *mockAuthStore
|
||||
hasRoot bool
|
||||
hasKeyPrefixAccess bool
|
||||
hasRecursiveAccess bool
|
||||
}{
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustAuthRequest("GET", "root", "good"),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"root": {
|
||||
User: "root",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"root"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: true,
|
||||
hasKeyPrefixAccess: true,
|
||||
hasRecursiveAccess: true,
|
||||
},
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustAuthRequest("GET", "user", "good"),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"foorole"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"foorole": {
|
||||
Role: "foorole",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo"},
|
||||
Write: []string{"/foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: true,
|
||||
hasRecursiveAccess: false,
|
||||
},
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustAuthRequest("GET", "user", "good"),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"foorole"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"foorole": {
|
||||
Role: "foorole",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo*"},
|
||||
Write: []string{"/foo*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: true,
|
||||
hasRecursiveAccess: true,
|
||||
},
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustAuthRequest("GET", "user", "bad"),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"foorole"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"foorole": {
|
||||
Role: "foorole",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo*"},
|
||||
Write: []string{"/foo*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: false,
|
||||
hasRecursiveAccess: false,
|
||||
},
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustAuthRequest("GET", "user", "good"),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{},
|
||||
err: errors.New("Not the user"),
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: false,
|
||||
hasRecursiveAccess: false,
|
||||
},
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustJSONRequest(t, "GET", "somepath", ""),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"foorole"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"guest": {
|
||||
Role: "guest",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo*"},
|
||||
Write: []string{"/foo*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: true,
|
||||
hasRecursiveAccess: true,
|
||||
},
|
||||
{
|
||||
key: "/bar",
|
||||
req: mustJSONRequest(t, "GET", "somepath", ""),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"foorole"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"guest": {
|
||||
Role: "guest",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo*"},
|
||||
Write: []string{"/foo*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: false,
|
||||
hasRecursiveAccess: false,
|
||||
},
|
||||
// check access for multiple roles
|
||||
{
|
||||
key: "/foo",
|
||||
req: mustAuthRequest("GET", "user", "good"),
|
||||
store: &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"role1", "role2"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"role1": {
|
||||
Role: "role1",
|
||||
},
|
||||
"role2": {
|
||||
Role: "role2",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo"},
|
||||
Write: []string{"/foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: true,
|
||||
hasRecursiveAccess: false,
|
||||
},
|
||||
{
|
||||
key: "/foo",
|
||||
req: (func() *http.Request {
|
||||
req := mustJSONRequest(t, "GET", "somepath", "")
|
||||
req.Header.Set("Authorization", "malformedencoding")
|
||||
return req
|
||||
})(),
|
||||
store: &mockAuthStore{
|
||||
enabled: true,
|
||||
users: map[string]*auth.User{
|
||||
"root": {
|
||||
User: "root",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"root"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"guest": {
|
||||
Role: "guest",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo*"},
|
||||
Write: []string{"/foo*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: false,
|
||||
hasRecursiveAccess: false,
|
||||
},
|
||||
{ // guest access in non-TLS mode
|
||||
key: "/foo",
|
||||
req: (func() *http.Request {
|
||||
return mustJSONRequest(t, "GET", "somepath", "")
|
||||
})(),
|
||||
store: &mockAuthStore{
|
||||
enabled: true,
|
||||
users: map[string]*auth.User{
|
||||
"root": {
|
||||
User: "root",
|
||||
Password: goodPassword,
|
||||
Roles: []string{"root"},
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"guest": {
|
||||
Role: "guest",
|
||||
Permissions: auth.Permissions{
|
||||
KV: auth.RWPermission{
|
||||
Read: []string{"/foo*"},
|
||||
Write: []string{"/foo*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hasRoot: false,
|
||||
hasKeyPrefixAccess: true,
|
||||
hasRecursiveAccess: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range table {
|
||||
if tt.hasRoot != hasRootAccess(tt.store, tt.req, true) {
|
||||
t.Errorf("#%d: hasRoot doesn't match (expected %v)", i, tt.hasRoot)
|
||||
}
|
||||
if tt.hasKeyPrefixAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, false, true) {
|
||||
t.Errorf("#%d: hasKeyPrefixAccess doesn't match (expected %v)", i, tt.hasRoot)
|
||||
}
|
||||
if tt.hasRecursiveAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, true, true) {
|
||||
t.Errorf("#%d: hasRecursiveAccess doesn't match (expected %v)", i, tt.hasRoot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserFromClientCertificate(t *testing.T) {
|
||||
witherror := &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Roles: []string{"root"},
|
||||
Password: "password",
|
||||
},
|
||||
"basicauth": {
|
||||
User: "basicauth",
|
||||
Roles: []string{"root"},
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
err: errors.New(""),
|
||||
}
|
||||
|
||||
noerror := &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Roles: []string{"root"},
|
||||
Password: "password",
|
||||
},
|
||||
"basicauth": {
|
||||
User: "basicauth",
|
||||
Roles: []string{"root"},
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var table = []struct {
|
||||
req *http.Request
|
||||
userExists bool
|
||||
store auth.Store
|
||||
username string
|
||||
}{
|
||||
{
|
||||
// non tls request
|
||||
req: unauthedRequest("GET"),
|
||||
userExists: false,
|
||||
store: witherror,
|
||||
},
|
||||
{
|
||||
// cert with cn of existing user
|
||||
req: tlsAuthedRequest(unauthedRequest("GET"), "user"),
|
||||
userExists: true,
|
||||
username: "user",
|
||||
store: noerror,
|
||||
},
|
||||
{
|
||||
// cert with cn of non-existing user
|
||||
req: tlsAuthedRequest(unauthedRequest("GET"), "otheruser"),
|
||||
userExists: false,
|
||||
store: witherror,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range table {
|
||||
user := userFromClientCertificate(tt.store, tt.req)
|
||||
userExists := user != nil
|
||||
|
||||
if tt.userExists != userExists {
|
||||
t.Errorf("#%d: userFromClientCertificate doesn't match (expected %v)", i, tt.userExists)
|
||||
}
|
||||
if user != nil && (tt.username != user.User) {
|
||||
t.Errorf("#%d: userFromClientCertificate username doesn't match (expected %s, got %s)", i, tt.username, user.User)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserFromBasicAuth(t *testing.T) {
|
||||
sec := &mockAuthStore{
|
||||
users: map[string]*auth.User{
|
||||
"user": {
|
||||
User: "user",
|
||||
Roles: []string{"root"},
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
roles: map[string]*auth.Role{
|
||||
"root": {
|
||||
Role: "root",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var table = []struct {
|
||||
username string
|
||||
req *http.Request
|
||||
userExists bool
|
||||
}{
|
||||
{
|
||||
// valid user, valid pass
|
||||
username: "user",
|
||||
req: mustAuthRequest("GET", "user", "password"),
|
||||
userExists: true,
|
||||
},
|
||||
{
|
||||
// valid user, bad pass
|
||||
username: "user",
|
||||
req: mustAuthRequest("GET", "user", "badpass"),
|
||||
userExists: false,
|
||||
},
|
||||
{
|
||||
// valid user, no pass
|
||||
username: "user",
|
||||
req: mustAuthRequest("GET", "user", ""),
|
||||
userExists: false,
|
||||
},
|
||||
{
|
||||
// missing user
|
||||
username: "missing",
|
||||
req: mustAuthRequest("GET", "missing", "badpass"),
|
||||
userExists: false,
|
||||
},
|
||||
{
|
||||
// no basic auth
|
||||
req: unauthedRequest("GET"),
|
||||
userExists: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range table {
|
||||
user := userFromBasicAuth(sec, tt.req)
|
||||
userExists := user != nil
|
||||
|
||||
if tt.userExists != userExists {
|
||||
t.Errorf("#%d: userFromBasicAuth doesn't match (expected %v)", i, tt.userExists)
|
||||
}
|
||||
if user != nil && (tt.username != user.User) {
|
||||
t.Errorf("#%d: userFromBasicAuth username doesn't match (expected %s, got %s)", i, tt.username, user.User)
|
||||
}
|
||||
}
|
||||
}
|
||||
2086
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client_test.go
generated
vendored
Normal file
2086
vendor/github.com/coreos/etcd/etcdserver/api/v2http/client_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
16
vendor/github.com/coreos/etcd/etcdserver/api/v2http/doc.go
generated
vendored
Normal file
16
vendor/github.com/coreos/etcd/etcdserver/api/v2http/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package v2http provides etcd client and server implementations.
|
||||
package v2http
|
||||
94
vendor/github.com/coreos/etcd/etcdserver/api/v2http/http.go
generated
vendored
Normal file
94
vendor/github.com/coreos/etcd/etcdserver/api/v2http/http.go
generated
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2http/httptypes"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/auth"
|
||||
"github.com/coreos/etcd/pkg/logutil"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
const (
|
||||
// time to wait for a Watch request
|
||||
defaultWatchTimeout = time.Duration(math.MaxInt64)
|
||||
)
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/api/v2http")
|
||||
mlog = logutil.NewMergeLogger(plog)
|
||||
)
|
||||
|
||||
// writeError logs and writes the given Error to the ResponseWriter
|
||||
// If Error is an etcdErr, it is rendered to the ResponseWriter
|
||||
// Otherwise, it is assumed to be a StatusInternalServerError
|
||||
func writeError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
switch e := err.(type) {
|
||||
case *etcdErr.Error:
|
||||
e.WriteTo(w)
|
||||
case *httptypes.HTTPError:
|
||||
if et := e.WriteTo(w); et != nil {
|
||||
plog.Debugf("error writing HTTPError (%v) to %s", et, r.RemoteAddr)
|
||||
}
|
||||
case auth.Error:
|
||||
herr := httptypes.NewHTTPError(e.HTTPStatus(), e.Error())
|
||||
if et := herr.WriteTo(w); et != nil {
|
||||
plog.Debugf("error writing HTTPError (%v) to %s", et, r.RemoteAddr)
|
||||
}
|
||||
default:
|
||||
switch err {
|
||||
case etcdserver.ErrTimeoutDueToLeaderFail, etcdserver.ErrTimeoutDueToConnectionLost, etcdserver.ErrNotEnoughStartedMembers, etcdserver.ErrUnhealthy:
|
||||
mlog.MergeError(err)
|
||||
default:
|
||||
mlog.MergeErrorf("got unexpected response error (%v)", err)
|
||||
}
|
||||
herr := httptypes.NewHTTPError(http.StatusInternalServerError, "Internal Server Error")
|
||||
if et := herr.WriteTo(w); et != nil {
|
||||
plog.Debugf("error writing HTTPError (%v) to %s", et, r.RemoteAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allowMethod verifies that the given method is one of the allowed methods,
|
||||
// and if not, it writes an error to w. A boolean is returned indicating
|
||||
// whether or not the method is allowed.
|
||||
func allowMethod(w http.ResponseWriter, m string, ms ...string) bool {
|
||||
for _, meth := range ms {
|
||||
if m == meth {
|
||||
return true
|
||||
}
|
||||
}
|
||||
w.Header().Set("Allow", strings.Join(ms, ","))
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return false
|
||||
}
|
||||
|
||||
func requestLogger(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
plog.Debugf("[%s] %s remote:%s", r.Method, r.RequestURI, r.RemoteAddr)
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
193
vendor/github.com/coreos/etcd/etcdserver/api/v2http/http_test.go
generated
vendored
Normal file
193
vendor/github.com/coreos/etcd/etcdserver/api/v2http/http_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeCluster struct {
|
||||
id uint64
|
||||
clientURLs []string
|
||||
members map[uint64]*membership.Member
|
||||
}
|
||||
|
||||
func (c *fakeCluster) ID() types.ID { return types.ID(c.id) }
|
||||
func (c *fakeCluster) ClientURLs() []string { return c.clientURLs }
|
||||
func (c *fakeCluster) Members() []*membership.Member {
|
||||
var ms membership.MembersByID
|
||||
for _, m := range c.members {
|
||||
ms = append(ms, m)
|
||||
}
|
||||
sort.Sort(ms)
|
||||
return []*membership.Member(ms)
|
||||
}
|
||||
func (c *fakeCluster) Member(id types.ID) *membership.Member { return c.members[uint64(id)] }
|
||||
func (c *fakeCluster) IsIDRemoved(id types.ID) bool { return false }
|
||||
func (c *fakeCluster) Version() *semver.Version { return nil }
|
||||
|
||||
// errServer implements the etcd.Server interface for testing.
|
||||
// It returns the given error from any Do/Process/AddMember/RemoveMember calls.
|
||||
type errServer struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (fs *errServer) Start() {}
|
||||
func (fs *errServer) Stop() {}
|
||||
func (fs *errServer) ID() types.ID { return types.ID(1) }
|
||||
func (fs *errServer) Leader() types.ID { return types.ID(1) }
|
||||
func (fs *errServer) Do(ctx context.Context, r etcdserverpb.Request) (etcdserver.Response, error) {
|
||||
return etcdserver.Response{}, fs.err
|
||||
}
|
||||
func (fs *errServer) Process(ctx context.Context, m raftpb.Message) error {
|
||||
return fs.err
|
||||
}
|
||||
func (fs *errServer) AddMember(ctx context.Context, m membership.Member) error {
|
||||
return fs.err
|
||||
}
|
||||
func (fs *errServer) RemoveMember(ctx context.Context, id uint64) error {
|
||||
return fs.err
|
||||
}
|
||||
func (fs *errServer) UpdateMember(ctx context.Context, m membership.Member) error {
|
||||
return fs.err
|
||||
}
|
||||
|
||||
func (fs *errServer) ClusterVersion() *semver.Version { return nil }
|
||||
|
||||
func TestWriteError(t *testing.T) {
|
||||
// nil error should not panic
|
||||
rec := httptest.NewRecorder()
|
||||
r := new(http.Request)
|
||||
writeError(rec, r, nil)
|
||||
h := rec.Header()
|
||||
if len(h) > 0 {
|
||||
t.Fatalf("unexpected non-empty headers: %#v", h)
|
||||
}
|
||||
b := rec.Body.String()
|
||||
if len(b) > 0 {
|
||||
t.Fatalf("unexpected non-empty body: %q", b)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
err error
|
||||
wcode int
|
||||
wi string
|
||||
}{
|
||||
{
|
||||
etcdErr.NewError(etcdErr.EcodeKeyNotFound, "/foo/bar", 123),
|
||||
http.StatusNotFound,
|
||||
"123",
|
||||
},
|
||||
{
|
||||
etcdErr.NewError(etcdErr.EcodeTestFailed, "/foo/bar", 456),
|
||||
http.StatusPreconditionFailed,
|
||||
"456",
|
||||
},
|
||||
{
|
||||
err: errors.New("something went wrong"),
|
||||
wcode: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
rw := httptest.NewRecorder()
|
||||
writeError(rw, r, tt.err)
|
||||
if code := rw.Code; code != tt.wcode {
|
||||
t.Errorf("#%d: code=%d, want %d", i, code, tt.wcode)
|
||||
}
|
||||
if idx := rw.Header().Get("X-Etcd-Index"); idx != tt.wi {
|
||||
t.Errorf("#%d: X-Etcd-Index=%q, want %q", i, idx, tt.wi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
m string
|
||||
ms []string
|
||||
w bool
|
||||
wh string
|
||||
}{
|
||||
// Accepted methods
|
||||
{
|
||||
m: "GET",
|
||||
ms: []string{"GET", "POST", "PUT"},
|
||||
w: true,
|
||||
},
|
||||
{
|
||||
m: "POST",
|
||||
ms: []string{"POST"},
|
||||
w: true,
|
||||
},
|
||||
// Made-up methods no good
|
||||
{
|
||||
m: "FAKE",
|
||||
ms: []string{"GET", "POST", "PUT"},
|
||||
w: false,
|
||||
wh: "GET,POST,PUT",
|
||||
},
|
||||
// Empty methods no good
|
||||
{
|
||||
m: "",
|
||||
ms: []string{"GET", "POST"},
|
||||
w: false,
|
||||
wh: "GET,POST",
|
||||
},
|
||||
// Empty accepted methods no good
|
||||
{
|
||||
m: "GET",
|
||||
ms: []string{""},
|
||||
w: false,
|
||||
wh: "",
|
||||
},
|
||||
// No methods accepted
|
||||
{
|
||||
m: "GET",
|
||||
ms: []string{},
|
||||
w: false,
|
||||
wh: "",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
rw := httptest.NewRecorder()
|
||||
g := allowMethod(rw, tt.m, tt.ms...)
|
||||
if g != tt.w {
|
||||
t.Errorf("#%d: got allowMethod()=%t, want %t", i, g, tt.w)
|
||||
}
|
||||
if !tt.w {
|
||||
if rw.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("#%d: code=%d, want %d", i, rw.Code, http.StatusMethodNotAllowed)
|
||||
}
|
||||
gh := rw.Header().Get("Allow")
|
||||
if gh != tt.wh {
|
||||
t.Errorf("#%d: Allow header=%q, want %q", i, gh, tt.wh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/errors.go
generated
vendored
Normal file
56
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/errors.go
generated
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httptypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/api/v2http/httptypes")
|
||||
)
|
||||
|
||||
type HTTPError struct {
|
||||
Message string `json:"message"`
|
||||
// Code is the HTTP status code
|
||||
Code int `json:"-"`
|
||||
}
|
||||
|
||||
func (e HTTPError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e HTTPError) WriteTo(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(e.Code)
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
plog.Panicf("marshal HTTPError should never fail (%v)", err)
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewHTTPError(code int, m string) *HTTPError {
|
||||
return &HTTPError{
|
||||
Message: m,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
49
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/errors_test.go
generated
vendored
Normal file
49
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/errors_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httptypes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPErrorWriteTo(t *testing.T) {
|
||||
err := NewHTTPError(http.StatusBadRequest, "what a bad request you made!")
|
||||
rr := httptest.NewRecorder()
|
||||
if e := err.WriteTo(rr); e != nil {
|
||||
t.Fatalf("HTTPError.WriteTo error (%v)", e)
|
||||
}
|
||||
|
||||
wcode := http.StatusBadRequest
|
||||
wheader := http.Header(map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
})
|
||||
wbody := `{"message":"what a bad request you made!"}`
|
||||
|
||||
if wcode != rr.Code {
|
||||
t.Errorf("HTTP status code %d, want %d", rr.Code, wcode)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(wheader, rr.HeaderMap) {
|
||||
t.Errorf("HTTP headers %v, want %v", rr.HeaderMap, wheader)
|
||||
}
|
||||
|
||||
gbody := rr.Body.String()
|
||||
if wbody != gbody {
|
||||
t.Errorf("HTTP body %q, want %q", gbody, wbody)
|
||||
}
|
||||
}
|
||||
69
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/member.go
generated
vendored
Normal file
69
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/member.go
generated
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package httptypes defines how etcd's HTTP API entities are serialized to and
|
||||
// deserialized from JSON.
|
||||
package httptypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
type Member struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PeerURLs []string `json:"peerURLs"`
|
||||
ClientURLs []string `json:"clientURLs"`
|
||||
}
|
||||
|
||||
type MemberCreateRequest struct {
|
||||
PeerURLs types.URLs
|
||||
}
|
||||
|
||||
type MemberUpdateRequest struct {
|
||||
MemberCreateRequest
|
||||
}
|
||||
|
||||
func (m *MemberCreateRequest) UnmarshalJSON(data []byte) error {
|
||||
s := struct {
|
||||
PeerURLs []string `json:"peerURLs"`
|
||||
}{}
|
||||
|
||||
err := json.Unmarshal(data, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
urls, err := types.NewURLs(s.PeerURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.PeerURLs = urls
|
||||
return nil
|
||||
}
|
||||
|
||||
type MemberCollection []Member
|
||||
|
||||
func (c *MemberCollection) MarshalJSON() ([]byte, error) {
|
||||
d := struct {
|
||||
Members []Member `json:"members"`
|
||||
}{
|
||||
Members: []Member(*c),
|
||||
}
|
||||
|
||||
return json.Marshal(d)
|
||||
}
|
||||
135
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/member_test.go
generated
vendored
Normal file
135
vendor/github.com/coreos/etcd/etcdserver/api/v2http/httptypes/member_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httptypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
func TestMemberUnmarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
body []byte
|
||||
wantMember Member
|
||||
wantError bool
|
||||
}{
|
||||
// no URLs, just check ID & Name
|
||||
{
|
||||
body: []byte(`{"id": "c", "name": "dungarees"}`),
|
||||
wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil},
|
||||
},
|
||||
|
||||
// both client and peer URLs
|
||||
{
|
||||
body: []byte(`{"peerURLs": ["http://127.0.0.1:2379"], "clientURLs": ["http://127.0.0.1:2379"]}`),
|
||||
wantMember: Member{
|
||||
PeerURLs: []string{
|
||||
"http://127.0.0.1:2379",
|
||||
},
|
||||
ClientURLs: []string{
|
||||
"http://127.0.0.1:2379",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// multiple peer URLs
|
||||
{
|
||||
body: []byte(`{"peerURLs": ["http://127.0.0.1:2379", "https://example.com"]}`),
|
||||
wantMember: Member{
|
||||
PeerURLs: []string{
|
||||
"http://127.0.0.1:2379",
|
||||
"https://example.com",
|
||||
},
|
||||
ClientURLs: nil,
|
||||
},
|
||||
},
|
||||
|
||||
// multiple client URLs
|
||||
{
|
||||
body: []byte(`{"clientURLs": ["http://127.0.0.1:2379", "https://example.com"]}`),
|
||||
wantMember: Member{
|
||||
PeerURLs: nil,
|
||||
ClientURLs: []string{
|
||||
"http://127.0.0.1:2379",
|
||||
"https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// invalid JSON
|
||||
{
|
||||
body: []byte(`{"peerU`),
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := Member{}
|
||||
err := json.Unmarshal(tt.body, &got)
|
||||
if tt.wantError != (err != nil) {
|
||||
t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tt.wantMember, got) {
|
||||
t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberCreateRequestUnmarshal(t *testing.T) {
|
||||
body := []byte(`{"peerURLs": ["http://127.0.0.1:8081", "https://127.0.0.1:8080"]}`)
|
||||
want := MemberCreateRequest{
|
||||
PeerURLs: types.URLs([]url.URL{
|
||||
{Scheme: "http", Host: "127.0.0.1:8081"},
|
||||
{Scheme: "https", Host: "127.0.0.1:8080"},
|
||||
}),
|
||||
}
|
||||
|
||||
var req MemberCreateRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Fatalf("Unmarshal returned unexpected err=%v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, req) {
|
||||
t.Fatalf("Failed to unmarshal MemberCreateRequest: want=%#v, got=%#v", want, req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberCreateRequestUnmarshalFail(t *testing.T) {
|
||||
tests := [][]byte{
|
||||
// invalid JSON
|
||||
[]byte(``),
|
||||
[]byte(`{`),
|
||||
|
||||
// spot-check validation done in types.NewURLs
|
||||
[]byte(`{"peerURLs": "foo"}`),
|
||||
[]byte(`{"peerURLs": ["."]}`),
|
||||
[]byte(`{"peerURLs": []}`),
|
||||
[]byte(`{"peerURLs": ["http://127.0.0.1:2379/foo"]}`),
|
||||
[]byte(`{"peerURLs": ["http://127.0.0.1"]}`),
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
var req MemberCreateRequest
|
||||
if err := json.Unmarshal(tt, &req); err == nil {
|
||||
t.Errorf("#%d: expected err, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
vendor/github.com/coreos/etcd/etcdserver/api/v2http/metrics.go
generated
vendored
Normal file
96
vendor/github.com/coreos/etcd/etcdserver/api/v2http/metrics.go
generated
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v2http/httptypes"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
incomingEvents = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "http",
|
||||
Name: "received_total",
|
||||
Help: "Counter of requests received into the system (successfully parsed and authd).",
|
||||
}, []string{"method"})
|
||||
|
||||
failedEvents = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "http",
|
||||
Name: "failed_total",
|
||||
Help: "Counter of handle failures of requests (non-watches), by method (GET/PUT etc.) and code (400, 500 etc.).",
|
||||
}, []string{"method", "code"})
|
||||
|
||||
successfulEventsHandlingTime = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "http",
|
||||
Name: "successful_duration_seconds",
|
||||
Help: "Bucketed histogram of processing time (s) of successfully handled requests (non-watches), by method (GET/PUT etc.).",
|
||||
Buckets: prometheus.ExponentialBuckets(0.0005, 2, 13),
|
||||
}, []string{"method"})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(incomingEvents)
|
||||
prometheus.MustRegister(failedEvents)
|
||||
prometheus.MustRegister(successfulEventsHandlingTime)
|
||||
}
|
||||
|
||||
func reportRequestReceived(request etcdserverpb.Request) {
|
||||
incomingEvents.WithLabelValues(methodFromRequest(request)).Inc()
|
||||
}
|
||||
|
||||
func reportRequestCompleted(request etcdserverpb.Request, response etcdserver.Response, startTime time.Time) {
|
||||
method := methodFromRequest(request)
|
||||
successfulEventsHandlingTime.WithLabelValues(method).Observe(time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
func reportRequestFailed(request etcdserverpb.Request, err error) {
|
||||
method := methodFromRequest(request)
|
||||
failedEvents.WithLabelValues(method, strconv.Itoa(codeFromError(err))).Inc()
|
||||
}
|
||||
|
||||
func methodFromRequest(request etcdserverpb.Request) string {
|
||||
if request.Method == "GET" && request.Quorum {
|
||||
return "QGET"
|
||||
}
|
||||
return request.Method
|
||||
}
|
||||
|
||||
func codeFromError(err error) int {
|
||||
if err == nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
switch e := err.(type) {
|
||||
case *etcdErr.Error:
|
||||
return (*etcdErr.Error)(e).StatusCode()
|
||||
case *httptypes.HTTPError:
|
||||
return (*httptypes.HTTPError)(e).Code
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
78
vendor/github.com/coreos/etcd/etcdserver/api/v2http/peer.go
generated
vendored
Normal file
78
vendor/github.com/coreos/etcd/etcdserver/api/v2http/peer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/lease/leasehttp"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
)
|
||||
|
||||
const (
|
||||
peerMembersPrefix = "/members"
|
||||
)
|
||||
|
||||
// NewPeerHandler generates an http.Handler to handle etcd peer requests.
|
||||
func NewPeerHandler(s *etcdserver.EtcdServer) http.Handler {
|
||||
var lh http.Handler
|
||||
l := s.Lessor()
|
||||
if l != nil {
|
||||
lh = leasehttp.NewHandler(l, func() <-chan struct{} { return s.ApplyWait() })
|
||||
}
|
||||
return newPeerHandler(s.Cluster(), s.RaftHandler(), lh)
|
||||
}
|
||||
|
||||
func newPeerHandler(cluster api.Cluster, raftHandler http.Handler, leaseHandler http.Handler) http.Handler {
|
||||
mh := &peerMembersHandler{
|
||||
cluster: cluster,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", http.NotFound)
|
||||
mux.Handle(rafthttp.RaftPrefix, raftHandler)
|
||||
mux.Handle(rafthttp.RaftPrefix+"/", raftHandler)
|
||||
mux.Handle(peerMembersPrefix, mh)
|
||||
if leaseHandler != nil {
|
||||
mux.Handle(leasehttp.LeasePrefix, leaseHandler)
|
||||
mux.Handle(leasehttp.LeaseInternalPrefix, leaseHandler)
|
||||
}
|
||||
mux.HandleFunc(versionPath, versionHandler(cluster, serveVersion))
|
||||
return mux
|
||||
}
|
||||
|
||||
type peerMembersHandler struct {
|
||||
cluster api.Cluster
|
||||
}
|
||||
|
||||
func (h *peerMembersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !allowMethod(w, r.Method, "GET") {
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Etcd-Cluster-ID", h.cluster.ID().String())
|
||||
|
||||
if r.URL.Path != peerMembersPrefix {
|
||||
http.Error(w, "bad path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ms := h.cluster.Members()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(ms); err != nil {
|
||||
plog.Warningf("failed to encode members response (%v)", err)
|
||||
}
|
||||
}
|
||||
134
vendor/github.com/coreos/etcd/etcdserver/api/v2http/peer_test.go
generated
vendored
Normal file
134
vendor/github.com/coreos/etcd/etcdserver/api/v2http/peer_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v2http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
)
|
||||
|
||||
// TestNewPeerHandlerOnRaftPrefix tests that NewPeerHandler returns a handler that
|
||||
// handles raft-prefix requests well.
|
||||
func TestNewPeerHandlerOnRaftPrefix(t *testing.T) {
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("test data"))
|
||||
})
|
||||
ph := newPeerHandler(&fakeCluster{}, h, nil)
|
||||
srv := httptest.NewServer(ph)
|
||||
defer srv.Close()
|
||||
|
||||
tests := []string{
|
||||
rafthttp.RaftPrefix,
|
||||
rafthttp.RaftPrefix + "/hello",
|
||||
}
|
||||
for i, tt := range tests {
|
||||
resp, err := http.Get(srv.URL + tt)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected http.Get error: %v", err)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected ioutil.ReadAll error: %v", err)
|
||||
}
|
||||
if w := "test data"; string(body) != w {
|
||||
t.Errorf("#%d: body = %s, want %s", i, body, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeMembersFails(t *testing.T) {
|
||||
tests := []struct {
|
||||
method string
|
||||
wcode int
|
||||
}{
|
||||
{
|
||||
"POST",
|
||||
http.StatusMethodNotAllowed,
|
||||
},
|
||||
{
|
||||
"DELETE",
|
||||
http.StatusMethodNotAllowed,
|
||||
},
|
||||
{
|
||||
"BAD",
|
||||
http.StatusMethodNotAllowed,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
rw := httptest.NewRecorder()
|
||||
h := &peerMembersHandler{cluster: nil}
|
||||
h.ServeHTTP(rw, &http.Request{Method: tt.method})
|
||||
if rw.Code != tt.wcode {
|
||||
t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeMembersGet(t *testing.T) {
|
||||
memb1 := membership.Member{ID: 1, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080"}}}
|
||||
memb2 := membership.Member{ID: 2, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8081"}}}
|
||||
cluster := &fakeCluster{
|
||||
id: 1,
|
||||
members: map[uint64]*membership.Member{1: &memb1, 2: &memb2},
|
||||
}
|
||||
h := &peerMembersHandler{cluster: cluster}
|
||||
msb, err := json.Marshal([]membership.Member{memb1, memb2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wms := string(msb) + "\n"
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
wcode int
|
||||
wct string
|
||||
wbody string
|
||||
}{
|
||||
{peerMembersPrefix, http.StatusOK, "application/json", wms},
|
||||
{path.Join(peerMembersPrefix, "bad"), http.StatusBadRequest, "text/plain; charset=utf-8", "bad path\n"},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
h.ServeHTTP(rw, req)
|
||||
|
||||
if rw.Code != tt.wcode {
|
||||
t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
|
||||
}
|
||||
if gct := rw.Header().Get("Content-Type"); gct != tt.wct {
|
||||
t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct)
|
||||
}
|
||||
if rw.Body.String() != tt.wbody {
|
||||
t.Errorf("#%d: body = %s, want %s", i, rw.Body.String(), tt.wbody)
|
||||
}
|
||||
gcid := rw.Header().Get("X-Etcd-Cluster-ID")
|
||||
wcid := cluster.ID().String()
|
||||
if gcid != wcid {
|
||||
t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
vendor/github.com/coreos/etcd/etcdserver/api/v2http/testdata/ca.pem
generated
vendored
Normal file
19
vendor/github.com/coreos/etcd/etcdserver/api/v2http/testdata/ca.pem
generated
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEjCCAfqgAwIBAgIIYpX+8HgWGfkwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
|
||||
AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA1MDBaFw0yMDExMjIwMzA1MDBaMBUx
|
||||
EzARBgNVBAMTCmV0Y2QgdGVzdHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||
AoIBAQDa9PkwEwiBD8mB+VIKz5r5gRHnNF4Icj6T6R/RsdatecQe6vU0EU4FXtKZ
|
||||
drWnCGlATyrQooqHpb+rDc7CUt3mXrIxrNkcGTMaesF7P0GWxVkyOGSjJMxGBv3e
|
||||
bAZknBe4eLMi68L1aT/uYmxcp/B3L2mfdFtc1Gd6mYJpNm1PgilRyIrO0mY5ysIX
|
||||
4WHCa3yudAv8HrFbQcw7l7OyKA6uSWg6h07lE3d5jw5YOly+hz0iaRtzhb4tJrYD
|
||||
Lm1tehb0nnoLuW6yYblRSoyBVDT50MFVlyvW40Po5WkOXw/wnsnyxWRR4yqU23wq
|
||||
quQU0HXJEBLFnT+KbLOQ0EAE35vXAgMBAAGjZjBkMA4GA1UdDwEB/wQEAwIBBjAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBSbUCGB95ochDrbEZlzGGYuA7xu
|
||||
xjAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlzGGYuA7xuxjANBgkqhkiG9w0BAQsF
|
||||
AAOCAQEAardO/SGCu7Snz3YRBUinzpZEUFTFend+FJtBkxBXCao1RvTXg8PBMkza
|
||||
LUsaR4mLsGoXLIbNCoIinvVG0QULYCZe11N3l1L0G2g5uhEM4MfJ2rwrMD0o17i+
|
||||
nwNRRE3tfKAlWhYQg+4ye36kQVxASPniHjdQgjKYUFTNXdyG6DzuAclaVte9iVw6
|
||||
cWl61fB2CZya3+uMtih8t/Kgl2KbMO2PvNByfnDjKmW+v58qHbXyoJZqnpvDn14+
|
||||
p2Ox+AvvxYiEiUIvFdWy101QB7NJMCtdwq6oG6OvIOgXzLgitTFSq4kfWDfupQjW
|
||||
iFoQ+vWmYhK5ld0nBaiz+JmHuemK7A==
|
||||
-----END CERTIFICATE-----
|
||||
20
vendor/github.com/coreos/etcd/etcdserver/api/v2http/testdata/otheruser.pem
generated
vendored
Normal file
20
vendor/github.com/coreos/etcd/etcdserver/api/v2http/testdata/otheruser.pem
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDOTCCAiGgAwIBAgIINYpsso1f3SswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
|
||||
AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMBQx
|
||||
EjAQBgNVBAMTCW90aGVydXNlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBAPOAUa5GblwIjHTEnox2c/Am9jV1TMvzBuVXxnp2UnNHMNwstAooFrEs/Z+d
|
||||
ft5AOsooP6zVuM3eBQa4i9huJbVNDfPU2H94yA89jYfJYUgo7C838V6NjGsCCptQ
|
||||
WzkKPNlDbT9xA/7XpIUJ2WltuYDRrjWq8pXQONqTjcg5n4l0JO8xdHJHRUkFQ76F
|
||||
1npXeLndgGaP11lqzpYlglEGi5URhzAT1xxQ0hLSe8WNmiCxxkq++C8Gx4sPg9mX
|
||||
M94aoJDzZSnoaqDxckbP/7Q0ZKe/fVdCFkd5+jqT4Mt7hwmz9jTCHcVnAz4EKI+t
|
||||
rbWgbCfMK6013GotXz7InStVe+MCAwEAAaOBjTCBijAOBgNVHQ8BAf8EBAMCBaAw
|
||||
HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
|
||||
VR0OBBYEFFwMmf+pnaejmri6y1T+lfU+MBq/MB8GA1UdIwQYMBaAFJtQIYH3mhyE
|
||||
OtsRmXMYZi4DvG7GMAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEACOn6
|
||||
mec29MTMGPt/EPOmSyhvTKSwH+5YWjCbyUFeoB8puxrJlIphK4mvT+sXp2wzno89
|
||||
FVCliO/rJurdErKvyOjlK1QrVGPYIt7Wz9ssAfvlwCyBM8PqgEG8dJN9aAkf2h4r
|
||||
Ye+hBh1y6Nnataf7lxe9mqAOvD/7wVIgzjCnMD1q5QSY2Mln3HwVQXtbZFbY363Z
|
||||
X9Fk3PUpjJSX9jbEz9kIlT8AJAdxl6GB8Z9B8PrA8qf4Bhk15ICRHxb67EhDrGWV
|
||||
8q7ArU2XBqs/+GWpUIMoGKNZv+K+/SksZK1KnzaUvApUCJzt+ac+p8HOgMdvDRgr
|
||||
GfVVJqcZgyEmeczy0A==
|
||||
-----END CERTIFICATE-----
|
||||
20
vendor/github.com/coreos/etcd/etcdserver/api/v2http/testdata/user.pem
generated
vendored
Normal file
20
vendor/github.com/coreos/etcd/etcdserver/api/v2http/testdata/user.pem
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDNDCCAhygAwIBAgIIcQ0DAfgevocwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
|
||||
AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMA8x
|
||||
DTALBgNVBAMTBHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0
|
||||
+3Lm1SmUJJLufaFTYz+e5qyQEshNRyeAhXIeZ1aw+yBjslXGZQ3/uGOwnOnGqUeA
|
||||
Nidc9ty4NsK6RVppHlezUrBnpl4hws8vHWFKZpU2R6kKL8EYLmg+iVqEBj7XqfAp
|
||||
8bJqqZI3KOqLXpRH55mA69KP7VEK9ngTVR/tERSrUPT8jcjwbvhSOqD8Qk07BUDR
|
||||
6RpDr94Mnaf+fMGG36Sh7iUl+i4Oh6FFar+7+b0+5Bhs2/6uVsK4A1Z3jqqfSQH8
|
||||
q8Wf5h9Ka4aqGSw4ia5G3Uw7Jsl2aDgpJ7uwJo1k8SclbMYnYdhZuo+U+esY/Fai
|
||||
YdbjG+AroZ+y9TB8bMlHAgMBAAGjgY0wgYowDgYDVR0PAQH/BAQDAgWgMB0GA1Ud
|
||||
JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW
|
||||
BBRuTt0lJIVKYaz76aSxl/MQOLRwfDAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlz
|
||||
GGYuA7xuxjALBgNVHREEBDACggAwDQYJKoZIhvcNAQELBQADggEBABLRWZm+Lgjs
|
||||
c5qDXbgOJW2pR630syY8ixR9c6HvzPVJim8mFioMX+xrlbOC6BmOUlOb9j83bTKn
|
||||
aOg/0xlpxNbd8QYzgRxZmHZLULPdiNeeRvIzsrzrH88+inrmZhRXRVcHjdO6CG6t
|
||||
hCdDdRiNU6GkF7dPna0xNcEOKe2wUfzd1ZtKOqzi1w+fKjSeMplZomeWgP4WRvkh
|
||||
JJ/0ujlMMckgyTxRh8EEaJ35OnpXX7EdipoWhOMmiUnlPqye2icC8Y+CMdZsrod6
|
||||
nkoEQnXDCLf/Iv0qj7B9iKbxn7t3QDVxY4UILUReDuD8yrGULlGOl//aY/T3pkZ6
|
||||
R5trduZhI3o=
|
||||
-----END CERTIFICATE-----
|
||||
157
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/auth.go
generated
vendored
Normal file
157
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/auth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type AuthServer struct {
|
||||
authenticator etcdserver.Authenticator
|
||||
}
|
||||
|
||||
func NewAuthServer(s *etcdserver.EtcdServer) *AuthServer {
|
||||
return &AuthServer{authenticator: s}
|
||||
}
|
||||
|
||||
func (as *AuthServer) AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*pb.AuthEnableResponse, error) {
|
||||
resp, err := as.authenticator.AuthEnable(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) AuthDisable(ctx context.Context, r *pb.AuthDisableRequest) (*pb.AuthDisableResponse, error) {
|
||||
resp, err := as.authenticator.AuthDisable(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) {
|
||||
resp, err := as.authenticator.Authenticate(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) RoleAdd(ctx context.Context, r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
|
||||
resp, err := as.authenticator.RoleAdd(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) RoleDelete(ctx context.Context, r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
|
||||
resp, err := as.authenticator.RoleDelete(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) RoleGet(ctx context.Context, r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
|
||||
resp, err := as.authenticator.RoleGet(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) RoleList(ctx context.Context, r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
|
||||
resp, err := as.authenticator.RoleList(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) RoleRevokePermission(ctx context.Context, r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
|
||||
resp, err := as.authenticator.RoleRevokePermission(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) RoleGrantPermission(ctx context.Context, r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
|
||||
resp, err := as.authenticator.RoleGrantPermission(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserAdd(ctx context.Context, r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
|
||||
resp, err := as.authenticator.UserAdd(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserDelete(ctx context.Context, r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
|
||||
resp, err := as.authenticator.UserDelete(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserGet(ctx context.Context, r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
|
||||
resp, err := as.authenticator.UserGet(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserList(ctx context.Context, r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
|
||||
resp, err := as.authenticator.UserList(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserGrantRole(ctx context.Context, r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
|
||||
resp, err := as.authenticator.UserGrantRole(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserRevokeRole(ctx context.Context, r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
|
||||
resp, err := as.authenticator.UserRevokeRole(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *AuthServer) UserChangePassword(ctx context.Context, r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
|
||||
resp, err := as.authenticator.UserChangePassword(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
34
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/codec.go
generated
vendored
Normal file
34
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/codec.go
generated
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import "github.com/gogo/protobuf/proto"
|
||||
|
||||
type codec struct{}
|
||||
|
||||
func (c *codec) Marshal(v interface{}) ([]byte, error) {
|
||||
b, err := proto.Marshal(v.(proto.Message))
|
||||
sentBytes.Add(float64(len(b)))
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (c *codec) Unmarshal(data []byte, v interface{}) error {
|
||||
receivedBytes.Add(float64(len(data)))
|
||||
return proto.Unmarshal(data, v.(proto.Message))
|
||||
}
|
||||
|
||||
func (c *codec) String() string {
|
||||
return "proto"
|
||||
}
|
||||
49
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/grpc.go
generated
vendored
Normal file
49
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/grpc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
grpclog.SetLogger(plog)
|
||||
}
|
||||
|
||||
func Server(s *etcdserver.EtcdServer, tls *tls.Config) *grpc.Server {
|
||||
var opts []grpc.ServerOption
|
||||
opts = append(opts, grpc.CustomCodec(&codec{}))
|
||||
if tls != nil {
|
||||
opts = append(opts, grpc.Creds(credentials.NewTLS(tls)))
|
||||
}
|
||||
opts = append(opts, grpc.UnaryInterceptor(newUnaryInterceptor(s)))
|
||||
opts = append(opts, grpc.StreamInterceptor(newStreamInterceptor(s)))
|
||||
|
||||
grpcServer := grpc.NewServer(opts...)
|
||||
pb.RegisterKVServer(grpcServer, NewQuotaKVServer(s))
|
||||
pb.RegisterWatchServer(grpcServer, NewWatchServer(s))
|
||||
pb.RegisterLeaseServer(grpcServer, NewQuotaLeaseServer(s))
|
||||
pb.RegisterClusterServer(grpcServer, NewClusterServer(s))
|
||||
pb.RegisterAuthServer(grpcServer, NewAuthServer(s))
|
||||
pb.RegisterMaintenanceServer(grpcServer, NewMaintenanceServer(s))
|
||||
|
||||
return grpcServer
|
||||
}
|
||||
46
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/header.go
generated
vendored
Normal file
46
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/header.go
generated
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
type header struct {
|
||||
clusterID int64
|
||||
memberID int64
|
||||
raftTimer etcdserver.RaftTimer
|
||||
rev func() int64
|
||||
}
|
||||
|
||||
func newHeader(s *etcdserver.EtcdServer) header {
|
||||
return header{
|
||||
clusterID: int64(s.Cluster().ID()),
|
||||
memberID: int64(s.ID()),
|
||||
raftTimer: s,
|
||||
rev: func() int64 { return s.KV().Rev() },
|
||||
}
|
||||
}
|
||||
|
||||
// fill populates pb.ResponseHeader using etcdserver information
|
||||
func (h *header) fill(rh *pb.ResponseHeader) {
|
||||
rh.ClusterId = uint64(h.clusterID)
|
||||
rh.MemberId = uint64(h.memberID)
|
||||
rh.RaftTerm = h.raftTimer.Term()
|
||||
if rh.Revision == 0 {
|
||||
rh.Revision = h.rev()
|
||||
}
|
||||
}
|
||||
144
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/interceptor.go
generated
vendored
Normal file
144
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/interceptor.go
generated
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
|
||||
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNoLeaderCnt = 3
|
||||
)
|
||||
|
||||
type streamsMap struct {
|
||||
mu sync.Mutex
|
||||
streams map[grpc.ServerStream]struct{}
|
||||
}
|
||||
|
||||
func newUnaryInterceptor(s *etcdserver.EtcdServer) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
if !api.IsCapabilityEnabled(api.V3rpcCapability) {
|
||||
return nil, rpctypes.ErrGRPCNotCapable
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if ok {
|
||||
if ks := md[rpctypes.MetadataRequireLeaderKey]; len(ks) > 0 && ks[0] == rpctypes.MetadataHasLeader {
|
||||
if s.Leader() == types.ID(raft.None) {
|
||||
return nil, rpctypes.ErrGRPCNoLeader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prometheus.UnaryServerInterceptor(ctx, req, info, handler)
|
||||
}
|
||||
}
|
||||
|
||||
func newStreamInterceptor(s *etcdserver.EtcdServer) grpc.StreamServerInterceptor {
|
||||
smap := monitorLeader(s)
|
||||
|
||||
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
if !api.IsCapabilityEnabled(api.V3rpcCapability) {
|
||||
return rpctypes.ErrGRPCNotCapable
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ss.Context())
|
||||
if ok {
|
||||
if ks := md[rpctypes.MetadataRequireLeaderKey]; len(ks) > 0 && ks[0] == rpctypes.MetadataHasLeader {
|
||||
if s.Leader() == types.ID(raft.None) {
|
||||
return rpctypes.ErrGRPCNoLeader
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithCancel(ss.Context())
|
||||
ss = serverStreamWithCtx{ctx: cctx, cancel: &cancel, ServerStream: ss}
|
||||
|
||||
smap.mu.Lock()
|
||||
smap.streams[ss] = struct{}{}
|
||||
smap.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
smap.mu.Lock()
|
||||
delete(smap.streams, ss)
|
||||
smap.mu.Unlock()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return prometheus.StreamServerInterceptor(srv, ss, info, handler)
|
||||
}
|
||||
}
|
||||
|
||||
type serverStreamWithCtx struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
cancel *context.CancelFunc
|
||||
}
|
||||
|
||||
func (ssc serverStreamWithCtx) Context() context.Context { return ssc.ctx }
|
||||
|
||||
func monitorLeader(s *etcdserver.EtcdServer) *streamsMap {
|
||||
smap := &streamsMap{
|
||||
streams: make(map[grpc.ServerStream]struct{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
election := time.Duration(s.Cfg.TickMs) * time.Duration(s.Cfg.ElectionTicks) * time.Millisecond
|
||||
noLeaderCnt := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.StopNotify():
|
||||
return
|
||||
case <-time.After(election):
|
||||
if s.Leader() == types.ID(raft.None) {
|
||||
noLeaderCnt++
|
||||
} else {
|
||||
noLeaderCnt = 0
|
||||
}
|
||||
|
||||
// We are more conservative on canceling existing streams. Reconnecting streams
|
||||
// cost much more than just rejecting new requests. So we wait until the member
|
||||
// cannot find a leader for maxNoLeaderCnt election timeouts to cancel existing streams.
|
||||
if noLeaderCnt >= maxNoLeaderCnt {
|
||||
smap.mu.Lock()
|
||||
for ss := range smap.streams {
|
||||
if ssWithCtx, ok := ss.(serverStreamWithCtx); ok {
|
||||
(*ssWithCtx.cancel)()
|
||||
<-ss.Context().Done()
|
||||
}
|
||||
}
|
||||
smap.streams = make(map[grpc.ServerStream]struct{})
|
||||
smap.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return smap
|
||||
}
|
||||
253
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/key.go
generated
vendored
Normal file
253
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/key.go
generated
vendored
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package v3rpc implements etcd v3 RPC system based on gRPC.
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/api/v3rpc")
|
||||
|
||||
// Max operations per txn list. For example, Txn.Success can have at most 128 operations,
|
||||
// and Txn.Failure can have at most 128 operations.
|
||||
MaxOpsPerTxn = 128
|
||||
)
|
||||
|
||||
type kvServer struct {
|
||||
hdr header
|
||||
kv etcdserver.RaftKV
|
||||
}
|
||||
|
||||
func NewKVServer(s *etcdserver.EtcdServer) pb.KVServer {
|
||||
return &kvServer{hdr: newHeader(s), kv: s}
|
||||
}
|
||||
|
||||
func (s *kvServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) {
|
||||
if err := checkRangeRequest(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.kv.Range(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
|
||||
if resp.Header == nil {
|
||||
plog.Panic("unexpected nil resp.Header")
|
||||
}
|
||||
s.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *kvServer) Put(ctx context.Context, r *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
if err := checkPutRequest(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.kv.Put(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
|
||||
if resp.Header == nil {
|
||||
plog.Panic("unexpected nil resp.Header")
|
||||
}
|
||||
s.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *kvServer) DeleteRange(ctx context.Context, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
|
||||
if err := checkDeleteRequest(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.kv.DeleteRange(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
|
||||
if resp.Header == nil {
|
||||
plog.Panic("unexpected nil resp.Header")
|
||||
}
|
||||
s.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *kvServer) Txn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
if err := checkTxnRequest(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.kv.Txn(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
|
||||
if resp.Header == nil {
|
||||
plog.Panic("unexpected nil resp.Header")
|
||||
}
|
||||
s.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *kvServer) Compact(ctx context.Context, r *pb.CompactionRequest) (*pb.CompactionResponse, error) {
|
||||
resp, err := s.kv.Compact(ctx, r)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
|
||||
if resp.Header == nil {
|
||||
plog.Panic("unexpected nil resp.Header")
|
||||
}
|
||||
s.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func checkRangeRequest(r *pb.RangeRequest) error {
|
||||
if len(r.Key) == 0 {
|
||||
return rpctypes.ErrGRPCEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPutRequest(r *pb.PutRequest) error {
|
||||
if len(r.Key) == 0 {
|
||||
return rpctypes.ErrGRPCEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDeleteRequest(r *pb.DeleteRangeRequest) error {
|
||||
if len(r.Key) == 0 {
|
||||
return rpctypes.ErrGRPCEmptyKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkTxnRequest(r *pb.TxnRequest) error {
|
||||
if len(r.Compare) > MaxOpsPerTxn || len(r.Success) > MaxOpsPerTxn || len(r.Failure) > MaxOpsPerTxn {
|
||||
return rpctypes.ErrGRPCTooManyOps
|
||||
}
|
||||
|
||||
for _, c := range r.Compare {
|
||||
if len(c.Key) == 0 {
|
||||
return rpctypes.ErrGRPCEmptyKey
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range r.Success {
|
||||
if err := checkRequestOp(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := checkRequestDupKeys(r.Success); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, u := range r.Failure {
|
||||
if err := checkRequestOp(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return checkRequestDupKeys(r.Failure)
|
||||
}
|
||||
|
||||
// checkRequestDupKeys gives rpctypes.ErrGRPCDuplicateKey if the same key is modified twice
|
||||
func checkRequestDupKeys(reqs []*pb.RequestOp) error {
|
||||
// check put overlap
|
||||
keys := make(map[string]struct{})
|
||||
for _, requ := range reqs {
|
||||
tv, ok := requ.Request.(*pb.RequestOp_RequestPut)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
preq := tv.RequestPut
|
||||
if preq == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := keys[string(preq.Key)]; ok {
|
||||
return rpctypes.ErrGRPCDuplicateKey
|
||||
}
|
||||
keys[string(preq.Key)] = struct{}{}
|
||||
}
|
||||
|
||||
// no need to check deletes if no puts; delete overlaps are permitted
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sort keys for range checking
|
||||
sortedKeys := []string{}
|
||||
for k := range keys {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
|
||||
// check put overlap with deletes
|
||||
for _, requ := range reqs {
|
||||
tv, ok := requ.Request.(*pb.RequestOp_RequestDeleteRange)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dreq := tv.RequestDeleteRange
|
||||
if dreq == nil {
|
||||
continue
|
||||
}
|
||||
if dreq.RangeEnd == nil {
|
||||
if _, found := keys[string(dreq.Key)]; found {
|
||||
return rpctypes.ErrGRPCDuplicateKey
|
||||
}
|
||||
} else {
|
||||
lo := sort.SearchStrings(sortedKeys, string(dreq.Key))
|
||||
hi := sort.SearchStrings(sortedKeys, string(dreq.RangeEnd))
|
||||
if lo != hi {
|
||||
// element between lo and hi => overlap
|
||||
return rpctypes.ErrGRPCDuplicateKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkRequestOp(u *pb.RequestOp) error {
|
||||
// TODO: ensure only one of the field is set.
|
||||
switch uv := u.Request.(type) {
|
||||
case *pb.RequestOp_RequestRange:
|
||||
if uv.RequestRange != nil {
|
||||
return checkRangeRequest(uv.RequestRange)
|
||||
}
|
||||
case *pb.RequestOp_RequestPut:
|
||||
if uv.RequestPut != nil {
|
||||
return checkPutRequest(uv.RequestPut)
|
||||
}
|
||||
case *pb.RequestOp_RequestDeleteRange:
|
||||
if uv.RequestDeleteRange != nil {
|
||||
return checkDeleteRequest(uv.RequestDeleteRange)
|
||||
}
|
||||
default:
|
||||
// empty op
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
98
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/lease.go
generated
vendored
Normal file
98
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/lease.go
generated
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/lease"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type LeaseServer struct {
|
||||
hdr header
|
||||
le etcdserver.Lessor
|
||||
}
|
||||
|
||||
func NewLeaseServer(s *etcdserver.EtcdServer) pb.LeaseServer {
|
||||
return &LeaseServer{le: s, hdr: newHeader(s)}
|
||||
}
|
||||
|
||||
func (ls *LeaseServer) LeaseGrant(ctx context.Context, cr *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
|
||||
resp, err := ls.le.LeaseGrant(ctx, cr)
|
||||
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
ls.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (ls *LeaseServer) LeaseRevoke(ctx context.Context, rr *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
|
||||
resp, err := ls.le.LeaseRevoke(ctx, rr)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
ls.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (ls *LeaseServer) LeaseTimeToLive(ctx context.Context, rr *pb.LeaseTimeToLiveRequest) (*pb.LeaseTimeToLiveResponse, error) {
|
||||
resp, err := ls.le.LeaseTimeToLive(ctx, rr)
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
ls.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (ls *LeaseServer) LeaseKeepAlive(stream pb.Lease_LeaseKeepAliveServer) error {
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create header before we sent out the renew request.
|
||||
// This can make sure that the revision is strictly smaller or equal to
|
||||
// when the keepalive happened at the local server (when the local server is the leader)
|
||||
// or remote leader.
|
||||
// Without this, a lease might be revoked at rev 3 but client can see the keepalive succeeded
|
||||
// at rev 4.
|
||||
resp := &pb.LeaseKeepAliveResponse{ID: req.ID, Header: &pb.ResponseHeader{}}
|
||||
ls.hdr.fill(resp.Header)
|
||||
|
||||
ttl, err := ls.le.LeaseRenew(stream.Context(), lease.LeaseID(req.ID))
|
||||
if err == lease.ErrLeaseNotFound {
|
||||
err = nil
|
||||
ttl = 0
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return togRPCError(err)
|
||||
}
|
||||
|
||||
resp.TTL = ttl
|
||||
err = stream.Send(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
193
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/maintenance.go
generated
vendored
Normal file
193
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/maintenance.go
generated
vendored
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
|
||||
"github.com/coreos/etcd/auth"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/version"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type KVGetter interface {
|
||||
KV() mvcc.ConsistentWatchableKV
|
||||
}
|
||||
|
||||
type BackendGetter interface {
|
||||
Backend() backend.Backend
|
||||
}
|
||||
|
||||
type Alarmer interface {
|
||||
Alarm(ctx context.Context, ar *pb.AlarmRequest) (*pb.AlarmResponse, error)
|
||||
}
|
||||
|
||||
type RaftStatusGetter interface {
|
||||
Index() uint64
|
||||
Term() uint64
|
||||
Leader() types.ID
|
||||
}
|
||||
|
||||
type AuthGetter interface {
|
||||
AuthStore() auth.AuthStore
|
||||
}
|
||||
|
||||
type maintenanceServer struct {
|
||||
rg RaftStatusGetter
|
||||
kg KVGetter
|
||||
bg BackendGetter
|
||||
a Alarmer
|
||||
hdr header
|
||||
}
|
||||
|
||||
func NewMaintenanceServer(s *etcdserver.EtcdServer) pb.MaintenanceServer {
|
||||
srv := &maintenanceServer{rg: s, kg: s, bg: s, a: s, hdr: newHeader(s)}
|
||||
return &authMaintenanceServer{srv, s}
|
||||
}
|
||||
|
||||
func (ms *maintenanceServer) Defragment(ctx context.Context, sr *pb.DefragmentRequest) (*pb.DefragmentResponse, error) {
|
||||
plog.Noticef("starting to defragment the storage backend...")
|
||||
err := ms.bg.Backend().Defrag()
|
||||
if err != nil {
|
||||
plog.Errorf("failed to defragment the storage backend (%v)", err)
|
||||
return nil, err
|
||||
}
|
||||
plog.Noticef("finished defragmenting the storage backend")
|
||||
return &pb.DefragmentResponse{}, nil
|
||||
}
|
||||
|
||||
func (ms *maintenanceServer) Snapshot(sr *pb.SnapshotRequest, srv pb.Maintenance_SnapshotServer) error {
|
||||
snap := ms.bg.Backend().Snapshot()
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
defer pr.Close()
|
||||
|
||||
go func() {
|
||||
snap.WriteTo(pw)
|
||||
if err := snap.Close(); err != nil {
|
||||
plog.Errorf("error closing snapshot (%v)", err)
|
||||
}
|
||||
pw.Close()
|
||||
}()
|
||||
|
||||
// send file data
|
||||
h := sha256.New()
|
||||
br := int64(0)
|
||||
buf := make([]byte, 32*1024)
|
||||
sz := snap.Size()
|
||||
for br < sz {
|
||||
n, err := io.ReadFull(pr, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
return togRPCError(err)
|
||||
}
|
||||
br += int64(n)
|
||||
resp := &pb.SnapshotResponse{
|
||||
RemainingBytes: uint64(sz - br),
|
||||
Blob: buf[:n],
|
||||
}
|
||||
if err = srv.Send(resp); err != nil {
|
||||
return togRPCError(err)
|
||||
}
|
||||
h.Write(buf[:n])
|
||||
}
|
||||
|
||||
// send sha
|
||||
sha := h.Sum(nil)
|
||||
hresp := &pb.SnapshotResponse{RemainingBytes: 0, Blob: sha}
|
||||
if err := srv.Send(hresp); err != nil {
|
||||
return togRPCError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *maintenanceServer) Hash(ctx context.Context, r *pb.HashRequest) (*pb.HashResponse, error) {
|
||||
h, rev, err := ms.kg.KV().Hash()
|
||||
if err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
resp := &pb.HashResponse{Header: &pb.ResponseHeader{Revision: rev}, Hash: h}
|
||||
ms.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (ms *maintenanceServer) Alarm(ctx context.Context, ar *pb.AlarmRequest) (*pb.AlarmResponse, error) {
|
||||
return ms.a.Alarm(ctx, ar)
|
||||
}
|
||||
|
||||
func (ms *maintenanceServer) Status(ctx context.Context, ar *pb.StatusRequest) (*pb.StatusResponse, error) {
|
||||
resp := &pb.StatusResponse{
|
||||
Header: &pb.ResponseHeader{Revision: ms.hdr.rev()},
|
||||
Version: version.Version,
|
||||
DbSize: ms.bg.Backend().Size(),
|
||||
Leader: uint64(ms.rg.Leader()),
|
||||
RaftIndex: ms.rg.Index(),
|
||||
RaftTerm: ms.rg.Term(),
|
||||
}
|
||||
ms.hdr.fill(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type authMaintenanceServer struct {
|
||||
*maintenanceServer
|
||||
ag AuthGetter
|
||||
}
|
||||
|
||||
func (ams *authMaintenanceServer) isAuthenticated(ctx context.Context) error {
|
||||
authInfo, err := ams.ag.AuthStore().AuthInfoFromCtx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ams.ag.AuthStore().IsAdminPermitted(authInfo)
|
||||
}
|
||||
|
||||
func (ams *authMaintenanceServer) Defragment(ctx context.Context, sr *pb.DefragmentRequest) (*pb.DefragmentResponse, error) {
|
||||
if err := ams.isAuthenticated(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ams.maintenanceServer.Defragment(ctx, sr)
|
||||
}
|
||||
|
||||
func (ams *authMaintenanceServer) Snapshot(sr *pb.SnapshotRequest, srv pb.Maintenance_SnapshotServer) error {
|
||||
if err := ams.isAuthenticated(srv.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ams.maintenanceServer.Snapshot(sr, srv)
|
||||
}
|
||||
|
||||
func (ams *authMaintenanceServer) Hash(ctx context.Context, r *pb.HashRequest) (*pb.HashResponse, error) {
|
||||
if err := ams.isAuthenticated(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ams.maintenanceServer.Hash(ctx, r)
|
||||
}
|
||||
|
||||
func (ams *authMaintenanceServer) Status(ctx context.Context, ar *pb.StatusRequest) (*pb.StatusResponse, error) {
|
||||
if err := ams.isAuthenticated(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ams.maintenanceServer.Status(ctx, ar)
|
||||
}
|
||||
97
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/member.go
generated
vendored
Normal file
97
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/member.go
generated
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type ClusterServer struct {
|
||||
cluster api.Cluster
|
||||
server etcdserver.Server
|
||||
raftTimer etcdserver.RaftTimer
|
||||
}
|
||||
|
||||
func NewClusterServer(s *etcdserver.EtcdServer) *ClusterServer {
|
||||
return &ClusterServer{
|
||||
cluster: s.Cluster(),
|
||||
server: s,
|
||||
raftTimer: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *ClusterServer) MemberAdd(ctx context.Context, r *pb.MemberAddRequest) (*pb.MemberAddResponse, error) {
|
||||
urls, err := types.NewURLs(r.PeerURLs)
|
||||
if err != nil {
|
||||
return nil, rpctypes.ErrGRPCMemberBadURLs
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
m := membership.NewMember("", urls, "", &now)
|
||||
if err = cs.server.AddMember(ctx, *m); err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
|
||||
return &pb.MemberAddResponse{
|
||||
Header: cs.header(),
|
||||
Member: &pb.Member{ID: uint64(m.ID), PeerURLs: m.PeerURLs},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cs *ClusterServer) MemberRemove(ctx context.Context, r *pb.MemberRemoveRequest) (*pb.MemberRemoveResponse, error) {
|
||||
if err := cs.server.RemoveMember(ctx, r.ID); err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return &pb.MemberRemoveResponse{Header: cs.header()}, nil
|
||||
}
|
||||
|
||||
func (cs *ClusterServer) MemberUpdate(ctx context.Context, r *pb.MemberUpdateRequest) (*pb.MemberUpdateResponse, error) {
|
||||
m := membership.Member{
|
||||
ID: types.ID(r.ID),
|
||||
RaftAttributes: membership.RaftAttributes{PeerURLs: r.PeerURLs},
|
||||
}
|
||||
if err := cs.server.UpdateMember(ctx, m); err != nil {
|
||||
return nil, togRPCError(err)
|
||||
}
|
||||
return &pb.MemberUpdateResponse{Header: cs.header()}, nil
|
||||
}
|
||||
|
||||
func (cs *ClusterServer) MemberList(ctx context.Context, r *pb.MemberListRequest) (*pb.MemberListResponse, error) {
|
||||
membs := cs.cluster.Members()
|
||||
|
||||
protoMembs := make([]*pb.Member, len(membs))
|
||||
for i := range membs {
|
||||
protoMembs[i] = &pb.Member{
|
||||
Name: membs[i].Name,
|
||||
ID: uint64(membs[i].ID),
|
||||
PeerURLs: membs[i].PeerURLs,
|
||||
ClientURLs: membs[i].ClientURLs,
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.MemberListResponse{Header: cs.header(), Members: protoMembs}, nil
|
||||
}
|
||||
|
||||
func (cs *ClusterServer) header() *pb.ResponseHeader {
|
||||
return &pb.ResponseHeader{ClusterId: uint64(cs.cluster.ID()), MemberId: uint64(cs.server.ID()), RaftTerm: cs.raftTimer.Term()}
|
||||
}
|
||||
38
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/metrics.go
generated
vendored
Normal file
38
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/metrics.go
generated
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
sentBytes = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "network",
|
||||
Name: "client_grpc_sent_bytes_total",
|
||||
Help: "The total number of bytes sent to grpc clients.",
|
||||
})
|
||||
|
||||
receivedBytes = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "network",
|
||||
Name: "client_grpc_received_bytes_total",
|
||||
Help: "The total number of bytes received from grpc clients.",
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(sentBytes)
|
||||
prometheus.MustRegister(receivedBytes)
|
||||
}
|
||||
89
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/quota.go
generated
vendored
Normal file
89
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/quota.go
generated
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type quotaKVServer struct {
|
||||
pb.KVServer
|
||||
qa quotaAlarmer
|
||||
}
|
||||
|
||||
type quotaAlarmer struct {
|
||||
q etcdserver.Quota
|
||||
a Alarmer
|
||||
id types.ID
|
||||
}
|
||||
|
||||
// check whether request satisfies the quota. If there is not enough space,
|
||||
// ignore request and raise the free space alarm.
|
||||
func (qa *quotaAlarmer) check(ctx context.Context, r interface{}) error {
|
||||
if qa.q.Available(r) {
|
||||
return nil
|
||||
}
|
||||
req := &pb.AlarmRequest{
|
||||
MemberID: uint64(qa.id),
|
||||
Action: pb.AlarmRequest_ACTIVATE,
|
||||
Alarm: pb.AlarmType_NOSPACE,
|
||||
}
|
||||
qa.a.Alarm(ctx, req)
|
||||
return rpctypes.ErrGRPCNoSpace
|
||||
}
|
||||
|
||||
func NewQuotaKVServer(s *etcdserver.EtcdServer) pb.KVServer {
|
||||
return "aKVServer{
|
||||
NewKVServer(s),
|
||||
quotaAlarmer{etcdserver.NewBackendQuota(s), s, s.ID()},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *quotaKVServer) Put(ctx context.Context, r *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
if err := s.qa.check(ctx, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.KVServer.Put(ctx, r)
|
||||
}
|
||||
|
||||
func (s *quotaKVServer) Txn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
if err := s.qa.check(ctx, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.KVServer.Txn(ctx, r)
|
||||
}
|
||||
|
||||
type quotaLeaseServer struct {
|
||||
pb.LeaseServer
|
||||
qa quotaAlarmer
|
||||
}
|
||||
|
||||
func (s *quotaLeaseServer) LeaseGrant(ctx context.Context, cr *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
|
||||
if err := s.qa.check(ctx, cr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.LeaseServer.LeaseGrant(ctx, cr)
|
||||
}
|
||||
|
||||
func NewQuotaLeaseServer(s *etcdserver.EtcdServer) pb.LeaseServer {
|
||||
return "aLeaseServer{
|
||||
NewLeaseServer(s),
|
||||
quotaAlarmer{etcdserver.NewBackendQuota(s), s, s.ID()},
|
||||
}
|
||||
}
|
||||
16
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/doc.go
generated
vendored
Normal file
16
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package rpctypes has types and values shared by the etcd server and client for v3 RPC interaction.
|
||||
package rpctypes
|
||||
177
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/error.go
generated
vendored
Normal file
177
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/error.go
generated
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rpctypes
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
// server-side error
|
||||
ErrGRPCEmptyKey = grpc.Errorf(codes.InvalidArgument, "etcdserver: key is not provided")
|
||||
ErrGRPCTooManyOps = grpc.Errorf(codes.InvalidArgument, "etcdserver: too many operations in txn request")
|
||||
ErrGRPCDuplicateKey = grpc.Errorf(codes.InvalidArgument, "etcdserver: duplicate key given in txn request")
|
||||
ErrGRPCCompacted = grpc.Errorf(codes.OutOfRange, "etcdserver: mvcc: required revision has been compacted")
|
||||
ErrGRPCFutureRev = grpc.Errorf(codes.OutOfRange, "etcdserver: mvcc: required revision is a future revision")
|
||||
ErrGRPCNoSpace = grpc.Errorf(codes.ResourceExhausted, "etcdserver: mvcc: database space exceeded")
|
||||
|
||||
ErrGRPCLeaseNotFound = grpc.Errorf(codes.NotFound, "etcdserver: requested lease not found")
|
||||
ErrGRPCLeaseExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: lease already exists")
|
||||
|
||||
ErrGRPCMemberExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: member ID already exist")
|
||||
ErrGRPCPeerURLExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: Peer URLs already exists")
|
||||
ErrGRPCMemberNotEnoughStarted = grpc.Errorf(codes.FailedPrecondition, "etcdserver: re-configuration failed due to not enough started members")
|
||||
ErrGRPCMemberBadURLs = grpc.Errorf(codes.InvalidArgument, "etcdserver: given member URLs are invalid")
|
||||
ErrGRPCMemberNotFound = grpc.Errorf(codes.NotFound, "etcdserver: member not found")
|
||||
|
||||
ErrGRPCRequestTooLarge = grpc.Errorf(codes.InvalidArgument, "etcdserver: request is too large")
|
||||
ErrGRPCRequestTooManyRequests = grpc.Errorf(codes.ResourceExhausted, "etcdserver: too many requests")
|
||||
|
||||
ErrGRPCRootUserNotExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: root user does not exist")
|
||||
ErrGRPCRootRoleNotExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: root user does not have root role")
|
||||
ErrGRPCUserAlreadyExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: user name already exists")
|
||||
ErrGRPCUserEmpty = grpc.Errorf(codes.InvalidArgument, "etcdserver: user name is empty")
|
||||
ErrGRPCUserNotFound = grpc.Errorf(codes.FailedPrecondition, "etcdserver: user name not found")
|
||||
ErrGRPCRoleAlreadyExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role name already exists")
|
||||
ErrGRPCRoleNotFound = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role name not found")
|
||||
ErrGRPCAuthFailed = grpc.Errorf(codes.InvalidArgument, "etcdserver: authentication failed, invalid user ID or password")
|
||||
ErrGRPCPermissionDenied = grpc.Errorf(codes.PermissionDenied, "etcdserver: permission denied")
|
||||
ErrGRPCRoleNotGranted = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role is not granted to the user")
|
||||
ErrGRPCPermissionNotGranted = grpc.Errorf(codes.FailedPrecondition, "etcdserver: permission is not granted to the role")
|
||||
ErrGRPCAuthNotEnabled = grpc.Errorf(codes.FailedPrecondition, "etcdserver: authentication is not enabled")
|
||||
ErrGRPCInvalidAuthToken = grpc.Errorf(codes.Unauthenticated, "etcdserver: invalid auth token")
|
||||
|
||||
ErrGRPCNoLeader = grpc.Errorf(codes.Unavailable, "etcdserver: no leader")
|
||||
ErrGRPCNotCapable = grpc.Errorf(codes.Unavailable, "etcdserver: not capable")
|
||||
ErrGRPCStopped = grpc.Errorf(codes.Unavailable, "etcdserver: server stopped")
|
||||
ErrGRPCTimeout = grpc.Errorf(codes.Unavailable, "etcdserver: request timed out")
|
||||
ErrGRPCTimeoutDueToLeaderFail = grpc.Errorf(codes.Unavailable, "etcdserver: request timed out, possibly due to previous leader failure")
|
||||
ErrGRPCTimeoutDueToConnectionLost = grpc.Errorf(codes.Unavailable, "etcdserver: request timed out, possibly due to connection lost")
|
||||
ErrGRPCUnhealthy = grpc.Errorf(codes.Unavailable, "etcdserver: unhealthy cluster")
|
||||
|
||||
errStringToError = map[string]error{
|
||||
grpc.ErrorDesc(ErrGRPCEmptyKey): ErrGRPCEmptyKey,
|
||||
grpc.ErrorDesc(ErrGRPCTooManyOps): ErrGRPCTooManyOps,
|
||||
grpc.ErrorDesc(ErrGRPCDuplicateKey): ErrGRPCDuplicateKey,
|
||||
grpc.ErrorDesc(ErrGRPCCompacted): ErrGRPCCompacted,
|
||||
grpc.ErrorDesc(ErrGRPCFutureRev): ErrGRPCFutureRev,
|
||||
grpc.ErrorDesc(ErrGRPCNoSpace): ErrGRPCNoSpace,
|
||||
|
||||
grpc.ErrorDesc(ErrGRPCLeaseNotFound): ErrGRPCLeaseNotFound,
|
||||
grpc.ErrorDesc(ErrGRPCLeaseExist): ErrGRPCLeaseExist,
|
||||
|
||||
grpc.ErrorDesc(ErrGRPCMemberExist): ErrGRPCMemberExist,
|
||||
grpc.ErrorDesc(ErrGRPCPeerURLExist): ErrGRPCPeerURLExist,
|
||||
grpc.ErrorDesc(ErrGRPCMemberNotEnoughStarted): ErrGRPCMemberNotEnoughStarted,
|
||||
grpc.ErrorDesc(ErrGRPCMemberBadURLs): ErrGRPCMemberBadURLs,
|
||||
grpc.ErrorDesc(ErrGRPCMemberNotFound): ErrGRPCMemberNotFound,
|
||||
|
||||
grpc.ErrorDesc(ErrGRPCRequestTooLarge): ErrGRPCRequestTooLarge,
|
||||
grpc.ErrorDesc(ErrGRPCRequestTooManyRequests): ErrGRPCRequestTooManyRequests,
|
||||
|
||||
grpc.ErrorDesc(ErrGRPCRootUserNotExist): ErrGRPCRootUserNotExist,
|
||||
grpc.ErrorDesc(ErrGRPCRootRoleNotExist): ErrGRPCRootRoleNotExist,
|
||||
grpc.ErrorDesc(ErrGRPCUserAlreadyExist): ErrGRPCUserAlreadyExist,
|
||||
grpc.ErrorDesc(ErrGRPCUserEmpty): ErrGRPCUserEmpty,
|
||||
grpc.ErrorDesc(ErrGRPCUserNotFound): ErrGRPCUserNotFound,
|
||||
grpc.ErrorDesc(ErrGRPCRoleAlreadyExist): ErrGRPCRoleAlreadyExist,
|
||||
grpc.ErrorDesc(ErrGRPCRoleNotFound): ErrGRPCRoleNotFound,
|
||||
grpc.ErrorDesc(ErrGRPCAuthFailed): ErrGRPCAuthFailed,
|
||||
grpc.ErrorDesc(ErrGRPCPermissionDenied): ErrGRPCPermissionDenied,
|
||||
grpc.ErrorDesc(ErrGRPCRoleNotGranted): ErrGRPCRoleNotGranted,
|
||||
grpc.ErrorDesc(ErrGRPCPermissionNotGranted): ErrGRPCPermissionNotGranted,
|
||||
grpc.ErrorDesc(ErrGRPCAuthNotEnabled): ErrGRPCAuthNotEnabled,
|
||||
grpc.ErrorDesc(ErrGRPCInvalidAuthToken): ErrGRPCInvalidAuthToken,
|
||||
|
||||
grpc.ErrorDesc(ErrGRPCNoLeader): ErrGRPCNoLeader,
|
||||
grpc.ErrorDesc(ErrGRPCNotCapable): ErrGRPCNotCapable,
|
||||
grpc.ErrorDesc(ErrGRPCStopped): ErrGRPCStopped,
|
||||
grpc.ErrorDesc(ErrGRPCTimeout): ErrGRPCTimeout,
|
||||
grpc.ErrorDesc(ErrGRPCTimeoutDueToLeaderFail): ErrGRPCTimeoutDueToLeaderFail,
|
||||
grpc.ErrorDesc(ErrGRPCTimeoutDueToConnectionLost): ErrGRPCTimeoutDueToConnectionLost,
|
||||
grpc.ErrorDesc(ErrGRPCUnhealthy): ErrGRPCUnhealthy,
|
||||
}
|
||||
|
||||
// client-side error
|
||||
ErrEmptyKey = Error(ErrGRPCEmptyKey)
|
||||
ErrTooManyOps = Error(ErrGRPCTooManyOps)
|
||||
ErrDuplicateKey = Error(ErrGRPCDuplicateKey)
|
||||
ErrCompacted = Error(ErrGRPCCompacted)
|
||||
ErrFutureRev = Error(ErrGRPCFutureRev)
|
||||
ErrNoSpace = Error(ErrGRPCNoSpace)
|
||||
|
||||
ErrLeaseNotFound = Error(ErrGRPCLeaseNotFound)
|
||||
ErrLeaseExist = Error(ErrGRPCLeaseExist)
|
||||
|
||||
ErrMemberExist = Error(ErrGRPCMemberExist)
|
||||
ErrPeerURLExist = Error(ErrGRPCPeerURLExist)
|
||||
ErrMemberNotEnoughStarted = Error(ErrGRPCMemberNotEnoughStarted)
|
||||
ErrMemberBadURLs = Error(ErrGRPCMemberBadURLs)
|
||||
ErrMemberNotFound = Error(ErrGRPCMemberNotFound)
|
||||
|
||||
ErrRequestTooLarge = Error(ErrGRPCRequestTooLarge)
|
||||
ErrTooManyRequests = Error(ErrGRPCRequestTooManyRequests)
|
||||
|
||||
ErrRootUserNotExist = Error(ErrGRPCRootUserNotExist)
|
||||
ErrRootRoleNotExist = Error(ErrGRPCRootRoleNotExist)
|
||||
ErrUserAlreadyExist = Error(ErrGRPCUserAlreadyExist)
|
||||
ErrUserEmpty = Error(ErrGRPCUserEmpty)
|
||||
ErrUserNotFound = Error(ErrGRPCUserNotFound)
|
||||
ErrRoleAlreadyExist = Error(ErrGRPCRoleAlreadyExist)
|
||||
ErrRoleNotFound = Error(ErrGRPCRoleNotFound)
|
||||
ErrAuthFailed = Error(ErrGRPCAuthFailed)
|
||||
ErrPermissionDenied = Error(ErrGRPCPermissionDenied)
|
||||
ErrRoleNotGranted = Error(ErrGRPCRoleNotGranted)
|
||||
ErrPermissionNotGranted = Error(ErrGRPCPermissionNotGranted)
|
||||
ErrAuthNotEnabled = Error(ErrGRPCAuthNotEnabled)
|
||||
ErrInvalidAuthToken = Error(ErrGRPCInvalidAuthToken)
|
||||
|
||||
ErrNoLeader = Error(ErrGRPCNoLeader)
|
||||
ErrNotCapable = Error(ErrGRPCNotCapable)
|
||||
ErrStopped = Error(ErrGRPCStopped)
|
||||
ErrTimeout = Error(ErrGRPCTimeout)
|
||||
ErrTimeoutDueToLeaderFail = Error(ErrGRPCTimeoutDueToLeaderFail)
|
||||
ErrTimeoutDueToConnectionLost = Error(ErrGRPCTimeoutDueToConnectionLost)
|
||||
ErrUnhealthy = Error(ErrGRPCUnhealthy)
|
||||
)
|
||||
|
||||
// EtcdError defines gRPC server errors.
|
||||
// (https://github.com/grpc/grpc-go/blob/master/rpc_util.go#L319-L323)
|
||||
type EtcdError struct {
|
||||
code codes.Code
|
||||
desc string
|
||||
}
|
||||
|
||||
// Code returns grpc/codes.Code.
|
||||
// TODO: define clientv3/codes.Code.
|
||||
func (e EtcdError) Code() codes.Code {
|
||||
return e.code
|
||||
}
|
||||
|
||||
func (e EtcdError) Error() string {
|
||||
return e.desc
|
||||
}
|
||||
|
||||
func Error(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
verr, ok := errStringToError[grpc.ErrorDesc(err)]
|
||||
if !ok { // not gRPC error
|
||||
return err
|
||||
}
|
||||
return EtcdError{code: grpc.Code(verr), desc: grpc.ErrorDesc(verr)}
|
||||
}
|
||||
42
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/error_test.go
generated
vendored
Normal file
42
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/error_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rpctypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
e1 := grpc.Errorf(codes.InvalidArgument, "etcdserver: key is not provided")
|
||||
e2 := ErrGRPCEmptyKey
|
||||
e3 := ErrEmptyKey
|
||||
|
||||
if e1.Error() != e2.Error() {
|
||||
t.Fatalf("expected %q == %q", e1.Error(), e2.Error())
|
||||
}
|
||||
if grpc.Code(e1) != e3.(EtcdError).Code() {
|
||||
t.Fatalf("expected them to be equal, got %v / %v", grpc.Code(e1), e3.(EtcdError).Code())
|
||||
}
|
||||
|
||||
if e1.Error() == e3.Error() {
|
||||
t.Fatalf("expected %q != %q", e1.Error(), e3.Error())
|
||||
}
|
||||
if grpc.Code(e2) != e3.(EtcdError).Code() {
|
||||
t.Fatalf("expected them to be equal, got %v / %v", grpc.Code(e2), e3.(EtcdError).Code())
|
||||
}
|
||||
}
|
||||
20
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/md.go
generated
vendored
Normal file
20
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/md.go
generated
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rpctypes
|
||||
|
||||
var (
|
||||
MetadataRequireLeaderKey = "hasleader"
|
||||
MetadataHasLeader = "true"
|
||||
)
|
||||
101
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/util.go
generated
vendored
Normal file
101
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/util.go
generated
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/auth"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/lease"
|
||||
"github.com/coreos/etcd/mvcc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func togRPCError(err error) error {
|
||||
switch err {
|
||||
case membership.ErrIDRemoved:
|
||||
return rpctypes.ErrGRPCMemberNotFound
|
||||
case membership.ErrIDNotFound:
|
||||
return rpctypes.ErrGRPCMemberNotFound
|
||||
case membership.ErrIDExists:
|
||||
return rpctypes.ErrGRPCMemberExist
|
||||
case membership.ErrPeerURLexists:
|
||||
return rpctypes.ErrGRPCPeerURLExist
|
||||
case etcdserver.ErrNotEnoughStartedMembers:
|
||||
return rpctypes.ErrMemberNotEnoughStarted
|
||||
|
||||
case mvcc.ErrCompacted:
|
||||
return rpctypes.ErrGRPCCompacted
|
||||
case mvcc.ErrFutureRev:
|
||||
return rpctypes.ErrGRPCFutureRev
|
||||
case lease.ErrLeaseNotFound:
|
||||
return rpctypes.ErrGRPCLeaseNotFound
|
||||
case etcdserver.ErrRequestTooLarge:
|
||||
return rpctypes.ErrGRPCRequestTooLarge
|
||||
case etcdserver.ErrNoSpace:
|
||||
return rpctypes.ErrGRPCNoSpace
|
||||
case etcdserver.ErrTooManyRequests:
|
||||
return rpctypes.ErrTooManyRequests
|
||||
|
||||
case etcdserver.ErrNoLeader:
|
||||
return rpctypes.ErrGRPCNoLeader
|
||||
case etcdserver.ErrStopped:
|
||||
return rpctypes.ErrGRPCStopped
|
||||
case etcdserver.ErrTimeout:
|
||||
return rpctypes.ErrGRPCTimeout
|
||||
case etcdserver.ErrTimeoutDueToLeaderFail:
|
||||
return rpctypes.ErrGRPCTimeoutDueToLeaderFail
|
||||
case etcdserver.ErrTimeoutDueToConnectionLost:
|
||||
return rpctypes.ErrGRPCTimeoutDueToConnectionLost
|
||||
case etcdserver.ErrUnhealthy:
|
||||
return rpctypes.ErrGRPCUnhealthy
|
||||
|
||||
case lease.ErrLeaseNotFound:
|
||||
return rpctypes.ErrGRPCLeaseNotFound
|
||||
case lease.ErrLeaseExists:
|
||||
return rpctypes.ErrGRPCLeaseExist
|
||||
|
||||
case auth.ErrRootUserNotExist:
|
||||
return rpctypes.ErrGRPCRootUserNotExist
|
||||
case auth.ErrRootRoleNotExist:
|
||||
return rpctypes.ErrGRPCRootRoleNotExist
|
||||
case auth.ErrUserAlreadyExist:
|
||||
return rpctypes.ErrGRPCUserAlreadyExist
|
||||
case auth.ErrUserEmpty:
|
||||
return rpctypes.ErrGRPCUserEmpty
|
||||
case auth.ErrUserNotFound:
|
||||
return rpctypes.ErrGRPCUserNotFound
|
||||
case auth.ErrRoleAlreadyExist:
|
||||
return rpctypes.ErrGRPCRoleAlreadyExist
|
||||
case auth.ErrRoleNotFound:
|
||||
return rpctypes.ErrGRPCRoleNotFound
|
||||
case auth.ErrAuthFailed:
|
||||
return rpctypes.ErrGRPCAuthFailed
|
||||
case auth.ErrPermissionDenied:
|
||||
return rpctypes.ErrGRPCPermissionDenied
|
||||
case auth.ErrRoleNotGranted:
|
||||
return rpctypes.ErrGRPCRoleNotGranted
|
||||
case auth.ErrPermissionNotGranted:
|
||||
return rpctypes.ErrGRPCPermissionNotGranted
|
||||
case auth.ErrAuthNotEnabled:
|
||||
return rpctypes.ErrGRPCAuthNotEnabled
|
||||
case auth.ErrInvalidAuthToken:
|
||||
return rpctypes.ErrGRPCInvalidAuthToken
|
||||
default:
|
||||
return grpc.Errorf(codes.Unknown, err.Error())
|
||||
}
|
||||
}
|
||||
383
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/watch.go
generated
vendored
Normal file
383
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/watch.go
generated
vendored
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v3rpc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
)
|
||||
|
||||
type watchServer struct {
|
||||
clusterID int64
|
||||
memberID int64
|
||||
raftTimer etcdserver.RaftTimer
|
||||
watchable mvcc.WatchableKV
|
||||
}
|
||||
|
||||
func NewWatchServer(s *etcdserver.EtcdServer) pb.WatchServer {
|
||||
return &watchServer{
|
||||
clusterID: int64(s.Cluster().ID()),
|
||||
memberID: int64(s.ID()),
|
||||
raftTimer: s,
|
||||
watchable: s.Watchable(),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// External test can read this with GetProgressReportInterval()
|
||||
// and change this to a small value to finish fast with
|
||||
// SetProgressReportInterval().
|
||||
progressReportInterval = 10 * time.Minute
|
||||
progressReportIntervalMu sync.RWMutex
|
||||
)
|
||||
|
||||
func GetProgressReportInterval() time.Duration {
|
||||
progressReportIntervalMu.RLock()
|
||||
defer progressReportIntervalMu.RUnlock()
|
||||
return progressReportInterval
|
||||
}
|
||||
|
||||
func SetProgressReportInterval(newTimeout time.Duration) {
|
||||
progressReportIntervalMu.Lock()
|
||||
defer progressReportIntervalMu.Unlock()
|
||||
progressReportInterval = newTimeout
|
||||
}
|
||||
|
||||
const (
|
||||
// We send ctrl response inside the read loop. We do not want
|
||||
// send to block read, but we still want ctrl response we sent to
|
||||
// be serialized. Thus we use a buffered chan to solve the problem.
|
||||
// A small buffer should be OK for most cases, since we expect the
|
||||
// ctrl requests are infrequent.
|
||||
ctrlStreamBufLen = 16
|
||||
)
|
||||
|
||||
// serverWatchStream is an etcd server side stream. It receives requests
|
||||
// from client side gRPC stream. It receives watch events from mvcc.WatchStream,
|
||||
// and creates responses that forwarded to gRPC stream.
|
||||
// It also forwards control message like watch created and canceled.
|
||||
type serverWatchStream struct {
|
||||
clusterID int64
|
||||
memberID int64
|
||||
raftTimer etcdserver.RaftTimer
|
||||
|
||||
watchable mvcc.WatchableKV
|
||||
|
||||
gRPCStream pb.Watch_WatchServer
|
||||
watchStream mvcc.WatchStream
|
||||
ctrlStream chan *pb.WatchResponse
|
||||
|
||||
// mu protects progress, prevKV
|
||||
mu sync.Mutex
|
||||
// progress tracks the watchID that stream might need to send
|
||||
// progress to.
|
||||
// TODO: combine progress and prevKV into a single struct?
|
||||
progress map[mvcc.WatchID]bool
|
||||
prevKV map[mvcc.WatchID]bool
|
||||
|
||||
// closec indicates the stream is closed.
|
||||
closec chan struct{}
|
||||
|
||||
// wg waits for the send loop to complete
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (ws *watchServer) Watch(stream pb.Watch_WatchServer) (err error) {
|
||||
sws := serverWatchStream{
|
||||
clusterID: ws.clusterID,
|
||||
memberID: ws.memberID,
|
||||
raftTimer: ws.raftTimer,
|
||||
|
||||
watchable: ws.watchable,
|
||||
|
||||
gRPCStream: stream,
|
||||
watchStream: ws.watchable.NewWatchStream(),
|
||||
// chan for sending control response like watcher created and canceled.
|
||||
ctrlStream: make(chan *pb.WatchResponse, ctrlStreamBufLen),
|
||||
progress: make(map[mvcc.WatchID]bool),
|
||||
prevKV: make(map[mvcc.WatchID]bool),
|
||||
closec: make(chan struct{}),
|
||||
}
|
||||
|
||||
sws.wg.Add(1)
|
||||
go func() {
|
||||
sws.sendLoop()
|
||||
sws.wg.Done()
|
||||
}()
|
||||
|
||||
errc := make(chan error, 1)
|
||||
// Ideally recvLoop would also use sws.wg to signal its completion
|
||||
// but when stream.Context().Done() is closed, the stream's recv
|
||||
// may continue to block since it uses a different context, leading to
|
||||
// deadlock when calling sws.close().
|
||||
go func() {
|
||||
if rerr := sws.recvLoop(); rerr != nil {
|
||||
errc <- rerr
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case err = <-errc:
|
||||
close(sws.ctrlStream)
|
||||
case <-stream.Context().Done():
|
||||
err = stream.Context().Err()
|
||||
// the only server-side cancellation is noleader for now.
|
||||
if err == context.Canceled {
|
||||
err = rpctypes.ErrGRPCNoLeader
|
||||
}
|
||||
}
|
||||
sws.close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (sws *serverWatchStream) recvLoop() error {
|
||||
for {
|
||||
req, err := sws.gRPCStream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch uv := req.RequestUnion.(type) {
|
||||
case *pb.WatchRequest_CreateRequest:
|
||||
if uv.CreateRequest == nil {
|
||||
break
|
||||
}
|
||||
|
||||
creq := uv.CreateRequest
|
||||
if len(creq.Key) == 0 {
|
||||
// \x00 is the smallest key
|
||||
creq.Key = []byte{0}
|
||||
}
|
||||
if len(creq.RangeEnd) == 1 && creq.RangeEnd[0] == 0 {
|
||||
// support >= key queries
|
||||
creq.RangeEnd = []byte{}
|
||||
}
|
||||
filters := FiltersFromRequest(creq)
|
||||
|
||||
wsrev := sws.watchStream.Rev()
|
||||
rev := creq.StartRevision
|
||||
if rev == 0 {
|
||||
rev = wsrev + 1
|
||||
}
|
||||
id := sws.watchStream.Watch(creq.Key, creq.RangeEnd, rev, filters...)
|
||||
if id != -1 {
|
||||
sws.mu.Lock()
|
||||
if creq.ProgressNotify {
|
||||
sws.progress[id] = true
|
||||
}
|
||||
if creq.PrevKv {
|
||||
sws.prevKV[id] = true
|
||||
}
|
||||
sws.mu.Unlock()
|
||||
}
|
||||
wr := &pb.WatchResponse{
|
||||
Header: sws.newResponseHeader(wsrev),
|
||||
WatchId: int64(id),
|
||||
Created: true,
|
||||
Canceled: id == -1,
|
||||
}
|
||||
select {
|
||||
case sws.ctrlStream <- wr:
|
||||
case <-sws.closec:
|
||||
return nil
|
||||
}
|
||||
case *pb.WatchRequest_CancelRequest:
|
||||
if uv.CancelRequest != nil {
|
||||
id := uv.CancelRequest.WatchId
|
||||
err := sws.watchStream.Cancel(mvcc.WatchID(id))
|
||||
if err == nil {
|
||||
sws.ctrlStream <- &pb.WatchResponse{
|
||||
Header: sws.newResponseHeader(sws.watchStream.Rev()),
|
||||
WatchId: id,
|
||||
Canceled: true,
|
||||
}
|
||||
sws.mu.Lock()
|
||||
delete(sws.progress, mvcc.WatchID(id))
|
||||
delete(sws.prevKV, mvcc.WatchID(id))
|
||||
sws.mu.Unlock()
|
||||
}
|
||||
}
|
||||
default:
|
||||
// we probably should not shutdown the entire stream when
|
||||
// receive an valid command.
|
||||
// so just do nothing instead.
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *serverWatchStream) sendLoop() {
|
||||
// watch ids that are currently active
|
||||
ids := make(map[mvcc.WatchID]struct{})
|
||||
// watch responses pending on a watch id creation message
|
||||
pending := make(map[mvcc.WatchID][]*pb.WatchResponse)
|
||||
|
||||
interval := GetProgressReportInterval()
|
||||
progressTicker := time.NewTicker(interval)
|
||||
|
||||
defer func() {
|
||||
progressTicker.Stop()
|
||||
// drain the chan to clean up pending events
|
||||
for ws := range sws.watchStream.Chan() {
|
||||
mvcc.ReportEventReceived(len(ws.Events))
|
||||
}
|
||||
for _, wrs := range pending {
|
||||
for _, ws := range wrs {
|
||||
mvcc.ReportEventReceived(len(ws.Events))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case wresp, ok := <-sws.watchStream.Chan():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: evs is []mvccpb.Event type
|
||||
// either return []*mvccpb.Event from the mvcc package
|
||||
// or define protocol buffer with []mvccpb.Event.
|
||||
evs := wresp.Events
|
||||
events := make([]*mvccpb.Event, len(evs))
|
||||
sws.mu.Lock()
|
||||
needPrevKV := sws.prevKV[wresp.WatchID]
|
||||
sws.mu.Unlock()
|
||||
for i := range evs {
|
||||
events[i] = &evs[i]
|
||||
|
||||
if needPrevKV {
|
||||
opt := mvcc.RangeOptions{Rev: evs[i].Kv.ModRevision - 1}
|
||||
r, err := sws.watchable.Range(evs[i].Kv.Key, nil, opt)
|
||||
if err == nil && len(r.KVs) != 0 {
|
||||
events[i].PrevKv = &(r.KVs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wr := &pb.WatchResponse{
|
||||
Header: sws.newResponseHeader(wresp.Revision),
|
||||
WatchId: int64(wresp.WatchID),
|
||||
Events: events,
|
||||
CompactRevision: wresp.CompactRevision,
|
||||
}
|
||||
|
||||
if _, hasId := ids[wresp.WatchID]; !hasId {
|
||||
// buffer if id not yet announced
|
||||
wrs := append(pending[wresp.WatchID], wr)
|
||||
pending[wresp.WatchID] = wrs
|
||||
continue
|
||||
}
|
||||
|
||||
mvcc.ReportEventReceived(len(evs))
|
||||
if err := sws.gRPCStream.Send(wr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sws.mu.Lock()
|
||||
if len(evs) > 0 && sws.progress[wresp.WatchID] {
|
||||
// elide next progress update if sent a key update
|
||||
sws.progress[wresp.WatchID] = false
|
||||
}
|
||||
sws.mu.Unlock()
|
||||
|
||||
case c, ok := <-sws.ctrlStream:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := sws.gRPCStream.Send(c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// track id creation
|
||||
wid := mvcc.WatchID(c.WatchId)
|
||||
if c.Canceled {
|
||||
delete(ids, wid)
|
||||
continue
|
||||
}
|
||||
if c.Created {
|
||||
// flush buffered events
|
||||
ids[wid] = struct{}{}
|
||||
for _, v := range pending[wid] {
|
||||
mvcc.ReportEventReceived(len(v.Events))
|
||||
if err := sws.gRPCStream.Send(v); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
delete(pending, wid)
|
||||
}
|
||||
case <-progressTicker.C:
|
||||
sws.mu.Lock()
|
||||
for id, ok := range sws.progress {
|
||||
if ok {
|
||||
sws.watchStream.RequestProgress(id)
|
||||
}
|
||||
sws.progress[id] = true
|
||||
}
|
||||
sws.mu.Unlock()
|
||||
case <-sws.closec:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *serverWatchStream) close() {
|
||||
sws.watchStream.Close()
|
||||
close(sws.closec)
|
||||
sws.wg.Wait()
|
||||
}
|
||||
|
||||
func (sws *serverWatchStream) newResponseHeader(rev int64) *pb.ResponseHeader {
|
||||
return &pb.ResponseHeader{
|
||||
ClusterId: uint64(sws.clusterID),
|
||||
MemberId: uint64(sws.memberID),
|
||||
Revision: rev,
|
||||
RaftTerm: sws.raftTimer.Term(),
|
||||
}
|
||||
}
|
||||
|
||||
func filterNoDelete(e mvccpb.Event) bool {
|
||||
return e.Type == mvccpb.DELETE
|
||||
}
|
||||
|
||||
func filterNoPut(e mvccpb.Event) bool {
|
||||
return e.Type == mvccpb.PUT
|
||||
}
|
||||
|
||||
func FiltersFromRequest(creq *pb.WatchCreateRequest) []mvcc.FilterFunc {
|
||||
filters := make([]mvcc.FilterFunc, 0, len(creq.Filters))
|
||||
for _, ft := range creq.Filters {
|
||||
switch ft {
|
||||
case pb.WatchCreateRequest_NOPUT:
|
||||
filters = append(filters, filterNoPut)
|
||||
case pb.WatchCreateRequest_NODELETE:
|
||||
filters = append(filters, filterNoDelete)
|
||||
default:
|
||||
}
|
||||
}
|
||||
return filters
|
||||
}
|
||||
838
vendor/github.com/coreos/etcd/etcdserver/apply.go
generated
vendored
Normal file
838
vendor/github.com/coreos/etcd/etcdserver/apply.go
generated
vendored
Normal file
|
|
@ -0,0 +1,838 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/lease"
|
||||
"github.com/coreos/etcd/mvcc"
|
||||
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// noTxn is an invalid txn ID.
|
||||
// To apply with independent Range, Put, Delete, you can pass noTxn
|
||||
// to apply functions instead of a valid txn ID.
|
||||
noTxn = -1
|
||||
|
||||
warnApplyDuration = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type applyResult struct {
|
||||
resp proto.Message
|
||||
err error
|
||||
// physc signals the physical effect of the request has completed in addition
|
||||
// to being logically reflected by the node. Currently only used for
|
||||
// Compaction requests.
|
||||
physc <-chan struct{}
|
||||
}
|
||||
|
||||
// applierV3 is the interface for processing V3 raft messages
|
||||
type applierV3 interface {
|
||||
Apply(r *pb.InternalRaftRequest) *applyResult
|
||||
|
||||
Put(txnID int64, p *pb.PutRequest) (*pb.PutResponse, error)
|
||||
Range(txnID int64, r *pb.RangeRequest) (*pb.RangeResponse, error)
|
||||
DeleteRange(txnID int64, dr *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error)
|
||||
Txn(rt *pb.TxnRequest) (*pb.TxnResponse, error)
|
||||
Compaction(compaction *pb.CompactionRequest) (*pb.CompactionResponse, <-chan struct{}, error)
|
||||
|
||||
LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error)
|
||||
LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error)
|
||||
|
||||
Alarm(*pb.AlarmRequest) (*pb.AlarmResponse, error)
|
||||
|
||||
Authenticate(r *pb.InternalAuthenticateRequest) (*pb.AuthenticateResponse, error)
|
||||
|
||||
AuthEnable() (*pb.AuthEnableResponse, error)
|
||||
AuthDisable() (*pb.AuthDisableResponse, error)
|
||||
|
||||
UserAdd(ua *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error)
|
||||
UserDelete(ua *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error)
|
||||
UserChangePassword(ua *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error)
|
||||
UserGrantRole(ua *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error)
|
||||
UserGet(ua *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error)
|
||||
UserRevokeRole(ua *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error)
|
||||
RoleAdd(ua *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error)
|
||||
RoleGrantPermission(ua *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error)
|
||||
RoleGet(ua *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error)
|
||||
RoleRevokePermission(ua *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error)
|
||||
RoleDelete(ua *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error)
|
||||
UserList(ua *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error)
|
||||
RoleList(ua *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
|
||||
}
|
||||
|
||||
type applierV3backend struct {
|
||||
s *EtcdServer
|
||||
}
|
||||
|
||||
func (s *EtcdServer) newApplierV3() applierV3 {
|
||||
return newAuthApplierV3(
|
||||
s.AuthStore(),
|
||||
newQuotaApplierV3(s, &applierV3backend{s}),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Apply(r *pb.InternalRaftRequest) *applyResult {
|
||||
ar := &applyResult{}
|
||||
|
||||
// call into a.s.applyV3.F instead of a.F so upper appliers can check individual calls
|
||||
switch {
|
||||
case r.Range != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.Range(noTxn, r.Range)
|
||||
case r.Put != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.Put(noTxn, r.Put)
|
||||
case r.DeleteRange != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.DeleteRange(noTxn, r.DeleteRange)
|
||||
case r.Txn != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.Txn(r.Txn)
|
||||
case r.Compaction != nil:
|
||||
ar.resp, ar.physc, ar.err = a.s.applyV3.Compaction(r.Compaction)
|
||||
case r.LeaseGrant != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.LeaseGrant(r.LeaseGrant)
|
||||
case r.LeaseRevoke != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.LeaseRevoke(r.LeaseRevoke)
|
||||
case r.Alarm != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.Alarm(r.Alarm)
|
||||
case r.Authenticate != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.Authenticate(r.Authenticate)
|
||||
case r.AuthEnable != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.AuthEnable()
|
||||
case r.AuthDisable != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.AuthDisable()
|
||||
case r.AuthUserAdd != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserAdd(r.AuthUserAdd)
|
||||
case r.AuthUserDelete != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserDelete(r.AuthUserDelete)
|
||||
case r.AuthUserChangePassword != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserChangePassword(r.AuthUserChangePassword)
|
||||
case r.AuthUserGrantRole != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserGrantRole(r.AuthUserGrantRole)
|
||||
case r.AuthUserGet != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserGet(r.AuthUserGet)
|
||||
case r.AuthUserRevokeRole != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserRevokeRole(r.AuthUserRevokeRole)
|
||||
case r.AuthRoleAdd != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.RoleAdd(r.AuthRoleAdd)
|
||||
case r.AuthRoleGrantPermission != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.RoleGrantPermission(r.AuthRoleGrantPermission)
|
||||
case r.AuthRoleGet != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.RoleGet(r.AuthRoleGet)
|
||||
case r.AuthRoleRevokePermission != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.RoleRevokePermission(r.AuthRoleRevokePermission)
|
||||
case r.AuthRoleDelete != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.RoleDelete(r.AuthRoleDelete)
|
||||
case r.AuthUserList != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.UserList(r.AuthUserList)
|
||||
case r.AuthRoleList != nil:
|
||||
ar.resp, ar.err = a.s.applyV3.RoleList(r.AuthRoleList)
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
return ar
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Put(txnID int64, p *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
resp := &pb.PutResponse{}
|
||||
resp.Header = &pb.ResponseHeader{}
|
||||
var (
|
||||
rev int64
|
||||
err error
|
||||
)
|
||||
|
||||
var rr *mvcc.RangeResult
|
||||
if p.PrevKv {
|
||||
if txnID != noTxn {
|
||||
rr, err = a.s.KV().TxnRange(txnID, p.Key, nil, mvcc.RangeOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rr, err = a.s.KV().Range(p.Key, nil, mvcc.RangeOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if txnID != noTxn {
|
||||
rev, err = a.s.KV().TxnPut(txnID, p.Key, p.Value, lease.LeaseID(p.Lease))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
leaseID := lease.LeaseID(p.Lease)
|
||||
if leaseID != lease.NoLease {
|
||||
if l := a.s.lessor.Lookup(leaseID); l == nil {
|
||||
return nil, lease.ErrLeaseNotFound
|
||||
}
|
||||
}
|
||||
rev = a.s.KV().Put(p.Key, p.Value, leaseID)
|
||||
}
|
||||
resp.Header.Revision = rev
|
||||
if rr != nil && len(rr.KVs) != 0 {
|
||||
resp.PrevKv = &rr.KVs[0]
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a *applierV3backend) DeleteRange(txnID int64, dr *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
|
||||
resp := &pb.DeleteRangeResponse{}
|
||||
resp.Header = &pb.ResponseHeader{}
|
||||
|
||||
var (
|
||||
n int64
|
||||
rev int64
|
||||
err error
|
||||
)
|
||||
|
||||
if isGteRange(dr.RangeEnd) {
|
||||
dr.RangeEnd = []byte{}
|
||||
}
|
||||
|
||||
var rr *mvcc.RangeResult
|
||||
if dr.PrevKv {
|
||||
if txnID != noTxn {
|
||||
rr, err = a.s.KV().TxnRange(txnID, dr.Key, dr.RangeEnd, mvcc.RangeOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rr, err = a.s.KV().Range(dr.Key, dr.RangeEnd, mvcc.RangeOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if txnID != noTxn {
|
||||
n, rev, err = a.s.KV().TxnDeleteRange(txnID, dr.Key, dr.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
n, rev = a.s.KV().DeleteRange(dr.Key, dr.RangeEnd)
|
||||
}
|
||||
|
||||
resp.Deleted = n
|
||||
if rr != nil {
|
||||
for i := range rr.KVs {
|
||||
resp.PrevKvs = append(resp.PrevKvs, &rr.KVs[i])
|
||||
}
|
||||
}
|
||||
resp.Header.Revision = rev
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Range(txnID int64, r *pb.RangeRequest) (*pb.RangeResponse, error) {
|
||||
resp := &pb.RangeResponse{}
|
||||
resp.Header = &pb.ResponseHeader{}
|
||||
|
||||
var (
|
||||
rr *mvcc.RangeResult
|
||||
err error
|
||||
)
|
||||
|
||||
if isGteRange(r.RangeEnd) {
|
||||
r.RangeEnd = []byte{}
|
||||
}
|
||||
|
||||
limit := r.Limit
|
||||
if r.SortOrder != pb.RangeRequest_NONE ||
|
||||
r.MinModRevision != 0 || r.MaxModRevision != 0 ||
|
||||
r.MinCreateRevision != 0 || r.MaxCreateRevision != 0 {
|
||||
// fetch everything; sort and truncate afterwards
|
||||
limit = 0
|
||||
}
|
||||
if limit > 0 {
|
||||
// fetch one extra for 'more' flag
|
||||
limit = limit + 1
|
||||
}
|
||||
|
||||
ro := mvcc.RangeOptions{
|
||||
Limit: limit,
|
||||
Rev: r.Revision,
|
||||
Count: r.CountOnly,
|
||||
}
|
||||
|
||||
if txnID != noTxn {
|
||||
rr, err = a.s.KV().TxnRange(txnID, r.Key, r.RangeEnd, ro)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rr, err = a.s.KV().Range(r.Key, r.RangeEnd, ro)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if r.MaxModRevision != 0 {
|
||||
f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision > r.MaxModRevision }
|
||||
pruneKVs(rr, f)
|
||||
}
|
||||
if r.MinModRevision != 0 {
|
||||
f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision < r.MinModRevision }
|
||||
pruneKVs(rr, f)
|
||||
}
|
||||
if r.MaxCreateRevision != 0 {
|
||||
f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision > r.MaxCreateRevision }
|
||||
pruneKVs(rr, f)
|
||||
}
|
||||
if r.MinCreateRevision != 0 {
|
||||
f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision < r.MinCreateRevision }
|
||||
pruneKVs(rr, f)
|
||||
}
|
||||
|
||||
sortOrder := r.SortOrder
|
||||
if r.SortTarget != pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_NONE {
|
||||
// Since current mvcc.Range implementation returns results
|
||||
// sorted by keys in lexiographically ascending order,
|
||||
// sort ASCEND by default only when target is not 'KEY'
|
||||
sortOrder = pb.RangeRequest_ASCEND
|
||||
}
|
||||
if sortOrder != pb.RangeRequest_NONE {
|
||||
var sorter sort.Interface
|
||||
switch {
|
||||
case r.SortTarget == pb.RangeRequest_KEY:
|
||||
sorter = &kvSortByKey{&kvSort{rr.KVs}}
|
||||
case r.SortTarget == pb.RangeRequest_VERSION:
|
||||
sorter = &kvSortByVersion{&kvSort{rr.KVs}}
|
||||
case r.SortTarget == pb.RangeRequest_CREATE:
|
||||
sorter = &kvSortByCreate{&kvSort{rr.KVs}}
|
||||
case r.SortTarget == pb.RangeRequest_MOD:
|
||||
sorter = &kvSortByMod{&kvSort{rr.KVs}}
|
||||
case r.SortTarget == pb.RangeRequest_VALUE:
|
||||
sorter = &kvSortByValue{&kvSort{rr.KVs}}
|
||||
}
|
||||
switch {
|
||||
case sortOrder == pb.RangeRequest_ASCEND:
|
||||
sort.Sort(sorter)
|
||||
case sortOrder == pb.RangeRequest_DESCEND:
|
||||
sort.Sort(sort.Reverse(sorter))
|
||||
}
|
||||
}
|
||||
|
||||
if r.Limit > 0 && len(rr.KVs) > int(r.Limit) {
|
||||
rr.KVs = rr.KVs[:r.Limit]
|
||||
resp.More = true
|
||||
}
|
||||
|
||||
resp.Header.Revision = rr.Rev
|
||||
resp.Count = int64(rr.Count)
|
||||
for i := range rr.KVs {
|
||||
if r.KeysOnly {
|
||||
rr.KVs[i].Value = nil
|
||||
}
|
||||
resp.Kvs = append(resp.Kvs, &rr.KVs[i])
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Txn(rt *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
ok := true
|
||||
for _, c := range rt.Compare {
|
||||
if _, ok = a.applyCompare(c); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var reqs []*pb.RequestOp
|
||||
if ok {
|
||||
reqs = rt.Success
|
||||
} else {
|
||||
reqs = rt.Failure
|
||||
}
|
||||
|
||||
if err := a.checkRequestLeases(reqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.checkRequestRange(reqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When executing the operations of txn, we need to hold the txn lock.
|
||||
// So the reader will not see any intermediate results.
|
||||
txnID := a.s.KV().TxnBegin()
|
||||
|
||||
resps := make([]*pb.ResponseOp, len(reqs))
|
||||
for i := range reqs {
|
||||
resps[i] = a.applyUnion(txnID, reqs[i])
|
||||
}
|
||||
|
||||
err := a.s.KV().TxnEnd(txnID)
|
||||
if err != nil {
|
||||
panic(fmt.Sprint("unexpected error when closing txn", txnID))
|
||||
}
|
||||
|
||||
txnResp := &pb.TxnResponse{}
|
||||
txnResp.Header = &pb.ResponseHeader{}
|
||||
txnResp.Header.Revision = a.s.KV().Rev()
|
||||
txnResp.Responses = resps
|
||||
txnResp.Succeeded = ok
|
||||
return txnResp, nil
|
||||
}
|
||||
|
||||
// applyCompare applies the compare request.
|
||||
// It returns the revision at which the comparison happens. If the comparison
|
||||
// succeeds, the it returns true. Otherwise it returns false.
|
||||
func (a *applierV3backend) applyCompare(c *pb.Compare) (int64, bool) {
|
||||
rr, err := a.s.KV().Range(c.Key, nil, mvcc.RangeOptions{})
|
||||
rev := rr.Rev
|
||||
|
||||
if err != nil {
|
||||
if err == mvcc.ErrTxnIDMismatch {
|
||||
panic("unexpected txn ID mismatch error")
|
||||
}
|
||||
return rev, false
|
||||
}
|
||||
var ckv mvccpb.KeyValue
|
||||
if len(rr.KVs) != 0 {
|
||||
ckv = rr.KVs[0]
|
||||
} else {
|
||||
// Use the zero value of ckv normally. However...
|
||||
if c.Target == pb.Compare_VALUE {
|
||||
// Always fail if we're comparing a value on a key that doesn't exist.
|
||||
// We can treat non-existence as the empty set explicitly, such that
|
||||
// even a key with a value of length 0 bytes is still a real key
|
||||
// that was written that way
|
||||
return rev, false
|
||||
}
|
||||
}
|
||||
|
||||
// -1 is less, 0 is equal, 1 is greater
|
||||
var result int
|
||||
switch c.Target {
|
||||
case pb.Compare_VALUE:
|
||||
tv, _ := c.TargetUnion.(*pb.Compare_Value)
|
||||
if tv != nil {
|
||||
result = bytes.Compare(ckv.Value, tv.Value)
|
||||
}
|
||||
case pb.Compare_CREATE:
|
||||
tv, _ := c.TargetUnion.(*pb.Compare_CreateRevision)
|
||||
if tv != nil {
|
||||
result = compareInt64(ckv.CreateRevision, tv.CreateRevision)
|
||||
}
|
||||
|
||||
case pb.Compare_MOD:
|
||||
tv, _ := c.TargetUnion.(*pb.Compare_ModRevision)
|
||||
if tv != nil {
|
||||
result = compareInt64(ckv.ModRevision, tv.ModRevision)
|
||||
}
|
||||
case pb.Compare_VERSION:
|
||||
tv, _ := c.TargetUnion.(*pb.Compare_Version)
|
||||
if tv != nil {
|
||||
result = compareInt64(ckv.Version, tv.Version)
|
||||
}
|
||||
}
|
||||
|
||||
switch c.Result {
|
||||
case pb.Compare_EQUAL:
|
||||
if result != 0 {
|
||||
return rev, false
|
||||
}
|
||||
case pb.Compare_NOT_EQUAL:
|
||||
if result == 0 {
|
||||
return rev, false
|
||||
}
|
||||
case pb.Compare_GREATER:
|
||||
if result != 1 {
|
||||
return rev, false
|
||||
}
|
||||
case pb.Compare_LESS:
|
||||
if result != -1 {
|
||||
return rev, false
|
||||
}
|
||||
}
|
||||
return rev, true
|
||||
}
|
||||
|
||||
func (a *applierV3backend) applyUnion(txnID int64, union *pb.RequestOp) *pb.ResponseOp {
|
||||
switch tv := union.Request.(type) {
|
||||
case *pb.RequestOp_RequestRange:
|
||||
if tv.RequestRange != nil {
|
||||
resp, err := a.Range(txnID, tv.RequestRange)
|
||||
if err != nil {
|
||||
plog.Panicf("unexpected error during txn: %v", err)
|
||||
}
|
||||
return &pb.ResponseOp{Response: &pb.ResponseOp_ResponseRange{ResponseRange: resp}}
|
||||
}
|
||||
case *pb.RequestOp_RequestPut:
|
||||
if tv.RequestPut != nil {
|
||||
resp, err := a.Put(txnID, tv.RequestPut)
|
||||
if err != nil {
|
||||
plog.Panicf("unexpected error during txn: %v", err)
|
||||
}
|
||||
return &pb.ResponseOp{Response: &pb.ResponseOp_ResponsePut{ResponsePut: resp}}
|
||||
}
|
||||
case *pb.RequestOp_RequestDeleteRange:
|
||||
if tv.RequestDeleteRange != nil {
|
||||
resp, err := a.DeleteRange(txnID, tv.RequestDeleteRange)
|
||||
if err != nil {
|
||||
plog.Panicf("unexpected error during txn: %v", err)
|
||||
}
|
||||
return &pb.ResponseOp{Response: &pb.ResponseOp_ResponseDeleteRange{ResponseDeleteRange: resp}}
|
||||
}
|
||||
default:
|
||||
// empty union
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Compaction(compaction *pb.CompactionRequest) (*pb.CompactionResponse, <-chan struct{}, error) {
|
||||
resp := &pb.CompactionResponse{}
|
||||
resp.Header = &pb.ResponseHeader{}
|
||||
ch, err := a.s.KV().Compact(compaction.Revision)
|
||||
if err != nil {
|
||||
return nil, ch, err
|
||||
}
|
||||
// get the current revision. which key to get is not important.
|
||||
rr, _ := a.s.KV().Range([]byte("compaction"), nil, mvcc.RangeOptions{})
|
||||
resp.Header.Revision = rr.Rev
|
||||
return resp, ch, err
|
||||
}
|
||||
|
||||
func (a *applierV3backend) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
|
||||
l, err := a.s.lessor.Grant(lease.LeaseID(lc.ID), lc.TTL)
|
||||
resp := &pb.LeaseGrantResponse{}
|
||||
if err == nil {
|
||||
resp.ID = int64(l.ID)
|
||||
resp.TTL = l.TTL()
|
||||
resp.Header = &pb.ResponseHeader{Revision: a.s.KV().Rev()}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (a *applierV3backend) LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
|
||||
err := a.s.lessor.Revoke(lease.LeaseID(lc.ID))
|
||||
return &pb.LeaseRevokeResponse{Header: &pb.ResponseHeader{Revision: a.s.KV().Rev()}}, err
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Alarm(ar *pb.AlarmRequest) (*pb.AlarmResponse, error) {
|
||||
resp := &pb.AlarmResponse{}
|
||||
oldCount := len(a.s.alarmStore.Get(ar.Alarm))
|
||||
|
||||
switch ar.Action {
|
||||
case pb.AlarmRequest_GET:
|
||||
resp.Alarms = a.s.alarmStore.Get(ar.Alarm)
|
||||
case pb.AlarmRequest_ACTIVATE:
|
||||
m := a.s.alarmStore.Activate(types.ID(ar.MemberID), ar.Alarm)
|
||||
if m == nil {
|
||||
break
|
||||
}
|
||||
resp.Alarms = append(resp.Alarms, m)
|
||||
activated := oldCount == 0 && len(a.s.alarmStore.Get(m.Alarm)) == 1
|
||||
if !activated {
|
||||
break
|
||||
}
|
||||
|
||||
switch m.Alarm {
|
||||
case pb.AlarmType_NOSPACE:
|
||||
plog.Warningf("alarm raised %+v", m)
|
||||
a.s.applyV3 = newApplierV3Capped(a)
|
||||
default:
|
||||
plog.Errorf("unimplemented alarm activation (%+v)", m)
|
||||
}
|
||||
case pb.AlarmRequest_DEACTIVATE:
|
||||
m := a.s.alarmStore.Deactivate(types.ID(ar.MemberID), ar.Alarm)
|
||||
if m == nil {
|
||||
break
|
||||
}
|
||||
resp.Alarms = append(resp.Alarms, m)
|
||||
deactivated := oldCount > 0 && len(a.s.alarmStore.Get(ar.Alarm)) == 0
|
||||
if !deactivated {
|
||||
break
|
||||
}
|
||||
|
||||
switch m.Alarm {
|
||||
case pb.AlarmType_NOSPACE:
|
||||
plog.Infof("alarm disarmed %+v", ar)
|
||||
a.s.applyV3 = a.s.newApplierV3()
|
||||
default:
|
||||
plog.Errorf("unimplemented alarm deactivation (%+v)", m)
|
||||
}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type applierV3Capped struct {
|
||||
applierV3
|
||||
q backendQuota
|
||||
}
|
||||
|
||||
// newApplierV3Capped creates an applyV3 that will reject Puts and transactions
|
||||
// with Puts so that the number of keys in the store is capped.
|
||||
func newApplierV3Capped(base applierV3) applierV3 { return &applierV3Capped{applierV3: base} }
|
||||
|
||||
func (a *applierV3Capped) Put(txnID int64, p *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
return nil, ErrNoSpace
|
||||
}
|
||||
|
||||
func (a *applierV3Capped) Txn(r *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
if a.q.Cost(r) > 0 {
|
||||
return nil, ErrNoSpace
|
||||
}
|
||||
return a.applierV3.Txn(r)
|
||||
}
|
||||
|
||||
func (a *applierV3Capped) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
|
||||
return nil, ErrNoSpace
|
||||
}
|
||||
|
||||
func (a *applierV3backend) AuthEnable() (*pb.AuthEnableResponse, error) {
|
||||
err := a.s.AuthStore().AuthEnable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.AuthEnableResponse{}, nil
|
||||
}
|
||||
|
||||
func (a *applierV3backend) AuthDisable() (*pb.AuthDisableResponse, error) {
|
||||
a.s.AuthStore().AuthDisable()
|
||||
return &pb.AuthDisableResponse{}, nil
|
||||
}
|
||||
|
||||
func (a *applierV3backend) Authenticate(r *pb.InternalAuthenticateRequest) (*pb.AuthenticateResponse, error) {
|
||||
ctx := context.WithValue(context.WithValue(context.TODO(), "index", a.s.consistIndex.ConsistentIndex()), "simpleToken", r.SimpleToken)
|
||||
return a.s.AuthStore().Authenticate(ctx, r.Name, r.Password)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
|
||||
return a.s.AuthStore().UserAdd(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
|
||||
return a.s.AuthStore().UserDelete(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
|
||||
return a.s.AuthStore().UserChangePassword(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
|
||||
return a.s.AuthStore().UserGrantRole(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
|
||||
return a.s.AuthStore().UserGet(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
|
||||
return a.s.AuthStore().UserRevokeRole(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
|
||||
return a.s.AuthStore().RoleAdd(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
|
||||
return a.s.AuthStore().RoleGrantPermission(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
|
||||
return a.s.AuthStore().RoleGet(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
|
||||
return a.s.AuthStore().RoleRevokePermission(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
|
||||
return a.s.AuthStore().RoleDelete(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
|
||||
return a.s.AuthStore().UserList(r)
|
||||
}
|
||||
|
||||
func (a *applierV3backend) RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
|
||||
return a.s.AuthStore().RoleList(r)
|
||||
}
|
||||
|
||||
type quotaApplierV3 struct {
|
||||
applierV3
|
||||
q Quota
|
||||
}
|
||||
|
||||
func newQuotaApplierV3(s *EtcdServer, app applierV3) applierV3 {
|
||||
return "aApplierV3{app, NewBackendQuota(s)}
|
||||
}
|
||||
|
||||
func (a *quotaApplierV3) Put(txnID int64, p *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
ok := a.q.Available(p)
|
||||
resp, err := a.applierV3.Put(txnID, p)
|
||||
if err == nil && !ok {
|
||||
err = ErrNoSpace
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (a *quotaApplierV3) Txn(rt *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
ok := a.q.Available(rt)
|
||||
resp, err := a.applierV3.Txn(rt)
|
||||
if err == nil && !ok {
|
||||
err = ErrNoSpace
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (a *quotaApplierV3) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
|
||||
ok := a.q.Available(lc)
|
||||
resp, err := a.applierV3.LeaseGrant(lc)
|
||||
if err == nil && !ok {
|
||||
err = ErrNoSpace
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type kvSort struct{ kvs []mvccpb.KeyValue }
|
||||
|
||||
func (s *kvSort) Swap(i, j int) {
|
||||
t := s.kvs[i]
|
||||
s.kvs[i] = s.kvs[j]
|
||||
s.kvs[j] = t
|
||||
}
|
||||
func (s *kvSort) Len() int { return len(s.kvs) }
|
||||
|
||||
type kvSortByKey struct{ *kvSort }
|
||||
|
||||
func (s *kvSortByKey) Less(i, j int) bool {
|
||||
return bytes.Compare(s.kvs[i].Key, s.kvs[j].Key) < 0
|
||||
}
|
||||
|
||||
type kvSortByVersion struct{ *kvSort }
|
||||
|
||||
func (s *kvSortByVersion) Less(i, j int) bool {
|
||||
return (s.kvs[i].Version - s.kvs[j].Version) < 0
|
||||
}
|
||||
|
||||
type kvSortByCreate struct{ *kvSort }
|
||||
|
||||
func (s *kvSortByCreate) Less(i, j int) bool {
|
||||
return (s.kvs[i].CreateRevision - s.kvs[j].CreateRevision) < 0
|
||||
}
|
||||
|
||||
type kvSortByMod struct{ *kvSort }
|
||||
|
||||
func (s *kvSortByMod) Less(i, j int) bool {
|
||||
return (s.kvs[i].ModRevision - s.kvs[j].ModRevision) < 0
|
||||
}
|
||||
|
||||
type kvSortByValue struct{ *kvSort }
|
||||
|
||||
func (s *kvSortByValue) Less(i, j int) bool {
|
||||
return bytes.Compare(s.kvs[i].Value, s.kvs[j].Value) < 0
|
||||
}
|
||||
|
||||
func (a *applierV3backend) checkRequestLeases(reqs []*pb.RequestOp) error {
|
||||
for _, requ := range reqs {
|
||||
tv, ok := requ.Request.(*pb.RequestOp_RequestPut)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
preq := tv.RequestPut
|
||||
if preq == nil || lease.LeaseID(preq.Lease) == lease.NoLease {
|
||||
continue
|
||||
}
|
||||
if l := a.s.lessor.Lookup(lease.LeaseID(preq.Lease)); l == nil {
|
||||
return lease.ErrLeaseNotFound
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *applierV3backend) checkRequestRange(reqs []*pb.RequestOp) error {
|
||||
for _, requ := range reqs {
|
||||
tv, ok := requ.Request.(*pb.RequestOp_RequestRange)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
greq := tv.RequestRange
|
||||
if greq == nil || greq.Revision == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if greq.Revision > a.s.KV().Rev() {
|
||||
return mvcc.ErrFutureRev
|
||||
}
|
||||
if greq.Revision < a.s.KV().FirstRev() {
|
||||
return mvcc.ErrCompacted
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareInt64(a, b int64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// isGteRange determines if the range end is a >= range. This works around grpc
|
||||
// sending empty byte strings as nil; >= is encoded in the range end as '\0'.
|
||||
func isGteRange(rangeEnd []byte) bool {
|
||||
return len(rangeEnd) == 1 && rangeEnd[0] == 0
|
||||
}
|
||||
|
||||
func noSideEffect(r *pb.InternalRaftRequest) bool {
|
||||
return r.Range != nil || r.AuthUserGet != nil || r.AuthRoleGet != nil
|
||||
}
|
||||
|
||||
func removeNeedlessRangeReqs(txn *pb.TxnRequest) {
|
||||
f := func(ops []*pb.RequestOp) []*pb.RequestOp {
|
||||
j := 0
|
||||
for i := 0; i < len(ops); i++ {
|
||||
if _, ok := ops[i].Request.(*pb.RequestOp_RequestRange); ok {
|
||||
continue
|
||||
}
|
||||
ops[j] = ops[i]
|
||||
j++
|
||||
}
|
||||
|
||||
return ops[:j]
|
||||
}
|
||||
|
||||
txn.Success = f(txn.Success)
|
||||
txn.Failure = f(txn.Failure)
|
||||
}
|
||||
|
||||
func pruneKVs(rr *mvcc.RangeResult, isPrunable func(*mvccpb.KeyValue) bool) {
|
||||
j := 0
|
||||
for i := range rr.KVs {
|
||||
rr.KVs[j] = rr.KVs[i]
|
||||
if !isPrunable(&rr.KVs[i]) {
|
||||
j++
|
||||
}
|
||||
}
|
||||
rr.KVs = rr.KVs[:j]
|
||||
}
|
||||
195
vendor/github.com/coreos/etcd/etcdserver/apply_auth.go
generated
vendored
Normal file
195
vendor/github.com/coreos/etcd/etcdserver/apply_auth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/etcd/auth"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
)
|
||||
|
||||
type authApplierV3 struct {
|
||||
applierV3
|
||||
as auth.AuthStore
|
||||
|
||||
// mu serializes Apply so that user isn't corrupted and so that
|
||||
// serialized requests don't leak data from TOCTOU errors
|
||||
mu sync.Mutex
|
||||
|
||||
authInfo auth.AuthInfo
|
||||
}
|
||||
|
||||
func newAuthApplierV3(as auth.AuthStore, base applierV3) *authApplierV3 {
|
||||
return &authApplierV3{applierV3: base, as: as}
|
||||
}
|
||||
|
||||
func (aa *authApplierV3) Apply(r *pb.InternalRaftRequest) *applyResult {
|
||||
aa.mu.Lock()
|
||||
defer aa.mu.Unlock()
|
||||
if r.Header != nil {
|
||||
// backward-compatible with pre-3.0 releases when internalRaftRequest
|
||||
// does not have header field
|
||||
aa.authInfo.Username = r.Header.Username
|
||||
aa.authInfo.Revision = r.Header.AuthRevision
|
||||
}
|
||||
if needAdminPermission(r) {
|
||||
if err := aa.as.IsAdminPermitted(&aa.authInfo); err != nil {
|
||||
aa.authInfo.Username = ""
|
||||
aa.authInfo.Revision = 0
|
||||
return &applyResult{err: err}
|
||||
}
|
||||
}
|
||||
ret := aa.applierV3.Apply(r)
|
||||
aa.authInfo.Username = ""
|
||||
aa.authInfo.Revision = 0
|
||||
return ret
|
||||
}
|
||||
|
||||
func (aa *authApplierV3) Put(txnID int64, r *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
if err := aa.as.IsPutPermitted(&aa.authInfo, r.Key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.PrevKv {
|
||||
err := aa.as.IsRangePermitted(&aa.authInfo, r.Key, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return aa.applierV3.Put(txnID, r)
|
||||
}
|
||||
|
||||
func (aa *authApplierV3) Range(txnID int64, r *pb.RangeRequest) (*pb.RangeResponse, error) {
|
||||
if err := aa.as.IsRangePermitted(&aa.authInfo, r.Key, r.RangeEnd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aa.applierV3.Range(txnID, r)
|
||||
}
|
||||
|
||||
func (aa *authApplierV3) DeleteRange(txnID int64, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
|
||||
if err := aa.as.IsDeleteRangePermitted(&aa.authInfo, r.Key, r.RangeEnd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.PrevKv {
|
||||
err := aa.as.IsRangePermitted(&aa.authInfo, r.Key, r.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return aa.applierV3.DeleteRange(txnID, r)
|
||||
}
|
||||
|
||||
func checkTxnReqsPermission(as auth.AuthStore, ai *auth.AuthInfo, reqs []*pb.RequestOp) error {
|
||||
for _, requ := range reqs {
|
||||
switch tv := requ.Request.(type) {
|
||||
case *pb.RequestOp_RequestRange:
|
||||
if tv.RequestRange == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := as.IsRangePermitted(ai, tv.RequestRange.Key, tv.RequestRange.RangeEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *pb.RequestOp_RequestPut:
|
||||
if tv.RequestPut == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := as.IsPutPermitted(ai, tv.RequestPut.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *pb.RequestOp_RequestDeleteRange:
|
||||
if tv.RequestDeleteRange == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if tv.RequestDeleteRange.PrevKv {
|
||||
err := as.IsRangePermitted(ai, tv.RequestDeleteRange.Key, tv.RequestDeleteRange.RangeEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := as.IsDeleteRangePermitted(ai, tv.RequestDeleteRange.Key, tv.RequestDeleteRange.RangeEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkTxnAuth(as auth.AuthStore, ai *auth.AuthInfo, rt *pb.TxnRequest) error {
|
||||
for _, c := range rt.Compare {
|
||||
if err := as.IsRangePermitted(ai, c.Key, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := checkTxnReqsPermission(as, ai, rt.Success); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkTxnReqsPermission(as, ai, rt.Failure); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (aa *authApplierV3) Txn(rt *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
if err := checkTxnAuth(aa.as, &aa.authInfo, rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aa.applierV3.Txn(rt)
|
||||
}
|
||||
|
||||
func needAdminPermission(r *pb.InternalRaftRequest) bool {
|
||||
switch {
|
||||
case r.AuthEnable != nil:
|
||||
return true
|
||||
case r.AuthDisable != nil:
|
||||
return true
|
||||
case r.AuthUserAdd != nil:
|
||||
return true
|
||||
case r.AuthUserDelete != nil:
|
||||
return true
|
||||
case r.AuthUserChangePassword != nil:
|
||||
return true
|
||||
case r.AuthUserGrantRole != nil:
|
||||
return true
|
||||
case r.AuthUserGet != nil:
|
||||
return true
|
||||
case r.AuthUserRevokeRole != nil:
|
||||
return true
|
||||
case r.AuthRoleAdd != nil:
|
||||
return true
|
||||
case r.AuthRoleGrantPermission != nil:
|
||||
return true
|
||||
case r.AuthRoleGet != nil:
|
||||
return true
|
||||
case r.AuthRoleRevokePermission != nil:
|
||||
return true
|
||||
case r.AuthRoleDelete != nil:
|
||||
return true
|
||||
case r.AuthUserList != nil:
|
||||
return true
|
||||
case r.AuthRoleList != nil:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
140
vendor/github.com/coreos/etcd/etcdserver/apply_v2.go
generated
vendored
Normal file
140
vendor/github.com/coreos/etcd/etcdserver/apply_v2.go
generated
vendored
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/api"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/pbutil"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
// ApplierV2 is the interface for processing V2 raft messages
|
||||
type ApplierV2 interface {
|
||||
Delete(r *pb.Request) Response
|
||||
Post(r *pb.Request) Response
|
||||
Put(r *pb.Request) Response
|
||||
QGet(r *pb.Request) Response
|
||||
Sync(r *pb.Request) Response
|
||||
}
|
||||
|
||||
func NewApplierV2(s store.Store, c *membership.RaftCluster) ApplierV2 {
|
||||
return &applierV2store{store: s, cluster: c}
|
||||
}
|
||||
|
||||
type applierV2store struct {
|
||||
store store.Store
|
||||
cluster *membership.RaftCluster
|
||||
}
|
||||
|
||||
func (a *applierV2store) Delete(r *pb.Request) Response {
|
||||
switch {
|
||||
case r.PrevIndex > 0 || r.PrevValue != "":
|
||||
return toResponse(a.store.CompareAndDelete(r.Path, r.PrevValue, r.PrevIndex))
|
||||
default:
|
||||
return toResponse(a.store.Delete(r.Path, r.Dir, r.Recursive))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *applierV2store) Post(r *pb.Request) Response {
|
||||
return toResponse(a.store.Create(r.Path, r.Dir, r.Val, true, toTTLOptions(r)))
|
||||
}
|
||||
|
||||
func (a *applierV2store) Put(r *pb.Request) Response {
|
||||
ttlOptions := toTTLOptions(r)
|
||||
exists, existsSet := pbutil.GetBool(r.PrevExist)
|
||||
switch {
|
||||
case existsSet:
|
||||
if exists {
|
||||
if r.PrevIndex == 0 && r.PrevValue == "" {
|
||||
return toResponse(a.store.Update(r.Path, r.Val, ttlOptions))
|
||||
}
|
||||
return toResponse(a.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
|
||||
}
|
||||
return toResponse(a.store.Create(r.Path, r.Dir, r.Val, false, ttlOptions))
|
||||
case r.PrevIndex > 0 || r.PrevValue != "":
|
||||
return toResponse(a.store.CompareAndSwap(r.Path, r.PrevValue, r.PrevIndex, r.Val, ttlOptions))
|
||||
default:
|
||||
if storeMemberAttributeRegexp.MatchString(r.Path) {
|
||||
id := membership.MustParseMemberIDFromKey(path.Dir(r.Path))
|
||||
var attr membership.Attributes
|
||||
if err := json.Unmarshal([]byte(r.Val), &attr); err != nil {
|
||||
plog.Panicf("unmarshal %s should never fail: %v", r.Val, err)
|
||||
}
|
||||
if a.cluster != nil {
|
||||
a.cluster.UpdateAttributes(id, attr)
|
||||
}
|
||||
// return an empty response since there is no consumer.
|
||||
return Response{}
|
||||
}
|
||||
if r.Path == membership.StoreClusterVersionKey() {
|
||||
if a.cluster != nil {
|
||||
a.cluster.SetVersion(semver.Must(semver.NewVersion(r.Val)), api.UpdateCapability)
|
||||
}
|
||||
// return an empty response since there is no consumer.
|
||||
return Response{}
|
||||
}
|
||||
return toResponse(a.store.Set(r.Path, r.Dir, r.Val, ttlOptions))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *applierV2store) QGet(r *pb.Request) Response {
|
||||
return toResponse(a.store.Get(r.Path, r.Recursive, r.Sorted))
|
||||
}
|
||||
|
||||
func (a *applierV2store) Sync(r *pb.Request) Response {
|
||||
a.store.DeleteExpiredKeys(time.Unix(0, r.Time))
|
||||
return Response{}
|
||||
}
|
||||
|
||||
// applyV2Request interprets r as a call to store.X and returns a Response interpreted
|
||||
// from store.Event
|
||||
func (s *EtcdServer) applyV2Request(r *pb.Request) Response {
|
||||
toTTLOptions(r)
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
return s.applyV2.Post(r)
|
||||
case "PUT":
|
||||
return s.applyV2.Put(r)
|
||||
case "DELETE":
|
||||
return s.applyV2.Delete(r)
|
||||
case "QGET":
|
||||
return s.applyV2.QGet(r)
|
||||
case "SYNC":
|
||||
return s.applyV2.Sync(r)
|
||||
default:
|
||||
// This should never be reached, but just in case:
|
||||
return Response{err: ErrUnknownMethod}
|
||||
}
|
||||
}
|
||||
|
||||
func toTTLOptions(r *pb.Request) store.TTLOptionSet {
|
||||
refresh, _ := pbutil.GetBool(r.Refresh)
|
||||
ttlOptions := store.TTLOptionSet{Refresh: refresh}
|
||||
if r.Expiration != 0 {
|
||||
ttlOptions.ExpireTime = time.Unix(0, r.Expiration)
|
||||
}
|
||||
return ttlOptions
|
||||
}
|
||||
|
||||
func toResponse(ev *store.Event, err error) Response {
|
||||
return Response{Event: ev, err: err}
|
||||
}
|
||||
647
vendor/github.com/coreos/etcd/etcdserver/auth/auth.go
generated
vendored
Normal file
647
vendor/github.com/coreos/etcd/etcdserver/auth/auth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,647 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package auth implements etcd authentication.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcderr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// StorePermsPrefix is the internal prefix of the storage layer dedicated to storing user data.
|
||||
StorePermsPrefix = "/2"
|
||||
|
||||
// RootRoleName is the name of the ROOT role, with privileges to manage the cluster.
|
||||
RootRoleName = "root"
|
||||
|
||||
// GuestRoleName is the name of the role that defines the privileges of an unauthenticated user.
|
||||
GuestRoleName = "guest"
|
||||
)
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/auth")
|
||||
)
|
||||
|
||||
var rootRole = Role{
|
||||
Role: RootRoleName,
|
||||
Permissions: Permissions{
|
||||
KV: RWPermission{
|
||||
Read: []string{"/*"},
|
||||
Write: []string{"/*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var guestRole = Role{
|
||||
Role: GuestRoleName,
|
||||
Permissions: Permissions{
|
||||
KV: RWPermission{
|
||||
Read: []string{"/*"},
|
||||
Write: []string{"/*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type doer interface {
|
||||
Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error)
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
AllUsers() ([]string, error)
|
||||
GetUser(name string) (User, error)
|
||||
CreateOrUpdateUser(user User) (out User, created bool, err error)
|
||||
CreateUser(user User) (User, error)
|
||||
DeleteUser(name string) error
|
||||
UpdateUser(user User) (User, error)
|
||||
AllRoles() ([]string, error)
|
||||
GetRole(name string) (Role, error)
|
||||
CreateRole(role Role) error
|
||||
DeleteRole(name string) error
|
||||
UpdateRole(role Role) (Role, error)
|
||||
AuthEnabled() bool
|
||||
EnableAuth() error
|
||||
DisableAuth() error
|
||||
PasswordStore
|
||||
}
|
||||
|
||||
type PasswordStore interface {
|
||||
CheckPassword(user User, password string) bool
|
||||
HashPassword(password string) (string, error)
|
||||
}
|
||||
|
||||
type store struct {
|
||||
server doer
|
||||
timeout time.Duration
|
||||
ensuredOnce bool
|
||||
|
||||
PasswordStore
|
||||
}
|
||||
|
||||
type User struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Roles []string `json:"roles"`
|
||||
Grant []string `json:"grant,omitempty"`
|
||||
Revoke []string `json:"revoke,omitempty"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
Role string `json:"role"`
|
||||
Permissions Permissions `json:"permissions"`
|
||||
Grant *Permissions `json:"grant,omitempty"`
|
||||
Revoke *Permissions `json:"revoke,omitempty"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
KV RWPermission `json:"kv"`
|
||||
}
|
||||
|
||||
func (p *Permissions) IsEmpty() bool {
|
||||
return p == nil || (len(p.KV.Read) == 0 && len(p.KV.Write) == 0)
|
||||
}
|
||||
|
||||
type RWPermission struct {
|
||||
Read []string `json:"read"`
|
||||
Write []string `json:"write"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Status int
|
||||
Errmsg string
|
||||
}
|
||||
|
||||
func (ae Error) Error() string { return ae.Errmsg }
|
||||
func (ae Error) HTTPStatus() int { return ae.Status }
|
||||
|
||||
func authErr(hs int, s string, v ...interface{}) Error {
|
||||
return Error{Status: hs, Errmsg: fmt.Sprintf("auth: "+s, v...)}
|
||||
}
|
||||
|
||||
func NewStore(server doer, timeout time.Duration) Store {
|
||||
s := &store{
|
||||
server: server,
|
||||
timeout: timeout,
|
||||
PasswordStore: passwordStore{},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// passwordStore implements PasswordStore using bcrypt to hash user passwords
|
||||
type passwordStore struct{}
|
||||
|
||||
func (_ passwordStore) CheckPassword(user User, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (_ passwordStore) HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
func (s *store) AllUsers() ([]string, error) {
|
||||
resp, err := s.requestResource("/users/", false, false)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return []string{}, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var nodes []string
|
||||
for _, n := range resp.Event.Node.Nodes {
|
||||
_, user := path.Split(n.Key)
|
||||
nodes = append(nodes, user)
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (s *store) GetUser(name string) (User, error) { return s.getUser(name, false) }
|
||||
|
||||
// CreateOrUpdateUser should be only used for creating the new user or when you are not
|
||||
// sure if it is a create or update. (When only password is passed in, we are not sure
|
||||
// if it is a update or create)
|
||||
func (s *store) CreateOrUpdateUser(user User) (out User, created bool, err error) {
|
||||
_, err = s.getUser(user.User, true)
|
||||
if err == nil {
|
||||
out, err = s.UpdateUser(user)
|
||||
return out, false, err
|
||||
}
|
||||
u, err := s.CreateUser(user)
|
||||
return u, true, err
|
||||
}
|
||||
|
||||
func (s *store) CreateUser(user User) (User, error) {
|
||||
// Attach root role to root user.
|
||||
if user.User == "root" {
|
||||
user = attachRootRole(user)
|
||||
}
|
||||
u, err := s.createUserInternal(user)
|
||||
if err == nil {
|
||||
plog.Noticef("created user %s", user.User)
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
func (s *store) createUserInternal(user User) (User, error) {
|
||||
if user.Password == "" {
|
||||
return user, authErr(http.StatusBadRequest, "Cannot create user %s with an empty password", user.User)
|
||||
}
|
||||
hash, err := s.HashPassword(user.Password)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
user.Password = hash
|
||||
|
||||
_, err = s.createResource("/users/"+user.User, user)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeNodeExist {
|
||||
return user, authErr(http.StatusConflict, "User %s already exists.", user.User)
|
||||
}
|
||||
}
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (s *store) DeleteUser(name string) error {
|
||||
if s.AuthEnabled() && name == "root" {
|
||||
return authErr(http.StatusForbidden, "Cannot delete root user while auth is enabled.")
|
||||
}
|
||||
_, err := s.deleteResource("/users/" + name)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return authErr(http.StatusNotFound, "User %s does not exist", name)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
plog.Noticef("deleted user %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) UpdateUser(user User) (User, error) {
|
||||
old, err := s.getUser(user.User, true)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return user, authErr(http.StatusNotFound, "User %s doesn't exist.", user.User)
|
||||
}
|
||||
}
|
||||
return old, err
|
||||
}
|
||||
|
||||
newUser, err := old.merge(user, s.PasswordStore)
|
||||
if err != nil {
|
||||
return old, err
|
||||
}
|
||||
if reflect.DeepEqual(old, newUser) {
|
||||
return old, authErr(http.StatusBadRequest, "User not updated. Use grant/revoke/password to update the user.")
|
||||
}
|
||||
_, err = s.updateResource("/users/"+user.User, newUser)
|
||||
if err == nil {
|
||||
plog.Noticef("updated user %s", user.User)
|
||||
}
|
||||
return newUser, err
|
||||
}
|
||||
|
||||
func (s *store) AllRoles() ([]string, error) {
|
||||
nodes := []string{RootRoleName}
|
||||
resp, err := s.requestResource("/roles/", false, false)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return nodes, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, n := range resp.Event.Node.Nodes {
|
||||
_, role := path.Split(n.Key)
|
||||
nodes = append(nodes, role)
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (s *store) GetRole(name string) (Role, error) { return s.getRole(name, false) }
|
||||
|
||||
func (s *store) CreateRole(role Role) error {
|
||||
if role.Role == RootRoleName {
|
||||
return authErr(http.StatusForbidden, "Cannot modify role %s: is root role.", role.Role)
|
||||
}
|
||||
_, err := s.createResource("/roles/"+role.Role, role)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeNodeExist {
|
||||
return authErr(http.StatusConflict, "Role %s already exists.", role.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
plog.Noticef("created new role %s", role.Role)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) DeleteRole(name string) error {
|
||||
if name == RootRoleName {
|
||||
return authErr(http.StatusForbidden, "Cannot modify role %s: is root role.", name)
|
||||
}
|
||||
_, err := s.deleteResource("/roles/" + name)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return authErr(http.StatusNotFound, "Role %s doesn't exist.", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
plog.Noticef("deleted role %s", name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) UpdateRole(role Role) (Role, error) {
|
||||
if role.Role == RootRoleName {
|
||||
return Role{}, authErr(http.StatusForbidden, "Cannot modify role %s: is root role.", role.Role)
|
||||
}
|
||||
old, err := s.getRole(role.Role, true)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return role, authErr(http.StatusNotFound, "Role %s doesn't exist.", role.Role)
|
||||
}
|
||||
}
|
||||
return old, err
|
||||
}
|
||||
newRole, err := old.merge(role)
|
||||
if err != nil {
|
||||
return old, err
|
||||
}
|
||||
if reflect.DeepEqual(old, newRole) {
|
||||
return old, authErr(http.StatusBadRequest, "Role not updated. Use grant/revoke to update the role.")
|
||||
}
|
||||
_, err = s.updateResource("/roles/"+role.Role, newRole)
|
||||
if err == nil {
|
||||
plog.Noticef("updated role %s", role.Role)
|
||||
}
|
||||
return newRole, err
|
||||
}
|
||||
|
||||
func (s *store) AuthEnabled() bool {
|
||||
return s.detectAuth()
|
||||
}
|
||||
|
||||
func (s *store) EnableAuth() error {
|
||||
if s.AuthEnabled() {
|
||||
return authErr(http.StatusConflict, "already enabled")
|
||||
}
|
||||
|
||||
if _, err := s.getUser("root", true); err != nil {
|
||||
return authErr(http.StatusConflict, "No root user available, please create one")
|
||||
}
|
||||
if _, err := s.getRole(GuestRoleName, true); err != nil {
|
||||
plog.Printf("no guest role access found, creating default")
|
||||
if err := s.CreateRole(guestRole); err != nil {
|
||||
plog.Errorf("error creating guest role. aborting auth enable.")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.enableAuth(); err != nil {
|
||||
plog.Errorf("error enabling auth (%v)", err)
|
||||
return err
|
||||
}
|
||||
|
||||
plog.Noticef("auth: enabled auth")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) DisableAuth() error {
|
||||
if !s.AuthEnabled() {
|
||||
return authErr(http.StatusConflict, "already disabled")
|
||||
}
|
||||
|
||||
err := s.disableAuth()
|
||||
if err == nil {
|
||||
plog.Noticef("auth: disabled auth")
|
||||
} else {
|
||||
plog.Errorf("error disabling auth (%v)", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// merge applies the properties of the passed-in User to the User on which it
|
||||
// is called and returns a new User with these modifications applied. Think of
|
||||
// all Users as immutable sets of data. Merge allows you to perform the set
|
||||
// operations (desired grants and revokes) atomically
|
||||
func (ou User) merge(nu User, s PasswordStore) (User, error) {
|
||||
var out User
|
||||
if ou.User != nu.User {
|
||||
return out, authErr(http.StatusConflict, "Merging user data with conflicting usernames: %s %s", ou.User, nu.User)
|
||||
}
|
||||
out.User = ou.User
|
||||
if nu.Password != "" {
|
||||
hash, err := s.HashPassword(nu.Password)
|
||||
if err != nil {
|
||||
return ou, err
|
||||
}
|
||||
out.Password = hash
|
||||
} else {
|
||||
out.Password = ou.Password
|
||||
}
|
||||
currentRoles := types.NewUnsafeSet(ou.Roles...)
|
||||
for _, g := range nu.Grant {
|
||||
if currentRoles.Contains(g) {
|
||||
plog.Noticef("granting duplicate role %s for user %s", g, nu.User)
|
||||
return User{}, authErr(http.StatusConflict, fmt.Sprintf("Granting duplicate role %s for user %s", g, nu.User))
|
||||
}
|
||||
currentRoles.Add(g)
|
||||
}
|
||||
for _, r := range nu.Revoke {
|
||||
if !currentRoles.Contains(r) {
|
||||
plog.Noticef("revoking ungranted role %s for user %s", r, nu.User)
|
||||
return User{}, authErr(http.StatusConflict, fmt.Sprintf("Revoking ungranted role %s for user %s", r, nu.User))
|
||||
}
|
||||
currentRoles.Remove(r)
|
||||
}
|
||||
out.Roles = currentRoles.Values()
|
||||
sort.Strings(out.Roles)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// merge for a role works the same as User above -- atomic Role application to
|
||||
// each of the substructures.
|
||||
func (r Role) merge(n Role) (Role, error) {
|
||||
var out Role
|
||||
var err error
|
||||
if r.Role != n.Role {
|
||||
return out, authErr(http.StatusConflict, "Merging role with conflicting names: %s %s", r.Role, n.Role)
|
||||
}
|
||||
out.Role = r.Role
|
||||
out.Permissions, err = r.Permissions.Grant(n.Grant)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Permissions, err = out.Permissions.Revoke(n.Revoke)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (r Role) HasKeyAccess(key string, write bool) bool {
|
||||
if r.Role == RootRoleName {
|
||||
return true
|
||||
}
|
||||
return r.Permissions.KV.HasAccess(key, write)
|
||||
}
|
||||
|
||||
func (r Role) HasRecursiveAccess(key string, write bool) bool {
|
||||
if r.Role == RootRoleName {
|
||||
return true
|
||||
}
|
||||
return r.Permissions.KV.HasRecursiveAccess(key, write)
|
||||
}
|
||||
|
||||
// Grant adds a set of permissions to the permission object on which it is called,
|
||||
// returning a new permission object.
|
||||
func (p Permissions) Grant(n *Permissions) (Permissions, error) {
|
||||
var out Permissions
|
||||
var err error
|
||||
if n == nil {
|
||||
return p, nil
|
||||
}
|
||||
out.KV, err = p.KV.Grant(n.KV)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Revoke removes a set of permissions to the permission object on which it is called,
|
||||
// returning a new permission object.
|
||||
func (p Permissions) Revoke(n *Permissions) (Permissions, error) {
|
||||
var out Permissions
|
||||
var err error
|
||||
if n == nil {
|
||||
return p, nil
|
||||
}
|
||||
out.KV, err = p.KV.Revoke(n.KV)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Grant adds a set of permissions to the permission object on which it is called,
|
||||
// returning a new permission object.
|
||||
func (rw RWPermission) Grant(n RWPermission) (RWPermission, error) {
|
||||
var out RWPermission
|
||||
currentRead := types.NewUnsafeSet(rw.Read...)
|
||||
for _, r := range n.Read {
|
||||
if currentRead.Contains(r) {
|
||||
return out, authErr(http.StatusConflict, "Granting duplicate read permission %s", r)
|
||||
}
|
||||
currentRead.Add(r)
|
||||
}
|
||||
currentWrite := types.NewUnsafeSet(rw.Write...)
|
||||
for _, w := range n.Write {
|
||||
if currentWrite.Contains(w) {
|
||||
return out, authErr(http.StatusConflict, "Granting duplicate write permission %s", w)
|
||||
}
|
||||
currentWrite.Add(w)
|
||||
}
|
||||
out.Read = currentRead.Values()
|
||||
out.Write = currentWrite.Values()
|
||||
sort.Strings(out.Read)
|
||||
sort.Strings(out.Write)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Revoke removes a set of permissions to the permission object on which it is called,
|
||||
// returning a new permission object.
|
||||
func (rw RWPermission) Revoke(n RWPermission) (RWPermission, error) {
|
||||
var out RWPermission
|
||||
currentRead := types.NewUnsafeSet(rw.Read...)
|
||||
for _, r := range n.Read {
|
||||
if !currentRead.Contains(r) {
|
||||
plog.Noticef("revoking ungranted read permission %s", r)
|
||||
continue
|
||||
}
|
||||
currentRead.Remove(r)
|
||||
}
|
||||
currentWrite := types.NewUnsafeSet(rw.Write...)
|
||||
for _, w := range n.Write {
|
||||
if !currentWrite.Contains(w) {
|
||||
plog.Noticef("revoking ungranted write permission %s", w)
|
||||
continue
|
||||
}
|
||||
currentWrite.Remove(w)
|
||||
}
|
||||
out.Read = currentRead.Values()
|
||||
out.Write = currentWrite.Values()
|
||||
sort.Strings(out.Read)
|
||||
sort.Strings(out.Write)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (rw RWPermission) HasAccess(key string, write bool) bool {
|
||||
var list []string
|
||||
if write {
|
||||
list = rw.Write
|
||||
} else {
|
||||
list = rw.Read
|
||||
}
|
||||
for _, pat := range list {
|
||||
match, err := simpleMatch(pat, key)
|
||||
if err == nil && match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rw RWPermission) HasRecursiveAccess(key string, write bool) bool {
|
||||
list := rw.Read
|
||||
if write {
|
||||
list = rw.Write
|
||||
}
|
||||
for _, pat := range list {
|
||||
match, err := prefixMatch(pat, key)
|
||||
if err == nil && match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func simpleMatch(pattern string, key string) (match bool, err error) {
|
||||
if pattern[len(pattern)-1] == '*' {
|
||||
return strings.HasPrefix(key, pattern[:len(pattern)-1]), nil
|
||||
}
|
||||
return key == pattern, nil
|
||||
}
|
||||
|
||||
func prefixMatch(pattern string, key string) (match bool, err error) {
|
||||
if pattern[len(pattern)-1] != '*' {
|
||||
return false, nil
|
||||
}
|
||||
return strings.HasPrefix(key, pattern[:len(pattern)-1]), nil
|
||||
}
|
||||
|
||||
func attachRootRole(u User) User {
|
||||
inRoles := false
|
||||
for _, r := range u.Roles {
|
||||
if r == RootRoleName {
|
||||
inRoles = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inRoles {
|
||||
u.Roles = append(u.Roles, RootRoleName)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func (s *store) getUser(name string, quorum bool) (User, error) {
|
||||
resp, err := s.requestResource("/users/"+name, false, quorum)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return User{}, authErr(http.StatusNotFound, "User %s does not exist.", name)
|
||||
}
|
||||
}
|
||||
return User{}, err
|
||||
}
|
||||
var u User
|
||||
err = json.Unmarshal([]byte(*resp.Event.Node.Value), &u)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
// Attach root role to root user.
|
||||
if u.User == "root" {
|
||||
u = attachRootRole(u)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (s *store) getRole(name string, quorum bool) (Role, error) {
|
||||
if name == RootRoleName {
|
||||
return rootRole, nil
|
||||
}
|
||||
resp, err := s.requestResource("/roles/"+name, false, quorum)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return Role{}, authErr(http.StatusNotFound, "Role %s does not exist.", name)
|
||||
}
|
||||
}
|
||||
return Role{}, err
|
||||
}
|
||||
var r Role
|
||||
err = json.Unmarshal([]byte(*resp.Event.Node.Value), &r)
|
||||
return r, err
|
||||
}
|
||||
166
vendor/github.com/coreos/etcd/etcdserver/auth/auth_requests.go
generated
vendored
Normal file
166
vendor/github.com/coreos/etcd/etcdserver/auth/auth_requests.go
generated
vendored
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
|
||||
etcderr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (s *store) ensureAuthDirectories() error {
|
||||
if s.ensuredOnce {
|
||||
return nil
|
||||
}
|
||||
for _, res := range []string{StorePermsPrefix, StorePermsPrefix + "/users/", StorePermsPrefix + "/roles/"} {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
|
||||
defer cancel()
|
||||
pe := false
|
||||
rr := etcdserverpb.Request{
|
||||
Method: "PUT",
|
||||
Path: res,
|
||||
Dir: true,
|
||||
PrevExist: &pe,
|
||||
}
|
||||
_, err := s.server.Do(ctx, rr)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeNodeExist {
|
||||
continue
|
||||
}
|
||||
}
|
||||
plog.Errorf("failed to create auth directories in the store (%v)", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
|
||||
defer cancel()
|
||||
pe := false
|
||||
rr := etcdserverpb.Request{
|
||||
Method: "PUT",
|
||||
Path: StorePermsPrefix + "/enabled",
|
||||
Val: "false",
|
||||
PrevExist: &pe,
|
||||
}
|
||||
_, err := s.server.Do(ctx, rr)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeNodeExist {
|
||||
s.ensuredOnce = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.ensuredOnce = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) enableAuth() error {
|
||||
_, err := s.updateResource("/enabled", true)
|
||||
return err
|
||||
}
|
||||
func (s *store) disableAuth() error {
|
||||
_, err := s.updateResource("/enabled", false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) detectAuth() bool {
|
||||
if s.server == nil {
|
||||
return false
|
||||
}
|
||||
value, err := s.requestResource("/enabled", false, false)
|
||||
if err != nil {
|
||||
if e, ok := err.(*etcderr.Error); ok {
|
||||
if e.ErrorCode == etcderr.EcodeKeyNotFound {
|
||||
return false
|
||||
}
|
||||
}
|
||||
plog.Errorf("failed to detect auth settings (%s)", err)
|
||||
return false
|
||||
}
|
||||
|
||||
var u bool
|
||||
err = json.Unmarshal([]byte(*value.Event.Node.Value), &u)
|
||||
if err != nil {
|
||||
plog.Errorf("internal bookkeeping value for enabled isn't valid JSON (%v)", err)
|
||||
return false
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func (s *store) requestResource(res string, dir, quorum bool) (etcdserver.Response, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
|
||||
defer cancel()
|
||||
p := path.Join(StorePermsPrefix, res)
|
||||
method := "GET"
|
||||
if quorum {
|
||||
method = "QGET"
|
||||
}
|
||||
rr := etcdserverpb.Request{
|
||||
Method: method,
|
||||
Path: p,
|
||||
Dir: dir,
|
||||
}
|
||||
return s.server.Do(ctx, rr)
|
||||
}
|
||||
|
||||
func (s *store) updateResource(res string, value interface{}) (etcdserver.Response, error) {
|
||||
return s.setResource(res, value, true)
|
||||
}
|
||||
func (s *store) createResource(res string, value interface{}) (etcdserver.Response, error) {
|
||||
return s.setResource(res, value, false)
|
||||
}
|
||||
func (s *store) setResource(res string, value interface{}, prevexist bool) (etcdserver.Response, error) {
|
||||
err := s.ensureAuthDirectories()
|
||||
if err != nil {
|
||||
return etcdserver.Response{}, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
|
||||
defer cancel()
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return etcdserver.Response{}, err
|
||||
}
|
||||
p := path.Join(StorePermsPrefix, res)
|
||||
rr := etcdserverpb.Request{
|
||||
Method: "PUT",
|
||||
Path: p,
|
||||
Val: string(data),
|
||||
PrevExist: &prevexist,
|
||||
}
|
||||
return s.server.Do(ctx, rr)
|
||||
}
|
||||
|
||||
func (s *store) deleteResource(res string) (etcdserver.Response, error) {
|
||||
err := s.ensureAuthDirectories()
|
||||
if err != nil {
|
||||
return etcdserver.Response{}, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
|
||||
defer cancel()
|
||||
pex := true
|
||||
p := path.Join(StorePermsPrefix, res)
|
||||
rr := etcdserverpb.Request{
|
||||
Method: "DELETE",
|
||||
Path: p,
|
||||
PrevExist: &pex,
|
||||
}
|
||||
return s.server.Do(ctx, rr)
|
||||
}
|
||||
676
vendor/github.com/coreos/etcd/etcdserver/auth/auth_test.go
generated
vendored
Normal file
676
vendor/github.com/coreos/etcd/etcdserver/auth/auth_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,676 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
etcderr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/etcdserver"
|
||||
"github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
etcdstore "github.com/coreos/etcd/store"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeDoer struct{}
|
||||
|
||||
func (_ fakeDoer) Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) {
|
||||
return etcdserver.Response{}, nil
|
||||
}
|
||||
|
||||
func TestCheckPassword(t *testing.T) {
|
||||
st := NewStore(fakeDoer{}, 5*time.Second)
|
||||
u := User{Password: "$2a$10$I3iddh1D..EIOXXQtsra4u8AjOtgEa2ERxVvYGfXFBJDo1omXwP.q"}
|
||||
matched := st.CheckPassword(u, "foo")
|
||||
if matched {
|
||||
t.Fatalf("expected false, got %v", matched)
|
||||
}
|
||||
}
|
||||
|
||||
const testTimeout = time.Millisecond
|
||||
|
||||
func TestMergeUser(t *testing.T) {
|
||||
tbl := []struct {
|
||||
input User
|
||||
merge User
|
||||
expect User
|
||||
iserr bool
|
||||
}{
|
||||
{
|
||||
User{User: "foo"},
|
||||
User{User: "bar"},
|
||||
User{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
User{User: "foo"},
|
||||
User{User: "foo"},
|
||||
User{User: "foo", Roles: []string{}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
User{User: "foo"},
|
||||
User{User: "foo", Grant: []string{"role1"}},
|
||||
User{User: "foo", Roles: []string{"role1"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
User{User: "foo", Roles: []string{"role1"}},
|
||||
User{User: "foo", Grant: []string{"role1"}},
|
||||
User{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
User{User: "foo", Roles: []string{"role1"}},
|
||||
User{User: "foo", Revoke: []string{"role2"}},
|
||||
User{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
User{User: "foo", Roles: []string{"role1"}},
|
||||
User{User: "foo", Grant: []string{"role2"}},
|
||||
User{User: "foo", Roles: []string{"role1", "role2"}},
|
||||
false,
|
||||
},
|
||||
{ // empty password will not overwrite the previous password
|
||||
User{User: "foo", Password: "foo", Roles: []string{}},
|
||||
User{User: "foo", Password: ""},
|
||||
User{User: "foo", Password: "foo", Roles: []string{}},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
out, err := tt.input.merge(tt.merge, passwordStore{})
|
||||
if err != nil && !tt.iserr {
|
||||
t.Fatalf("Got unexpected error on item %d", i)
|
||||
}
|
||||
if !tt.iserr {
|
||||
if !reflect.DeepEqual(out, tt.expect) {
|
||||
t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeRole(t *testing.T) {
|
||||
tbl := []struct {
|
||||
input Role
|
||||
merge Role
|
||||
expect Role
|
||||
iserr bool
|
||||
}{
|
||||
{
|
||||
Role{Role: "foo"},
|
||||
Role{Role: "bar"},
|
||||
Role{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
Role{Role: "foo"},
|
||||
Role{Role: "foo", Grant: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
|
||||
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
|
||||
Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
|
||||
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{}, Write: []string{}}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/bardir"}}}},
|
||||
Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}}}},
|
||||
Role{},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tbl {
|
||||
out, err := tt.input.merge(tt.merge)
|
||||
if err != nil && !tt.iserr {
|
||||
t.Fatalf("Got unexpected error on item %d", i)
|
||||
}
|
||||
if !tt.iserr {
|
||||
if !reflect.DeepEqual(out, tt.expect) {
|
||||
t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testDoer struct {
|
||||
get []etcdserver.Response
|
||||
put []etcdserver.Response
|
||||
getindex int
|
||||
putindex int
|
||||
explicitlyEnabled bool
|
||||
}
|
||||
|
||||
func (td *testDoer) Do(_ context.Context, req etcdserverpb.Request) (etcdserver.Response, error) {
|
||||
if td.explicitlyEnabled && (req.Path == StorePermsPrefix+"/enabled") {
|
||||
t := "true"
|
||||
return etcdserver.Response{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
Value: &t,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if (req.Method == "GET" || req.Method == "QGET") && td.get != nil {
|
||||
res := td.get[td.getindex]
|
||||
if res.Event == nil {
|
||||
td.getindex++
|
||||
return etcdserver.Response{}, &etcderr.Error{
|
||||
ErrorCode: etcderr.EcodeKeyNotFound,
|
||||
}
|
||||
}
|
||||
td.getindex++
|
||||
return res, nil
|
||||
}
|
||||
if req.Method == "PUT" && td.put != nil {
|
||||
res := td.put[td.putindex]
|
||||
if res.Event == nil {
|
||||
td.putindex++
|
||||
return etcdserver.Response{}, &etcderr.Error{
|
||||
ErrorCode: etcderr.EcodeNodeExist,
|
||||
}
|
||||
}
|
||||
td.putindex++
|
||||
return res, nil
|
||||
}
|
||||
return etcdserver.Response{}, nil
|
||||
}
|
||||
|
||||
func TestAllUsers(t *testing.T) {
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Nodes: etcdstore.NodeExterns([]*etcdstore.NodeExtern{
|
||||
{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
},
|
||||
{
|
||||
Key: StorePermsPrefix + "/users/dog",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := []string{"cat", "dog"}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
|
||||
users, err := s.AllUsers()
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
if !reflect.DeepEqual(users, expected) {
|
||||
t.Error("AllUsers doesn't match given store. Got", users, "expected", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAndDeleteUser(t *testing.T) {
|
||||
data := `{"user": "cat", "roles" : ["animal"]}`
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
Value: &data,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: true,
|
||||
}
|
||||
expected := User{User: "cat", Roles: []string{"animal"}}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
|
||||
out, err := s.GetUser("cat")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, expected) {
|
||||
t.Error("GetUser doesn't match given store. Got", out, "expected", expected)
|
||||
}
|
||||
err = s.DeleteUser("cat")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllRoles(t *testing.T) {
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Nodes: etcdstore.NodeExterns([]*etcdstore.NodeExtern{
|
||||
{
|
||||
Key: StorePermsPrefix + "/roles/animal",
|
||||
},
|
||||
{
|
||||
Key: StorePermsPrefix + "/roles/human",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: true,
|
||||
}
|
||||
expected := []string{"animal", "human", "root"}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
|
||||
out, err := s.AllRoles()
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, expected) {
|
||||
t.Error("AllRoles doesn't match given store. Got", out, "expected", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAndDeleteRole(t *testing.T) {
|
||||
data := `{"role": "animal"}`
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/roles/animal",
|
||||
Value: &data,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: true,
|
||||
}
|
||||
expected := Role{Role: "animal"}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
|
||||
out, err := s.GetRole("animal")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, expected) {
|
||||
t.Error("GetRole doesn't match given store. Got", out, "expected", expected)
|
||||
}
|
||||
err = s.DeleteRole("animal")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsure(t *testing.T) {
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Set,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix,
|
||||
Dir: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Set,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/",
|
||||
Dir: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Set,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/roles/",
|
||||
Dir: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
|
||||
err := s.ensureAuthDirectories()
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fastPasswordStore struct {
|
||||
}
|
||||
|
||||
func (_ fastPasswordStore) CheckPassword(user User, password string) bool {
|
||||
return user.Password == password
|
||||
}
|
||||
|
||||
func (_ fastPasswordStore) HashPassword(password string) (string, error) { return password, nil }
|
||||
|
||||
func TestCreateAndUpdateUser(t *testing.T) {
|
||||
olduser := `{"user": "cat", "roles" : ["animal"]}`
|
||||
newuser := `{"user": "cat", "roles" : ["animal", "pet"]}`
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: nil,
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
Value: &olduser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
Value: &olduser,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
put: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Update,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
Value: &olduser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Update,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/users/cat",
|
||||
Value: &newuser,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: true,
|
||||
}
|
||||
user := User{User: "cat", Password: "meow", Roles: []string{"animal"}}
|
||||
update := User{User: "cat", Grant: []string{"pet"}}
|
||||
expected := User{User: "cat", Roles: []string{"animal", "pet"}}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: true, PasswordStore: fastPasswordStore{}}
|
||||
out, created, err := s.CreateOrUpdateUser(user)
|
||||
if !created {
|
||||
t.Error("Should have created user, instead updated?")
|
||||
}
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
out.Password = "meow"
|
||||
if !reflect.DeepEqual(out, user) {
|
||||
t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected)
|
||||
}
|
||||
out, created, err = s.CreateOrUpdateUser(update)
|
||||
if created {
|
||||
t.Error("Should have updated user, instead created?")
|
||||
}
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, expected) {
|
||||
t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRole(t *testing.T) {
|
||||
oldrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}`
|
||||
newrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": ["/animal"]}}}`
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/roles/animal",
|
||||
Value: &oldrole,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
put: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Update,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/roles/animal",
|
||||
Value: &newrole,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: true,
|
||||
}
|
||||
update := Role{Role: "animal", Grant: &Permissions{KV: RWPermission{Read: []string{}, Write: []string{"/animal"}}}}
|
||||
expected := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{"/animal"}}}}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
|
||||
out, err := s.UpdateRole(update)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, expected) {
|
||||
t.Error("UpdateRole doesn't match given update. Got", out, "expected", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRole(t *testing.T) {
|
||||
role := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}`
|
||||
d := &testDoer{
|
||||
put: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Create,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/roles/animal",
|
||||
Value: &role,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: nil,
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: true,
|
||||
}
|
||||
r := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{}}}}
|
||||
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
|
||||
err := s.CreateRole(Role{Role: "root"})
|
||||
if err == nil {
|
||||
t.Error("Should error creating root role")
|
||||
}
|
||||
err = s.CreateRole(r)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
err = s.CreateRole(r)
|
||||
if err == nil {
|
||||
t.Error("Creating duplicate role, should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAuth(t *testing.T) {
|
||||
rootUser := `{"user": "root", "password": ""}`
|
||||
guestRole := `{"role": "guest", "permissions" : {"kv": {"read": ["*"], "write": ["*"]}}}`
|
||||
trueval := "true"
|
||||
falseval := "false"
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/enabled",
|
||||
Value: &falseval,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/user/root",
|
||||
Value: &rootUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: nil,
|
||||
},
|
||||
},
|
||||
put: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Create,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/roles/guest",
|
||||
Value: &guestRole,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Update,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/enabled",
|
||||
Value: &trueval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: false,
|
||||
}
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
|
||||
err := s.EnableAuth()
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableAuth(t *testing.T) {
|
||||
trueval := "true"
|
||||
falseval := "false"
|
||||
d := &testDoer{
|
||||
get: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/enabled",
|
||||
Value: &falseval,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Get,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/enabled",
|
||||
Value: &trueval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
put: []etcdserver.Response{
|
||||
{
|
||||
Event: &etcdstore.Event{
|
||||
Action: etcdstore.Update,
|
||||
Node: &etcdstore.NodeExtern{
|
||||
Key: StorePermsPrefix + "/enabled",
|
||||
Value: &falseval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
explicitlyEnabled: false,
|
||||
}
|
||||
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
|
||||
err := s.DisableAuth()
|
||||
if err == nil {
|
||||
t.Error("Expected error; already disabled")
|
||||
}
|
||||
|
||||
err = s.DisableAuth()
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleMatch(t *testing.T) {
|
||||
role := Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir/*", "/fookey"}, Write: []string{"/bardir/*", "/barkey"}}}}
|
||||
if !role.HasKeyAccess("/foodir/foo/bar", false) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
if !role.HasKeyAccess("/fookey", false) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
if !role.HasRecursiveAccess("/foodir/*", false) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
if !role.HasRecursiveAccess("/foodir/foo*", false) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
if !role.HasRecursiveAccess("/bardir/*", true) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
if !role.HasKeyAccess("/bardir/bar/foo", true) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
if !role.HasKeyAccess("/barkey", true) {
|
||||
t.Fatal("role lacks expected access")
|
||||
}
|
||||
|
||||
if role.HasKeyAccess("/bardir/bar/foo", false) {
|
||||
t.Fatal("role has unexpected access")
|
||||
}
|
||||
if role.HasKeyAccess("/barkey", false) {
|
||||
t.Fatal("role has unexpected access")
|
||||
}
|
||||
if role.HasKeyAccess("/foodir/foo/bar", true) {
|
||||
t.Fatal("role has unexpected access")
|
||||
}
|
||||
if role.HasKeyAccess("/fookey", true) {
|
||||
t.Fatal("role has unexpected access")
|
||||
}
|
||||
}
|
||||
268
vendor/github.com/coreos/etcd/etcdserver/cluster_util.go
generated
vendored
Normal file
268
vendor/github.com/coreos/etcd/etcdserver/cluster_util.go
generated
vendored
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/httputil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/version"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
// isMemberBootstrapped tries to check if the given member has been bootstrapped
|
||||
// in the given cluster.
|
||||
func isMemberBootstrapped(cl *membership.RaftCluster, member string, rt http.RoundTripper, timeout time.Duration) bool {
|
||||
rcl, err := getClusterFromRemotePeers(getRemotePeerURLs(cl, member), timeout, false, rt)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
id := cl.MemberByName(member).ID
|
||||
m := rcl.Member(id)
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if len(m.ClientURLs) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetClusterFromRemotePeers takes a set of URLs representing etcd peers, and
|
||||
// attempts to construct a Cluster by accessing the members endpoint on one of
|
||||
// these URLs. The first URL to provide a response is used. If no URLs provide
|
||||
// a response, or a Cluster cannot be successfully created from a received
|
||||
// response, an error is returned.
|
||||
// Each request has a 10-second timeout. Because the upper limit of TTL is 5s,
|
||||
// 10 second is enough for building connection and finishing request.
|
||||
func GetClusterFromRemotePeers(urls []string, rt http.RoundTripper) (*membership.RaftCluster, error) {
|
||||
return getClusterFromRemotePeers(urls, 10*time.Second, true, rt)
|
||||
}
|
||||
|
||||
// If logerr is true, it prints out more error messages.
|
||||
func getClusterFromRemotePeers(urls []string, timeout time.Duration, logerr bool, rt http.RoundTripper) (*membership.RaftCluster, error) {
|
||||
cc := &http.Client{
|
||||
Transport: rt,
|
||||
Timeout: timeout,
|
||||
}
|
||||
for _, u := range urls {
|
||||
resp, err := cc.Get(u + "/members")
|
||||
if err != nil {
|
||||
if logerr {
|
||||
plog.Warningf("could not get cluster response from %s: %v", u, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
if logerr {
|
||||
plog.Warningf("could not read the body of cluster response: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var membs []*membership.Member
|
||||
if err = json.Unmarshal(b, &membs); err != nil {
|
||||
if logerr {
|
||||
plog.Warningf("could not unmarshal cluster response: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
id, err := types.IDFromString(resp.Header.Get("X-Etcd-Cluster-ID"))
|
||||
if err != nil {
|
||||
if logerr {
|
||||
plog.Warningf("could not parse the cluster ID from cluster res: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// check the length of membership members
|
||||
// if the membership members are present then prepare and return raft cluster
|
||||
// if membership members are not present then the raft cluster formed will be
|
||||
// an invalid empty cluster hence return failed to get raft cluster member(s) from the given urls error
|
||||
if len(membs) > 0 {
|
||||
return membership.NewClusterFromMembers("", id, membs), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to get raft cluster member(s) from the given urls.")
|
||||
}
|
||||
return nil, fmt.Errorf("could not retrieve cluster information from the given urls")
|
||||
}
|
||||
|
||||
// getRemotePeerURLs returns peer urls of remote members in the cluster. The
|
||||
// returned list is sorted in ascending lexicographical order.
|
||||
func getRemotePeerURLs(cl *membership.RaftCluster, local string) []string {
|
||||
us := make([]string, 0)
|
||||
for _, m := range cl.Members() {
|
||||
if m.Name == local {
|
||||
continue
|
||||
}
|
||||
us = append(us, m.PeerURLs...)
|
||||
}
|
||||
sort.Strings(us)
|
||||
return us
|
||||
}
|
||||
|
||||
// getVersions returns the versions of the members in the given cluster.
|
||||
// The key of the returned map is the member's ID. The value of the returned map
|
||||
// is the semver versions string, including server and cluster.
|
||||
// If it fails to get the version of a member, the key will be nil.
|
||||
func getVersions(cl *membership.RaftCluster, local types.ID, rt http.RoundTripper) map[string]*version.Versions {
|
||||
members := cl.Members()
|
||||
vers := make(map[string]*version.Versions)
|
||||
for _, m := range members {
|
||||
if m.ID == local {
|
||||
cv := "not_decided"
|
||||
if cl.Version() != nil {
|
||||
cv = cl.Version().String()
|
||||
}
|
||||
vers[m.ID.String()] = &version.Versions{Server: version.Version, Cluster: cv}
|
||||
continue
|
||||
}
|
||||
ver, err := getVersion(m, rt)
|
||||
if err != nil {
|
||||
plog.Warningf("cannot get the version of member %s (%v)", m.ID, err)
|
||||
vers[m.ID.String()] = nil
|
||||
} else {
|
||||
vers[m.ID.String()] = ver
|
||||
}
|
||||
}
|
||||
return vers
|
||||
}
|
||||
|
||||
// decideClusterVersion decides the cluster version based on the versions map.
|
||||
// The returned version is the min server version in the map, or nil if the min
|
||||
// version in unknown.
|
||||
func decideClusterVersion(vers map[string]*version.Versions) *semver.Version {
|
||||
var cv *semver.Version
|
||||
lv := semver.Must(semver.NewVersion(version.Version))
|
||||
|
||||
for mid, ver := range vers {
|
||||
if ver == nil {
|
||||
return nil
|
||||
}
|
||||
v, err := semver.NewVersion(ver.Server)
|
||||
if err != nil {
|
||||
plog.Errorf("cannot understand the version of member %s (%v)", mid, err)
|
||||
return nil
|
||||
}
|
||||
if lv.LessThan(*v) {
|
||||
plog.Warningf("the local etcd version %s is not up-to-date", lv.String())
|
||||
plog.Warningf("member %s has a higher version %s", mid, ver.Server)
|
||||
}
|
||||
if cv == nil {
|
||||
cv = v
|
||||
} else if v.LessThan(*cv) {
|
||||
cv = v
|
||||
}
|
||||
}
|
||||
return cv
|
||||
}
|
||||
|
||||
// isCompatibleWithCluster return true if the local member has a compatible version with
|
||||
// the current running cluster.
|
||||
// The version is considered as compatible when at least one of the other members in the cluster has a
|
||||
// cluster version in the range of [MinClusterVersion, Version] and no known members has a cluster version
|
||||
// out of the range.
|
||||
// We set this rule since when the local member joins, another member might be offline.
|
||||
func isCompatibleWithCluster(cl *membership.RaftCluster, local types.ID, rt http.RoundTripper) bool {
|
||||
vers := getVersions(cl, local, rt)
|
||||
minV := semver.Must(semver.NewVersion(version.MinClusterVersion))
|
||||
maxV := semver.Must(semver.NewVersion(version.Version))
|
||||
maxV = &semver.Version{
|
||||
Major: maxV.Major,
|
||||
Minor: maxV.Minor,
|
||||
}
|
||||
|
||||
return isCompatibleWithVers(vers, local, minV, maxV)
|
||||
}
|
||||
|
||||
func isCompatibleWithVers(vers map[string]*version.Versions, local types.ID, minV, maxV *semver.Version) bool {
|
||||
var ok bool
|
||||
for id, v := range vers {
|
||||
// ignore comparison with local version
|
||||
if id == local.String() {
|
||||
continue
|
||||
}
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
clusterv, err := semver.NewVersion(v.Cluster)
|
||||
if err != nil {
|
||||
plog.Errorf("cannot understand the cluster version of member %s (%v)", id, err)
|
||||
continue
|
||||
}
|
||||
if clusterv.LessThan(*minV) {
|
||||
plog.Warningf("the running cluster version(%v) is lower than the minimal cluster version(%v) supported", clusterv.String(), minV.String())
|
||||
return false
|
||||
}
|
||||
if maxV.LessThan(*clusterv) {
|
||||
plog.Warningf("the running cluster version(%v) is higher than the maximum cluster version(%v) supported", clusterv.String(), maxV.String())
|
||||
return false
|
||||
}
|
||||
ok = true
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// getVersion returns the Versions of the given member via its
|
||||
// peerURLs. Returns the last error if it fails to get the version.
|
||||
func getVersion(m *membership.Member, rt http.RoundTripper) (*version.Versions, error) {
|
||||
cc := &http.Client{
|
||||
Transport: rt,
|
||||
}
|
||||
var (
|
||||
err error
|
||||
resp *http.Response
|
||||
)
|
||||
|
||||
for _, u := range m.PeerURLs {
|
||||
resp, err = cc.Get(u + "/version")
|
||||
if err != nil {
|
||||
plog.Warningf("failed to reach the peerURL(%s) of member %s (%v)", u, m.ID, err)
|
||||
continue
|
||||
}
|
||||
// etcd 2.0 does not have version endpoint on peer url.
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
httputil.GracefulClose(resp)
|
||||
return &version.Versions{
|
||||
Server: "2.0.0",
|
||||
Cluster: "2.0.0",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var b []byte
|
||||
b, err = ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
plog.Warningf("failed to read out the response body from the peerURL(%s) of member %s (%v)", u, m.ID, err)
|
||||
continue
|
||||
}
|
||||
var vers version.Versions
|
||||
if err = json.Unmarshal(b, &vers); err != nil {
|
||||
plog.Warningf("failed to unmarshal the response body got from the peerURL(%s) of member %s (%v)", u, m.ID, err)
|
||||
continue
|
||||
}
|
||||
return &vers, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
131
vendor/github.com/coreos/etcd/etcdserver/cluster_util_test.go
generated
vendored
Normal file
131
vendor/github.com/coreos/etcd/etcdserver/cluster_util_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/version"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
func TestDecideClusterVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
vers map[string]*version.Versions
|
||||
wdver *semver.Version
|
||||
}{
|
||||
{
|
||||
map[string]*version.Versions{"a": {Server: "2.0.0"}},
|
||||
semver.Must(semver.NewVersion("2.0.0")),
|
||||
},
|
||||
// unknown
|
||||
{
|
||||
map[string]*version.Versions{"a": nil},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
map[string]*version.Versions{"a": {Server: "2.0.0"}, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}},
|
||||
semver.Must(semver.NewVersion("2.0.0")),
|
||||
},
|
||||
{
|
||||
map[string]*version.Versions{"a": {Server: "2.1.0"}, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}},
|
||||
semver.Must(semver.NewVersion("2.1.0")),
|
||||
},
|
||||
{
|
||||
map[string]*version.Versions{"a": nil, "b": {Server: "2.1.0"}, "c": {Server: "2.1.0"}},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
dver := decideClusterVersion(tt.vers)
|
||||
if !reflect.DeepEqual(dver, tt.wdver) {
|
||||
t.Errorf("#%d: ver = %+v, want %+v", i, dver, tt.wdver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCompatibleWithVers(t *testing.T) {
|
||||
tests := []struct {
|
||||
vers map[string]*version.Versions
|
||||
local types.ID
|
||||
minV, maxV *semver.Version
|
||||
wok bool
|
||||
}{
|
||||
// too low
|
||||
{
|
||||
map[string]*version.Versions{
|
||||
"a": {Server: "2.0.0", Cluster: "not_decided"},
|
||||
"b": {Server: "2.1.0", Cluster: "2.1.0"},
|
||||
"c": {Server: "2.1.0", Cluster: "2.1.0"},
|
||||
},
|
||||
0xa,
|
||||
semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.0.0")),
|
||||
false,
|
||||
},
|
||||
{
|
||||
map[string]*version.Versions{
|
||||
"a": {Server: "2.1.0", Cluster: "not_decided"},
|
||||
"b": {Server: "2.1.0", Cluster: "2.1.0"},
|
||||
"c": {Server: "2.1.0", Cluster: "2.1.0"},
|
||||
},
|
||||
0xa,
|
||||
semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")),
|
||||
true,
|
||||
},
|
||||
// too high
|
||||
{
|
||||
map[string]*version.Versions{
|
||||
"a": {Server: "2.2.0", Cluster: "not_decided"},
|
||||
"b": {Server: "2.0.0", Cluster: "2.0.0"},
|
||||
"c": {Server: "2.0.0", Cluster: "2.0.0"},
|
||||
},
|
||||
0xa,
|
||||
semver.Must(semver.NewVersion("2.1.0")), semver.Must(semver.NewVersion("2.2.0")),
|
||||
false,
|
||||
},
|
||||
// cannot get b's version, expect ok
|
||||
{
|
||||
map[string]*version.Versions{
|
||||
"a": {Server: "2.1.0", Cluster: "not_decided"},
|
||||
"b": nil,
|
||||
"c": {Server: "2.1.0", Cluster: "2.1.0"},
|
||||
},
|
||||
0xa,
|
||||
semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")),
|
||||
true,
|
||||
},
|
||||
// cannot get b and c's version, expect not ok
|
||||
{
|
||||
map[string]*version.Versions{
|
||||
"a": {Server: "2.1.0", Cluster: "not_decided"},
|
||||
"b": nil,
|
||||
"c": nil,
|
||||
},
|
||||
0xa,
|
||||
semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")),
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
ok := isCompatibleWithVers(tt.vers, tt.local, tt.minV, tt.maxV)
|
||||
if ok != tt.wok {
|
||||
t.Errorf("#%d: ok = %+v, want %+v", i, ok, tt.wok)
|
||||
}
|
||||
}
|
||||
}
|
||||
200
vendor/github.com/coreos/etcd/etcdserver/config.go
generated
vendored
Normal file
200
vendor/github.com/coreos/etcd/etcdserver/config.go
generated
vendored
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/coreos/etcd/pkg/netutil"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
// ServerConfig holds the configuration of etcd as taken from the command line or discovery.
|
||||
type ServerConfig struct {
|
||||
Name string
|
||||
DiscoveryURL string
|
||||
DiscoveryProxy string
|
||||
ClientURLs types.URLs
|
||||
PeerURLs types.URLs
|
||||
DataDir string
|
||||
// DedicatedWALDir config will make the etcd to write the WAL to the WALDir
|
||||
// rather than the dataDir/member/wal.
|
||||
DedicatedWALDir string
|
||||
SnapCount uint64
|
||||
MaxSnapFiles uint
|
||||
MaxWALFiles uint
|
||||
InitialPeerURLsMap types.URLsMap
|
||||
InitialClusterToken string
|
||||
NewCluster bool
|
||||
ForceNewCluster bool
|
||||
PeerTLSInfo transport.TLSInfo
|
||||
|
||||
TickMs uint
|
||||
ElectionTicks int
|
||||
BootstrapTimeout time.Duration
|
||||
|
||||
AutoCompactionRetention int
|
||||
QuotaBackendBytes int64
|
||||
|
||||
StrictReconfigCheck bool
|
||||
|
||||
// ClientCertAuthEnabled is true when cert has been signed by the client CA.
|
||||
ClientCertAuthEnabled bool
|
||||
}
|
||||
|
||||
// VerifyBootstrap sanity-checks the initial config for bootstrap case
|
||||
// and returns an error for things that should never happen.
|
||||
func (c *ServerConfig) VerifyBootstrap() error {
|
||||
if err := c.hasLocalMember(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.advertiseMatchesCluster(); err != nil {
|
||||
return err
|
||||
}
|
||||
if checkDuplicateURL(c.InitialPeerURLsMap) {
|
||||
return fmt.Errorf("initial cluster %s has duplicate url", c.InitialPeerURLsMap)
|
||||
}
|
||||
if c.InitialPeerURLsMap.String() == "" && c.DiscoveryURL == "" {
|
||||
return fmt.Errorf("initial cluster unset and no discovery URL found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyJoinExisting sanity-checks the initial config for join existing cluster
|
||||
// case and returns an error for things that should never happen.
|
||||
func (c *ServerConfig) VerifyJoinExisting() error {
|
||||
// The member has announced its peer urls to the cluster before starting; no need to
|
||||
// set the configuration again.
|
||||
if err := c.hasLocalMember(); err != nil {
|
||||
return err
|
||||
}
|
||||
if checkDuplicateURL(c.InitialPeerURLsMap) {
|
||||
return fmt.Errorf("initial cluster %s has duplicate url", c.InitialPeerURLsMap)
|
||||
}
|
||||
if c.DiscoveryURL != "" {
|
||||
return fmt.Errorf("discovery URL should not be set when joining existing initial cluster")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasLocalMember checks that the cluster at least contains the local server.
|
||||
func (c *ServerConfig) hasLocalMember() error {
|
||||
if urls := c.InitialPeerURLsMap[c.Name]; urls == nil {
|
||||
return fmt.Errorf("couldn't find local name %q in the initial cluster configuration", c.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// advertiseMatchesCluster confirms peer URLs match those in the cluster peer list.
|
||||
func (c *ServerConfig) advertiseMatchesCluster() error {
|
||||
urls, apurls := c.InitialPeerURLsMap[c.Name], c.PeerURLs.StringSlice()
|
||||
urls.Sort()
|
||||
sort.Strings(apurls)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
|
||||
defer cancel()
|
||||
if !netutil.URLStringsEqual(ctx, apurls, urls.StringSlice()) {
|
||||
umap := map[string]types.URLs{c.Name: c.PeerURLs}
|
||||
return fmt.Errorf("--initial-cluster must include %s given --initial-advertise-peer-urls=%s", types.URLsMap(umap).String(), strings.Join(apurls, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ServerConfig) MemberDir() string { return filepath.Join(c.DataDir, "member") }
|
||||
|
||||
func (c *ServerConfig) WALDir() string {
|
||||
if c.DedicatedWALDir != "" {
|
||||
return c.DedicatedWALDir
|
||||
}
|
||||
return filepath.Join(c.MemberDir(), "wal")
|
||||
}
|
||||
|
||||
func (c *ServerConfig) SnapDir() string { return filepath.Join(c.MemberDir(), "snap") }
|
||||
|
||||
func (c *ServerConfig) ShouldDiscover() bool { return c.DiscoveryURL != "" }
|
||||
|
||||
// ReqTimeout returns timeout for request to finish.
|
||||
func (c *ServerConfig) ReqTimeout() time.Duration {
|
||||
// 5s for queue waiting, computation and disk IO delay
|
||||
// + 2 * election timeout for possible leader election
|
||||
return 5*time.Second + 2*time.Duration(c.ElectionTicks)*time.Duration(c.TickMs)*time.Millisecond
|
||||
}
|
||||
|
||||
func (c *ServerConfig) electionTimeout() time.Duration {
|
||||
return time.Duration(c.ElectionTicks) * time.Duration(c.TickMs) * time.Millisecond
|
||||
}
|
||||
|
||||
func (c *ServerConfig) peerDialTimeout() time.Duration {
|
||||
// 1s for queue wait and system delay
|
||||
// + one RTT, which is smaller than 1/5 election timeout
|
||||
return time.Second + time.Duration(c.ElectionTicks)*time.Duration(c.TickMs)*time.Millisecond/5
|
||||
}
|
||||
|
||||
func (c *ServerConfig) PrintWithInitial() { c.print(true) }
|
||||
|
||||
func (c *ServerConfig) Print() { c.print(false) }
|
||||
|
||||
func (c *ServerConfig) print(initial bool) {
|
||||
plog.Infof("name = %s", c.Name)
|
||||
if c.ForceNewCluster {
|
||||
plog.Infof("force new cluster")
|
||||
}
|
||||
plog.Infof("data dir = %s", c.DataDir)
|
||||
plog.Infof("member dir = %s", c.MemberDir())
|
||||
if c.DedicatedWALDir != "" {
|
||||
plog.Infof("dedicated WAL dir = %s", c.DedicatedWALDir)
|
||||
}
|
||||
plog.Infof("heartbeat = %dms", c.TickMs)
|
||||
plog.Infof("election = %dms", c.ElectionTicks*int(c.TickMs))
|
||||
plog.Infof("snapshot count = %d", c.SnapCount)
|
||||
if len(c.DiscoveryURL) != 0 {
|
||||
plog.Infof("discovery URL= %s", c.DiscoveryURL)
|
||||
if len(c.DiscoveryProxy) != 0 {
|
||||
plog.Infof("discovery proxy = %s", c.DiscoveryProxy)
|
||||
}
|
||||
}
|
||||
plog.Infof("advertise client URLs = %s", c.ClientURLs)
|
||||
if initial {
|
||||
plog.Infof("initial advertise peer URLs = %s", c.PeerURLs)
|
||||
plog.Infof("initial cluster = %s", c.InitialPeerURLsMap)
|
||||
}
|
||||
}
|
||||
|
||||
func checkDuplicateURL(urlsmap types.URLsMap) bool {
|
||||
um := make(map[string]bool)
|
||||
for _, urls := range urlsmap {
|
||||
for _, url := range urls {
|
||||
u := url.String()
|
||||
if um[u] {
|
||||
return true
|
||||
}
|
||||
um[u] = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ServerConfig) bootstrapTimeout() time.Duration {
|
||||
if c.BootstrapTimeout != 0 {
|
||||
return c.BootstrapTimeout
|
||||
}
|
||||
return time.Second
|
||||
}
|
||||
196
vendor/github.com/coreos/etcd/etcdserver/config_test.go
generated
vendored
Normal file
196
vendor/github.com/coreos/etcd/etcdserver/config_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
func mustNewURLs(t *testing.T, urls []string) []url.URL {
|
||||
if len(urls) == 0 {
|
||||
return nil
|
||||
}
|
||||
u, err := types.NewURLs(urls)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating new URLs from %q: %v", urls, err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func TestConfigVerifyBootstrapWithoutClusterAndDiscoveryURLFail(t *testing.T) {
|
||||
c := &ServerConfig{
|
||||
Name: "node1",
|
||||
DiscoveryURL: "",
|
||||
InitialPeerURLsMap: types.URLsMap{},
|
||||
}
|
||||
if err := c.VerifyBootstrap(); err == nil {
|
||||
t.Errorf("err = nil, want not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigVerifyExistingWithDiscoveryURLFail(t *testing.T) {
|
||||
cluster, err := types.NewURLsMap("node1=http://127.0.0.1:2380")
|
||||
if err != nil {
|
||||
t.Fatalf("NewCluster error: %v", err)
|
||||
}
|
||||
c := &ServerConfig{
|
||||
Name: "node1",
|
||||
DiscoveryURL: "http://127.0.0.1:2379/abcdefg",
|
||||
PeerURLs: mustNewURLs(t, []string{"http://127.0.0.1:2380"}),
|
||||
InitialPeerURLsMap: cluster,
|
||||
NewCluster: false,
|
||||
}
|
||||
if err := c.VerifyJoinExisting(); err == nil {
|
||||
t.Errorf("err = nil, want not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigVerifyLocalMember(t *testing.T) {
|
||||
tests := []struct {
|
||||
clusterSetting string
|
||||
apurls []string
|
||||
strict bool
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
// Node must exist in cluster
|
||||
"",
|
||||
nil,
|
||||
true,
|
||||
|
||||
true,
|
||||
},
|
||||
{
|
||||
// Initial cluster set
|
||||
"node1=http://localhost:7001,node2=http://localhost:7002",
|
||||
[]string{"http://localhost:7001"},
|
||||
true,
|
||||
|
||||
false,
|
||||
},
|
||||
{
|
||||
// Default initial cluster
|
||||
"node1=http://localhost:2380,node1=http://localhost:7001",
|
||||
[]string{"http://localhost:2380", "http://localhost:7001"},
|
||||
true,
|
||||
|
||||
false,
|
||||
},
|
||||
{
|
||||
// Advertised peer URLs must match those in cluster-state
|
||||
"node1=http://localhost:7001",
|
||||
[]string{"http://localhost:12345"},
|
||||
true,
|
||||
|
||||
true,
|
||||
},
|
||||
{
|
||||
// Advertised peer URLs must match those in cluster-state
|
||||
"node1=http://localhost:2380,node1=http://localhost:12345",
|
||||
[]string{"http://localhost:12345"},
|
||||
true,
|
||||
|
||||
true,
|
||||
},
|
||||
{
|
||||
// Advertised peer URLs must match those in cluster-state
|
||||
"node1=http://localhost:2380",
|
||||
[]string{},
|
||||
true,
|
||||
|
||||
true,
|
||||
},
|
||||
{
|
||||
// do not care about the urls if strict is not set
|
||||
"node1=http://localhost:2380",
|
||||
[]string{},
|
||||
false,
|
||||
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
cluster, err := types.NewURLsMap(tt.clusterSetting)
|
||||
if err != nil {
|
||||
t.Fatalf("#%d: Got unexpected error: %v", i, err)
|
||||
}
|
||||
cfg := ServerConfig{
|
||||
Name: "node1",
|
||||
InitialPeerURLsMap: cluster,
|
||||
}
|
||||
if tt.apurls != nil {
|
||||
cfg.PeerURLs = mustNewURLs(t, tt.apurls)
|
||||
}
|
||||
if err = cfg.hasLocalMember(); err == nil && tt.strict {
|
||||
err = cfg.advertiseMatchesCluster()
|
||||
}
|
||||
if (err == nil) && tt.shouldError {
|
||||
t.Errorf("#%d: Got no error where one was expected", i)
|
||||
}
|
||||
if (err != nil) && !tt.shouldError {
|
||||
t.Errorf("#%d: Got unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapDir(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"/": "/member/snap",
|
||||
"/var/lib/etc": "/var/lib/etc/member/snap",
|
||||
}
|
||||
for dd, w := range tests {
|
||||
cfg := ServerConfig{
|
||||
DataDir: dd,
|
||||
}
|
||||
if g := cfg.SnapDir(); g != w {
|
||||
t.Errorf("DataDir=%q: SnapDir()=%q, want=%q", dd, g, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWALDir(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"/": "/member/wal",
|
||||
"/var/lib/etc": "/var/lib/etc/member/wal",
|
||||
}
|
||||
for dd, w := range tests {
|
||||
cfg := ServerConfig{
|
||||
DataDir: dd,
|
||||
}
|
||||
if g := cfg.WALDir(); g != w {
|
||||
t.Errorf("DataDir=%q: WALDir()=%q, want=%q", dd, g, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDiscover(t *testing.T) {
|
||||
tests := map[string]bool{
|
||||
"": false,
|
||||
"foo": true,
|
||||
"http://discovery.etcd.io/asdf": true,
|
||||
}
|
||||
for durl, w := range tests {
|
||||
cfg := ServerConfig{
|
||||
DiscoveryURL: durl,
|
||||
}
|
||||
if g := cfg.ShouldDiscover(); g != w {
|
||||
t.Errorf("durl=%q: ShouldDiscover()=%t, want=%t", durl, g, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
33
vendor/github.com/coreos/etcd/etcdserver/consistent_index.go
generated
vendored
Normal file
33
vendor/github.com/coreos/etcd/etcdserver/consistent_index.go
generated
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// consistentIndex represents the offset of an entry in a consistent replica log.
|
||||
// It implements the mvcc.ConsistentIndexGetter interface.
|
||||
// It is always set to the offset of current entry before executing the entry,
|
||||
// so ConsistentWatchableKV could get the consistent index from it.
|
||||
type consistentIndex uint64
|
||||
|
||||
func (i *consistentIndex) setConsistentIndex(v uint64) {
|
||||
atomic.StoreUint64((*uint64)(i), v)
|
||||
}
|
||||
|
||||
func (i *consistentIndex) ConsistentIndex() uint64 {
|
||||
return atomic.LoadUint64((*uint64)(i))
|
||||
}
|
||||
25
vendor/github.com/coreos/etcd/etcdserver/consistent_index_test.go
generated
vendored
Normal file
25
vendor/github.com/coreos/etcd/etcdserver/consistent_index_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestConsistentIndex(t *testing.T) {
|
||||
var i consistentIndex
|
||||
i.setConsistentIndex(10)
|
||||
if g := i.ConsistentIndex(); g != 10 {
|
||||
t.Errorf("value = %d, want 10", g)
|
||||
}
|
||||
}
|
||||
16
vendor/github.com/coreos/etcd/etcdserver/doc.go
generated
vendored
Normal file
16
vendor/github.com/coreos/etcd/etcdserver/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package etcdserver defines how etcd servers interact and store their states.
|
||||
package etcdserver
|
||||
45
vendor/github.com/coreos/etcd/etcdserver/errors.go
generated
vendored
Normal file
45
vendor/github.com/coreos/etcd/etcdserver/errors.go
generated
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownMethod = errors.New("etcdserver: unknown method")
|
||||
ErrStopped = errors.New("etcdserver: server stopped")
|
||||
ErrCanceled = errors.New("etcdserver: request cancelled")
|
||||
ErrTimeout = errors.New("etcdserver: request timed out")
|
||||
ErrTimeoutDueToLeaderFail = errors.New("etcdserver: request timed out, possibly due to previous leader failure")
|
||||
ErrTimeoutDueToConnectionLost = errors.New("etcdserver: request timed out, possibly due to connection lost")
|
||||
ErrTimeoutLeaderTransfer = errors.New("etcdserver: request timed out, leader transfer took too long")
|
||||
ErrNotEnoughStartedMembers = errors.New("etcdserver: re-configuration failed due to not enough started members")
|
||||
ErrNoLeader = errors.New("etcdserver: no leader")
|
||||
ErrRequestTooLarge = errors.New("etcdserver: request is too large")
|
||||
ErrNoSpace = errors.New("etcdserver: no space")
|
||||
ErrTooManyRequests = errors.New("etcdserver: too many requests")
|
||||
ErrUnhealthy = errors.New("etcdserver: unhealthy cluster")
|
||||
)
|
||||
|
||||
type DiscoveryError struct {
|
||||
Op string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e DiscoveryError) Error() string {
|
||||
return fmt.Sprintf("failed to %s discovery cluster (%v)", e.Op, e.Err)
|
||||
}
|
||||
1045
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.pb.go
generated
vendored
Normal file
1045
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
34
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.proto
generated
vendored
Normal file
34
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.proto
generated
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
syntax = "proto2";
|
||||
package etcdserverpb;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
|
||||
option (gogoproto.marshaler_all) = true;
|
||||
option (gogoproto.sizer_all) = true;
|
||||
option (gogoproto.unmarshaler_all) = true;
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
|
||||
message Request {
|
||||
optional uint64 ID = 1 [(gogoproto.nullable) = false];
|
||||
optional string Method = 2 [(gogoproto.nullable) = false];
|
||||
optional string Path = 3 [(gogoproto.nullable) = false];
|
||||
optional string Val = 4 [(gogoproto.nullable) = false];
|
||||
optional bool Dir = 5 [(gogoproto.nullable) = false];
|
||||
optional string PrevValue = 6 [(gogoproto.nullable) = false];
|
||||
optional uint64 PrevIndex = 7 [(gogoproto.nullable) = false];
|
||||
optional bool PrevExist = 8 [(gogoproto.nullable) = true];
|
||||
optional int64 Expiration = 9 [(gogoproto.nullable) = false];
|
||||
optional bool Wait = 10 [(gogoproto.nullable) = false];
|
||||
optional uint64 Since = 11 [(gogoproto.nullable) = false];
|
||||
optional bool Recursive = 12 [(gogoproto.nullable) = false];
|
||||
optional bool Sorted = 13 [(gogoproto.nullable) = false];
|
||||
optional bool Quorum = 14 [(gogoproto.nullable) = false];
|
||||
optional int64 Time = 15 [(gogoproto.nullable) = false];
|
||||
optional bool Stream = 16 [(gogoproto.nullable) = false];
|
||||
optional bool Refresh = 17 [(gogoproto.nullable) = true];
|
||||
}
|
||||
|
||||
message Metadata {
|
||||
optional uint64 NodeID = 1 [(gogoproto.nullable) = false];
|
||||
optional uint64 ClusterID = 2 [(gogoproto.nullable) = false];
|
||||
}
|
||||
2094
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.pb.go
generated
vendored
Normal file
2094
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
74
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.proto
generated
vendored
Normal file
74
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.proto
generated
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
syntax = "proto3";
|
||||
package etcdserverpb;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "etcdserver.proto";
|
||||
import "rpc.proto";
|
||||
|
||||
option (gogoproto.marshaler_all) = true;
|
||||
option (gogoproto.sizer_all) = true;
|
||||
option (gogoproto.unmarshaler_all) = true;
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
|
||||
message RequestHeader {
|
||||
uint64 ID = 1;
|
||||
// username is a username that is associated with an auth token of gRPC connection
|
||||
string username = 2;
|
||||
// auth_revision is a revision number of auth.authStore. It is not related to mvcc
|
||||
uint64 auth_revision = 3;
|
||||
}
|
||||
|
||||
// An InternalRaftRequest is the union of all requests which can be
|
||||
// sent via raft.
|
||||
message InternalRaftRequest {
|
||||
RequestHeader header = 100;
|
||||
uint64 ID = 1;
|
||||
|
||||
Request v2 = 2;
|
||||
|
||||
RangeRequest range = 3;
|
||||
PutRequest put = 4;
|
||||
DeleteRangeRequest delete_range = 5;
|
||||
TxnRequest txn = 6;
|
||||
CompactionRequest compaction = 7;
|
||||
|
||||
LeaseGrantRequest lease_grant = 8;
|
||||
LeaseRevokeRequest lease_revoke = 9;
|
||||
|
||||
AlarmRequest alarm = 10;
|
||||
|
||||
AuthEnableRequest auth_enable = 1000;
|
||||
AuthDisableRequest auth_disable = 1011;
|
||||
|
||||
InternalAuthenticateRequest authenticate = 1012;
|
||||
|
||||
AuthUserAddRequest auth_user_add = 1100;
|
||||
AuthUserDeleteRequest auth_user_delete = 1101;
|
||||
AuthUserGetRequest auth_user_get = 1102;
|
||||
AuthUserChangePasswordRequest auth_user_change_password = 1103;
|
||||
AuthUserGrantRoleRequest auth_user_grant_role = 1104;
|
||||
AuthUserRevokeRoleRequest auth_user_revoke_role = 1105;
|
||||
AuthUserListRequest auth_user_list = 1106;
|
||||
AuthRoleListRequest auth_role_list = 1107;
|
||||
|
||||
AuthRoleAddRequest auth_role_add = 1200;
|
||||
AuthRoleDeleteRequest auth_role_delete = 1201;
|
||||
AuthRoleGetRequest auth_role_get = 1202;
|
||||
AuthRoleGrantPermissionRequest auth_role_grant_permission = 1203;
|
||||
AuthRoleRevokePermissionRequest auth_role_revoke_permission = 1204;
|
||||
}
|
||||
|
||||
message EmptyResponse {
|
||||
}
|
||||
|
||||
// What is the difference between AuthenticateRequest (defined in rpc.proto) and InternalAuthenticateRequest?
|
||||
// InternalAuthenticateRequest has a member that is filled by etcdserver and shouldn't be user-facing.
|
||||
// For avoiding misusage the field, we have an internal version of AuthenticateRequest.
|
||||
message InternalAuthenticateRequest {
|
||||
string name = 1;
|
||||
string password = 2;
|
||||
|
||||
// simple_token is generated in API layer (etcdserver/v3_server.go)
|
||||
string simple_token = 3;
|
||||
}
|
||||
|
||||
16258
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.pb.go
generated
vendored
Normal file
16258
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
1911
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.pb.gw.go
generated
vendored
Normal file
1911
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.pb.gw.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
966
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.proto
generated
vendored
Normal file
966
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.proto
generated
vendored
Normal file
|
|
@ -0,0 +1,966 @@
|
|||
syntax = "proto3";
|
||||
package etcdserverpb;
|
||||
|
||||
import "gogoproto/gogo.proto";
|
||||
import "etcd/mvcc/mvccpb/kv.proto";
|
||||
import "etcd/auth/authpb/auth.proto";
|
||||
|
||||
// for grpc-gateway
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
option (gogoproto.marshaler_all) = true;
|
||||
option (gogoproto.unmarshaler_all) = true;
|
||||
|
||||
service KV {
|
||||
// Range gets the keys in the range from the key-value store.
|
||||
rpc Range(RangeRequest) returns (RangeResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/range"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Put puts the given key into the key-value store.
|
||||
// A put request increments the revision of the key-value store
|
||||
// and generates one event in the event history.
|
||||
rpc Put(PutRequest) returns (PutResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/put"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// DeleteRange deletes the given range from the key-value store.
|
||||
// A delete request increments the revision of the key-value store
|
||||
// and generates a delete event in the event history for every deleted key.
|
||||
rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/deleterange"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Txn processes multiple requests in a single transaction.
|
||||
// A txn request increments the revision of the key-value store
|
||||
// and generates events with the same revision for every completed request.
|
||||
// It is not allowed to modify the same key several times within one txn.
|
||||
rpc Txn(TxnRequest) returns (TxnResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/txn"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Compact compacts the event history in the etcd key-value store. The key-value
|
||||
// store should be periodically compacted or the event history will continue to grow
|
||||
// indefinitely.
|
||||
rpc Compact(CompactionRequest) returns (CompactionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/compaction"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
service Watch {
|
||||
// Watch watches for events happening or that have happened. Both input and output
|
||||
// are streams; the input stream is for creating and canceling watchers and the output
|
||||
// stream sends events. One watch RPC can watch on multiple key ranges, streaming events
|
||||
// for several watches at once. The entire event history can be watched starting from the
|
||||
// last compaction revision.
|
||||
rpc Watch(stream WatchRequest) returns (stream WatchResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/watch"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
service Lease {
|
||||
// LeaseGrant creates a lease which expires if the server does not receive a keepAlive
|
||||
// within a given time to live period. All keys attached to the lease will be expired and
|
||||
// deleted if the lease expires. Each expired key generates a delete event in the event history.
|
||||
rpc LeaseGrant(LeaseGrantRequest) returns (LeaseGrantResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/lease/grant"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// LeaseRevoke revokes a lease. All keys attached to the lease will expire and be deleted.
|
||||
rpc LeaseRevoke(LeaseRevokeRequest) returns (LeaseRevokeResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/lease/revoke"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// LeaseKeepAlive keeps the lease alive by streaming keep alive requests from the client
|
||||
// to the server and streaming keep alive responses from the server to the client.
|
||||
rpc LeaseKeepAlive(stream LeaseKeepAliveRequest) returns (stream LeaseKeepAliveResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/lease/keepalive"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// LeaseTimeToLive retrieves lease information.
|
||||
rpc LeaseTimeToLive(LeaseTimeToLiveRequest) returns (LeaseTimeToLiveResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/kv/lease/timetolive"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(xiangli) List all existing Leases?
|
||||
}
|
||||
|
||||
service Cluster {
|
||||
// MemberAdd adds a member into the cluster.
|
||||
rpc MemberAdd(MemberAddRequest) returns (MemberAddResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/cluster/member/add"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// MemberRemove removes an existing member from the cluster.
|
||||
rpc MemberRemove(MemberRemoveRequest) returns (MemberRemoveResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/cluster/member/remove"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// MemberUpdate updates the member configuration.
|
||||
rpc MemberUpdate(MemberUpdateRequest) returns (MemberUpdateResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/cluster/member/update"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// MemberList lists all the members in the cluster.
|
||||
rpc MemberList(MemberListRequest) returns (MemberListResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/cluster/member/list"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
service Maintenance {
|
||||
// Alarm activates, deactivates, and queries alarms regarding cluster health.
|
||||
rpc Alarm(AlarmRequest) returns (AlarmResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/maintenance/alarm"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Status gets the status of the member.
|
||||
rpc Status(StatusRequest) returns (StatusResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/maintenance/status"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Defragment defragments a member's backend database to recover storage space.
|
||||
rpc Defragment(DefragmentRequest) returns (DefragmentResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/maintenance/defragment"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Hash returns the hash of the local KV state for consistency checking purpose.
|
||||
// This is designed for testing; do not use this in production when there
|
||||
// are ongoing transactions.
|
||||
rpc Hash(HashRequest) returns (HashResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/maintenance/hash"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Snapshot sends a snapshot of the entire backend from a member over a stream to a client.
|
||||
rpc Snapshot(SnapshotRequest) returns (stream SnapshotResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/maintenance/snapshot"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
service Auth {
|
||||
// AuthEnable enables authentication.
|
||||
rpc AuthEnable(AuthEnableRequest) returns (AuthEnableResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/enable"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// AuthDisable disables authentication.
|
||||
rpc AuthDisable(AuthDisableRequest) returns (AuthDisableResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/disable"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// Authenticate processes an authenticate request.
|
||||
rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/authenticate"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserAdd adds a new user.
|
||||
rpc UserAdd(AuthUserAddRequest) returns (AuthUserAddResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/add"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserGet gets detailed user information.
|
||||
rpc UserGet(AuthUserGetRequest) returns (AuthUserGetResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/get"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserList gets a list of all users.
|
||||
rpc UserList(AuthUserListRequest) returns (AuthUserListResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/list"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserDelete deletes a specified user.
|
||||
rpc UserDelete(AuthUserDeleteRequest) returns (AuthUserDeleteResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/delete"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserChangePassword changes the password of a specified user.
|
||||
rpc UserChangePassword(AuthUserChangePasswordRequest) returns (AuthUserChangePasswordResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/changepw"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserGrant grants a role to a specified user.
|
||||
rpc UserGrantRole(AuthUserGrantRoleRequest) returns (AuthUserGrantRoleResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/grant"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// UserRevokeRole revokes a role of specified user.
|
||||
rpc UserRevokeRole(AuthUserRevokeRoleRequest) returns (AuthUserRevokeRoleResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/user/revoke"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RoleAdd adds a new role.
|
||||
rpc RoleAdd(AuthRoleAddRequest) returns (AuthRoleAddResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/role/add"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RoleGet gets detailed role information.
|
||||
rpc RoleGet(AuthRoleGetRequest) returns (AuthRoleGetResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/role/get"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RoleList gets lists of all roles.
|
||||
rpc RoleList(AuthRoleListRequest) returns (AuthRoleListResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/role/list"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RoleDelete deletes a specified role.
|
||||
rpc RoleDelete(AuthRoleDeleteRequest) returns (AuthRoleDeleteResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/role/delete"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RoleGrantPermission grants a permission of a specified key or range to a specified role.
|
||||
rpc RoleGrantPermission(AuthRoleGrantPermissionRequest) returns (AuthRoleGrantPermissionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/role/grant"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RoleRevokePermission revokes a key or range permission of a specified role.
|
||||
rpc RoleRevokePermission(AuthRoleRevokePermissionRequest) returns (AuthRoleRevokePermissionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v3alpha/auth/role/revoke"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message ResponseHeader {
|
||||
// cluster_id is the ID of the cluster which sent the response.
|
||||
uint64 cluster_id = 1;
|
||||
// member_id is the ID of the member which sent the response.
|
||||
uint64 member_id = 2;
|
||||
// revision is the key-value store revision when the request was applied.
|
||||
int64 revision = 3;
|
||||
// raft_term is the raft term when the request was applied.
|
||||
uint64 raft_term = 4;
|
||||
}
|
||||
|
||||
message RangeRequest {
|
||||
enum SortOrder {
|
||||
NONE = 0; // default, no sorting
|
||||
ASCEND = 1; // lowest target value first
|
||||
DESCEND = 2; // highest target value first
|
||||
}
|
||||
enum SortTarget {
|
||||
KEY = 0;
|
||||
VERSION = 1;
|
||||
CREATE = 2;
|
||||
MOD = 3;
|
||||
VALUE = 4;
|
||||
}
|
||||
|
||||
// key is the first key for the range. If range_end is not given, the request only looks up key.
|
||||
bytes key = 1;
|
||||
// range_end is the upper bound on the requested range [key, range_end).
|
||||
// If range_end is '\0', the range is all keys >= key.
|
||||
// If the range_end is one bit larger than the given key,
|
||||
// then the range requests get the all keys with the prefix (the given key).
|
||||
// If both key and range_end are '\0', then range requests returns all keys.
|
||||
bytes range_end = 2;
|
||||
// limit is a limit on the number of keys returned for the request.
|
||||
int64 limit = 3;
|
||||
// revision is the point-in-time of the key-value store to use for the range.
|
||||
// If revision is less or equal to zero, the range is over the newest key-value store.
|
||||
// If the revision has been compacted, ErrCompacted is returned as a response.
|
||||
int64 revision = 4;
|
||||
|
||||
// sort_order is the order for returned sorted results.
|
||||
SortOrder sort_order = 5;
|
||||
|
||||
// sort_target is the key-value field to use for sorting.
|
||||
SortTarget sort_target = 6;
|
||||
|
||||
// serializable sets the range request to use serializable member-local reads.
|
||||
// Range requests are linearizable by default; linearizable requests have higher
|
||||
// latency and lower throughput than serializable requests but reflect the current
|
||||
// consensus of the cluster. For better performance, in exchange for possible stale reads,
|
||||
// a serializable range request is served locally without needing to reach consensus
|
||||
// with other nodes in the cluster.
|
||||
bool serializable = 7;
|
||||
|
||||
// keys_only when set returns only the keys and not the values.
|
||||
bool keys_only = 8;
|
||||
|
||||
// count_only when set returns only the count of the keys in the range.
|
||||
bool count_only = 9;
|
||||
|
||||
// min_mod_revision is the lower bound for returned key mod revisions; all keys with
|
||||
// lesser mod revisions will be filtered away.
|
||||
int64 min_mod_revision = 10;
|
||||
|
||||
// max_mod_revision is the upper bound for returned key mod revisions; all keys with
|
||||
// greater mod revisions will be filtered away.
|
||||
int64 max_mod_revision = 11;
|
||||
|
||||
// min_create_revision is the lower bound for returned key create revisions; all keys with
|
||||
// lesser create trevisions will be filtered away.
|
||||
int64 min_create_revision = 12;
|
||||
|
||||
// max_create_revision is the upper bound for returned key create revisions; all keys with
|
||||
// greater create revisions will be filtered away.
|
||||
int64 max_create_revision = 13;
|
||||
}
|
||||
|
||||
message RangeResponse {
|
||||
ResponseHeader header = 1;
|
||||
// kvs is the list of key-value pairs matched by the range request.
|
||||
// kvs is empty when count is requested.
|
||||
repeated mvccpb.KeyValue kvs = 2;
|
||||
// more indicates if there are more keys to return in the requested range.
|
||||
bool more = 3;
|
||||
// count is set to the number of keys within the range when requested.
|
||||
int64 count = 4;
|
||||
}
|
||||
|
||||
message PutRequest {
|
||||
// key is the key, in bytes, to put into the key-value store.
|
||||
bytes key = 1;
|
||||
// value is the value, in bytes, to associate with the key in the key-value store.
|
||||
bytes value = 2;
|
||||
// lease is the lease ID to associate with the key in the key-value store. A lease
|
||||
// value of 0 indicates no lease.
|
||||
int64 lease = 3;
|
||||
|
||||
// If prev_kv is set, etcd gets the previous key-value pair before changing it.
|
||||
// The previous key-value pair will be returned in the put response.
|
||||
bool prev_kv = 4;
|
||||
}
|
||||
|
||||
message PutResponse {
|
||||
ResponseHeader header = 1;
|
||||
// if prev_kv is set in the request, the previous key-value pair will be returned.
|
||||
mvccpb.KeyValue prev_kv = 2;
|
||||
}
|
||||
|
||||
message DeleteRangeRequest {
|
||||
// key is the first key to delete in the range.
|
||||
bytes key = 1;
|
||||
// range_end is the key following the last key to delete for the range [key, range_end).
|
||||
// If range_end is not given, the range is defined to contain only the key argument.
|
||||
// If range_end is one bit larger than the given key, then the range is all
|
||||
// the all keys with the prefix (the given key).
|
||||
// If range_end is '\0', the range is all keys greater than or equal to the key argument.
|
||||
bytes range_end = 2;
|
||||
|
||||
// If prev_kv is set, etcd gets the previous key-value pairs before deleting it.
|
||||
// The previous key-value pairs will be returned in the delte response.
|
||||
bool prev_kv = 3;
|
||||
}
|
||||
|
||||
message DeleteRangeResponse {
|
||||
ResponseHeader header = 1;
|
||||
// deleted is the number of keys deleted by the delete range request.
|
||||
int64 deleted = 2;
|
||||
// if prev_kv is set in the request, the previous key-value pairs will be returned.
|
||||
repeated mvccpb.KeyValue prev_kvs = 3;
|
||||
}
|
||||
|
||||
message RequestOp {
|
||||
// request is a union of request types accepted by a transaction.
|
||||
oneof request {
|
||||
RangeRequest request_range = 1;
|
||||
PutRequest request_put = 2;
|
||||
DeleteRangeRequest request_delete_range = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ResponseOp {
|
||||
// response is a union of response types returned by a transaction.
|
||||
oneof response {
|
||||
RangeResponse response_range = 1;
|
||||
PutResponse response_put = 2;
|
||||
DeleteRangeResponse response_delete_range = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Compare {
|
||||
enum CompareResult {
|
||||
EQUAL = 0;
|
||||
GREATER = 1;
|
||||
LESS = 2;
|
||||
NOT_EQUAL = 3;
|
||||
}
|
||||
enum CompareTarget {
|
||||
VERSION = 0;
|
||||
CREATE = 1;
|
||||
MOD = 2;
|
||||
VALUE= 3;
|
||||
}
|
||||
// result is logical comparison operation for this comparison.
|
||||
CompareResult result = 1;
|
||||
// target is the key-value field to inspect for the comparison.
|
||||
CompareTarget target = 2;
|
||||
// key is the subject key for the comparison operation.
|
||||
bytes key = 3;
|
||||
oneof target_union {
|
||||
// version is the version of the given key
|
||||
int64 version = 4;
|
||||
// create_revision is the creation revision of the given key
|
||||
int64 create_revision = 5;
|
||||
// mod_revision is the last modified revision of the given key.
|
||||
int64 mod_revision = 6;
|
||||
// value is the value of the given key, in bytes.
|
||||
bytes value = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// From google paxosdb paper:
|
||||
// Our implementation hinges around a powerful primitive which we call MultiOp. All other database
|
||||
// operations except for iteration are implemented as a single call to MultiOp. A MultiOp is applied atomically
|
||||
// and consists of three components:
|
||||
// 1. A list of tests called guard. Each test in guard checks a single entry in the database. It may check
|
||||
// for the absence or presence of a value, or compare with a given value. Two different tests in the guard
|
||||
// may apply to the same or different entries in the database. All tests in the guard are applied and
|
||||
// MultiOp returns the results. If all tests are true, MultiOp executes t op (see item 2 below), otherwise
|
||||
// it executes f op (see item 3 below).
|
||||
// 2. A list of database operations called t op. Each operation in the list is either an insert, delete, or
|
||||
// lookup operation, and applies to a single database entry. Two different operations in the list may apply
|
||||
// to the same or different entries in the database. These operations are executed
|
||||
// if guard evaluates to
|
||||
// true.
|
||||
// 3. A list of database operations called f op. Like t op, but executed if guard evaluates to false.
|
||||
message TxnRequest {
|
||||
// compare is a list of predicates representing a conjunction of terms.
|
||||
// If the comparisons succeed, then the success requests will be processed in order,
|
||||
// and the response will contain their respective responses in order.
|
||||
// If the comparisons fail, then the failure requests will be processed in order,
|
||||
// and the response will contain their respective responses in order.
|
||||
repeated Compare compare = 1;
|
||||
// success is a list of requests which will be applied when compare evaluates to true.
|
||||
repeated RequestOp success = 2;
|
||||
// failure is a list of requests which will be applied when compare evaluates to false.
|
||||
repeated RequestOp failure = 3;
|
||||
}
|
||||
|
||||
message TxnResponse {
|
||||
ResponseHeader header = 1;
|
||||
// succeeded is set to true if the compare evaluated to true or false otherwise.
|
||||
bool succeeded = 2;
|
||||
// responses is a list of responses corresponding to the results from applying
|
||||
// success if succeeded is true or failure if succeeded is false.
|
||||
repeated ResponseOp responses = 3;
|
||||
}
|
||||
|
||||
// CompactionRequest compacts the key-value store up to a given revision. All superseded keys
|
||||
// with a revision less than the compaction revision will be removed.
|
||||
message CompactionRequest {
|
||||
// revision is the key-value store revision for the compaction operation.
|
||||
int64 revision = 1;
|
||||
// physical is set so the RPC will wait until the compaction is physically
|
||||
// applied to the local database such that compacted entries are totally
|
||||
// removed from the backend database.
|
||||
bool physical = 2;
|
||||
}
|
||||
|
||||
message CompactionResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message HashRequest {
|
||||
}
|
||||
|
||||
message HashResponse {
|
||||
ResponseHeader header = 1;
|
||||
// hash is the hash value computed from the responding member's key-value store.
|
||||
uint32 hash = 2;
|
||||
}
|
||||
|
||||
message SnapshotRequest {
|
||||
}
|
||||
|
||||
message SnapshotResponse {
|
||||
// header has the current key-value store information. The first header in the snapshot
|
||||
// stream indicates the point in time of the snapshot.
|
||||
ResponseHeader header = 1;
|
||||
|
||||
// remaining_bytes is the number of blob bytes to be sent after this message
|
||||
uint64 remaining_bytes = 2;
|
||||
|
||||
// blob contains the next chunk of the snapshot in the snapshot stream.
|
||||
bytes blob = 3;
|
||||
}
|
||||
|
||||
message WatchRequest {
|
||||
// request_union is a request to either create a new watcher or cancel an existing watcher.
|
||||
oneof request_union {
|
||||
WatchCreateRequest create_request = 1;
|
||||
WatchCancelRequest cancel_request = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message WatchCreateRequest {
|
||||
// key is the key to register for watching.
|
||||
bytes key = 1;
|
||||
// range_end is the end of the range [key, range_end) to watch. If range_end is not given,
|
||||
// only the key argument is watched. If range_end is equal to '\0', all keys greater than
|
||||
// or equal to the key argument are watched.
|
||||
// If the range_end is one bit larger than the given key,
|
||||
// then all keys with the prefix (the given key) will be watched.
|
||||
bytes range_end = 2;
|
||||
// start_revision is an optional revision to watch from (inclusive). No start_revision is "now".
|
||||
int64 start_revision = 3;
|
||||
// progress_notify is set so that the etcd server will periodically send a WatchResponse with
|
||||
// no events to the new watcher if there are no recent events. It is useful when clients
|
||||
// wish to recover a disconnected watcher starting from a recent known revision.
|
||||
// The etcd server may decide how often it will send notifications based on current load.
|
||||
bool progress_notify = 4;
|
||||
|
||||
enum FilterType {
|
||||
// filter out put event.
|
||||
NOPUT = 0;
|
||||
// filter out delete event.
|
||||
NODELETE = 1;
|
||||
}
|
||||
// filters filter the events at server side before it sends back to the watcher.
|
||||
repeated FilterType filters = 5;
|
||||
|
||||
// If prev_kv is set, created watcher gets the previous KV before the event happens.
|
||||
// If the previous KV is already compacted, nothing will be returned.
|
||||
bool prev_kv = 6;
|
||||
}
|
||||
|
||||
message WatchCancelRequest {
|
||||
// watch_id is the watcher id to cancel so that no more events are transmitted.
|
||||
int64 watch_id = 1;
|
||||
}
|
||||
|
||||
message WatchResponse {
|
||||
ResponseHeader header = 1;
|
||||
// watch_id is the ID of the watcher that corresponds to the response.
|
||||
int64 watch_id = 2;
|
||||
// created is set to true if the response is for a create watch request.
|
||||
// The client should record the watch_id and expect to receive events for
|
||||
// the created watcher from the same stream.
|
||||
// All events sent to the created watcher will attach with the same watch_id.
|
||||
bool created = 3;
|
||||
// canceled is set to true if the response is for a cancel watch request.
|
||||
// No further events will be sent to the canceled watcher.
|
||||
bool canceled = 4;
|
||||
// compact_revision is set to the minimum index if a watcher tries to watch
|
||||
// at a compacted index.
|
||||
//
|
||||
// This happens when creating a watcher at a compacted revision or the watcher cannot
|
||||
// catch up with the progress of the key-value store.
|
||||
//
|
||||
// The client should treat the watcher as canceled and should not try to create any
|
||||
// watcher with the same start_revision again.
|
||||
int64 compact_revision = 5;
|
||||
|
||||
repeated mvccpb.Event events = 11;
|
||||
}
|
||||
|
||||
message LeaseGrantRequest {
|
||||
// TTL is the advisory time-to-live in seconds.
|
||||
int64 TTL = 1;
|
||||
// ID is the requested ID for the lease. If ID is set to 0, the lessor chooses an ID.
|
||||
int64 ID = 2;
|
||||
}
|
||||
|
||||
message LeaseGrantResponse {
|
||||
ResponseHeader header = 1;
|
||||
// ID is the lease ID for the granted lease.
|
||||
int64 ID = 2;
|
||||
// TTL is the server chosen lease time-to-live in seconds.
|
||||
int64 TTL = 3;
|
||||
string error = 4;
|
||||
}
|
||||
|
||||
message LeaseRevokeRequest {
|
||||
// ID is the lease ID to revoke. When the ID is revoked, all associated keys will be deleted.
|
||||
int64 ID = 1;
|
||||
}
|
||||
|
||||
message LeaseRevokeResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message LeaseKeepAliveRequest {
|
||||
// ID is the lease ID for the lease to keep alive.
|
||||
int64 ID = 1;
|
||||
}
|
||||
|
||||
message LeaseKeepAliveResponse {
|
||||
ResponseHeader header = 1;
|
||||
// ID is the lease ID from the keep alive request.
|
||||
int64 ID = 2;
|
||||
// TTL is the new time-to-live for the lease.
|
||||
int64 TTL = 3;
|
||||
}
|
||||
|
||||
message LeaseTimeToLiveRequest {
|
||||
// ID is the lease ID for the lease.
|
||||
int64 ID = 1;
|
||||
// keys is true to query all the keys attached to this lease.
|
||||
bool keys = 2;
|
||||
}
|
||||
|
||||
message LeaseTimeToLiveResponse {
|
||||
ResponseHeader header = 1;
|
||||
// ID is the lease ID from the keep alive request.
|
||||
int64 ID = 2;
|
||||
// TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds.
|
||||
int64 TTL = 3;
|
||||
// GrantedTTL is the initial granted time in seconds upon lease creation/renewal.
|
||||
int64 grantedTTL = 4;
|
||||
// Keys is the list of keys attached to this lease.
|
||||
repeated bytes keys = 5;
|
||||
}
|
||||
|
||||
message Member {
|
||||
// ID is the member ID for this member.
|
||||
uint64 ID = 1;
|
||||
// name is the human-readable name of the member. If the member is not started, the name will be an empty string.
|
||||
string name = 2;
|
||||
// peerURLs is the list of URLs the member exposes to the cluster for communication.
|
||||
repeated string peerURLs = 3;
|
||||
// clientURLs is the list of URLs the member exposes to clients for communication. If the member is not started, clientURLs will be empty.
|
||||
repeated string clientURLs = 4;
|
||||
}
|
||||
|
||||
message MemberAddRequest {
|
||||
// peerURLs is the list of URLs the added member will use to communicate with the cluster.
|
||||
repeated string peerURLs = 1;
|
||||
}
|
||||
|
||||
message MemberAddResponse {
|
||||
ResponseHeader header = 1;
|
||||
// member is the member information for the added member.
|
||||
Member member = 2;
|
||||
}
|
||||
|
||||
message MemberRemoveRequest {
|
||||
// ID is the member ID of the member to remove.
|
||||
uint64 ID = 1;
|
||||
}
|
||||
|
||||
message MemberRemoveResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message MemberUpdateRequest {
|
||||
// ID is the member ID of the member to update.
|
||||
uint64 ID = 1;
|
||||
// peerURLs is the new list of URLs the member will use to communicate with the cluster.
|
||||
repeated string peerURLs = 2;
|
||||
}
|
||||
|
||||
message MemberUpdateResponse{
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message MemberListRequest {
|
||||
}
|
||||
|
||||
message MemberListResponse {
|
||||
ResponseHeader header = 1;
|
||||
// members is a list of all members associated with the cluster.
|
||||
repeated Member members = 2;
|
||||
}
|
||||
|
||||
message DefragmentRequest {
|
||||
}
|
||||
|
||||
message DefragmentResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
enum AlarmType {
|
||||
NONE = 0; // default, used to query if any alarm is active
|
||||
NOSPACE = 1; // space quota is exhausted
|
||||
}
|
||||
|
||||
message AlarmRequest {
|
||||
enum AlarmAction {
|
||||
GET = 0;
|
||||
ACTIVATE = 1;
|
||||
DEACTIVATE = 2;
|
||||
}
|
||||
// action is the kind of alarm request to issue. The action
|
||||
// may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a
|
||||
// raised alarm.
|
||||
AlarmAction action = 1;
|
||||
// memberID is the ID of the member associated with the alarm. If memberID is 0, the
|
||||
// alarm request covers all members.
|
||||
uint64 memberID = 2;
|
||||
// alarm is the type of alarm to consider for this request.
|
||||
AlarmType alarm = 3;
|
||||
}
|
||||
|
||||
message AlarmMember {
|
||||
// memberID is the ID of the member associated with the raised alarm.
|
||||
uint64 memberID = 1;
|
||||
// alarm is the type of alarm which has been raised.
|
||||
AlarmType alarm = 2;
|
||||
}
|
||||
|
||||
message AlarmResponse {
|
||||
ResponseHeader header = 1;
|
||||
// alarms is a list of alarms associated with the alarm request.
|
||||
repeated AlarmMember alarms = 2;
|
||||
}
|
||||
|
||||
message StatusRequest {
|
||||
}
|
||||
|
||||
message StatusResponse {
|
||||
ResponseHeader header = 1;
|
||||
// version is the cluster protocol version used by the responding member.
|
||||
string version = 2;
|
||||
// dbSize is the size of the backend database, in bytes, of the responding member.
|
||||
int64 dbSize = 3;
|
||||
// leader is the member ID which the responding member believes is the current leader.
|
||||
uint64 leader = 4;
|
||||
// raftIndex is the current raft index of the responding member.
|
||||
uint64 raftIndex = 5;
|
||||
// raftTerm is the current raft term of the responding member.
|
||||
uint64 raftTerm = 6;
|
||||
}
|
||||
|
||||
message AuthEnableRequest {
|
||||
}
|
||||
|
||||
message AuthDisableRequest {
|
||||
}
|
||||
|
||||
message AuthenticateRequest {
|
||||
string name = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message AuthUserAddRequest {
|
||||
string name = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message AuthUserGetRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message AuthUserDeleteRequest {
|
||||
// name is the name of the user to delete.
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message AuthUserChangePasswordRequest {
|
||||
// name is the name of the user whose password is being changed.
|
||||
string name = 1;
|
||||
// password is the new password for the user.
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message AuthUserGrantRoleRequest {
|
||||
// user is the name of the user which should be granted a given role.
|
||||
string user = 1;
|
||||
// role is the name of the role to grant to the user.
|
||||
string role = 2;
|
||||
}
|
||||
|
||||
message AuthUserRevokeRoleRequest {
|
||||
string name = 1;
|
||||
string role = 2;
|
||||
}
|
||||
|
||||
message AuthRoleAddRequest {
|
||||
// name is the name of the role to add to the authentication system.
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message AuthRoleGetRequest {
|
||||
string role = 1;
|
||||
}
|
||||
|
||||
message AuthUserListRequest {
|
||||
}
|
||||
|
||||
message AuthRoleListRequest {
|
||||
}
|
||||
|
||||
message AuthRoleDeleteRequest {
|
||||
string role = 1;
|
||||
}
|
||||
|
||||
message AuthRoleGrantPermissionRequest {
|
||||
// name is the name of the role which will be granted the permission.
|
||||
string name = 1;
|
||||
// perm is the permission to grant to the role.
|
||||
authpb.Permission perm = 2;
|
||||
}
|
||||
|
||||
message AuthRoleRevokePermissionRequest {
|
||||
string role = 1;
|
||||
string key = 2;
|
||||
string range_end = 3;
|
||||
}
|
||||
|
||||
message AuthEnableResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthDisableResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthenticateResponse {
|
||||
ResponseHeader header = 1;
|
||||
// token is an authorized token that can be used in succeeding RPCs
|
||||
string token = 2;
|
||||
}
|
||||
|
||||
message AuthUserAddResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthUserGetResponse {
|
||||
ResponseHeader header = 1;
|
||||
|
||||
repeated string roles = 2;
|
||||
}
|
||||
|
||||
message AuthUserDeleteResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthUserChangePasswordResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthUserGrantRoleResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthUserRevokeRoleResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthRoleAddResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthRoleGetResponse {
|
||||
ResponseHeader header = 1;
|
||||
|
||||
repeated authpb.Permission perm = 2;
|
||||
}
|
||||
|
||||
message AuthRoleListResponse {
|
||||
ResponseHeader header = 1;
|
||||
|
||||
repeated string roles = 2;
|
||||
}
|
||||
|
||||
message AuthUserListResponse {
|
||||
ResponseHeader header = 1;
|
||||
|
||||
repeated string users = 2;
|
||||
}
|
||||
|
||||
message AuthRoleDeleteResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthRoleGrantPermissionResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
|
||||
message AuthRoleRevokePermissionResponse {
|
||||
ResponseHeader header = 1;
|
||||
}
|
||||
512
vendor/github.com/coreos/etcd/etcdserver/membership/cluster.go
generated
vendored
Normal file
512
vendor/github.com/coreos/etcd/etcdserver/membership/cluster.go
generated
vendored
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package membership
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/pkg/netutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/etcd/version"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
// RaftCluster is a list of Members that belong to the same raft cluster
|
||||
type RaftCluster struct {
|
||||
id types.ID
|
||||
token string
|
||||
|
||||
store store.Store
|
||||
be backend.Backend
|
||||
|
||||
sync.Mutex // guards the fields below
|
||||
version *semver.Version
|
||||
members map[types.ID]*Member
|
||||
// removed contains the ids of removed members in the cluster.
|
||||
// removed id cannot be reused.
|
||||
removed map[types.ID]bool
|
||||
}
|
||||
|
||||
func NewClusterFromURLsMap(token string, urlsmap types.URLsMap) (*RaftCluster, error) {
|
||||
c := NewCluster(token)
|
||||
for name, urls := range urlsmap {
|
||||
m := NewMember(name, urls, token, nil)
|
||||
if _, ok := c.members[m.ID]; ok {
|
||||
return nil, fmt.Errorf("member exists with identical ID %v", m)
|
||||
}
|
||||
if uint64(m.ID) == raft.None {
|
||||
return nil, fmt.Errorf("cannot use %x as member id", raft.None)
|
||||
}
|
||||
c.members[m.ID] = m
|
||||
}
|
||||
c.genID()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewClusterFromMembers(token string, id types.ID, membs []*Member) *RaftCluster {
|
||||
c := NewCluster(token)
|
||||
c.id = id
|
||||
for _, m := range membs {
|
||||
c.members[m.ID] = m
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func NewCluster(token string) *RaftCluster {
|
||||
return &RaftCluster{
|
||||
token: token,
|
||||
members: make(map[types.ID]*Member),
|
||||
removed: make(map[types.ID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RaftCluster) ID() types.ID { return c.id }
|
||||
|
||||
func (c *RaftCluster) Members() []*Member {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
var ms MembersByID
|
||||
for _, m := range c.members {
|
||||
ms = append(ms, m.Clone())
|
||||
}
|
||||
sort.Sort(ms)
|
||||
return []*Member(ms)
|
||||
}
|
||||
|
||||
func (c *RaftCluster) Member(id types.ID) *Member {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.members[id].Clone()
|
||||
}
|
||||
|
||||
// MemberByName returns a Member with the given name if exists.
|
||||
// If more than one member has the given name, it will panic.
|
||||
func (c *RaftCluster) MemberByName(name string) *Member {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
var memb *Member
|
||||
for _, m := range c.members {
|
||||
if m.Name == name {
|
||||
if memb != nil {
|
||||
plog.Panicf("two members with the given name %q exist", name)
|
||||
}
|
||||
memb = m
|
||||
}
|
||||
}
|
||||
return memb.Clone()
|
||||
}
|
||||
|
||||
func (c *RaftCluster) MemberIDs() []types.ID {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
var ids []types.ID
|
||||
for _, m := range c.members {
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
sort.Sort(types.IDSlice(ids))
|
||||
return ids
|
||||
}
|
||||
|
||||
func (c *RaftCluster) IsIDRemoved(id types.ID) bool {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.removed[id]
|
||||
}
|
||||
|
||||
// PeerURLs returns a list of all peer addresses.
|
||||
// The returned list is sorted in ascending lexicographical order.
|
||||
func (c *RaftCluster) PeerURLs() []string {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
urls := make([]string, 0)
|
||||
for _, p := range c.members {
|
||||
urls = append(urls, p.PeerURLs...)
|
||||
}
|
||||
sort.Strings(urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
// ClientURLs returns a list of all client addresses.
|
||||
// The returned list is sorted in ascending lexicographical order.
|
||||
func (c *RaftCluster) ClientURLs() []string {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
urls := make([]string, 0)
|
||||
for _, p := range c.members {
|
||||
urls = append(urls, p.ClientURLs...)
|
||||
}
|
||||
sort.Strings(urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
func (c *RaftCluster) String() string {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
b := &bytes.Buffer{}
|
||||
fmt.Fprintf(b, "{ClusterID:%s ", c.id)
|
||||
var ms []string
|
||||
for _, m := range c.members {
|
||||
ms = append(ms, fmt.Sprintf("%+v", m))
|
||||
}
|
||||
fmt.Fprintf(b, "Members:[%s] ", strings.Join(ms, " "))
|
||||
var ids []string
|
||||
for id := range c.removed {
|
||||
ids = append(ids, fmt.Sprintf("%s", id))
|
||||
}
|
||||
fmt.Fprintf(b, "RemovedMemberIDs:[%s]}", strings.Join(ids, " "))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c *RaftCluster) genID() {
|
||||
mIDs := c.MemberIDs()
|
||||
b := make([]byte, 8*len(mIDs))
|
||||
for i, id := range mIDs {
|
||||
binary.BigEndian.PutUint64(b[8*i:], uint64(id))
|
||||
}
|
||||
hash := sha1.Sum(b)
|
||||
c.id = types.ID(binary.BigEndian.Uint64(hash[:8]))
|
||||
}
|
||||
|
||||
func (c *RaftCluster) SetID(id types.ID) { c.id = id }
|
||||
|
||||
func (c *RaftCluster) SetStore(st store.Store) { c.store = st }
|
||||
|
||||
func (c *RaftCluster) SetBackend(be backend.Backend) {
|
||||
c.be = be
|
||||
mustCreateBackendBuckets(c.be)
|
||||
}
|
||||
|
||||
func (c *RaftCluster) Recover(onSet func(*semver.Version)) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.members, c.removed = membersFromStore(c.store)
|
||||
c.version = clusterVersionFromStore(c.store)
|
||||
mustDetectDowngrade(c.version)
|
||||
onSet(c.version)
|
||||
|
||||
for _, m := range c.members {
|
||||
plog.Infof("added member %s %v to cluster %s from store", m.ID, m.PeerURLs, c.id)
|
||||
}
|
||||
if c.version != nil {
|
||||
plog.Infof("set the cluster version to %v from store", version.Cluster(c.version.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateConfigurationChange takes a proposed ConfChange and
|
||||
// ensures that it is still valid.
|
||||
func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange) error {
|
||||
members, removed := membersFromStore(c.store)
|
||||
id := types.ID(cc.NodeID)
|
||||
if removed[id] {
|
||||
return ErrIDRemoved
|
||||
}
|
||||
switch cc.Type {
|
||||
case raftpb.ConfChangeAddNode:
|
||||
if members[id] != nil {
|
||||
return ErrIDExists
|
||||
}
|
||||
urls := make(map[string]bool)
|
||||
for _, m := range members {
|
||||
for _, u := range m.PeerURLs {
|
||||
urls[u] = true
|
||||
}
|
||||
}
|
||||
m := new(Member)
|
||||
if err := json.Unmarshal(cc.Context, m); err != nil {
|
||||
plog.Panicf("unmarshal member should never fail: %v", err)
|
||||
}
|
||||
for _, u := range m.PeerURLs {
|
||||
if urls[u] {
|
||||
return ErrPeerURLexists
|
||||
}
|
||||
}
|
||||
case raftpb.ConfChangeRemoveNode:
|
||||
if members[id] == nil {
|
||||
return ErrIDNotFound
|
||||
}
|
||||
case raftpb.ConfChangeUpdateNode:
|
||||
if members[id] == nil {
|
||||
return ErrIDNotFound
|
||||
}
|
||||
urls := make(map[string]bool)
|
||||
for _, m := range members {
|
||||
if m.ID == id {
|
||||
continue
|
||||
}
|
||||
for _, u := range m.PeerURLs {
|
||||
urls[u] = true
|
||||
}
|
||||
}
|
||||
m := new(Member)
|
||||
if err := json.Unmarshal(cc.Context, m); err != nil {
|
||||
plog.Panicf("unmarshal member should never fail: %v", err)
|
||||
}
|
||||
for _, u := range m.PeerURLs {
|
||||
if urls[u] {
|
||||
return ErrPeerURLexists
|
||||
}
|
||||
}
|
||||
default:
|
||||
plog.Panicf("ConfChange type should be either AddNode, RemoveNode or UpdateNode")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddMember adds a new Member into the cluster, and saves the given member's
|
||||
// raftAttributes into the store. The given member should have empty attributes.
|
||||
// A Member with a matching id must not exist.
|
||||
func (c *RaftCluster) AddMember(m *Member) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.store != nil {
|
||||
mustSaveMemberToStore(c.store, m)
|
||||
}
|
||||
if c.be != nil {
|
||||
mustSaveMemberToBackend(c.be, m)
|
||||
}
|
||||
|
||||
c.members[m.ID] = m
|
||||
|
||||
plog.Infof("added member %s %v to cluster %s", m.ID, m.PeerURLs, c.id)
|
||||
}
|
||||
|
||||
// RemoveMember removes a member from the store.
|
||||
// The given id MUST exist, or the function panics.
|
||||
func (c *RaftCluster) RemoveMember(id types.ID) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.store != nil {
|
||||
mustDeleteMemberFromStore(c.store, id)
|
||||
}
|
||||
if c.be != nil {
|
||||
mustDeleteMemberFromBackend(c.be, id)
|
||||
}
|
||||
|
||||
delete(c.members, id)
|
||||
c.removed[id] = true
|
||||
|
||||
plog.Infof("removed member %s from cluster %s", id, c.id)
|
||||
}
|
||||
|
||||
func (c *RaftCluster) UpdateAttributes(id types.ID, attr Attributes) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if m, ok := c.members[id]; ok {
|
||||
m.Attributes = attr
|
||||
if c.store != nil {
|
||||
mustUpdateMemberAttrInStore(c.store, m)
|
||||
}
|
||||
if c.be != nil {
|
||||
mustSaveMemberToBackend(c.be, m)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, ok := c.removed[id]
|
||||
if !ok {
|
||||
plog.Panicf("error updating attributes of unknown member %s", id)
|
||||
}
|
||||
plog.Warningf("skipped updating attributes of removed member %s", id)
|
||||
}
|
||||
|
||||
func (c *RaftCluster) UpdateRaftAttributes(id types.ID, raftAttr RaftAttributes) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.members[id].RaftAttributes = raftAttr
|
||||
if c.store != nil {
|
||||
mustUpdateMemberInStore(c.store, c.members[id])
|
||||
}
|
||||
if c.be != nil {
|
||||
mustSaveMemberToBackend(c.be, c.members[id])
|
||||
}
|
||||
|
||||
plog.Noticef("updated member %s %v in cluster %s", id, raftAttr.PeerURLs, c.id)
|
||||
}
|
||||
|
||||
func (c *RaftCluster) Version() *semver.Version {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.version == nil {
|
||||
return nil
|
||||
}
|
||||
return semver.Must(semver.NewVersion(c.version.String()))
|
||||
}
|
||||
|
||||
func (c *RaftCluster) SetVersion(ver *semver.Version, onSet func(*semver.Version)) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.version != nil {
|
||||
plog.Noticef("updated the cluster version from %v to %v", version.Cluster(c.version.String()), version.Cluster(ver.String()))
|
||||
} else {
|
||||
plog.Noticef("set the initial cluster version to %v", version.Cluster(ver.String()))
|
||||
}
|
||||
c.version = ver
|
||||
mustDetectDowngrade(c.version)
|
||||
if c.store != nil {
|
||||
mustSaveClusterVersionToStore(c.store, ver)
|
||||
}
|
||||
if c.be != nil {
|
||||
mustSaveClusterVersionToBackend(c.be, ver)
|
||||
}
|
||||
onSet(ver)
|
||||
}
|
||||
|
||||
func (c *RaftCluster) IsReadyToAddNewMember() bool {
|
||||
nmembers := 1
|
||||
nstarted := 0
|
||||
|
||||
for _, member := range c.members {
|
||||
if member.IsStarted() {
|
||||
nstarted++
|
||||
}
|
||||
nmembers++
|
||||
}
|
||||
|
||||
if nstarted == 1 && nmembers == 2 {
|
||||
// a case of adding a new node to 1-member cluster for restoring cluster data
|
||||
// https://github.com/coreos/etcd/blob/master/Documentation/v2/admin_guide.md#restoring-the-cluster
|
||||
|
||||
plog.Debugf("The number of started member is 1. This cluster can accept add member request.")
|
||||
return true
|
||||
}
|
||||
|
||||
nquorum := nmembers/2 + 1
|
||||
if nstarted < nquorum {
|
||||
plog.Warningf("Reject add member request: the number of started member (%d) will be less than the quorum number of the cluster (%d)", nstarted, nquorum)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *RaftCluster) IsReadyToRemoveMember(id uint64) bool {
|
||||
nmembers := 0
|
||||
nstarted := 0
|
||||
|
||||
for _, member := range c.members {
|
||||
if uint64(member.ID) == id {
|
||||
continue
|
||||
}
|
||||
|
||||
if member.IsStarted() {
|
||||
nstarted++
|
||||
}
|
||||
nmembers++
|
||||
}
|
||||
|
||||
nquorum := nmembers/2 + 1
|
||||
if nstarted < nquorum {
|
||||
plog.Warningf("Reject remove member request: the number of started member (%d) will be less than the quorum number of the cluster (%d)", nstarted, nquorum)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func membersFromStore(st store.Store) (map[types.ID]*Member, map[types.ID]bool) {
|
||||
members := make(map[types.ID]*Member)
|
||||
removed := make(map[types.ID]bool)
|
||||
e, err := st.Get(StoreMembersPrefix, true, true)
|
||||
if err != nil {
|
||||
if isKeyNotFound(err) {
|
||||
return members, removed
|
||||
}
|
||||
plog.Panicf("get storeMembers should never fail: %v", err)
|
||||
}
|
||||
for _, n := range e.Node.Nodes {
|
||||
var m *Member
|
||||
m, err = nodeToMember(n)
|
||||
if err != nil {
|
||||
plog.Panicf("nodeToMember should never fail: %v", err)
|
||||
}
|
||||
members[m.ID] = m
|
||||
}
|
||||
|
||||
e, err = st.Get(storeRemovedMembersPrefix, true, true)
|
||||
if err != nil {
|
||||
if isKeyNotFound(err) {
|
||||
return members, removed
|
||||
}
|
||||
plog.Panicf("get storeRemovedMembers should never fail: %v", err)
|
||||
}
|
||||
for _, n := range e.Node.Nodes {
|
||||
removed[MustParseMemberIDFromKey(n.Key)] = true
|
||||
}
|
||||
return members, removed
|
||||
}
|
||||
|
||||
func clusterVersionFromStore(st store.Store) *semver.Version {
|
||||
e, err := st.Get(path.Join(storePrefix, "version"), false, false)
|
||||
if err != nil {
|
||||
if isKeyNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
plog.Panicf("unexpected error (%v) when getting cluster version from store", err)
|
||||
}
|
||||
return semver.Must(semver.NewVersion(*e.Node.Value))
|
||||
}
|
||||
|
||||
// ValidateClusterAndAssignIDs validates the local cluster by matching the PeerURLs
|
||||
// with the existing cluster. If the validation succeeds, it assigns the IDs
|
||||
// from the existing cluster to the local cluster.
|
||||
// If the validation fails, an error will be returned.
|
||||
func ValidateClusterAndAssignIDs(local *RaftCluster, existing *RaftCluster) error {
|
||||
ems := existing.Members()
|
||||
lms := local.Members()
|
||||
if len(ems) != len(lms) {
|
||||
return fmt.Errorf("member count is unequal")
|
||||
}
|
||||
sort.Sort(MembersByPeerURLs(ems))
|
||||
sort.Sort(MembersByPeerURLs(lms))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
|
||||
defer cancel()
|
||||
for i := range ems {
|
||||
if !netutil.URLStringsEqual(ctx, ems[i].PeerURLs, lms[i].PeerURLs) {
|
||||
return fmt.Errorf("unmatched member while checking PeerURLs")
|
||||
}
|
||||
lms[i].ID = ems[i].ID
|
||||
}
|
||||
local.members = make(map[types.ID]*Member)
|
||||
for _, m := range lms {
|
||||
local.members[m.ID] = m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustDetectDowngrade(cv *semver.Version) {
|
||||
lv := semver.Must(semver.NewVersion(version.Version))
|
||||
// only keep major.minor version for comparison against cluster version
|
||||
lv = &semver.Version{Major: lv.Major, Minor: lv.Minor}
|
||||
if cv != nil && lv.LessThan(*cv) {
|
||||
plog.Fatalf("cluster cannot be downgraded (current version: %s is lower than determined cluster version: %s).", version.Version, version.Cluster(cv.String()))
|
||||
}
|
||||
}
|
||||
734
vendor/github.com/coreos/etcd/etcdserver/membership/cluster_test.go
generated
vendored
Normal file
734
vendor/github.com/coreos/etcd/etcdserver/membership/cluster_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,734 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package membership
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/pkg/mock/mockstore"
|
||||
"github.com/coreos/etcd/pkg/testutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/store"
|
||||
)
|
||||
|
||||
func TestClusterMember(t *testing.T) {
|
||||
membs := []*Member{
|
||||
newTestMember(1, nil, "node1", nil),
|
||||
newTestMember(2, nil, "node2", nil),
|
||||
}
|
||||
tests := []struct {
|
||||
id types.ID
|
||||
match bool
|
||||
}{
|
||||
{1, true},
|
||||
{2, true},
|
||||
{3, false},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(membs)
|
||||
m := c.Member(tt.id)
|
||||
if g := m != nil; g != tt.match {
|
||||
t.Errorf("#%d: find member = %v, want %v", i, g, tt.match)
|
||||
}
|
||||
if m != nil && m.ID != tt.id {
|
||||
t.Errorf("#%d: id = %x, want %x", i, m.ID, tt.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterMemberByName(t *testing.T) {
|
||||
membs := []*Member{
|
||||
newTestMember(1, nil, "node1", nil),
|
||||
newTestMember(2, nil, "node2", nil),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
match bool
|
||||
}{
|
||||
{"node1", true},
|
||||
{"node2", true},
|
||||
{"node3", false},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(membs)
|
||||
m := c.MemberByName(tt.name)
|
||||
if g := m != nil; g != tt.match {
|
||||
t.Errorf("#%d: find member = %v, want %v", i, g, tt.match)
|
||||
}
|
||||
if m != nil && m.Name != tt.name {
|
||||
t.Errorf("#%d: name = %v, want %v", i, m.Name, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterMemberIDs(t *testing.T) {
|
||||
c := newTestCluster([]*Member{
|
||||
newTestMember(1, nil, "", nil),
|
||||
newTestMember(4, nil, "", nil),
|
||||
newTestMember(100, nil, "", nil),
|
||||
})
|
||||
w := []types.ID{1, 4, 100}
|
||||
g := c.MemberIDs()
|
||||
if !reflect.DeepEqual(w, g) {
|
||||
t.Errorf("IDs = %+v, want %+v", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterPeerURLs(t *testing.T) {
|
||||
tests := []struct {
|
||||
mems []*Member
|
||||
wurls []string
|
||||
}{
|
||||
// single peer with a single address
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(1, []string{"http://192.0.2.1"}, "", nil),
|
||||
},
|
||||
wurls: []string{"http://192.0.2.1"},
|
||||
},
|
||||
|
||||
// single peer with a single address with a port
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(1, []string{"http://192.0.2.1:8001"}, "", nil),
|
||||
},
|
||||
wurls: []string{"http://192.0.2.1:8001"},
|
||||
},
|
||||
|
||||
// several members explicitly unsorted
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(2, []string{"http://192.0.2.3", "http://192.0.2.4"}, "", nil),
|
||||
newTestMember(3, []string{"http://192.0.2.5", "http://192.0.2.6"}, "", nil),
|
||||
newTestMember(1, []string{"http://192.0.2.1", "http://192.0.2.2"}, "", nil),
|
||||
},
|
||||
wurls: []string{"http://192.0.2.1", "http://192.0.2.2", "http://192.0.2.3", "http://192.0.2.4", "http://192.0.2.5", "http://192.0.2.6"},
|
||||
},
|
||||
|
||||
// no members
|
||||
{
|
||||
mems: []*Member{},
|
||||
wurls: []string{},
|
||||
},
|
||||
|
||||
// peer with no peer urls
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(3, []string{}, "", nil),
|
||||
},
|
||||
wurls: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(tt.mems)
|
||||
urls := c.PeerURLs()
|
||||
if !reflect.DeepEqual(urls, tt.wurls) {
|
||||
t.Errorf("#%d: PeerURLs = %v, want %v", i, urls, tt.wurls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterClientURLs(t *testing.T) {
|
||||
tests := []struct {
|
||||
mems []*Member
|
||||
wurls []string
|
||||
}{
|
||||
// single peer with a single address
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(1, nil, "", []string{"http://192.0.2.1"}),
|
||||
},
|
||||
wurls: []string{"http://192.0.2.1"},
|
||||
},
|
||||
|
||||
// single peer with a single address with a port
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(1, nil, "", []string{"http://192.0.2.1:8001"}),
|
||||
},
|
||||
wurls: []string{"http://192.0.2.1:8001"},
|
||||
},
|
||||
|
||||
// several members explicitly unsorted
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(2, nil, "", []string{"http://192.0.2.3", "http://192.0.2.4"}),
|
||||
newTestMember(3, nil, "", []string{"http://192.0.2.5", "http://192.0.2.6"}),
|
||||
newTestMember(1, nil, "", []string{"http://192.0.2.1", "http://192.0.2.2"}),
|
||||
},
|
||||
wurls: []string{"http://192.0.2.1", "http://192.0.2.2", "http://192.0.2.3", "http://192.0.2.4", "http://192.0.2.5", "http://192.0.2.6"},
|
||||
},
|
||||
|
||||
// no members
|
||||
{
|
||||
mems: []*Member{},
|
||||
wurls: []string{},
|
||||
},
|
||||
|
||||
// peer with no client urls
|
||||
{
|
||||
mems: []*Member{
|
||||
newTestMember(3, nil, "", []string{}),
|
||||
},
|
||||
wurls: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(tt.mems)
|
||||
urls := c.ClientURLs()
|
||||
if !reflect.DeepEqual(urls, tt.wurls) {
|
||||
t.Errorf("#%d: ClientURLs = %v, want %v", i, urls, tt.wurls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterValidateAndAssignIDsBad(t *testing.T) {
|
||||
tests := []struct {
|
||||
clmembs []*Member
|
||||
membs []*Member
|
||||
}{
|
||||
{
|
||||
// unmatched length
|
||||
[]*Member{
|
||||
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
|
||||
},
|
||||
[]*Member{},
|
||||
},
|
||||
{
|
||||
// unmatched peer urls
|
||||
[]*Member{
|
||||
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
|
||||
},
|
||||
[]*Member{
|
||||
newTestMember(1, []string{"http://127.0.0.1:4001"}, "", nil),
|
||||
},
|
||||
},
|
||||
{
|
||||
// unmatched peer urls
|
||||
[]*Member{
|
||||
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
|
||||
newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
|
||||
},
|
||||
[]*Member{
|
||||
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
|
||||
newTestMember(2, []string{"http://127.0.0.2:4001"}, "", nil),
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
ecl := newTestCluster(tt.clmembs)
|
||||
lcl := newTestCluster(tt.membs)
|
||||
if err := ValidateClusterAndAssignIDs(lcl, ecl); err == nil {
|
||||
t.Errorf("#%d: unexpected update success", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterValidateAndAssignIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
clmembs []*Member
|
||||
membs []*Member
|
||||
wids []types.ID
|
||||
}{
|
||||
{
|
||||
[]*Member{
|
||||
newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
|
||||
newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
|
||||
},
|
||||
[]*Member{
|
||||
newTestMember(3, []string{"http://127.0.0.1:2379"}, "", nil),
|
||||
newTestMember(4, []string{"http://127.0.0.2:2379"}, "", nil),
|
||||
},
|
||||
[]types.ID{3, 4},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
lcl := newTestCluster(tt.clmembs)
|
||||
ecl := newTestCluster(tt.membs)
|
||||
if err := ValidateClusterAndAssignIDs(lcl, ecl); err != nil {
|
||||
t.Errorf("#%d: unexpect update error: %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(lcl.MemberIDs(), tt.wids) {
|
||||
t.Errorf("#%d: ids = %v, want %v", i, lcl.MemberIDs(), tt.wids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterValidateConfigurationChange(t *testing.T) {
|
||||
cl := NewCluster("")
|
||||
cl.SetStore(store.New())
|
||||
for i := 1; i <= 4; i++ {
|
||||
attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", i)}}
|
||||
cl.AddMember(&Member{ID: types.ID(i), RaftAttributes: attr})
|
||||
}
|
||||
cl.RemoveMember(4)
|
||||
|
||||
attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 1)}}
|
||||
ctx, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
|
||||
ctx5, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 3)}}
|
||||
ctx2to3, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
|
||||
ctx2to5, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
cc raftpb.ConfChange
|
||||
werr error
|
||||
}{
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeRemoveNode,
|
||||
NodeID: 3,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: 4,
|
||||
},
|
||||
ErrIDRemoved,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeRemoveNode,
|
||||
NodeID: 4,
|
||||
},
|
||||
ErrIDRemoved,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: 1,
|
||||
},
|
||||
ErrIDExists,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: 5,
|
||||
Context: ctx,
|
||||
},
|
||||
ErrPeerURLexists,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeRemoveNode,
|
||||
NodeID: 5,
|
||||
},
|
||||
ErrIDNotFound,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: 5,
|
||||
Context: ctx5,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeUpdateNode,
|
||||
NodeID: 5,
|
||||
Context: ctx,
|
||||
},
|
||||
ErrIDNotFound,
|
||||
},
|
||||
// try to change the peer url of 2 to the peer url of 3
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeUpdateNode,
|
||||
NodeID: 2,
|
||||
Context: ctx2to3,
|
||||
},
|
||||
ErrPeerURLexists,
|
||||
},
|
||||
{
|
||||
raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeUpdateNode,
|
||||
NodeID: 2,
|
||||
Context: ctx2to5,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
err := cl.ValidateConfigurationChange(tt.cc)
|
||||
if err != tt.werr {
|
||||
t.Errorf("#%d: validateConfigurationChange error = %v, want %v", i, err, tt.werr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterGenID(t *testing.T) {
|
||||
cs := newTestCluster([]*Member{
|
||||
newTestMember(1, nil, "", nil),
|
||||
newTestMember(2, nil, "", nil),
|
||||
})
|
||||
|
||||
cs.genID()
|
||||
if cs.ID() == 0 {
|
||||
t.Fatalf("cluster.ID = %v, want not 0", cs.ID())
|
||||
}
|
||||
previd := cs.ID()
|
||||
|
||||
cs.SetStore(mockstore.NewNop())
|
||||
cs.AddMember(newTestMember(3, nil, "", nil))
|
||||
cs.genID()
|
||||
if cs.ID() == previd {
|
||||
t.Fatalf("cluster.ID = %v, want not %v", cs.ID(), previd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeToMemberBad(t *testing.T) {
|
||||
tests := []*store.NodeExtern{
|
||||
{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/strange"},
|
||||
}},
|
||||
{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/raftAttributes", Value: stringp("garbage")},
|
||||
}},
|
||||
{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
|
||||
}},
|
||||
{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
|
||||
{Key: "/1234/strange"},
|
||||
}},
|
||||
{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
|
||||
{Key: "/1234/attributes", Value: stringp("garbage")},
|
||||
}},
|
||||
{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
|
||||
{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
|
||||
{Key: "/1234/strange"},
|
||||
}},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
if _, err := nodeToMember(tt); err == nil {
|
||||
t.Errorf("#%d: unexpected nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterAddMember(t *testing.T) {
|
||||
st := mockstore.NewRecorder()
|
||||
c := newTestCluster(nil)
|
||||
c.SetStore(st)
|
||||
c.AddMember(newTestMember(1, nil, "node1", nil))
|
||||
|
||||
wactions := []testutil.Action{
|
||||
{
|
||||
Name: "Create",
|
||||
Params: []interface{}{
|
||||
path.Join(StoreMembersPrefix, "1", "raftAttributes"),
|
||||
false,
|
||||
`{"peerURLs":null}`,
|
||||
false,
|
||||
store.TTLOptionSet{ExpireTime: store.Permanent},
|
||||
},
|
||||
},
|
||||
}
|
||||
if g := st.Action(); !reflect.DeepEqual(g, wactions) {
|
||||
t.Errorf("actions = %v, want %v", g, wactions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterMembers(t *testing.T) {
|
||||
cls := &RaftCluster{
|
||||
members: map[types.ID]*Member{
|
||||
1: {ID: 1},
|
||||
20: {ID: 20},
|
||||
100: {ID: 100},
|
||||
5: {ID: 5},
|
||||
50: {ID: 50},
|
||||
},
|
||||
}
|
||||
w := []*Member{
|
||||
{ID: 1},
|
||||
{ID: 5},
|
||||
{ID: 20},
|
||||
{ID: 50},
|
||||
{ID: 100},
|
||||
}
|
||||
if g := cls.Members(); !reflect.DeepEqual(g, w) {
|
||||
t.Fatalf("Members()=%#v, want %#v", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterRemoveMember(t *testing.T) {
|
||||
st := mockstore.NewRecorder()
|
||||
c := newTestCluster(nil)
|
||||
c.SetStore(st)
|
||||
c.RemoveMember(1)
|
||||
|
||||
wactions := []testutil.Action{
|
||||
{Name: "Delete", Params: []interface{}{MemberStoreKey(1), true, true}},
|
||||
{Name: "Create", Params: []interface{}{RemovedMemberStoreKey(1), false, "", false, store.TTLOptionSet{ExpireTime: store.Permanent}}},
|
||||
}
|
||||
if !reflect.DeepEqual(st.Action(), wactions) {
|
||||
t.Errorf("actions = %v, want %v", st.Action(), wactions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterUpdateAttributes(t *testing.T) {
|
||||
name := "etcd"
|
||||
clientURLs := []string{"http://127.0.0.1:4001"}
|
||||
tests := []struct {
|
||||
mems []*Member
|
||||
removed map[types.ID]bool
|
||||
wmems []*Member
|
||||
}{
|
||||
// update attributes of existing member
|
||||
{
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "", nil),
|
||||
},
|
||||
nil,
|
||||
[]*Member{
|
||||
newTestMember(1, nil, name, clientURLs),
|
||||
},
|
||||
},
|
||||
// update attributes of removed member
|
||||
{
|
||||
nil,
|
||||
map[types.ID]bool{types.ID(1): true},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(tt.mems)
|
||||
c.removed = tt.removed
|
||||
|
||||
c.UpdateAttributes(types.ID(1), Attributes{Name: name, ClientURLs: clientURLs})
|
||||
if g := c.Members(); !reflect.DeepEqual(g, tt.wmems) {
|
||||
t.Errorf("#%d: members = %+v, want %+v", i, g, tt.wmems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeToMember(t *testing.T) {
|
||||
n := &store.NodeExtern{Key: "/1234", Nodes: []*store.NodeExtern{
|
||||
{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
|
||||
{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
|
||||
}}
|
||||
wm := &Member{ID: 0x1234, RaftAttributes: RaftAttributes{}, Attributes: Attributes{Name: "node1"}}
|
||||
m, err := nodeToMember(n)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected nodeToMember error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(m, wm) {
|
||||
t.Errorf("member = %+v, want %+v", m, wm)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestCluster(membs []*Member) *RaftCluster {
|
||||
c := &RaftCluster{members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
|
||||
for _, m := range membs {
|
||||
c.members[m.ID] = m
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func stringp(s string) *string { return &s }
|
||||
|
||||
func TestIsReadyToAddNewMember(t *testing.T) {
|
||||
tests := []struct {
|
||||
members []*Member
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
// 0/3 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "", nil),
|
||||
newTestMember(2, nil, "", nil),
|
||||
newTestMember(3, nil, "", nil),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 1/2 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "", nil),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 1/3 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "", nil),
|
||||
newTestMember(3, nil, "", nil),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 1/1 members ready, should succeed (special case of 1-member cluster for recovery)
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 2/3 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "", nil),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 3/3 members ready, should be fine to add one member and retain quorum
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "3", nil),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 3/4 members ready, should be fine to add one member and retain quorum
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "3", nil),
|
||||
newTestMember(4, nil, "", nil),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
// empty cluster, it is impossible but should fail
|
||||
[]*Member{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(tt.members)
|
||||
if got := c.IsReadyToAddNewMember(); got != tt.want {
|
||||
t.Errorf("%d: isReadyToAddNewMember returned %t, want %t", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsReadyToRemoveMember(t *testing.T) {
|
||||
tests := []struct {
|
||||
members []*Member
|
||||
removeID uint64
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
// 1/1 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
},
|
||||
1,
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 0/3 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "", nil),
|
||||
newTestMember(2, nil, "", nil),
|
||||
newTestMember(3, nil, "", nil),
|
||||
},
|
||||
1,
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 1/2 members ready, should be fine to remove unstarted member
|
||||
// (isReadyToRemoveMember() logic should return success, but operation itself would fail)
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "", nil),
|
||||
},
|
||||
2,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 2/3 members ready, should fail
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "", nil),
|
||||
},
|
||||
2,
|
||||
false,
|
||||
},
|
||||
{
|
||||
// 3/3 members ready, should be fine to remove one member and retain quorum
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "3", nil),
|
||||
},
|
||||
3,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 3/4 members ready, should be fine to remove one member
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "3", nil),
|
||||
newTestMember(4, nil, "", nil),
|
||||
},
|
||||
3,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// 3/4 members ready, should be fine to remove unstarted member
|
||||
[]*Member{
|
||||
newTestMember(1, nil, "1", nil),
|
||||
newTestMember(2, nil, "2", nil),
|
||||
newTestMember(3, nil, "3", nil),
|
||||
newTestMember(4, nil, "", nil),
|
||||
},
|
||||
4,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := newTestCluster(tt.members)
|
||||
if got := c.IsReadyToRemoveMember(tt.removeID); got != tt.want {
|
||||
t.Errorf("%d: isReadyToAddNewMember returned %t, want %t", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
33
vendor/github.com/coreos/etcd/etcdserver/membership/errors.go
generated
vendored
Normal file
33
vendor/github.com/coreos/etcd/etcdserver/membership/errors.go
generated
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package membership
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrIDRemoved = errors.New("membership: ID removed")
|
||||
ErrIDExists = errors.New("membership: ID exists")
|
||||
ErrIDNotFound = errors.New("membership: ID not found")
|
||||
ErrPeerURLexists = errors.New("membership: peerURL exists")
|
||||
)
|
||||
|
||||
func isKeyNotFound(err error) bool {
|
||||
e, ok := err.(*etcdErr.Error)
|
||||
return ok && e.ErrorCode == etcdErr.EcodeKeyNotFound
|
||||
}
|
||||
124
vendor/github.com/coreos/etcd/etcdserver/membership/member.go
generated
vendored
Normal file
124
vendor/github.com/coreos/etcd/etcdserver/membership/member.go
generated
vendored
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package membership
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/membership")
|
||||
)
|
||||
|
||||
// RaftAttributes represents the raft related attributes of an etcd member.
|
||||
type RaftAttributes struct {
|
||||
// PeerURLs is the list of peers in the raft cluster.
|
||||
// TODO(philips): ensure these are URLs
|
||||
PeerURLs []string `json:"peerURLs"`
|
||||
}
|
||||
|
||||
// Attributes represents all the non-raft related attributes of an etcd member.
|
||||
type Attributes struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ClientURLs []string `json:"clientURLs,omitempty"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
ID types.ID `json:"id"`
|
||||
RaftAttributes
|
||||
Attributes
|
||||
}
|
||||
|
||||
// NewMember creates a Member without an ID and generates one based on the
|
||||
// cluster name, peer URLs, and time. This is used for bootstrapping/adding new member.
|
||||
func NewMember(name string, peerURLs types.URLs, clusterName string, now *time.Time) *Member {
|
||||
m := &Member{
|
||||
RaftAttributes: RaftAttributes{PeerURLs: peerURLs.StringSlice()},
|
||||
Attributes: Attributes{Name: name},
|
||||
}
|
||||
|
||||
var b []byte
|
||||
sort.Strings(m.PeerURLs)
|
||||
for _, p := range m.PeerURLs {
|
||||
b = append(b, []byte(p)...)
|
||||
}
|
||||
|
||||
b = append(b, []byte(clusterName)...)
|
||||
if now != nil {
|
||||
b = append(b, []byte(fmt.Sprintf("%d", now.Unix()))...)
|
||||
}
|
||||
|
||||
hash := sha1.Sum(b)
|
||||
m.ID = types.ID(binary.BigEndian.Uint64(hash[:8]))
|
||||
return m
|
||||
}
|
||||
|
||||
// PickPeerURL chooses a random address from a given Member's PeerURLs.
|
||||
// It will panic if there is no PeerURLs available in Member.
|
||||
func (m *Member) PickPeerURL() string {
|
||||
if len(m.PeerURLs) == 0 {
|
||||
plog.Panicf("member should always have some peer url")
|
||||
}
|
||||
return m.PeerURLs[rand.Intn(len(m.PeerURLs))]
|
||||
}
|
||||
|
||||
func (m *Member) Clone() *Member {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
mm := &Member{
|
||||
ID: m.ID,
|
||||
Attributes: Attributes{
|
||||
Name: m.Name,
|
||||
},
|
||||
}
|
||||
if m.PeerURLs != nil {
|
||||
mm.PeerURLs = make([]string, len(m.PeerURLs))
|
||||
copy(mm.PeerURLs, m.PeerURLs)
|
||||
}
|
||||
if m.ClientURLs != nil {
|
||||
mm.ClientURLs = make([]string, len(m.ClientURLs))
|
||||
copy(mm.ClientURLs, m.ClientURLs)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
|
||||
func (m *Member) IsStarted() bool {
|
||||
return len(m.Name) != 0
|
||||
}
|
||||
|
||||
// MembersByID implements sort by ID interface
|
||||
type MembersByID []*Member
|
||||
|
||||
func (ms MembersByID) Len() int { return len(ms) }
|
||||
func (ms MembersByID) Less(i, j int) bool { return ms[i].ID < ms[j].ID }
|
||||
func (ms MembersByID) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
|
||||
|
||||
// MembersByPeerURLs implements sort by peer urls interface
|
||||
type MembersByPeerURLs []*Member
|
||||
|
||||
func (ms MembersByPeerURLs) Len() int { return len(ms) }
|
||||
func (ms MembersByPeerURLs) Less(i, j int) bool {
|
||||
return ms[i].PeerURLs[0] < ms[j].PeerURLs[0]
|
||||
}
|
||||
func (ms MembersByPeerURLs) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
|
||||
115
vendor/github.com/coreos/etcd/etcdserver/membership/member_test.go
generated
vendored
Normal file
115
vendor/github.com/coreos/etcd/etcdserver/membership/member_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package membership
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
func timeParse(value string) *time.Time {
|
||||
t, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &t
|
||||
}
|
||||
|
||||
func TestMemberTime(t *testing.T) {
|
||||
tests := []struct {
|
||||
mem *Member
|
||||
id types.ID
|
||||
}{
|
||||
{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil), 14544069596553697298},
|
||||
// Same ID, different name (names shouldn't matter)
|
||||
{NewMember("memfoo", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", nil), 14544069596553697298},
|
||||
// Same ID, different Time
|
||||
{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "", timeParse("1984-12-23T15:04:05Z")), 2448790162483548276},
|
||||
// Different cluster name
|
||||
{NewMember("mcm1", []url.URL{{Scheme: "http", Host: "10.0.0.8:2379"}}, "etcd", timeParse("1984-12-23T15:04:05Z")), 6973882743191604649},
|
||||
{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.1:2379"}}, "", timeParse("1984-12-23T15:04:05Z")), 1466075294948436910},
|
||||
// Order shouldn't matter
|
||||
{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.1:2379"}, {Scheme: "http", Host: "10.0.0.2:2379"}}, "", nil), 16552244735972308939},
|
||||
{NewMember("mem1", []url.URL{{Scheme: "http", Host: "10.0.0.2:2379"}, {Scheme: "http", Host: "10.0.0.1:2379"}}, "", nil), 16552244735972308939},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
if tt.mem.ID != tt.id {
|
||||
t.Errorf("#%d: mem.ID = %v, want %v", i, tt.mem.ID, tt.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberPick(t *testing.T) {
|
||||
tests := []struct {
|
||||
memb *Member
|
||||
urls map[string]bool
|
||||
}{
|
||||
{
|
||||
newTestMember(1, []string{"abc", "def", "ghi", "jkl", "mno", "pqr", "stu"}, "", nil),
|
||||
map[string]bool{
|
||||
"abc": true,
|
||||
"def": true,
|
||||
"ghi": true,
|
||||
"jkl": true,
|
||||
"mno": true,
|
||||
"pqr": true,
|
||||
"stu": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
newTestMember(2, []string{"xyz"}, "", nil),
|
||||
map[string]bool{"xyz": true},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
for j := 0; j < 1000; j++ {
|
||||
a := tt.memb.PickPeerURL()
|
||||
if !tt.urls[a] {
|
||||
t.Errorf("#%d: returned ID %q not in expected range!", i, a)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberClone(t *testing.T) {
|
||||
tests := []*Member{
|
||||
newTestMember(1, nil, "abc", nil),
|
||||
newTestMember(1, []string{"http://a"}, "abc", nil),
|
||||
newTestMember(1, nil, "abc", []string{"http://b"}),
|
||||
newTestMember(1, []string{"http://a"}, "abc", []string{"http://b"}),
|
||||
}
|
||||
for i, tt := range tests {
|
||||
nm := tt.Clone()
|
||||
if nm == tt {
|
||||
t.Errorf("#%d: the pointers are the same, and clone doesn't happen", i)
|
||||
}
|
||||
if !reflect.DeepEqual(nm, tt) {
|
||||
t.Errorf("#%d: member = %+v, want %+v", i, nm, tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestMember(id uint64, peerURLs []string, name string, clientURLs []string) *Member {
|
||||
return &Member{
|
||||
ID: types.ID(id),
|
||||
RaftAttributes: RaftAttributes{PeerURLs: peerURLs},
|
||||
Attributes: Attributes{Name: name, ClientURLs: clientURLs},
|
||||
}
|
||||
}
|
||||
193
vendor/github.com/coreos/etcd/etcdserver/membership/store.go
generated
vendored
Normal file
193
vendor/github.com/coreos/etcd/etcdserver/membership/store.go
generated
vendored
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package membership
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/store"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
const (
|
||||
attributesSuffix = "attributes"
|
||||
raftAttributesSuffix = "raftAttributes"
|
||||
|
||||
// the prefix for stroing membership related information in store provided by store pkg.
|
||||
storePrefix = "/0"
|
||||
)
|
||||
|
||||
var (
|
||||
membersBucketName = []byte("members")
|
||||
membersRemovedBuckedName = []byte("members_removed")
|
||||
clusterBucketName = []byte("cluster")
|
||||
|
||||
StoreMembersPrefix = path.Join(storePrefix, "members")
|
||||
storeRemovedMembersPrefix = path.Join(storePrefix, "removed_members")
|
||||
)
|
||||
|
||||
func mustSaveMemberToBackend(be backend.Backend, m *Member) {
|
||||
mkey := backendMemberKey(m.ID)
|
||||
mvalue, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
plog.Panicf("marshal raftAttributes should never fail: %v", err)
|
||||
}
|
||||
|
||||
tx := be.BatchTx()
|
||||
tx.Lock()
|
||||
tx.UnsafePut(membersBucketName, mkey, mvalue)
|
||||
tx.Unlock()
|
||||
}
|
||||
|
||||
func mustDeleteMemberFromBackend(be backend.Backend, id types.ID) {
|
||||
mkey := backendMemberKey(id)
|
||||
|
||||
tx := be.BatchTx()
|
||||
tx.Lock()
|
||||
tx.UnsafeDelete(membersBucketName, mkey)
|
||||
tx.UnsafePut(membersRemovedBuckedName, mkey, []byte("removed"))
|
||||
tx.Unlock()
|
||||
}
|
||||
|
||||
func mustSaveClusterVersionToBackend(be backend.Backend, ver *semver.Version) {
|
||||
ckey := backendClusterVersionKey()
|
||||
|
||||
tx := be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
tx.UnsafePut(clusterBucketName, ckey, []byte(ver.String()))
|
||||
}
|
||||
|
||||
func mustSaveMemberToStore(s store.Store, m *Member) {
|
||||
b, err := json.Marshal(m.RaftAttributes)
|
||||
if err != nil {
|
||||
plog.Panicf("marshal raftAttributes should never fail: %v", err)
|
||||
}
|
||||
p := path.Join(MemberStoreKey(m.ID), raftAttributesSuffix)
|
||||
if _, err := s.Create(p, false, string(b), false, store.TTLOptionSet{ExpireTime: store.Permanent}); err != nil {
|
||||
plog.Panicf("create raftAttributes should never fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustDeleteMemberFromStore(s store.Store, id types.ID) {
|
||||
if _, err := s.Delete(MemberStoreKey(id), true, true); err != nil {
|
||||
plog.Panicf("delete member should never fail: %v", err)
|
||||
}
|
||||
if _, err := s.Create(RemovedMemberStoreKey(id), false, "", false, store.TTLOptionSet{ExpireTime: store.Permanent}); err != nil {
|
||||
plog.Panicf("create removedMember should never fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustUpdateMemberInStore(s store.Store, m *Member) {
|
||||
b, err := json.Marshal(m.RaftAttributes)
|
||||
if err != nil {
|
||||
plog.Panicf("marshal raftAttributes should never fail: %v", err)
|
||||
}
|
||||
p := path.Join(MemberStoreKey(m.ID), raftAttributesSuffix)
|
||||
if _, err := s.Update(p, string(b), store.TTLOptionSet{ExpireTime: store.Permanent}); err != nil {
|
||||
plog.Panicf("update raftAttributes should never fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustUpdateMemberAttrInStore(s store.Store, m *Member) {
|
||||
b, err := json.Marshal(m.Attributes)
|
||||
if err != nil {
|
||||
plog.Panicf("marshal raftAttributes should never fail: %v", err)
|
||||
}
|
||||
p := path.Join(MemberStoreKey(m.ID), attributesSuffix)
|
||||
if _, err := s.Set(p, false, string(b), store.TTLOptionSet{ExpireTime: store.Permanent}); err != nil {
|
||||
plog.Panicf("update raftAttributes should never fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustSaveClusterVersionToStore(s store.Store, ver *semver.Version) {
|
||||
if _, err := s.Set(StoreClusterVersionKey(), false, ver.String(), store.TTLOptionSet{ExpireTime: store.Permanent}); err != nil {
|
||||
plog.Panicf("save cluster version should never fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// nodeToMember builds member from a key value node.
|
||||
// the child nodes of the given node MUST be sorted by key.
|
||||
func nodeToMember(n *store.NodeExtern) (*Member, error) {
|
||||
m := &Member{ID: MustParseMemberIDFromKey(n.Key)}
|
||||
attrs := make(map[string][]byte)
|
||||
raftAttrKey := path.Join(n.Key, raftAttributesSuffix)
|
||||
attrKey := path.Join(n.Key, attributesSuffix)
|
||||
for _, nn := range n.Nodes {
|
||||
if nn.Key != raftAttrKey && nn.Key != attrKey {
|
||||
return nil, fmt.Errorf("unknown key %q", nn.Key)
|
||||
}
|
||||
attrs[nn.Key] = []byte(*nn.Value)
|
||||
}
|
||||
if data := attrs[raftAttrKey]; data != nil {
|
||||
if err := json.Unmarshal(data, &m.RaftAttributes); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal raftAttributes error: %v", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("raftAttributes key doesn't exist")
|
||||
}
|
||||
if data := attrs[attrKey]; data != nil {
|
||||
if err := json.Unmarshal(data, &m.Attributes); err != nil {
|
||||
return m, fmt.Errorf("unmarshal attributes error: %v", err)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func backendMemberKey(id types.ID) []byte {
|
||||
return []byte(id.String())
|
||||
}
|
||||
|
||||
func backendClusterVersionKey() []byte {
|
||||
return []byte("clusterVersion")
|
||||
}
|
||||
|
||||
func mustCreateBackendBuckets(be backend.Backend) {
|
||||
tx := be.BatchTx()
|
||||
tx.Lock()
|
||||
defer tx.Unlock()
|
||||
tx.UnsafeCreateBucket(membersBucketName)
|
||||
tx.UnsafeCreateBucket(membersRemovedBuckedName)
|
||||
tx.UnsafeCreateBucket(clusterBucketName)
|
||||
}
|
||||
|
||||
func MemberStoreKey(id types.ID) string {
|
||||
return path.Join(StoreMembersPrefix, id.String())
|
||||
}
|
||||
|
||||
func StoreClusterVersionKey() string {
|
||||
return path.Join(storePrefix, "version")
|
||||
}
|
||||
|
||||
func MemberAttributesStorePath(id types.ID) string {
|
||||
return path.Join(MemberStoreKey(id), attributesSuffix)
|
||||
}
|
||||
|
||||
func MustParseMemberIDFromKey(key string) types.ID {
|
||||
id, err := types.IDFromString(path.Base(key))
|
||||
if err != nil {
|
||||
plog.Panicf("unexpected parse member id error: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func RemovedMemberStoreKey(id types.ID) string {
|
||||
return path.Join(storeRemovedMembersPrefix, id.String())
|
||||
}
|
||||
95
vendor/github.com/coreos/etcd/etcdserver/metrics.go
generated
vendored
Normal file
95
vendor/github.com/coreos/etcd/etcdserver/metrics.go
generated
vendored
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/runtime"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
hasLeader = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "server",
|
||||
Name: "has_leader",
|
||||
Help: "Whether or not a leader exists. 1 is existence, 0 is not.",
|
||||
})
|
||||
leaderChanges = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "server",
|
||||
Name: "leader_changes_seen_total",
|
||||
Help: "The number of leader changes seen.",
|
||||
})
|
||||
proposalsCommitted = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "server",
|
||||
Name: "proposals_committed_total",
|
||||
Help: "The total number of consensus proposals committed.",
|
||||
})
|
||||
proposalsApplied = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "server",
|
||||
Name: "proposals_applied_total",
|
||||
Help: "The total number of consensus proposals applied.",
|
||||
})
|
||||
proposalsPending = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "server",
|
||||
Name: "proposals_pending",
|
||||
Help: "The current number of pending proposals to commit.",
|
||||
})
|
||||
proposalsFailed = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "etcd",
|
||||
Subsystem: "server",
|
||||
Name: "proposals_failed_total",
|
||||
Help: "The total number of failed proposals seen.",
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(hasLeader)
|
||||
prometheus.MustRegister(leaderChanges)
|
||||
prometheus.MustRegister(proposalsCommitted)
|
||||
prometheus.MustRegister(proposalsApplied)
|
||||
prometheus.MustRegister(proposalsPending)
|
||||
prometheus.MustRegister(proposalsFailed)
|
||||
}
|
||||
|
||||
func monitorFileDescriptor(done <-chan struct{}) {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
used, err := runtime.FDUsage()
|
||||
if err != nil {
|
||||
plog.Errorf("cannot monitor file descriptor usage (%v)", err)
|
||||
return
|
||||
}
|
||||
limit, err := runtime.FDLimit()
|
||||
if err != nil {
|
||||
plog.Errorf("cannot monitor file descriptor usage (%v)", err)
|
||||
return
|
||||
}
|
||||
if used >= limit/5*4 {
|
||||
plog.Warningf("80%% of the file descriptor limit is used [used = %d, limit = %d]", used, limit)
|
||||
}
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
114
vendor/github.com/coreos/etcd/etcdserver/quota.go
generated
vendored
Normal file
114
vendor/github.com/coreos/etcd/etcdserver/quota.go
generated
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
)
|
||||
|
||||
// Quota represents an arbitrary quota against arbitrary requests. Each request
|
||||
// costs some charge; if there is not enough remaining charge, then there are
|
||||
// too few resources available within the quota to apply the request.
|
||||
type Quota interface {
|
||||
// Available judges whether the given request fits within the quota.
|
||||
Available(req interface{}) bool
|
||||
// Cost computes the charge against the quota for a given request.
|
||||
Cost(req interface{}) int
|
||||
// Remaining is the amount of charge left for the quota.
|
||||
Remaining() int64
|
||||
}
|
||||
|
||||
type passthroughQuota struct{}
|
||||
|
||||
func (*passthroughQuota) Available(interface{}) bool { return true }
|
||||
func (*passthroughQuota) Cost(interface{}) int { return 0 }
|
||||
func (*passthroughQuota) Remaining() int64 { return 1 }
|
||||
|
||||
type backendQuota struct {
|
||||
s *EtcdServer
|
||||
maxBackendBytes int64
|
||||
}
|
||||
|
||||
const (
|
||||
// leaseOverhead is an estimate for the cost of storing a lease
|
||||
leaseOverhead = 64
|
||||
// kvOverhead is an estimate for the cost of storing a key's metadata
|
||||
kvOverhead = 256
|
||||
)
|
||||
|
||||
func NewBackendQuota(s *EtcdServer) Quota {
|
||||
if s.Cfg.QuotaBackendBytes < 0 {
|
||||
// disable quotas if negative
|
||||
plog.Warningf("disabling backend quota")
|
||||
return &passthroughQuota{}
|
||||
}
|
||||
if s.Cfg.QuotaBackendBytes == 0 {
|
||||
// use default size if no quota size given
|
||||
return &backendQuota{s, backend.DefaultQuotaBytes}
|
||||
}
|
||||
if s.Cfg.QuotaBackendBytes > backend.MaxQuotaBytes {
|
||||
plog.Warningf("backend quota %v exceeds maximum quota %v; using maximum", s.Cfg.QuotaBackendBytes, backend.MaxQuotaBytes)
|
||||
return &backendQuota{s, backend.MaxQuotaBytes}
|
||||
}
|
||||
return &backendQuota{s, s.Cfg.QuotaBackendBytes}
|
||||
}
|
||||
|
||||
func (b *backendQuota) Available(v interface{}) bool {
|
||||
// TODO: maybe optimize backend.Size()
|
||||
return b.s.Backend().Size()+int64(b.Cost(v)) < b.maxBackendBytes
|
||||
}
|
||||
|
||||
func (b *backendQuota) Cost(v interface{}) int {
|
||||
switch r := v.(type) {
|
||||
case *pb.PutRequest:
|
||||
return costPut(r)
|
||||
case *pb.TxnRequest:
|
||||
return costTxn(r)
|
||||
case *pb.LeaseGrantRequest:
|
||||
return leaseOverhead
|
||||
default:
|
||||
panic("unexpected cost")
|
||||
}
|
||||
}
|
||||
|
||||
func costPut(r *pb.PutRequest) int { return kvOverhead + len(r.Key) + len(r.Value) }
|
||||
|
||||
func costTxnReq(u *pb.RequestOp) int {
|
||||
r := u.GetRequestPut()
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
return costPut(r)
|
||||
}
|
||||
|
||||
func costTxn(r *pb.TxnRequest) int {
|
||||
sizeSuccess := 0
|
||||
for _, u := range r.Success {
|
||||
sizeSuccess += costTxnReq(u)
|
||||
}
|
||||
sizeFailure := 0
|
||||
for _, u := range r.Failure {
|
||||
sizeFailure += costTxnReq(u)
|
||||
}
|
||||
if sizeFailure > sizeSuccess {
|
||||
return sizeFailure
|
||||
}
|
||||
return sizeSuccess
|
||||
}
|
||||
|
||||
func (b *backendQuota) Remaining() int64 {
|
||||
return b.maxBackendBytes - b.s.Backend().Size()
|
||||
}
|
||||
547
vendor/github.com/coreos/etcd/etcdserver/raft.go
generated
vendored
Normal file
547
vendor/github.com/coreos/etcd/etcdserver/raft.go
generated
vendored
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/contention"
|
||||
"github.com/coreos/etcd/pkg/pbutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
"github.com/coreos/etcd/wal"
|
||||
"github.com/coreos/etcd/wal/walpb"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
const (
|
||||
// Number of entries for slow follower to catch-up after compacting
|
||||
// the raft storage entries.
|
||||
// We expect the follower has a millisecond level latency with the leader.
|
||||
// The max throughput is around 10K. Keep a 5K entries is enough for helping
|
||||
// follower to catch up.
|
||||
numberOfCatchUpEntries = 5000
|
||||
|
||||
// The max throughput of etcd will not exceed 100MB/s (100K * 1KB value).
|
||||
// Assuming the RTT is around 10ms, 1MB max size is large enough.
|
||||
maxSizePerMsg = 1 * 1024 * 1024
|
||||
// Never overflow the rafthttp buffer, which is 4096.
|
||||
// TODO: a better const?
|
||||
maxInflightMsgs = 4096 / 8
|
||||
)
|
||||
|
||||
var (
|
||||
// protects raftStatus
|
||||
raftStatusMu sync.Mutex
|
||||
// indirection for expvar func interface
|
||||
// expvar panics when publishing duplicate name
|
||||
// expvar does not support remove a registered name
|
||||
// so only register a func that calls raftStatus
|
||||
// and change raftStatus as we need.
|
||||
raftStatus func() raft.Status
|
||||
)
|
||||
|
||||
func init() {
|
||||
raft.SetLogger(capnslog.NewPackageLogger("github.com/coreos/etcd", "raft"))
|
||||
expvar.Publish("raft.status", expvar.Func(func() interface{} {
|
||||
raftStatusMu.Lock()
|
||||
defer raftStatusMu.Unlock()
|
||||
return raftStatus()
|
||||
}))
|
||||
}
|
||||
|
||||
type RaftTimer interface {
|
||||
Index() uint64
|
||||
Term() uint64
|
||||
}
|
||||
|
||||
// apply contains entries, snapshot to be applied. Once
|
||||
// an apply is consumed, the entries will be persisted to
|
||||
// to raft storage concurrently; the application must read
|
||||
// raftDone before assuming the raft messages are stable.
|
||||
type apply struct {
|
||||
entries []raftpb.Entry
|
||||
snapshot raftpb.Snapshot
|
||||
raftDone <-chan struct{} // rx {} after raft has persisted messages
|
||||
}
|
||||
|
||||
type raftNode struct {
|
||||
// Cache of the latest raft index and raft term the server has seen.
|
||||
// These three unit64 fields must be the first elements to keep 64-bit
|
||||
// alignment for atomic access to the fields.
|
||||
index uint64
|
||||
term uint64
|
||||
lead uint64
|
||||
|
||||
mu sync.Mutex
|
||||
// last lead elected time
|
||||
lt time.Time
|
||||
|
||||
// to check if msg receiver is removed from cluster
|
||||
isIDRemoved func(id uint64) bool
|
||||
|
||||
raft.Node
|
||||
|
||||
// a chan to send/receive snapshot
|
||||
msgSnapC chan raftpb.Message
|
||||
|
||||
// a chan to send out apply
|
||||
applyc chan apply
|
||||
|
||||
// a chan to send out readState
|
||||
readStateC chan raft.ReadState
|
||||
|
||||
// utility
|
||||
ticker <-chan time.Time
|
||||
// contention detectors for raft heartbeat message
|
||||
td *contention.TimeoutDetector
|
||||
heartbeat time.Duration // for logging
|
||||
raftStorage *raft.MemoryStorage
|
||||
storage Storage
|
||||
// transport specifies the transport to send and receive msgs to members.
|
||||
// Sending messages MUST NOT block. It is okay to drop messages, since
|
||||
// clients should timeout and reissue their messages.
|
||||
// If transport is nil, server will panic.
|
||||
transport rafthttp.Transporter
|
||||
|
||||
stopped chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// start prepares and starts raftNode in a new goroutine. It is no longer safe
|
||||
// to modify the fields after it has been started.
|
||||
func (r *raftNode) start(rh *raftReadyHandler) {
|
||||
r.applyc = make(chan apply)
|
||||
r.stopped = make(chan struct{})
|
||||
r.done = make(chan struct{})
|
||||
internalTimeout := time.Second
|
||||
|
||||
go func() {
|
||||
defer r.onStop()
|
||||
islead := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.ticker:
|
||||
r.Tick()
|
||||
case rd := <-r.Ready():
|
||||
if rd.SoftState != nil {
|
||||
if lead := atomic.LoadUint64(&r.lead); rd.SoftState.Lead != raft.None && lead != rd.SoftState.Lead {
|
||||
r.mu.Lock()
|
||||
r.lt = time.Now()
|
||||
r.mu.Unlock()
|
||||
leaderChanges.Inc()
|
||||
}
|
||||
|
||||
if rd.SoftState.Lead == raft.None {
|
||||
hasLeader.Set(0)
|
||||
} else {
|
||||
hasLeader.Set(1)
|
||||
}
|
||||
|
||||
atomic.StoreUint64(&r.lead, rd.SoftState.Lead)
|
||||
islead = rd.RaftState == raft.StateLeader
|
||||
rh.updateLeadership()
|
||||
}
|
||||
|
||||
if len(rd.ReadStates) != 0 {
|
||||
select {
|
||||
case r.readStateC <- rd.ReadStates[len(rd.ReadStates)-1]:
|
||||
case <-time.After(internalTimeout):
|
||||
plog.Warningf("timed out sending read state")
|
||||
case <-r.stopped:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
raftDone := make(chan struct{}, 1)
|
||||
ap := apply{
|
||||
entries: rd.CommittedEntries,
|
||||
snapshot: rd.Snapshot,
|
||||
raftDone: raftDone,
|
||||
}
|
||||
|
||||
updateCommittedIndex(&ap, rh)
|
||||
|
||||
select {
|
||||
case r.applyc <- ap:
|
||||
case <-r.stopped:
|
||||
return
|
||||
}
|
||||
|
||||
// the leader can write to its disk in parallel with replicating to the followers and them
|
||||
// writing to their disks.
|
||||
// For more details, check raft thesis 10.2.1
|
||||
if islead {
|
||||
// gofail: var raftBeforeLeaderSend struct{}
|
||||
r.sendMessages(rd.Messages)
|
||||
}
|
||||
|
||||
// gofail: var raftBeforeSave struct{}
|
||||
if err := r.storage.Save(rd.HardState, rd.Entries); err != nil {
|
||||
plog.Fatalf("raft save state and entries error: %v", err)
|
||||
}
|
||||
if !raft.IsEmptyHardState(rd.HardState) {
|
||||
proposalsCommitted.Set(float64(rd.HardState.Commit))
|
||||
}
|
||||
// gofail: var raftAfterSave struct{}
|
||||
|
||||
if !raft.IsEmptySnap(rd.Snapshot) {
|
||||
// gofail: var raftBeforeSaveSnap struct{}
|
||||
if err := r.storage.SaveSnap(rd.Snapshot); err != nil {
|
||||
plog.Fatalf("raft save snapshot error: %v", err)
|
||||
}
|
||||
// gofail: var raftAfterSaveSnap struct{}
|
||||
r.raftStorage.ApplySnapshot(rd.Snapshot)
|
||||
plog.Infof("raft applied incoming snapshot at index %d", rd.Snapshot.Metadata.Index)
|
||||
// gofail: var raftAfterApplySnap struct{}
|
||||
}
|
||||
|
||||
r.raftStorage.Append(rd.Entries)
|
||||
|
||||
if !islead {
|
||||
// gofail: var raftBeforeFollowerSend struct{}
|
||||
r.sendMessages(rd.Messages)
|
||||
}
|
||||
raftDone <- struct{}{}
|
||||
r.Advance()
|
||||
case <-r.stopped:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func updateCommittedIndex(ap *apply, rh *raftReadyHandler) {
|
||||
var ci uint64
|
||||
if len(ap.entries) != 0 {
|
||||
ci = ap.entries[len(ap.entries)-1].Index
|
||||
}
|
||||
if ap.snapshot.Metadata.Index > ci {
|
||||
ci = ap.snapshot.Metadata.Index
|
||||
}
|
||||
if ci != 0 {
|
||||
rh.updateCommittedIndex(ci)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *raftNode) sendMessages(ms []raftpb.Message) {
|
||||
sentAppResp := false
|
||||
for i := len(ms) - 1; i >= 0; i-- {
|
||||
if r.isIDRemoved(ms[i].To) {
|
||||
ms[i].To = 0
|
||||
}
|
||||
|
||||
if ms[i].Type == raftpb.MsgAppResp {
|
||||
if sentAppResp {
|
||||
ms[i].To = 0
|
||||
} else {
|
||||
sentAppResp = true
|
||||
}
|
||||
}
|
||||
|
||||
if ms[i].Type == raftpb.MsgSnap {
|
||||
// There are two separate data store: the store for v2, and the KV for v3.
|
||||
// The msgSnap only contains the most recent snapshot of store without KV.
|
||||
// So we need to redirect the msgSnap to etcd server main loop for merging in the
|
||||
// current store snapshot and KV snapshot.
|
||||
select {
|
||||
case r.msgSnapC <- ms[i]:
|
||||
default:
|
||||
// drop msgSnap if the inflight chan if full.
|
||||
}
|
||||
ms[i].To = 0
|
||||
}
|
||||
if ms[i].Type == raftpb.MsgHeartbeat {
|
||||
ok, exceed := r.td.Observe(ms[i].To)
|
||||
if !ok {
|
||||
// TODO: limit request rate.
|
||||
plog.Warningf("failed to send out heartbeat on time (exceeded the %v timeout for %v)", r.heartbeat, exceed)
|
||||
plog.Warningf("server is likely overloaded")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.transport.Send(ms)
|
||||
}
|
||||
|
||||
func (r *raftNode) apply() chan apply {
|
||||
return r.applyc
|
||||
}
|
||||
|
||||
func (r *raftNode) leadElectedTime() time.Time {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.lt
|
||||
}
|
||||
|
||||
func (r *raftNode) stop() {
|
||||
r.stopped <- struct{}{}
|
||||
<-r.done
|
||||
}
|
||||
|
||||
func (r *raftNode) onStop() {
|
||||
r.Stop()
|
||||
r.transport.Stop()
|
||||
if err := r.storage.Close(); err != nil {
|
||||
plog.Panicf("raft close storage error: %v", err)
|
||||
}
|
||||
close(r.done)
|
||||
}
|
||||
|
||||
// for testing
|
||||
func (r *raftNode) pauseSending() {
|
||||
p := r.transport.(rafthttp.Pausable)
|
||||
p.Pause()
|
||||
}
|
||||
|
||||
func (r *raftNode) resumeSending() {
|
||||
p := r.transport.(rafthttp.Pausable)
|
||||
p.Resume()
|
||||
}
|
||||
|
||||
// advanceTicksForElection advances ticks to the node for fast election.
|
||||
// This reduces the time to wait for first leader election if bootstrapping the whole
|
||||
// cluster, while leaving at least 1 heartbeat for possible existing leader
|
||||
// to contact it.
|
||||
func advanceTicksForElection(n raft.Node, electionTicks int) {
|
||||
for i := 0; i < electionTicks-1; i++ {
|
||||
n.Tick()
|
||||
}
|
||||
}
|
||||
|
||||
func startNode(cfg *ServerConfig, cl *membership.RaftCluster, ids []types.ID) (id types.ID, n raft.Node, s *raft.MemoryStorage, w *wal.WAL) {
|
||||
var err error
|
||||
member := cl.MemberByName(cfg.Name)
|
||||
metadata := pbutil.MustMarshal(
|
||||
&pb.Metadata{
|
||||
NodeID: uint64(member.ID),
|
||||
ClusterID: uint64(cl.ID()),
|
||||
},
|
||||
)
|
||||
if w, err = wal.Create(cfg.WALDir(), metadata); err != nil {
|
||||
plog.Fatalf("create wal error: %v", err)
|
||||
}
|
||||
peers := make([]raft.Peer, len(ids))
|
||||
for i, id := range ids {
|
||||
ctx, err := json.Marshal((*cl).Member(id))
|
||||
if err != nil {
|
||||
plog.Panicf("marshal member should never fail: %v", err)
|
||||
}
|
||||
peers[i] = raft.Peer{ID: uint64(id), Context: ctx}
|
||||
}
|
||||
id = member.ID
|
||||
plog.Infof("starting member %s in cluster %s", id, cl.ID())
|
||||
s = raft.NewMemoryStorage()
|
||||
c := &raft.Config{
|
||||
ID: uint64(id),
|
||||
ElectionTick: cfg.ElectionTicks,
|
||||
HeartbeatTick: 1,
|
||||
Storage: s,
|
||||
MaxSizePerMsg: maxSizePerMsg,
|
||||
MaxInflightMsgs: maxInflightMsgs,
|
||||
CheckQuorum: true,
|
||||
}
|
||||
|
||||
n = raft.StartNode(c, peers)
|
||||
raftStatusMu.Lock()
|
||||
raftStatus = n.Status
|
||||
raftStatusMu.Unlock()
|
||||
advanceTicksForElection(n, c.ElectionTick)
|
||||
return
|
||||
}
|
||||
|
||||
func restartNode(cfg *ServerConfig, snapshot *raftpb.Snapshot) (types.ID, *membership.RaftCluster, raft.Node, *raft.MemoryStorage, *wal.WAL) {
|
||||
var walsnap walpb.Snapshot
|
||||
if snapshot != nil {
|
||||
walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term
|
||||
}
|
||||
w, id, cid, st, ents := readWAL(cfg.WALDir(), walsnap)
|
||||
|
||||
plog.Infof("restarting member %s in cluster %s at commit index %d", id, cid, st.Commit)
|
||||
cl := membership.NewCluster("")
|
||||
cl.SetID(cid)
|
||||
s := raft.NewMemoryStorage()
|
||||
if snapshot != nil {
|
||||
s.ApplySnapshot(*snapshot)
|
||||
}
|
||||
s.SetHardState(st)
|
||||
s.Append(ents)
|
||||
c := &raft.Config{
|
||||
ID: uint64(id),
|
||||
ElectionTick: cfg.ElectionTicks,
|
||||
HeartbeatTick: 1,
|
||||
Storage: s,
|
||||
MaxSizePerMsg: maxSizePerMsg,
|
||||
MaxInflightMsgs: maxInflightMsgs,
|
||||
CheckQuorum: true,
|
||||
}
|
||||
|
||||
n := raft.RestartNode(c)
|
||||
raftStatusMu.Lock()
|
||||
raftStatus = n.Status
|
||||
raftStatusMu.Unlock()
|
||||
advanceTicksForElection(n, c.ElectionTick)
|
||||
return id, cl, n, s, w
|
||||
}
|
||||
|
||||
func restartAsStandaloneNode(cfg *ServerConfig, snapshot *raftpb.Snapshot) (types.ID, *membership.RaftCluster, raft.Node, *raft.MemoryStorage, *wal.WAL) {
|
||||
var walsnap walpb.Snapshot
|
||||
if snapshot != nil {
|
||||
walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term
|
||||
}
|
||||
w, id, cid, st, ents := readWAL(cfg.WALDir(), walsnap)
|
||||
|
||||
// discard the previously uncommitted entries
|
||||
for i, ent := range ents {
|
||||
if ent.Index > st.Commit {
|
||||
plog.Infof("discarding %d uncommitted WAL entries ", len(ents)-i)
|
||||
ents = ents[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// force append the configuration change entries
|
||||
toAppEnts := createConfigChangeEnts(getIDs(snapshot, ents), uint64(id), st.Term, st.Commit)
|
||||
ents = append(ents, toAppEnts...)
|
||||
|
||||
// force commit newly appended entries
|
||||
err := w.Save(raftpb.HardState{}, toAppEnts)
|
||||
if err != nil {
|
||||
plog.Fatalf("%v", err)
|
||||
}
|
||||
if len(ents) != 0 {
|
||||
st.Commit = ents[len(ents)-1].Index
|
||||
}
|
||||
|
||||
plog.Printf("forcing restart of member %s in cluster %s at commit index %d", id, cid, st.Commit)
|
||||
cl := membership.NewCluster("")
|
||||
cl.SetID(cid)
|
||||
s := raft.NewMemoryStorage()
|
||||
if snapshot != nil {
|
||||
s.ApplySnapshot(*snapshot)
|
||||
}
|
||||
s.SetHardState(st)
|
||||
s.Append(ents)
|
||||
c := &raft.Config{
|
||||
ID: uint64(id),
|
||||
ElectionTick: cfg.ElectionTicks,
|
||||
HeartbeatTick: 1,
|
||||
Storage: s,
|
||||
MaxSizePerMsg: maxSizePerMsg,
|
||||
MaxInflightMsgs: maxInflightMsgs,
|
||||
}
|
||||
n := raft.RestartNode(c)
|
||||
raftStatus = n.Status
|
||||
return id, cl, n, s, w
|
||||
}
|
||||
|
||||
// getIDs returns an ordered set of IDs included in the given snapshot and
|
||||
// the entries. The given snapshot/entries can contain two kinds of
|
||||
// ID-related entry:
|
||||
// - ConfChangeAddNode, in which case the contained ID will be added into the set.
|
||||
// - ConfChangeRemoveNode, in which case the contained ID will be removed from the set.
|
||||
func getIDs(snap *raftpb.Snapshot, ents []raftpb.Entry) []uint64 {
|
||||
ids := make(map[uint64]bool)
|
||||
if snap != nil {
|
||||
for _, id := range snap.Metadata.ConfState.Nodes {
|
||||
ids[id] = true
|
||||
}
|
||||
}
|
||||
for _, e := range ents {
|
||||
if e.Type != raftpb.EntryConfChange {
|
||||
continue
|
||||
}
|
||||
var cc raftpb.ConfChange
|
||||
pbutil.MustUnmarshal(&cc, e.Data)
|
||||
switch cc.Type {
|
||||
case raftpb.ConfChangeAddNode:
|
||||
ids[cc.NodeID] = true
|
||||
case raftpb.ConfChangeRemoveNode:
|
||||
delete(ids, cc.NodeID)
|
||||
case raftpb.ConfChangeUpdateNode:
|
||||
// do nothing
|
||||
default:
|
||||
plog.Panicf("ConfChange Type should be either ConfChangeAddNode or ConfChangeRemoveNode!")
|
||||
}
|
||||
}
|
||||
sids := make(types.Uint64Slice, 0, len(ids))
|
||||
for id := range ids {
|
||||
sids = append(sids, id)
|
||||
}
|
||||
sort.Sort(sids)
|
||||
return []uint64(sids)
|
||||
}
|
||||
|
||||
// createConfigChangeEnts creates a series of Raft entries (i.e.
|
||||
// EntryConfChange) to remove the set of given IDs from the cluster. The ID
|
||||
// `self` is _not_ removed, even if present in the set.
|
||||
// If `self` is not inside the given ids, it creates a Raft entry to add a
|
||||
// default member with the given `self`.
|
||||
func createConfigChangeEnts(ids []uint64, self uint64, term, index uint64) []raftpb.Entry {
|
||||
ents := make([]raftpb.Entry, 0)
|
||||
next := index + 1
|
||||
found := false
|
||||
for _, id := range ids {
|
||||
if id == self {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
cc := &raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeRemoveNode,
|
||||
NodeID: id,
|
||||
}
|
||||
e := raftpb.Entry{
|
||||
Type: raftpb.EntryConfChange,
|
||||
Data: pbutil.MustMarshal(cc),
|
||||
Term: term,
|
||||
Index: next,
|
||||
}
|
||||
ents = append(ents, e)
|
||||
next++
|
||||
}
|
||||
if !found {
|
||||
m := membership.Member{
|
||||
ID: types.ID(self),
|
||||
RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:2380"}},
|
||||
}
|
||||
ctx, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
plog.Panicf("marshal member should never fail: %v", err)
|
||||
}
|
||||
cc := &raftpb.ConfChange{
|
||||
Type: raftpb.ConfChangeAddNode,
|
||||
NodeID: self,
|
||||
Context: ctx,
|
||||
}
|
||||
e := raftpb.Entry{
|
||||
Type: raftpb.EntryConfChange,
|
||||
Data: pbutil.MustMarshal(cc),
|
||||
Term: term,
|
||||
Index: next,
|
||||
}
|
||||
ents = append(ents, e)
|
||||
}
|
||||
return ents
|
||||
}
|
||||
176
vendor/github.com/coreos/etcd/etcdserver/raft_test.go
generated
vendored
Normal file
176
vendor/github.com/coreos/etcd/etcdserver/raft_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/mock/mockstorage"
|
||||
"github.com/coreos/etcd/pkg/pbutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
)
|
||||
|
||||
func TestGetIDs(t *testing.T) {
|
||||
addcc := &raftpb.ConfChange{Type: raftpb.ConfChangeAddNode, NodeID: 2}
|
||||
addEntry := raftpb.Entry{Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(addcc)}
|
||||
removecc := &raftpb.ConfChange{Type: raftpb.ConfChangeRemoveNode, NodeID: 2}
|
||||
removeEntry := raftpb.Entry{Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc)}
|
||||
normalEntry := raftpb.Entry{Type: raftpb.EntryNormal}
|
||||
updatecc := &raftpb.ConfChange{Type: raftpb.ConfChangeUpdateNode, NodeID: 2}
|
||||
updateEntry := raftpb.Entry{Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(updatecc)}
|
||||
|
||||
tests := []struct {
|
||||
confState *raftpb.ConfState
|
||||
ents []raftpb.Entry
|
||||
|
||||
widSet []uint64
|
||||
}{
|
||||
{nil, []raftpb.Entry{}, []uint64{}},
|
||||
{&raftpb.ConfState{Nodes: []uint64{1}},
|
||||
[]raftpb.Entry{}, []uint64{1}},
|
||||
{&raftpb.ConfState{Nodes: []uint64{1}},
|
||||
[]raftpb.Entry{addEntry}, []uint64{1, 2}},
|
||||
{&raftpb.ConfState{Nodes: []uint64{1}},
|
||||
[]raftpb.Entry{addEntry, removeEntry}, []uint64{1}},
|
||||
{&raftpb.ConfState{Nodes: []uint64{1}},
|
||||
[]raftpb.Entry{addEntry, normalEntry}, []uint64{1, 2}},
|
||||
{&raftpb.ConfState{Nodes: []uint64{1}},
|
||||
[]raftpb.Entry{addEntry, normalEntry, updateEntry}, []uint64{1, 2}},
|
||||
{&raftpb.ConfState{Nodes: []uint64{1}},
|
||||
[]raftpb.Entry{addEntry, removeEntry, normalEntry}, []uint64{1}},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
var snap raftpb.Snapshot
|
||||
if tt.confState != nil {
|
||||
snap.Metadata.ConfState = *tt.confState
|
||||
}
|
||||
idSet := getIDs(&snap, tt.ents)
|
||||
if !reflect.DeepEqual(idSet, tt.widSet) {
|
||||
t.Errorf("#%d: idset = %#v, want %#v", i, idSet, tt.widSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConfigChangeEnts(t *testing.T) {
|
||||
m := membership.Member{
|
||||
ID: types.ID(1),
|
||||
RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:2380"}},
|
||||
}
|
||||
ctx, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addcc1 := &raftpb.ConfChange{Type: raftpb.ConfChangeAddNode, NodeID: 1, Context: ctx}
|
||||
removecc2 := &raftpb.ConfChange{Type: raftpb.ConfChangeRemoveNode, NodeID: 2}
|
||||
removecc3 := &raftpb.ConfChange{Type: raftpb.ConfChangeRemoveNode, NodeID: 3}
|
||||
tests := []struct {
|
||||
ids []uint64
|
||||
self uint64
|
||||
term, index uint64
|
||||
|
||||
wents []raftpb.Entry
|
||||
}{
|
||||
{
|
||||
[]uint64{1},
|
||||
1,
|
||||
1, 1,
|
||||
|
||||
[]raftpb.Entry{},
|
||||
},
|
||||
{
|
||||
[]uint64{1, 2},
|
||||
1,
|
||||
1, 1,
|
||||
|
||||
[]raftpb.Entry{{Term: 1, Index: 2, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)}},
|
||||
},
|
||||
{
|
||||
[]uint64{1, 2},
|
||||
1,
|
||||
2, 2,
|
||||
|
||||
[]raftpb.Entry{{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)}},
|
||||
},
|
||||
{
|
||||
[]uint64{1, 2, 3},
|
||||
1,
|
||||
2, 2,
|
||||
|
||||
[]raftpb.Entry{
|
||||
{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)},
|
||||
{Term: 2, Index: 4, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc3)},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]uint64{2, 3},
|
||||
2,
|
||||
2, 2,
|
||||
|
||||
[]raftpb.Entry{
|
||||
{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc3)},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]uint64{2, 3},
|
||||
1,
|
||||
2, 2,
|
||||
|
||||
[]raftpb.Entry{
|
||||
{Term: 2, Index: 3, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc2)},
|
||||
{Term: 2, Index: 4, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(removecc3)},
|
||||
{Term: 2, Index: 5, Type: raftpb.EntryConfChange, Data: pbutil.MustMarshal(addcc1)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
gents := createConfigChangeEnts(tt.ids, tt.self, tt.term, tt.index)
|
||||
if !reflect.DeepEqual(gents, tt.wents) {
|
||||
t.Errorf("#%d: ents = %v, want %v", i, gents, tt.wents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopRaftWhenWaitingForApplyDone(t *testing.T) {
|
||||
n := newNopReadyNode()
|
||||
srv := &EtcdServer{r: raftNode{
|
||||
Node: n,
|
||||
storage: mockstorage.NewStorageRecorder(""),
|
||||
raftStorage: raft.NewMemoryStorage(),
|
||||
transport: rafthttp.NewNopTransporter(),
|
||||
}}
|
||||
srv.r.start(nil)
|
||||
n.readyc <- raft.Ready{}
|
||||
select {
|
||||
case <-srv.r.applyc:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("failed to receive apply struct")
|
||||
}
|
||||
|
||||
srv.r.stopped <- struct{}{}
|
||||
select {
|
||||
case <-srv.r.done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("failed to stop raft loop")
|
||||
}
|
||||
}
|
||||
1643
vendor/github.com/coreos/etcd/etcdserver/server.go
generated
vendored
Normal file
1643
vendor/github.com/coreos/etcd/etcdserver/server.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
1498
vendor/github.com/coreos/etcd/etcdserver/server_test.go
generated
vendored
Normal file
1498
vendor/github.com/coreos/etcd/etcdserver/server_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
68
vendor/github.com/coreos/etcd/etcdserver/snapshot_merge.go
generated
vendored
Normal file
68
vendor/github.com/coreos/etcd/etcdserver/snapshot_merge.go
generated
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/coreos/etcd/mvcc/backend"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/snap"
|
||||
)
|
||||
|
||||
// createMergedSnapshotMessage creates a snapshot message that contains: raft status (term, conf),
|
||||
// a snapshot of v2 store inside raft.Snapshot as []byte, a snapshot of v3 KV in the top level message
|
||||
// as ReadCloser.
|
||||
func (s *EtcdServer) createMergedSnapshotMessage(m raftpb.Message, snapt, snapi uint64, confState raftpb.ConfState) snap.Message {
|
||||
// get a snapshot of v2 store as []byte
|
||||
clone := s.store.Clone()
|
||||
d, err := clone.SaveNoCopy()
|
||||
if err != nil {
|
||||
plog.Panicf("store save should never fail: %v", err)
|
||||
}
|
||||
|
||||
// commit kv to write metadata(for example: consistent index).
|
||||
s.KV().Commit()
|
||||
dbsnap := s.be.Snapshot()
|
||||
// get a snapshot of v3 KV as readCloser
|
||||
rc := newSnapshotReaderCloser(dbsnap)
|
||||
|
||||
// put the []byte snapshot of store into raft snapshot and return the merged snapshot with
|
||||
// KV readCloser snapshot.
|
||||
snapshot := raftpb.Snapshot{
|
||||
Metadata: raftpb.SnapshotMetadata{
|
||||
Index: snapi,
|
||||
Term: snapt,
|
||||
ConfState: confState,
|
||||
},
|
||||
Data: d,
|
||||
}
|
||||
m.Snapshot = snapshot
|
||||
|
||||
return *snap.NewMessage(m, rc, dbsnap.Size())
|
||||
}
|
||||
|
||||
func newSnapshotReaderCloser(snapshot backend.Snapshot) io.ReadCloser {
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
n, err := snapshot.WriteTo(pw)
|
||||
if err == nil {
|
||||
plog.Infof("wrote database snapshot out [total bytes: %d]", n)
|
||||
}
|
||||
pw.CloseWithError(err)
|
||||
snapshot.Close()
|
||||
}()
|
||||
return pr
|
||||
}
|
||||
123
vendor/github.com/coreos/etcd/etcdserver/stats/leader.go
generated
vendored
Normal file
123
vendor/github.com/coreos/etcd/etcdserver/stats/leader.go
generated
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LeaderStats is used by the leader in an etcd cluster, and encapsulates
|
||||
// statistics about communication with its followers
|
||||
type LeaderStats struct {
|
||||
// Leader is the ID of the leader in the etcd cluster.
|
||||
// TODO(jonboulle): clarify that these are IDs, not names
|
||||
Leader string `json:"leader"`
|
||||
Followers map[string]*FollowerStats `json:"followers"`
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewLeaderStats generates a new LeaderStats with the given id as leader
|
||||
func NewLeaderStats(id string) *LeaderStats {
|
||||
return &LeaderStats{
|
||||
Leader: id,
|
||||
Followers: make(map[string]*FollowerStats),
|
||||
}
|
||||
}
|
||||
|
||||
func (ls *LeaderStats) JSON() []byte {
|
||||
ls.Lock()
|
||||
stats := *ls
|
||||
ls.Unlock()
|
||||
b, err := json.Marshal(stats)
|
||||
// TODO(jonboulle): appropriate error handling?
|
||||
if err != nil {
|
||||
plog.Errorf("error marshalling leader stats (%v)", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (ls *LeaderStats) Follower(name string) *FollowerStats {
|
||||
ls.Lock()
|
||||
defer ls.Unlock()
|
||||
fs, ok := ls.Followers[name]
|
||||
if !ok {
|
||||
fs = &FollowerStats{}
|
||||
fs.Latency.Minimum = 1 << 63
|
||||
ls.Followers[name] = fs
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// FollowerStats encapsulates various statistics about a follower in an etcd cluster
|
||||
type FollowerStats struct {
|
||||
Latency LatencyStats `json:"latency"`
|
||||
Counts CountsStats `json:"counts"`
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// LatencyStats encapsulates latency statistics.
|
||||
type LatencyStats struct {
|
||||
Current float64 `json:"current"`
|
||||
Average float64 `json:"average"`
|
||||
averageSquare float64
|
||||
StandardDeviation float64 `json:"standardDeviation"`
|
||||
Minimum float64 `json:"minimum"`
|
||||
Maximum float64 `json:"maximum"`
|
||||
}
|
||||
|
||||
// CountsStats encapsulates raft statistics.
|
||||
type CountsStats struct {
|
||||
Fail uint64 `json:"fail"`
|
||||
Success uint64 `json:"success"`
|
||||
}
|
||||
|
||||
// Succ updates the FollowerStats with a successful send
|
||||
func (fs *FollowerStats) Succ(d time.Duration) {
|
||||
fs.Lock()
|
||||
defer fs.Unlock()
|
||||
|
||||
total := float64(fs.Counts.Success) * fs.Latency.Average
|
||||
totalSquare := float64(fs.Counts.Success) * fs.Latency.averageSquare
|
||||
|
||||
fs.Counts.Success++
|
||||
|
||||
fs.Latency.Current = float64(d) / (1000000.0)
|
||||
|
||||
if fs.Latency.Current > fs.Latency.Maximum {
|
||||
fs.Latency.Maximum = fs.Latency.Current
|
||||
}
|
||||
|
||||
if fs.Latency.Current < fs.Latency.Minimum {
|
||||
fs.Latency.Minimum = fs.Latency.Current
|
||||
}
|
||||
|
||||
fs.Latency.Average = (total + fs.Latency.Current) / float64(fs.Counts.Success)
|
||||
fs.Latency.averageSquare = (totalSquare + fs.Latency.Current*fs.Latency.Current) / float64(fs.Counts.Success)
|
||||
|
||||
// sdv = sqrt(avg(x^2) - avg(x)^2)
|
||||
fs.Latency.StandardDeviation = math.Sqrt(fs.Latency.averageSquare - fs.Latency.Average*fs.Latency.Average)
|
||||
}
|
||||
|
||||
// Fail updates the FollowerStats with an unsuccessful send
|
||||
func (fs *FollowerStats) Fail() {
|
||||
fs.Lock()
|
||||
defer fs.Unlock()
|
||||
fs.Counts.Fail++
|
||||
}
|
||||
110
vendor/github.com/coreos/etcd/etcdserver/stats/queue.go
generated
vendored
Normal file
110
vendor/github.com/coreos/etcd/etcdserver/stats/queue.go
generated
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
queueCapacity = 200
|
||||
)
|
||||
|
||||
// RequestStats represent the stats for a request.
|
||||
// It encapsulates the sending time and the size of the request.
|
||||
type RequestStats struct {
|
||||
SendingTime time.Time
|
||||
Size int
|
||||
}
|
||||
|
||||
type statsQueue struct {
|
||||
items [queueCapacity]*RequestStats
|
||||
size int
|
||||
front int
|
||||
back int
|
||||
totalReqSize int
|
||||
rwl sync.RWMutex
|
||||
}
|
||||
|
||||
func (q *statsQueue) Len() int {
|
||||
return q.size
|
||||
}
|
||||
|
||||
func (q *statsQueue) ReqSize() int {
|
||||
return q.totalReqSize
|
||||
}
|
||||
|
||||
// FrontAndBack gets the front and back elements in the queue
|
||||
// We must grab front and back together with the protection of the lock
|
||||
func (q *statsQueue) frontAndBack() (*RequestStats, *RequestStats) {
|
||||
q.rwl.RLock()
|
||||
defer q.rwl.RUnlock()
|
||||
if q.size != 0 {
|
||||
return q.items[q.front], q.items[q.back]
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Insert function insert a RequestStats into the queue and update the records
|
||||
func (q *statsQueue) Insert(p *RequestStats) {
|
||||
q.rwl.Lock()
|
||||
defer q.rwl.Unlock()
|
||||
|
||||
q.back = (q.back + 1) % queueCapacity
|
||||
|
||||
if q.size == queueCapacity { //dequeue
|
||||
q.totalReqSize -= q.items[q.front].Size
|
||||
q.front = (q.back + 1) % queueCapacity
|
||||
} else {
|
||||
q.size++
|
||||
}
|
||||
|
||||
q.items[q.back] = p
|
||||
q.totalReqSize += q.items[q.back].Size
|
||||
|
||||
}
|
||||
|
||||
// Rate function returns the package rate and byte rate
|
||||
func (q *statsQueue) Rate() (float64, float64) {
|
||||
front, back := q.frontAndBack()
|
||||
|
||||
if front == nil || back == nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
if time.Since(back.SendingTime) > time.Second {
|
||||
q.Clear()
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
sampleDuration := back.SendingTime.Sub(front.SendingTime)
|
||||
|
||||
pr := float64(q.Len()) / float64(sampleDuration) * float64(time.Second)
|
||||
|
||||
br := float64(q.ReqSize()) / float64(sampleDuration) * float64(time.Second)
|
||||
|
||||
return pr, br
|
||||
}
|
||||
|
||||
// Clear function clear up the statsQueue
|
||||
func (q *statsQueue) Clear() {
|
||||
q.rwl.Lock()
|
||||
defer q.rwl.Unlock()
|
||||
q.back = -1
|
||||
q.front = 0
|
||||
q.size = 0
|
||||
q.totalReqSize = 0
|
||||
}
|
||||
150
vendor/github.com/coreos/etcd/etcdserver/stats/server.go
generated
vendored
Normal file
150
vendor/github.com/coreos/etcd/etcdserver/stats/server.go
generated
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/raft"
|
||||
)
|
||||
|
||||
// ServerStats encapsulates various statistics about an EtcdServer and its
|
||||
// communication with other members of the cluster
|
||||
type ServerStats struct {
|
||||
Name string `json:"name"`
|
||||
// ID is the raft ID of the node.
|
||||
// TODO(jonboulle): use ID instead of name?
|
||||
ID string `json:"id"`
|
||||
State raft.StateType `json:"state"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
|
||||
LeaderInfo struct {
|
||||
Name string `json:"leader"`
|
||||
Uptime string `json:"uptime"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
} `json:"leaderInfo"`
|
||||
|
||||
RecvAppendRequestCnt uint64 `json:"recvAppendRequestCnt,"`
|
||||
RecvingPkgRate float64 `json:"recvPkgRate,omitempty"`
|
||||
RecvingBandwidthRate float64 `json:"recvBandwidthRate,omitempty"`
|
||||
|
||||
SendAppendRequestCnt uint64 `json:"sendAppendRequestCnt"`
|
||||
SendingPkgRate float64 `json:"sendPkgRate,omitempty"`
|
||||
SendingBandwidthRate float64 `json:"sendBandwidthRate,omitempty"`
|
||||
|
||||
sendRateQueue *statsQueue
|
||||
recvRateQueue *statsQueue
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (ss *ServerStats) JSON() []byte {
|
||||
ss.Lock()
|
||||
stats := *ss
|
||||
ss.Unlock()
|
||||
stats.LeaderInfo.Uptime = time.Since(stats.LeaderInfo.StartTime).String()
|
||||
stats.SendingPkgRate, stats.SendingBandwidthRate = stats.SendRates()
|
||||
stats.RecvingPkgRate, stats.RecvingBandwidthRate = stats.RecvRates()
|
||||
b, err := json.Marshal(stats)
|
||||
// TODO(jonboulle): appropriate error handling?
|
||||
if err != nil {
|
||||
log.Printf("stats: error marshalling server stats: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Initialize clears the statistics of ServerStats and resets its start time
|
||||
func (ss *ServerStats) Initialize() {
|
||||
if ss == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
ss.StartTime = now
|
||||
ss.LeaderInfo.StartTime = now
|
||||
ss.sendRateQueue = &statsQueue{
|
||||
back: -1,
|
||||
}
|
||||
ss.recvRateQueue = &statsQueue{
|
||||
back: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// RecvRates calculates and returns the rate of received append requests
|
||||
func (ss *ServerStats) RecvRates() (float64, float64) {
|
||||
return ss.recvRateQueue.Rate()
|
||||
}
|
||||
|
||||
// SendRates calculates and returns the rate of sent append requests
|
||||
func (ss *ServerStats) SendRates() (float64, float64) {
|
||||
return ss.sendRateQueue.Rate()
|
||||
}
|
||||
|
||||
// RecvAppendReq updates the ServerStats in response to an AppendRequest
|
||||
// from the given leader being received
|
||||
func (ss *ServerStats) RecvAppendReq(leader string, reqSize int) {
|
||||
ss.Lock()
|
||||
defer ss.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
ss.State = raft.StateFollower
|
||||
if leader != ss.LeaderInfo.Name {
|
||||
ss.LeaderInfo.Name = leader
|
||||
ss.LeaderInfo.StartTime = now
|
||||
}
|
||||
|
||||
ss.recvRateQueue.Insert(
|
||||
&RequestStats{
|
||||
SendingTime: now,
|
||||
Size: reqSize,
|
||||
},
|
||||
)
|
||||
ss.RecvAppendRequestCnt++
|
||||
}
|
||||
|
||||
// SendAppendReq updates the ServerStats in response to an AppendRequest
|
||||
// being sent by this server
|
||||
func (ss *ServerStats) SendAppendReq(reqSize int) {
|
||||
ss.Lock()
|
||||
defer ss.Unlock()
|
||||
|
||||
ss.becomeLeader()
|
||||
|
||||
ss.sendRateQueue.Insert(
|
||||
&RequestStats{
|
||||
SendingTime: time.Now(),
|
||||
Size: reqSize,
|
||||
},
|
||||
)
|
||||
|
||||
ss.SendAppendRequestCnt++
|
||||
}
|
||||
|
||||
func (ss *ServerStats) BecomeLeader() {
|
||||
ss.Lock()
|
||||
defer ss.Unlock()
|
||||
ss.becomeLeader()
|
||||
}
|
||||
|
||||
func (ss *ServerStats) becomeLeader() {
|
||||
if ss.State != raft.StateLeader {
|
||||
ss.State = raft.StateLeader
|
||||
ss.LeaderInfo.Name = ss.ID
|
||||
ss.LeaderInfo.StartTime = time.Now()
|
||||
}
|
||||
}
|
||||
32
vendor/github.com/coreos/etcd/etcdserver/stats/stats.go
generated
vendored
Normal file
32
vendor/github.com/coreos/etcd/etcdserver/stats/stats.go
generated
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package stats defines a standard interface for etcd cluster statistics.
|
||||
package stats
|
||||
|
||||
import "github.com/coreos/pkg/capnslog"
|
||||
|
||||
var (
|
||||
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "etcdserver/stats")
|
||||
)
|
||||
|
||||
type Stats interface {
|
||||
// SelfStats returns the struct representing statistics of this server
|
||||
SelfStats() []byte
|
||||
// LeaderStats returns the statistics of all followers in the cluster
|
||||
// if this server is leader. Otherwise, nil is returned.
|
||||
LeaderStats() []byte
|
||||
// StoreStats returns statistics of the store backing this EtcdServer
|
||||
StoreStats() []byte
|
||||
}
|
||||
101
vendor/github.com/coreos/etcd/etcdserver/storage.go
generated
vendored
Normal file
101
vendor/github.com/coreos/etcd/etcdserver/storage.go
generated
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/pkg/pbutil"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/snap"
|
||||
"github.com/coreos/etcd/wal"
|
||||
"github.com/coreos/etcd/wal/walpb"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
// Save function saves ents and state to the underlying stable storage.
|
||||
// Save MUST block until st and ents are on stable storage.
|
||||
Save(st raftpb.HardState, ents []raftpb.Entry) error
|
||||
// SaveSnap function saves snapshot to the underlying stable storage.
|
||||
SaveSnap(snap raftpb.Snapshot) error
|
||||
// DBFilePath returns the file path of database snapshot saved with given
|
||||
// id.
|
||||
DBFilePath(id uint64) (string, error)
|
||||
// Close closes the Storage and performs finalization.
|
||||
Close() error
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
*wal.WAL
|
||||
*snap.Snapshotter
|
||||
}
|
||||
|
||||
func NewStorage(w *wal.WAL, s *snap.Snapshotter) Storage {
|
||||
return &storage{w, s}
|
||||
}
|
||||
|
||||
// SaveSnap saves the snapshot to disk and release the locked
|
||||
// wal files since they will not be used.
|
||||
func (st *storage) SaveSnap(snap raftpb.Snapshot) error {
|
||||
walsnap := walpb.Snapshot{
|
||||
Index: snap.Metadata.Index,
|
||||
Term: snap.Metadata.Term,
|
||||
}
|
||||
err := st.WAL.SaveSnapshot(walsnap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = st.Snapshotter.SaveSnap(snap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.WAL.ReleaseLockTo(snap.Metadata.Index)
|
||||
}
|
||||
|
||||
func readWAL(waldir string, snap walpb.Snapshot) (w *wal.WAL, id, cid types.ID, st raftpb.HardState, ents []raftpb.Entry) {
|
||||
var (
|
||||
err error
|
||||
wmetadata []byte
|
||||
)
|
||||
|
||||
repaired := false
|
||||
for {
|
||||
if w, err = wal.Open(waldir, snap); err != nil {
|
||||
plog.Fatalf("open wal error: %v", err)
|
||||
}
|
||||
if wmetadata, st, ents, err = w.ReadAll(); err != nil {
|
||||
w.Close()
|
||||
// we can only repair ErrUnexpectedEOF and we never repair twice.
|
||||
if repaired || err != io.ErrUnexpectedEOF {
|
||||
plog.Fatalf("read wal error (%v) and cannot be repaired", err)
|
||||
}
|
||||
if !wal.Repair(waldir) {
|
||||
plog.Fatalf("WAL error (%v) cannot be repaired", err)
|
||||
} else {
|
||||
plog.Infof("repaired WAL error (%v)", err)
|
||||
repaired = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
var metadata pb.Metadata
|
||||
pbutil.MustUnmarshal(&metadata, wmetadata)
|
||||
id = types.ID(metadata.NodeID)
|
||||
cid = types.ID(metadata.ClusterID)
|
||||
return
|
||||
}
|
||||
97
vendor/github.com/coreos/etcd/etcdserver/util.go
generated
vendored
Normal file
97
vendor/github.com/coreos/etcd/etcdserver/util.go
generated
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
)
|
||||
|
||||
// isConnectedToQuorumSince checks whether the local member is connected to the
|
||||
// quorum of the cluster since the given time.
|
||||
func isConnectedToQuorumSince(transport rafthttp.Transporter, since time.Time, self types.ID, members []*membership.Member) bool {
|
||||
return numConnectedSince(transport, since, self, members) >= (len(members)/2)+1
|
||||
}
|
||||
|
||||
// isConnectedSince checks whether the local member is connected to the
|
||||
// remote member since the given time.
|
||||
func isConnectedSince(transport rafthttp.Transporter, since time.Time, remote types.ID) bool {
|
||||
t := transport.ActiveSince(remote)
|
||||
return !t.IsZero() && t.Before(since)
|
||||
}
|
||||
|
||||
// isConnectedFullySince checks whether the local member is connected to all
|
||||
// members in the cluster since the given time.
|
||||
func isConnectedFullySince(transport rafthttp.Transporter, since time.Time, self types.ID, members []*membership.Member) bool {
|
||||
return numConnectedSince(transport, since, self, members) == len(members)
|
||||
}
|
||||
|
||||
// numConnectedSince counts how many members are connected to the local member
|
||||
// since the given time.
|
||||
func numConnectedSince(transport rafthttp.Transporter, since time.Time, self types.ID, members []*membership.Member) int {
|
||||
connectedNum := 0
|
||||
for _, m := range members {
|
||||
if m.ID == self || isConnectedSince(transport, since, m.ID) {
|
||||
connectedNum++
|
||||
}
|
||||
}
|
||||
return connectedNum
|
||||
}
|
||||
|
||||
// longestConnected chooses the member with longest active-since-time.
|
||||
// It returns false, if nothing is active.
|
||||
func longestConnected(tp rafthttp.Transporter, membs []types.ID) (types.ID, bool) {
|
||||
var longest types.ID
|
||||
var oldest time.Time
|
||||
for _, id := range membs {
|
||||
tm := tp.ActiveSince(id)
|
||||
if tm.IsZero() { // inactive
|
||||
continue
|
||||
}
|
||||
|
||||
if oldest.IsZero() { // first longest candidate
|
||||
oldest = tm
|
||||
longest = id
|
||||
}
|
||||
|
||||
if tm.Before(oldest) {
|
||||
oldest = tm
|
||||
longest = id
|
||||
}
|
||||
}
|
||||
if uint64(longest) == 0 {
|
||||
return longest, false
|
||||
}
|
||||
return longest, true
|
||||
}
|
||||
|
||||
type notifier struct {
|
||||
c chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
func newNotifier() *notifier {
|
||||
return ¬ifier{
|
||||
c: make(chan struct{}, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (nc *notifier) notify(err error) {
|
||||
nc.err = err
|
||||
close(nc.c)
|
||||
}
|
||||
89
vendor/github.com/coreos/etcd/etcdserver/util_test.go
generated
vendored
Normal file
89
vendor/github.com/coreos/etcd/etcdserver/util_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/etcd/raft/raftpb"
|
||||
"github.com/coreos/etcd/rafthttp"
|
||||
"github.com/coreos/etcd/snap"
|
||||
)
|
||||
|
||||
func TestLongestConnected(t *testing.T) {
|
||||
umap, err := types.NewURLsMap("mem1=http://10.1:2379,mem2=http://10.2:2379,mem3=http://10.3:2379")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clus, err := membership.NewClusterFromURLsMap("test", umap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
memberIDs := clus.MemberIDs()
|
||||
|
||||
tr := newNopTransporterWithActiveTime(memberIDs)
|
||||
transferee, ok := longestConnected(tr, memberIDs)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected ok %v", ok)
|
||||
}
|
||||
if memberIDs[0] != transferee {
|
||||
t.Fatalf("expected first member %s to be transferee, got %s", memberIDs[0], transferee)
|
||||
}
|
||||
|
||||
// make all members non-active
|
||||
amap := make(map[types.ID]time.Time)
|
||||
for _, id := range memberIDs {
|
||||
amap[id] = time.Time{}
|
||||
}
|
||||
tr.(*nopTransporterWithActiveTime).reset(amap)
|
||||
|
||||
_, ok2 := longestConnected(tr, memberIDs)
|
||||
if ok2 {
|
||||
t.Fatalf("unexpected ok %v", ok)
|
||||
}
|
||||
}
|
||||
|
||||
type nopTransporterWithActiveTime struct {
|
||||
activeMap map[types.ID]time.Time
|
||||
}
|
||||
|
||||
// newNopTransporterWithActiveTime creates nopTransporterWithActiveTime with the first member
|
||||
// being the most stable (longest active-since time).
|
||||
func newNopTransporterWithActiveTime(memberIDs []types.ID) rafthttp.Transporter {
|
||||
am := make(map[types.ID]time.Time)
|
||||
for i, id := range memberIDs {
|
||||
am[id] = time.Now().Add(time.Duration(i) * time.Second)
|
||||
}
|
||||
return &nopTransporterWithActiveTime{activeMap: am}
|
||||
}
|
||||
|
||||
func (s *nopTransporterWithActiveTime) Start() error { return nil }
|
||||
func (s *nopTransporterWithActiveTime) Handler() http.Handler { return nil }
|
||||
func (s *nopTransporterWithActiveTime) Send(m []raftpb.Message) {}
|
||||
func (s *nopTransporterWithActiveTime) SendSnapshot(m snap.Message) {}
|
||||
func (s *nopTransporterWithActiveTime) AddRemote(id types.ID, us []string) {}
|
||||
func (s *nopTransporterWithActiveTime) AddPeer(id types.ID, us []string) {}
|
||||
func (s *nopTransporterWithActiveTime) RemovePeer(id types.ID) {}
|
||||
func (s *nopTransporterWithActiveTime) RemoveAllPeers() {}
|
||||
func (s *nopTransporterWithActiveTime) UpdatePeer(id types.ID, us []string) {}
|
||||
func (s *nopTransporterWithActiveTime) ActiveSince(id types.ID) time.Time { return s.activeMap[id] }
|
||||
func (s *nopTransporterWithActiveTime) Stop() {}
|
||||
func (s *nopTransporterWithActiveTime) Pause() {}
|
||||
func (s *nopTransporterWithActiveTime) Resume() {}
|
||||
func (s *nopTransporterWithActiveTime) reset(am map[types.ID]time.Time) { s.activeMap = am }
|
||||
125
vendor/github.com/coreos/etcd/etcdserver/v2_server.go
generated
vendored
Normal file
125
vendor/github.com/coreos/etcd/etcdserver/v2_server.go
generated
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright 2016 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type v2API interface {
|
||||
Post(ctx context.Context, r *pb.Request) (Response, error)
|
||||
Put(ctx context.Context, r *pb.Request) (Response, error)
|
||||
Delete(ctx context.Context, r *pb.Request) (Response, error)
|
||||
QGet(ctx context.Context, r *pb.Request) (Response, error)
|
||||
Get(ctx context.Context, r *pb.Request) (Response, error)
|
||||
Head(ctx context.Context, r *pb.Request) (Response, error)
|
||||
}
|
||||
|
||||
type v2apiStore struct{ s *EtcdServer }
|
||||
|
||||
func (a *v2apiStore) Post(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
return a.processRaftRequest(ctx, r)
|
||||
}
|
||||
|
||||
func (a *v2apiStore) Put(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
return a.processRaftRequest(ctx, r)
|
||||
}
|
||||
|
||||
func (a *v2apiStore) Delete(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
return a.processRaftRequest(ctx, r)
|
||||
}
|
||||
|
||||
func (a *v2apiStore) QGet(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
return a.processRaftRequest(ctx, r)
|
||||
}
|
||||
|
||||
func (a *v2apiStore) processRaftRequest(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
data, err := r.Marshal()
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
ch := a.s.w.Register(r.ID)
|
||||
|
||||
start := time.Now()
|
||||
a.s.r.Propose(ctx, data)
|
||||
proposalsPending.Inc()
|
||||
defer proposalsPending.Dec()
|
||||
|
||||
select {
|
||||
case x := <-ch:
|
||||
resp := x.(Response)
|
||||
return resp, resp.err
|
||||
case <-ctx.Done():
|
||||
proposalsFailed.Inc()
|
||||
a.s.w.Trigger(r.ID, nil) // GC wait
|
||||
return Response{}, a.s.parseProposeCtxErr(ctx.Err(), start)
|
||||
case <-a.s.stopping:
|
||||
}
|
||||
return Response{}, ErrStopped
|
||||
}
|
||||
|
||||
func (a *v2apiStore) Get(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
if r.Wait {
|
||||
wc, err := a.s.store.Watch(r.Path, r.Recursive, r.Stream, r.Since)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
return Response{Watcher: wc}, nil
|
||||
}
|
||||
ev, err := a.s.store.Get(r.Path, r.Recursive, r.Sorted)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
return Response{Event: ev}, nil
|
||||
}
|
||||
|
||||
func (a *v2apiStore) Head(ctx context.Context, r *pb.Request) (Response, error) {
|
||||
ev, err := a.s.store.Get(r.Path, r.Recursive, r.Sorted)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
return Response{Event: ev}, nil
|
||||
}
|
||||
|
||||
// Do interprets r and performs an operation on s.store according to r.Method
|
||||
// and other fields. If r.Method is "POST", "PUT", "DELETE", or a "GET" with
|
||||
// Quorum == true, r will be sent through consensus before performing its
|
||||
// respective operation. Do will block until an action is performed or there is
|
||||
// an error.
|
||||
func (s *EtcdServer) Do(ctx context.Context, r pb.Request) (Response, error) {
|
||||
r.ID = s.reqIDGen.Next()
|
||||
if r.Method == "GET" && r.Quorum {
|
||||
r.Method = "QGET"
|
||||
}
|
||||
v2api := (v2API)(&v2apiStore{s})
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
return v2api.Post(ctx, &r)
|
||||
case "PUT":
|
||||
return v2api.Put(ctx, &r)
|
||||
case "DELETE":
|
||||
return v2api.Delete(ctx, &r)
|
||||
case "QGET":
|
||||
return v2api.QGet(ctx, &r)
|
||||
case "GET":
|
||||
return v2api.Get(ctx, &r)
|
||||
case "HEAD":
|
||||
return v2api.Head(ctx, &r)
|
||||
}
|
||||
return Response{}, ErrUnknownMethod
|
||||
}
|
||||
804
vendor/github.com/coreos/etcd/etcdserver/v3_server.go
generated
vendored
Normal file
804
vendor/github.com/coreos/etcd/etcdserver/v3_server.go
generated
vendored
Normal file
|
|
@ -0,0 +1,804 @@
|
|||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package etcdserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/auth"
|
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||
"github.com/coreos/etcd/etcdserver/membership"
|
||||
"github.com/coreos/etcd/lease"
|
||||
"github.com/coreos/etcd/lease/leasehttp"
|
||||
"github.com/coreos/etcd/mvcc"
|
||||
"github.com/coreos/etcd/raft"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
// the max request size that raft accepts.
|
||||
// TODO: make this a flag? But we probably do not want to
|
||||
// accept large request which might block raft stream. User
|
||||
// specify a large value might end up with shooting in the foot.
|
||||
maxRequestBytes = 1.5 * 1024 * 1024
|
||||
|
||||
// In the health case, there might be a small gap (10s of entries) between
|
||||
// the applied index and committed index.
|
||||
// However, if the committed entries are very heavy to apply, the gap might grow.
|
||||
// We should stop accepting new proposals if the gap growing to a certain point.
|
||||
maxGapBetweenApplyAndCommitIndex = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
newRangeClusterVersion = *semver.Must(semver.NewVersion("3.1.0"))
|
||||
)
|
||||
|
||||
type RaftKV interface {
|
||||
Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error)
|
||||
Put(ctx context.Context, r *pb.PutRequest) (*pb.PutResponse, error)
|
||||
DeleteRange(ctx context.Context, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error)
|
||||
Txn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse, error)
|
||||
Compact(ctx context.Context, r *pb.CompactionRequest) (*pb.CompactionResponse, error)
|
||||
}
|
||||
|
||||
type Lessor interface {
|
||||
// LeaseGrant sends LeaseGrant request to raft and apply it after committed.
|
||||
LeaseGrant(ctx context.Context, r *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error)
|
||||
// LeaseRevoke sends LeaseRevoke request to raft and apply it after committed.
|
||||
LeaseRevoke(ctx context.Context, r *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error)
|
||||
|
||||
// LeaseRenew renews the lease with given ID. The renewed TTL is returned. Or an error
|
||||
// is returned.
|
||||
LeaseRenew(ctx context.Context, id lease.LeaseID) (int64, error)
|
||||
|
||||
// LeaseTimeToLive retrieves lease information.
|
||||
LeaseTimeToLive(ctx context.Context, r *pb.LeaseTimeToLiveRequest) (*pb.LeaseTimeToLiveResponse, error)
|
||||
}
|
||||
|
||||
type Authenticator interface {
|
||||
AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*pb.AuthEnableResponse, error)
|
||||
AuthDisable(ctx context.Context, r *pb.AuthDisableRequest) (*pb.AuthDisableResponse, error)
|
||||
Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error)
|
||||
UserAdd(ctx context.Context, r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error)
|
||||
UserDelete(ctx context.Context, r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error)
|
||||
UserChangePassword(ctx context.Context, r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error)
|
||||
UserGrantRole(ctx context.Context, r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error)
|
||||
UserGet(ctx context.Context, r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error)
|
||||
UserRevokeRole(ctx context.Context, r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error)
|
||||
RoleAdd(ctx context.Context, r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error)
|
||||
RoleGrantPermission(ctx context.Context, r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error)
|
||||
RoleGet(ctx context.Context, r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error)
|
||||
RoleRevokePermission(ctx context.Context, r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error)
|
||||
RoleDelete(ctx context.Context, r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error)
|
||||
UserList(ctx context.Context, r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error)
|
||||
RoleList(ctx context.Context, r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
|
||||
}
|
||||
|
||||
func (s *EtcdServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) {
|
||||
// TODO: remove this checking when we release etcd 3.2
|
||||
if s.ClusterVersion() == nil || s.ClusterVersion().LessThan(newRangeClusterVersion) {
|
||||
return s.legacyRange(ctx, r)
|
||||
}
|
||||
|
||||
if !r.Serializable {
|
||||
err := s.linearizableReadNotify(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var resp *pb.RangeResponse
|
||||
var err error
|
||||
chk := func(ai *auth.AuthInfo) error {
|
||||
return s.authStore.IsRangePermitted(ai, r.Key, r.RangeEnd)
|
||||
}
|
||||
get := func() { resp, err = s.applyV3Base.Range(noTxn, r) }
|
||||
if serr := s.doSerialize(ctx, chk, get); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// TODO: remove this func when we release etcd 3.2
|
||||
func (s *EtcdServer) legacyRange(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) {
|
||||
if r.Serializable {
|
||||
var resp *pb.RangeResponse
|
||||
var err error
|
||||
chk := func(ai *auth.AuthInfo) error {
|
||||
return s.authStore.IsRangePermitted(ai, r.Key, r.RangeEnd)
|
||||
}
|
||||
get := func() { resp, err = s.applyV3Base.Range(noTxn, r) }
|
||||
if serr := s.doSerialize(ctx, chk, get); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{Range: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.RangeResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) Put(ctx context.Context, r *pb.PutRequest) (*pb.PutResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{Put: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.PutResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) DeleteRange(ctx context.Context, r *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{DeleteRange: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.DeleteRangeResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) Txn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
// TODO: remove this checking when we release etcd 3.2
|
||||
if s.ClusterVersion() == nil || s.ClusterVersion().LessThan(newRangeClusterVersion) {
|
||||
return s.legacyTxn(ctx, r)
|
||||
}
|
||||
|
||||
if isTxnReadonly(r) {
|
||||
if !isTxnSerializable(r) {
|
||||
err := s.linearizableReadNotify(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var resp *pb.TxnResponse
|
||||
var err error
|
||||
chk := func(ai *auth.AuthInfo) error {
|
||||
return checkTxnAuth(s.authStore, ai, r)
|
||||
}
|
||||
get := func() { resp, err = s.applyV3Base.Txn(r) }
|
||||
if serr := s.doSerialize(ctx, chk, get); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{Txn: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.TxnResponse), nil
|
||||
}
|
||||
|
||||
// TODO: remove this func when we release etcd 3.2
|
||||
func (s *EtcdServer) legacyTxn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse, error) {
|
||||
if isTxnSerializable(r) {
|
||||
var resp *pb.TxnResponse
|
||||
var err error
|
||||
chk := func(ai *auth.AuthInfo) error {
|
||||
return checkTxnAuth(s.authStore, ai, r)
|
||||
}
|
||||
get := func() { resp, err = s.applyV3Base.Txn(r) }
|
||||
if serr := s.doSerialize(ctx, chk, get); serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{Txn: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.TxnResponse), nil
|
||||
}
|
||||
|
||||
func isTxnSerializable(r *pb.TxnRequest) bool {
|
||||
for _, u := range r.Success {
|
||||
if r := u.GetRequestRange(); r == nil || !r.Serializable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, u := range r.Failure {
|
||||
if r := u.GetRequestRange(); r == nil || !r.Serializable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isTxnReadonly(r *pb.TxnRequest) bool {
|
||||
for _, u := range r.Success {
|
||||
if r := u.GetRequestRange(); r == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, u := range r.Failure {
|
||||
if r := u.GetRequestRange(); r == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *EtcdServer) Compact(ctx context.Context, r *pb.CompactionRequest) (*pb.CompactionResponse, error) {
|
||||
result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{Compaction: r})
|
||||
if r.Physical && result != nil && result.physc != nil {
|
||||
<-result.physc
|
||||
// The compaction is done deleting keys; the hash is now settled
|
||||
// but the data is not necessarily committed. If there's a crash,
|
||||
// the hash may revert to a hash prior to compaction completing
|
||||
// if the compaction resumes. Force the finished compaction to
|
||||
// commit so it won't resume following a crash.
|
||||
s.be.ForceCommit()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
resp := result.resp.(*pb.CompactionResponse)
|
||||
if resp == nil {
|
||||
resp = &pb.CompactionResponse{}
|
||||
}
|
||||
if resp.Header == nil {
|
||||
resp.Header = &pb.ResponseHeader{}
|
||||
}
|
||||
resp.Header.Revision = s.kv.Rev()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) LeaseGrant(ctx context.Context, r *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
|
||||
// no id given? choose one
|
||||
for r.ID == int64(lease.NoLease) {
|
||||
// only use positive int64 id's
|
||||
r.ID = int64(s.reqIDGen.Next() & ((1 << 63) - 1))
|
||||
}
|
||||
result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{LeaseGrant: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.LeaseGrantResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) LeaseRevoke(ctx context.Context, r *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
|
||||
result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{LeaseRevoke: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.LeaseRevokeResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) LeaseRenew(ctx context.Context, id lease.LeaseID) (int64, error) {
|
||||
ttl, err := s.lessor.Renew(id)
|
||||
if err == nil { // already requested to primary lessor(leader)
|
||||
return ttl, nil
|
||||
}
|
||||
if err != lease.ErrNotPrimary {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, s.Cfg.ReqTimeout())
|
||||
defer cancel()
|
||||
|
||||
// renewals don't go through raft; forward to leader manually
|
||||
for cctx.Err() == nil && err != nil {
|
||||
leader, lerr := s.waitLeader(cctx)
|
||||
if lerr != nil {
|
||||
return -1, lerr
|
||||
}
|
||||
for _, url := range leader.PeerURLs {
|
||||
lurl := url + leasehttp.LeasePrefix
|
||||
ttl, err = leasehttp.RenewHTTP(cctx, id, lurl, s.peerRt)
|
||||
if err == nil || err == lease.ErrLeaseNotFound {
|
||||
return ttl, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1, ErrTimeout
|
||||
}
|
||||
|
||||
func (s *EtcdServer) LeaseTimeToLive(ctx context.Context, r *pb.LeaseTimeToLiveRequest) (*pb.LeaseTimeToLiveResponse, error) {
|
||||
if s.Leader() == s.ID() {
|
||||
// primary; timetolive directly from leader
|
||||
le := s.lessor.Lookup(lease.LeaseID(r.ID))
|
||||
if le == nil {
|
||||
return nil, lease.ErrLeaseNotFound
|
||||
}
|
||||
// TODO: fill out ResponseHeader
|
||||
resp := &pb.LeaseTimeToLiveResponse{Header: &pb.ResponseHeader{}, ID: r.ID, TTL: int64(le.Remaining().Seconds()), GrantedTTL: le.TTL()}
|
||||
if r.Keys {
|
||||
ks := le.Keys()
|
||||
kbs := make([][]byte, len(ks))
|
||||
for i := range ks {
|
||||
kbs[i] = []byte(ks[i])
|
||||
}
|
||||
resp.Keys = kbs
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, s.Cfg.ReqTimeout())
|
||||
defer cancel()
|
||||
|
||||
// forward to leader
|
||||
for cctx.Err() == nil {
|
||||
leader, err := s.waitLeader(cctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, url := range leader.PeerURLs {
|
||||
lurl := url + leasehttp.LeaseInternalPrefix
|
||||
resp, err := leasehttp.TimeToLiveHTTP(cctx, lease.LeaseID(r.ID), r.Keys, lurl, s.peerRt)
|
||||
if err == nil {
|
||||
return resp.LeaseTimeToLiveResponse, nil
|
||||
}
|
||||
if err == lease.ErrLeaseNotFound {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
func (s *EtcdServer) waitLeader(ctx context.Context) (*membership.Member, error) {
|
||||
leader := s.cluster.Member(s.Leader())
|
||||
for leader == nil {
|
||||
// wait an election
|
||||
dur := time.Duration(s.Cfg.ElectionTicks) * time.Duration(s.Cfg.TickMs) * time.Millisecond
|
||||
select {
|
||||
case <-time.After(dur):
|
||||
leader = s.cluster.Member(s.Leader())
|
||||
case <-s.stopping:
|
||||
return nil, ErrStopped
|
||||
case <-ctx.Done():
|
||||
return nil, ErrNoLeader
|
||||
}
|
||||
}
|
||||
if leader == nil || len(leader.PeerURLs) == 0 {
|
||||
return nil, ErrNoLeader
|
||||
}
|
||||
return leader, nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) Alarm(ctx context.Context, r *pb.AlarmRequest) (*pb.AlarmResponse, error) {
|
||||
result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{Alarm: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AlarmResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) AuthEnable(ctx context.Context, r *pb.AuthEnableRequest) (*pb.AuthEnableResponse, error) {
|
||||
result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{AuthEnable: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthEnableResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) AuthDisable(ctx context.Context, r *pb.AuthDisableRequest) (*pb.AuthDisableResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthDisable: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthDisableResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) Authenticate(ctx context.Context, r *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) {
|
||||
var result *applyResult
|
||||
|
||||
err := s.linearizableReadNotify(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
checkedRevision, err := s.AuthStore().CheckPassword(r.Name, r.Password)
|
||||
if err != nil {
|
||||
plog.Errorf("invalid authentication request to user %s was issued", r.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st, err := s.AuthStore().GenSimpleToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
internalReq := &pb.InternalAuthenticateRequest{
|
||||
Name: r.Name,
|
||||
Password: r.Password,
|
||||
SimpleToken: st,
|
||||
}
|
||||
|
||||
result, err = s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{Authenticate: internalReq})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
|
||||
if checkedRevision != s.AuthStore().Revision() {
|
||||
plog.Infof("revision when password checked is obsolete, retrying")
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return result.resp.(*pb.AuthenticateResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserAdd(ctx context.Context, r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserAdd: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserAddResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserDelete(ctx context.Context, r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserDelete: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserDeleteResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserChangePassword(ctx context.Context, r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserChangePassword: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserChangePasswordResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserGrantRole(ctx context.Context, r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserGrantRole: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserGrantRoleResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserGet(ctx context.Context, r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserGet: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserGetResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserList(ctx context.Context, r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserList: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserListResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) UserRevokeRole(ctx context.Context, r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthUserRevokeRole: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthUserRevokeRoleResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) RoleAdd(ctx context.Context, r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthRoleAdd: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthRoleAddResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) RoleGrantPermission(ctx context.Context, r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthRoleGrantPermission: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthRoleGrantPermissionResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) RoleGet(ctx context.Context, r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthRoleGet: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthRoleGetResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) RoleList(ctx context.Context, r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthRoleList: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthRoleListResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) RoleRevokePermission(ctx context.Context, r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthRoleRevokePermission: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthRoleRevokePermissionResponse), nil
|
||||
}
|
||||
|
||||
func (s *EtcdServer) RoleDelete(ctx context.Context, r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
|
||||
result, err := s.processInternalRaftRequest(ctx, pb.InternalRaftRequest{AuthRoleDelete: r})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return result.resp.(*pb.AuthRoleDeleteResponse), nil
|
||||
}
|
||||
|
||||
// doSerialize handles the auth logic, with permissions checked by "chk", for a serialized request "get". Returns a non-nil error on authentication failure.
|
||||
func (s *EtcdServer) doSerialize(ctx context.Context, chk func(*auth.AuthInfo) error, get func()) error {
|
||||
for {
|
||||
ai, err := s.AuthStore().AuthInfoFromCtx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ai == nil {
|
||||
// chk expects non-nil AuthInfo; use empty credentials
|
||||
ai = &auth.AuthInfo{}
|
||||
}
|
||||
if err = chk(ai); err != nil {
|
||||
if err == auth.ErrAuthOldRevision {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
// fetch response for serialized request
|
||||
get()
|
||||
// empty credentials or current auth info means no need to retry
|
||||
if ai.Revision == 0 || ai.Revision == s.authStore.Revision() {
|
||||
return nil
|
||||
}
|
||||
// avoid TOCTOU error, retry of the request is required.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EtcdServer) processInternalRaftRequestOnce(ctx context.Context, r pb.InternalRaftRequest) (*applyResult, error) {
|
||||
ai := s.getAppliedIndex()
|
||||
ci := s.getCommittedIndex()
|
||||
if ci > ai+maxGapBetweenApplyAndCommitIndex {
|
||||
return nil, ErrTooManyRequests
|
||||
}
|
||||
|
||||
r.Header = &pb.RequestHeader{
|
||||
ID: s.reqIDGen.Next(),
|
||||
}
|
||||
|
||||
authInfo, err := s.AuthStore().AuthInfoFromCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if authInfo != nil {
|
||||
r.Header.Username = authInfo.Username
|
||||
r.Header.AuthRevision = authInfo.Revision
|
||||
}
|
||||
|
||||
data, err := r.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) > maxRequestBytes {
|
||||
return nil, ErrRequestTooLarge
|
||||
}
|
||||
|
||||
id := r.ID
|
||||
if id == 0 {
|
||||
id = r.Header.ID
|
||||
}
|
||||
ch := s.w.Register(id)
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, s.Cfg.ReqTimeout())
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
s.r.Propose(cctx, data)
|
||||
proposalsPending.Inc()
|
||||
defer proposalsPending.Dec()
|
||||
|
||||
select {
|
||||
case x := <-ch:
|
||||
return x.(*applyResult), nil
|
||||
case <-cctx.Done():
|
||||
proposalsFailed.Inc()
|
||||
s.w.Trigger(id, nil) // GC wait
|
||||
return nil, s.parseProposeCtxErr(cctx.Err(), start)
|
||||
case <-s.done:
|
||||
return nil, ErrStopped
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EtcdServer) processInternalRaftRequest(ctx context.Context, r pb.InternalRaftRequest) (*applyResult, error) {
|
||||
var result *applyResult
|
||||
var err error
|
||||
for {
|
||||
result, err = s.processInternalRaftRequestOnce(ctx, r)
|
||||
if err != auth.ErrAuthOldRevision {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Watchable returns a watchable interface attached to the etcdserver.
|
||||
func (s *EtcdServer) Watchable() mvcc.WatchableKV { return s.KV() }
|
||||
|
||||
func (s *EtcdServer) linearizableReadLoop() {
|
||||
var rs raft.ReadState
|
||||
|
||||
for {
|
||||
ctx := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(ctx, s.reqIDGen.Next())
|
||||
|
||||
select {
|
||||
case <-s.readwaitc:
|
||||
case <-s.stopping:
|
||||
return
|
||||
}
|
||||
|
||||
nextnr := newNotifier()
|
||||
|
||||
s.readMu.Lock()
|
||||
nr := s.readNotifier
|
||||
s.readNotifier = nextnr
|
||||
s.readMu.Unlock()
|
||||
|
||||
cctx, cancel := context.WithTimeout(context.Background(), s.Cfg.ReqTimeout())
|
||||
if err := s.r.ReadIndex(cctx, ctx); err != nil {
|
||||
cancel()
|
||||
if err == raft.ErrStopped {
|
||||
return
|
||||
}
|
||||
plog.Errorf("failed to get read index from raft: %v", err)
|
||||
nr.notify(err)
|
||||
continue
|
||||
}
|
||||
cancel()
|
||||
|
||||
var (
|
||||
timeout bool
|
||||
done bool
|
||||
)
|
||||
for !timeout && !done {
|
||||
select {
|
||||
case rs = <-s.r.readStateC:
|
||||
done = bytes.Equal(rs.RequestCtx, ctx)
|
||||
if !done {
|
||||
// a previous request might time out. now we should ignore the response of it and
|
||||
// continue waiting for the response of the current requests.
|
||||
plog.Warningf("ignored out-of-date read index response (want %v, got %v)", rs.RequestCtx, ctx)
|
||||
}
|
||||
case <-time.After(s.Cfg.ReqTimeout()):
|
||||
plog.Warningf("timed out waiting for read index response")
|
||||
nr.notify(ErrTimeout)
|
||||
timeout = true
|
||||
case <-s.stopping:
|
||||
return
|
||||
}
|
||||
}
|
||||
if !done {
|
||||
continue
|
||||
}
|
||||
|
||||
if ai := s.getAppliedIndex(); ai < rs.Index {
|
||||
select {
|
||||
case <-s.applyWait.Wait(rs.Index):
|
||||
case <-s.stopping:
|
||||
return
|
||||
}
|
||||
}
|
||||
// unblock all l-reads requested at indices before rs.Index
|
||||
nr.notify(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EtcdServer) linearizableReadNotify(ctx context.Context) error {
|
||||
s.readMu.RLock()
|
||||
nc := s.readNotifier
|
||||
s.readMu.RUnlock()
|
||||
|
||||
// signal linearizable loop for current notify if it hasn't been already
|
||||
select {
|
||||
case s.readwaitc <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
// wait for read state notification
|
||||
select {
|
||||
case <-nc.c:
|
||||
return nc.err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-s.done:
|
||||
return ErrStopped
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue