func TestHTTPMembersAPIAddSuccess(t *testing.T) { wantAction := &membersAPIActionAdd{ peerURLs: types.URLs([]url.URL{ {Scheme: "http", Host: "127.0.0.1:7002"}, }), } mAPI := &httpMembersAPI{ client: &actionAssertingHTTPClient{ t: t, act: wantAction, resp: http.Response{ StatusCode: http.StatusCreated, }, body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"]}`), }, } wantResponseMember := &Member{ ID: "94088180e21eb87b", PeerURLs: []string{"http://127.0.0.1:7002"}, } m, err := mAPI.Add(context.Background(), "http://127.0.0.1:7002") if err != nil { t.Errorf("got non-nil err: %#v", err) } if !reflect.DeepEqual(wantResponseMember, m) { t.Errorf("incorrect Member: want=%#v got=%#v", wantResponseMember, m) } }
func TestMembersAPIActionAdd(t *testing.T) { ep := url.URL{Scheme: "http", Host: "example.com"} act := &membersAPIActionAdd{ peerURLs: types.URLs([]url.URL{ url.URL{Scheme: "https", Host: "127.0.0.1:8081"}, url.URL{Scheme: "http", Host: "127.0.0.1:8080"}, }), } wantURL := &url.URL{ Scheme: "http", Host: "example.com", Path: "/v2/members", } wantHeader := http.Header{ "Content-Type": []string{"application/json"}, } wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`) got := *act.HTTPRequest(ep) err := assertResponse(got, wantURL, wantHeader, wantBody) if err != nil { t.Error(err.Error()) } }
// urlsEqual checks equality of url.URLS between two arrays. // This check pass even if an URL is in hostname and opposite is in IP address. func urlsEqual(a []url.URL, b []url.URL) bool { if len(a) != len(b) { return false } urls, err := ResolveTCPAddrs([][]url.URL{a, b}) if err != nil { return false } a, b = urls[0], urls[1] sort.Sort(types.URLs(a)) sort.Sort(types.URLs(b)) for i := range a { if !reflect.DeepEqual(a[i], b[i]) { return false } } return true }
func TestTransportUpdate(t *testing.T) { peer := newFakePeer() tr := &transport{ peers: map[types.ID]Peer{types.ID(1): peer}, } u := "http://localhost:2380" tr.UpdatePeer(types.ID(1), []string{u}) wurls := types.URLs(testutil.MustNewURLs(t, []string{"http://localhost:2380"})) if !reflect.DeepEqual(peer.urls, wurls) { t.Errorf("urls = %+v, want %+v", peer.urls, wurls) } }
func TestMemberCreateRequestMarshal(t *testing.T) { req := memberCreateOrUpdateRequest{ PeerURLs: types.URLs([]url.URL{ {Scheme: "http", Host: "127.0.0.1:8081"}, {Scheme: "https", Host: "127.0.0.1:8080"}, }), } want := []byte(`{"peerURLs":["http://127.0.0.1:8081","https://127.0.0.1:8080"]}`) got, err := json.Marshal(&req) if err != nil { t.Fatalf("Marshal returned unexpected err=%v", err) } if !reflect.DeepEqual(want, got) { t.Fatalf("Failed to marshal memberCreateRequest: want=%s, got=%s", want, 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{ url.URL{Scheme: "http", Host: "127.0.0.1:8081"}, url.URL{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) } }
// Set parses command line sets of names to IPs formatted like: // mach0=http://1.1.1.1,mach0=http://2.2.2.2,mach0=http://1.1.1.1,mach1=http://2.2.2.2,mach1=http://3.3.3.3 func (c *Cluster) Set(s string) error { *c = Cluster{} v, err := url.ParseQuery(strings.Replace(s, ",", "&", -1)) if err != nil { return err } for name, urls := range v { if len(urls) == 0 || urls[0] == "" { return fmt.Errorf("Empty URL given for %q", name) } m := newMember(name, types.URLs(*flags.NewURLsValue(strings.Join(urls, ","))), nil) err := c.Add(*m) if err != nil { return err } } return nil }
// NewClusterFromString returns a Cluster instantiated from the given cluster token // and cluster string, by parsing members from a set of discovery-formatted // names-to-IPs, like: // mach0=http://1.1.1.1,mach0=http://2.2.2.2,mach1=http://3.3.3.3,mach2=http://4.4.4.4 func NewClusterFromString(token string, cluster string) (*Cluster, error) { c := newCluster(token) v, err := url.ParseQuery(strings.Replace(cluster, ",", "&", -1)) if err != nil { return nil, err } for name, urls := range v { if len(urls) == 0 || urls[0] == "" { return nil, fmt.Errorf("Empty URL given for %q", name) } purls := &flags.URLsValue{} if err := purls.Set(strings.Join(urls, ",")); err != nil { return nil, err } m := NewMember(name, types.URLs(*purls), c.token, nil) if _, ok := c.members[m.ID]; ok { return nil, fmt.Errorf("Member exists with identical ID %v", m) } c.members[m.ID] = m } c.genID() return c, nil }
func Main() { cfg := NewConfig() err := cfg.Parse(os.Args[1:]) if err != nil { plog.Errorf("error verifying flags, %v. See 'etcd --help'.", err) switch err { case errUnsetAdvertiseClientURLsFlag: plog.Errorf("When listening on specific address(es), this etcd process must advertise accessible url(s) to each connected client.") } os.Exit(1) } setupLogging(cfg) var stopped <-chan struct{} plog.Infof("etcd Version: %s\n", version.Version) plog.Infof("Git SHA: %s\n", version.GitSHA) plog.Infof("Go Version: %s\n", runtime.Version()) plog.Infof("Go OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) GoMaxProcs := runtime.GOMAXPROCS(0) plog.Infof("setting maximum number of CPUs to %d, total number of available CPUs is %d", GoMaxProcs, runtime.NumCPU()) // TODO: check whether fields are set instead of whether fields have default value if cfg.name != defaultName && cfg.initialCluster == initialClusterFromName(defaultName) { cfg.initialCluster = initialClusterFromName(cfg.name) } if cfg.dir == "" { cfg.dir = fmt.Sprintf("%v.etcd", cfg.name) plog.Warningf("no data-dir provided, using default data-dir ./%s", cfg.dir) } which := identifyDataDirOrDie(cfg.dir) if which != dirEmpty { plog.Noticef("the server is already initialized as %v before, starting as etcd %v...", which, which) switch which { case dirMember: stopped, err = startEtcd(cfg) case dirProxy: err = startProxy(cfg) default: plog.Panicf("unhandled dir type %v", which) } } else { shouldProxy := cfg.isProxy() if !shouldProxy { stopped, err = startEtcd(cfg) if err == discovery.ErrFullCluster && cfg.shouldFallbackToProxy() { plog.Noticef("discovery cluster full, falling back to %s", fallbackFlagProxy) shouldProxy = true } } if shouldProxy { err = startProxy(cfg) } } if err != nil { switch err { case discovery.ErrDuplicateID: plog.Errorf("member %q has previously registered with discovery service token (%s).", cfg.name, cfg.durl) plog.Errorf("But etcd could not find valid cluster configuration in the given data dir (%s).", cfg.dir) plog.Infof("Please check the given data dir path if the previous bootstrap succeeded") plog.Infof("or use a new discovery token if the previous bootstrap failed.") case discovery.ErrDuplicateName: plog.Errorf("member with duplicated name has registered with discovery service token(%s).", cfg.durl) plog.Errorf("please check (cURL) the discovery token for more information.") plog.Errorf("please do not reuse the discovery token and generate a new one to bootstrap the cluster.") default: if strings.Contains(err.Error(), "include") && strings.Contains(err.Error(), "--initial-cluster") { plog.Infof("%v", err) if cfg.initialCluster == initialClusterFromName(cfg.name) { plog.Infof("forgot to set --initial-cluster flag?") } if types.URLs(cfg.apurls).String() == defaultInitialAdvertisePeerURLs { plog.Infof("forgot to set --initial-advertise-peer-urls flag?") } if cfg.initialCluster == initialClusterFromName(cfg.name) && len(cfg.durl) == 0 { plog.Infof("if you want to use discovery service, please set --discovery flag.") } os.Exit(1) } if etcdserver.IsDiscoveryError(err) { plog.Errorf("%v", err) plog.Infof("discovery token %s was used, but failed to bootstrap the cluster.", cfg.durl) plog.Infof("please generate a new discovery token and try to bootstrap again.") os.Exit(1) } plog.Fatalf("%v", err) } os.Exit(1) } osutil.HandleInterrupts() if systemdutil.IsRunningSystemd() { // At this point, the initialization of etcd is done. // The listeners are listening on the TCP ports and ready // for accepting connections. // The http server is probably ready for serving incoming // connections. If it is not, the connection might be pending // for less than one second. err := daemon.SdNotify("READY=1") if err != nil { plog.Errorf("failed to notify systemd for readiness") } } <-stopped osutil.Exit(0) }