clientv3: Fix maintenance APIs to directly dial grpc endpoints correctly.

This commit is contained in:
Joe Betz 2018-07-20 14:16:50 -07:00
parent 0458c5d54b
commit b3b06a862a
No known key found for this signature in database
GPG Key ID: 4930C680B6E0DDB8
5 changed files with 112 additions and 43 deletions

View File

@ -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
}

View File

@ -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()

View File

@ -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")
}
}

View File

@ -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)
} }

View File

@ -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 {