Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grpc: cleanup parse target and authority tests #4787

Merged
merged 4 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
122 changes: 122 additions & 0 deletions clientconn_authority_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
*
* Copyright 2021 gRPC 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 grpc

import (
"context"
"net"
"testing"

"google.golang.org/grpc/credentials"
"google.golang.org/grpc/testdata"
)

func (s) TestClientConnAuthority(t *testing.T) {
serverNameOverride := "over.write.server.name"
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), serverNameOverride)
if err != nil {
t.Fatalf("credentials.NewClientTLSFromFile(_, %q) failed: %v", err, serverNameOverride)
}

tests := []struct {
name string
target string
opts []DialOption
wantAuthority string
}{
{
name: "default",
target: "Non-Existent.Server:8080",
opts: []DialOption{WithInsecure()},
wantAuthority: "Non-Existent.Server:8080",
},
{
name: "override-via-creds",
target: "Non-Existent.Server:8080",
opts: []DialOption{WithTransportCredentials(creds)},
wantAuthority: serverNameOverride,
},
{
name: "override-via-WithAuthority",
target: "Non-Existent.Server:8080",
opts: []DialOption{WithInsecure(), WithAuthority("authority-override")},
wantAuthority: "authority-override",
},
{
name: "override-via-creds-and-WithAuthority",
target: "Non-Existent.Server:8080",
// WithAuthority override works only for insecure creds.
opts: []DialOption{WithTransportCredentials(creds), WithAuthority("authority-override")},
wantAuthority: serverNameOverride,
},
{
name: "unix relative",
target: "unix:sock.sock",
opts: []DialOption{WithInsecure()},
wantAuthority: "localhost",
},
{
name: "unix relative with custom dialer",
target: "unix:sock.sock",
opts: []DialOption{WithInsecure(), WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "", addr)
})},
wantAuthority: "localhost",
},
{
name: "unix absolute",
target: "unix:/sock.sock",
opts: []DialOption{WithInsecure()},
wantAuthority: "localhost",
},
{
name: "unix absolute with custom dialer",
target: "unix:///sock.sock",
opts: []DialOption{WithInsecure(), WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "", addr)
})},
wantAuthority: "localhost",
},
{
name: "localhost colon port",
target: "localhost:50051",
opts: []DialOption{WithInsecure()},
wantAuthority: "localhost:50051",
},
{
name: "colon port",
target: ":50051",
opts: []DialOption{WithInsecure()},
wantAuthority: "localhost:50051",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cc, err := Dial(test.target, test.opts...)
if err != nil {
t.Fatalf("Dial(%q) failed: %v", test.target, err)
}
defer cc.Close()
if cc.authority != test.wantAuthority {
t.Fatalf("cc.authority = %q, want %q", cc.authority, test.wantAuthority)
}
})
}
}
183 changes: 183 additions & 0 deletions clientconn_parsed_target_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
*
* Copyright 2021 gRPC 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 grpc

import (
"context"
"errors"
"net"
"testing"
"time"

"google.golang.org/grpc/resolver"
)

func (s) TestParsedTarget_Success_WithoutCustomDialer(t *testing.T) {
defScheme := resolver.GetDefaultScheme()
tests := []struct {
target string
wantParsed resolver.Target
}{
// No scheme is specified.
{target: "", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: ""}},
{target: "://", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "://"}},
{target: ":///", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: ":///"}},
{target: "://a/", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "://a/"}},
{target: ":///a", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: ":///a"}},
{target: "://a/b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "://a/b"}},
{target: "/", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "/"}},
{target: "a/b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a/b"}},
{target: "a//b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a//b"}},
{target: "google.com", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "google.com"}},
{target: "google.com/?a=b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "google.com/?a=b"}},
{target: "/unix/socket/address", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "/unix/socket/address"}},

// An unregistered scheme is specified.
{target: "a:///", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a:///"}},
{target: "a://b/", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a://b/"}},
{target: "a:///b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a:///b"}},
{target: "a://b/c", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a://b/c"}},
{target: "a:b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a:b"}},
{target: "a:/b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a:/b"}},
{target: "a://b", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "a://b"}},

// A registered scheme is specified.
{target: "dns:///google.com", wantParsed: resolver.Target{Scheme: "dns", Authority: "", Endpoint: "google.com"}},
{target: "dns://a.server.com/google.com", wantParsed: resolver.Target{Scheme: "dns", Authority: "a.server.com", Endpoint: "google.com"}},
{target: "dns://a.server.com/google.com/?a=b", wantParsed: resolver.Target{Scheme: "dns", Authority: "a.server.com", Endpoint: "google.com/?a=b"}},
{target: "unix:///a/b/c", wantParsed: resolver.Target{Scheme: "unix", Authority: "", Endpoint: "/a/b/c"}},
{target: "unix-abstract:a/b/c", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "a/b/c"}},
{target: "unix-abstract:a b", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "a b"}},
{target: "unix-abstract:a:b", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "a:b"}},
{target: "unix-abstract:a-b", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "a-b"}},
{target: "unix-abstract:/ a///://::!@#$%^&*()b", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "/ a///://::!@#$%^&*()b"}},
{target: "unix-abstract:passthrough:abc", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "passthrough:abc"}},
{target: "unix-abstract:unix:///abc", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "unix:///abc"}},
{target: "unix-abstract:///a/b/c", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "/a/b/c"}},
{target: "unix-abstract:///", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "/"}},
{target: "unix-abstract://authority", wantParsed: resolver.Target{Scheme: "unix-abstract", Authority: "", Endpoint: "//authority"}},
{target: "unix://domain", wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "unix://domain"}},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be Scheme: "unix", Endpoint: "//domain"? This is what happens with unix-abstract above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently handling unix-abstract://[authority/]endpoint specially in grpcutil.ParseTarget. But we don't do the same for unix scheme. See this block of code: https://github.com/grpc/grpc-go/blob/master/internal/grpcutil/target.go#L79-L82

But the unix resolver (which owns both schemes) does not like any authority being specified and will fail the resolver.Build call if one is specified.

So, when we move to using url.Parse(), both these targets will parse as follows (without custom dialer):
Scheme: "unix-abstract", Authority: "", Endpoint: "authority" and
Scheme: "unix", Authority: "", Endpoint: "domain"
which is really the correct interpretation as per RFC 3986. And since we will also add a field to resolver.Target to store the original dial target specified by the user, we could do some extra handling in the unix resolver if required. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current behavior doesn't actually work with a non-custom dialer, so we should be able to make it do whatever we want. Well, not whatever we want, we should make sure it is compliant with https://github.com/grpc/grpc/blob/master/doc/naming.md.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack.

{target: "passthrough:///unix:///a/b/c", wantParsed: resolver.Target{Scheme: "passthrough", Authority: "", Endpoint: "unix:///a/b/c"}},
}

for _, test := range tests {
t.Run(test.target, func(t *testing.T) {
cc, err := Dial(test.target, WithInsecure())
if err != nil {
t.Fatalf("Dial(%q) failed: %v", test.target, err)
}
defer cc.Close()

if gotParsed := cc.parsedTarget; gotParsed != test.wantParsed {
t.Errorf("cc.parsedTarget = %+v, want %+v", gotParsed, test.wantParsed)
}
})
}
}

func (s) TestParsedTarget_Failure_WithoutCustomDialer(t *testing.T) {
targets := []string{
"unix://a/b/c",
"unix-abstract://authority/a/b/c",
}

for _, target := range targets {
t.Run(target, func(t *testing.T) {
if cc, err := Dial(target, WithInsecure()); err == nil {
dfawley marked this conversation as resolved.
Show resolved Hide resolved
defer cc.Close()
t.Fatalf("Dial(%q) succeeded cc.parsedTarget = %+v, expected to fail", target, cc.parsedTarget)
}
})
}
}

func (s) TestParsedTarget_WithCustomDialer(t *testing.T) {
defScheme := resolver.GetDefaultScheme()
tests := []struct {
target string
wantParsed resolver.Target
wantDialerAddress string
}{
// unix:[local_path], unix:[/absolute], and unix://[/absolute] have
// different behaviors with a custom dialer.
{
target: "unix:a/b/c",
wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "unix:a/b/c"},
wantDialerAddress: "unix:a/b/c",
},
{
target: "unix:/a/b/c",
wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "unix:/a/b/c"},
wantDialerAddress: "unix:/a/b/c",
},
{
target: "unix:///a/b/c",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we test a few things that aren't unix too? Maybe dns:somehostname:port, e.g.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't currently have any special behavior for non-unix schemes when a custom dialer is specified. Are you saying we should add tests for the dns scheme now and make sure things don't break when we make it the default?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably shouldn't be changing any parsing from what we do today when a custom dialer is involved, not just unix. Maybe we can reason about some changes being okay, but if, e.g., we use the DNS resolver instead of the passthrough resolver, we might break a custom dialer expecting the target name to be provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added some tests for dns and "no scheme".

wantParsed: resolver.Target{Scheme: "unix", Authority: "", Endpoint: "/a/b/c"},
wantDialerAddress: "unix:///a/b/c",
},
{
target: "dns:///127.0.0.1:50051",
wantParsed: resolver.Target{Scheme: "dns", Authority: "", Endpoint: "127.0.0.1:50051"},
wantDialerAddress: "127.0.0.1:50051",
},
{
target: ":///127.0.0.1:50051",
wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: ":///127.0.0.1:50051"},
wantDialerAddress: ":///127.0.0.1:50051",
},
{
target: "dns://authority/127.0.0.1:50051",
wantParsed: resolver.Target{Scheme: "dns", Authority: "authority", Endpoint: "127.0.0.1:50051"},
wantDialerAddress: "127.0.0.1:50051",
},
{
target: "://authority/127.0.0.1:50051",
wantParsed: resolver.Target{Scheme: defScheme, Authority: "", Endpoint: "://authority/127.0.0.1:50051"},
wantDialerAddress: "://authority/127.0.0.1:50051",
},
}

for _, test := range tests {
t.Run(test.target, func(t *testing.T) {
addrCh := make(chan string, 1)
dialer := func(ctx context.Context, address string) (net.Conn, error) {
addrCh <- address
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly concerned this could lead to a goroutine hang if this gets called three times before we close the ClientConn. You might be able to stimulate this by putting in a long enough sleep into the test after the select where we read from the channel. Blocking on the context inside a select here should remove that possibility.

Maybe this is practically impossible given the timings (a long enough backoff), but it still seems worthwhile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per our offline discussion, the subConn in this case moves to Idle and therefore there are no retries here. So, we should be good. Thanks.

return nil, errors.New("dialer error")
}

cc, err := Dial(test.target, WithInsecure(), WithContextDialer(dialer))
if err != nil {
t.Fatalf("Dial(%q) failed: %v", test.target, err)
}
defer cc.Close()

select {
case addr := <-addrCh:
if addr != test.wantDialerAddress {
t.Fatalf("address in custom dialer is %q, want %q", addr, test.wantDialerAddress)
}
case <-time.After(time.Second):
t.Fatal("timeout when waiting for custom dialer to be invoked")
}
if gotParsed := cc.parsedTarget; gotParsed != test.wantParsed {
t.Errorf("cc.parsedTarget for dial target %q = %+v, want %+v", test.target, gotParsed, test.wantParsed)
}
})
}
}
56 changes: 0 additions & 56 deletions clientconn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,62 +376,6 @@ func (s) TestWithTransportCredentialsTLS(t *testing.T) {
}
}

func (s) TestDefaultAuthority(t *testing.T) {
target := "Non-Existent.Server:8080"
conn, err := Dial(target, WithInsecure())
if err != nil {
t.Fatalf("Dial(_, _) = _, %v, want _, <nil>", err)
}
defer conn.Close()
if conn.authority != target {
t.Fatalf("%v.authority = %v, want %v", conn, conn.authority, target)
}
}

func (s) TestTLSServerNameOverwrite(t *testing.T) {
overwriteServerName := "over.write.server.name"
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), overwriteServerName)
if err != nil {
t.Fatalf("Failed to create credentials %v", err)
}
conn, err := Dial("passthrough:///Non-Existent.Server:80", WithTransportCredentials(creds))
if err != nil {
t.Fatalf("Dial(_, _) = _, %v, want _, <nil>", err)
}
defer conn.Close()
if conn.authority != overwriteServerName {
t.Fatalf("%v.authority = %v, want %v", conn, conn.authority, overwriteServerName)
}
}

func (s) TestWithAuthority(t *testing.T) {
overwriteServerName := "over.write.server.name"
conn, err := Dial("passthrough:///Non-Existent.Server:80", WithInsecure(), WithAuthority(overwriteServerName))
if err != nil {
t.Fatalf("Dial(_, _) = _, %v, want _, <nil>", err)
}
defer conn.Close()
if conn.authority != overwriteServerName {
t.Fatalf("%v.authority = %v, want %v", conn, conn.authority, overwriteServerName)
}
}

func (s) TestWithAuthorityAndTLS(t *testing.T) {
overwriteServerName := "over.write.server.name"
creds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), overwriteServerName)
if err != nil {
t.Fatalf("Failed to create credentials %v", err)
}
conn, err := Dial("passthrough:///Non-Existent.Server:80", WithTransportCredentials(creds), WithAuthority("no.effect.authority"))
if err != nil {
t.Fatalf("Dial(_, _) = _, %v, want _, <nil>", err)
}
defer conn.Close()
if conn.authority != overwriteServerName {
t.Fatalf("%v.authority = %v, want %v", conn, conn.authority, overwriteServerName)
}
}

// When creating a transport configured with n addresses, only calculate the
// backoff once per "round" of attempts instead of once per address (n times
// per "round" of attempts).
Expand Down