-
Notifications
You must be signed in to change notification settings - Fork 31
feat: Support configure connections using DNS #843
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
Conversation
dd0c834
to
a04a6f9
Compare
dialer.go
Outdated
@@ -253,6 +258,13 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) { | |||
if err != nil { | |||
return nil, err | |||
} | |||
var r NetResolver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
r := net.DefaultResolver
if cfg.resolver != nil {
r = cfg.resolver
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
dialer.go
Outdated
if err != nil { | ||
return nil, err | ||
// The connection name was not project:region:instance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't need this block of code since it's part of resolveInstanceName
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. I missed this during a refactor.
dialer.go
Outdated
@@ -565,3 +597,43 @@ func (d *Dialer) connectionInfoCache( | |||
|
|||
return c, nil | |||
} | |||
|
|||
func (d *Dialer) queryDNS(ctx context.Context, domainName string) (instance.ConnName, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a non-trivial method, let's add a godoc (even though it's private) for future readers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Method comment added.
dialer.go
Outdated
|
||
// If resolve failed and no records were found, return the error. | ||
if err != nil { | ||
return instance.ConnName{}, fmt.Errorf("unable to resolve SRV record for %s: %v", domainName, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
%q might be nice here for the SRV record -- it will wrap the value in quotes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
options.go
Outdated
@@ -34,6 +34,13 @@ import ( | |||
// An Option is an option for configuring a Dialer. | |||
type Option func(d *dialerConfig) | |||
|
|||
// NetResolver groups the methods on net.Resolver that are used by the DNS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're adding something to the public API for tests only, we should change our approach (maybe by patching an unexported field).
If on the other hand we think this is a useful interface for callers, we should remove "This allows the default net.Resolver instance to be overridden from tests" and elaborate on why a person might want to provide their own interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, nit if we're keeping this interface let's move it down to just above where it's used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to just above the WithResolver()
function.
I think this makes sense as a public API. I can imagine a customer keeping their DNS entries in a separate, private DNS server. Then they would need to configure the connector to use that DNS server.
README.md
Outdated
|
||
#### Configure your DNS Records | ||
|
||
Add a DNS SRV record for the Cloud SQL instance to the DNS used by your |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
People will do this for public IP. We should warn them that it leaks PII if they do that. This should be recommended for private contexts only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a warning and clarified that they should use private dns.
a04a6f9
to
87d1d3b
Compare
87d1d3b
to
de69d61
Compare
options.go
Outdated
// application may need to connect to a specific DNS server using a specially | ||
// configured instance of net.Resolver. | ||
type NetResolver interface { | ||
LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The more I think about this, the more I think this is the wrong interface. What we want generically is a way to go from some name (DNS record or key value store path or whatever the user wants) to an instance connection name. This would be more future proof for when we want to support alternate resolution techniques
type InstanceConnectionNameResolver interface {
Resolve(ctx context.Context, name string) (string, error)
}
Then we could provide a default DNS resolver that does the equivalent of the SRV lookup. I think this is a small change, but will unlock many more use cases (especially as we discover the shortcomings of DNS polling).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good insight, I agree. The Java implementation started heading in this direction too. I will update the PR accordingly.
de69d61
to
4c2b080
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created a new public interface InstanceConnectionNameResolver
and an internal implementation called DNSInstanceConnectionNameResolver
.
Cloud SQL instances to a public DNS server. This would allow anyone on the | ||
internet to discover the Cloud SQL instance name. | ||
|
||
For example: suppose you wanted to use the domain name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"For example, suppose ... to your database instance my-project:region:my-instance
, you would create the following DNS record:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Switching to TXT records.
README.md
Outdated
) | ||
|
||
func connect() { | ||
cleanup, err := mysql.RegisterDriver("cloudsql-mysql", cloudsqlconn.WithCredentialsFile("key.json")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have an explicit opt-in to this DNS feature, e.g.,
WithDNSResolution()
wdyt?
dialer.go
Outdated
@@ -253,6 +256,11 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) { | |||
if err != nil { | |||
return nil, err | |||
} | |||
var r InstanceConnectionNameResolver = cloudsql.DefaultInstanceConnectionNameResolver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we used a shorter name, e.g., DefaultResolver
? We have a public instance
package where we could put this, the interface, and the SRVResolver.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. See review comment.
options.go
Outdated
@@ -234,6 +236,21 @@ func WithIAMAuthN() Option { | |||
} | |||
} | |||
|
|||
// InstanceConnectionNameResolver resolves This allows an application to replace the default | |||
// DNSInstanceConnectionNameResolver with a custom implementation. | |||
type InstanceConnectionNameResolver interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably go in the instance
package next to the code that parses instance connection names. And that way we could just call it Resolver
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. See review comment.
options.go
Outdated
// InstanceConnectionNameResolver resolves This allows an application to replace the default | ||
// DNSInstanceConnectionNameResolver with a custom implementation. | ||
type InstanceConnectionNameResolver interface { | ||
Lookup(ctx context.Context, name string) (instanceName instance.ConnName, err error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolver -> Resolve would be more Go-like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. See review comment.
// net.DefaultResolver with a custom implementation. For example: the | ||
// application may need to connect to a specific DNS server using a specially | ||
// configured instance of net.Resolver. | ||
type netResolver interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we remove the duplicate interface and test this e2e? Or alternatively, what would it take to assemble a mock DNS server in unit tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need a mock for practicality. We need to write test cases covering our code when the DNS records are misconfigured. Also, we need to do this across all our supported connector languages. Mocking the DNS response is the most reliably way to implement these test cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a bit of refactoring as recommended.
I moved the ConnectionNameResolver
interface into the exported instance
package and renamed it's method from Lookup()
to Resolve()
I created two option methods: WithResolver(r ConnectionNameResolver)
and WithDnsResolver()
.
I configured the default behavior of the dialer to only accept instance names.
Cloud SQL instances to a public DNS server. This would allow anyone on the | ||
internet to discover the Cloud SQL instance name. | ||
|
||
For example: suppose you wanted to use the domain name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Switching to TXT records.
// net.DefaultResolver with a custom implementation. For example: the | ||
// application may need to connect to a specific DNS server using a specially | ||
// configured instance of net.Resolver. | ||
type netResolver interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need a mock for practicality. We need to write test cases covering our code when the DNS records are misconfigured. Also, we need to do this across all our supported connector languages. Mocking the DNS response is the most reliably way to implement these test cases.
dialer.go
Outdated
@@ -253,6 +256,11 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) { | |||
if err != nil { | |||
return nil, err | |||
} | |||
var r InstanceConnectionNameResolver = cloudsql.DefaultInstanceConnectionNameResolver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. See review comment.
options.go
Outdated
@@ -234,6 +236,21 @@ func WithIAMAuthN() Option { | |||
} | |||
} | |||
|
|||
// InstanceConnectionNameResolver resolves This allows an application to replace the default | |||
// DNSInstanceConnectionNameResolver with a custom implementation. | |||
type InstanceConnectionNameResolver interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. See review comment.
options.go
Outdated
// InstanceConnectionNameResolver resolves This allows an application to replace the default | ||
// DNSInstanceConnectionNameResolver with a custom implementation. | ||
type InstanceConnectionNameResolver interface { | ||
Lookup(ctx context.Context, name string) (instanceName instance.ConnName, err error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. See review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. A few more changes to make here.
README.md
Outdated
|
||
func connect() { | ||
cleanup, err := mysql.RegisterDriver("cloudsql-mysql", | ||
cloudsqlconn.WithDnsResolver(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithDNSResolver()
Also, let's do the equivalent of gofmt on this sample code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
dialer_test.go
Outdated
|
||
func (r *fakeResolver) Resolve(_ context.Context, name string) (instance.ConnName, error) { | ||
// For TestDialerSuccessfullyDialsDnsTxtRecord | ||
if name == "db.example.com" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of relying on hardcoded values for particular tests, you could set fields in fakeResolver
which makes this more future proof and easy to extend:
type fakeResolve struct {
instances map[string]struct{
connName instance.ConnName
err error
}
}
And then this implementation becomes:
func (r *fakeResolver) Resolve(_ context.Context, name string) (instance.ConnName, error) {
icn, ok := r.instances[name]
if !ok {
return instance.ConnName{}, errors.New("not found")
}
return icn, nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems excessive. After simplifying obsolete test cases, fakeResolver
only ever needs to resolve one domain name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it helps test readability. Otherwise, a person has to go searching for what's special about db.example.com
.
If we're not supporting multiple instances anymore this also works:
WithResolver(&fakeResolver{validName: "db.example.com"}),
type fakeResolve struct {
validName string
}
func (r *fakeResolver) Resolve(_ context.Context, name string) (instance.ConnName, error) {
if name != r.validName {
// return error
}
// return connection name
}
The point is to make the fake extensible -- not just for this use, but for future use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this is what you do below in the resolver test -- let's do the same thing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be a well-liked feature.
The connector may be configured to use a DNS name to look up the instance name instead of configuring the connector with the instance name directly. Add a DNS TXT record for the Cloud SQL instance to a private DNS server or a private Google Cloud DNS Zone used by your application. For example: - Record type: TXT - Name: prod-db.mycompany.example.com – This is the domain name used by the application - Value: my-project:region:my-instance – This is the instance connection name Configure the dialer with the cloudsqlconn.WithDNSResolver() option. Open a database connection using the DNS name: ``` const clientOpts = await connector.getOptions({ domainName: "db.example.com", }); ``` Part of #421 See also: GoogleCloudPlatform/cloud-sql-go-connector#843
The dialer may be configured to use a DNS name to look up the instance
name instead of configuring the connector with the instance name directly.
Add a DNS TXT record for the Cloud SQL instance to a private DNS server
or a private Google Cloud DNS Zone used by your application. For example:
TXT
prod-db.mycompany.example.com
– This is the domain name used by the applicationmy-project:region:my-instance
– This is the instance connection nameConfigure the dialer with the
cloudsqlconn.WithDNSResolver()
option.Open a database connection using the DNS name:
Part of #842