diff --git a/instance/conn_name.go b/instance/conn_name.go index 0f9aec20..79120947 100644 --- a/instance/conn_name.go +++ b/instance/conn_name.go @@ -31,8 +31,7 @@ var ( // The domain name pattern in accordance with RFC 1035, RFC 1123 and RFC 2181. domainNameRegex = regexp.MustCompile(`^(?:[_a-z0-9](?:[_a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?)?$`) - instanceDNSSuffixRegex = regexp.MustCompile(`\.sql(-[a-zA-Z0-9_]+)?\.goog\.?$`) - globalInstanceDNSRegex = regexp.MustCompile(`\.global\.sql(-[a-zA-Z0-9_]+)?\.goog\.?$`) + instanceDNSNamePattern = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.(sql|sql-psa|sql-psc)\.goog\.?$`) ) // ConnName represents the "instance connection name", in the format @@ -129,47 +128,20 @@ type ConnectionNameResolver interface { // Suffix is one of: sql.goog, sql-psa.goog, sql-psc.goog (with optional trailing dot). // This returns ok == false when the region is "global" func ParseInstanceDNSName(dnsName string) (instanceLabel, projectLabel, region, suffix string, ok bool) { - dnsName = strings.TrimSuffix(dnsName, ".") dnsName = strings.ToLower(dnsName) - - parts := strings.Split(dnsName, ".") - if len(parts) != 5 { - return "", "", "", "", false - } - - if parts[4] != "goog" { - return "", "", "", "", false - } - - suffixType := parts[3] - if suffixType != "sql" && suffixType != "sql-psa" && suffixType != "sql-psc" { + m := instanceDNSNamePattern.FindStringSubmatch(dnsName) + if m == nil { return "", "", "", "", false } + instanceLabel = m[1] + projectLabel = m[2] + region = m[3] + suffix = m[4] + ".goog" - instanceLabel = parts[0] - projectLabel = parts[1] - region = parts[2] - suffix = suffixType + ".goog" - - // Validate region != "global". "global" is used for redirect to write endpoint DNS if region == "global" { return "", "", "", "", false } - // Validate labels - if len(instanceLabel) != 12 { - return "", "", "", "", false - } - for _, c := range instanceLabel { - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { - return "", "", "", "", false - } - } - - if !strings.Contains(region, "-") { - return "", "", "", "", false - } - return instanceLabel, projectLabel, region, suffix, true } @@ -177,5 +149,9 @@ func ParseInstanceDNSName(dnsName string) (instanceLabel, projectLabel, region, // Cloud SQL instance DNS name pattern. func IsInstanceDNSName(dnsName string) bool { dnsName = strings.ToLower(dnsName) - return instanceDNSSuffixRegex.MatchString(dnsName) && !globalInstanceDNSRegex.MatchString(dnsName) + m := instanceDNSNamePattern.FindStringSubmatch(dnsName) + if m == nil { + return false + } + return m[3] != "global" } diff --git a/internal/cloudsql/resolver_test.go b/internal/cloudsql/resolver_test.go index 9cb0da9b..fd5d4509 100644 --- a/internal/cloudsql/resolver_test.go +++ b/internal/cloudsql/resolver_test.go @@ -121,6 +121,34 @@ func TestDNSInstanceNameResolver_Lookup_Success_DirectPSC(t *testing.T) { } } +func TestDNSInstanceNameResolver_Lookup_Success_DirectPSC_Permissive(t *testing.T) { + // non-hex hash, short labels, region without hyphen + dnsName := "g123.p.uscentral.sql-psc.goog" + realConnName := "my-project:uscentral:my-instance" + want, _ := instance.ParseConnNameWithDomainName(realConnName, dnsName) + + client, cleanup, err := mock.NewSQLAdminService( + context.Background(), + mock.ResolveConnectSettingsSuccess(dnsName+".", "uscentral", realConnName, 1), + ) + if err != nil { + t.Fatal(err) + } + defer cleanup() + + r := dnsInstanceConnectionNameResolver{ + dnsResolver: &fakeResolver{}, + client: client, + } + got, err := r.Resolve(context.Background(), dnsName) + if err != nil { + t.Fatal("got error", err) + } + if got != want { + t.Fatal("Got", got, "Want", want) + } +} + func TestDNSInstanceNameResolver_Lookup_Success_CnamePSC(t *testing.T) { dnsName := "db.example.com" cnameTarget := "0123456789ab.fedcba9876543.europe-north2.sql-psc.goog" @@ -155,11 +183,12 @@ func TestDNSInstanceNameResolver_Lookup_Success_CnamePSC(t *testing.T) { func TestDNSInstanceNameResolver_Lookup_Fails_InvalidPattern(t *testing.T) { invalidDNSNames := []string{ - "0123456789ab.fedcba9876543.europe-north2.sql-psc.goog.com", // wrong suffix domain - "0123456789ag.fedcba9876543.europe-north2.sql-psc.goog", // non-hex char 'g' in hash - "0123456789a.fedcba9876543.europe-north2.sql-psc.goog", // wrong hash length (11) - "0123456789abc.fedcba9876543.europe-north2.sql-psc.goog", // wrong hash length (13) - "0123456789ab.fedcba9876543.europenorth2.sql-psc.goog", // region has no hyphen + "0123456789ab.fedcba9876543.europe-north2.sql-psc.goog.com", // wrong suffix domain + "0123456789ab.fedcba9876543.europe-north2.sql-psc.goog.other", // wrong suffix + "0123456789ab..europe-north2.sql-psc.goog", // empty project label + "-abc.def.ghi.sql.goog", // label starts with hyphen + "abc-.def.ghi.sql.goog", // label ends with hyphen + "abc.def.ghi.sql-other.goog", // invalid suffix type } for _, dnsName := range invalidDNSNames { diff --git a/monitored_cache_test.go b/monitored_cache_test.go index 09e1929b..25a1af0b 100644 --- a/monitored_cache_test.go +++ b/monitored_cache_test.go @@ -124,3 +124,43 @@ func TestMonitoredCache_Close(t *testing.T) { } } + +func TestMonitoredCache_OptimizationForImmutableNames(t *testing.T) { + tcs := []struct { + desc string + domainName string + wantTicker bool + }{ + { + desc: "custom DNS name", + domainName: "db.example.com", + wantTicker: true, + }, + { + desc: "immutable instance DNS name", + domainName: "0123456789ab.fedcba9876543.us-central1.sql-psc.goog", + wantTicker: false, + }, + { + desc: "mutable global instance DNS name", + domainName: "0123456789ab.fedcba9876543.global.sql-psc.goog", + wantTicker: true, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + cn, err := instance.ParseConnNameWithDomainName("my-project:my-region:my-instance", tc.domainName) + if err != nil { + t.Fatalf("ParseConnNameWithDomainName failed: %v", err) + } + c := newMonitoredCache(&spyConnectionInfoCache{}, cn, 10*time.Millisecond, nil, &testLog{t: t}) + defer c.Close() + + hasTicker := c.domainNameTicker != nil + if hasTicker != tc.wantTicker { + t.Errorf("got ticker = %v, want %v", hasTicker, tc.wantTicker) + } + }) + } +}