Skip to content

Commit 6db5dfc

Browse files
authored
feat: Support Developer Edition connections (#2610)
* feat: Support AI Developer Edition connections through the Cloud SQL Auth Proxy Cloud SQL AI Developer Edition instances allow connections through the SqlDataService. With this change and the corresponding chagnes in the Cloud SQL Go Connector, the Auth Proxy will allow users to connect to AIDE instances. Set the --sql-data flag to enable this support. * Internal changes to skip fuse on Kokoro tests. * fix: force tcp4/tcp6 based on IP address type This avoids binding to IPv6 wildcard [::] when 0.0.0.0 is requested, which can cause connection refused errors for IPv4 loopback (127.0.0.1) clients in environments where bindv6only is enabled. * fix: add -buildvcs=false to go build in build.sh * Update cloud-sql-go-connector to v1.22.0 and add --sql-data-endpoint alias Update cloud-sql-go-connector dependency to v1.22.0 in go.mod. Create --sql-data-endpoint as a hidden alias for --sqldata-api-endpoint, supporting both CLI flags and environment variables. TAG=agy CONV=9333d162-5ca9-4d37-8cf4-47ce7a321b70 * Fix code review issues.
1 parent a403b1b commit 6db5dfc

15 files changed

Lines changed: 438 additions & 50 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
/logs/
1414
.tools
1515
test_results.txt
16+
test-results

Dockerfile.alpine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
2626
go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.alpine"
2727

2828
# Final stage
29-
FROM alpine:3@sha256:4d889c14e7d5a73929ab00be2ef8ff22437e7cbc545931e52554a7b00e123d8b
29+
FROM alpine:3@sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e848e56d95f
3030

3131
LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy"
3232

Dockerfile.bookworm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
2626
go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.bookworm"
2727

2828
# Final stage
29-
FROM gcr.io/cloud-marketplace-containers/google/debian12@sha256:d10f3a95f20cdf7e3d9d7cfbdcbdcc6ab26643714dab2d286e3030971d768188
29+
FROM gcr.io/cloud-marketplace-containers/google/debian12@sha256:8705219e4580f8c36633a84b491d35a66c3e788e81161d6b5f370daa948b9402
3030

3131
LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy"
3232

build.sh

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,25 @@ function clean() {
3333

3434
## build - Builds the project without running tests.
3535
function build() {
36-
go build -o ./cloud-sql-proxy main.go
36+
local metadata="${1:-}"
37+
local ldflags=""
38+
if [[ -n "$metadata" ]] ; then
39+
ldflags="-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=$metadata"
40+
fi
41+
go build -buildvcs=false -ldflags "$ldflags" -o ./cloud-sql-proxy main.go
3742
}
3843

3944
## test - Runs local unit tests.
4045
function test() {
41-
go test -v -race -cover -short ./...
46+
get_golang_tool 'go-junit-report' 'jstemmer/go-junit-report' 'github.com/jstemmer/go-junit-report/v2'
47+
mkdir -p test-results
48+
local args=( "./..." )
49+
if [[ "$#" -gt 0 ]] ; then
50+
args=( "$@" )
51+
fi
52+
go test -v -race -cover -short "${args[@]}" -json \
53+
| .tools/go-junit-report -iocopy -parser gojson -out test-results/unit.xml \
54+
| jq -j 'select(.Output) | .Output '
4255
}
4356

4457
## e2e - Runs end-to-end integration tests.
@@ -53,7 +66,11 @@ function e2e() {
5366
# e2e_ci - Run end-to-end integration tests in the CI system.
5467
# This assumes that the secrets in the env vars are already set.
5568
function e2e_ci() {
56-
go test -race -v ./... | tee test_results.txt
69+
get_golang_tool 'go-junit-report' 'jstemmer/go-junit-report' 'github.com/jstemmer/go-junit-report/v2'
70+
mkdir -p test-results
71+
go test -race -v ./... -json \
72+
| .tools/go-junit-report -iocopy -parser gojson -out test-results/e2e.xml \
73+
| jq -j 'select(.Output) | .Output '
5774
}
5875

5976
function get_golang_tool() {
@@ -227,16 +244,47 @@ function write_e2e_env(){
227244
done
228245

229246
# Set IAM User env vars to the local gcloud user
230-
echo "export MYSQL_IAM_USER='${local_user%%@*}'"
231-
echo "export POSTGRES_USER_IAM='$local_user'"
247+
echo "export MYSQL_IAM_USER='$(iam_user_mysql)'"
248+
echo "export POSTGRES_USER_IAM='$(iam_user_pg)'"
232249
} > "$1"
233250

234251
}
235252

253+
function iam_user_pg() {
254+
# Truncate the suffix `.iam.gserviceaccount.com` if it exists. Otherwise return the email.
255+
local email
256+
local pguser
257+
258+
email="$(iam_user_email)"
259+
pguser="${email%%.iam.gserviceaccount.com}"
260+
if [[ -n "$pguser" ]] ; then
261+
echo "$pguser"
262+
else
263+
echo "$email"
264+
fi
265+
266+
}
267+
268+
function iam_user_mysql() {
269+
# Truncate the part after the @
270+
local email
271+
local mysqluser
272+
273+
email=$(iam_user_email)
274+
mysqluser="${email%%@*}"
275+
echo "$mysqluser"
276+
}
277+
278+
function iam_user_email() {
279+
gcloud auth list --format json | jq -r '.[] | select (.status == "ACTIVE") | .account'
280+
}
281+
282+
236283
## build_image - Builds and pushes the proxy container image using local source.
237-
## Usage: ./build.sh build_image [image-url]
284+
## Usage: ./build.sh build_image [image-url] [metadata]
238285
function build_image() {
239286
local image_url="${1:-}"
287+
local metadata="${2:-container}"
240288
local push_arg=""
241289

242290
if [[ -n "$image_url" ]]; then
@@ -254,7 +302,7 @@ function build_image() {
254302
trap cleanup_build EXIT
255303

256304
echo "Building binary locally..."
257-
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container" -o cloud-sql-proxy
305+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs=false -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=$metadata" -o cloud-sql-proxy
258306

259307
echo "Creating temporary Dockerfile..."
260308
cat > Dockerfile.local <<EOF

cmd/root.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,12 @@ func NewCommand(opts ...Option) *Command {
494494

495495
// Flags that apply only to the root command
496496
localFlags := rootCmd.Flags()
497+
localFlags.SetNormalizeFunc(func(_ *pflag.FlagSet, name string) pflag.NormalizedName {
498+
if name == "sql-data-endpoint" {
499+
return pflag.NormalizedName("sqldata-api-endpoint")
500+
}
501+
return pflag.NormalizedName(name)
502+
})
497503
// Flags that apply to all sub-commands
498504
globalFlags := rootCmd.PersistentFlags()
499505

@@ -584,6 +590,9 @@ the cached copy has expired. Use this setting in environments where the
584590
CPU may be throttled and a background refresh cannot run reliably
585591
(e.g., Cloud Run)`,
586592
)
593+
localFlags.StringVar(&c.conf.SQLDataEndpoint, "sqldata-api-endpoint", "",
594+
"Override the SQL Data API endpoint",
595+
)
587596

588597
localFlags.BoolVar(&c.conf.RunConnectionTest, "run-connection-test", false, `Runs a connection test
589598
against all specified instances. If an instance is unreachable, the Proxy exits with a failure
@@ -606,6 +615,10 @@ only applicable to Unix sockets)`)
606615
"(*) Connect to the private ip address for all instances")
607616
localFlags.BoolVar(&c.conf.PSC, "psc", false,
608617
"(*) Connect to the PSC endpoint for all instances")
618+
localFlags.BoolVar(&c.conf.SQLDataEnabled, "sql-data", false,
619+
"Enable SQL Data to tunnel through the Cloud SQL Admin API without"+
620+
" needing network access to your public or private IP",
621+
)
609622

610623
return c
611624
}
@@ -619,9 +632,14 @@ func loadConfig(c *Command, args []string, opts []Option) error {
619632
c.Flags().VisitAll(func(f *pflag.Flag) {
620633
// Override any unset flags with Viper values to use the pflags
621634
// object as a single source of truth.
622-
if !f.Changed && v.IsSet(f.Name) {
623-
val := v.Get(f.Name)
624-
_ = c.Flags().Set(f.Name, fmt.Sprintf("%v", val))
635+
if !f.Changed {
636+
if v.IsSet(f.Name) {
637+
val := v.Get(f.Name)
638+
_ = c.Flags().Set(f.Name, fmt.Sprintf("%v", val))
639+
} else if f.Name == "sqldata-api-endpoint" && v.IsSet("sql-data-endpoint") {
640+
val := v.Get("sql-data-endpoint")
641+
_ = c.Flags().Set(f.Name, fmt.Sprintf("%v", val))
642+
}
625643
}
626644
})
627645

@@ -805,8 +823,18 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
805823
}
806824

807825
// If more than one IP type is set, error.
808-
if conf.PrivateIP && conf.PSC {
809-
return newBadCommandError("cannot specify --private-ip and --psc flags at the same time")
826+
var ipTypes int
827+
if conf.PrivateIP {
828+
ipTypes++
829+
}
830+
if conf.PSC {
831+
ipTypes++
832+
}
833+
if conf.SQLDataEnabled {
834+
ipTypes++
835+
}
836+
if ipTypes > 1 {
837+
return newBadCommandError("cannot specify --private-ip, --psc, and --sql-data flags at the same time")
810838
}
811839

812840
// If more than one auth method is set, error.
@@ -898,6 +926,7 @@ and re-try with just --auto-iam-authn`)
898926
p, pok := q["port"]
899927
u, uok := q["unix-socket"]
900928
up, upok := q["unix-socket-path"]
929+
sd, sdok := q["sql-data"]
901930

902931
if aok && uok {
903932
return newBadCommandError("cannot specify both address and unix-socket query params")
@@ -955,6 +984,16 @@ and re-try with just --auto-iam-authn`)
955984
}
956985
ic.UnixSocketPath = up[0]
957986
}
987+
if sdok {
988+
if len(sd) != 1 {
989+
return newBadCommandError(fmt.Sprintf("sql-data query param should be only one value %q", a))
990+
}
991+
if sd[0] != "true" && sd[0] != "false" {
992+
return newBadCommandError(fmt.Sprintf("sql-data query param should be \"true\" or \"false\" %q", a))
993+
}
994+
b := sd[0] == "true"
995+
ic.SQLDataEnabled = &b
996+
}
958997

959998
ic.IAMAuthN, err = parseBoolOpt(q, "auto-iam-authn")
960999
if err != nil {

cmd/root_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,36 @@ func TestNewCommandArguments(t *testing.T) {
466466
RunConnectionTest: true,
467467
}),
468468
},
469+
{
470+
desc: "using the sql-data flag",
471+
args: []string{"--sql-data", "proj:region:inst"},
472+
want: withDefaults(&proxy.Config{
473+
SQLDataEnabled: true,
474+
}),
475+
},
476+
{
477+
desc: "using the sqldata-api-endpoint flag",
478+
args: []string{"--sqldata-api-endpoint", "https://test.googleapis.com", "proj:region:inst"},
479+
want: withDefaults(&proxy.Config{
480+
SQLDataEndpoint: "https://test.googleapis.com",
481+
}),
482+
},
483+
{
484+
desc: "using the sql-data-endpoint flag alias",
485+
args: []string{"--sql-data-endpoint", "https://test.googleapis.com", "proj:region:inst"},
486+
want: withDefaults(&proxy.Config{
487+
SQLDataEndpoint: "https://test.googleapis.com",
488+
}),
489+
},
490+
{
491+
desc: "using the sql-data query param",
492+
args: []string{"proj:region:inst?sql-data=true"},
493+
want: withDefaults(&proxy.Config{
494+
Instances: []proxy.InstanceConnConfig{{
495+
SQLDataEnabled: pointer(true),
496+
}},
497+
}),
498+
},
469499
}
470500

471501
for _, tc := range tcs {
@@ -821,6 +851,30 @@ func TestNewCommandWithEnvironmentConfig(t *testing.T) {
821851
AutoIP: true,
822852
}),
823853
},
854+
{
855+
desc: "using the sql-data envvar",
856+
envName: "CSQL_PROXY_SQL_DATA",
857+
envValue: "true",
858+
want: withDefaults(&proxy.Config{
859+
SQLDataEnabled: true,
860+
}),
861+
},
862+
{
863+
desc: "using the sqldata-api-endpoint envvar",
864+
envName: "CSQL_PROXY_SQLDATA_API_ENDPOINT",
865+
envValue: "https://test.googleapis.com",
866+
want: withDefaults(&proxy.Config{
867+
SQLDataEndpoint: "https://test.googleapis.com",
868+
}),
869+
},
870+
{
871+
desc: "using the sql-data-endpoint envvar alias",
872+
envName: "CSQL_PROXY_SQL_DATA_ENDPOINT",
873+
envValue: "https://test.googleapis.com",
874+
want: withDefaults(&proxy.Config{
875+
SQLDataEndpoint: "https://test.googleapis.com",
876+
}),
877+
},
824878
}
825879
for _, tc := range tcs {
826880
t.Run(tc.desc, func(t *testing.T) {
@@ -1033,6 +1087,54 @@ func TestPSCQueryParams(t *testing.T) {
10331087
}
10341088
}
10351089

1090+
func TestSQLDataQueryParams(t *testing.T) {
1091+
tcs := []struct {
1092+
desc string
1093+
args []string
1094+
want *bool
1095+
}{
1096+
{
1097+
desc: "when the query string is absent",
1098+
args: []string{"proj:region:inst"},
1099+
want: nil,
1100+
},
1101+
{
1102+
desc: "when the query string is true",
1103+
args: []string{"proj:region:inst?sql-data=true"},
1104+
want: pointer(true),
1105+
},
1106+
{
1107+
desc: "when the query string is false",
1108+
args: []string{"proj:region:inst?sql-data=false"},
1109+
want: pointer(false),
1110+
},
1111+
}
1112+
for _, tc := range tcs {
1113+
t.Run(tc.desc, func(t *testing.T) {
1114+
c, err := invokeProxyCommand(tc.args)
1115+
if err != nil {
1116+
t.Fatalf("command.Execute: %v", err)
1117+
}
1118+
if tc.want == nil {
1119+
if len(c.conf.Instances) > 0 && c.conf.Instances[0].SQLDataEnabled != nil {
1120+
t.Fatalf("args = %v, want nil, got = %v", tc.args, *c.conf.Instances[0].SQLDataEnabled)
1121+
}
1122+
return
1123+
}
1124+
if len(c.conf.Instances) == 0 {
1125+
t.Fatal("expected at least one instance")
1126+
}
1127+
got := c.conf.Instances[0].SQLDataEnabled
1128+
if got == nil {
1129+
t.Fatalf("args = %v, want = %v, got = nil", tc.args, *tc.want)
1130+
}
1131+
if *got != *tc.want {
1132+
t.Errorf("args = %v, want = %v, got = %v", tc.args, *tc.want, *got)
1133+
}
1134+
})
1135+
}
1136+
}
1137+
10361138
func TestNewCommandWithErrors(t *testing.T) {
10371139
tcs := []struct {
10381140
desc string
@@ -1152,6 +1254,14 @@ func TestNewCommandWithErrors(t *testing.T) {
11521254
desc: "when the iam authn login query param contains multiple values",
11531255
args: []string{"proj:region:inst?auto-iam-authn=true&auto-iam-authn=false"},
11541256
},
1257+
{
1258+
desc: "when the sql-data query param contains multiple values",
1259+
args: []string{"proj:region:inst?sql-data=true&sql-data=false"},
1260+
},
1261+
{
1262+
desc: "when the sql-data query param is bogus",
1263+
args: []string{"proj:region:inst?sql-data=nope"},
1264+
},
11551265
{
11561266
desc: "when the iam authn login query param is bogus",
11571267
args: []string{"proj:region:inst?auto-iam-authn=nope"},
@@ -1185,6 +1295,13 @@ func TestNewCommandWithErrors(t *testing.T) {
11851295
"p:r:i",
11861296
},
11871297
},
1298+
{
1299+
desc: "using --private-ip with --sql-data",
1300+
args: []string{
1301+
"--private-ip", "--sql-data",
1302+
"p:r:i",
1303+
},
1304+
},
11881305
{
11891306
desc: "using private-ip query param with --auto-ip",
11901307
args: []string{

docs/cmd/cloud-sql-proxy.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,9 @@ cloud-sql-proxy INSTANCE_CONNECTION_NAME... [flags]
279279
status code.
280280
--skip-failed-instance-config If set, the Proxy will skip any instances that are invalid/unreachable (
281281
only applicable to Unix sockets)
282+
--sql-data Enable SQL Data to tunnel through the Cloud SQL Admin API without needing network access to your public or private IP
282283
--sqladmin-api-endpoint string API endpoint for all Cloud SQL Admin API requests. (default: https://sqladmin.googleapis.com)
284+
--sqldata-api-endpoint string Override the SQL Data API endpoint
283285
-l, --structured-logs Enable structured logging with LogEntry format
284286
--telemetry-prefix string Prefix for Cloud Monitoring metrics.
285287
--telemetry-project string Enable Cloud Monitoring and Cloud Trace with the provided project ID.

0 commit comments

Comments
 (0)