mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
clientv3: Fix maintenance APIs to directly dial grpc endpoints correctly.
This commit is contained in:
parent
0458c5d54b
commit
b3b06a862a
@ -99,6 +99,15 @@ func Target(id, endpoint string) string {
|
|||||||
return fmt.Sprintf("%s://%s/%s", scheme, id, endpoint)
|
return fmt.Sprintf("%s://%s/%s", scheme, id, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DirectTarget constructs a direct resolver target to a single endpoint.
|
||||||
|
// TODO: It should be possible to use the 'passthrough' resolver instead
|
||||||
|
// of a custom resolver for this use case, but TLS connections fail for
|
||||||
|
// a reason we haven't been able to determine.
|
||||||
|
func DirectTarget(endpoint string) string {
|
||||||
|
_, host, scheme := ParseEndpoint(endpoint)
|
||||||
|
return Target(fmt.Sprintf("direct:%s", scheme), host)
|
||||||
|
}
|
||||||
|
|
||||||
// IsTarget checks if a given target string in an endpoint resolver target.
|
// IsTarget checks if a given target string in an endpoint resolver target.
|
||||||
func IsTarget(target string) bool {
|
func IsTarget(target string) bool {
|
||||||
return strings.HasPrefix(target, "endpoint://")
|
return strings.HasPrefix(target, "endpoint://")
|
||||||
@ -114,6 +123,11 @@ func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts res
|
|||||||
return nil, fmt.Errorf("'etcd' target scheme requires non-empty authority identifying etcd cluster being routed to")
|
return nil, fmt.Errorf("'etcd' target scheme requires non-empty authority identifying etcd cluster being routed to")
|
||||||
}
|
}
|
||||||
id := target.Authority
|
id := target.Authority
|
||||||
|
|
||||||
|
if isDirectEndpoint(target) {
|
||||||
|
return buildDirectEndpointResolver(target, cc, opts)
|
||||||
|
}
|
||||||
|
|
||||||
es, err := b.getResolverGroup(id)
|
es, err := b.getResolverGroup(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to build resolver: %v", err)
|
return nil, fmt.Errorf("failed to build resolver: %v", err)
|
||||||
@ -126,6 +140,25 @@ func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts res
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isDirectEndpoint(target resolver.Target) bool {
|
||||||
|
return strings.HasPrefix(target.Authority, "direct:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDirectEndpointResolver(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
|
||||||
|
parts := strings.SplitN(target.Authority, ":", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "direct" {
|
||||||
|
return nil, fmt.Errorf("'endpoint' resolver authority must be of form 'direct:<scheme>', but got %s", target.Authority)
|
||||||
|
}
|
||||||
|
scheme := parts[1]
|
||||||
|
ep := scheme + "://" + target.Endpoint
|
||||||
|
r := &DirectResolver{
|
||||||
|
endpoint: ep,
|
||||||
|
cc: cc,
|
||||||
|
}
|
||||||
|
r.cc.NewAddress(epsToAddrs(ep))
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *builder) newResolverGroup(id string) (*ResolverGroup, error) {
|
func (b *builder) newResolverGroup(id string) (*ResolverGroup, error) {
|
||||||
b.mu.RLock()
|
b.mu.RLock()
|
||||||
_, ok := b.resolverGroups[id]
|
_, ok := b.resolverGroups[id]
|
||||||
@ -187,6 +220,15 @@ func (r *Resolver) Close() {
|
|||||||
es.removeResolver(r)
|
es.removeResolver(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DirectResolver provides a resolver for a single etcd endpoint.
|
||||||
|
type DirectResolver struct {
|
||||||
|
endpoint string
|
||||||
|
cc resolver.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*DirectResolver) ResolveNow(o resolver.ResolveNowOption) {}
|
||||||
|
func (*DirectResolver) Close() {}
|
||||||
|
|
||||||
// ParseEndpoint endpoint parses an endpoint of the form
|
// ParseEndpoint endpoint parses an endpoint of the form
|
||||||
// (http|https)://<host>*|(unix|unixs)://<path>)
|
// (http|https)://<host>*|(unix|unixs)://<path>)
|
||||||
// and returns a protocol ('tcp' or 'unix'),
|
// and returns a protocol ('tcp' or 'unix'),
|
||||||
@ -213,17 +255,3 @@ func ParseEndpoint(endpoint string) (proto string, host string, scheme string) {
|
|||||||
}
|
}
|
||||||
return proto, host, scheme
|
return proto, host, scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTarget parses a endpoint://<id>/<endpoint> string and returns the parsed id and endpoint.
|
|
||||||
// If the target is malformed, an error is returned.
|
|
||||||
func ParseTarget(target string) (string, string, error) {
|
|
||||||
noPrefix := strings.TrimPrefix(target, targetPrefix)
|
|
||||||
if noPrefix == target {
|
|
||||||
return "", "", fmt.Errorf("malformed target, %s prefix is required: %s", targetPrefix, target)
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(noPrefix, "/", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return "", "", fmt.Errorf("malformed target, expected %s://<id>/<endpoint>, but got %s", scheme, target)
|
|
||||||
}
|
|
||||||
return parts[0], parts[1], nil
|
|
||||||
}
|
|
||||||
|
@ -229,13 +229,8 @@ func (c *Client) processCreds(scheme string) (creds *credentials.TransportCreden
|
|||||||
return creds
|
return creds
|
||||||
}
|
}
|
||||||
|
|
||||||
// dialSetupOpts gives the dial opts prior to any authentication
|
// dialSetupOpts gives the dial opts prior to any authentication.
|
||||||
func (c *Client) dialSetupOpts(target string, dopts ...grpc.DialOption) (opts []grpc.DialOption, err error) {
|
func (c *Client) dialSetupOpts(scheme string, dopts ...grpc.DialOption) (opts []grpc.DialOption, err error) {
|
||||||
_, ep, err := endpoint.ParseTarget(target)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse target: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.cfg.DialKeepAliveTime > 0 {
|
if c.cfg.DialKeepAliveTime > 0 {
|
||||||
params := keepalive.ClientParameters{
|
params := keepalive.ClientParameters{
|
||||||
Time: c.cfg.DialKeepAliveTime,
|
Time: c.cfg.DialKeepAliveTime,
|
||||||
@ -245,16 +240,9 @@ func (c *Client) dialSetupOpts(target string, dopts ...grpc.DialOption) (opts []
|
|||||||
}
|
}
|
||||||
opts = append(opts, dopts...)
|
opts = append(opts, dopts...)
|
||||||
|
|
||||||
|
// Provide a net dialer that supports cancelation and timeout.
|
||||||
f := func(dialEp string, t time.Duration) (net.Conn, error) {
|
f := func(dialEp string, t time.Duration) (net.Conn, error) {
|
||||||
proto, host, _ := endpoint.ParseEndpoint(dialEp)
|
proto, host, _ := endpoint.ParseEndpoint(dialEp)
|
||||||
if host == "" && ep != "" {
|
|
||||||
// dialing an endpoint not in the balancer; use
|
|
||||||
// endpoint passed into dial
|
|
||||||
proto, host, _ = endpoint.ParseEndpoint(ep)
|
|
||||||
}
|
|
||||||
if proto == "" {
|
|
||||||
return nil, fmt.Errorf("unknown scheme for %q", host)
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case <-c.ctx.Done():
|
case <-c.ctx.Done():
|
||||||
return nil, c.ctx.Err()
|
return nil, c.ctx.Err()
|
||||||
@ -266,7 +254,7 @@ func (c *Client) dialSetupOpts(target string, dopts ...grpc.DialOption) (opts []
|
|||||||
opts = append(opts, grpc.WithDialer(f))
|
opts = append(opts, grpc.WithDialer(f))
|
||||||
|
|
||||||
creds := c.creds
|
creds := c.creds
|
||||||
if _, _, scheme := endpoint.ParseEndpoint(ep); len(scheme) != 0 {
|
if len(scheme) != 0 {
|
||||||
creds = c.processCreds(scheme)
|
creds = c.processCreds(scheme)
|
||||||
}
|
}
|
||||||
if creds != nil {
|
if creds != nil {
|
||||||
@ -291,8 +279,9 @@ func (c *Client) dialSetupOpts(target string, dopts ...grpc.DialOption) (opts []
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dial connects to a single endpoint using the client's config.
|
// Dial connects to a single endpoint using the client's config.
|
||||||
func (c *Client) Dial(endpoint string) (*grpc.ClientConn, error) {
|
func (c *Client) Dial(ep string) (*grpc.ClientConn, error) {
|
||||||
return c.dial(endpoint)
|
_, _, scheme := endpoint.ParseEndpoint(ep)
|
||||||
|
return c.dial(endpoint.DirectTarget(ep), scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getToken(ctx context.Context) error {
|
func (c *Client) getToken(ctx context.Context) error {
|
||||||
@ -303,9 +292,9 @@ func (c *Client) getToken(ctx context.Context) error {
|
|||||||
ep := c.cfg.Endpoints[i]
|
ep := c.cfg.Endpoints[i]
|
||||||
// use dial options without dopts to avoid reusing the client balancer
|
// use dial options without dopts to avoid reusing the client balancer
|
||||||
var dOpts []grpc.DialOption
|
var dOpts []grpc.DialOption
|
||||||
_, host, _ := endpoint.ParseEndpoint(ep)
|
_, host, scheme := endpoint.ParseEndpoint(ep)
|
||||||
target := c.resolverGroup.Target(host)
|
target := c.resolverGroup.Target(host)
|
||||||
dOpts, err = c.dialSetupOpts(target, c.cfg.DialOptions...)
|
dOpts, err = c.dialSetupOpts(scheme, c.cfg.DialOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("failed to configure auth dialer: %v", err)
|
err = fmt.Errorf("failed to configure auth dialer: %v", err)
|
||||||
continue
|
continue
|
||||||
@ -333,13 +322,17 @@ func (c *Client) getToken(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) dial(ep string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
// dialWithBalancer dials the client's current load balanced resolver group. The scheme of the host
|
||||||
// We pass a target to DialContext of the form: endpoint://<clusterName>/<host-part> that
|
// of the provided endpoint determines the scheme used for all endpoints of the client connection.
|
||||||
// does not include scheme (http/https/unix/unixs) or path parts.
|
func (c *Client) dialWithBalancer(ep string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||||
_, host, _ := endpoint.ParseEndpoint(ep)
|
_, host, scheme := endpoint.ParseEndpoint(ep)
|
||||||
target := c.resolverGroup.Target(host)
|
target := c.resolverGroup.Target(host)
|
||||||
|
return c.dial(target, scheme, dopts...)
|
||||||
|
}
|
||||||
|
|
||||||
opts, err := c.dialSetupOpts(target, dopts...)
|
// dial configures and dials any grpc balancer target.
|
||||||
|
func (c *Client) dial(target string, scheme string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||||
|
opts, err := c.dialSetupOpts(scheme, dopts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to configure dialer: %v", err)
|
return nil, fmt.Errorf("failed to configure dialer: %v", err)
|
||||||
}
|
}
|
||||||
@ -467,7 +460,7 @@ func newClient(cfg *Config) (*Client, error) {
|
|||||||
|
|
||||||
// Use an provided endpoint target so that for https:// without any tls config given, then
|
// Use an provided endpoint target so that for https:// without any tls config given, then
|
||||||
// grpc will assume the certificate server name is the endpoint host.
|
// grpc will assume the certificate server name is the endpoint host.
|
||||||
conn, err := client.dial(dialEndpoint, grpc.WithBalancerName(roundRobinBalancerName))
|
conn, err := client.dialWithBalancer(dialEndpoint, grpc.WithBalancerName(roundRobinBalancerName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.cancel()
|
client.cancel()
|
||||||
client.resolverGroup.Close()
|
client.resolverGroup.Close()
|
||||||
|
@ -25,7 +25,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/clientv3"
|
||||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
"github.com/coreos/etcd/integration"
|
"github.com/coreos/etcd/integration"
|
||||||
"github.com/coreos/etcd/lease"
|
"github.com/coreos/etcd/lease"
|
||||||
@ -193,3 +195,47 @@ func TestMaintenanceSnapshotErrorInflight(t *testing.T) {
|
|||||||
t.Errorf("expected client timeout, got %v", err)
|
t.Errorf("expected client timeout, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMaintenanceStatus(t *testing.T) {
|
||||||
|
defer testutil.AfterTest(t)
|
||||||
|
|
||||||
|
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 3})
|
||||||
|
defer clus.Terminate(t)
|
||||||
|
|
||||||
|
clus.WaitLeader(t)
|
||||||
|
|
||||||
|
eps := make([]string, 3)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
eps[i] = clus.Members[i].GRPCAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := clientv3.New(clientv3.Config{Endpoints: eps, DialOptions: []grpc.DialOption{grpc.WithBlock()}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
prevID, leaderFound := uint64(0), false
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
resp, err := cli.Status(context.TODO(), eps[i])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if prevID == 0 {
|
||||||
|
prevID, leaderFound = resp.Header.MemberId, resp.Header.MemberId == resp.Leader
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prevID == resp.Header.MemberId {
|
||||||
|
t.Errorf("#%d: status returned duplicate member ID with %016x", i, prevID)
|
||||||
|
}
|
||||||
|
if leaderFound && resp.Header.MemberId == resp.Leader {
|
||||||
|
t.Errorf("#%d: leader already found, but found another %016x", i, resp.Header.MemberId)
|
||||||
|
}
|
||||||
|
if !leaderFound {
|
||||||
|
leaderFound = resp.Header.MemberId == resp.Leader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !leaderFound {
|
||||||
|
t.Fatal("no leader found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -76,7 +76,7 @@ type maintenance struct {
|
|||||||
func NewMaintenance(c *Client) Maintenance {
|
func NewMaintenance(c *Client) Maintenance {
|
||||||
api := &maintenance{
|
api := &maintenance{
|
||||||
dial: func(endpoint string) (pb.MaintenanceClient, func(), error) {
|
dial: func(endpoint string) (pb.MaintenanceClient, func(), error) {
|
||||||
conn, err := c.dial(endpoint)
|
conn, err := c.Dial(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to dial endpoint %s with maintenance client: %v", endpoint, err)
|
return nil, nil, fmt.Errorf("failed to dial endpoint %s with maintenance client: %v", endpoint, err)
|
||||||
}
|
}
|
||||||
|
@ -72,10 +72,12 @@ func testCtlV3MoveLeader(t *testing.T, cfg etcdProcessClusterConfig) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
resp, err := cli.Status(context.Background(), ep)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
resp, err := cli.Status(ctx, ep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatalf("failed to get status from endpoint %s: %v", ep, err)
|
||||||
}
|
}
|
||||||
|
cancel()
|
||||||
cli.Close()
|
cli.Close()
|
||||||
|
|
||||||
if resp.Header.GetMemberId() == resp.Leader {
|
if resp.Header.GetMemberId() == resp.Leader {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user