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")), }, // unknow { 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 TestServerVersion(t *testing.T) { tests := []struct { h http.Header wv *semver.Version }{ // backward compatibility with etcd 2.0 { http.Header{}, semver.Must(semver.NewVersion("2.0.0")), }, { http.Header{"X-Server-Version": []string{"2.1.0"}}, semver.Must(semver.NewVersion("2.1.0")), }, { http.Header{"X-Server-Version": []string{"2.1.0-alpha.0+git"}}, semver.Must(semver.NewVersion("2.1.0-alpha.0+git")), }, } for i, tt := range tests { v := serverVersion(tt.h) if v.String() != tt.wv.String() { t.Errorf("#%d: version = %s, want %s", i, v, tt.wv) } } }
// checkVersionCompability checks whether the given version is compatible // with the local version. func checkVersionCompability(name string, server, minCluster *semver.Version) error { localServer := semver.Must(semver.NewVersion(version.Version)) localMinCluster := semver.Must(semver.NewVersion(version.MinClusterVersion)) if compareMajorMinorVersion(server, localMinCluster) == -1 { return fmt.Errorf("remote version is too low: remote[%s]=%s, local=%s", name, server, localServer) } if compareMajorMinorVersion(minCluster, localServer) == 1 { return fmt.Errorf("local version is too low: remote[%s]=%s, local=%s", name, server, localServer) } return nil }
func (c *cluster) Version() *semver.Version { c.Lock() defer c.Unlock() if c.version == nil { return nil } return semver.Must(semver.NewVersion(c.version.String())) }
// serverVersion returns the min cluster version from the given header. func minClusterVersion(h http.Header) *semver.Version { verStr := h.Get("X-Min-Cluster-Version") // backward compatibility with etcd 2.0 if verStr == "" { verStr = "2.0.0" } return semver.Must(semver.NewVersion(verStr)) }
func TestCheckVersionCompatibility(t *testing.T) { ls := semver.Must(semver.NewVersion(version.Version)) lmc := semver.Must(semver.NewVersion(version.MinClusterVersion)) tests := []struct { server *semver.Version minCluster *semver.Version wok bool }{ // the same version as local { ls, lmc, true, }, // one version lower { lmc, &semver.Version{}, true, }, // one version higher { &semver.Version{Major: ls.Major + 1}, ls, true, }, // too low version { &semver.Version{Major: lmc.Major - 1}, &semver.Version{}, false, }, // too high version { &semver.Version{Major: ls.Major + 1, Minor: 1}, &semver.Version{Major: ls.Major + 1}, false, }, } for i, tt := range tests { err := checkVersionCompability("", tt.server, tt.minCluster) if ok := err == nil; ok != tt.wok { t.Errorf("#%d: ok = %v, want %v", i, ok, tt.wok) } } }
func clusterVersionFromStore(st store.Store) *semver.Version { e, err := st.Get(path.Join(StoreClusterPrefix, "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)) }
func TestCompareMajorMinorVersion(t *testing.T) { tests := []struct { va, vb *semver.Version w int }{ // equal to { semver.Must(semver.NewVersion("2.1.0")), semver.Must(semver.NewVersion("2.1.0")), 0, }, // smaller than { semver.Must(semver.NewVersion("2.0.0")), semver.Must(semver.NewVersion("2.1.0")), -1, }, // bigger than { semver.Must(semver.NewVersion("2.2.0")), semver.Must(semver.NewVersion("2.1.0")), 1, }, // ignore patch { semver.Must(semver.NewVersion("2.1.1")), semver.Must(semver.NewVersion("2.1.0")), 0, }, // ignore prerelease { semver.Must(semver.NewVersion("2.1.0-alpha.0")), semver.Must(semver.NewVersion("2.1.0")), 0, }, } for i, tt := range tests { if g := compareMajorMinorVersion(tt.va, tt.vb); g != tt.w { t.Errorf("#%d: compare = %d, want %d", i, g, tt.w) } } }
func TestCheckStreamSupport(t *testing.T) { tests := []struct { v *semver.Version t streamType w bool }{ // support { semver.Must(semver.NewVersion("2.0.0")), streamTypeMsgApp, true, }, // ignore patch { semver.Must(semver.NewVersion("2.0.9")), streamTypeMsgApp, true, }, // ignore prerelease { semver.Must(semver.NewVersion("2.0.0-alpha")), streamTypeMsgApp, true, }, // not support { semver.Must(semver.NewVersion("2.0.0")), streamTypeMsgAppV2, false, }, } for i, tt := range tests { if g := checkStreamSupport(tt.v, tt.t); g != tt.w { t.Errorf("#%d: check = %v, want %v", i, g, tt.w) } } }
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) } } }
func (cr *streamReader) dial(t streamType) (io.ReadCloser, error) { u := cr.picker.pick() cr.mu.Lock() term := cr.msgAppTerm cr.mu.Unlock() uu := u uu.Path = path.Join(t.endpoint(), cr.local.String()) req, err := http.NewRequest("GET", uu.String(), nil) if err != nil { cr.picker.unreachable(u) return nil, fmt.Errorf("failed to make http request to %s (%v)", u, err) } req.Header.Set("X-Server-From", cr.local.String()) req.Header.Set("X-Server-Version", version.Version) req.Header.Set("X-Min-Cluster-Version", version.MinClusterVersion) req.Header.Set("X-Etcd-Cluster-ID", cr.cid.String()) req.Header.Set("X-Raft-To", cr.remote.String()) if t == streamTypeMsgApp { req.Header.Set("X-Raft-Term", strconv.FormatUint(term, 10)) } cr.mu.Lock() select { case <-cr.stopc: cr.mu.Unlock() return nil, fmt.Errorf("stream reader is stopped") default: } cr.cancel = httputil.RequestCanceler(cr.tr, req) cr.mu.Unlock() resp, err := cr.tr.RoundTrip(req) if err != nil { cr.picker.unreachable(u) return nil, err } rv := serverVersion(resp.Header) lv := semver.Must(semver.NewVersion(version.Version)) if compareMajorMinorVersion(rv, lv) == -1 && !checkStreamSupport(rv, t) { resp.Body.Close() return nil, errUnsupportedStreamType } switch resp.StatusCode { case http.StatusGone: resp.Body.Close() err := fmt.Errorf("the member has been permanently removed from the cluster") select { case cr.errorc <- err: default: } return nil, err case http.StatusOK: return resp.Body, nil case http.StatusNotFound: resp.Body.Close() return nil, fmt.Errorf("remote member %s could not recognize local member", cr.remote) case http.StatusPreconditionFailed: b, err := ioutil.ReadAll(resp.Body) if err != nil { cr.picker.unreachable(u) return nil, err } resp.Body.Close() switch strings.TrimSuffix(string(b), "\n") { case errIncompatibleVersion.Error(): plog.Errorf("request sent was ignored by peer %s (server version incompatible)", cr.remote) return nil, errIncompatibleVersion case errClusterIDMismatch.Error(): plog.Errorf("request sent was ignored (cluster ID mismatch: remote[%s]=%s, local=%s)", cr.remote, resp.Header.Get("X-Etcd-Cluster-ID"), cr.cid) return nil, errClusterIDMismatch default: return nil, fmt.Errorf("unhandled error %q when precondition failed", string(b)) } default: resp.Body.Close() return nil, fmt.Errorf("unhandled http status %d", resp.StatusCode) } }