Skip to content

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

Merged
merged 6 commits into from
Aug 8, 2024

Conversation

hessjcg
Copy link
Contributor

@hessjcg hessjcg commented Jul 16, 2024

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:

  • 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:

db, err := sql.Open(
    "cloudsql-mysql",
    "myuser:mypass@cloudsql-mysql(prod-db.mycompany.example.com)/mydb",
)

Part of #842

@hessjcg hessjcg requested a review from a team as a code owner July 16, 2024 22:49
@hessjcg hessjcg force-pushed the gh-842-configure-with-dns branch 5 times, most recently from dd0c834 to a04a6f9 Compare July 17, 2024 15:22
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
Copy link
Member

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
}

https://go.dev/wiki/CodeReviewComments#indent-error-flow

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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.

@hessjcg hessjcg force-pushed the gh-842-configure-with-dns branch from a04a6f9 to 87d1d3b Compare July 17, 2024 16:10
@hessjcg hessjcg requested a review from enocom July 17, 2024 16:11
@hessjcg hessjcg force-pushed the gh-842-configure-with-dns branch from 87d1d3b to de69d61 Compare July 17, 2024 17:56
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)
Copy link
Member

@enocom enocom Jul 17, 2024

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).

Copy link
Contributor Author

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.

@hessjcg hessjcg force-pushed the gh-842-configure-with-dns branch from de69d61 to 4c2b080 Compare July 19, 2024 16:16
Copy link
Contributor Author

@hessjcg hessjcg left a 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
Copy link
Member

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:

Copy link
Contributor Author

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"))
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@hessjcg hessjcg left a 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
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. See review comment.

Copy link
Member

@enocom enocom left a 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(),
Copy link
Member

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.

Copy link
Contributor Author

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" {
Copy link
Member

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
}

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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.

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.

@hessjcg hessjcg requested a review from enocom August 6, 2024 20:17
@enocom enocom changed the title feat: Automatially configure connections using DNS feat: Support configure connections using DNS Aug 8, 2024
Copy link
Member

@enocom enocom left a 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.

@hessjcg hessjcg merged commit ec6b3a0 into main Aug 8, 2024
13 checks passed
@hessjcg hessjcg deleted the gh-842-configure-with-dns branch August 8, 2024 19:12
hessjcg added a commit to GoogleCloudPlatform/cloud-sql-nodejs-connector that referenced this pull request Mar 17, 2025
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants