feat: Support configure connections using DNS#843
Conversation
dd0c834 to
a04a6f9
Compare
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var r NetResolver |
There was a problem hiding this comment.
r := net.DefaultResolver
if cfg.resolver != nil {
r = cfg.resolver
}| cn, err := d.resolveInstanceName(ctx, icn) | ||
| if err != nil { | ||
| return nil, err | ||
| // The connection name was not project:region:instance |
There was a problem hiding this comment.
I think we don't need this block of code since it's part of resolveInstanceName?
There was a problem hiding this comment.
Fixed. I missed this during a refactor.
| return c, nil | ||
| } | ||
|
|
||
| func (d *Dialer) queryDNS(ctx context.Context, domainName string) (instance.ConnName, error) { |
There was a problem hiding this comment.
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.
Method comment added.
|
|
||
| // 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.
%q might be nice here for the SRV record -- it will wrap the value in quotes.
| // 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.
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.
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.
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.
|
|
||
| #### 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.
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.
I added a warning and clarified that they should use private dns.
a04a6f9 to
87d1d3b
Compare
87d1d3b to
de69d61
Compare
| // 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.
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.
Good insight, I agree. The Java implementation started heading in this direction too. I will update the PR accordingly.
de69d61 to
4c2b080
Compare
hessjcg
left a comment
There was a problem hiding this comment.
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.
"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.
Updated. Switching to TXT records.
| ) | ||
|
|
||
| func connect() { | ||
| cleanup, err := mysql.RegisterDriver("cloudsql-mysql", cloudsqlconn.WithCredentialsFile("key.json")) |
There was a problem hiding this comment.
I think we should have an explicit opt-in to this DNS feature, e.g.,
WithDNSResolution()
wdyt?
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var r InstanceConnectionNameResolver = cloudsql.DefaultInstanceConnectionNameResolver |
There was a problem hiding this comment.
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.
Yes. See review comment.
|
|
||
| // 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.
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.
Yes. See review comment.
| // 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.
Resolver -> Resolve would be more Go-like.
There was a problem hiding this comment.
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.
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.
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.
hessjcg
left a comment
There was a problem hiding this comment.
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.
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.
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.
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var r InstanceConnectionNameResolver = cloudsql.DefaultInstanceConnectionNameResolver |
There was a problem hiding this comment.
Yes. See review comment.
|
|
||
| // 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.
Yes. See review comment.
| // 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.
Yes. See review comment.
enocom
left a comment
There was a problem hiding this comment.
Looking good. A few more changes to make here.
|
|
||
| func connect() { | ||
| cleanup, err := mysql.RegisterDriver("cloudsql-mysql", | ||
| cloudsqlconn.WithDnsResolver(), |
There was a problem hiding this comment.
WithDNSResolver()
Also, let's do the equivalent of gofmt on this sample code.
|
|
||
| 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.
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.
This seems excessive. After simplifying obsolete test cases, fakeResolver only ever needs to resolve one domain name.
There was a problem hiding this comment.
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.
Actually this is what you do below in the resolver test -- let's do the same thing here.
enocom
left a comment
There was a problem hiding this comment.
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:
TXTprod-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