diff --git a/.gitignore b/.gitignore index de8224085..958b50e44 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /logs/ .tools test_results.txt +test-results diff --git a/Dockerfile.alpine b/Dockerfile.alpine index b31b9a6f4..04f900640 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -26,7 +26,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.alpine" # Final stage -FROM alpine:3@sha256:4d889c14e7d5a73929ab00be2ef8ff22437e7cbc545931e52554a7b00e123d8b +FROM alpine:3@sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e848e56d95f LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy" diff --git a/Dockerfile.bookworm b/Dockerfile.bookworm index 12fbf5799..bfa66e7a6 100644 --- a/Dockerfile.bookworm +++ b/Dockerfile.bookworm @@ -26,7 +26,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -ldflags "-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=container.bookworm" # Final stage -FROM gcr.io/cloud-marketplace-containers/google/debian12@sha256:d10f3a95f20cdf7e3d9d7cfbdcbdcc6ab26643714dab2d286e3030971d768188 +FROM gcr.io/cloud-marketplace-containers/google/debian12@sha256:8705219e4580f8c36633a84b491d35a66c3e788e81161d6b5f370daa948b9402 LABEL org.opencontainers.image.source="https://github.com/GoogleCloudPlatform/cloud-sql-proxy" diff --git a/build.sh b/build.sh index efdd6d303..eae421672 100755 --- a/build.sh +++ b/build.sh @@ -33,12 +33,25 @@ function clean() { ## build - Builds the project without running tests. function build() { - go build -o ./cloud-sql-proxy main.go + local metadata="${1:-}" + local ldflags="" + if [[ -n "$metadata" ]] ; then + ldflags="-X github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/cmd.metadataString=$metadata" + fi + go build -buildvcs=false -ldflags "$ldflags" -o ./cloud-sql-proxy main.go } ## test - Runs local unit tests. function test() { - go test -v -race -cover -short ./... + get_golang_tool 'go-junit-report' 'jstemmer/go-junit-report' 'github.com/jstemmer/go-junit-report/v2' + mkdir -p test-results + local args=( "./..." ) + if [[ "$#" -gt 0 ]] ; then + args=( "$@" ) + fi + go test -v -race -cover -short "${args[@]}" -json \ + | .tools/go-junit-report -iocopy -parser gojson -out test-results/unit.xml \ + | jq -j 'select(.Output) | .Output ' } ## e2e - Runs end-to-end integration tests. @@ -53,7 +66,11 @@ function e2e() { # e2e_ci - Run end-to-end integration tests in the CI system. # This assumes that the secrets in the env vars are already set. function e2e_ci() { - go test -race -v ./... | tee test_results.txt + get_golang_tool 'go-junit-report' 'jstemmer/go-junit-report' 'github.com/jstemmer/go-junit-report/v2' + mkdir -p test-results + go test -race -v ./... -json \ + | .tools/go-junit-report -iocopy -parser gojson -out test-results/e2e.xml \ + | jq -j 'select(.Output) | .Output ' } function get_golang_tool() { @@ -227,16 +244,47 @@ function write_e2e_env(){ done # Set IAM User env vars to the local gcloud user - echo "export MYSQL_IAM_USER='${local_user%%@*}'" - echo "export POSTGRES_USER_IAM='$local_user'" + echo "export MYSQL_IAM_USER='$(iam_user_mysql)'" + echo "export POSTGRES_USER_IAM='$(iam_user_pg)'" } > "$1" } +function iam_user_pg() { + # Truncate the suffix `.iam.gserviceaccount.com` if it exists. Otherwise return the email. + local email + local pguser + + email="$(iam_user_email)" + pguser="${email%%.iam.gserviceaccount.com}" + if [[ -n "$pguser" ]] ; then + echo "$pguser" + else + echo "$email" + fi + +} + +function iam_user_mysql() { + # Truncate the part after the @ + local email + local mysqluser + + email=$(iam_user_email) + mysqluser="${email%%@*}" + echo "$mysqluser" +} + +function iam_user_email() { + gcloud auth list --format json | jq -r '.[] | select (.status == "ACTIVE") | .account' +} + + ## build_image - Builds and pushes the proxy container image using local source. -## Usage: ./build.sh build_image [image-url] +## Usage: ./build.sh build_image [image-url] [metadata] function build_image() { local image_url="${1:-}" + local metadata="${2:-container}" local push_arg="" if [[ -n "$image_url" ]]; then @@ -254,7 +302,7 @@ function build_image() { trap cleanup_build EXIT echo "Building binary locally..." - 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 + 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 echo "Creating temporary Dockerfile..." cat > Dockerfile.local < 1 { + return newBadCommandError("cannot specify --private-ip, --psc, and --sql-data flags at the same time") } // If more than one auth method is set, error. @@ -898,6 +926,7 @@ and re-try with just --auto-iam-authn`) p, pok := q["port"] u, uok := q["unix-socket"] up, upok := q["unix-socket-path"] + sd, sdok := q["sql-data"] if aok && uok { return newBadCommandError("cannot specify both address and unix-socket query params") @@ -955,6 +984,16 @@ and re-try with just --auto-iam-authn`) } ic.UnixSocketPath = up[0] } + if sdok { + if len(sd) != 1 { + return newBadCommandError(fmt.Sprintf("sql-data query param should be only one value %q", a)) + } + if sd[0] != "true" && sd[0] != "false" { + return newBadCommandError(fmt.Sprintf("sql-data query param should be \"true\" or \"false\" %q", a)) + } + b := sd[0] == "true" + ic.SQLDataEnabled = &b + } ic.IAMAuthN, err = parseBoolOpt(q, "auto-iam-authn") if err != nil { diff --git a/cmd/root_test.go b/cmd/root_test.go index 4a4be698c..5641e6ca2 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -466,6 +466,36 @@ func TestNewCommandArguments(t *testing.T) { RunConnectionTest: true, }), }, + { + desc: "using the sql-data flag", + args: []string{"--sql-data", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + SQLDataEnabled: true, + }), + }, + { + desc: "using the sqldata-api-endpoint flag", + args: []string{"--sqldata-api-endpoint", "https://test.googleapis.com", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + SQLDataEndpoint: "https://test.googleapis.com", + }), + }, + { + desc: "using the sql-data-endpoint flag alias", + args: []string{"--sql-data-endpoint", "https://test.googleapis.com", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + SQLDataEndpoint: "https://test.googleapis.com", + }), + }, + { + desc: "using the sql-data query param", + args: []string{"proj:region:inst?sql-data=true"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + SQLDataEnabled: pointer(true), + }}, + }), + }, } for _, tc := range tcs { @@ -821,6 +851,30 @@ func TestNewCommandWithEnvironmentConfig(t *testing.T) { AutoIP: true, }), }, + { + desc: "using the sql-data envvar", + envName: "CSQL_PROXY_SQL_DATA", + envValue: "true", + want: withDefaults(&proxy.Config{ + SQLDataEnabled: true, + }), + }, + { + desc: "using the sqldata-api-endpoint envvar", + envName: "CSQL_PROXY_SQLDATA_API_ENDPOINT", + envValue: "https://test.googleapis.com", + want: withDefaults(&proxy.Config{ + SQLDataEndpoint: "https://test.googleapis.com", + }), + }, + { + desc: "using the sql-data-endpoint envvar alias", + envName: "CSQL_PROXY_SQL_DATA_ENDPOINT", + envValue: "https://test.googleapis.com", + want: withDefaults(&proxy.Config{ + SQLDataEndpoint: "https://test.googleapis.com", + }), + }, } for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { @@ -1033,6 +1087,54 @@ func TestPSCQueryParams(t *testing.T) { } } +func TestSQLDataQueryParams(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 is true", + args: []string{"proj:region:inst?sql-data=true"}, + want: pointer(true), + }, + { + desc: "when the query string is false", + args: []string{"proj:region:inst?sql-data=false"}, + 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 { + if len(c.conf.Instances) > 0 && c.conf.Instances[0].SQLDataEnabled != nil { + t.Fatalf("args = %v, want nil, got = %v", tc.args, *c.conf.Instances[0].SQLDataEnabled) + } + return + } + if len(c.conf.Instances) == 0 { + t.Fatal("expected at least one instance") + } + got := c.conf.Instances[0].SQLDataEnabled + if got == nil { + t.Fatalf("args = %v, want = %v, got = nil", tc.args, *tc.want) + } + if *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 @@ -1152,6 +1254,14 @@ func TestNewCommandWithErrors(t *testing.T) { desc: "when the iam authn login query param contains multiple values", args: []string{"proj:region:inst?auto-iam-authn=true&auto-iam-authn=false"}, }, + { + desc: "when the sql-data query param contains multiple values", + args: []string{"proj:region:inst?sql-data=true&sql-data=false"}, + }, + { + desc: "when the sql-data query param is bogus", + args: []string{"proj:region:inst?sql-data=nope"}, + }, { desc: "when the iam authn login query param is bogus", args: []string{"proj:region:inst?auto-iam-authn=nope"}, @@ -1185,6 +1295,13 @@ func TestNewCommandWithErrors(t *testing.T) { "p:r:i", }, }, + { + desc: "using --private-ip with --sql-data", + args: []string{ + "--private-ip", "--sql-data", + "p:r:i", + }, + }, { desc: "using private-ip query param with --auto-ip", args: []string{ diff --git a/docs/cmd/cloud-sql-proxy.md b/docs/cmd/cloud-sql-proxy.md index 6454113ec..ab5260df2 100644 --- a/docs/cmd/cloud-sql-proxy.md +++ b/docs/cmd/cloud-sql-proxy.md @@ -279,7 +279,9 @@ cloud-sql-proxy INSTANCE_CONNECTION_NAME... [flags] status code. --skip-failed-instance-config If set, the Proxy will skip any instances that are invalid/unreachable ( only applicable to Unix sockets) + --sql-data Enable SQL Data to tunnel through the Cloud SQL Admin API without needing network access to your public or private IP --sqladmin-api-endpoint string API endpoint for all Cloud SQL Admin API requests. (default: https://sqladmin.googleapis.com) + --sqldata-api-endpoint string Override the SQL Data API endpoint -l, --structured-logs Enable structured logging with LogEntry format --telemetry-prefix string Prefix for Cloud Monitoring metrics. --telemetry-project string Enable Cloud Monitoring and Cloud Trace with the provided project ID. diff --git a/go.mod b/go.mod index bdda07c1d..9c1d9712b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/GoogleCloudPlatform/cloud-sql-proxy/v2 go 1.25.8 require ( - cloud.google.com/go/cloudsqlconn v1.21.2 + cloud.google.com/go/cloudsqlconn v1.22.1 contrib.go.opencensus.io/exporter/prometheus v0.4.2 contrib.go.opencensus.io/exporter/stackdriver v0.13.14 github.com/coreos/go-systemd/v22 v22.7.0 @@ -18,7 +18,7 @@ require ( go.opencensus.io v0.24.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sys v0.46.0 - google.golang.org/api v0.284.0 + google.golang.org/api v0.286.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -34,7 +34,7 @@ require ( github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -45,7 +45,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -53,10 +53,10 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.68.1 // indirect + github.com/prometheus/common v0.69.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/prometheus v0.312.0 // indirect github.com/prometheus/statsd_exporter v0.30.0 // indirect @@ -75,13 +75,13 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto v0.0.0-20260608224507-4308a22a1bab // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260608224507-4308a22a1bab // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab // indirect + google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index d68b5c98f..2e3a45611 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/cloudsqlconn v1.21.2 h1:Iw/3W+6eIB9AChWFF2wlqLwpgaZ+DOX2DBUKKfsSDMI= -cloud.google.com/go/cloudsqlconn v1.21.2/go.mod h1:AXcbXAjdud2Hl6JLe80VHCaOjAsvh8O/JgQnhrRvJl8= +cloud.google.com/go/cloudsqlconn v1.22.1 h1:c4HkWMSV4pDL8kJid+nIuF+6iE07pR5Mn/sRVY17wf4= +cloud.google.com/go/cloudsqlconn v1.22.1/go.mod h1:p7l+u0ThOzSvC5a4fkywi1hEyD8S709X1zEqux9tsq0= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -107,8 +107,8 @@ github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= @@ -205,8 +205,8 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= -github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= +github.com/googleapis/enterprise-certificate-proxy v0.3.17 h1:73NfMHdiqo9JFU9+7a5ExpVa10/R29pXfZIaW559nrg= +github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= @@ -268,8 +268,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q= +github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -301,8 +301,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= @@ -459,8 +459,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -603,8 +603,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.284.0 h1:i+cKTgeQRcRySkP7QTl5PDO7/pAm8EcMFIUMlNbk4Vc= -google.golang.org/api v0.284.0/go.mod h1:AU44fU+XVZOCcd8uLaBIa/ZgzgPf/0qqY3+m7lQaado= +google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= +google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -640,12 +640,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20260608224507-4308a22a1bab h1:bG8JpL3dfsvJKRgrh7yMkswdxzBqQDRYqkLDHo3+708= -google.golang.org/genproto v0.0.0-20260608224507-4308a22a1bab/go.mod h1:cVHIikDNAdx8ISZeW+2rYkEMf3xn0GSaBYmVnWXQBUo= -google.golang.org/genproto/googleapis/api v0.0.0-20260608224507-4308a22a1bab h1:Foefixyu0l973HSYkX8Etw/fPxAmKRhyMGwuqXFiVI0= -google.golang.org/genproto/googleapis/api v0.0.0-20260608224507-4308a22a1bab/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab h1:cY0oV1VnAqvaim8VsR8ZyEKAudzbRJMRGwD3W/L7yOw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto v0.0.0-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY= +google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA= +google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs= +google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d/go.mod h1:O0ZOWSrfWfJ+Z5HbwZ+wNtHsg/vk1k2C/w67eww8PfQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d h1:mpAgMyM9vQHxycBlDq50y1VHpfSfVwzXvrQKtYbXuUY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/proxy/fuse_test.go b/internal/proxy/fuse_test.go index 02face315..1048d27ba 100644 --- a/internal/proxy/fuse_test.go +++ b/internal/proxy/fuse_test.go @@ -80,6 +80,9 @@ func TestFUSEREADME(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } dir := randTmpDir(t) d := &fakeDialer{} _, _, cleanup := newTestClient(t, d, dir, randTmpDir(t)) @@ -143,6 +146,9 @@ func TestFUSEDialInstance(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } fuseDir := randTmpDir(t) fuseTempDir := randTmpDir(t) tcs := []struct { @@ -201,6 +207,9 @@ func TestFUSEAcceptErrorReturnedFromServe(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } fuseDir := randTmpDir(t) fuseTempDir := randTmpDir(t) @@ -255,6 +264,9 @@ func TestFUSEReadDir(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } fuseDir := randTmpDir(t) _, _, cleanup := newTestClient(t, &fakeDialer{}, fuseDir, randTmpDir(t)) defer cleanup() @@ -284,6 +296,9 @@ func TestLookupIgnoresContext(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } // create context and cancel it immediately ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -305,6 +320,9 @@ func TestFUSEErrors(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } ctx := context.Background() d := &fakeDialer{} c, _, _ := newTestClient(t, d, randTmpDir(t), randTmpDir(t)) @@ -345,6 +363,9 @@ func TestFUSEWithBadInstanceName(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } fuseDir := randTmpDir(t) d := &fakeDialer{} _, _, cleanup := newTestClient(t, d, fuseDir, randTmpDir(t)) @@ -364,6 +385,9 @@ func TestFUSECheckConnections(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } fuseDir := randTmpDir(t) d := &fakeDialer{} c, _, cleanup := newTestClient(t, d, fuseDir, randTmpDir(t)) @@ -399,6 +423,9 @@ func TestFUSEClose(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } fuseDir := randTmpDir(t) d := &fakeDialer{} c, _, _ := newTestClient(t, d, fuseDir, randTmpDir(t)) @@ -422,6 +449,9 @@ func TestFUSEWithBadDir(t *testing.T) { if testing.Short() { t.Skip("skipping fuse tests in short mode.") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } conf := &proxy.Config{FUSEDir: "/not/a/dir", FUSETempDir: randTmpDir(t)} _, err := proxy.NewClient(context.Background(), &fakeDialer{}, testLogger, conf, nil) if err == nil { diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index ff58c552a..af8f3e768 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -95,6 +95,8 @@ type InstanceConnConfig struct { // necessary. If set, UnixSocketPath takes precedence over UnixSocket, Addr // and Port. UnixSocketPath string + // SQLDataEnabled enables connections through the SqlDataService for this connection. + SQLDataEnabled *bool // IAMAuthN enables automatic IAM DB Authentication for the instance. // MySQL and Postgres only. If it is nil, the value was not specified. IAMAuthN *bool @@ -200,6 +202,11 @@ type Config struct { // of a request context, e.g., Cloud Run. LazyRefresh bool + // SQLDataEnabled configures the dialer to use the SQL Data API. + SQLDataEnabled bool + // SQLDataEndpoint configures the endpoint of the SQL Data service. + SQLDataEndpoint string + // Instances are configuration for individual instances. Instance // configuration takes precedence over global configuration. Instances []InstanceConnConfig @@ -282,6 +289,9 @@ func dialOptions(c Config, i InstanceConnConfig) []cloudsqlconn.DialOption { if i.IAMAuthN != nil { opts = append(opts, cloudsqlconn.WithDialIAMAuthN(*i.IAMAuthN)) } + if i.SQLDataEnabled != nil && *i.SQLDataEnabled || c.SQLDataEnabled { + opts = append(opts, cloudsqlconn.WithSQLData()) + } switch { // If private IP is enabled at the instance level, or private IP is enabled globally @@ -469,6 +479,10 @@ func (c *Config) DialerOptions(l cloudsql.Logger) ([]cloudsqlconn.Option, error) opts = append(opts, cloudsqlconn.WithLazyRefresh()) } + if c.SQLDataEndpoint != "" { + opts = append(opts, cloudsqlconn.WithSQLDataEndpoint(c.SQLDataEndpoint)) + } + return opts, nil } @@ -564,8 +578,12 @@ func NewClient(ctx context.Context, d cloudsql.Dialer, l cloudsql.Logger, conf * return configureFUSE(c, conf) } + // unless the proxy is in SqlDataEnabled mode, initiate a refresh operation to warm the cache for _, inst := range conf.Instances { - // Initiate refresh operation and warm the cache. + // Skip instances with SqlDataEnabled + if conf.SQLDataEnabled || inst.SQLDataEnabled != nil && *inst.SQLDataEnabled { + continue + } go func(name string) { _, _ = d.EngineVersion(ctx, name) }(inst.Name) } @@ -852,6 +870,13 @@ func (c *Client) newSocketMount(ctx context.Context, conf *Config, pc *portConfi if inst.Addr != "" { a = inst.Addr } + if ip := net.ParseIP(a); ip != nil { + if ip.To4() != nil { + network = "tcp4" + } else { + network = "tcp6" + } + } var np int switch { @@ -859,6 +884,10 @@ func (c *Client) newSocketMount(ctx context.Context, conf *Config, pc *portConfi np = inst.Port case conf.Port != 0: np = pc.nextPort() + case conf.SQLDataEnabled || inst.SQLDataEnabled != nil && *inst.SQLDataEnabled: + // TODO: Only Postgres is supported by the SqlDataService + // when more engines are supported, this code will need to change. + np = pc.nextDBPort("POSTGRES") default: version, err := c.dialer.EngineVersion(ctx, inst.Name) // Exit if the port is not specified for inactive instance @@ -873,10 +902,17 @@ func (c *Client) newSocketMount(ctx context.Context, conf *Config, pc *portConfi } else { network = "unix" - version, err := c.dialer.EngineVersion(ctx, inst.Name) - if err != nil { - c.logger.Errorf("[%v] could not resolve instance version: %v", inst.Name, err) - return nil, err + var version string + switch { + case conf.SQLDataEnabled || inst.SQLDataEnabled != nil && *inst.SQLDataEnabled: + version = "POSTGRES" + default: + var err error + version, err = c.dialer.EngineVersion(ctx, inst.Name) + if err != nil { + c.logger.Errorf("[%v] could not resolve instance version: %v", inst.Name, err) + return nil, err + } } address, err = newUnixSocketMount(inst, conf.UnixSocket, strings.HasPrefix(version, "POSTGRES")) diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 2de5421f5..c529d6a84 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -33,6 +33,11 @@ import ( "github.com/GoogleCloudPlatform/cloud-sql-proxy/v2/internal/proxy" ) +var ( + sd = "proj:region:sd" + sd2 = "proj:region:sd2" +) + var testLogger = log.NewStdLogger(os.Stdout, os.Stdout) type fakeDialer struct { @@ -114,6 +119,12 @@ func createTempDir(t *testing.T) (string, func()) { } } +// pointer returns the address of v and makes it easy to take the address of a +// predeclared identifier. +func pointer[T any](v T) *T { + return &v +} + func TestClientInitialization(t *testing.T) { ctx := context.Background() testDir, cleanup := createTempDir(t) @@ -213,6 +224,47 @@ func TestClientInitialization(t *testing.T) { "127.0.0.1:1434", }, }, + { + desc: "with SQL Data enabled globally defaulting to Postgres port", + in: &proxy.Config{ + Addr: "127.0.0.1", + SQLDataEnabled: true, + Instances: []proxy.InstanceConnConfig{ + {Name: sd}, + {Name: sd2}, + }, + }, + wantTCPAddrs: []string{ + "127.0.0.1:5432", + "127.0.0.1:5433", + }, + }, + { + desc: "with SQL Data enabled per-instance defaulting to Postgres port", + in: &proxy.Config{ + Addr: "127.0.0.1", + Instances: []proxy.InstanceConnConfig{ + {Name: sd, SQLDataEnabled: pointer(true)}, + {Name: sd2, SQLDataEnabled: pointer(true)}, + }, + }, + wantTCPAddrs: []string{ + "127.0.0.1:5432", + "127.0.0.1:5433", + }, + }, + { + desc: "with SQL Data enabled but explicit port", + in: &proxy.Config{ + Addr: "127.0.0.1", + Instances: []proxy.InstanceConnConfig{ + {Name: sd, SQLDataEnabled: pointer(true), Port: 60000}, + }, + }, + wantTCPAddrs: []string{ + "127.0.0.1:60000", + }, + }, { desc: "with a Unix socket", in: &proxy.Config{ @@ -589,6 +641,7 @@ func TestClientNotifiesCallerOnServe(t *testing.T) { if err != nil { t.Fatalf("want error = nil, got = %v", err) } + defer c.Close() done := make(chan struct{}) notify := func() { close(done) } @@ -650,6 +703,62 @@ func TestClientConnCount(t *testing.T) { verifyOpen(t, 1) } +func TestSQLDataWarmup(t *testing.T) { + tcs := []struct { + desc string + in *proxy.Config + want int + }{ + { + desc: "standard warmup", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{{Name: "proj:reg:inst", Port: 30104}}, + }, + want: 1, + }, + { + desc: "warmup skipped globally", + in: &proxy.Config{ + SQLDataEnabled: true, + Instances: []proxy.InstanceConnConfig{{Name: "proj:reg:inst", Port: 30105}}, + }, + want: 0, + }, + { + desc: "warmup skipped per-instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + Name: "proj:reg:inst", + Port: 30106, + SQLDataEnabled: pointer(true), + }}, + }, + want: 0, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + d := &fakeDialer{} + c, err := proxy.NewClient(context.Background(), d, testLogger, tc.in, nil) + if err != nil { + t.Fatalf("proxy.NewClient error: %v", err) + } + defer c.Close() + + var got int + for i := 0; i < 10; i++ { + got = d.engineVersionAttempts() + if got == tc.want { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatalf("warmup attempts, want = %v, got = %v", tc.want, got) + }) + } +} + func TestCheckConnections(t *testing.T) { in := &proxy.Config{ Addr: "127.0.0.1", diff --git a/tests/connection_test.go b/tests/connection_test.go index 84f7e7bd1..accf27975 100644 --- a/tests/connection_test.go +++ b/tests/connection_test.go @@ -61,7 +61,7 @@ func removeAuthEnvVar(t *testing.T) (*oauth2.Token, string, func()) { func keyfile(t *testing.T) string { path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") if path == "" { - t.Fatal("GOOGLE_APPLICATION_CREDENTIALS not set") + t.Skip("GOOGLE_APPLICATION_CREDENTIALS not set") } creds, err := os.ReadFile(path) if err != nil { diff --git a/tests/fuse_test.go b/tests/fuse_test.go index a1b57368f..7898979a8 100644 --- a/tests/fuse_test.go +++ b/tests/fuse_test.go @@ -32,6 +32,9 @@ func TestPostgresFUSEConnect(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } tmpDir, cleanup := createTempDir(t) defer cleanup() diff --git a/tests/postgres_test.go b/tests/postgres_test.go index fe6400d02..341c47414 100644 --- a/tests/postgres_test.go +++ b/tests/postgres_test.go @@ -247,6 +247,9 @@ func TestPostgresIAMDBAuthn(t *testing.T) { if testing.Short() { t.Skip("skipping Postgres integration tests") } + if os.Getenv("SKIP_FUSE_E2E_TESTS") == "true" { + t.Skip("skipping Postgres FUSE integration tests because SKIP_FUSE_E2E_TESTS is set") + } requirePostgresVars(t) if *postgresIAMUser == "" { t.Fatal("'postgres_user_iam' not set")