Repository: GoogleCloudPlatform/cloud-sql-proxy
Pull request #1863 (2023-07-07 14:59:08)

feat: add PSC support to proxy
Add support for [Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect) (PSC) connection to Cloud SQL using the Proxy.


Patch:
diff --git a/cmd/root.go b/cmd/root.go
index b831b4f..4fa36bd 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -450,6 +450,8 @@ status code.`)
 		"(*) Enables Automatic IAM Authentication for all instances")
 	pflags.BoolVar(&c.conf.PrivateIP, "private-ip", false,
 		"(*) Connect to the private ip address for all instances")
+	pflags.BoolVar(&c.conf.PSC, "psc", false,
+		"(*) Connect to the PSC endpoint for all instances")
 
 	v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer("-", "_")))
 	v.SetEnvPrefix(envPrefix)
@@ -509,6 +511,11 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
 		return newBadCommandError("cannot specify --private-ip and --auto-ip together")
 	}
 
+	// If more than one IP type is set, error.
+	if conf.PrivateIP && conf.PSC {
+		return newBadCommandError("cannot specify --private-ip and --psc flags at the same time")
+	}
+
 	// If more than one auth method is set, error.
 	if conf.Token != "" && conf.CredentialsFile != "" {
 		return newBadCommandError("cannot specify --token and --credentials-file flags at the same time")
@@ -666,6 +673,15 @@ and re-try with just --auto-iam-authn`)
 				return newBadCommandError("cannot use --auto-ip with private-ip")
 			}
 
+			ic.PSC, err = parseBoolOpt(q, "psc")
+			if err != nil {
+				return err
+			}
+
+			if ic.PrivateIP != nil && ic.PSC != nil {
+				return newBadCommandError("cannot specify both private-ip and psc query params")
+			}
+
 		}
 		ics = append(ics, ic)
 	}
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index 461db95..5e1970b 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -102,6 +102,10 @@ type InstanceConnConfig struct {
 	// PrivateIP tells the proxy to attempt to connect to the db instance's
 	// private IP address instead of the public IP address
 	PrivateIP *bool
+
+	// PSC tells the proxy to attempt to connect to the db instance's
+	// private service connect endpoint
+	PSC *bool
 }
 
 // Config contains all the configuration provided by the caller.
@@ -168,6 +172,10 @@ type Config struct {
 	// for all instances.
 	PrivateIP bool
 
+	// PSC enables connections via the database server's private service connect
+	// endpoint for all instances
+	PSC bool
+
 	// AutoIP supports a legacy behavior where the Proxy will connect to
 	// the first IP address returned from the SQL ADmin API response. This
 	// setting should be avoided and used only to support legacy Proxy
@@ -253,6 +261,10 @@ func dialOptions(c Config, i InstanceConnConfig) []cloudsqlconn.DialOption {
 	// add the option.
 	case i.PrivateIP != nil && *i.PrivateIP || i.PrivateIP == nil && c.PrivateIP:
 		opts = append(opts, cloudsqlconn.WithPrivateIP())
+	// If PSC is enabled at the instance level, or PSC is enabled globally
+	// add the option.
+	case i.PSC != nil && *i.PSC || i.PSC == nil && c.PSC:
+		opts = append(opts, cloudsqlconn.WithPSC())
 	case c.AutoIP:
 		opts = append(opts, cloudsqlconn.WithAutoIP())
 	default:


Test patch:
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 6283e56..a64683c 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -356,6 +356,22 @@ func TestNewCommandArguments(t *testing.T) {
 				}},
 			}),
 		},
+		{
+			desc: "using the psc flag",
+			args: []string{"--psc", "proj:region:inst"},
+			want: withDefaults(&proxy.Config{
+				PSC: true,
+			}),
+		},
+		{
+			desc: "using the psc flag query param",
+			args: []string{"proj:region:inst?psc=true"},
+			want: withDefaults(&proxy.Config{
+				Instances: []proxy.InstanceConnConfig{{
+					PSC: pointer(true),
+				}},
+			}),
+		},
 		{
 			desc: "using the quota project flag",
 			args: []string{"--quota-project", "proj", "proj:region:inst"},
@@ -638,6 +654,14 @@ func TestNewCommandWithEnvironmentConfig(t *testing.T) {
 				PrivateIP: true,
 			}),
 		},
+		{
+			desc:     "using the psc envvar",
+			envName:  "CSQL_PROXY_PSC",
+			envValue: "true",
+			want: withDefaults(&proxy.Config{
+				PSC: true,
+			}),
+		},
 		{
 			desc:     "using the quota project envvar",
 			envName:  "CSQL_PROXY_QUOTA_PROJECT",
@@ -905,6 +929,79 @@ func TestPrivateIPQueryParams(t *testing.T) {
 	}
 }
 
+func TestPSCQueryParams(t *testing.T) {
+	tcs := []struct {
+		desc string
+		args []string
+		want *bool
+	}{
+		{
+			desc: "when the query string is absent",
+			args: []string{"proj:region:inst"},
+			want: nil,
+		},
+		{
+			desc: "when the query string has no value",
+			args: []string{"proj:region:inst?psc"},
+			want: pointer(true),
+		},
+		{
+			desc: "when the query string is true",
+			args: []string{"proj:region:inst?psc=true"},
+			want: pointer(true),
+		},
+		{
+			desc: "when the query string is True",
+			args: []string{"proj:region:inst?psc=True"},
+			want: pointer(true),
+		},
+		{
+			desc: "when the query string is (short) T",
+			args: []string{"proj:region:inst?psc=T"},
+			want: pointer(true),
+		},
+		{
+			desc: "when the query string is (short) t",
+			args: []string{"proj:region:inst?psc=t"},
+			want: pointer(true),
+		},
+		{
+			desc: "when the query string is false",
+			args: []string{"proj:region:inst?psc=false"},
+			want: pointer(false),
+		},
+		{
+			desc: "when the query string is (short) f",
+			args: []string{"proj:region:inst?psc=f"},
+			want: pointer(false),
+		},
+		{
+			desc: "when the query string is False",
+			args: []string{"proj:region:inst?psc=False"},
+			want: pointer(false),
+		},
+		{
+			desc: "when the query string is (short) F",
+			args: []string{"proj:region:inst?psc=F"},
+			want: pointer(false),
+		},
+	}
+	for _, tc := range tcs {
+		t.Run(tc.desc, func(t *testing.T) {
+			c, err := invokeProxyCommand(tc.args)
+			if err != nil {
+				t.Fatalf("command.Execute: %v", err)
+			}
+			if tc.want == nil && c.conf.Instances[0].PSC == nil {
+				return
+			}
+			if got := c.conf.Instances[0].PSC; *got != *tc.want {
+				t.Errorf("args = %v, want = %v, got = %v", tc.args, *tc.want, *got)
+			}
+		})
+	}
+}
+
 func TestNewCommandWithErrors(t *testing.T) {
 	tcs := []struct {
 		desc string
@@ -1064,6 +1161,17 @@ func TestNewCommandWithErrors(t *testing.T) {
 				"p:r:i?private-ip=true",
 			},
 		},
+		{
+			desc: "using private IP and psc query params",
+			args: []string{"p:r:i?private-ip=true&psc=true"},
+		},
+		{
+			desc: "using --private-ip with --psc",
+			args: []string{
+				"--private-ip", "--psc",
+				"p:r:i",
+			},
+		},
 		{
 			desc: "run-connection-test with fuse",
 			args: []string{