From 08c6f10c8f4542be7b60ebb538118b8dc0dcd01f Mon Sep 17 00:00:00 2001 From: Eno Compton Date: Tue, 12 Apr 2022 16:30:57 -0600 Subject: [PATCH 1/4] Import Cloud SQL Auth Proxy Import from v2 branch at: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/dd7198d940518471166d7f91efa24ae2e50061bc --- .build/alpine.yaml | 29 + .build/buster.yaml | 29 + .build/default.yaml | 28 + .build/gcs_upload.yaml | 98 ++ .envrc.example | 19 + .github/ISSUE_TEMPLATE/bug-report.md | 50 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/documentation-issue.md | 32 + .github/ISSUE_TEMPLATE/feature-request.md | 32 + .github/ISSUE_TEMPLATE/question.md | 31 + .github/PULL_REQUEST_TEMPLATE.md | 17 + .github/blunderbuss.yml | 25 + .github/header-checker-lint.yml | 22 + .github/release-please.yml | 18 + .github/renovate.json | 27 + .github/workflows/coverage.yaml | 50 + .github/workflows/tests.yaml | 34 + .gitignore | 14 + .kokoro/go116/linux/common.cfg | 35 + .kokoro/go116/linux/continuous.cfg | 15 + .kokoro/go116/linux/periodic.cfg | 15 + .kokoro/go116/linux/presubmit.cfg | 15 + .kokoro/go116/windows/common.cfg | 20 + .kokoro/go116/windows/continuous.cfg | 15 + .kokoro/go116/windows/periodic.cfg | 15 + .kokoro/go116/windows/presubmit.cfg | 15 + .kokoro/go117/linux/common.cfg | 35 + .kokoro/go117/linux/continuous.cfg | 15 + .kokoro/go117/linux/periodic.cfg | 15 + .kokoro/go117/linux/presubmit.cfg | 15 + .kokoro/go118/linux/common.cfg | 35 + .kokoro/go118/linux/continuous.cfg | 15 + .kokoro/go118/linux/periodic.cfg | 15 + .kokoro/go118/linux/presubmit.cfg | 15 + .kokoro/go118/macos/common.cfg | 21 + .kokoro/go118/macos/continuous.cfg | 15 + .kokoro/go118/macos/periodic.cfg | 15 + .kokoro/go118/macos/presubmit.cfg | 15 + .kokoro/release_artifacts.sh | 56 ++ .kokoro/tag_latest.sh | 42 + .kokoro/tests/run_tests.bat | 1 + .kokoro/tests/run_tests.sh | 34 + .kokoro/tests/run_tests_macos.sh | 41 + .kokoro/tests/run_tests_windows.sh | 34 + .kokoro/trampoline.sh | 16 + CHANGELOG.md | 215 +++++ CONTRIBUTORS | 40 + Dockerfile | 28 + Dockerfile.alpine | 36 + Dockerfile.buster | 34 + cloudsql/cloudsql.go | 34 + cmd/cloud_sql_proxy/cloud_sql_proxy.go | 734 +++++++++++++++ cmd/cloud_sql_proxy/cloud_sql_proxy_test.go | 33 + .../internal/healthcheck/healthcheck.go | 194 ++++ .../internal/healthcheck/healthcheck_test.go | 251 +++++ cmd/cloud_sql_proxy/proxy.go | 388 ++++++++ cmd/cloud_sql_proxy/proxy_test.go | 214 +++++ cmd/cloud_sql_proxy/version.txt | 1 + cmd/errors.go | 52 ++ cmd/root.go | 269 ++++++ cmd/root_test.go | 277 ++++++ cmd/version.txt | 1 + examples/k8s-health-check/README.md | 70 ++ .../proxy_with_http_health_check.yaml | 135 +++ examples/k8s-service/README.md | 277 ++++++ examples/k8s-service/ca_csr.json | 16 + examples/k8s-service/deployment.yaml | 67 ++ .../k8s-service/pgbouncer_deployment.yaml | 90 ++ examples/k8s-service/pgbouncer_service.yaml | 25 + examples/k8s-service/server_csr.json | 19 + examples/k8s-sidecar/README.md | 258 +++++ examples/k8s-sidecar/no_proxy_private_ip.yaml | 53 ++ examples/k8s-sidecar/proxy_with_sa_key.yaml | 98 ++ .../proxy_with_workload_identity.yaml | 92 ++ examples/k8s-sidecar/service_account.yaml | 20 + go.mod | 23 + go.sum | 882 ++++++++++++++++++ internal/proxy/proxy.go | 334 +++++++ internal/proxy/proxy_test.go | 169 ++++ logging/logging.go | 106 +++ main.go | 23 + proxy/README.md | 33 + proxy/certs/certs.go | 365 ++++++++ proxy/certs/certs_test.go | 174 ++++ proxy/dialers/mysql/hook.go | 94 ++ proxy/dialers/mysql/hook_test.go | 47 + proxy/dialers/postgres/hook.go | 61 ++ proxy/dialers/postgres/hook_test.go | 38 + proxy/fuse/fuse.go | 378 ++++++++ proxy/fuse/fuse_darwin.go | 43 + proxy/fuse/fuse_linux.go | 34 + proxy/fuse/fuse_linux_test.go | 47 + proxy/fuse/fuse_openbsd.go | 32 + proxy/fuse/fuse_test.go | 247 +++++ proxy/fuse/fuse_windows.go | 31 + proxy/limits/limits.go | 89 ++ proxy/limits/limits_freebsd.go | 82 ++ proxy/limits/limits_test.go | 136 +++ proxy/limits/limits_windows.go | 30 + proxy/proxy/client.go | 652 +++++++++++++ proxy/proxy/client_test.go | 637 +++++++++++++ proxy/proxy/common.go | 225 +++++ proxy/proxy/common_test.go | 115 +++ proxy/proxy/connect_tls_117.go | 45 + proxy/proxy/connect_tls_other.go | 113 +++ proxy/proxy/dial.go | 115 +++ proxy/util/cloudsqlutil.go | 45 + proxy/util/cloudsqlutil_test.go | 38 + proxy/util/gcloudutil.go | 123 +++ tests/alldb_test.go | 75 ++ tests/common_test.go | 193 ++++ tests/connection_test.go | 124 +++ tests/dialer_test.go | 146 +++ tests/healthcheck_test.go | 56 ++ tests/mysql_test.go | 118 +++ tests/postgres_test.go | 165 ++++ tests/sqlserver_test.go | 81 ++ testsV2/common_test.go | 147 +++ testsV2/connection_test.go | 80 ++ testsV2/mysql_test.go | 102 ++ testsV2/other_test.go | 57 ++ testsV2/postgres_test.go | 88 ++ testsV2/sqlserver_test.go | 86 ++ 123 files changed, 12303 insertions(+) create mode 100644 .build/alpine.yaml create mode 100644 .build/buster.yaml create mode 100644 .build/default.yaml create mode 100644 .build/gcs_upload.yaml create mode 100644 .envrc.example create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation-issue.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/blunderbuss.yml create mode 100644 .github/header-checker-lint.yml create mode 100644 .github/release-please.yml create mode 100644 .github/renovate.json create mode 100644 .github/workflows/coverage.yaml create mode 100644 .github/workflows/tests.yaml create mode 100644 .gitignore create mode 100644 .kokoro/go116/linux/common.cfg create mode 100644 .kokoro/go116/linux/continuous.cfg create mode 100644 .kokoro/go116/linux/periodic.cfg create mode 100644 .kokoro/go116/linux/presubmit.cfg create mode 100644 .kokoro/go116/windows/common.cfg create mode 100644 .kokoro/go116/windows/continuous.cfg create mode 100644 .kokoro/go116/windows/periodic.cfg create mode 100644 .kokoro/go116/windows/presubmit.cfg create mode 100644 .kokoro/go117/linux/common.cfg create mode 100644 .kokoro/go117/linux/continuous.cfg create mode 100644 .kokoro/go117/linux/periodic.cfg create mode 100644 .kokoro/go117/linux/presubmit.cfg create mode 100644 .kokoro/go118/linux/common.cfg create mode 100644 .kokoro/go118/linux/continuous.cfg create mode 100644 .kokoro/go118/linux/periodic.cfg create mode 100644 .kokoro/go118/linux/presubmit.cfg create mode 100644 .kokoro/go118/macos/common.cfg create mode 100644 .kokoro/go118/macos/continuous.cfg create mode 100644 .kokoro/go118/macos/periodic.cfg create mode 100644 .kokoro/go118/macos/presubmit.cfg create mode 100755 .kokoro/release_artifacts.sh create mode 100755 .kokoro/tag_latest.sh create mode 100644 .kokoro/tests/run_tests.bat create mode 100755 .kokoro/tests/run_tests.sh create mode 100644 .kokoro/tests/run_tests_macos.sh create mode 100755 .kokoro/tests/run_tests_windows.sh create mode 100644 .kokoro/trampoline.sh create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTORS create mode 100644 Dockerfile create mode 100644 Dockerfile.alpine create mode 100644 Dockerfile.buster create mode 100644 cloudsql/cloudsql.go create mode 100644 cmd/cloud_sql_proxy/cloud_sql_proxy.go create mode 100644 cmd/cloud_sql_proxy/cloud_sql_proxy_test.go create mode 100644 cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go create mode 100644 cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go create mode 100644 cmd/cloud_sql_proxy/proxy.go create mode 100644 cmd/cloud_sql_proxy/proxy_test.go create mode 100644 cmd/cloud_sql_proxy/version.txt create mode 100644 cmd/errors.go create mode 100644 cmd/root.go create mode 100644 cmd/root_test.go create mode 100644 cmd/version.txt create mode 100644 examples/k8s-health-check/README.md create mode 100644 examples/k8s-health-check/proxy_with_http_health_check.yaml create mode 100644 examples/k8s-service/README.md create mode 100644 examples/k8s-service/ca_csr.json create mode 100644 examples/k8s-service/deployment.yaml create mode 100644 examples/k8s-service/pgbouncer_deployment.yaml create mode 100644 examples/k8s-service/pgbouncer_service.yaml create mode 100644 examples/k8s-service/server_csr.json create mode 100644 examples/k8s-sidecar/README.md create mode 100644 examples/k8s-sidecar/no_proxy_private_ip.yaml create mode 100644 examples/k8s-sidecar/proxy_with_sa_key.yaml create mode 100644 examples/k8s-sidecar/proxy_with_workload_identity.yaml create mode 100644 examples/k8s-sidecar/service_account.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/proxy/proxy.go create mode 100644 internal/proxy/proxy_test.go create mode 100644 logging/logging.go create mode 100644 main.go create mode 100644 proxy/README.md create mode 100644 proxy/certs/certs.go create mode 100644 proxy/certs/certs_test.go create mode 100644 proxy/dialers/mysql/hook.go create mode 100644 proxy/dialers/mysql/hook_test.go create mode 100644 proxy/dialers/postgres/hook.go create mode 100644 proxy/dialers/postgres/hook_test.go create mode 100644 proxy/fuse/fuse.go create mode 100644 proxy/fuse/fuse_darwin.go create mode 100644 proxy/fuse/fuse_linux.go create mode 100644 proxy/fuse/fuse_linux_test.go create mode 100644 proxy/fuse/fuse_openbsd.go create mode 100644 proxy/fuse/fuse_test.go create mode 100644 proxy/fuse/fuse_windows.go create mode 100644 proxy/limits/limits.go create mode 100644 proxy/limits/limits_freebsd.go create mode 100644 proxy/limits/limits_test.go create mode 100644 proxy/limits/limits_windows.go create mode 100644 proxy/proxy/client.go create mode 100644 proxy/proxy/client_test.go create mode 100644 proxy/proxy/common.go create mode 100644 proxy/proxy/common_test.go create mode 100644 proxy/proxy/connect_tls_117.go create mode 100644 proxy/proxy/connect_tls_other.go create mode 100644 proxy/proxy/dial.go create mode 100644 proxy/util/cloudsqlutil.go create mode 100644 proxy/util/cloudsqlutil_test.go create mode 100644 proxy/util/gcloudutil.go create mode 100644 tests/alldb_test.go create mode 100644 tests/common_test.go create mode 100644 tests/connection_test.go create mode 100644 tests/dialer_test.go create mode 100644 tests/healthcheck_test.go create mode 100644 tests/mysql_test.go create mode 100644 tests/postgres_test.go create mode 100644 tests/sqlserver_test.go create mode 100644 testsV2/common_test.go create mode 100644 testsV2/connection_test.go create mode 100644 testsV2/mysql_test.go create mode 100644 testsV2/other_test.go create mode 100644 testsV2/postgres_test.go create mode 100644 testsV2/sqlserver_test.go diff --git a/.build/alpine.yaml b/.build/alpine.yaml new file mode 100644 index 0000000000..e170df50cd --- /dev/null +++ b/.build/alpine.yaml @@ -0,0 +1,29 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: +- name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - '-f=Dockerfile.alpine' + - '.' +images: + - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' + - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' \ No newline at end of file diff --git a/.build/buster.yaml b/.build/buster.yaml new file mode 100644 index 0000000000..194e766f40 --- /dev/null +++ b/.build/buster.yaml @@ -0,0 +1,29 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: +- name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - '-f=Dockerfile.buster' + - '.' +images: + - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' + - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' \ No newline at end of file diff --git a/.build/default.yaml b/.build/default.yaml new file mode 100644 index 0000000000..80b832bf85 --- /dev/null +++ b/.build/default.yaml @@ -0,0 +1,28 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: +- name: 'gcr.io/cloud-builders/docker' + args: + - 'build' + - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - '.' +images: + - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' + - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' \ No newline at end of file diff --git a/.build/gcs_upload.yaml b/.build/gcs_upload.yaml new file mode 100644 index 0000000000..d99e047d21 --- /dev/null +++ b/.build/gcs_upload.yaml @@ -0,0 +1,98 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +timeout: 900s +options: + env: + - "GOPATH=/workspace/GOPATH" + - "CGO_ENABLED=0" + +steps: + - id: linux.amd64 + name: "golang:1.17" + env: + - "GOOS=linux" + - "GOARCH=amd64" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' + - id: linux.386 + name: "golang:1.17" + env: + - "GOOS=linux" + - "GOARCH=386" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' + - id: linux.arm64 + name: "golang:1.17" + env: + - "GOOS=linux" + - "GOARCH=arm64" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' + - id: linux.arm + name: "golang:1.17" + env: + - "GOOS=linux" + - "GOARCH=arm" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' + - id: darwin.amd64 + name: "golang:1.17" + env: + - "GOOS=darwin" + - "GOARCH=amd64" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' + - id: darwin.arm64 + name: "golang:1.17" + env: + - "GOOS=darwin" + - "GOARCH=arm64" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' + - id: windows.amd64 + name: "golang:1.17" + env: + - "GOOS=windows" + - "GOARCH=amd64" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy_x64.exe ./cmd/cloud_sql_proxy' + - id: windows.386 + name: "golang:1.17" + env: + - "GOOS=windows" + - "GOARCH=386" + entrypoint: "bash" + args: + - "-c" + - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy_x86.exe ./cmd/cloud_sql_proxy' +artifacts: + objects: + location: "gs://cloudsql-proxy/v${_VERSION}/" + paths: + - "cloud_sql_proxy*" diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 0000000000..8bf5e9e3d2 --- /dev/null +++ b/.envrc.example @@ -0,0 +1,19 @@ +export GOOGLE_CLOUD_PROJECT="project-name" + +export MYSQL_CONNECTION_NAME="project:region:instance" +export MYSQL_USER="mysql-user" +export MYSQL_PASS="mysql-password" +export MYSQL_DB="mysql-db-name" + +export POSTGRES_CONNECTION_NAME="project:region:instance" +export POSTGRES_USER="postgres-user" +export POSTGRES_PASS="postgres-password" +export POSTGRES_DB="postgres-db-name" +export POSTGRES_USER_IAM="some-user-with-db-access@example.com" + +export SQLSERVER_CONNECTION_NAME="project:region:instance" +export SQLSERVER_USER="sqlserver-user" +export SQLSERVER_PASS="sqlserver-password" +export SQLSERVER_DB="sqlserver-db-name" + +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..a555a7142d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,50 @@ +--- +name: Bug Report +about: Report defective or unintentional behavior you've experienced. +title: "Brief summary of what bug or error was observed" +labels: 'type: bug' + +--- + + + +## Bug Description + +Please enter a detailed description of the bug, and any information about what +behavior you noticed and how it differs from what you expected. + +## Example code (or command) + +``` +// example +``` + +## Stacktrace +``` +Any relevant stacktrace here. Be sure to filter sensitive information. +``` + +## How to reproduce + + 1. ? + 2. ? + +## Environment + +1. OS type and version: +2. Cloud SQL Proxy version (`./cloud_sql_proxy -version`): diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..94e6589ed0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: +- name: Cloud SQL Issue tracker + url: https://issuetracker.google.com/savedsearches/559773 + about: Please use the Cloud SQL Issue tracker for problems with Cloud SQL itself. +- name: StackOverflow + url: https://stackoverflow.com/questions/tagged/google-cloud-sql + about: Please use the `google-cloud-sql` tag for questions on StackOverflow. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation-issue.md b/.github/ISSUE_TEMPLATE/documentation-issue.md new file mode 100644 index 0000000000..3692027d2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-issue.md @@ -0,0 +1,32 @@ +--- +name: Documentation Issue +about: Report wrong or missing information with the documentation in the repo. +title: "Brief summary of what is missing or incorrect" +labels: 'type: docs' + +--- + + +## Description +Provide a short description of what is missing or incorrect, as well as a link to the specific location of the information. + +## Solution +What would you prefer the documentation say? Why would this information be more accurate or helpful? + +## Additional Context +Please reference any other relevant issues, PRs, descriptions, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000000..6849df4250 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,32 @@ +--- +name: Feature Request +about: Suggest an idea for new or improved behavior. +title: "Brief summary of the proposed feature" +labels: 'type: feature request' + +--- + + +## Feature Description +A clear and concise description of what feature you would like to see, and why it would be useful to have added. + +## Alternatives Considered +Are there any workaround or third party tools to replicate this behavior? Why would adding this feature be preferred over them? + +## Additional Context +Please reference any other issues, PRs, descriptions, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000000..14f5e6bacd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,31 @@ +--- +name: Question +about: Questions on how something works or the best way to do something. +title: "Breif summary of your question" +labels: 'type: question' + +--- + + + +## Question +What's your question? Please provide as much relevant information as possible +to reduce turnaround time. + +## Additional Context +Please reference any other relevant issues, PRs, descriptions, or screenshots here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..9208f5ba3d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## Change Description + +Please provide a detailed description on what changes your PR will have. + + +## Checklist + +- [ ] Make sure to open an issue as a + [bug/issue](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/new/choose) + before writing your code! That way we can discuss the change, evaluate + designs, and agree on the general idea. +- [ ] Ensure the tests and linter pass +- [ ] Appropriate documentation is updated (if necessary) + +## Relevant issues: + +- Fixes # \ No newline at end of file diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml new file mode 100644 index 0000000000..de680bfe1e --- /dev/null +++ b/.github/blunderbuss.yml @@ -0,0 +1,25 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +assign_issues: +# - shubha-rajan + - enocom +# - jackwotherspoon +# - kurtisvg + +assign_prs: +# - shubha-rajan + - enocom +# - jackwotherspoon +# - kurtisvg diff --git a/.github/header-checker-lint.yml b/.github/header-checker-lint.yml new file mode 100644 index 0000000000..ece9919898 --- /dev/null +++ b/.github/header-checker-lint.yml @@ -0,0 +1,22 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +allowedCopyrightHolders: + - 'Google LLC' +allowedLicenses: + - 'Apache-2.0' +sourceFileExtensions: + - 'go' + - 'yaml' + - 'yml' diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 0000000000..09418236db --- /dev/null +++ b/.github/release-please.yml @@ -0,0 +1,18 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +handleGHRelease: true +packageName: cloud-sql-proxy +releaseType: simple +versionFile: 'cmd/version.txt' diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..9964bed603 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,27 @@ +{ + "extends": [ + "config:base", + ":semanticCommitTypeAll(chore)" + ], + "ignorePresets": [":semanticPrefixFixDepsChoreOthers"], + "prConcurrentLimit": 0, + "rebaseStalePrs": true, + "dependencyDashboard": true, + "semanticCommits": true, + "postUpdateOptions": [ + "gomodTidy" + ], + "ignoreDeps": [ + "golang.org/x/net" + ], + "timezone": "America/Los_Angeles", + "schedule": [ + "after 8am on Friday", + "before 12pm on Friday" + ], + "force": { + "constraints": { + "go": "1.16" + } + } +} diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 0000000000..f2528bce6b --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,50 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: code coverage +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: "1.17" + + - name: Checkout base branch + uses: actions/checkout@v3 + with: + ref: ${{ github.base_ref }} + - name: Calculate base code coverage + run: | + go test -short -coverprofile current_cover.out ./... || true + export CUR_COVER=$(go tool cover -func current_cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') + echo "CUR_COVER=$CUR_COVER" >> $GITHUB_ENV + + - name: Checkout PR branch + uses: actions/checkout@v3 + - name: Calculate PR code coverage + run: | + go test -short -coverprofile pr_cover.out ./... || true + export PR_COVER=$(go tool cover -func pr_cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') + echo "PR_COVER=$PR_COVER" >> $GITHUB_ENV + + - name: Verify code coverage. If your reading this and the step has failed, please add tests to cover your changes. + run: | + go tool cover -func pr_cover.out + if [ "${{ env.PR_COVER }}" -lt "${{ env.CUR_COVER }}" ]; then + exit 1; + fi diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000000..094de1a17f --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,34 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: tests +on: [pull_request] + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: '1.17' + - name: Install goimports + run: go get golang.org/x/tools/cmd/goimports + - name: Checkout code + uses: actions/checkout@v3 + - run: goimports -w . + - run: go mod tidy + - name: Verify no changes from goimports and go mod tidy. If you're reading this and the check has failed, run `goimports -w . && go mod tidy`. + run: git diff --exit-code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..9c416bacaf --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# direnv +.envrc + +# IDEs +.idea/ +.vscode/ + +# Compiled binary +/cmd/cloud_sql_proxy/cloud_sql_proxy +/cloud_sql_proxy +# v2 binary +/cloudsql-proxy + +/key.json diff --git a/.kokoro/go116/linux/common.cfg b/.kokoro/go116/linux/common.cfg new file mode 100644 index 0000000000..fb89301c09 --- /dev/null +++ b/.kokoro/go116/linux/common.cfg @@ -0,0 +1,35 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto + +# Get secrets for tests. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/go116:latest" +} + +# Tell the trampoline which tests to run. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" +} diff --git a/.kokoro/go116/linux/continuous.cfg b/.kokoro/go116/linux/continuous.cfg new file mode 100644 index 0000000000..da14d0b262 --- /dev/null +++ b/.kokoro/go116/linux/continuous.cfg @@ -0,0 +1,15 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/linux/periodic.cfg b/.kokoro/go116/linux/periodic.cfg new file mode 100644 index 0000000000..da14d0b262 --- /dev/null +++ b/.kokoro/go116/linux/periodic.cfg @@ -0,0 +1,15 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/linux/presubmit.cfg b/.kokoro/go116/linux/presubmit.cfg new file mode 100644 index 0000000000..da14d0b262 --- /dev/null +++ b/.kokoro/go116/linux/presubmit.cfg @@ -0,0 +1,15 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/windows/common.cfg b/.kokoro/go116/windows/common.cfg new file mode 100644 index 0000000000..1d832aea09 --- /dev/null +++ b/.kokoro/go116/windows/common.cfg @@ -0,0 +1,20 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto + +# Get secrets for tests. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" +build_file: "cloud-sql-proxy/.kokoro/tests/run_tests.bat" + diff --git a/.kokoro/go116/windows/continuous.cfg b/.kokoro/go116/windows/continuous.cfg new file mode 100644 index 0000000000..5306d19b51 --- /dev/null +++ b/.kokoro/go116/windows/continuous.cfg @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/windows/periodic.cfg b/.kokoro/go116/windows/periodic.cfg new file mode 100644 index 0000000000..5306d19b51 --- /dev/null +++ b/.kokoro/go116/windows/periodic.cfg @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/windows/presubmit.cfg b/.kokoro/go116/windows/presubmit.cfg new file mode 100644 index 0000000000..5306d19b51 --- /dev/null +++ b/.kokoro/go116/windows/presubmit.cfg @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go117/linux/common.cfg b/.kokoro/go117/linux/common.cfg new file mode 100644 index 0000000000..eaa711dc96 --- /dev/null +++ b/.kokoro/go117/linux/common.cfg @@ -0,0 +1,35 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto + +# Get secrets for tests. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/go117:latest" +} + +# Tell the trampoline which tests to run. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" +} diff --git a/.kokoro/go117/linux/continuous.cfg b/.kokoro/go117/linux/continuous.cfg new file mode 100644 index 0000000000..5306d19b51 --- /dev/null +++ b/.kokoro/go117/linux/continuous.cfg @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go117/linux/periodic.cfg b/.kokoro/go117/linux/periodic.cfg new file mode 100644 index 0000000000..5306d19b51 --- /dev/null +++ b/.kokoro/go117/linux/periodic.cfg @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go117/linux/presubmit.cfg b/.kokoro/go117/linux/presubmit.cfg new file mode 100644 index 0000000000..5306d19b51 --- /dev/null +++ b/.kokoro/go117/linux/presubmit.cfg @@ -0,0 +1,15 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/linux/common.cfg b/.kokoro/go118/linux/common.cfg new file mode 100644 index 0000000000..f2140104ac --- /dev/null +++ b/.kokoro/go118/linux/common.cfg @@ -0,0 +1,35 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto + +# Get secrets for tests. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" + +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/go118:latest" +} + +# Tell the trampoline which tests to run. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" +} diff --git a/.kokoro/go118/linux/continuous.cfg b/.kokoro/go118/linux/continuous.cfg new file mode 100644 index 0000000000..a1d3379d4b --- /dev/null +++ b/.kokoro/go118/linux/continuous.cfg @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/linux/periodic.cfg b/.kokoro/go118/linux/periodic.cfg new file mode 100644 index 0000000000..a1d3379d4b --- /dev/null +++ b/.kokoro/go118/linux/periodic.cfg @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/linux/presubmit.cfg b/.kokoro/go118/linux/presubmit.cfg new file mode 100644 index 0000000000..a1d3379d4b --- /dev/null +++ b/.kokoro/go118/linux/presubmit.cfg @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/macos/common.cfg b/.kokoro/go118/macos/common.cfg new file mode 100644 index 0000000000..2c95194994 --- /dev/null +++ b/.kokoro/go118/macos/common.cfg @@ -0,0 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto + +# Get secrets for tests. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" + +# Use the trampoline script to run in docker. +build_file: "cloud-sql-proxy/.kokoro/tests/run_tests_macos.sh" diff --git a/.kokoro/go118/macos/continuous.cfg b/.kokoro/go118/macos/continuous.cfg new file mode 100644 index 0000000000..a1d3379d4b --- /dev/null +++ b/.kokoro/go118/macos/continuous.cfg @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/macos/periodic.cfg b/.kokoro/go118/macos/periodic.cfg new file mode 100644 index 0000000000..a1d3379d4b --- /dev/null +++ b/.kokoro/go118/macos/periodic.cfg @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/macos/presubmit.cfg b/.kokoro/go118/macos/presubmit.cfg new file mode 100644 index 0000000000..a1d3379d4b --- /dev/null +++ b/.kokoro/go118/macos/presubmit.cfg @@ -0,0 +1,15 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/release_artifacts.sh b/.kokoro/release_artifacts.sh new file mode 100755 index 0000000000..8ea59a8418 --- /dev/null +++ b/.kokoro/release_artifacts.sh @@ -0,0 +1,56 @@ +#! /bin/bash +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script distributes the artifacts for the Cloud SQL proxy to their different channels. + +set -e # exit immediatly if any step fails + +PROJ_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. >/dev/null 2>&1 && pwd )" +cd $PROJ_ROOT + +# get the current version +export VERSION=$(cat version.txt) +if [ -z "$VERSION" ]; then + echo "error: No version.txt found in $PROJ_ROOT" + exit 1 +fi + + +read -p "This will release new Cloud SQL proxy artifacts for \"$VERSION\", even if they already exist. Are you sure (y/Y)? " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + exit 1 +fi + +# Build and push the container images +gcloud builds submit --async --config .build/default.yaml --substitutions _VERSION=$VERSION +gcloud builds submit --async --config .build/buster.yaml --substitutions _VERSION=$VERSION +gcloud builds submit --async --config .build/alpine.yaml --substitutions _VERSION=$VERSION + +# Build the binarys and upload to GCS +gcloud builds submit --config .build/gcs_upload.yaml --substitutions _VERSION=$VERSION +# cleam up any artifacts.json left by previous builds +gsutil rm -f gs://cloudsql-proxy/v$VERSION/*.json 2> /dev/null || true + +# Generate sha256 hashes for authentication +echo -e "Add the following table to the release notes on GitHub: \n\n" +echo "| filename | sha256 hash |" +echo "|----------|-------------|" +for f in $(gsutil ls "gs://cloudsql-proxy/v$VERSION/cloud_sql_proxy*"); do + file=$(basename $f) + sha=$(gsutil cat $f | sha256sum --binary | head -c 64) + echo "| [$file](https://storage.googleapis.com/cloudsql-proxy/v$VERSION/$file) | $sha |" +done diff --git a/.kokoro/tag_latest.sh b/.kokoro/tag_latest.sh new file mode 100755 index 0000000000..0edf7c4a25 --- /dev/null +++ b/.kokoro/tag_latest.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# This script finds all container images with the provided version tag and adds +# the "latest" tag to them. +# +# For example: +# 1. Add a "latest" tag to the v1.23.1 images +# +# ./tag_latest 1.23.1 +# +# 2. Print out the gcloud commands without running them: +# +# ./tag_latest 1.23.1 -dry-run +# + +if [ "$1" = "" ] +then + echo "Usage: $0 [-dry-run]" + exit 1 +fi + +dry_run=false +if [ "$2" = "-dry-run" ] +then + dry_run=true +fi + +tag_latest() { + local new_version=$1 + for registry in "gcr.io" "us.gcr.io" "eu.gcr.io" "asia.gcr.io" + do + local base_image="$registry/cloudsql-docker/gce-proxy" + if [ "$dry_run" != true ] + then + gcloud container images add-tag "$base_image:$new_version" "$base_image:latest" + else + echo [DRY RUN] gcloud container images add-tag "$base_image:$new_version" "$base_image:latest" + fi + done +} + +tag_latest "$1" diff --git a/.kokoro/tests/run_tests.bat b/.kokoro/tests/run_tests.bat new file mode 100644 index 0000000000..b0252d8b23 --- /dev/null +++ b/.kokoro/tests/run_tests.bat @@ -0,0 +1 @@ +"C:\Program Files\Git\bin\bash.exe" github/cloud-sql-proxy/.kokoro/tests/run_tests_windows.sh diff --git a/.kokoro/tests/run_tests.sh b/.kokoro/tests/run_tests.sh new file mode 100755 index 0000000000..5525bc7c93 --- /dev/null +++ b/.kokoro/tests/run_tests.sh @@ -0,0 +1,34 @@ +#! /bin/bash +# Copyright 2020 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# `-e` enables the script to automatically fail when a command fails +set -e + +export GO111MODULE=on + +# Kokoro setup +if [ -n "$KOKORO_GFILE_DIR" ]; then + # Move into project directory + cd github/cloud-sql-proxy + # install fuse project + apt-get -qq update && apt-get -qq install fuse -y + # source secrets + source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" + export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" +fi + +echo -e "******************** Running tests... ********************\n" +go test -race -v ./... +echo -e "******************** Tests complete. ********************\n" diff --git a/.kokoro/tests/run_tests_macos.sh b/.kokoro/tests/run_tests_macos.sh new file mode 100644 index 0000000000..d669c5f8eb --- /dev/null +++ b/.kokoro/tests/run_tests_macos.sh @@ -0,0 +1,41 @@ +#! /bin/bash +# Copyright 2020 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# `-e` enables the script to automatically fail when a command fails +set -e + +export GO111MODULE=on + +# kokoro setup +if [ -n "$KOKORO_GFILE_DIR" ]; then + # move into project directory + cd github/cloud-sql-proxy + # install fuse project + brew update > /dev/null + brew install --cask --quiet osxfuse + # install go version + brew install go@1.18 + echo -e "******************** Printing Go version... ********************\n" + echo `go version` + # source secrets + source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" + export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" +fi + +# On macOS, the default $TMPDIR is too long for suitable use due to the unix socket length limits +export TMPDIR="/tmp" +echo -e "******************** Running tests... ********************\n" +go test -race -v ./... +echo -e "******************** Tests complete. ********************\n" diff --git a/.kokoro/tests/run_tests_windows.sh b/.kokoro/tests/run_tests_windows.sh new file mode 100755 index 0000000000..42ada2fc3a --- /dev/null +++ b/.kokoro/tests/run_tests_windows.sh @@ -0,0 +1,34 @@ +#! /bin/bash +# Copyright 2021 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# `-e` enables the script to automatically fail when a command fails +set -e + +export GO111MODULE=on +export PATH=/c/Go/bin:$PATH +export GOPATH=/c/Go + +# Kokoro setup +if [ -n "$KOKORO_GFILE_DIR" ]; then + # Move into project directory + cd github/cloud-sql-proxy + # source secrets + source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" + export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" +fi + +echo -e "******************** Running tests... ********************\n" +go test -v ./... +echo -e "******************** Tests complete. ********************\n" diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh new file mode 100644 index 0000000000..f0e0070c07 --- /dev/null +++ b/.kokoro/trampoline.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Copyright 2019 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..0068795d40 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,215 @@ +# Changelog + +## [1.30.0](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.29.0...v1.30.0) (2022-04-04) + + +### Features + +* drop support and testing for Go 1.13, 1.14, 1.15 ([#1148](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1148)) ([158b0d5](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/158b0d57d46054be6a0d1600d5030b23be69dc9b)) + +## [1.29.0](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.28.1...v1.29.0) (2022-03-01) + + +### Features + +* add Go version support policy ([#1109](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1109)) ([ae6f4a1](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ae6f4a1a534df8a273c0ea96880154b90bc65e77)) + +### [1.28.1](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.28.0...v1.28.1) (2022-01-31) + + +### Bug Fixes + +* invalidated config should retain error ([#1068](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1068)) ([49d3003](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/49d3003c018afdc0cde54340d5be808f9dcd5c84)) +* remove unnecessary token parsing ([#1074](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1074)) ([e138611](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/e1386118ad239e6c1ff16df6f2be1351a6432bb3)) +* return error from instance version ([#1069](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1069)) ([d9fc819](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d9fc819a197bd75d0060bd46b8e06da6bdd6630c)) + +## [1.28.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.27.1...v1.28.0) (2022-01-04) + + +### Features + +* add support for ReadTime in Admin API requests ([#1040](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1040)) ([a7c8b5c](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/a7c8b5cf4d10c17bea405ce67ee642232b43fdec)) +* add support for specifying a quota project ([#1044](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1044)) ([dc66aca](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/dc66aca88190ae3f6d39f191489fdfb280146ed9)) +* allow multiple -instances flags ([#1046](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1046)) ([1972693](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/1972693b8ac65c912bb719dc23d4f578cb6ff9e2)), closes [#1030](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1030) + + +### Bug Fixes + +* increase rateLimit burst size to 2 ([#1048](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1048)) ([df6b6f9](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/df6b6f9ed8860d28f5e934db495257d288c42f2b)) + +### [1.27.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.27.0...v1.27.1) (2021-12-07) + + +### Bug Fixes + +* update dependencies to latest versions ([#1034](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1034)) ([8954d24](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/8954d241a71b59d9bf82cb47469e6652d3f379e7)) + +## [1.27.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.26.0...v1.27.0) (2021-11-02) + + +### Features + +* switch to supported FUSE library ([#953](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/953)) ([10f2133](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/10f2133010f3bf7ef8a13b43e0bfa16bdca8cedb)) +* verify FUSE is installed on macOS / linux ([#959](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/959)) ([9ab868e](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/9ab868ef344b9a82c06f97928420f98a4d37c5ce)) + + +### Bug Fixes + +* fail fast on invalid config ([#999](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/999)) ([18a0960](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/18a096037d9ceb2ca71218984b65fe342fc2a778)) +* respect context deadline for TLS handshakes ([#987](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/987)) ([12ff12c](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/12ff12c9f87459dc40e2e6e4a2d08bebb0786ee7)), closes [#986](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/986) +* validate instance connections in liveness probe ([#995](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/995)) ([e5cc8d4](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/e5cc8d4f8676fed2013cc491578a1aaf7416ec3e)) + +## [1.26.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.25.0...v1.26.0) (2021-10-05) + + +### Features + +* improve reliability of refresh operations ([#883](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/883)) ([480992a](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/480992a7671abe9b76f940175f4ed17f5271d3f8)) + +## [1.25.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.24.0...v1.25.0) (2021-09-07) + + +### Features + +* add health checks to proxy ([#859](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/859)) ([ea62bdd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ea62bddaaf3aa7df79250d045ba2f5f3fe7edaea)) +* add instance dialing to health check ([#871](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/871)) ([eca3793](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/eca37935e7cd54efcd612c170e46f45c1d8e3556)) +* require TLS v1.3 at minimum ([#906](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/906)) ([cafa966](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/cafa966e50170ad94f12f067549ba3aedf8ecdca)) + + +### Bug Fixes + +* ensure proxy shuts down gracefully on SIGTERM ([#877](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/877)) ([9793555](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/97935551ac44cb7a92e2901def1938d604dfeecb)) +* validate instances in fuse mode ([#875](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/875)) ([96f8b65](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/96f8b655b09b711fd9adfcb486626b64d3b917f3)) + +## [1.24.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.23.1...v1.24.0) (2021-08-02) + + +### Features + +* Add option to delay key generation until first connect ([#841](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/841)) ([4999ffd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4999ffd0c3406e91874648630f9805b2d5f0ac50)) +* stop building darwin 386 binaries ([#846](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/846)) ([77d7c40](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/77d7c40ff79cf99a10d2dbae39b737625a08582f)), closes [#780](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/780) + + +### Bug Fixes + +* invalidate cached config on handshake error ([#817](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/817)) ([5d98f5c](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/5d98f5c40e0b58da479bf6897712d53e6846f613)) +* strip padding from access tokens if present ([#851](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/851)) ([1f195e5](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/1f195e500c1a8989dcf4d73c429620ddd5b20891)) +* structured_logs compatibility with Google Cloud Logging ([#861](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/861)) ([74a6ec7](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/74a6ec70b63f4f0488470164fa4da68a26779fb2)) + +### [1.23.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.23.0...v1.23.1) (2021-07-12) + + +### Bug Fixes + +* improve log message when refresh is throttled ([#830](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/830)) ([4ffee2a](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4ffee2a1950fd6fb6703647d178a436b566b8a80)) + +## [1.23.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.22.0...v1.23.0) (2021-06-01) + + +### Features + +* add deprecation warning for Darwin 386 ([#781](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/781)) ([cdc552b](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/cdc552b8da7abb3378d43c060acb019de7e12fcc)) + + +### Bug Fixes + +* change to static base container ([#791](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/791)) ([d66233e](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d66233e2a0aecb6e80a4f802b0dc6a5cd2fa9041)) + +## [1.22.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.21.0...v1.22.0) (2021-04-21) + + +### Features + +* Add support for systemd notify ([#719](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/719)) ([4305eff](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4305eff05f1d33da4251a7b512b723cb086e4ce5)) + + +### Bug Fixes + +* Allow combined use of structured logs and -log_debug_stdout ([#726](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/726)) ([45bda77](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/45bda776fc964a3464a1703035b4f2a719779bc6)) +* return early when cert refresh fails ([#748](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/748)) ([fd21f66](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/fd21f66f2d8dc3b8e787ab0b467db4d4b85921cb)) +* structured logging respects the -verbose flag ([#737](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/737)) ([f35422f](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/f35422f449a0c79f6b2225de21c26c2da04d3528)) + +## [1.21.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.20.2...v1.21.0) (2021-04-05) + + +### Features + +* add support for structured logs ([#650](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/650)) ([ca8993a](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ca8993a2110affa0b0cbbfdebf6f6bdd86004e9f)) + + +### Bug Fixes + +* improve cache to prevent multiple concurrent refreshes ([#674](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/674)) ([c5ffa69](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c5ffa69952eba713e7acc688841f9b448a180625)) +* lower refresh buffer and config throttle when IAM authn is enabled ([#680](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/680)) ([58acab3](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/58acab3b03375032501f17c85949db493af7a292)) +* prevent refreshCfg from scheduling multiple refreshes ([#666](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/666)) ([52db349](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/52db3492ac78a9a68218c2a12840c4016b1d0b99)) + +### [1.20.2](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.20.1...v1.20.2) (2021-03-05) + + +### Bug Fixes + +* ensure certificate expiration is correct ([#659](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/659)) ([2fd2504](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/2fd2504381405b0d5fe7cc81d3c55a15f949df99)) +* perform initial gcloud check and reuse token ([#657](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/657)) ([f3bf3f9](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/f3bf3f931621285875363fab5fe3563bc82a3d94)) + +### [1.20.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.20.0...v1.20.1) (2021-03-04) + + +### Bug Fixes + +* prevent untrusted gcloud exe's from running ([#649](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/649)) ([0f0ff49](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/0f0ff49a0fac990ba1ec05a6cbd4e666e3141c08)) +* use new oauth2 token with cert refresh ([#648](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/648)) ([6d5e455](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6d5e4558a63957714f6347c9768e671586c0a605)) +* verify TokenSource exists in TokenExpiration() ([#642](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/642)) ([d01d7eb](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d01d7eb78652cf83f713b5d47bb696378929e8a6)) + +## [1.20.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.19.2...v1.20.0) (2021-02-24) + + +### Features + +* add ARM releases ([#631](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/631)) ([d3fb7f6](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d3fb7f6394f2c641f0ba7339ab29a1c02d82e396)) +* Added '-enable_iam_login' flag for IAM db authentication ([#583](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/583)) ([470f92d](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/470f92d29d7a32f7903a3cb6d49fb09363185866)) + + +### [1.19.2](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.19.1...v1.19.2) (2021-02-16) + + +### Bug Fixes + +* improve logging for file descriptor limits ([#609](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/609)) ([b42b681](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/b42b68134543fbee7da4fbb9a8d667fd9153bec2)), closes [#413](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/413) + +### [1.19.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.19.0...v1.19.1) (2020-12-02) + + +### Bug Fixes + +* Ensure necessary fields are 64-bit aligned ([#550](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/550)) ([4575c8f](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4575c8f8cb496ac3069208e446c47fb6c6acb868)) + +## [1.19.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.18.0...v1.19.0) (2020-11-18) + + +### Features + +* Added DialContext to Client and proxy package ([#483](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/483)) ([c84aa50](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c84aa5079668e07e3d2dc8f254d30e1103a6ead3)) +* use regionalized instance ids to prevent global conflicts with sqladmin v1 ([#504](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/504)) ([6c45513](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6c455136a24b841dbfc015a1f8ed7505f9e77dec)) + + +### Bug Fixes + +* **containers:** Allow non-root users to mount fuse filesystems for alpine and buster images ([#540](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/540)) ([5b653f5](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/5b653f5df6d9c4c226e3c4f6036d5e7d4c43c699)) +* only allow fuse mode to unmount if an error occurs first ([#537](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/537)) ([6caef36](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6caef36968d23b931c824450e418e29ac6277191)) +* refreshCfg no longer caches error over valid cert ([#521](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/521)) ([4a6b3d8](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4a6b3d8c895e2634afd8cee2341db668f20b9a33)) + +## [1.18.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.17.0...v1.18.0) (2020-09-08) + + +### Features + +* **containers:** Add "-alpine" and "-buster" based images. ([#415](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/415)) ([ebcf294](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ebcf294b9ee028340695868fb6f4cc4bbe09d849)) +* **containers:** Add fuse to alpine and buster images ([#459](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/459)) ([0f28fcd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/0f28fcd008a5bb863ec2ca1402c31ae81d7dae5d)) + + +### Bug Fixes +* Print out any errors during SIGTERM-caused shutdown ([#389](https://github.com/GoogleCloudPlatform/cloudsql-proxy/pull/389)) +* Optimize `-term-timeout` wait ([#391](https://github.com/GoogleCloudPlatform/cloudsql-proxy/pull/391)) +* Add socket suffix for Postgres instances when running in `-fuse` mode ([#426](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/426)) ([20ffaec](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/20ffaec2f0f00a2516206a0453bd0d1c6e62770c)) +* **containers:** Specify nonroot user by uid to work with runAsNonRoot ([#402](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/402)) ([c5c0be1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c5c0be1b60bfc1c3fa862039619908a328066e5e)) +* Releases are now tagged using `vMAJOR.MINOR.PATCH` for correct compatibility with go-modules. Please note that this will effect container image tags (which were previously only `vMAJOR.MINOR`), since these tags correspond directly to the release on GitHub. diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000000..63a15e2c31 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,40 @@ +# This is the official list of people who can contribute +# (and typically have contributed) code to the repository. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# The submission process automatically checks to make sure +# that people submitting code are listed in this file (by email address). +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# https://cla.developers.google.com/about/google-individual +# https://cla.developers.google.com/about/google-corporate +# +# The CLA can be filled out on the web: +# +# https://cla.developers.google.com/ +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. + +# Names should be added to this file like so: +# Name +# +# An entry with two email addresses specifies that the +# first address should be used in the submit logs and +# that the second address should be recognized as the +# same person when interacting with Rietveld. + +# Please keep the list sorted. + +Ben Brown +Frank van Rest +Kevin Malachowski +Mykola Smith + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..4e1652f0e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Use the latest stable golang 1.x to compile to a binary +FROM golang:1 as build + +WORKDIR /go/src/cloudsql-proxy +COPY . . + +RUN go get ./... +RUN CGO_ENABLED=0 go build -ldflags "-X main.metadataString=container" -o cloud_sql_proxy ./cmd/cloud_sql_proxy + +# Final Stage +FROM gcr.io/distroless/static:nonroot +COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy +# set the uid as an integer for compatibility with runAsNonRoot in Kubernetes +USER 65532 diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 0000000000..bb742e24b7 --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,36 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Use the latest stable golang 1.x to compile to a binary +FROM golang:1 as build + +WORKDIR /go/src/cloudsql-proxy +COPY . . + +RUN go get ./... +RUN go build -ldflags "-X main.metadataString=container.alpine" -o cloud_sql_proxy ./cmd/cloud_sql_proxy + +# Final stage +FROM alpine:3 +RUN apk add --no-cache \ + ca-certificates \ + libc6-compat +# Install fuse and allow enable non-root users to mount +RUN apk add --no-cache fuse && sed -i 's/^#user_allow_other$/user_allow_other/g' /etc/fuse.conf +# Add a non-root user matching the nonroot user from the main container +RUN addgroup -g 65532 -S nonroot && adduser -u 65532 -S nonroot -G nonroot +# Set the uid as an integer for compatibility with runAsNonRoot in Kubernetes +USER 65532 + +COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy diff --git a/Dockerfile.buster b/Dockerfile.buster new file mode 100644 index 0000000000..d24a0dab12 --- /dev/null +++ b/Dockerfile.buster @@ -0,0 +1,34 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Use the latest stable golang 1.x to compile to a binary +FROM golang:1 as build + +WORKDIR /go/src/cloudsql-proxy +COPY . . + +RUN go get ./... +RUN go build -ldflags "-X main.metadataString=container.buster" -o cloud_sql_proxy ./cmd/cloud_sql_proxy + +# Final stage +FROM debian:buster +RUN apt-get update && apt-get install -y ca-certificates +# Install fuse and allow enable non-root users to mount +RUN apt-get update && apt-get install -y fuse && sed -i 's/^#user_allow_other$/user_allow_other/g' /etc/fuse.conf +# Add a non-root user matching the nonroot user from the main container +RUN groupadd -g 65532 -r nonroot && useradd -u 65532 -g 65532 -r nonroot +# Set the uid as an integer for compatibility with runAsNonRoot in Kubernetes +USER 65532 + +COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy diff --git a/cloudsql/cloudsql.go b/cloudsql/cloudsql.go new file mode 100644 index 0000000000..4e4f5371a4 --- /dev/null +++ b/cloudsql/cloudsql.go @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudsql + +import ( + "context" + "io" + "net" + + "cloud.google.com/go/cloudsqlconn" +) + +// Dialer dials a Cloud SQL instance and returns its database engine version. +type Dialer interface { + // Dial returns a connection to the specified instance. + Dial(ctx context.Context, inst string, opts ...cloudsqlconn.DialOption) (net.Conn, error) + // EngineVersion retrieves the provided instance's database version (e.g., + // POSTGRES_14) + EngineVersion(ctx context.Context, inst string) (string, error) + + io.Closer +} diff --git a/cmd/cloud_sql_proxy/cloud_sql_proxy.go b/cmd/cloud_sql_proxy/cloud_sql_proxy.go new file mode 100644 index 0000000000..61ce326ea1 --- /dev/null +++ b/cmd/cloud_sql_proxy/cloud_sql_proxy.go @@ -0,0 +1,734 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// cloudsql-proxy can be used as a proxy to Cloud SQL databases. It supports +// connecting to many instances and authenticating via different means. +// Specifically, a list of instances may be provided on the command line, in +// GCE metadata (for VMs), or provided during connection time via a +// FUSE-mounted directory. See flags for a more specific explanation. +package main + +import ( + _ "embed" + "errors" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/signal" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd/cloud_sql_proxy/internal/healthcheck" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/certs" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/fuse" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/limits" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/util" + + "cloud.google.com/go/compute/metadata" + "github.com/coreos/go-systemd/v22/daemon" + "golang.org/x/net/context" + "golang.org/x/oauth2" + goauth "golang.org/x/oauth2/google" + sqladmin "google.golang.org/api/sqladmin/v1beta4" +) + +var ( + version = flag.Bool("version", false, "Print the version of the proxy and exit") + verbose = flag.Bool("verbose", true, + `If false, verbose output such as information about when connections are +created/closed without error are suppressed`, + ) + quiet = flag.Bool("quiet", false, "Disable log messages") + logDebugStdout = flag.Bool("log_debug_stdout", false, "If true, log messages that are not errors will output to stdout instead of stderr") + structuredLogs = flag.Bool("structured_logs", false, "Configures all log messages to be emitted as JSON.") + + refreshCfgThrottle = flag.Duration("refresh_config_throttle", proxy.DefaultRefreshCfgThrottle, + `If set, this flag specifies the amount of forced sleep between successive +API calls in order to protect client API quota. Minimum allowed value is + `+minimumRefreshCfgThrottle.String(), + ) + checkRegion = flag.Bool("check_region", false, `If specified, the 'region' portion of the connection string is required for +Unix socket-based connections.`) + + // Settings for how to choose which instance to connect to. + dir = flag.String("dir", "", "Directory to use for placing Unix sockets representing database instances") + projects = flag.String("projects", "", + `Open sockets for each Cloud SQL Instance in the projects specified +(comma-separated list)`, + ) + instances stringListValue // -instances flag is defined in runProxy() + instanceSrc = flag.String("instances_metadata", "", `If provided, it is treated as a path to a metadata value which +is polled for a comma-separated list of instances to connect to. For example, +to use the instance metadata value named 'cloud-sql-instances' you would +provide 'instance/attributes/cloud-sql-instances'. Not compatible with -fuse`) + useFuse = flag.Bool("fuse", false, `Mount a directory at 'dir' using FUSE for accessing instances. Note that the +directory at 'dir' must be empty before this program is started.`) + fuseTmp = flag.String("fuse_tmp", defaultTmp, `Used as a temporary directory if -fuse is set. Note that files in this directory +can be removed automatically by this program.`) + + // Settings for limits + maxConnections = flag.Uint64("max_connections", 0, + `If provided, the maximum number of connections to establish before refusing +new connections. Defaults to 0 (no limit)`, + ) + fdRlimit = flag.Uint64("fd_rlimit", limits.ExpectedFDs, + `Sets the rlimit on the number of open file descriptors for the proxy to +the provided value. If set to zero, disables attempts to set the rlimit. +Defaults to a value which can support 4K connections to one instance`, + ) + termTimeout = flag.Duration("term_timeout", 0, + `When set, the proxy will wait for existing connections to close before +terminating. Any connections that haven't closed after the timeout will be +dropped`, + ) + + // Settings for authentication. + token = flag.String("token", "", "When set, the proxy uses this Bearer token for authorization.") + tokenFile = flag.String("credential_file", "", + `If provided, this json file will be used to retrieve Service Account +credentials. You may set the GOOGLE_APPLICATION_CREDENTIALS environment +variable for the same effect.`, + ) + ipAddressTypes = flag.String("ip_address_types", "PUBLIC,PRIVATE", + `Default to be 'PUBLIC,PRIVATE'. Options: a list of strings separated by +',', e.g. 'PUBLIC,PRIVATE' `, + ) + // Settings for IAM db proxy authentication + enableIAMLogin = flag.Bool("enable_iam_login", false, "Enables database user authentication using Cloud SQL's IAM DB Authentication (Postgres only).") + + skipInvalidInstanceConfigs = flag.Bool("skip_failed_instance_config", false, + `Setting this flag will allow you to prevent the proxy from terminating +when some instance configurations could not be parsed and/or are +unavailable.`, + ) + + // Setting to choose what API to connect to + host = flag.String("host", "", + `When set, the proxy uses this host as the base API path. Example: +https://sqladmin.googleapis.com`, + ) + quotaProject = flag.String("quota_project", "", + `Specifies the project to use for Cloud SQL Admin API quota tracking.`) + + // Settings for healthcheck + useHTTPHealthCheck = flag.Bool("use_http_health_check", false, "When set, creates an HTTP server that checks and communicates the health of the proxy client.") + healthCheckPort = flag.String("health_check_port", "8090", "When applicable, health checks take place on this port number. Defaults to 8090.") +) + +const ( + minimumRefreshCfgThrottle = time.Second + + port = 3307 +) + +func init() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, ` +The Cloud SQL Auth proxy allows simple, secure connectivity to Google Cloud SQL. It +is a long-running process that opens local sockets (either TCP or Unix sockets) +according to the parameters passed to it. A local application connects to a +Cloud SQL instance by using the corresponding socket. + + +Authorization: + * On Google Compute Engine, the default service account is used. + The Cloud SQL API must be enabled for the VM. + + * When the gcloud command-line tool is installed on the local machine, the + "active account" is used for authentication. Run 'gcloud auth list' to see + which accounts are installed on your local machine and + 'gcloud config list account' to view the active account. + + * To configure the proxy using a service account, pass the -credential_file + parameter or set the GOOGLE_APPLICATION_CREDENTIALS environment variable. + This will override gcloud or GCE (Google Compute Engine) credentials, + if they exist. + + * To configure the proxy using IAM authentication, pass the -enable_iam_login + flag. This will cause the proxy to use IAM account credentials for + database user authentication. + +General: + -quiet + Disable log messages (e.g. when new connections are established). + WARNING: this option disables ALL logging output (including connection + errors), which will likely make debugging difficult. The -quiet flag takes + precedence over the -verbose flag. + + -log_debug_stdout + When explicitly set to true, verbose and info log messages will be directed + to stdout as opposed to the default stderr. + + -verbose + When explicitly set to false, disable log messages that are not errors nor + first-time startup messages (e.g. when new connections are established). + + -structured_logs + When set to true, all log messages are written out as JSON. + + -term_timeout + How long to wait for connections to close after receiving a SIGTERM before + shutting down the proxy. Defaults to 0. If all connections close before the + duration, the proxy will shutdown early. + +Connection: + -instances + To connect to a specific list of instances, set the instances parameter + to a comma-separated list of instance connection strings. For example: + + -instances=my-project:my-region:my-instance + + For convenience, this flag may be specified multiple times. + + For connectivity over TCP, you must specify a tcp port as part of the + instance string. For example, the following example opens a loopback TCP + socket on port 3306, which will be proxied to connect to the instance + 'my-instance' in project 'my-project'. To listen on other interfaces than + localhost, a custom bind address (e.g., 0.0.0.0) may be provided. For + example: + + -instances=my-project:my-region:my-instance=tcp:3306 + or + -instances=my-project:my-region:my-instance=tcp:0.0.0.0:3306 + + When connecting over TCP, the -instances parameter is required. + + To set a custom socket name, you can specify it as part of the instance + string. The following example opens a unix socket in the directory + specified by -dir, which will be proxied to connect to the instance + 'my-instance' in project 'my-project': + + -instances=my-project:my-region:my-instance=unix:custom-socket-name + + Note: The directory specified by -dir must exist and the socket file path + (i.e., dir plus INSTANCE_CONNECTION_NAME) must be under your platform's + limit (typically 108 characters on many Unix systems, but varies by platform). + + To override the -dir parameter, specify an absolute path as shown in the + following example: + + -instances=my-project:my-region:my-instance=unix:/my/custom/sql-socket + + Supplying INSTANCES environment variable achieves the same effect. One can + use that to keep k8s manifest files constant across multiple environments + + -instances_metadata + When running on GCE (Google Compute Engine) you can avoid the need to + specify the list of instances on the command line by using the Metadata + server. This parameter specifies a path to a metadata value which is then + interpreted as a list of instances in the exact same way as the -instances + parameter. Updates to the metadata value will be observed and acted on by + the Proxy. + + -projects + To direct the proxy to allow connections to all instances in specific + projects, set the projects parameter: + + -projects=my-project + + -fuse + If your local environment has FUSE installed, you can specify the -fuse + flag to avoid the requirement to specify instances in advance. With FUSE, + any attempts to open a Unix socket in the directory specified by -dir + automatically creates that socket and connects to the corresponding + instance. + + -dir + When using Unix sockets (the default for systems which support them), the + Proxy places the sockets in the directory specified by the -dir parameter. + +Automatic instance discovery: + If the Google Cloud SQL is installed on the local machine and no instance + connection flags are specified, the proxy connects to all instances in the + gcloud tool's active project. Run 'gcloud config list project' to + display the active project. + + +Information for all flags: +`) + flag.VisitAll(func(f *flag.Flag) { + usage := strings.Replace(f.Usage, "\n", "\n ", -1) + fmt.Fprintf(os.Stderr, " -%s\n %s\n\n", f.Name, usage) + }) + } +} + +var defaultTmp = filepath.Join(os.TempDir(), "cloudsql-proxy-tmp") + +// versionString indiciates the version of the proxy currently in use. +//go:embed version.txt +var versionString string + +// metadataString indiciates additional build or distribution metadata. +var metadataString = "" + +// semanticVersion returns the version of the proxy in a semver format. +func semanticVersion() string { + v := strings.TrimSpace(versionString) + if metadataString != "" { + v += "+" + metadataString + } + return v +} + +// userAgentFromVersionString returns an appropriate user agent string for identifying this proxy process. +func userAgentFromVersionString() string { + return "cloud_sql_proxy/" + semanticVersion() +} + +const accountErrorSuffix = `Please create a new VM with Cloud SQL access (scope) enabled under "Identity and API access". Alternatively, create a new "service account key" and specify it using the -credential_file parameter` + +type stringListValue []string + +func (i *stringListValue) String() string { + return strings.Join(*i, ",") +} + +func (i *stringListValue) Set(s string) error { + *i = append(*i, stringList(s)...) + return nil +} + +func checkFlags(onGCE bool) error { + if !onGCE { + if *instanceSrc != "" { + return errors.New("-instances_metadata unsupported outside of Google Compute Engine") + } + return nil + } + + if *token != "" || *tokenFile != "" || os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { + return nil + } + + // Check if gcloud credentials are available and if so, skip checking the GCE VM service account scope. + _, err := util.GcloudConfig() + if err == nil { + return nil + } + + scopes, err := metadata.Scopes("default") + if err != nil { + if _, ok := err.(metadata.NotDefinedError); ok { + return errors.New("no service account found for this Compute Engine VM. " + accountErrorSuffix) + } + return fmt.Errorf("error checking scopes: %T %v | %+v", err, err, err) + } + + ok := false + for _, sc := range scopes { + if sc == proxy.SQLScope || sc == "https://www.googleapis.com/auth/cloud-platform" { + ok = true + break + } + } + if !ok { + return errors.New(`the default Compute Engine service account is not configured with sufficient permissions to access the Cloud SQL API from this VM. ` + accountErrorSuffix) + } + return nil +} + +func authenticatedClientFromPath(ctx context.Context, f string) (*http.Client, oauth2.TokenSource, error) { + all, err := ioutil.ReadFile(f) + if err != nil { + return nil, nil, fmt.Errorf("invalid json file %q: %v", f, err) + } + // First try and load this as a service account config, which allows us to see the service account email: + if cfg, err := goauth.JWTConfigFromJSON(all, proxy.SQLScope); err == nil { + logging.Infof("using credential file for authentication; email=%s", cfg.Email) + return cfg.Client(ctx), cfg.TokenSource(ctx), nil + } + + cred, err := goauth.CredentialsFromJSON(ctx, all, proxy.SQLScope) + if err != nil { + return nil, nil, fmt.Errorf("invalid json file %q: %v", f, err) + } + logging.Infof("using credential file for authentication; path=%q", f) + return oauth2.NewClient(ctx, cred.TokenSource), cred.TokenSource, nil +} + +func authenticatedClient(ctx context.Context) (*http.Client, oauth2.TokenSource, error) { + if *tokenFile != "" { + return authenticatedClientFromPath(ctx, *tokenFile) + } else if tok := *token; tok != "" { + src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: tok}) + return oauth2.NewClient(ctx, src), src, nil + } else if f := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); f != "" { + return authenticatedClientFromPath(ctx, f) + } + + // If flags or env don't specify an auth source, try either gcloud or application default + // credentials. + src, err := util.GcloudTokenSource(ctx) + if err != nil { + src, err = goauth.DefaultTokenSource(ctx, proxy.SQLScope) + } + if err != nil { + return nil, nil, err + } + + return oauth2.NewClient(ctx, src), src, nil +} + +// quotaProjectTransport is an http.RoundTripper that adds an X-Goog-User-Project +// header to all requests for quota and billing purposes. +// +// For details, see: +// https://cloud.google.com/apis/docs/system-parameters#definitions +type quotaProjectTransport struct { + base http.RoundTripper + project string +} + +var _ http.RoundTripper = quotaProjectTransport{} + +// RoundTrip adds a X-Goog-User-Project header to each request. +func (t quotaProjectTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.Header == nil { + req.Header = make(http.Header) + } + req.Header.Add("X-Goog-User-Project", t.project) + return t.base.RoundTrip(req) +} + +// configureQuotaProject configures an HTTP client to use the provided project +// for quota calculations for all requests. +func configureQuotaProject(c *http.Client, project string) { + // Copy the given client's tripper. Note that tripper can be nil, which is equivalent to + // http.DefaultTransport. (See https://golang.org/pkg/net/http/#Client) + base := c.Transport + if base == nil { + base = http.DefaultTransport + } + c.Transport = quotaProjectTransport{ + base: base, + project: project, + } +} + +func stringList(s string) []string { + spl := strings.Split(s, ",") + if len(spl) == 1 && spl[0] == "" { + return nil + } + return spl +} + +func listInstances(ctx context.Context, cl *http.Client, projects []string) ([]string, error) { + if len(projects) == 0 { + // No projects requested. + return nil, nil + } + + sql, err := sqladmin.New(cl) + if err != nil { + return nil, err + } + if *host != "" { + sql.BasePath = *host + } + + ch := make(chan string) + var wg sync.WaitGroup + wg.Add(len(projects)) + for _, proj := range projects { + proj := proj + go func() { + err := sql.Instances.List(proj).Pages(ctx, func(r *sqladmin.InstancesListResponse) error { + for _, in := range r.Items { + // The Proxy is only support on Second Gen + if in.BackendType == "SECOND_GEN" { + ch <- in.ConnectionName + } + } + return nil + }) + if err != nil { + logging.Errorf("Error listing instances in %v: %v", proj, err) + } + wg.Done() + }() + } + go func() { + wg.Wait() + close(ch) + }() + var ret []string + for x := range ch { + ret = append(ret, x) + } + if len(ret) == 0 { + return nil, fmt.Errorf("no Cloud SQL Instances found in these projects: %v", projects) + } + return ret, nil +} + +func gcloudProject() ([]string, error) { + cfg, err := util.GcloudConfig() + if err != nil { + return nil, err + } + if cfg.Configuration.Properties.Core.Project == "" { + return nil, fmt.Errorf("gcloud has no active project, you can set it by running `gcloud config set project `") + } + return []string{cfg.Configuration.Properties.Core.Project}, nil +} + +func runProxy() int { + flag.Var(&instances, "instances", + `Comma-separated list of fully qualified instances (project:region:name) +to connect to. If the name has the suffix '=tcp:port', a TCP server is opened +on the specified port on localhost to proxy to that instance. It is also possible +to listen on a custom address by providing a host, e.g., '=tcp:0.0.0.0:port'. If +no value is provided for 'tcp', one socket file per instance is opened in 'dir'. +For convenience, this flag may be specified multiple times. +You may use the INSTANCES environment variable for the same effect. Using both will +use the value from the flag, Not compatible with -fuse.`, + ) + + flag.Parse() + + if *version { + fmt.Println("Cloud SQL Auth proxy:", semanticVersion()) + return 0 + } + + if *logDebugStdout { + logging.LogDebugToStdout() + } + + if !*verbose { + logging.LogVerboseToNowhere() + } + + if *structuredLogs { + cleanup, err := logging.EnableStructuredLogs(*logDebugStdout, *verbose) + if err != nil { + logging.Errorf("failed to enable structured logs: %v", err) + return 1 + } + defer cleanup() + } + + if *quiet { + logging.Infof("Cloud SQL Auth proxy logging has been disabled by the -quiet flag. All messages (including errors) will be suppressed.") + logging.DisableLogging() + } + + // Split the input ipAddressTypes to the slice of string + ipAddrTypeOptsInput := strings.Split(*ipAddressTypes, ",") + + if *fdRlimit != 0 { + if err := limits.SetupFDLimits(*fdRlimit); err != nil { + logging.Infof("failed to setup file descriptor limits: %v", err) + } + } + + if *host != "" && !strings.HasSuffix(*host, "/") { + logging.Errorf("Flag host should always end with /") + flag.PrintDefaults() + return 0 + } + + // TODO: needs a better place for consolidation + // if instances is blank and env var INSTANCES is supplied use it + if envInstances := os.Getenv("INSTANCES"); len(instances) == 0 && envInstances != "" { + instances.Set(envInstances) + } + + projList := stringList(*projects) + // TODO: it'd be really great to consolidate flag verification in one place. + if len(instances) == 0 && *instanceSrc == "" && len(projList) == 0 && !*useFuse { + var err error + projList, err = gcloudProject() + if err == nil { + logging.Infof("Using gcloud's active project: %v", projList) + } else if gErr, ok := err.(*util.GcloudError); ok && gErr.Status == util.GcloudNotFound { + logging.Errorf("gcloud is not in the path and -instances and -projects are empty") + return 1 + } else { + logging.Errorf("unable to retrieve the active gcloud project and -instances and -projects are empty: %v", err) + return 1 + } + } + + onGCE := metadata.OnGCE() + if err := checkFlags(onGCE); err != nil { + logging.Errorf(err.Error()) + return 1 + } + + ctx, cancel := context.WithCancel(context.Background()) + client, tokSrc, err := authenticatedClient(ctx) + if err != nil { + logging.Errorf(err.Error()) + return 1 + } + + if *quotaProject != "" { + logging.Infof("Using the project %q for SQL Admin API quota", *quotaProject) + configureQuotaProject(client, *quotaProject) + } + + ins, err := listInstances(ctx, client, projList) + if err != nil { + logging.Errorf(err.Error()) + return 1 + } + instances = append(instances, ins...) + cfgs, err := CreateInstanceConfigs(*dir, *useFuse, instances, *instanceSrc, client, *skipInvalidInstanceConfigs) + if err != nil { + logging.Errorf(err.Error()) + return 1 + } + + // We only need to store connections in a ConnSet if FUSE is used; otherwise + // it is not efficient to do so. + var connset *proxy.ConnSet + if *useFuse { + connset = proxy.NewConnSet() + } + + // Create proxy client first; fuse uses its cache to resolve database version. + refreshCfgThrottle := *refreshCfgThrottle + if refreshCfgThrottle < minimumRefreshCfgThrottle { + refreshCfgThrottle = minimumRefreshCfgThrottle + } + refreshCfgBuffer := proxy.DefaultRefreshCfgBuffer + if *enableIAMLogin { + refreshCfgThrottle = proxy.IAMLoginRefreshThrottle + refreshCfgBuffer = proxy.IAMLoginRefreshCfgBuffer + } + proxyClient := &proxy.Client{ + Port: port, + MaxConnections: *maxConnections, + Certs: certs.NewCertSourceOpts(client, certs.RemoteOpts{ + APIBasePath: *host, + IgnoreRegion: !*checkRegion, + UserAgent: userAgentFromVersionString(), + IPAddrTypeOpts: ipAddrTypeOptsInput, + EnableIAMLogin: *enableIAMLogin, + TokenSource: tokSrc, + }), + Conns: connset, + RefreshCfgThrottle: refreshCfgThrottle, + RefreshCfgBuffer: refreshCfgBuffer, + } + + var hc *healthcheck.Server + if *useHTTPHealthCheck { + // Extract a list of all instances specified statically. List is empty when in fuse mode. + var insts []string + for _, cfg := range cfgs { + insts = append(insts, cfg.Instance) + } + hc, err = healthcheck.NewServer(proxyClient, *healthCheckPort, insts) + if err != nil { + logging.Errorf("[Health Check] Could not initialize health check server: %v", err) + return 1 + } + defer hc.Close(ctx) + } + + // Initialize a source of new connections to Cloud SQL instances. + var connSrc <-chan proxy.Conn + if *useFuse { + c, fuse, err := fuse.NewConnSrc(*dir, *fuseTmp, proxyClient, connset) + if err != nil { + logging.Errorf("Could not start fuse directory at %q: %v", *dir, err) + return 1 + } + connSrc = c + defer fuse.Close() + } else { + updates := make(chan string) + if *instanceSrc != "" { + go func() { + for { + err := metadata.Subscribe(*instanceSrc, func(v string, ok bool) error { + if ok { + updates <- v + } + return nil + }) + if err != nil { + logging.Errorf("Error on receiving new instances from metadata: %v", err) + } + time.Sleep(5 * time.Second) + } + }() + } + + c, err := WatchInstances(*dir, cfgs, updates, client) + if err != nil { + logging.Errorf(err.Error()) + return 1 + } + connSrc = c + } + + logging.Infof("Ready for new connections") + + if hc != nil { + hc.NotifyStarted() + } + + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) + + shutdown := make(chan int, 1) + go func() { + defer func() { cancel(); close(shutdown) }() + <-signals + logging.Infof("Received TERM signal. Waiting up to %s before terminating.", *termTimeout) + go func() { + if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil { + logging.Errorf("Failed to notify systemd of termination: %v", err) + } + }() + + err := proxyClient.Shutdown(*termTimeout) + if err != nil { + logging.Errorf("Error during SIGTERM shutdown: %v", err) + shutdown <- 2 + return + } + }() + + // If running under systemd with Type=notify, we'll send a message to the + // service manager that we are ready to handle connections now, and any other + // units that are waiting for us can start. + go func() { + if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { + logging.Errorf("Failed to notify systemd of readiness: %v", err) + } + }() + proxyClient.RunContext(ctx, connSrc) + if code, ok := <-shutdown; ok { + return code + } + return 0 +} + +func main() { + code := runProxy() + os.Exit(code) +} diff --git a/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go b/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go new file mode 100644 index 0000000000..4319379bc8 --- /dev/null +++ b/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go @@ -0,0 +1,33 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + "strings" + "testing" +) + +func TestVersionStripsNewline(t *testing.T) { + v, err := os.ReadFile("version.txt") + if err != nil { + t.Fatalf("failed to read verion.txt: %v", err) + } + want := strings.TrimSpace(string(v)) + + if got := semanticVersion(); got != want { + t.Fatalf("want = %q, got = %q", want, got) + } +} diff --git a/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go new file mode 100644 index 0000000000..7b5ed0e708 --- /dev/null +++ b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go @@ -0,0 +1,194 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package healthcheck tests and communicates the health of the Cloud SQL Auth proxy. +package healthcheck + +import ( + "context" + "errors" + "net" + "net/http" + "sync" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" +) + +const ( + startupPath = "/startup" + livenessPath = "/liveness" + readinessPath = "/readiness" +) + +// Server is a type used to implement health checks for the proxy. +type Server struct { + // started is used to indicate whether the proxy has finished starting up. + // If started is open, startup has not finished. If started is closed, + // startup is complete. + started chan struct{} + // once ensures that started can only be closed once. + once *sync.Once + // port designates the port number on which Server listens and serves. + port string + // srv is a pointer to the HTTP server used to communicate proxy health. + srv *http.Server + // instances is a list of all instances specified statically (e.g. as flags to the binary) + instances []string +} + +// NewServer initializes a Server and exposes HTTP endpoints used to +// communicate proxy health. +func NewServer(c *proxy.Client, port string, staticInst []string) (*Server, error) { + mux := http.NewServeMux() + + srv := &http.Server{ + Addr: ":" + port, + Handler: mux, + } + + hcServer := &Server{ + started: make(chan struct{}), + once: &sync.Once{}, + port: port, + srv: srv, + instances: staticInst, + } + + mux.HandleFunc(startupPath, func(w http.ResponseWriter, _ *http.Request) { + if !hcServer.proxyStarted() { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("error")) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + + mux.HandleFunc(readinessPath, func(w http.ResponseWriter, _ *http.Request) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if !isReady(ctx, c, hcServer) { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("error")) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + + mux.HandleFunc(livenessPath, func(w http.ResponseWriter, _ *http.Request) { + if !isLive(c) { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("error")) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + + ln, err := net.Listen("tcp", srv.Addr) + if err != nil { + return nil, err + } + + go func() { + if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { + logging.Errorf("[Health Check] Failed to serve: %v", err) + } + }() + + return hcServer, nil +} + +// Close gracefully shuts down the HTTP server belonging to the Server. +func (s *Server) Close(ctx context.Context) error { + return s.srv.Shutdown(ctx) +} + +// NotifyStarted tells the Server that the proxy has finished startup. +func (s *Server) NotifyStarted() { + s.once.Do(func() { close(s.started) }) +} + +// proxyStarted returns true if started is closed, false otherwise. +func (s *Server) proxyStarted() bool { + select { + case <-s.started: + return true + default: + return false + } +} + +// isLive returns true as long as the proxy Client has all valid connections. +func isLive(c *proxy.Client) bool { + invalid := c.InvalidInstances() + alive := len(invalid) == 0 + if !alive { + for _, err := range invalid { + logging.Errorf("[Health Check] Liveness failed: %v", err) + } + } + return alive +} + +// isReady will check the following criteria: +// 1. Finished starting up / been sent the 'Ready for Connections' log. +// 2. Not yet hit the MaxConnections limit, if set. +// 3. Able to dial all specified instances without error. +func isReady(ctx context.Context, c *proxy.Client, s *Server) bool { + // Not ready until we reach the 'Ready for Connections' log. + if !s.proxyStarted() { + logging.Errorf("[Health Check] Readiness failed because proxy has not finished starting up.") + return false + } + + // Not ready if the proxy is at the optional MaxConnections limit. + if !c.AvailableConn() { + logging.Errorf("[Health Check] Readiness failed because proxy has reached the maximum connections limit (%v).", c.MaxConnections) + return false + } + + // Not ready if one or more instances cannot be dialed. + instances := s.instances + if s.instances == nil { // Proxy is in fuse mode. + instances = c.GetInstances() + } + + canDial := true + var once sync.Once + var wg sync.WaitGroup + + for _, inst := range instances { + wg.Add(1) + go func(inst string) { + defer wg.Done() + conn, err := c.DialContext(ctx, inst) + if err != nil { + logging.Errorf("[Health Check] Readiness failed because proxy couldn't connect to %q: %v", inst, err) + once.Do(func() { canDial = false }) + return + } + + err = conn.Close() + if err != nil { + logging.Errorf("[Health Check] Readiness: error while closing connection: %v", err) + } + }(inst) + } + wg.Wait() + + return canDial +} diff --git a/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go new file mode 100644 index 0000000000..f6e0e2c9a2 --- /dev/null +++ b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go @@ -0,0 +1,251 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package healthcheck_test + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "net" + "net/http" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd/cloud_sql_proxy/internal/healthcheck" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" +) + +const ( + startupPath = "/startup" + livenessPath = "/liveness" + readinessPath = "/readiness" + testPort = "8090" +) + +type fakeCertSource struct{} + +func (cs *fakeCertSource) Local(instance string) (tls.Certificate, error) { + return tls.Certificate{ + Leaf: &x509.Certificate{ + NotAfter: time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }, nil +} + +func (cs *fakeCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { + return &x509.Certificate{}, "fake address", "fake name", "fake version", nil +} + +type failingCertSource struct{} + +func (cs *failingCertSource) Local(instance string) (tls.Certificate, error) { + return tls.Certificate{}, errors.New("failed") +} + +func (cs *failingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { + return nil, "", "", "", errors.New("failed") +} + +// Test to verify that when the proxy client is up, the liveness endpoint writes http.StatusOK. +func TestLivenessPasses(t *testing.T) { + s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) + if err != nil { + t.Fatalf("Could not initialize health check: %v", err) + } + defer s.Close(context.Background()) + + resp, err := http.Get("http://localhost:" + testPort + livenessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) + } +} + +func TestLivenessFails(t *testing.T) { + c := &proxy.Client{ + Certs: &failingCertSource{}, + Dialer: func(string, string) (net.Conn, error) { + return nil, errors.New("error") + }, + } + // ensure cache has errored config + _, err := c.Dial("proj:region:instance") + if err == nil { + t.Fatalf("expected Dial to fail, but it succeeded") + } + + s, err := healthcheck.NewServer(c, testPort, []string{"proj:region:instance"}) + if err != nil { + t.Fatalf("Could not initialize health check: %v", err) + } + defer s.Close(context.Background()) + + resp, err := http.Get("http://localhost:" + testPort + livenessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + defer resp.Body.Close() + want := http.StatusServiceUnavailable + if got := resp.StatusCode; got != want { + t.Errorf("want %v, got %v", want, got) + } +} + +// Test to verify that when startup HAS finished (and MaxConnections limit not specified), +// the startup and readiness endpoints write http.StatusOK. +func TestStartupPass(t *testing.T) { + s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) + if err != nil { + t.Fatalf("Could not initialize health check: %v", err) + } + defer s.Close(context.Background()) + + // Simulate the proxy client completing startup. + s.NotifyStarted() + + resp, err := http.Get("http://localhost:" + testPort + startupPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("%v: want %v, got %v", startupPath, http.StatusOK, resp.StatusCode) + } + + resp, err = http.Get("http://localhost:" + testPort + readinessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("%v: want %v, got %v", readinessPath, http.StatusOK, resp.StatusCode) + } +} + +// Test to verify that when startup has NOT finished, the startup and readiness endpoints write +// http.StatusServiceUnavailable. +func TestStartupFail(t *testing.T) { + s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) + if err != nil { + t.Fatalf("Could not initialize health check: %v", err) + } + defer s.Close(context.Background()) + + resp, err := http.Get("http://localhost:" + testPort + startupPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusServiceUnavailable { + t.Errorf("%v: want %v, got %v", startupPath, http.StatusOK, resp.StatusCode) + } + + resp, err = http.Get("http://localhost:" + testPort + readinessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusServiceUnavailable { + t.Errorf("%v: want %v, got %v", readinessPath, http.StatusOK, resp.StatusCode) + } +} + +// Test to verify that when startup has finished, but MaxConnections has been reached, +// the readiness endpoint writes http.StatusServiceUnavailable. +func TestMaxConnectionsReached(t *testing.T) { + c := &proxy.Client{ + MaxConnections: 1, + } + s, err := healthcheck.NewServer(c, testPort, nil) + if err != nil { + t.Fatalf("Could not initialize health check: %v", err) + } + defer s.Close(context.Background()) + + s.NotifyStarted() + c.ConnectionsCounter = c.MaxConnections // Simulate reaching the limit for maximum number of connections + + resp, err := http.Get("http://localhost:" + testPort + readinessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusServiceUnavailable { + t.Errorf("want %v, got %v", http.StatusServiceUnavailable, resp.StatusCode) + } +} + +// Test to verify that when dialing instance(s) returns an error, the readiness endpoint +// writes http.StatusServiceUnavailable. +func TestDialFail(t *testing.T) { + tests := map[string]struct { + insts []string + }{ + "Single instance": {insts: []string{"project:region:instance"}}, + "Multiple instances": {insts: []string{"project:region:instance-1", "project:region:instance-2", "project:region:instance-3"}}, + } + + c := &proxy.Client{ + Certs: &fakeCertSource{}, + Dialer: func(string, string) (net.Conn, error) { + return nil, errors.New("error") + }, + } + + for name, test := range tests { + func() { + s, err := healthcheck.NewServer(c, testPort, test.insts) + if err != nil { + t.Fatalf("%v: Could not initialize health check: %v", name, err) + } + defer s.Close(context.Background()) + s.NotifyStarted() + + resp, err := http.Get("http://localhost:" + testPort + readinessPath) + if err != nil { + t.Fatalf("%v: HTTP GET failed: %v", name, err) + } + if resp.StatusCode != http.StatusServiceUnavailable { + t.Errorf("want %v, got %v", http.StatusServiceUnavailable, resp.StatusCode) + } + }() + } +} + +// Test to verify that after closing a healthcheck, its liveness endpoint serves +// an error. +func TestCloseHealthCheck(t *testing.T) { + s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) + if err != nil { + t.Fatalf("Could not initialize health check: %v", err) + } + defer s.Close(context.Background()) + + resp, err := http.Get("http://localhost:" + testPort + livenessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) + } + + err = s.Close(context.Background()) + if err != nil { + t.Fatalf("Failed to close health check: %v", err) + } + + _, err = http.Get("http://localhost:" + testPort + livenessPath) + if err == nil { + t.Fatalf("HTTP GET did not return error after closing health check server.") + } +} diff --git a/cmd/cloud_sql_proxy/proxy.go b/cmd/cloud_sql_proxy/proxy.go new file mode 100644 index 0000000000..c9943ee2c2 --- /dev/null +++ b/cmd/cloud_sql_proxy/proxy.go @@ -0,0 +1,388 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +// This file contains code for supporting local sockets for the Cloud SQL Auth proxy. + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/fuse" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" + sqladmin "google.golang.org/api/sqladmin/v1beta4" +) + +// WatchInstances handles the lifecycle of local sockets used for proxying +// local connections. Values received from the updates channel are +// interpretted as a comma-separated list of instances. The set of sockets in +// 'dir' is the union of 'instances' and the most recent list from 'updates'. +func WatchInstances(dir string, cfgs []instanceConfig, updates <-chan string, cl *http.Client) (<-chan proxy.Conn, error) { + ch := make(chan proxy.Conn, 1) + + // Instances specified statically (e.g. as flags to the binary) will always + // be available. They are ignored if also returned by the GCE metadata since + // the socket will already be open. + staticInstances := make(map[string]net.Listener, len(cfgs)) + for _, v := range cfgs { + l, err := listenInstance(ch, v) + if err != nil { + return nil, err + } + staticInstances[v.Instance] = l + } + + if updates != nil { + go watchInstancesLoop(dir, ch, updates, staticInstances, cl) + } + return ch, nil +} + +func watchInstancesLoop(dir string, dst chan<- proxy.Conn, updates <-chan string, static map[string]net.Listener, cl *http.Client) { + dynamicInstances := make(map[string]net.Listener) + for instances := range updates { + // All instances were legal when we started, so we pass false below to ensure we don't skip them + // later if they became unhealthy for some reason; this would be a serious enough problem. + list, err := parseInstanceConfigs(dir, strings.Split(instances, ","), cl, false) + if err != nil { + logging.Errorf("%v", err) + // If we do not have a valid list of instances, skip this update + continue + } + + stillOpen := make(map[string]net.Listener) + for _, cfg := range list { + instance := cfg.Instance + + // If the instance is specified in the static list don't do anything: + // it's already open and should stay open forever. + if _, ok := static[instance]; ok { + continue + } + + if l, ok := dynamicInstances[instance]; ok { + delete(dynamicInstances, instance) + stillOpen[instance] = l + continue + } + + l, err := listenInstance(dst, cfg) + if err != nil { + logging.Errorf("Couldn't open socket for %q: %v", instance, err) + continue + } + stillOpen[instance] = l + } + + // Any instance in dynamicInstances was not in the most recent metadata + // update. Clean up those instances' sockets by closing them; note that + // this does not affect any existing connections instance. + for instance, listener := range dynamicInstances { + logging.Infof("Closing socket for instance %v", instance) + listener.Close() + } + + dynamicInstances = stillOpen + } + + for _, v := range static { + if err := v.Close(); err != nil { + logging.Errorf("Error closing %q: %v", v.Addr(), err) + } + } + for _, v := range dynamicInstances { + if err := v.Close(); err != nil { + logging.Errorf("Error closing %q: %v", v.Addr(), err) + } + } +} + +func remove(path string) { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + logging.Infof("Remove(%q) error: %v", path, err) + } +} + +// listenInstance starts listening on a new unix socket in dir to connect to the +// specified instance. New connections to this socket are sent to dst. +func listenInstance(dst chan<- proxy.Conn, cfg instanceConfig) (net.Listener, error) { + unix := cfg.Network == "unix" + if unix { + remove(cfg.Address) + } + l, err := net.Listen(cfg.Network, cfg.Address) + if err != nil { + return nil, err + } + if unix { + if err := os.Chmod(cfg.Address, 0777|os.ModeSocket); err != nil { + logging.Errorf("couldn't update permissions for socket file %q: %v; other users may not be unable to connect", cfg.Address, err) + } + } + + go func() { + for { + start := time.Now() + c, err := l.Accept() + if err != nil { + logging.Errorf("Error in accept for %q on %v: %v", cfg, cfg.Address, err) + if nerr, ok := err.(net.Error); ok && nerr.Temporary() { + d := 10*time.Millisecond - time.Since(start) + if d > 0 { + time.Sleep(d) + } + continue + } + l.Close() + return + } + logging.Verbosef("New connection for %q", cfg.Instance) + + switch clientConn := c.(type) { + case *net.TCPConn: + clientConn.SetKeepAlive(true) + clientConn.SetKeepAlivePeriod(1 * time.Minute) + + } + dst <- proxy.Conn{cfg.Instance, c} + } + }() + + logging.Infof("Listening on %s for %s", cfg.Address, cfg.Instance) + return l, nil +} + +type instanceConfig struct { + Instance string + Network, Address string +} + +// loopbackForNet maps a network (e.g. tcp6) to the loopback address for that +// network. It is updated during the initialization of validNets to include a +// valid loopback address for "tcp". +var loopbackForNet = map[string]string{ + "tcp4": "127.0.0.1", + "tcp6": "::1", +} + +// validNets tracks the networks that are valid for this platform and machine. +var validNets = func() map[string]bool { + m := map[string]bool{ + "unix": runtime.GOOS != "windows", + } + + anyTCP := false + for _, n := range []string{"tcp4", "tcp6"} { + host, ok := loopbackForNet[n] + if !ok { + // This is effectively a compile-time error. + panic(fmt.Sprintf("no loopback address found for %v", n)) + } + // Open any port to see if the net is valid. + x, err := net.Listen(n, net.JoinHostPort(host, "0")) + if err != nil { + // Error is too verbose to be useful. + continue + } + x.Close() + m[n] = true + + if !anyTCP { + anyTCP = true + // Set the loopback value for generic tcp if it hasn't already been + // set. (If both tcp4/tcp6 are supported the first one in the list + // (tcp4's 127.0.0.1) is used. + loopbackForNet["tcp"] = host + } + } + if anyTCP { + m["tcp"] = true + } + return m +}() + +func parseInstanceConfig(dir, instance string, cl *http.Client) (instanceConfig, error) { + var ret instanceConfig + proj, region, name, args, err := proxy.ParseInstanceConnectionName(instance) + if err != nil { + return instanceConfig{}, err + } + ret.Instance = args[0] + regionName := fmt.Sprintf("%s~%s", region, name) + if len(args) == 1 { + // Default to listening via unix socket in specified directory + ret.Network = "unix" + ret.Address = filepath.Join(dir, instance) + } else { + // Parse the instance options if present. + opts := strings.SplitN(args[1], ":", 2) + if len(opts) != 2 { + return instanceConfig{}, fmt.Errorf("invalid instance options: must be in the form `unix:/path/to/socket`, `tcp:port`, `tcp:host:port`; invalid option was %q", strings.Join(opts, ":")) + } + ret.Network = opts[0] + var err error + if ret.Network == "unix" { + if strings.HasPrefix(opts[1], "/") { + ret.Address = opts[1] // Root path. + } else { + ret.Address = filepath.Join(dir, opts[1]) + } + } else { + ret.Address, err = parseTCPOpts(opts[0], opts[1]) + } + if err != nil { + return instanceConfig{}, err + } + } + + // Use the SQL Admin API to verify compatibility with the instance. + sql, err := sqladmin.New(cl) + if err != nil { + return instanceConfig{}, err + } + if *host != "" { + sql.BasePath = *host + } + inst, err := sql.Connect.Get(proj, regionName).Do() + if err != nil { + return instanceConfig{}, err + } + if inst.BackendType == "FIRST_GEN" { + logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") + return instanceConfig{}, fmt.Errorf("%q is a first generation instance", instance) + } + // Postgres instances use a special suffix on the unix socket. + // See https://www.postgresql.org/docs/11/runtime-config-connection.html + if ret.Network == "unix" && strings.HasPrefix(strings.ToLower(inst.DatabaseVersion), "postgres") { + // Verify the directory exists. + if err := os.MkdirAll(ret.Address, 0755); err != nil { + return instanceConfig{}, err + } + ret.Address = filepath.Join(ret.Address, ".s.PGSQL.5432") + } + + if !validNets[ret.Network] { + return ret, fmt.Errorf("invalid %q: unsupported network: %v", instance, ret.Network) + } + return ret, nil +} + +// parseTCPOpts parses the instance options when specifying tcp port options. +func parseTCPOpts(ntwk, addrOpt string) (string, error) { + if strings.Contains(addrOpt, ":") { + return addrOpt, nil // User provided a host and port; use that. + } + // No "host" part of the address. Be safe and assume that they want a loopback address. + addr, ok := loopbackForNet[ntwk] + if !ok { + return "", fmt.Errorf("invalid %q:%q: unrecognized network %v", ntwk, addrOpt, ntwk) + } + return net.JoinHostPort(addr, addrOpt), nil +} + +// parseInstanceConfigs calls parseInstanceConfig for each instance in the +// provided slice, collecting errors along the way. There may be valid +// instanceConfigs returned even if there's an error. +func parseInstanceConfigs(dir string, instances []string, cl *http.Client, skipFailedInstanceConfigs bool) ([]instanceConfig, error) { + errs := new(bytes.Buffer) + var cfg []instanceConfig + for _, v := range instances { + if v == "" { + continue + } + if c, err := parseInstanceConfig(dir, v, cl); err != nil { + if skipFailedInstanceConfigs { + logging.Infof("There was a problem when parsing an instance configuration but ignoring due to the configuration. Error: %v", err) + } else { + fmt.Fprintf(errs, "\n\t%v", err) + } + + } else { + cfg = append(cfg, c) + } + } + + var err error + if errs.Len() > 0 { + err = fmt.Errorf("errors parsing config:%s", errs) + } + return cfg, err +} + +// CreateInstanceConfigs verifies that the parameters passed to it are valid +// for the proxy for the platform and system and then returns a slice of valid +// instanceConfig. It is possible for the instanceConfig to be empty if no valid +// configurations were specified, however `err` will be set. +func CreateInstanceConfigs(dir string, useFuse bool, instances []string, instancesSrc string, cl *http.Client, skipFailedInstanceConfigs bool) ([]instanceConfig, error) { + if useFuse && !fuse.Supported() { + return nil, errors.New("FUSE not supported on this system") + } + + cfgs, err := parseInstanceConfigs(dir, instances, cl, skipFailedInstanceConfigs) + if err != nil { + return nil, err + } + + if dir == "" { + // Reasons to set '-dir': + // - Using -fuse + // - Using the metadata to get a list of instances + // - Having an instance that uses a 'unix' network + if useFuse { + return nil, errors.New("must set -dir because -fuse was set") + } else if instancesSrc != "" { + return nil, errors.New("must set -dir because -instances_metadata was set") + } else { + for _, v := range cfgs { + if v.Network == "unix" { + return nil, fmt.Errorf("must set -dir: using a unix socket for %v", v.Instance) + } + } + } + // Otherwise it's safe to not set -dir + } + + if useFuse { + if len(instances) != 0 || instancesSrc != "" { + return nil, errors.New("-fuse is not compatible with -projects, -instances, or -instances_metadata") + } + return nil, nil + } + // FUSE disabled. + if len(instances) == 0 && instancesSrc == "" { + // Failure to specifying instance can be caused by following reasons. + // 1. not enough information is provided by flags + // 2. failed to invoke gcloud + var flags string + if fuse.Supported() { + flags = "-projects, -fuse, -instances or -instances_metadata" + } else { + flags = "-projects, -instances or -instances_metadata" + } + + errStr := fmt.Sprintf("no instance selected because none of %s is specified", flags) + return nil, errors.New(errStr) + } + return cfgs, nil +} diff --git a/cmd/cloud_sql_proxy/proxy_test.go b/cmd/cloud_sql_proxy/proxy_test.go new file mode 100644 index 0000000000..54837c3f44 --- /dev/null +++ b/cmd/cloud_sql_proxy/proxy_test.go @@ -0,0 +1,214 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "runtime" + "testing" +) + +type mockTripper struct { +} + +func (m *mockTripper) RoundTrip(r *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte("{}")))}, nil +} + +var mockClient = &http.Client{Transport: &mockTripper{}} + +func TestCreateInstanceConfigs(t *testing.T) { + for _, v := range []struct { + desc string + //inputs + dir string + useFuse bool + instances []string + instancesSrc string + + // We don't need to check the []instancesConfig return value, we already + // have a TestParseInstanceConfig. + wantErr bool + + skipFailedInstanceConfig bool + + supportedOnWindows bool + }{ + { + "setting -fuse and -dir", + "dir", true, nil, "", false, false, false, + }, { + "setting -fuse", + "", true, nil, "", true, false, false, + }, { + "setting -fuse, -dir, and -instances", + "dir", true, []string{"proj:reg:x"}, "", true, false, false, + }, { + "setting -fuse, -dir, and -instances_metadata", + "dir", true, nil, "md", true, false, false, + }, { + "setting -dir and -instances (unix socket)", + "dir", false, []string{"proj:reg:x"}, "", false, false, false, + }, { + // tests for the case where invalid configs can still exist, when skipped + "setting -dir and -instances (unix socket) w/ something invalid", + "dir", false, []string{"proj:reg:x", "INVALID_PROJECT_STRING"}, "", false, true, false, + }, { + "Seting -instance (unix socket)", + "", false, []string{"proj:reg:x"}, "", true, false, false, + }, { + "setting -instance (tcp socket)", + "", false, []string{"proj:reg:x=tcp:1234"}, "", false, false, true, + }, { + "setting -instance (tcp socket) and -instances_metadata", + "", false, []string{"proj:reg:x=tcp:1234"}, "md", true, false, true, + }, { + "setting -dir, -instance (tcp socket), and -instances_metadata", + "dir", false, []string{"proj:reg:x=tcp:1234"}, "md", false, false, true, + }, { + "setting -dir, -instance (unix socket), and -instances_metadata", + "dir", false, []string{"proj:reg:x"}, "md", false, false, false, + }, { + "setting -dir and -instances_metadata", + "dir", false, nil, "md", false, false, false, + }, { + "setting -instances_metadata", + "", false, nil, "md", true, false, true, + }, + } { + if runtime.GOOS == "windows" && !v.supportedOnWindows { + continue + } + if v.useFuse && testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + _, err := CreateInstanceConfigs(v.dir, v.useFuse, v.instances, v.instancesSrc, mockClient, v.skipFailedInstanceConfig) + if v.wantErr { + if err == nil { + t.Errorf("CreateInstanceConfigs passed when %s, wanted error", v.desc) + } + continue + } + if err != nil { + t.Errorf("CreateInstanceConfigs gave error when %s: %v", v.desc, err) + } + } +} + +func TestParseInstanceConfig(t *testing.T) { + // sentinel values + var ( + anyLoopbackAddress = "" + wantErr = instanceConfig{"", "", ""} + ) + + tcs := []struct { + // inputs + dir, instance string + + wantCfg instanceConfig + }{ + { + "/x", "domain.com:my-proj:my-reg:my-instance", + instanceConfig{"domain.com:my-proj:my-reg:my-instance", "unix", "/x/domain.com:my-proj:my-reg:my-instance"}, + }, { + "/x", "my-proj:my-reg:my-instance", + instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/my-proj:my-reg:my-instance"}, + }, { + "/x", "my-proj:my-reg:my-instance=unix:socket_name", + instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/socket_name"}, + }, { + "/x", "my-proj:my-reg:my-instance=unix:/my/custom/sql-socket", + instanceConfig{"my-proj:my-reg:my-instance", "unix", "/my/custom/sql-socket"}, + }, { + "/x", "my-proj:my-reg:my-instance=tcp:1234", + instanceConfig{"my-proj:my-reg:my-instance", "tcp", anyLoopbackAddress}, + }, { + "/x", "my-proj:my-reg:my-instance=tcp4:1234", + instanceConfig{"my-proj:my-reg:my-instance", "tcp4", "127.0.0.1:1234"}, + }, { + "/x", "my-proj:my-reg:my-instance=tcp6:1234", + instanceConfig{"my-proj:my-reg:my-instance", "tcp6", "[::1]:1234"}, + }, { + "/x", "my-proj:my-reg:my-instance=tcp:my-host:1111", + instanceConfig{"my-proj:my-reg:my-instance", "tcp", "my-host:1111"}, + }, { + "/x", "my-proj:my-reg:my-instance=", + wantErr, + }, { + "/x", "my-proj:my-reg:my-instance=cool network", + wantErr, + }, { + "/x", "my-proj:my-reg:my-instance=cool network:1234", + wantErr, + }, { + "/x", "my-proj:my-reg:my-instance=oh:so:many:colons", + wantErr, + }, + } + + for _, tc := range tcs { + t.Run(fmt.Sprintf("parseInstanceConfig(%q, %q)", tc.dir, tc.instance), func(t *testing.T) { + if os.Getenv("EXPECT_IPV4_AND_IPV6") != "true" { + // Skip ipv4 and ipv6 if they are not supported by the machine. + // (assumption is that validNets isn't buggy) + if tc.wantCfg.Network == "tcp4" || tc.wantCfg.Network == "tcp6" { + if !validNets[tc.wantCfg.Network] { + t.Skipf("%q net not supported, skipping", tc.wantCfg.Network) + } + } + // Skip unix sockets on Windows + if runtime.GOOS == "windows" && tc.wantCfg.Network == "unix" { + t.Skipf("%q net not supported on Windows, skipping", tc.wantCfg.Network) + } + } + + got, err := parseInstanceConfig(tc.dir, tc.instance, mockClient) + if tc.wantCfg == wantErr { + if err != nil { + return // pass. an error was expected and returned. + } + t.Fatalf("parseInstanceConfig(%s, %s) = %+v, wanted error", tc.dir, tc.instance, got) + } + if err != nil { + t.Fatalf("parseInstanceConfig(%s, %s) had unexpected error: %v", tc.dir, tc.instance, err) + } + + if tc.wantCfg.Address == anyLoopbackAddress { + host, _, err := net.SplitHostPort(got.Address) + if err != nil { + t.Fatalf("net.SplitHostPort(%v): %v", got.Address, err) + } + ip := net.ParseIP(host) + if !ip.IsLoopback() { + t.Fatalf("want loopback, got addr: %v", got.Address) + } + + // use a placeholder address, so the rest of the config can be compared + got.Address = "" + tc.wantCfg.Address = got.Address + } + + if got != tc.wantCfg { + t.Errorf("parseInstanceConfig(%s, %s) = %+v, want %+v", tc.dir, tc.instance, got, tc.wantCfg) + } + }) + } +} diff --git a/cmd/cloud_sql_proxy/version.txt b/cmd/cloud_sql_proxy/version.txt new file mode 100644 index 0000000000..a6c4ddd7b3 --- /dev/null +++ b/cmd/cloud_sql_proxy/version.txt @@ -0,0 +1 @@ +1.30.1-dev diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 0000000000..0af70ba2f0 --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,52 @@ +// Copyright 2021 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" +) + +var ( + errSigInt = &exitError{ + Err: errors.New("SIGINT signal received"), + Code: 130, + } + + errSigTerm = &exitError{ + Err: errors.New("SIGTERM signal received"), + Code: 137, + } +) + +func newBadCommandError(msg string) error { + return &exitError{ + Err: errors.New(msg), + Code: 1, + } +} + +// exitError is an error with an exit code, that's returned when the cmd exits. +// When possible, try to match these conventions: https://tldp.org/LDP/abs/html/exitcodes.html +type exitError struct { + Code int + Err error +} + +func (e *exitError) Error() string { + if e.Err == nil { + return "" + } + return e.Err.Error() +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000000..dede5b78c5 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,269 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + _ "embed" + "errors" + "fmt" + "net" + "net/url" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy" + "github.com/spf13/cobra" +) + +var ( + // versionString indicates the version of this library. + //go:embed version.txt + versionString string + userAgent string +) + +func init() { + versionString = strings.TrimSpace(versionString) + userAgent = "cloud-sql-auth-proxy/" + versionString +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := NewCommand().Execute(); err != nil { + exit := 1 + if terr, ok := err.(*exitError); ok { + exit = terr.Code + } + os.Exit(exit) + } +} + +// Command represents an invocation of the Cloud SQL Auth Proxy. +type Command struct { + *cobra.Command + conf *proxy.Config +} + +// Option is a function that configures a Command. +type Option func(*proxy.Config) + +// WithDialer configures the Command to use the provided dialer to connect to +// Cloud SQL instances. +func WithDialer(d cloudsql.Dialer) Option { + return func(c *proxy.Config) { + c.Dialer = d + } +} + +// NewCommand returns a Command object representing an invocation of the proxy. +func NewCommand(opts ...Option) *Command { + c := &Command{ + conf: &proxy.Config{}, + } + for _, o := range opts { + o(c.conf) + } + + cmd := &cobra.Command{ + Use: "cloud_sql_proxy instance_connection_name...", + Version: versionString, + Short: "cloud_sql_proxy provides a secure way to authorize connections to Cloud SQL.", + Long: `The Cloud SQL Auth proxy provides IAM-based authorization and encryption when +connecting to Cloud SQL instances. It listens on a local port and forwards connections +to your instance's IP address, providing a secure connection without having to manage +any client SSL certificates.`, + Args: func(cmd *cobra.Command, args []string) error { + err := parseConfig(cmd, c.conf, args) + if err != nil { + return err + } + // The arguments are parsed. Usage is no longer needed. + cmd.SilenceUsage = true + return nil + }, + RunE: func(*cobra.Command, []string) error { + return runSignalWrapper(c) + }, + } + + // Global-only flags + cmd.PersistentFlags().StringVarP(&c.conf.Token, "token", "t", "", + "Bearer token used for authorization.") + cmd.PersistentFlags().StringVarP(&c.conf.CredentialsFile, "credentials-file", "c", "", + "Path to a service account key to use for authentication.") + + // Global and per instance flags + cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1", + "Address on which to bind Cloud SQL instance listeners.") + cmd.PersistentFlags().IntVarP(&c.conf.Port, "port", "p", 0, + "Initial port to use for listeners. Subsequent listeners increment from this value.") + + c.Command = cmd + return c +} + +func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error { + // If no instance connection names were provided, error. + if len(args) == 0 { + return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)") + } + // First, validate global config. + if ip := net.ParseIP(conf.Addr); ip == nil { + return newBadCommandError(fmt.Sprintf("not a valid IP address: %q", conf.Addr)) + } + + // If both token and credentials file were set, error. + if conf.Token != "" && conf.CredentialsFile != "" { + return newBadCommandError("Cannot specify --token and --credentials-file flags at the same time") + } + + switch { + case conf.Token != "": + cmd.Printf("Authorizing with the -token flag\n") + case conf.CredentialsFile != "": + cmd.Printf("Authorizing with the credentials file at %q\n", conf.CredentialsFile) + default: + cmd.Printf("Authorizing with Application Default Credentials") + } + + var ics []proxy.InstanceConnConfig + for _, a := range args { + // Assume no query params initially + ic := proxy.InstanceConnConfig{ + Name: a, + } + // If there are query params, update instance config. + if res := strings.SplitN(a, "?", 2); len(res) > 1 { + ic.Name = res[0] + q, err := url.ParseQuery(res[1]) + if err != nil { + return newBadCommandError(fmt.Sprintf("could not parse query: %q", res[1])) + } + + if a, ok := q["address"]; ok { + if len(a) != 1 { + return newBadCommandError(fmt.Sprintf("address query param should be only one value: %q", a)) + } + if ip := net.ParseIP(a[0]); ip == nil { + return newBadCommandError( + fmt.Sprintf("address query param is not a valid IP address: %q", + a[0], + )) + } + ic.Addr = a[0] + } + + if p, ok := q["port"]; ok { + if len(p) != 1 { + return newBadCommandError(fmt.Sprintf("port query param should be only one value: %q", a)) + } + pp, err := strconv.Atoi(p[0]) + if err != nil { + return newBadCommandError( + fmt.Sprintf("port query param is not a valid integer: %q", + p[0], + )) + } + ic.Port = pp + } + } + ics = append(ics, ic) + } + + conf.Instances = ics + return nil +} + +// runSignalWrapper watches for SIGTERM and SIGINT and interupts execution if necessary. +func runSignalWrapper(cmd *Command) error { + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + shutdownCh := make(chan error) + + // watch for sigterm / sigint signals + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) + go func() { + var s os.Signal + select { + case s = <-signals: + case <-cmd.Context().Done(): + // this should only happen when the context supplied in tests in canceled + s = syscall.SIGINT + } + switch s { + case syscall.SIGINT: + shutdownCh <- errSigInt + case syscall.SIGTERM: + shutdownCh <- errSigTerm + } + }() + + // Start the proxy asynchronously, so we can exit early if a shutdown signal is sent + startCh := make(chan *proxy.Client) + go func() { + defer close(startCh) + // Check if the caller has configured a dialer. + // Otherwise, initialize a new one. + d := cmd.conf.Dialer + if d == nil { + opts := append(cmd.conf.DialerOpts(), cloudsqlconn.WithUserAgent(userAgent)) + var err error + d, err = cloudsqlconn.NewDialer(ctx, opts...) + if err != nil { + shutdownCh <- fmt.Errorf("error initializing dialer: %v", err) + return + } + } + p, err := proxy.NewClient(ctx, d, cmd.Command, cmd.conf) + if err != nil { + shutdownCh <- fmt.Errorf("unable to start: %v", err) + return + } + startCh <- p + }() + // Wait for either startup to finish or a signal to interupt + var p *proxy.Client + select { + case err := <-shutdownCh: + return err + case p = <-startCh: + } + cmd.Println("The proxy has started successfully and is ready for new connections!") + defer p.Close() + + go func() { + shutdownCh <- p.Serve(ctx) + }() + + err := <-shutdownCh + switch { + case errors.Is(err, errSigInt): + cmd.PrintErrln("SIGINT signal received. Shuting down...") + case errors.Is(err, errSigTerm): + cmd.PrintErrln("SIGTERM signal received. Shuting down...") + default: + cmd.PrintErrf("The proxy has encountered a terminal error: %v\n", err) + } + return err +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000000..12457d4cf1 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,277 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "errors" + "net" + "sync" + "testing" + "time" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/spf13/cobra" +) + +func TestNewCommandArguments(t *testing.T) { + withDefaults := func(c *proxy.Config) *proxy.Config { + if c.Addr == "" { + c.Addr = "127.0.0.1" + } + if c.Instances == nil { + c.Instances = []proxy.InstanceConnConfig{{}} + } + if i := &c.Instances[0]; i.Name == "" { + i.Name = "proj:region:inst" + } + return c + } + tcs := []struct { + desc string + args []string + want *proxy.Config + }{ + { + desc: "basic invocation with defaults", + args: []string{"proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Addr: "127.0.0.1", + Instances: []proxy.InstanceConnConfig{{Name: "proj:region:inst"}}, + }), + }, + { + desc: "using the address flag", + args: []string{"--address", "0.0.0.0", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Addr: "0.0.0.0", + Instances: []proxy.InstanceConnConfig{{Name: "proj:region:inst"}}, + }), + }, + { + desc: "using the address (short) flag", + args: []string{"-a", "0.0.0.0", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Addr: "0.0.0.0", + Instances: []proxy.InstanceConnConfig{{Name: "proj:region:inst"}}, + }), + }, + { + desc: "using the address query param", + args: []string{"proj:region:inst?address=0.0.0.0"}, + want: withDefaults(&proxy.Config{ + Addr: "127.0.0.1", + Instances: []proxy.InstanceConnConfig{{ + Addr: "0.0.0.0", + Name: "proj:region:inst", + }}, + }), + }, + { + desc: "using the port flag", + args: []string{"--port", "6000", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Port: 6000, + }), + }, + { + desc: "using the port (short) flag", + args: []string{"-p", "6000", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Port: 6000, + }), + }, + { + desc: "using the port query param", + args: []string{"proj:region:inst?port=6000"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + Port: 6000, + }}, + }), + }, + { + desc: "using the token flag", + args: []string{"--token", "MYCOOLTOKEN", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Token: "MYCOOLTOKEN", + }), + }, + { + desc: "using the token (short) flag", + args: []string{"-t", "MYCOOLTOKEN", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + Token: "MYCOOLTOKEN", + }), + }, + { + desc: "using the credentiale file flag", + args: []string{"--credentials-file", "/path/to/file", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + CredentialsFile: "/path/to/file", + }), + }, + { + desc: "using the (short) credentiale file flag", + args: []string{"-c", "/path/to/file", "proj:region:inst"}, + want: withDefaults(&proxy.Config{ + CredentialsFile: "/path/to/file", + }), + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c := NewCommand() + // Keep the test output quiet + c.SilenceUsage = true + c.SilenceErrors = true + // Disable execute behavior + c.RunE = func(*cobra.Command, []string) error { + return nil + } + c.SetArgs(tc.args) + + err := c.Execute() + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + + if got := c.conf; !cmp.Equal(tc.want, got, cmpopts.IgnoreUnexported(proxy.Config{})) { + t.Fatalf("want = %#v\ngot = %#v\ndiff = %v", tc.want, got, cmp.Diff(tc.want, got)) + } + }) + } +} + +func TestNewCommandWithErrors(t *testing.T) { + tcs := []struct { + desc string + args []string + }{ + { + desc: "basic invocation missing instance connection name", + args: []string{}, + }, + { + desc: "when the query string is bogus", + args: []string{"proj:region:inst?%=foo"}, + }, + { + desc: "when the address query param is empty", + args: []string{"proj:region:inst?address="}, + }, + { + desc: "using the address flag with a bad IP address", + args: []string{"--address", "bogus", "proj:region:inst"}, + }, + { + desc: "when the address query param is not an IP address", + args: []string{"proj:region:inst?address=世界"}, + }, + { + desc: "when the address query param contains multiple values", + args: []string{"proj:region:inst?address=0.0.0.0&address=1.1.1.1&address=2.2.2.2"}, + }, + { + desc: "when the query string is invalid", + args: []string{"proj:region:inst?address=1.1.1.1?foo=2.2.2.2"}, + }, + { + desc: "when the port query param contains multiple values", + args: []string{"proj:region:inst?port=1&port=2"}, + }, + { + desc: "when the port query param is not a number", + args: []string{"proj:region:inst?port=hi"}, + }, + { + desc: "when both token and credentials file is set", + args: []string{ + "--token", "my-token", + "--credentials-file", "/path/to/file", "proj:region:inst"}, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c := NewCommand() + // Keep the test output quiet + c.SilenceUsage = true + c.SilenceErrors = true + // Disable execute behavior + c.RunE = func(*cobra.Command, []string) error { + return nil + } + c.SetArgs(tc.args) + + err := c.Execute() + if err == nil { + t.Fatal("want error != nil, got = nil") + } + }) + } +} + +type spyDialer struct { + mu sync.Mutex + got string +} + +func (s *spyDialer) instance() string { + s.mu.Lock() + defer s.mu.Unlock() + i := s.got + return i +} + +func (*spyDialer) Dial(_ context.Context, inst string, _ ...cloudsqlconn.DialOption) (net.Conn, error) { + return nil, errors.New("spy dialer does not dial") +} + +func (s *spyDialer) EngineVersion(ctx context.Context, inst string) (string, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.got = inst + return "", nil +} + +func (*spyDialer) Close() error { + return nil +} + +func TestCommandWithCustomDialer(t *testing.T) { + want := "my-project:my-region:my-instance" + s := &spyDialer{} + c := NewCommand(WithDialer(s)) + // Keep the test output quiet + c.SilenceUsage = true + c.SilenceErrors = true + c.SetArgs([]string{want}) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + if err := c.ExecuteContext(ctx); !errors.As(err, &errSigInt) { + t.Fatalf("want errSigInt, got = %v", err) + } + + if got := s.instance(); got != want { + t.Fatalf("want = %v, got = %v", want, got) + } +} diff --git a/cmd/version.txt b/cmd/version.txt new file mode 100644 index 0000000000..d72f26267b --- /dev/null +++ b/cmd/version.txt @@ -0,0 +1 @@ +2.0.0-dev diff --git a/examples/k8s-health-check/README.md b/examples/k8s-health-check/README.md new file mode 100644 index 0000000000..2508f009f8 --- /dev/null +++ b/examples/k8s-health-check/README.md @@ -0,0 +1,70 @@ +# Cloud SQL proxy health checks + +Kubernetes supports three types of health checks. +1. Startup probes determine whether a container is done starting up. As soon as this probe succeeds, Kubernetes switches over to using liveness and readiness probing. +2. Liveness probes determine whether a container is healthy. When this probe is unsuccessful, the container is restarted. +3. Readiness probes determine whether a container can serve new traffic. When this probe fails, Kubernetes will wait to send requests to the container. + +## Running Cloud SQL proxy with health checks in Kubernetes +1. Configure your Cloud SQL proxy container to include health check probes. + > [proxy_with_http_health_check.yaml](proxy_with_http_health_check.yaml#L77-L111) + ```yaml + # Recommended configurations for health check probes. + # Probe parameters can be adjusted to best fit the requirements of your application. + # For details, see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + httpGet: + path: /liveness + port: 8090 + # Number of seconds after the container has started before the first probe is scheduled. Defaults to 0. + # Not necessary when the startup probe is in use. + initialDelaySeconds: 0 + # Frequency of the probe. Defaults to 10. + periodSeconds: 10 + # Number of seconds after which the probe times out. Defaults to 1. + timeoutSeconds: 5 + # Number of times the probe is allowed to fail before the transition from healthy to failure state. + # Defaults to 3. + failureThreshold: 1 + readinessProbe: + httpGet: + path: /readiness + port: 8090 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + # Number of times the probe must report success to transition from failure to healthy state. + # Defaults to 1 for readiness probe. + successThreshold: 1 + failureThreshold: 1 + startupProbe: + httpGet: + path: /startup + port: 8090 + periodSeconds: 1 + timeoutSeconds: 5 + failureThreshold: 20 + ``` + +2. Add `-use_http_health_check` and `-health-check-port` (optional) to your proxy container configuration under `command: `. + > [proxy_with_http_health_check.yaml](proxy_with_http_health_check.yaml#L39-L55) + ```yaml + command: + - "/cloud_sql_proxy" + + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "-ip_address_types=PRIVATE" + + # Replace DB_PORT with the port the proxy should listen on + # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 + - "-instances==tcp:" + # Enables HTTP health checks. + - "-use_http_health_check" + # Specifies the health check server port. + # Defaults to 8090. + - "-health_check_port=" + # This flag specifies where the service account key can be found + - "-credential_file=/secrets/service_account.json" + ``` + diff --git a/examples/k8s-health-check/proxy_with_http_health_check.yaml b/examples/k8s-health-check/proxy_with_http_health_check.yaml new file mode 100644 index 0000000000..d8ff78ad50 --- /dev/null +++ b/examples/k8s-health-check/proxy_with_http_health_check.yaml @@ -0,0 +1,135 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# You must configure probes in your deployment to use health checks in Kubernetes. +# This sample configuration for HTTP probes is adapted from proxy_with_workload_identity.yaml. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + containers: + - name: + # ... other container configuration + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + - name: cloud-sql-proxy + # It is recommended to use the latest version of the Cloud SQL proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version + command: + - "/cloud_sql_proxy" + + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "-ip_address_types=PRIVATE" + + # Replace DB_PORT with the port the proxy should listen on + # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 + - "-instances==tcp:" + # Enables HTTP health checks. + - "-use_http_health_check" + # Specifies the health check server port. + # Defaults to 8090. + - "-health_check_port=" + # This flag specifies where the service account key can be found + - "-credential_file=/secrets/service_account.json" + securityContext: + # The default Cloud SQL proxy image runs as the + # "nonroot" user and group (uid: 65532) by default. + runAsNonRoot: true + volumeMounts: + - name: + mountPath: /secrets/ + readOnly: true + # Resource configuration depends on an application's requirements. You + # should adjust the following values based on what your application + # needs. For details, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + requests: + # The proxy's memory use scales linearly with the number of active + # connections. Fewer open connections will use less memory. Adjust + # this value based on your application's requirements. + memory: "2Gi" + # The proxy's CPU use scales linearly with the amount of IO between + # the database and the application. Adjust this value based on your + # application's requirements. + cpu: "1" + # Recommended configurations for health check probes. + # Probe parameters can be adjusted to best fit the requirements of your application. + # For details, see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + httpGet: + path: /liveness + port: 8090 + # Number of seconds after the container has started before the first probe is scheduled. Defaults to 0. + # Not necessary when the startup probe is in use. + initialDelaySeconds: 0 + # Frequency of the probe. + periodSeconds: 60 + # Number of seconds after which the probe times out. + timeoutSeconds: 30 + # Number of times the probe is allowed to fail before the transition + # from healthy to failure state. + # + # If periodSeconds = 60, 5 tries will result in five minutes of + # checks. The proxy starts to refresh a certificate five minutes + # before its expiration. If those five minutes lapse without a + # successful refresh, the liveness probe will fail and the pod will be + # restarted. + failureThreshold: 5 + readinessProbe: + httpGet: + path: /readiness + port: 8090 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + # Number of times the probe must report success to transition from failure to healthy state. + # Defaults to 1 for readiness probe. + successThreshold: 1 + failureThreshold: 1 + startupProbe: + httpGet: + path: /startup + port: 8090 + periodSeconds: 1 + timeoutSeconds: 5 + failureThreshold: 20 + volumes: + - name: + secret: + secretName: diff --git a/examples/k8s-service/README.md b/examples/k8s-service/README.md new file mode 100644 index 0000000000..525de08d6f --- /dev/null +++ b/examples/k8s-service/README.md @@ -0,0 +1,277 @@ +# Running the Cloud SQL Proxy as a Service + +This example demonstrates how to run the Cloud SQL Auth Proxy with PgBouncer on +Kubernetes as a service. It assumes you have already successfully completed all +the steps in [Using the Cloud SQL Auth Proxy on Kubernetes][sidecar]. + +In this example, you will deploy [PgBouncer][] with the Cloud SQL Auth Proxy as +a sidecar, in addition to configuring encryption between the application and +PgBouncer. + +## A Word of Warning + +Running PgBouncer with the Cloud SQL Auth Proxy may pose a significant +operational burden and should be undertaken with caution given the attendant +complexity. + +In general, we recommend [running the proxy as a sidecar][sidecar] to your +application because it is simple, there is less overhead, it is secure out of +the box, and there is less latency involved. + +However, the service pattern is useful when you are at very large scale, when +you clearly need a database connection pooler, and when you are running into SQL +Admin API quota problems. + +## Initial Setup + +Before we deploy PgBouncer with the Cloud SQL Auth Proxy, there are three +initial steps to take. + +### Generate Certificates for PgBouncer + +First, you will need to generate certificates to encrypt the connection between +the application and PgBouncer. We recommend using [CFSSL][] to handle +certificate generation. Note: this example uses self-signed certificates. In +some cases, using a certificate signed by a public certificate authority may be +preferred. Alternatively, Kubernetes includes [an API for issuing +certificates][k8s-tls]. See the documentation on +[certificates][certificate-docs] for more details. + +The certificate signing request is encoded as JSON in +[`ca_csr.json`](ca_csr.json) for the certificate authority and in +[`server_csr.json`](server_csr.json) for the "server," here PgBouncer. + +First, we initialize our certificate authority. + +``` shell +# This step produces ca-key.pem (the CA private key) +# and ca.pem (the CA certificate). +cfssl genkey -initca ca_csr.json | cfssljson -bare ca +``` + +Next, we generate a public and private key for the server. These will be what +we will use to encrypt traffic from the application to PgBouncer. + +``` shell +# This step produces server-key.pem (the server private key) +# and server.pem (the server certicate). +cfssl gencert -ca cert -ca-key key server_csr.json | cfssljson -bare server +``` + +### Save the certificates as secrets + +Second, with all the necessary certificates generated, we will save them as +secrets: + +``` shell +# First the CA cert +kubectl create secret tls --key="ca-key.pem" --cert="ca.pem" + +# Next the server cert +kubectl create secret tls --key="server-key.pem" \ + --cert="server.pem" +``` + +### Containerize PgBouncer + +Third, we will containerize PgBouncer. Some users may prefer to containerize +PgBouncer themselves. For this example, we will make use of an open source +container, [edoburu/pgbouncer][edoburu]. One nice benefit of `edoburu/pgbouncer` +is that it will generate all the PgBouncer configuration based on environment +variables passed to the container. + +## Deploy PgBouncer as a Service + +With PgBouncer containerized, we will now create a deployment with PgBouncer and +the proxy as a sidecar. + +First, we mount our CA certificate and server certificate and private key, +renaming the certificate secrets to `cert.pem` and server private key to +`key.pem`: + +> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L15-L29) + +``` yaml +volumes: +- name: cacert + secret: + secretName: + items: + - key: tls.crt + path: cert.pem +- name: servercert + secret: + secretName: + items: + - key: tls.crt + path: cert.pem + - key: tls.key + path: key.pem +``` + +Next, we specify volume mounts in our PgBouncer container where the secrets will +be stored: + +> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L31-L41) + +``` yaml +- name: pgbouncer + image: + ports: + - containerPort: 5432 + volumeMounts: + - name: cacert + mountPath: "/etc/ca" + readOnly: true + - name: servercert + mountPath: "/etc/server" + readOnly: true +``` + +Then we configure PgBouncer through environment variables. Note: we use 5431 for +`DB_PORT` to leave 5432 available. + +> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L42-L69) + +``` yaml +env: +- name: DB_HOST + value: "127.0.0.1" +- name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username +- name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: + key: password +- name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database +- name: DB_PORT + value: "5431" +- name: CLIENT_TLS_SSLMODE + value: "require" +- name: CLIENT_TLS_CA_FILE + value: "/etc/ca/cert.pem" +- name: CLIENT_TLS_KEY_FILE + value: "/etc/server/key.pem" +- name: CLIENT_TLS_CERT_FILE + value: "/etc/server/cert.pem" +``` + +For the PgBouncer deployment, we add the proxy as a sidecar, starting it on port +5431: + +> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L70-L76) + +``` yaml +- name: cloud-sql-proxy + image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version + command: + - "/cloud_sql_proxy" + - "-instances==tcp:5431" + securityContext: + runAsNonRoot: true +``` + +Next, we create a PgBouncer service, listening on port 5342: + +> [`pgbouncer_service.yaml`](pgbouncer_service.yaml#L1-L11) + +``` yaml +apiVersion: v1 +kind: Service +metadata: + name: +spec: + selector: + app: + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 +``` + +With the PgBouncer service and deployment done, we are ready to point our +application at it. + +## Configure your application + +First, we configure a volume for the CA certificate, mapping the file name to +`cert.pem`. + +> [`deployment.yaml`](deployment.yaml#L1-L11) + +``` yaml +volumes: +- name: cacert + secret: + secretName: + items: + - key: tls.crt + path: cert.pem +``` + +Next, we mount the volume within the application container: + +> [`deployment.yaml`](deployment.yaml#L28-L31) + +``` yaml +volumeMounts: +- name: cacert + mountPath: "/etc/ca" + readOnly: true +``` + +Then, we configure environment variables for connecting to the database, this +time including a `CA_CERT`: + +> [`deployment.yaml`](deployment.yaml#L32-L53) + +``` yaml +env: +- name: DB_HOST + value: ".default.svc.cluster.local" # using the "default" namespace +- name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username +- name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password +- name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database +- name: DB_PORT + value: "5432" +- name: CA_CERT + value: "/etc/ca/cert.pem" +``` + +Note: now the `DB_HOST` value uses an internal DNS record pointing at the +PgBouncer service. + +Finally, when configuring a database connection string, the application must +provide the additional properties: + +1. `sslmode` must be set to at least `verify-ca` +1. `sslrootcert` must set to the environment variable `CA_CERT` + + +[certificate-docs]: https://kubernetes.io/docs/tasks/administer-cluster/certificates/ +[CFSSL]: https://github.com/cloudflare/cfssl +[edoburu]: https://hub.docker.com/r/edoburu/pgbouncer +[sidecar]: ../k8s-sidecar/README.md +[k8s-tls]: https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ +[PgBouncer]: https://www.pgbouncer.org + diff --git a/examples/k8s-service/ca_csr.json b/examples/k8s-service/ca_csr.json new file mode 100644 index 0000000000..9bb69a34d6 --- /dev/null +++ b/examples/k8s-service/ca_csr.json @@ -0,0 +1,16 @@ +{ + "hosts": [], + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "US", + "L": "Boulder", + "O": "My Cool Self-Signing Certificate Authority", + "OU": "WWW", + "ST": "Colorado" + } + ] +} diff --git a/examples/k8s-service/deployment.yaml b/examples/k8s-service/deployment.yaml new file mode 100644 index 0000000000..c09c1532f4 --- /dev/null +++ b/examples/k8s-service/deployment.yaml @@ -0,0 +1,67 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: +spec: + replicas: 5 + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + serviceAccountName: + volumes: + - name: cacert + secret: + secretName: + items: + - key: tls.crt + path: cert.pem + containers: + - name: + image: + ports: + - containerPort: 8080 + volumeMounts: + - name: cacert + mountPath: "/etc/ca" + readOnly: true + env: + - name: DB_HOST + value: ".default.svc.cluster.local" # using the "default" namespace + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + - name: DB_PORT + value: "5432" + - name: CA_CERT + value: "/etc/ca/cert.pem" diff --git a/examples/k8s-service/pgbouncer_deployment.yaml b/examples/k8s-service/pgbouncer_deployment.yaml new file mode 100644 index 0000000000..5490ea6004 --- /dev/null +++ b/examples/k8s-service/pgbouncer_deployment.yaml @@ -0,0 +1,90 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + serviceAccountName: + volumes: + - name: cacert + secret: + secretName: + items: + - key: tls.crt + path: cert.pem + - name: servercert + secret: + secretName: + items: + - key: tls.crt + path: cert.pem + - key: tls.key + path: key.pem + containers: + - name: pgbouncer + image: + ports: + - containerPort: 5432 + volumeMounts: + - name: cacert + mountPath: "/etc/ca" + readOnly: true + - name: servercert + mountPath: "/etc/server" + readOnly: true + env: + - name: DB_HOST + value: "127.0.0.1" + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + - name: DB_PORT + value: "5431" + - name: CLIENT_TLS_SSLMODE + value: "require" + - name: CLIENT_TLS_CA_FILE + value: "/etc/ca/cert.pem" + - name: CLIENT_TLS_KEY_FILE + value: "/etc/server/key.pem" + - name: CLIENT_TLS_CERT_FILE + value: "/etc/server/cert.pem" + - name: cloud-sql-proxy + image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure to use the latest version + command: + - "/cloud_sql_proxy" + - "-instances==tcp:5431" + securityContext: + runAsNonRoot: true diff --git a/examples/k8s-service/pgbouncer_service.yaml b/examples/k8s-service/pgbouncer_service.yaml new file mode 100644 index 0000000000..429a1ccaab --- /dev/null +++ b/examples/k8s-service/pgbouncer_service.yaml @@ -0,0 +1,25 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: Service +metadata: + name: +spec: + selector: + app: + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 diff --git a/examples/k8s-service/server_csr.json b/examples/k8s-service/server_csr.json new file mode 100644 index 0000000000..5f6a4735f9 --- /dev/null +++ b/examples/k8s-service/server_csr.json @@ -0,0 +1,19 @@ +{ + "hosts": [ + "pgbouncersvc.default.svc.cluster.local", + "localhost" + ], + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "US", + "L": "Boulder", + "O": "My Cool Kubernetes Cluster", + "OU": "WWW", + "ST": "Colorado" + } + ] +} diff --git a/examples/k8s-sidecar/README.md b/examples/k8s-sidecar/README.md new file mode 100644 index 0000000000..faed917539 --- /dev/null +++ b/examples/k8s-sidecar/README.md @@ -0,0 +1,258 @@ +# Using the Cloud SQL proxy on Kubernetes + +The Cloud SQL proxy is the recommended way to connect to Cloud SQL, even when +using private IP. This is because the proxy provides strong encryption and +authentication using IAM, which help keep your database secure. + +## Configure your application with Secrets + +In Kubernetes, [Secrets][ksa-secret] are a secure way to pass configuration +details to your application. Each Secret object can contain multiple key/value +pairs that can be pass to your application in multiple ways. When connecting to +a database, you can create a Secret with details such as your database name, +user, and password which can be injected into your application as env vars. + +1. Create a secret with information needed to access your database: + ```shell + kubectl create secret generic \ + --from-literal=username= \ + --from-literal=password= \ + --from-literal=database= + ``` +2. Next, configure your application's container to mount the secrets as env + vars: + > [proxy_with_workload_identity.yaml](proxy_with_workload_identity.yaml#L21-L36) + ```yaml + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + ``` +3. Finally, configure your application to use these values. In the example +above, the values will be in the env vars `DB_USER`, `DB_PASS`, and `DB_NAME`. + +[ksa-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ + +## Setting up a service account + +The first step to running the Cloud SQL proxy in Kubernetes is creating a +service account to represent your application. It is recommended that you create +a service account unique to each application, instead of using the same service +account everywhere. This model is more secure since it allows your to limit +permissions on a per-application basis. + +The service account for your application needs to meet the following criteria: + +1. Belong to a project with the [Cloud SQL Admin API][admin-api] enabled +1. [Has been granted][grant-sa] the + [`Cloud SQL Client` IAM role (or equivalent)][csql-roles] + for the project containing the instance you want to connect to +1. If connecting using private IP, you must use a + [VPC-native GKE cluster][vpc-gke], in the same VPC as your Cloud SQL instance + +[admin-api]: https://console.cloud.google.com/flows/enableapi?apiid=sqladmin&redirect=https://console.cloud.google.com +[grant-sa]: https://cloud.google.com/iam/docs/granting-roles-to-service-accounts +[csql-roles]: https://cloud.google.com/iam/docs/understanding-roles#cloud-sql-roles +[vpc-gke]: https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips + +## Providing the service account to the proxy + +Next, you need to configure Kubernetes to provide the service account to the +Cloud SQL Auth proxy. There are two recommended ways to do this. + +### Workload Identity + +If you are using [Google Kubernetes Engine][gke], the preferred method is to +use GKE's [Workload Identity][workload-id] feature. This method allows you to +bind a [Kubernetes Service Account (KSA)][ksa] to a Google Service Account +(GSA). The GSA will then be accessible to applications using the matching KSA. + +1. [Enable Workload Identity for your cluster][enable-wi] +1. [Enable Workload Identity for your node pool][enable-wi-node-pool] +1. Create a KSA for your application `kubectl apply -f service-account.yaml`: + + > [service-account.yaml](service_account.yaml#L2-L5) + ```yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: # TODO(developer): replace these values + ``` +1. Enable the IAM binding between your `` and ``: + ```sh + gcloud iam service-accounts add-iam-policy-binding \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:.svc.id.goog[/]" \ + @.iam.gserviceaccount.com + ``` +1. Add an annotation to `` to complete the binding: + ```sh + kubectl annotate serviceaccount \ + \ + iam.gke.io/gcp-service-account=@.iam.gserviceaccount.com + ``` +1. Finally, make sure to specify the service account for the k8s pod spec: + > [proxy_with_workload_identity.yaml](proxy_with_workload_identity.yaml#L2-L15) + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: + spec: + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + serviceAccountName: + ``` + +[gke]: https://cloud.google.com/kubernetes-engine +[workload-id]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity +[ksa]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ +[enable-wi]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable_on_existing_cluster +[enable-wi-node-pool]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#option_2_node_pool_modification + +### Service account key file + +Alternatively, if your can't use Workload Identity, the recommended pattern is +to mount a service account key file into the Cloud SQL proxy pod and use the +`-credential_file` flag. + +1. Create a credential file for your service account key: + ```sh + gcloud iam service-accounts keys create ~/key.json \ + --iam-account @project-id.iam.gserviceaccount.com + ``` +1. Turn your service account key into a k8s [Secret][k8s-secret]: + ```shell + kubectl create secret generic \ + --from-file=service_account.json=~/key.json + ``` +3. Mount the secret as a volume under the`spec:` for your k8s object: + > [proxy_with_sa_key.yaml](proxy_with_sa_key.yaml#L74-L77) + ```yaml + volumes: + - name: + secret: + secretName: + ``` + +4. Follow the instructions in the next section to access the volume from the + proxy's pod. + +[k8s-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ + +## Run the Cloud SQL proxy as a sidecar + +We recommend running the proxy in a "sidecar" pattern (as an additional +container sharing a pod with your application). We recommend this over running +as a separate service for several reasons: + +* Prevents your SQL traffic from being exposed locally - the proxy provides + encryption on outgoing connections, but you should limit exposure for + incoming connections +* Prevents a single point of failure - each application's access to + your database is independent from the others, making it more resilient. +* Limits access to the proxy, allowing you to use IAM permissions per + application rather than exposing the database to the entire cluster +* Allows you to scope resource requests more accurately - because the + proxy consumes resources linearly to usage, this pattern allows you to more + accurately scope and request resources to match your applications as it + scales + +1. Add the Cloud SQL proxy to the pod configuration under `containers`: + > [proxy_with_workload-identity.yaml](proxy_with_workload_identity.yaml#L39-L69) + ```yaml + - name: cloud-sql-proxy + # It is recommended to use the latest version of the Cloud SQL proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version + command: + - "/cloud_sql_proxy" + + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "-ip_address_types=PRIVATE" + + # Replace DB_PORT with the port the proxy should listen on + # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 + - "-instances==tcp:" + securityContext: + # The default Cloud SQL proxy image runs as the + # "nonroot" user and group (uid: 65532) by default. + runAsNonRoot: true + # Resource configuration depends on an application's requirements. You + # should adjust the following values based on what your application + # needs. For details, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + requests: + # The proxy's memory use scales linearly with the number of active + # connections. Fewer open connections will use less memory. Adjust + # this value based on your application's requirements. + memory: "2Gi" + # The proxy's CPU use scales linearly with the amount of IO between + # the database and the application. Adjust this value based on your + # application's requirements. + cpu: "1" + ``` + If you are using a service account key, specify your secret volume and add + the `-credential_file` flag to the command: + + > [proxy_with_sa_key.yaml](proxy_with_sa_key.yaml#L49-L58) + ```yaml + # This flag specifies where the service account key can be found + - "-credential_file=/secrets/service_account.json" + securityContext: + # The default Cloud SQL proxy image runs as the + # "nonroot" user and group (uid: 65532) by default. + runAsNonRoot: true + volumeMounts: + - name: + mountPath: /secrets/ + readOnly: true + ``` + +1. Finally, configure your application to connect via `127.0.0.1` on whichever + `` you specified in the command section. + + +## Connecting without the Cloud SQL proxy + +While not as secure, it is possible to connect from a VPC-native GKE cluster to +a Cloud SQL instance on the same VPC using private IP without the proxy. + +1. Create a secret with your instance's private IP address: + ```shell + kubectl create secret generic \ + --from-literal=db_host= + ``` + +2. Next make sure you add the secret to your application's container: + > [no_proxy_private_ip.yaml](no_proxy_private_ip.yaml#L34-L38) + ```yaml + - name: DB_HOST + valueFrom: + secretKeyRef: + name: + key: db_host + ``` + +3. Finally, configure your application to connect using the IP address from the + `DB_HOST` env var. You will need to use the correct port for your db-engine + (MySQL: `3306`, Postgres: `5432`, SQLServer: `1433`). diff --git a/examples/k8s-sidecar/no_proxy_private_ip.yaml b/examples/k8s-sidecar/no_proxy_private_ip.yaml new file mode 100644 index 0000000000..3a18438b9f --- /dev/null +++ b/examples/k8s-sidecar/no_proxy_private_ip.yaml @@ -0,0 +1,53 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + containers: + - name: + # ... other container configuration + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + # [START cloud_sql_proxy_secret_host] + - name: DB_HOST + valueFrom: + secretKeyRef: + name: + key: db_host + # [END cloud_sql_proxy_secret_host] diff --git a/examples/k8s-sidecar/proxy_with_sa_key.yaml b/examples/k8s-sidecar/proxy_with_sa_key.yaml new file mode 100644 index 0000000000..fd71ad05ca --- /dev/null +++ b/examples/k8s-sidecar/proxy_with_sa_key.yaml @@ -0,0 +1,98 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + containers: + - name: + # ... other container configuration + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + - name: cloud-sql-proxy + # It is recommended to use the latest version of the Cloud SQL proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version + command: + - "/cloud_sql_proxy" + + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "-ip_address_types=PRIVATE" + + # By default, the proxy will write all logs to stderr. In some + # environments, anything printed to stderr is consider an error. To + # disable this behavior and write all logs to stdout (except errors + # which will still go to stderr), use: + - "-log_debug_stdout" + + # Replace DB_PORT with the port the proxy should listen on + # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 + - "-instances==tcp:" + + # [START cloud_sql_proxy_k8s_volume_mount] + # This flag specifies where the service account key can be found + - "-credential_file=/secrets/service_account.json" + securityContext: + # The default Cloud SQL proxy image runs as the + # "nonroot" user and group (uid: 65532) by default. + runAsNonRoot: true + volumeMounts: + - name: + mountPath: /secrets/ + readOnly: true + # [END cloud_sql_proxy_k8s_volume_mount] + # Resource configuration depends on an application's requirements. You + # should adjust the following values based on what your application + # needs. For details, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + requests: + # The proxy's memory use scales linearly with the number of active + # connections. Fewer open connections will use less memory. Adjust + # this value based on your application's requirements. + memory: "2Gi" + # The proxy's CPU use scales linearly with the amount of IO between + # the database and the application. Adjust this value based on your + # application's requirements. + cpu: "1" + # [START cloud_sql_proxy_k8s_volume_secret] + volumes: + - name: + secret: + secretName: + # [END cloud_sql_proxy_k8s_volume_secret] diff --git a/examples/k8s-sidecar/proxy_with_workload_identity.yaml b/examples/k8s-sidecar/proxy_with_workload_identity.yaml new file mode 100644 index 0000000000..b9eaa3ee96 --- /dev/null +++ b/examples/k8s-sidecar/proxy_with_workload_identity.yaml @@ -0,0 +1,92 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START cloud_sql_proxy_k8s_sa] +apiVersion: apps/v1 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + template: + metadata: + labels: + app: + spec: + serviceAccountName: + # [END cloud_sql_proxy_k8s_sa] + # [START cloud_sql_proxy_k8s_secrets] + containers: + - name: + # ... other container configuration + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: + key: username + - name: DB_PASS + valueFrom: + secretKeyRef: + name: + key: password + - name: DB_NAME + valueFrom: + secretKeyRef: + name: + key: database + # [END cloud_sql_proxy_k8s_secrets] + # [START cloud_sql_proxy_k8s_container] + - name: cloud-sql-proxy + # It is recommended to use the latest version of the Cloud SQL proxy + # Make sure to update on a regular schedule! + image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version + command: + - "/cloud_sql_proxy" + + # If connecting from a VPC-native GKE cluster, you can use the + # following flag to have the proxy connect over private IP + # - "-ip_address_types=PRIVATE" + + # By default, the proxy will write all logs to stderr. In some + # environments, anything printed to stderr is consider an error. To + # disable this behavior and write all logs to stdout (except errors + # which will still go to stderr), use: + - "-log_debug_stdout" + + # Replace DB_PORT with the port the proxy should listen on + # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 + - "-instances==tcp:" + securityContext: + # The default Cloud SQL proxy image runs as the + # "nonroot" user and group (uid: 65532) by default. + runAsNonRoot: true + # You should use resource requests/limits as a best practice to prevent + # pods from consuming too many resources and affecting the execution of + # other pods. You should adjust the following values based on what your + # application needs. For details, see + # https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + requests: + # The proxy's memory use scales linearly with the number of active + # connections. Fewer open connections will use less memory. Adjust + # this value based on your application's requirements. + memory: "2Gi" + # The proxy's CPU use scales linearly with the amount of IO between + # the database and the application. Adjust this value based on your + # application's requirements. + cpu: "1" + # [END cloud_sql_proxy_k8s_container] diff --git a/examples/k8s-sidecar/service_account.yaml b/examples/k8s-sidecar/service_account.yaml new file mode 100644 index 0000000000..d66893229b --- /dev/null +++ b/examples/k8s-sidecar/service_account.yaml @@ -0,0 +1,20 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START cloud_sql_proxy_k8s_sa_yml] +apiVersion: v1 +kind: ServiceAccount +metadata: + name: # TODO(developer): replace these values +# [END cloud_sql_proxy_k8s_sa_yml] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..e0d14fa86b --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/GoogleCloudPlatform/cloudsql-proxy/v2 + +go 1.16 + +require ( + cloud.google.com/go/cloudsqlconn v0.2.1-0.20220401153611-87e713b37755 + cloud.google.com/go/compute v1.5.0 + github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 + github.com/coreos/go-systemd/v22 v22.3.2 + github.com/denisenkom/go-mssqldb v0.12.0 + github.com/go-sql-driver/mysql v1.6.0 + github.com/google/go-cmp v0.5.7 + github.com/hanwen/go-fuse/v2 v2.1.0 + github.com/jackc/pgx/v4 v4.15.0 + github.com/lib/pq v1.10.5 + github.com/spf13/cobra v1.2.1 + go.uber.org/zap v1.21.0 + golang.org/x/net v0.0.0-20220325170049-de3da57026de + golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a + golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 + golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 + google.golang.org/api v0.74.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000..89bd7fb980 --- /dev/null +++ b/go.sum @@ -0,0 +1,882 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +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 v0.2.1-0.20220401153611-87e713b37755 h1:74ZeQyxqrRSBoytWhFCDqScGNuF4PpCh7fe+BR9eX/I= +cloud.google.com/go/cloudsqlconn v0.2.1-0.20220401153611-87e713b37755/go.mod h1:y/ogms5BQof7JguwhcHsTPfskl7BFxXI8lvyXCQu/5E= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0 h1:YNu23BtH0PKF+fg3ykSorCp6jSTjcEtfnYLzbmcjVRA= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= +github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4= +github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0 h1:s7jOdKSaksJVOxE0Y/S32otcfiP+UQ0cL8/GTKaONwE= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= +github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= +github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= +github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ= +github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= +github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= +github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= +github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211020060615-d418f374d309/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.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +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.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.68.0/go.mod h1:sOM8pTpwgflXRhz+oC8H2Dr+UcbMqkPPWNJo88Q7TH8= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +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= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +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-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7 h1:HOL66YCI20JvN2hVk6o2YIp9i/3RvzVUz82PqNr7fXw= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +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= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go new file mode 100644 index 0000000000..93b7230926 --- /dev/null +++ b/internal/proxy/proxy.go @@ -0,0 +1,334 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package proxy + +import ( + "context" + "fmt" + "io" + "net" + "strings" + "sync" + "time" + + "cloud.google.com/go/cloudsqlconn" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql" + "github.com/spf13/cobra" + "golang.org/x/oauth2" +) + +// InstanceConnConfig holds the configuration for an individual instance +// connection. +type InstanceConnConfig struct { + // Name is the instance connection name. + Name string + // Addr is the address on which to bind a listener for the instance. + Addr string + // Port is the port on which to bind a listener for the instance. + Port int +} + +// Config contains all the configuration provided by the caller. +type Config struct { + // Token is the Bearer token used for authorization. + Token string + + // CredentialsFile is the path to a service account key. + CredentialsFile string + + // Addr is the address on which to bind all instances. + Addr string + + // Port is the initial port to bind to. Subsequent instances bind to + // increments from this value. + Port int + + // Instances are configuration for individual instances. Instance + // configuration takes precedence over global configuration. + Instances []InstanceConnConfig + + // Dialer specifies the dialer to use when connecting to Cloud SQL + // instances. + Dialer cloudsql.Dialer +} + +func (c *Config) DialerOpts() []cloudsqlconn.Option { + var opts []cloudsqlconn.Option + switch { + case c.Token != "": + opts = append(opts, cloudsqlconn.WithTokenSource( + oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token}), + )) + case c.CredentialsFile != "": + opts = append(opts, cloudsqlconn.WithCredentialsFile( + c.CredentialsFile, + )) + } + return opts +} + +type portConfig struct { + global int + postgres int + mysql int + sqlserver int +} + +func newPortConfig(global int) *portConfig { + return &portConfig{ + global: global, + postgres: 5432, + mysql: 3306, + sqlserver: 1433, + } +} + +// nextPort returns the next port based on the initial global value. +func (c *portConfig) nextPort() int { + p := c.global + c.global++ + return p +} + +func (c *portConfig) nextDBPort(version string) int { + switch { + case strings.HasPrefix(version, "MYSQL"): + p := c.mysql + c.mysql++ + return p + case strings.HasPrefix(version, "POSTGRES"): + p := c.postgres + c.postgres++ + return p + case strings.HasPrefix(version, "SQLSERVER"): + p := c.sqlserver + c.sqlserver++ + return p + default: + // Unexpected engine version, use global port setting instead. + return c.nextPort() + } +} + +// Client represents the state of the current instantiation of the proxy. +type Client struct { + cmd *cobra.Command + dialer cloudsql.Dialer + + // mnts is a list of all mounted sockets for this client + mnts []*socketMount +} + +// NewClient completes the initial setup required to get the proxy to a "steady" state. +func NewClient(ctx context.Context, d cloudsql.Dialer, cmd *cobra.Command, conf *Config) (*Client, error) { + var mnts []*socketMount + for _, inst := range conf.Instances { + go func(name string) { + // Initiate refresh operation + d.EngineVersion(ctx, name) + }(inst.Name) + } + pc := newPortConfig(conf.Port) + for _, inst := range conf.Instances { + m := &socketMount{inst: inst.Name} + a := conf.Addr + if inst.Addr != "" { + a = inst.Addr + } + version, err := d.EngineVersion(ctx, inst.Name) + if err != nil { + return nil, err + } + var np int + switch { + case inst.Port != 0: + np = inst.Port + case conf.Port != 0: + np = pc.nextPort() + default: + np = pc.nextDBPort(version) + } + addr, err := m.listen(ctx, "tcp", net.JoinHostPort(a, fmt.Sprint(np))) + if err != nil { + for _, m := range mnts { + m.close() + } + return nil, fmt.Errorf("[%v] Unable to mount socket: %v", inst.Name, err) + } + cmd.Printf("[%s] Listening on %s\n", inst.Name, addr.String()) + mnts = append(mnts, m) + } + return &Client{mnts: mnts, cmd: cmd, dialer: d}, nil +} + +// Serve listens on the mounted ports and beging proxying the connections to the instances. +func (c *Client) Serve(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + exitCh := make(chan error) + for _, m := range c.mnts { + go func(mnt *socketMount) { + err := c.serveSocketMount(ctx, mnt) + if err != nil { + select { + // Best effort attempt to send error. + // If this send fails, it means the reading goroutine has + // already pulled a value out of the channel and is no longer + // reading any more values. In other words, we report only the + // first error. + case exitCh <- err: + default: + return + } + } + }(m) + } + return <-exitCh +} + +// Close triggers the proxyClient to shutdown. +func (c *Client) Close() { + defer c.dialer.Close() + for _, m := range c.mnts { + m.close() + } +} + +// serveSocketMount persistently listens to the socketMounts listener and proxies connections to a +// given Cloud SQL instance. +func (c *Client) serveSocketMount(ctx context.Context, s *socketMount) error { + if s.listener == nil { + return fmt.Errorf("[%s] mount doesn't have a listener set", s.inst) + } + for { + cConn, err := s.listener.Accept() + if err != nil { + if nerr, ok := err.(net.Error); ok && nerr.Temporary() { + c.cmd.PrintErrf("[%s] Error accepting connection: %v\n", s.inst, err) + // For transient errors, wait a small amount of time to see if it resolves itself + time.Sleep(10 * time.Millisecond) + continue + } + return err + } + // handle the connection in a separate goroutine + go func() { + c.cmd.Printf("[%s] accepted connection from %s\n", s.inst, cConn.RemoteAddr()) + + // give a max of 30 seconds to connect to the instance + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + sConn, err := c.dialer.Dial(ctx, s.inst) + if err != nil { + c.cmd.Printf("[%s] failed to connect to instance: %v\n", s.inst, err) + cConn.Close() + return + } + c.proxyConn(s.inst, cConn, sConn) + }() + } +} + +// socketMount is a tcp/unix socket that listens for a Cloud SQL instance. +type socketMount struct { + inst string + listener net.Listener +} + +// listen causes a socketMount to create a Listener at the specified network address. +func (s *socketMount) listen(ctx context.Context, network string, host string) (net.Addr, error) { + lc := net.ListenConfig{KeepAlive: 30 * time.Second} + l, err := lc.Listen(ctx, network, host) + if err != nil { + return nil, err + } + s.listener = l + return s.listener.Addr(), nil +} + +// close stops the mount from listening for any more connections +func (s *socketMount) close() error { + err := s.listener.Close() + s.listener = nil + return err +} + +// proxyConn sets up a bidirectional copy between two open connections +func (c *Client) proxyConn(inst string, client, server net.Conn) { + // only allow the first side to give an error for terminating a connection + var o sync.Once + cleanup := func(errDesc string, isErr bool) { + o.Do(func() { + client.Close() + server.Close() + if isErr { + c.cmd.PrintErrln(errDesc) + } else { + c.cmd.Println(errDesc) + } + }) + } + + // copy bytes from client to server + go func() { + buf := make([]byte, 8*1024) // 8kb + for { + n, cErr := client.Read(buf) + var sErr error + if n > 0 { + _, sErr = server.Write(buf[:n]) + } + switch { + case cErr == io.EOF: + cleanup(fmt.Sprintf("[%s] client closed the connection", inst), false) + return + case cErr != nil: + cleanup(fmt.Sprintf("[%s] connection aborted - error reading from client: %v", inst, cErr), true) + return + case sErr == io.EOF: + cleanup(fmt.Sprintf("[%s] instance closed the connection", inst), false) + return + case sErr != nil: + cleanup(fmt.Sprintf("[%s] connection aborted - error writing to instance: %v", inst, cErr), true) + return + } + } + }() + + // copy bytes from server to client + buf := make([]byte, 8*1024) // 8kb + for { + n, sErr := server.Read(buf) + var cErr error + if n > 0 { + _, cErr = client.Write(buf[:n]) + } + switch { + case sErr == io.EOF: + cleanup(fmt.Sprintf("[%s] instance closed the connection", inst), false) + return + case sErr != nil: + cleanup(fmt.Sprintf("[%s] connection aborted - error reading from instance: %v", inst, sErr), true) + return + case cErr == io.EOF: + cleanup(fmt.Sprintf("[%s] client closed the connection", inst), false) + return + case cErr != nil: + cleanup(fmt.Sprintf("[%s] connection aborted - error writing to client: %v", inst, sErr), true) + return + } + } +} diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go new file mode 100644 index 0000000000..34a490dd29 --- /dev/null +++ b/internal/proxy/proxy_test.go @@ -0,0 +1,169 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package proxy_test + +import ( + "context" + "net" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cloudsql" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/internal/proxy" + "github.com/spf13/cobra" +) + +type fakeDialer struct { + cloudsql.Dialer +} + +func (fakeDialer) Close() error { + return nil +} + +func (fakeDialer) EngineVersion(_ context.Context, inst string) (string, error) { + switch { + case strings.Contains(inst, "pg"): + return "POSTGRES_14", nil + case strings.Contains(inst, "mysql"): + return "MYSQL_8_0", nil + case strings.Contains(inst, "sqlserver"): + return "SQLSERVER_2019_STANDARD", nil + default: + return "POSTGRES_14", nil + } +} + +func TestClientInitialization(t *testing.T) { + ctx := context.Background() + pg := "proj:region:pg" + pg2 := "proj:region:pg2" + mysql := "proj:region:mysql" + mysql2 := "proj:region:mysql2" + sqlserver := "proj:region:sqlserver" + sqlserver2 := "proj:region:sqlserver2" + + tcs := []struct { + desc string + in *proxy.Config + wantAddrs []string + }{ + { + desc: "multiple instances", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 5000, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + {Name: mysql}, + {Name: sqlserver}, + }, + }, + wantAddrs: []string{"127.0.0.1:5000", "127.0.0.1:5001", "127.0.0.1:5002"}, + }, + { + desc: "with instance address", + in: &proxy.Config{ + Addr: "1.1.1.1", // bad address, binding shouldn't happen here. + Port: 5000, + Instances: []proxy.InstanceConnConfig{ + {Addr: "0.0.0.0", Name: pg}, + }, + }, + wantAddrs: []string{"0.0.0.0:5000"}, + }, + { + desc: "IPv6 support", + in: &proxy.Config{ + Addr: "::1", + Port: 5000, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + }, + }, + wantAddrs: []string{"[::1]:5000"}, + }, + { + desc: "with instance port", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 5000, + Instances: []proxy.InstanceConnConfig{ + {Name: pg, Port: 6000}, + }, + }, + wantAddrs: []string{"127.0.0.1:6000"}, + }, + { + desc: "with global port and instance port", + in: &proxy.Config{ + Addr: "127.0.0.1", + Port: 5000, + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + {Name: mysql, Port: 6000}, + {Name: sqlserver}, + }, + }, + wantAddrs: []string{ + "127.0.0.1:5000", + "127.0.0.1:6000", + "127.0.0.1:5001", + }, + }, + { + desc: "with incrementing automatic port selection", + in: &proxy.Config{ + Addr: "127.0.0.1", + Instances: []proxy.InstanceConnConfig{ + {Name: pg}, + {Name: pg2}, + {Name: mysql}, + {Name: mysql2}, + {Name: sqlserver}, + {Name: sqlserver2}, + }, + }, + wantAddrs: []string{ + "127.0.0.1:5432", + "127.0.0.1:5433", + "127.0.0.1:3306", + "127.0.0.1:3307", + "127.0.0.1:1433", + "127.0.0.1:1434", + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + c, err := proxy.NewClient(ctx, fakeDialer{}, &cobra.Command{}, tc.in) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + defer c.Close() + for _, addr := range tc.wantAddrs { + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("want error = nil, got = %v", err) + } + err = conn.Close() + if err != nil { + t.Logf("failed to close connection: %v", err) + } + } + }) + } +} diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000000..ec6d20f8c9 --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,106 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package logging contains helpers to support log messages. If you are using +// the Cloud SQL Auth proxy as a Go library, you can override these variables to +// control where log messages end up. +package logging + +import ( + "log" + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Verbosef is called to write verbose logs, such as when a new connection is +// established correctly. +var Verbosef = log.Printf + +// Infof is called to write informational logs, such as when startup has +var Infof = log.Printf + +// Errorf is called to write an error log, such as when a new connection fails. +var Errorf = log.Printf + +// LogDebugToStdout updates Verbosef and Info logging to use stdout instead of stderr. +func LogDebugToStdout() { + logger := log.New(os.Stdout, "", log.LstdFlags) + Verbosef = logger.Printf + Infof = logger.Printf +} + +func noop(string, ...interface{}) {} + +// LogVerboseToNowhere updates Verbosef so verbose log messages are discarded +func LogVerboseToNowhere() { + Verbosef = noop +} + +// DisableLogging sets all logging levels to no-op's. +func DisableLogging() { + Verbosef = noop + Infof = noop + Errorf = noop +} + +// EnableStructuredLogs replaces all logging functions with structured logging +// variants. +func EnableStructuredLogs(logDebugStdout, verbose bool) (func(), error) { + // Configuration of zap is based on its Advanced Configuration example. + // See: https://pkg.go.dev/go.uber.org/zap#example-package-AdvancedConfiguration + + // Define level-handling logic. + highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.ErrorLevel + }) + lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl < zapcore.ErrorLevel + }) + + // Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In + // particular, *os.File types must be locked before use. + consoleErrors := zapcore.Lock(os.Stderr) + consoleDebugging := consoleErrors + if logDebugStdout { + consoleDebugging = zapcore.Lock(os.Stdout) + } + + config := zap.NewProductionEncoderConfig() + config.LevelKey = "severity" + config.MessageKey = "message" + config.TimeKey = "timestamp" + config.EncodeLevel = zapcore.CapitalLevelEncoder + config.EncodeTime = zapcore.ISO8601TimeEncoder + consoleEncoder := zapcore.NewJSONEncoder(config) + core := zapcore.NewTee( + zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), + zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), + ) + // By default, caller and stacktrace are not included, so add them here + logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + + sugar := logger.Sugar() + Verbosef = sugar.Infof + if !verbose { + Verbosef = noop + } + Infof = sugar.Infof + Errorf = sugar.Errorf + + return func() { + logger.Sync() + }, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000000..03867e9574 --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +// Copyright 2021 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 0000000000..1d0c8ea95f --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,33 @@ +# Cloud SQL proxy dialer for Go + +You can also use the Cloud SQL proxy directly from a Go program. + +These packages are primarily used as implementation for the Cloud SQL proxy +command, and may be changed in backwards incompatible ways in the future. + +## Usage + +If your program is written in [Go](https://golang.org) you can use the Cloud SQL +Proxy as a library, avoiding the need to start the Proxy as a companion process. + +Alternatively, there are Cloud SQL Connectors for [Java][] and [Python][]. + + +### MySQL + +If you're using the MySQL [go-sql-driver][go-mysql] you can use helper +functions found in the [`proxy/dialers/mysql`][mysql-godoc] + +See [example usage](dialers/mysql/hook_test.go). + +### Postgres + +If you're using the Postgres [lib/pq](https://github.com/lib/pq), you can +use the `cloudsqlpostgres` driver from [here](proxy/dialers/postgres). + +See [example usage](dialers/postgres/hook_test.go). + +[Java]: https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory +[Python]: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector +[go-mysql]: https://github.com/go-sql-driver/mysql +[mysql-godoc]: https://pkg.go.dev/github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql diff --git a/proxy/certs/certs.go b/proxy/certs/certs.go new file mode 100644 index 0000000000..892cf2964e --- /dev/null +++ b/proxy/certs/certs.go @@ -0,0 +1,365 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package certs implements a CertSource which speaks to the public Cloud SQL API endpoint. +package certs + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "math" + mrand "math/rand" + "net/http" + "strings" + "sync" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/util" + "golang.org/x/oauth2" + "google.golang.org/api/googleapi" + sqladmin "google.golang.org/api/sqladmin/v1beta4" +) + +const defaultUserAgent = "custom cloud_sql_proxy version >= 1.10" + +// NewCertSource returns a CertSource which can be used to authenticate using +// the provided client, which must not be nil. +// +// This function is deprecated; use NewCertSourceOpts instead. +func NewCertSource(host string, c *http.Client, checkRegion bool) *RemoteCertSource { + return NewCertSourceOpts(c, RemoteOpts{ + APIBasePath: host, + IgnoreRegion: !checkRegion, + UserAgent: defaultUserAgent, + }) +} + +// RemoteOpts are a collection of options for NewCertSourceOpts. All fields are +// optional. +type RemoteOpts struct { + // APIBasePath specifies the base path for the sqladmin API. If left blank, + // the default from the autogenerated sqladmin library is used (which is + // sufficient for nearly all users) + APIBasePath string + + // IgnoreRegion specifies whether a missing or mismatched region in the + // instance name should be ignored. In a future version this value will be + // forced to 'false' by the RemoteCertSource. + IgnoreRegion bool + + // A string for the RemoteCertSource to identify itself when contacting the + // sqladmin API. + UserAgent string + + // IP address type options + IPAddrTypeOpts []string + + // Enable IAM proxy db authentication + EnableIAMLogin bool + + // Token source for token information used in cert creation + TokenSource oauth2.TokenSource + + // DelayKeyGenerate, if true, causes the RSA key to be generated lazily + // on the first connection to a database. The default behavior is to generate + // the key when the CertSource is created. + DelayKeyGenerate bool +} + +// NewCertSourceOpts returns a CertSource configured with the provided Opts. +// The provided http.Client must not be nil. +// +// Use this function instead of NewCertSource; it has a more forward-compatible +// signature. +func NewCertSourceOpts(c *http.Client, opts RemoteOpts) *RemoteCertSource { + serv, err := sqladmin.New(c) + if err != nil { + panic(err) // Only will happen if the provided client is nil. + } + if opts.APIBasePath != "" { + serv.BasePath = opts.APIBasePath + } + ua := opts.UserAgent + if ua == "" { + ua = defaultUserAgent + } + serv.UserAgent = ua + + // Set default value to be "PUBLIC,PRIVATE" if not specified + if len(opts.IPAddrTypeOpts) == 0 { + opts.IPAddrTypeOpts = []string{"PUBLIC", "PRIVATE"} + } + + // Add "PUBLIC" as an alias for "PRIMARY" + for index, ipAddressType := range opts.IPAddrTypeOpts { + if strings.ToUpper(ipAddressType) == "PUBLIC" { + opts.IPAddrTypeOpts[index] = "PRIMARY" + } + } + + certSource := &RemoteCertSource{ + serv: serv, + checkRegion: !opts.IgnoreRegion, + IPAddrTypes: opts.IPAddrTypeOpts, + EnableIAMLogin: opts.EnableIAMLogin, + TokenSource: opts.TokenSource, + } + if !opts.DelayKeyGenerate { + // Generate the RSA key now, but don't block on it. + go certSource.generateKey() + } + + return certSource +} + +// RemoteCertSource implements a CertSource, using Cloud SQL APIs to +// return Local certificates for identifying oneself as a specific user +// to the remote instance and Remote certificates for confirming the +// remote database's identity. +type RemoteCertSource struct { + // keyOnce is used to create `key` lazily. + keyOnce sync.Once + // key is the private key used for certificates returned by Local. + key *rsa.PrivateKey + // serv is used to make authenticated API calls to Cloud SQL. + serv *sqladmin.Service + // If set, providing an incorrect region in their connection string will be + // treated as an error. This is to provide the same functionality that will + // occur when API calls require the region. + checkRegion bool + // a list of ip address types that users select + IPAddrTypes []string + // flag to enable IAM proxy db authentication + EnableIAMLogin bool + // token source for the token information used in cert creation + TokenSource oauth2.TokenSource +} + +// Constants for backoffAPIRetry. These cause the retry logic to scale the +// backoff delay from 200ms to around 3.5s. +const ( + baseBackoff = float64(200 * time.Millisecond) + backoffMult = 1.618 + backoffRetries = 5 +) + +func backoffAPIRetry(desc, instance string, do func(staleRead time.Time) error) error { + var ( + err error + t time.Time + ) + for i := 0; i < backoffRetries; i++ { + err = do(t) + gErr, ok := err.(*googleapi.Error) + switch { + case !ok: + // 'ok' will also be false if err is nil. + return err + case gErr.Code == 403 && len(gErr.Errors) > 0 && gErr.Errors[0].Reason == "insufficientPermissions": + // The case where the admin API has not yet been enabled. + return fmt.Errorf("ensure that the Cloud SQL API is enabled for your project (https://console.cloud.google.com/flows/enableapi?apiid=sqladmin). Error during %s %s: %v", desc, instance, err) + case gErr.Code == 404 || gErr.Code == 403: + return fmt.Errorf("ensure that the account has access to %q (and make sure there's no typo in that name). Error during %s %s: %v", instance, desc, instance, err) + case gErr.Code < 500: + // Only Server-level HTTP errors are immediately retryable. + return err + } + + // sleep = baseBackoff * backoffMult^(retries + randomFactor) + exp := float64(i+1) + mrand.Float64() + sleep := time.Duration(baseBackoff * math.Pow(backoffMult, exp)) + logging.Errorf("Error in %s %s: %v; retrying in %v", desc, instance, err, sleep) + time.Sleep(sleep) + // Create timestamp 30 seconds before now for stale read requests + t = time.Now().UTC().Add(-30 * time.Second) + } + return err +} + +func refreshToken(ts oauth2.TokenSource, tok *oauth2.Token) (*oauth2.Token, error) { + expiredToken := &oauth2.Token{ + AccessToken: tok.AccessToken, + TokenType: tok.TokenType, + RefreshToken: tok.RefreshToken, + Expiry: time.Time{}.Add(1), // Expired + } + return oauth2.ReuseTokenSource(expiredToken, ts).Token() +} + +// Local returns a certificate that may be used to establish a TLS +// connection to the specified instance. +func (s *RemoteCertSource) Local(instance string) (tls.Certificate, error) { + pkix, err := x509.MarshalPKIXPublicKey(s.generateKey().Public()) + if err != nil { + return tls.Certificate{}, err + } + + p, r, n := util.SplitName(instance) + regionName := fmt.Sprintf("%s~%s", r, n) + pubKey := string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"})) + generateEphemeralCertRequest := &sqladmin.GenerateEphemeralCertRequest{ + PublicKey: pubKey, + } + var tok *oauth2.Token + // If IAM login is enabled, add the OAuth2 token into the ephemeral + // certificate request. + if s.EnableIAMLogin { + var tokErr error + tok, tokErr = s.TokenSource.Token() + if tokErr != nil { + return tls.Certificate{}, tokErr + } + // Always refresh the token to ensure its expiration is far enough in + // the future. + tok, tokErr = refreshToken(s.TokenSource, tok) + if tokErr != nil { + return tls.Certificate{}, tokErr + } + generateEphemeralCertRequest.AccessToken = tok.AccessToken + } + req := s.serv.Connect.GenerateEphemeralCert(p, regionName, generateEphemeralCertRequest) + + var data *sqladmin.GenerateEphemeralCertResponse + err = backoffAPIRetry("generateEphemeral for", instance, func(staleRead time.Time) error { + if !staleRead.IsZero() { + generateEphemeralCertRequest.ReadTime = staleRead.Format(time.RFC3339) + } + data, err = req.Do() + return err + }) + if err != nil { + return tls.Certificate{}, err + } + + c, err := parseCert(data.EphemeralCert.Cert) + if err != nil { + return tls.Certificate{}, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err) + } + + if s.EnableIAMLogin { + // Adjust the certificate's expiration to be the earlier of tok.Expiry or c.NotAfter + if tok.Expiry.Before(c.NotAfter) { + c.NotAfter = tok.Expiry + } + } + return tls.Certificate{ + Certificate: [][]byte{c.Raw}, + PrivateKey: s.generateKey(), + Leaf: c, + }, nil +} + +func parseCert(pemCert string) (*x509.Certificate, error) { + bl, _ := pem.Decode([]byte(pemCert)) + if bl == nil { + return nil, errors.New("invalid PEM: " + pemCert) + } + return x509.ParseCertificate(bl.Bytes) +} + +// Return the RSA private key, which is lazily initialized. +func (s *RemoteCertSource) generateKey() *rsa.PrivateKey { + s.keyOnce.Do(func() { + start := time.Now() + pkey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) // very unexpected. + } + logging.Verbosef("Generated RSA key in %v", time.Since(start)) + s.key = pkey + }) + return s.key +} + +// Find the first matching IP address by user input IP address types +func (s *RemoteCertSource) findIPAddr(data *sqladmin.ConnectSettings, instance string) (ipAddrInUse string, err error) { + for _, eachIPAddrTypeByUser := range s.IPAddrTypes { + for _, eachIPAddrTypeOfInstance := range data.IpAddresses { + if strings.ToUpper(eachIPAddrTypeOfInstance.Type) == strings.ToUpper(eachIPAddrTypeByUser) { + ipAddrInUse = eachIPAddrTypeOfInstance.IpAddress + return ipAddrInUse, nil + } + } + } + + ipAddrTypesOfInstance := "" + for _, eachIPAddrTypeOfInstance := range data.IpAddresses { + ipAddrTypesOfInstance += fmt.Sprintf("(TYPE=%v, IP_ADDR=%v)", eachIPAddrTypeOfInstance.Type, eachIPAddrTypeOfInstance.IpAddress) + } + + ipAddrTypeOfUser := fmt.Sprintf("%v", s.IPAddrTypes) + + return "", fmt.Errorf("User input IP address type %v does not match the instance %v, the instance's IP addresses are %v ", ipAddrTypeOfUser, instance, ipAddrTypesOfInstance) +} + +// Remote returns the specified instance's CA certificate, address, and name. +func (s *RemoteCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { + p, region, n := util.SplitName(instance) + regionName := fmt.Sprintf("%s~%s", region, n) + req := s.serv.Connect.Get(p, regionName) + + var data *sqladmin.ConnectSettings + err = backoffAPIRetry("get instance", instance, func(staleRead time.Time) error { + if !staleRead.IsZero() { + req.ReadTime(staleRead.Format(time.RFC3339)) + } + data, err = req.Do() + return err + }) + if err != nil { + return nil, "", "", "", err + } + + // TODO(chowski): remove this when us-central is removed. + if data.Region == "us-central" { + data.Region = "us-central1" + } + if data.Region != region { + if region == "" { + err = fmt.Errorf("instance %v doesn't provide region", instance) + } else { + err = fmt.Errorf(`for connection string "%s": got region %q, want %q`, instance, region, data.Region) + } + if s.checkRegion { + return nil, "", "", "", err + } + logging.Errorf("%v", err) + logging.Errorf("WARNING: specifying the correct region in an instance string will become required in a future version!") + } + + if len(data.IpAddresses) == 0 { + return nil, "", "", "", fmt.Errorf("no IP address found for %v", instance) + } + if data.BackendType == "FIRST_GEN" { + logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") + return nil, "", "", "", fmt.Errorf("%q is a first generation instance", instance) + } + + // Find the first matching IP address by user input IP address types + ipAddrInUse := "" + ipAddrInUse, err = s.findIPAddr(data, instance) + if err != nil { + return nil, "", "", "", err + } + + c, err := parseCert(data.ServerCaCert.Cert) + + return c, ipAddrInUse, p + ":" + n, data.DatabaseVersion, err +} diff --git a/proxy/certs/certs_test.go b/proxy/certs/certs_test.go new file mode 100644 index 0000000000..7f1f780647 --- /dev/null +++ b/proxy/certs/certs_test.go @@ -0,0 +1,174 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package certs + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "google.golang.org/api/option" + sqladmin "google.golang.org/api/sqladmin/v1beta4" +) + +const fakeCert = `-----BEGIN CERTIFICATE----- +MIICgTCCAWmgAwIBAgIBADANBgkqhkiG9w0BAQsFADAAMCIYDzAwMDEwMTAxMDAw +MDAwWhgPMDAwMTAxMDEwMDAwMDBaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvN0H6/ecloIfNyRu8KKtVSIK0JaW1lB1C1/ZI9iZmihqiUrxeyKTb +9hWuMPJ3u9NfSn1Vlwuj0bw7/T8e3Ol5BImcGxYxWMefkqFtqnjCafo2wnIea/eQ +JFLt4wXYkeveHReUseGtaBzpCo4wYOiqgxyIrGiQ/rq4Xjr2hXuqTg4TTgxv+0Iv +nrJwn61pitGvLPjsl9quzSQ6CdM3tWfb6cwozF5uJatbxRCZDsp1qUBXX9/zYqmx +8regdRG95btNgXLCfNS0iX0jopl00vGwYRGGKjfPZ5AkpuxX9M4Ys3X7pOspaQMC +Zf4VjXdwOljqZxIOGhOBbrXQacSywTLjAgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUA +A4IBAQAXj/0iiU2AQGztlFstLVwQ9yz+7/pfqAr26DYu9hpI/QvrZsJWjwNUNlX+ +7gwhrwiJs7xsLZqnEr2qvj6at/MtxIEVgQd43sOsWW9de8R5WNQNzsCb+5npWcx7 +vtcKXD9jFFLDDCIYjAf9+6m/QrMJtIf++zBmjguShccjZzY+GQih78oWqNTYqRQs +//wOP15vFQ/gB4DcJ0UyO9icVgbJha66yzG7XABDEepha5uhpLhwFaONU8jMxW7A +fOx52xqIUu3m4M3Ci0ZIp22TeGVuJ/Dy1CPbDOshcb0dXTE+mU5T91SHKRF4jz77 ++9TQIXHGk7lJyVVhbed8xm/p727f +-----END CERTIFICATE-----` + +func TestLocalCertSupportsStaleReads(t *testing.T) { + var ( + gotReadTimes []string + ok bool + ) + handleEphemeralCert := func(w http.ResponseWriter, r *http.Request) { + var actual sqladmin.GenerateEphemeralCertRequest + data, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read request body: %v", err) + } + defer r.Body.Close() + if err = json.Unmarshal(data, &actual); err != nil { + t.Fatalf("failed to unmarshal request body: %v", err) + } + gotReadTimes = append(gotReadTimes, actual.ReadTime) + if !ok { + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintln(w, `{"message":"the first request fails"}`) + ok = true + return + } + // the second request succeeds + fmt.Fprintln(w, fmt.Sprintf(`{"ephemeralCert":{"cert": %q}}`, fakeCert)) + } + ts := httptest.NewServer(http.HandlerFunc(handleEphemeralCert)) + defer ts.Close() + + cs := NewCertSourceOpts(ts.Client(), RemoteOpts{}) + // replace SQL Admin API client with client backed by test server + var err error + cs.serv, err = sqladmin.NewService(context.Background(), + option.WithEndpoint(ts.URL), option.WithHTTPClient(ts.Client())) + if err != nil { + t.Fatalf("failed to replace SQL Admin client: %v", err) + } + + // Send request to generate a cert + _, err = cs.Local("my-proj:reg:my-inst") + if err != nil { + t.Fatal(err) + } + + // Verify read time is not present for first request + // and is 30 seconds before "now" for second request + if len(gotReadTimes) != 2 { + t.Fatalf("expected two results, got = %v", len(gotReadTimes)) + } + if gotReadTimes[0] != "" { + t.Fatalf("expected empty ReadTime for first request, got = %v", gotReadTimes[0]) + } + wantStaleness := 30 * time.Second + if !staleTimestamp(gotReadTimes[1], wantStaleness) { + t.Fatalf("expected timestamp at least %v old, got = %v (now = %v)", + wantStaleness, gotReadTimes[1], time.Now().UTC().Format(time.RFC3339)) + } +} + +func staleTimestamp(ts string, staleness time.Duration) bool { + t, err := time.Parse(time.RFC3339, ts) + if err != nil { + // ts was not in expected format, fail + return false + } + return t.Before(time.Now().Add(-staleness)) +} + +func TestRemoteCertSupportsStaleReads(t *testing.T) { + var ( + gotReadTimes []string + ok bool + ) + handleConnectSettings := func(w http.ResponseWriter, r *http.Request) { + rt := r.URL.Query()["readTime"] + // if the URL parameter isn't nil, record its value; otherwise add an + // empty string to indicate no query param was set + if rt != nil { + gotReadTimes = append(gotReadTimes, rt[0]) + } else { + gotReadTimes = append(gotReadTimes, "") + } + if !ok { + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintln(w, `{"message":"the first request fails"}`) + ok = true + return + } + fmt.Fprintln(w, fmt.Sprintf(`{ + "region":"us-central1", + "ipAddresses": [ + {"type":"PRIMARY", "ipAddress":"127.0.0.1"} + ], + "serverCaCert": {"cert": %q} + }`, fakeCert)) + } + ts := httptest.NewServer(http.HandlerFunc(handleConnectSettings)) + defer ts.Close() + + cs := NewCertSourceOpts(ts.Client(), RemoteOpts{}) + var err error + // replace SQL Admin API client with client backed by test server + cs.serv, err = sqladmin.NewService(context.Background(), + option.WithEndpoint(ts.URL), option.WithHTTPClient(ts.Client())) + if err != nil { + t.Fatalf("failed to replace SQL Admin client: %v", err) + } + + // Send request to retrieve instance metadata + _, _, _, _, err = cs.Remote("my-proj:us-central1:my-inst") + if err != nil { + t.Fatal(err) + } + + // Verify read time is not present for first request + // and is 30 seconds before "now" for second request + if len(gotReadTimes) != 2 { + t.Fatalf("expected two results, got = %v", len(gotReadTimes)) + } + if gotReadTimes[0] != "" { + t.Fatalf("expected empty ReadTime for first request, got = %v", gotReadTimes[0]) + } + wantStaleness := 30 * time.Second + if !staleTimestamp(gotReadTimes[1], wantStaleness) { + t.Fatalf("expected timestamp at least %v old, got = %v (now = %v)", + wantStaleness, gotReadTimes[1], time.Now().UTC().Format(time.RFC3339)) + } +} diff --git a/proxy/dialers/mysql/hook.go b/proxy/dialers/mysql/hook.go new file mode 100644 index 0000000000..7bee55d5b7 --- /dev/null +++ b/proxy/dialers/mysql/hook.go @@ -0,0 +1,94 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package mysql adds a 'cloudsql' network to use when you want to access a +// Cloud SQL Database via the mysql driver found at +// github.com/go-sql-driver/mysql. It also exposes helper functions for +// dialing. +package mysql + +import ( + "database/sql" + "errors" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" + "github.com/go-sql-driver/mysql" +) + +func init() { + mysql.RegisterDialContext("cloudsql", proxy.DialContext) +} + +// Dial logs into the specified Cloud SQL Instance using the given user and no +// password. To set more options, consider calling DialCfg instead. +// +// The provided instance should be in the form project-name:region:instance-name. +// +// The returned *sql.DB may be valid even if there's also an error returned +// (e.g. if there was a transient connection error). +func Dial(instance, user string) (*sql.DB, error) { + cfg := mysql.NewConfig() + cfg.User = user + cfg.Addr = instance + return DialCfg(cfg) +} + +// DialPassword is similar to Dial, but allows you to specify a password. +// +// Note that using a password with the proxy is not necessary as long as the +// user's hostname in the mysql.user table is 'cloudsqlproxy~'. For more +// information, see: +// https://cloud.google.com/sql/docs/sql-proxy#user +func DialPassword(instance, user, password string) (*sql.DB, error) { + cfg := mysql.NewConfig() + cfg.User = user + cfg.Passwd = password + cfg.Addr = instance + return DialCfg(cfg) +} + +// Cfg returns the effective *mysql.Config to represent connectivity to the +// provided instance via the given user and password. The config can be +// modified and passed to DialCfg to connect. If you don't modify the returned +// config before dialing, consider using Dial or DialPassword. +func Cfg(instance, user, password string) *mysql.Config { + cfg := mysql.NewConfig() + cfg.User = user + cfg.Passwd = password + cfg.Addr = instance + cfg.Net = "cloudsql" + return cfg +} + +// DialCfg opens up a SQL connection to a Cloud SQL Instance specified by the +// provided configuration. It is otherwise the same as Dial. +// +// The cfg.Addr should be the instance's connection string, in the format of: +// project-name:region:instance-name. +func DialCfg(cfg *mysql.Config) (*sql.DB, error) { + if cfg.TLSConfig != "" { + return nil, errors.New("do not specify TLS when using the Proxy") + } + + // Copy the config so that we can modify it without feeling bad. + c := *cfg + c.Net = "cloudsql" + dsn := c.FormatDSN() + + db, err := sql.Open("mysql", dsn) + if err == nil { + err = db.Ping() + } + return db, err +} diff --git a/proxy/dialers/mysql/hook_test.go b/proxy/dialers/mysql/hook_test.go new file mode 100644 index 0000000000..a2c8df3201 --- /dev/null +++ b/proxy/dialers/mysql/hook_test.go @@ -0,0 +1,47 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mysql_test + +import ( + "fmt" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/mysql" +) + +// ExampleCfg shows how to use Cloud SQL Auth proxy dialer if you must update some +// settings normally passed in the DSN such as the DBName or timeouts. +func ExampleCfg() { + cfg := mysql.Cfg("project:region:instance-name", "user", "") + cfg.DBName = "DB_1" + cfg.ParseTime = true + + const timeout = 10 * time.Second + cfg.Timeout = timeout + cfg.ReadTimeout = timeout + cfg.WriteTimeout = timeout + + db, err := mysql.DialCfg(cfg) + if err != nil { + panic("couldn't dial: " + err.Error()) + } + // Close db after this method exits since we don't need it for the + // connection pooling. + defer db.Close() + + var now time.Time + fmt.Println(db.QueryRow("SELECT NOW()").Scan(&now)) + fmt.Println(now) +} diff --git a/proxy/dialers/postgres/hook.go b/proxy/dialers/postgres/hook.go new file mode 100644 index 0000000000..678bef0fe4 --- /dev/null +++ b/proxy/dialers/postgres/hook.go @@ -0,0 +1,61 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package postgres adds a 'cloudsqlpostgres' driver to use when you want +// to access a Cloud SQL Database via the go database/sql library. +// It is a wrapper over the driver found at github.com/lib/pq. +// To use this driver, you can look at an example in +// postgres_test package in the hook_test.go file +package postgres + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "net" + "regexp" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" + "github.com/lib/pq" +) + +func init() { + sql.Register("cloudsqlpostgres", &Driver{}) +} + +type Driver struct{} + +type dialer struct{} + +// instanceRegexp is used to parse the addr returned by lib/pq. +// lib/pq returns the format '[project:region:instance]:port' +var instanceRegexp = regexp.MustCompile(`^\[(.+)\]:[0-9]+$`) + +func (d dialer) Dial(ntw, addr string) (net.Conn, error) { + matches := instanceRegexp.FindStringSubmatch(addr) + if len(matches) != 2 { + return nil, fmt.Errorf("failed to parse addr: %q. It should conform to the regular expression %q", addr, instanceRegexp) + } + instance := matches[1] + return proxy.Dial(instance) +} + +func (d dialer) DialTimeout(ntw, addr string, timeout time.Duration) (net.Conn, error) { + return nil, fmt.Errorf("timeout is not currently supported for cloudsqlpostgres dialer") +} + +func (d *Driver) Open(name string) (driver.Conn, error) { + return pq.DialOpen(dialer{}, name) +} diff --git a/proxy/dialers/postgres/hook_test.go b/proxy/dialers/postgres/hook_test.go new file mode 100644 index 0000000000..cfead5bbcc --- /dev/null +++ b/proxy/dialers/postgres/hook_test.go @@ -0,0 +1,38 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Package postgres_test contains an example on how to use cloudsqlpostgres dialer +package postgres_test + +import ( + "database/sql" + "fmt" + "log" + "time" + + _ "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/postgres" +) + +// Example shows how to use cloudsqlpostgres dialer +func Example() { + // Note that sslmode=disable is required it does not mean that the connection + // is unencrypted. All connections via the proxy are completely encrypted. + db, err := sql.Open("cloudsqlpostgres", "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable") + if err != nil { + log.Fatal(err) + } + defer db.Close() + var now time.Time + fmt.Println(db.QueryRow("SELECT NOW()").Scan(&now)) + fmt.Println(now) +} diff --git a/proxy/fuse/fuse.go b/proxy/fuse/fuse.go new file mode 100644 index 0000000000..ec3b8178c9 --- /dev/null +++ b/proxy/fuse/fuse.go @@ -0,0 +1,378 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows && !openbsd +// +build !windows,!openbsd + +// Package fuse provides a connection source wherein the user does not need to +// specify which instance they are connecting to before they start the +// executable. Instead, simply attempting to access a file in the provided +// directory will transparently create a proxied connection to an instance +// which has that name. +// +// Specifically, given that NewConnSrc was called with the mounting directory +// as /cloudsql: +// +// 1) Execute `mysql -S /cloudsql/speckle:instance` +// 2) The 'mysql' executable looks up the file "speckle:instance" inside "/cloudsql" +// 3) This lookup is intercepted by the code in this package. A local unix socket +// located in a temporary directory is opened for listening and the lookup for +// "speckle:instance" returns to mysql saying that it is a symbolic link +// pointing to this new local socket. +// 4) mysql dials the local unix socket, creating a new connection to the +// specified instance. +package fuse + +import ( + "bytes" + "errors" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/hanwen/go-fuse/v2/fuse/nodefs" + "golang.org/x/net/context" +) + +// NewConnSrc returns a source of new connections based on Lookups in the +// provided mount directory. If there isn't a directory located at tmpdir one +// is created. The second return parameter can be used to shutdown and release +// any resources. As a result of this shutdown, or during any other fatal +// error, the returned chan will be closed. +// +// The connset parameter is optional. +func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { + if err := os.MkdirAll(tmpdir, 0777); err != nil { + return nil, nil, err + } + if connset == nil { + // Make a dummy one. + connset = proxy.NewConnSet() + } + conns := make(chan proxy.Conn, 1) + root := &fsRoot{ + tmpDir: tmpdir, + linkDir: mountdir, + dst: conns, + links: make(map[string]*symlink), + connset: connset, + client: client, + } + + srv, err := fs.Mount(mountdir, root, &fs.Options{ + MountOptions: fuse.MountOptions{AllowOther: true}, + }) + if err != nil { + return nil, nil, fmt.Errorf("FUSE mount failed: %q: %v", mountdir, err) + } + + closer := fuseCloser(func() error { + err := srv.Unmount() // Best effort unmount + if err != nil { + logging.Errorf("Unmount failed: %v", err) + } + return root.Close() + }) + return conns, closer, nil +} + +type fuseCloser func() error + +func (fc fuseCloser) Close() error { + return fc() +} + +// symlink implements a symbolic link, returning the underlying path when +// Readlink is called. +type symlink struct { + fs.Inode + path string +} + +var _ fs.NodeReadlinker = &symlink{} + +func (s *symlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) { + return []byte(s.path), fs.OK +} + +// fsRoot provides the in-memory file system that supports lazy connections to +// Cloud SQL instances. +type fsRoot struct { + fs.Inode + + // tmpDir defines a temporary directory where all the sockets are placed + // faciliating connections to Cloud SQL instances. + tmpDir string + // linkDir is the directory that holds symbolic links to the tmp dir for + // each Cloud SQL instance connection. After shutdown, this directory is + // cleaned out. + linkDir string + + client *proxy.Client + connset *proxy.ConnSet + + // sockLock protects fields in this struct related to sockets; specifically + // 'links' and 'closers'. + sockLock sync.Mutex + links map[string]*symlink + // closers includes a reference to all open Unix socket listeners. When + // fs.Close is called, all of these listeners are also closed. + closers []io.Closer + + sync.RWMutex + dst chan<- proxy.Conn +} + +var _ interface { + fs.InodeEmbedder + fs.NodeGetattrer + fs.NodeLookuper + fs.NodeReaddirer +} = &fsRoot{} + +func (r *fsRoot) newConn(instance string, c net.Conn) { + r.RLock() + // dst will be nil if Close has been called already. + if ch := r.dst; ch != nil { + ch <- proxy.Conn{Instance: instance, Conn: c} + } else { + logging.Errorf("Ignored new conn request to %q: system has been closed", instance) + } + r.RUnlock() +} + +// Close shuts down the fsRoot filesystem and closes all open Unix socket +// listeners. +func (r *fsRoot) Close() error { + r.Lock() + if r.dst != nil { + // Since newConn only sends on dst while holding a reader lock, holding the + // writer lock is sufficient to ensure there are no pending sends on the + // channel when it is closed. + close(r.dst) + // Setting it to nil prevents further sends. + r.dst = nil + } + r.Unlock() + + var errs bytes.Buffer + r.sockLock.Lock() + for _, c := range r.closers { + if err := c.Close(); err != nil { + fmt.Fprintln(&errs, err) + } + } + r.sockLock.Unlock() + + if errs.Len() == 0 { + return nil + } + logging.Errorf("Close %q: %v", r.linkDir, errs.String()) + return errors.New(errs.String()) +} + +// Getattr implements fs.NodeGetattrer and represents fsRoot as a directory. +func (r *fsRoot) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + *out = fuse.AttrOut{Attr: fuse.Attr{ + Mode: 0555 | fuse.S_IFDIR, + }} + return fs.OK +} + +// Lookup implements fs.NodeLookuper and handles all requests, either for the +// README, or for a new connection to a Cloud SQL instance. When receiving a +// request for a Cloud SQL instance, Lookup will return a symlink to a Unix +// socket that provides connectivity to a remote instance. +func (r *fsRoot) Lookup(ctx context.Context, instance string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + if instance == "README" { + return r.NewInode(ctx, &readme{}, fs.StableAttr{}), fs.OK + } + r.sockLock.Lock() + defer r.sockLock.Unlock() + + if _, _, _, _, err := proxy.ParseInstanceConnectionName(instance); err != nil { + return nil, syscall.ENOENT + } + + if ret, ok := r.links[instance]; ok { + return ret.EmbeddedInode(), fs.OK + } + + // path is the location of the Unix socket + path := filepath.Join(r.tmpDir, instance) + os.RemoveAll(path) // Best effort; the following will fail if this does. + // linkpath is the location the symlink points to + linkpath := path + + // Add a ".s.PGSQL.5432" suffix to path for Postgres instances + if r.client != nil { + version, err := r.client.InstanceVersionContext(ctx, instance) + if err != nil { + logging.Errorf("Failed to get Instance version for %s: %v", instance, err) + return nil, syscall.ENOENT + } + if strings.HasPrefix(strings.ToLower(version), "postgres") { + if err := os.MkdirAll(path, 0755); err != nil { + logging.Errorf("Failed to create path %s: %v", path, err) + return nil, syscall.EIO + } + path = filepath.Join(linkpath, ".s.PGSQL.5432") + } + } + // TODO: check path length -- if it exceeds the max supported socket length, + // return an error that helps the user understand what went wrong. + // Otherwise, we get a "bind: invalid argument" error. + + sock, err := net.Listen("unix", path) + if err != nil { + logging.Errorf("couldn't listen at %q: %v", path, err) + return nil, syscall.EEXIST + } + if err := os.Chmod(path, 0777|os.ModeSocket); err != nil { + logging.Errorf("couldn't update permissions for socket file %q: %v; other users may be unable to connect", path, err) + } + + go r.listenerLifecycle(sock, instance, path) + + ret := &symlink{path: linkpath} + inode := r.NewInode(ctx, ret, fs.StableAttr{Mode: 0777 | fuse.S_IFLNK}) + r.links[instance] = ret + // TODO(chowski): memory leak when listeners exit on their own via removeListener. + r.closers = append(r.closers, sock) + + return inode, fs.OK +} + +// removeListener marks that a Listener for an instance has exited and is no +// longer serving new connections. +func (r *fsRoot) removeListener(instance, path string) { + r.sockLock.Lock() + defer r.sockLock.Unlock() + v, ok := r.links[instance] + if ok && v.path == path { + delete(r.links, instance) + } else { + logging.Errorf("Removing a listener for %q at %q which was already replaced", instance, path) + } +} + +// listenerLifecycle calls l.Accept in a loop, and for each new connection +// r.newConn is called. After the Listener returns an error it is removed. +func (r *fsRoot) listenerLifecycle(l net.Listener, instance, path string) { + for { + start := time.Now() + c, err := l.Accept() + if err != nil { + logging.Errorf("error in Accept for %q: %v", instance, err) + if nerr, ok := err.(net.Error); ok && nerr.Temporary() { + d := 10*time.Millisecond - time.Since(start) + if d > 0 { + time.Sleep(d) + } + continue + } + break + } + r.newConn(instance, c) + } + r.removeListener(instance, path) + l.Close() + if err := os.Remove(path); err != nil { + logging.Errorf("couldn't remove %q: %v", path, err) + } +} + +// Readdir implements fs.NodeReaddirer and returns a list of files for each +// instance to which the proxy is actively connected. In addition, the list +// includes a README. +func (r *fsRoot) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { + activeConns := r.connset.IDs() + entries := []fuse.DirEntry{ + {Name: "README", Mode: 0555 | fuse.S_IFREG}, + } + for _, conn := range activeConns { + entries = append(entries, fuse.DirEntry{ + Name: conn, + Mode: 0777 | syscall.S_IFSOCK, + }) + } + ds := fs.NewListDirStream(entries) + return ds, fs.OK +} + +// readme represents a static read-only text file. +type readme struct { + fs.Inode +} + +var _ interface { + fs.InodeEmbedder + fs.NodeGetattrer + fs.NodeReader + fs.NodeOpener +} = &readme{} + +const readmeText = ` +When programs attempt to open files in this directory, a remote connection to +the Cloud SQL instance of the same name will be established. + +That is, running: + + mysql -u root -S "/path/to/this/directory/project:region:instance-2" + -or- + psql "host=/path/to/this/directory/project:region:instance-2 dbname=mydb user=myuser" + +will open a new connection to the specified instance, given you have the correct +permissions. + +Listing the contents of this directory will show all instances with active +connections. +` + +// Getattr implements fs.NodeGetattrer and indicates that this file is a regular +// file. +func (*readme) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + *out = fuse.AttrOut{Attr: fuse.Attr{ + Mode: 0444 | syscall.S_IFREG, + Size: uint64(len(readmeText)), + }} + return fs.OK +} + +// Read implements fs.NodeReader and supports incremental reads. +func (*readme) Read(ctx context.Context, f fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + end := int(off) + len(dest) + if end > len(readmeText) { + end = len(readmeText) + } + return fuse.ReadResultData([]byte(readmeText[off:end])), fs.OK +} + +// Open implements fs.NodeOpener and supports opening the README as a read-only +// file. +func (*readme) Open(ctx context.Context, mode uint32) (fs.FileHandle, uint32, syscall.Errno) { + df := nodefs.NewDataFile([]byte(readmeText)) + rf := nodefs.NewReadOnlyFile(df) + return rf, 0, fs.OK +} diff --git a/proxy/fuse/fuse_darwin.go b/proxy/fuse/fuse_darwin.go new file mode 100644 index 0000000000..5adb84482e --- /dev/null +++ b/proxy/fuse/fuse_darwin.go @@ -0,0 +1,43 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fuse + +import ( + "os" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" +) + +const ( + macfusePath = "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse" + osxfusePath = "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse" +) + +// Supported checks if macfuse or osxfuse are installed on the host by looking +// for both in their known installation location. +func Supported() bool { + // This code follows the same strategy as hanwen/go-fuse. + // See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_darwin.go#L121-L124. + + // check for macfuse first (newer version of osxfuse) + if _, err := os.Stat(macfusePath); err != nil { + // if that fails, check for osxfuse next + if _, err := os.Stat(osxfusePath); err != nil { + logging.Errorf("Failed to find osxfuse or macfuse. Verify FUSE installation and try again (see https://osxfuse.github.io).") + return false + } + } + return true +} diff --git a/proxy/fuse/fuse_linux.go b/proxy/fuse/fuse_linux.go new file mode 100644 index 0000000000..45f14b885c --- /dev/null +++ b/proxy/fuse/fuse_linux.go @@ -0,0 +1,34 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fuse + +import ( + "os/exec" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" +) + +// Supported returns true if the current system supports FUSE. +func Supported() bool { + // This code follows the same strategy found in hanwen/go-fuse. + // See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_linux.go#L184-L198. + if _, err := exec.LookPath("fusermount"); err != nil { + if _, err := exec.LookPath("/bin/fusermount"); err != nil { + logging.Errorf("Failed to find fusermount binary in PATH or /bin. Verify FUSE installation and try again.") + return false + } + } + return true +} diff --git a/proxy/fuse/fuse_linux_test.go b/proxy/fuse/fuse_linux_test.go new file mode 100644 index 0000000000..928442a5ec --- /dev/null +++ b/proxy/fuse/fuse_linux_test.go @@ -0,0 +1,47 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux +// +build linux + +package fuse_test + +import ( + "os" + "testing" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/fuse" +) + +func TestFUSESupport(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + + removePath := func() func() { + original := os.Getenv("PATH") + os.Unsetenv("PATH") + return func() { os.Setenv("PATH", original) } + } + if !fuse.Supported() { + t.Fatal("expected FUSE to be supported") + } + cleanup := removePath() + defer cleanup() + + if !fuse.Supported() { + t.Fatal("expected FUSE to be supported") + } + +} diff --git a/proxy/fuse/fuse_openbsd.go b/proxy/fuse/fuse_openbsd.go new file mode 100644 index 0000000000..61908d4a64 --- /dev/null +++ b/proxy/fuse/fuse_openbsd.go @@ -0,0 +1,32 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package fuse is a package stub for openbsd, which isn't supported by our +// fuse library. +package fuse + +import ( + "errors" + "io" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" +) + +func Supported() bool { + return false +} + +func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { + return nil, nil, errors.New("fuse not supported on openbsd") +} diff --git a/proxy/fuse/fuse_test.go b/proxy/fuse/fuse_test.go new file mode 100644 index 0000000000..3bd3a21783 --- /dev/null +++ b/proxy/fuse/fuse_test.go @@ -0,0 +1,247 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows +// +build !windows + +package fuse + +import ( + "bytes" + "io" + "io/ioutil" + "net" + "os" + "path/filepath" + "sync" + "syscall" + "testing" + "time" +) + +func randTmpDir(t interface { + Fatalf(format string, args ...interface{}) +}) string { + name, err := ioutil.TempDir("", "*") + if err != nil { + t.Fatalf("failed to create tmp dir: %v", err) + } + return name +} + +// tryFunc executes the provided function up to maxCount times, sleeping 100ms +// between attempts. +func tryFunc(f func() error, maxCount int) error { + var errCount int + for { + err := f() + if err == nil { + return nil + } + errCount++ + if errCount == maxCount { + return err + } + time.Sleep(100 * time.Millisecond) + } +} + +func TestFuseClose(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + + dir := randTmpDir(t) + tmpdir := randTmpDir(t) + src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) + if err != nil { + t.Fatal(err) + } + + if err := tryFunc(fuse.Close, 10); err != nil { + t.Fatal(err) + } + if got, ok := <-src; ok { + t.Fatalf("got new connection %#v, expected closed source", got) + } +} + +// TestBadDir verifies that the fuse module does not create directories, only simple files. +func TestBadDir(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + + dir := randTmpDir(t) + tmpdir := randTmpDir(t) + _, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := tryFunc(fuse.Close, 10); err != nil { + t.Fatal(err) + } + }() + + _, err = os.Stat(filepath.Join(dir, "proj:region:inst-1", "proj:region:inst-2")) + if err == nil { + t.Fatal("able to find a directory inside the mount point, expected only regular files") + } + if err := err.(*os.PathError); err.Err != syscall.ENOTDIR { + t.Fatalf("got %#v, want ENOTDIR (%v)", err.Err, syscall.ENOTDIR) + } +} + +func TestReadme(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + + dir := randTmpDir(t) + tmpdir := randTmpDir(t) + _, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := tryFunc(fuse.Close, 10); err != nil { + t.Fatal(err) + } + }() + + data, err := ioutil.ReadFile(filepath.Join(dir, "README")) + if err != nil { + t.Fatal(err) + } + // We just care that the file exists. Print out the contents for + // informational purposes. + t.Log(string(data)) +} + +func TestSingleInstance(t *testing.T) { + if testing.Short() { + t.Skip("skipping fuse tests in short mode.") + } + + dir := randTmpDir(t) + tmpdir := randTmpDir(t) + src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := tryFunc(fuse.Close, 10); err != nil { + t.Fatal(err) + } + }() + + const want = "test:instance:string" + path := filepath.Join(dir, want) + + fi, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + + if fi.Mode()&os.ModeType != os.ModeSocket { + t.Fatalf("%q had mode %v (%X), expected a socket file", path, fi.Mode(), uint32(fi.Mode())) + } + + c, err := net.Dial("unix", path) + if err != nil { + t.Fatal(err) + } + defer c.Close() + + got, ok := <-src + if !ok { + t.Fatal("connection source was closed, expected a connection") + } else if got.Instance != want { + t.Fatalf("got %q, want %q", got.Instance, want) + } else if got.Conn == nil { + t.Fatal("got nil connection, wanted a connection") + } + + const sent = "test string" + go func() { + if _, err := c.Write([]byte(sent)); err != nil { + t.Error(err) + } + if err := c.Close(); err != nil { + t.Error(err) + } + }() + + gotData := new(bytes.Buffer) + if _, err := io.Copy(gotData, got.Conn); err != nil { + t.Fatal(err) + } else if gotData.String() != sent { + t.Fatalf("got %q, want %v", gotData.String(), sent) + } +} + +func BenchmarkNewConnection(b *testing.B) { + if testing.Short() { + b.Skip("skipping fuse tests in short mode.") + } + + dir := randTmpDir(b) + tmpdir := randTmpDir(b) + src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) + if err != nil { + b.Fatal(err) + } + + const want = "X" + incomingCount := 0 + var incoming sync.Mutex // Is unlocked when the following goroutine exits. + go func() { + incoming.Lock() + defer incoming.Unlock() + + for c := range src { + c.Conn.Write([]byte(want)) + c.Conn.Close() + incomingCount++ + } + }() + + const instance = "test:instance:string" + path := filepath.Join(dir, instance) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c, err := net.Dial("unix", path) + if err != nil { + b.Errorf("couldn't dial: %v", err) + } + + data, err := ioutil.ReadAll(c) + if err != nil { + b.Errorf("got read error: %v", err) + } else if got := string(data); got != want { + b.Errorf("read %q, want %q", string(data), want) + } + } + if err := fuse.Close(); err != nil { + b.Fatal(err) + } + + // Wait for the 'incoming' goroutine to finish. + incoming.Lock() + if incomingCount != b.N { + b.Fatalf("got %d connections, want %d", incomingCount, b.N) + } +} diff --git a/proxy/fuse/fuse_windows.go b/proxy/fuse/fuse_windows.go new file mode 100644 index 0000000000..1b4666a7b2 --- /dev/null +++ b/proxy/fuse/fuse_windows.go @@ -0,0 +1,31 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package fuse is a package stub for windows, which does not support FUSE. +package fuse + +import ( + "errors" + "io" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" +) + +func Supported() bool { + return false +} + +func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { + return nil, nil, errors.New("fuse not supported on windows") +} diff --git a/proxy/limits/limits.go b/proxy/limits/limits.go new file mode 100644 index 0000000000..02b3bb913a --- /dev/null +++ b/proxy/limits/limits.go @@ -0,0 +1,89 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows && !freebsd +// +build !windows,!freebsd + +// Package limits provides routines to check and enforce certain resource +// limits on the Cloud SQL client proxy process. +package limits + +import ( + "fmt" + "syscall" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" +) + +var ( + // For overriding in unittests. + syscallGetrlimit = syscall.Getrlimit + syscallSetrlimit = syscall.Setrlimit +) + +// Each connection handled by the proxy requires two file descriptors, one +// for the local end of the connection and one for the remote. So, the proxy +// process should be able to open at least 8K file descriptors if it is to +// handle 4K connections to one instance. +const ExpectedFDs = 8500 + +// SetupFDLimits ensures that the process running the Cloud SQL proxy can have +// at least wantFDs number of open file descriptors. It returns an error if it +// cannot ensure the same. +func SetupFDLimits(wantFDs uint64) error { + rlim := &syscall.Rlimit{} + if err := syscallGetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { + return fmt.Errorf("failed to read rlimit for max file descriptors: %v", err) + } + + if rlim.Cur >= wantFDs { + logging.Verbosef("current FDs rlimit set to %d, wanted limit is %d. Nothing to do here.", rlim.Cur, wantFDs) + return nil + } + + // Linux man page: + // The soft limit is the value that the kernel enforces for the corre‐ + // sponding resource. The hard limit acts as a ceiling for the soft limit: + // an unprivileged process may set only its soft limit to a value in the + // range from 0 up to the hard limit, and (irreversibly) lower its hard + // limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE + // capability in the initial user namespace) may make arbitrary changes to + // either limit value. + if rlim.Max < wantFDs { + // When the hard limit is less than what is requested, let's just give it a + // shot, and if we fail, we fallback and try just setting the softlimit. + rlim2 := &syscall.Rlimit{} + rlim2.Max = wantFDs + rlim2.Cur = wantFDs + if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim2); err == nil { + logging.Verbosef("Rlimits for file descriptors set to {Current = %v, Max = %v}", rlim2.Cur, rlim2.Max) + return nil + } + } + + rlim.Cur = wantFDs + if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { + return fmt.Errorf( + `failed to set rlimit {Current = %v, Max = %v} for max file +descriptors. The hard limit on file descriptors (4096) is lower than the +requested rlimit. The proxy will only be able to handle ~2048 +connections. To hide this message, please request a limit within the available range.`, + rlim.Cur, + rlim.Max, + ) + } + + logging.Verbosef("Rlimits for file descriptors set to {Current = %v, Max = %v}", rlim.Cur, rlim.Max) + return nil +} diff --git a/proxy/limits/limits_freebsd.go b/proxy/limits/limits_freebsd.go new file mode 100644 index 0000000000..485b6a7ae9 --- /dev/null +++ b/proxy/limits/limits_freebsd.go @@ -0,0 +1,82 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build freebsd +// +build freebsd + +// Package limits provides routines to check and enforce certain resource +// limits on the Cloud SQL client proxy process. +package limits + +import ( + "fmt" + "syscall" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" +) + +var ( + // For overriding in unittests. + syscallGetrlimit = syscall.Getrlimit + syscallSetrlimit = syscall.Setrlimit +) + +// Each connection handled by the proxy requires two file descriptors, one +// for the local end of the connection and one for the remote. So, the proxy +// process should be able to open at least 8K file descriptors if it is to +// handle 4K connections to one instance. +const ExpectedFDs = 8500 + +// SetupFDLimits ensures that the process running the Cloud SQL proxy can have +// at least wantFDs number of open file descriptors. It returns an error if it +// cannot ensure the same. +func SetupFDLimits(wantFDs uint64) error { + rlim := &syscall.Rlimit{} + if err := syscallGetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { + return fmt.Errorf("failed to read rlimit for max file descriptors: %v", err) + } + + if uint64(rlim.Cur) >= wantFDs { + logging.Verbosef("current FDs rlimit set to %d, wanted limit is %d. Nothing to do here.", rlim.Cur, wantFDs) + return nil + } + + // Linux man page: + // The soft limit is the value that the kernel enforces for the corre‐ + // sponding resource. The hard limit acts as a ceiling for the soft limit: + // an unprivileged process may set only its soft limit to a value in the + // range from 0 up to the hard limit, and (irreversibly) lower its hard + // limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE + // capability in the initial user namespace) may make arbitrary changes to + // either limit value. + if uint64(rlim.Max) < wantFDs { + // When the hard limit is less than what is requested, let's just give it a + // shot, and if we fail, we fallback and try just setting the softlimit. + rlim2 := &syscall.Rlimit{} + rlim2.Max = int64(wantFDs) + rlim2.Cur = int64(wantFDs) + if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim2); err == nil { + logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim2) + return nil + } + } + + rlim.Cur = int64(wantFDs) + if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { + return fmt.Errorf("failed to set rlimit {%v} for max file descriptors: %v", rlim, err) + } + + logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim) + return nil +} diff --git a/proxy/limits/limits_test.go b/proxy/limits/limits_test.go new file mode 100644 index 0000000000..1ae394a54b --- /dev/null +++ b/proxy/limits/limits_test.go @@ -0,0 +1,136 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows +// +build !windows + +package limits + +import ( + "errors" + "math" + "syscall" + "testing" +) + +type rlimitFunc func(int, *syscall.Rlimit) error + +func TestSetupFDLimits(t *testing.T) { + tests := []struct { + desc string + getFunc rlimitFunc + setFunc rlimitFunc + wantFDs uint64 + wantErr bool + }{ + { + desc: "Getrlimit fails", + getFunc: func(_ int, _ *syscall.Rlimit) error { + return errors.New("failed to read rlimit for max file descriptors") + }, + setFunc: func(_ int, _ *syscall.Rlimit) error { + panic("shouldn't be called") + }, + wantFDs: 0, + wantErr: true, + }, + { + desc: "Getrlimit max is less than wantFDs", + getFunc: func(_ int, rlim *syscall.Rlimit) error { + rlim.Cur = 512 + rlim.Max = 512 + return nil + }, + setFunc: func(_ int, rlim *syscall.Rlimit) error { + if rlim.Cur != 1024 || rlim.Max != 1024 { + return errors.New("setrlimit called with unexpected value") + } + return nil + }, + wantFDs: 1024, + wantErr: false, + }, + { + desc: "Getrlimit returns rlim_infinity", + getFunc: func(_ int, rlim *syscall.Rlimit) error { + rlim.Cur = math.MaxUint64 + rlim.Max = math.MaxUint64 + return nil + }, + setFunc: func(_ int, _ *syscall.Rlimit) error { + panic("shouldn't be called") + }, + wantFDs: 1024, + wantErr: false, + }, + { + desc: "Getrlimit cur is greater than wantFDs", + getFunc: func(_ int, rlim *syscall.Rlimit) error { + rlim.Cur = 512 + rlim.Max = 512 + return nil + }, + setFunc: func(_ int, _ *syscall.Rlimit) error { + panic("shouldn't be called") + }, + wantFDs: 256, + wantErr: false, + }, + { + desc: "Setrlimit fails", + getFunc: func(_ int, rlim *syscall.Rlimit) error { + rlim.Cur = 128 + rlim.Max = 512 + return nil + }, + setFunc: func(_ int, _ *syscall.Rlimit) error { + return errors.New("failed to set rlimit for max file descriptors") + }, + wantFDs: 256, + wantErr: true, + }, + { + desc: "Success", + getFunc: func(_ int, rlim *syscall.Rlimit) error { + rlim.Cur = 128 + rlim.Max = 512 + return nil + }, + setFunc: func(_ int, _ *syscall.Rlimit) error { + return nil + }, + wantFDs: 256, + wantErr: false, + }, + } + + for _, test := range tests { + oldGetFunc := syscallGetrlimit + syscallGetrlimit = test.getFunc + defer func() { + syscallGetrlimit = oldGetFunc + }() + + oldSetFunc := syscallSetrlimit + syscallSetrlimit = test.setFunc + defer func() { + syscallSetrlimit = oldSetFunc + }() + + gotErr := SetupFDLimits(test.wantFDs) + if (gotErr != nil) != test.wantErr { + t.Errorf("%s: limits.SetupFDLimits(%d) returned error %v, wantErr %v", test.desc, test.wantFDs, gotErr, test.wantErr) + } + } +} diff --git a/proxy/limits/limits_windows.go b/proxy/limits/limits_windows.go new file mode 100644 index 0000000000..9bfab790b0 --- /dev/null +++ b/proxy/limits/limits_windows.go @@ -0,0 +1,30 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package limits is a package stub for windows, and we currently don't support +// setting limits in windows. +package limits + +import "errors" + +// We don't support limit on the number of file handles in windows. +const ExpectedFDs = 0 + +func SetupFDLimits(wantFDs uint64) error { + if wantFDs != 0 { + return errors.New("setting limits on the number of file handles is not supported") + } + + return nil +} diff --git a/proxy/proxy/client.go b/proxy/proxy/client.go new file mode 100644 index 0000000000..9c570fcc58 --- /dev/null +++ b/proxy/proxy/client.go @@ -0,0 +1,652 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package proxy + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/util" + "golang.org/x/net/proxy" + "golang.org/x/time/rate" +) + +const ( + // DefaultRefreshCfgThrottle is the time a refresh attempt must wait since + // the last attempt. + DefaultRefreshCfgThrottle = time.Minute + // IAMLoginRefreshThrottle is the time a refresh attempt must wait since the + // last attempt when using IAM login. + IAMLoginRefreshThrottle = 30 * time.Second + keepAlivePeriod = time.Minute + // DefaultRefreshCfgBuffer is the minimum amount of time for which a + // certificate must be valid to ensure the next refresh attempt has adequate + // time to complete. + DefaultRefreshCfgBuffer = 5 * time.Minute + // IAMLoginRefreshCfgBuffer is the minimum amount of time for which a + // certificate holding an Access Token must be valid. Because some token + // sources (e.g., ouath2.ComputeTokenSource) are refreshed with only ~60 + // seconds before expiration, this value must be smaller than the + // DefaultRefreshCfgBuffer. + IAMLoginRefreshCfgBuffer = 55 * time.Second +) + +var ( + // errNotCached is returned when the instance was not found in the Client's + // cache. It is an internal detail and is not actually ever returned to the + // user. + errNotCached = errors.New("instance was not found in cache") +) + +// Conn represents a connection from a client to a specific instance. +type Conn struct { + Instance string + Conn net.Conn +} + +// CertSource is how a Client obtains various certificates required for operation. +type CertSource interface { + // Local returns a certificate that can be used to authenticate with the + // provided instance. + Local(instance string) (tls.Certificate, error) + // Remote returns the instance's CA certificate, address, and name. + Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) +} + +// Client is a type to handle connecting to a Server. All fields are required +// unless otherwise specified. +type Client struct { + // ConnectionsCounter is used to enforce the optional maxConnections limit + ConnectionsCounter uint64 + + // MaxConnections is the maximum number of connections to establish + // before refusing new connections. 0 means no limit. + MaxConnections uint64 + + // Port designates which remote port should be used when connecting to + // instances. This value is defined by the server-side code, but for now it + // should always be 3307. + Port int + // Required; specifies how certificates are obtained. + Certs CertSource + // Optionally tracks connections through this client. If nil, connections + // are not tracked and will not be closed before method Run exits. + Conns *ConnSet + // ContextDialer should return a new connection to the provided address. + // It is called on each new connection to an instance. + // If left nil, Dialer will be tried first, and if that one is nil too then net.Dial will be used. + ContextDialer func(ctx context.Context, net, addr string) (net.Conn, error) + // Dialer should return a new connection to the provided address. It will be used only if ContextDialer is nil. + Dialer func(net, addr string) (net.Conn, error) + + // The cfgCache holds the most recent connection configuration keyed by + // instance. Relevant functions are refreshCfg and cachedCfg. It is + // protected by cacheL. + cfgCache map[string]cacheEntry + cacheL sync.RWMutex + // limiters holds a rate limiter keyed by instance. It is protected by + // cacheL. + limiters map[string]*rate.Limiter + + // refreshCfgL prevents multiple goroutines from contacting the Cloud SQL API at once. + refreshCfgL sync.Mutex + + // RefreshCfgThrottle is the amount of time to wait between configuration + // refreshes. If not set, it defaults to 1 minute. + // + // This is to prevent quota exhaustion in the case of client-side + // malfunction. + RefreshCfgThrottle time.Duration + + // RefreshCertBuffer is the amount of time before the configuration expires + // to attempt to refresh it. If not set, it defaults to 5 minutes. When IAM + // Login is enabled, this value should be set to IAMLoginRefreshCfgBuffer. + RefreshCfgBuffer time.Duration +} + +type cacheEntry struct { + lastRefreshed time.Time + // If err is not nil, the addr and cfg are not valid. + err error + addr string + version string + cfg *tls.Config + // done represents the status of any pending refresh operation related to this instance. + // If unset the op hasn't started, if open the op is still pending, and if closed the op has finished. + done chan struct{} +} + +// Run causes the client to start waiting for new connections to connSrc and +// proxy them to the destination instance. It blocks until connSrc is closed. +func (c *Client) Run(connSrc <-chan Conn) { + c.RunContext(context.Background(), connSrc) +} + +func (c *Client) run(ctx context.Context, connSrc <-chan Conn) { + for { + select { + case conn, ok := <-connSrc: + if !ok { + return + } + go c.handleConn(ctx, conn) + case <-ctx.Done(): + return + } + } +} + +// RunContext is like Run with an additional context.Context argument. +func (c *Client) RunContext(ctx context.Context, connSrc <-chan Conn) { + c.run(ctx, connSrc) + + if err := c.Conns.Close(); err != nil { + logging.Errorf("closing client had error: %v", err) + } +} + +func (c *Client) handleConn(ctx context.Context, conn Conn) { + active := atomic.AddUint64(&c.ConnectionsCounter, 1) + + // Deferred decrement of ConnectionsCounter upon connection closing + defer atomic.AddUint64(&c.ConnectionsCounter, ^uint64(0)) + + if c.MaxConnections > 0 && active > c.MaxConnections { + logging.Errorf("too many open connections (max %d)", c.MaxConnections) + conn.Conn.Close() + return + } + + server, err := c.DialContext(ctx, conn.Instance) + if err != nil { + logging.Errorf("couldn't connect to %q: %v", conn.Instance, err) + conn.Conn.Close() + return + } + + c.Conns.Add(conn.Instance, conn.Conn) + copyThenClose(server, conn.Conn, conn.Instance, "local connection on "+conn.Conn.LocalAddr().String()) + + if err := c.Conns.Remove(conn.Instance, conn.Conn); err != nil { + logging.Errorf("%s", err) + } +} + +// refreshCfg uses the CertSource inside the Client to find the instance's +// address as well as construct a new tls.Config to connect to the instance. +// This function should only be called from the scope of "cachedCfg", which +// controls the logic around throttling. +func (c *Client) refreshCfg(instance string) (addr string, cfg *tls.Config, version string, err error) { + c.refreshCfgL.Lock() + defer c.refreshCfgL.Unlock() + logging.Verbosef("refreshing ephemeral certificate for instance %s", instance) + + mycert, err := c.Certs.Local(instance) + if err != nil { + return "", nil, "", err + } + + scert, addr, name, version, err := c.Certs.Remote(instance) + if err != nil { + return "", nil, "", err + } + certs := x509.NewCertPool() + certs.AddCert(scert) + + cfg = &tls.Config{ + ServerName: name, + Certificates: []tls.Certificate{mycert}, + RootCAs: certs, + // We need to set InsecureSkipVerify to true due to + // https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194 + // https://tip.golang.org/doc/go1.11#crypto/x509 + // + // Since we have a secure channel to the Cloud SQL API which we use to retrieve the + // certificates, we instead need to implement our own VerifyPeerCertificate function + // that will verify that the certificate is OK. + InsecureSkipVerify: true, + VerifyPeerCertificate: genVerifyPeerCertificateFunc(name, certs), + MinVersion: tls.VersionTLS13, + } + + return fmt.Sprintf("%s:%d", addr, c.Port), cfg, version, nil +} + +// refreshCertAfter refreshes the epehemeral certificate of the instance after timeToRefresh. +func (c *Client) refreshCertAfter(instance string, timeToRefresh time.Duration) { + <-time.After(timeToRefresh) + logging.Verbosef("ephemeral certificate for instance %s will expire soon, refreshing now.", instance) + if _, _, _, err := c.cachedCfg(context.Background(), instance); err != nil { + logging.Errorf("failed to refresh the ephemeral certificate for %s before expiring: %v", instance, err) + } +} + +// genVerifyPeerCertificateFunc creates a VerifyPeerCertificate func that verifies that the peer +// certificate is in the cert pool. We need to define our own because of our sketchy non-standard +// CNs. +func genVerifyPeerCertificateFunc(instanceName string, pool *x509.CertPool) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if len(rawCerts) == 0 { + return fmt.Errorf("no certificate to verify") + } + + cert, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return fmt.Errorf("x509.ParseCertificate(rawCerts[0]) returned error: %v", err) + } + + opts := x509.VerifyOptions{Roots: pool} + if _, err = cert.Verify(opts); err != nil { + return err + } + + if cert.Subject.CommonName != instanceName { + return fmt.Errorf("certificate had CN %q, expected %q", cert.Subject.CommonName, instanceName) + } + return nil + } +} + +func isExpired(cfg *tls.Config) bool { + if cfg == nil { + return true + } + return time.Now().After(cfg.Certificates[0].Leaf.NotAfter) +} + +// startRefresh kicks off a refreshCfg asynchronously, that updates the cacheEntry and closes the returned channel once the refresh is completed. This function +// should only be called from the scope of "cachedCfg", which controls the logic around throttling refreshes. +func (c *Client) startRefresh(instance string, refreshCfgBuffer time.Duration) chan struct{} { + done := make(chan struct{}) + go func() { + defer close(done) + addr, cfg, ver, err := c.refreshCfg(instance) + + c.cacheL.Lock() + old := c.cfgCache[instance] + // if we failed to refresh cfg do not throw out potentially valid one + if err != nil && !isExpired(old.cfg) { + logging.Errorf("failed to refresh the ephemeral certificate for %s, returning previous cert instead: %v", instance, err) + addr, cfg, ver, err = old.addr, old.cfg, old.version, old.err + } + e := cacheEntry{ + lastRefreshed: time.Now(), + err: err, + addr: addr, + version: ver, + cfg: cfg, + done: done, + } + c.cfgCache[instance] = e + c.cacheL.Unlock() + + if !isValid(e) { + // Note: Future refreshes will not be scheduled unless another + // connection attempt is made. + logging.Errorf("failed to refresh the ephemeral certificate for %v: %v", instance, err) + return + } + + certExpiration := cfg.Certificates[0].Leaf.NotAfter + now := time.Now() + timeToRefresh := certExpiration.Sub(now) - refreshCfgBuffer + if timeToRefresh <= 0 { + // If a new certificate expires before our buffer has expired, we should wait a bit and schedule a new refresh to much closer to the expiration's date + // This situation probably only occurs when the oauth2 token isn't refreshed before the cert is, so by scheduling closer to the expiration we can hope the oauth2 token is newer. + timeToRefresh = certExpiration.Sub(now) - (5 * time.Second) + logging.Errorf("new ephemeral certificate expires sooner than expected (adjusting refresh time to compensate): current time: %v, certificate expires: %v", now, certExpiration) + } + logging.Infof("Scheduling refresh of ephemeral certificate in %s", timeToRefresh) + go c.refreshCertAfter(instance, timeToRefresh) + }() + return done +} + +// isValid returns true if the cacheEntry is still useable +func isValid(c cacheEntry) bool { + // the entry is only valid there wasn't an error retrieving it and it has a cfg + return c.err == nil && c.cfg != nil +} + +// InvalidError is an error from an instance connection that is invalid because +// its recent refresh attempt has failed, its TLS config is invalid, etc. +type InvalidError struct { + // instance is the instance connection name + instance string + // err is what makes the instance invalid + err error + // hasTLS reports whether the instance has a valid TLS config + hasTLS bool +} + +func (e *InvalidError) Error() string { + if e.hasTLS { + return e.instance + ": " + e.err.Error() + } + return e.instance + ": missing TLS config, " + e.err.Error() +} + +// InvalidInstances reports whether the existing connections have valid +// configuration. +func (c *Client) InvalidInstances() []*InvalidError { + c.cacheL.RLock() + defer c.cacheL.RUnlock() + + var invalid []*InvalidError + for instance, entry := range c.cfgCache { + var refreshInProgress bool + select { + case <-entry.done: + // refresh has already completed + default: + refreshInProgress = true + } + if !isValid(entry) && !refreshInProgress { + invalid = append(invalid, &InvalidError{ + instance: instance, + err: entry.err, + hasTLS: entry.cfg != nil, + }) + } + } + return invalid +} + +func needsRefresh(e cacheEntry, refreshCfgBuffer time.Duration) bool { + if e.done == nil { // no refresh started + return true + } + if !isValid(e) || e.cfg.Certificates[0].Leaf.NotAfter.Sub(time.Now()) <= refreshCfgBuffer { + // if the entry is invalid or close enough to expiring check + // use the entry's done channel to determine if a refresh has started yet + select { + case <-e.done: // last refresh completed, so it's time for a new one + return true + default: // new refresh already started, so we can wait on that + return false + } + } + return false +} + +func (c *Client) cachedCfg(ctx context.Context, instance string) (string, *tls.Config, string, error) { + c.cacheL.RLock() + + throttle := c.RefreshCfgThrottle + if throttle == 0 { + throttle = DefaultRefreshCfgThrottle + } + refreshCfgBuffer := c.RefreshCfgBuffer + if refreshCfgBuffer == 0 { + refreshCfgBuffer = DefaultRefreshCfgBuffer + } + + e := c.cfgCache[instance] + c.cacheL.RUnlock() + if needsRefresh(e, refreshCfgBuffer) { + // Reenter the critical section with intent to make changes + c.cacheL.Lock() + if c.cfgCache == nil { + c.cfgCache = make(map[string]cacheEntry) + } + if c.limiters == nil { + c.limiters = make(map[string]*rate.Limiter) + } + // the state may have changed between critical sections, so double check + e = c.cfgCache[instance] + limiter := c.limiters[instance] + if limiter == nil { + limiter = rate.NewLimiter(rate.Every(throttle), 2) + c.limiters[instance] = limiter + } + if needsRefresh(e, refreshCfgBuffer) { + if limiter.Allow() { + // start a new refresh and update the cachedEntry to reflect that + e.done = c.startRefresh(instance, refreshCfgBuffer) + e.lastRefreshed = time.Now() + c.cfgCache[instance] = e + } else { + // TODO: Investigate returning this as an error instead of just logging + logging.Infof("refresh operation throttled for %s: reusing config from last refresh (%s ago)", instance, time.Since(e.lastRefreshed)) + } + } + c.cacheL.Unlock() + } + + if !isValid(e) { + // if the previous result was invalid, wait for the next result to complete + select { + case <-ctx.Done(): + return "", nil, "", ctx.Err() + case <-e.done: + } + + c.cacheL.RLock() + // the state may have changed between critical sections, so double check + e = c.cfgCache[instance] + c.cacheL.RUnlock() + } + return e.addr, e.cfg, e.version, e.err +} + +// DialContext uses the configuration stored in the client to connect to an instance. +// If this func returns a nil error the connection is correctly authenticated +// to connect to the instance. +func (c *Client) DialContext(ctx context.Context, instance string) (net.Conn, error) { + addr, cfg, _, err := c.cachedCfg(ctx, instance) + if err != nil { + return nil, err + } + + // TODO: attempt an early refresh if an connect fails? + return c.tryConnect(ctx, addr, instance, cfg) +} + +// Dial does the same as DialContext but using context.Background() as the context. +func (c *Client) Dial(instance string) (net.Conn, error) { + return c.DialContext(context.Background(), instance) +} + +// ErrUnexpectedFailure indicates the internal refresh operation failed unexpectedly. +var ErrUnexpectedFailure = errors.New("ErrUnexpectedFailure") + +func (c *Client) tryConnect(ctx context.Context, addr, instance string, cfg *tls.Config) (net.Conn, error) { + // When multiple dial attempts start in quick succession, the internal + // refresh logic is sometimes subject to a race condition. If the first + // attempt fails on a handshake error, it will invalidate the cached config. + // In some cases, a second dial attempt will initiate a connection with an + // invalid config. This check fails fast in such cases. + if addr == "" { + return nil, ErrUnexpectedFailure + } + dial := c.selectDialer() + conn, err := dial(ctx, "tcp", addr) + if err != nil { + return nil, err + } + type setKeepAliver interface { + SetKeepAlive(keepalive bool) error + SetKeepAlivePeriod(d time.Duration) error + } + + if s, ok := conn.(setKeepAliver); ok { + if err := s.SetKeepAlive(true); err != nil { + logging.Verbosef("Couldn't set KeepAlive to true: %v", err) + } else if err := s.SetKeepAlivePeriod(keepAlivePeriod); err != nil { + logging.Verbosef("Couldn't set KeepAlivePeriod to %v", keepAlivePeriod) + } + } else { + logging.Verbosef("KeepAlive not supported: long-running tcp connections may be killed by the OS.") + } + + return c.connectTLS(ctx, conn, instance, cfg) +} + +func (c *Client) selectDialer() func(context.Context, string, string) (net.Conn, error) { + if c.ContextDialer != nil { + return c.ContextDialer + } + + if c.Dialer != nil { + return func(_ context.Context, net, addr string) (net.Conn, error) { + return c.Dialer(net, addr) + } + } + + dialer := proxy.FromEnvironment() + if ctxDialer, ok := dialer.(proxy.ContextDialer); ok { + // although proxy.FromEnvironment() returns a Dialer interface which only has a Dial method, + // it happens in fact that method often returns ContextDialers. + return ctxDialer.DialContext + } + + return func(_ context.Context, net, addr string) (net.Conn, error) { + return dialer.Dial(net, addr) + } +} + +func (c *Client) invalidateCfg(cfg *tls.Config, instance string, err error) { + c.cacheL.RLock() + e := c.cfgCache[instance] + c.cacheL.RUnlock() + if e.cfg != cfg { + return + } + c.cacheL.Lock() + defer c.cacheL.Unlock() + e = c.cfgCache[instance] + // the state may have changed between critical sections, so double check + if e.cfg != cfg { + return + } + err = fmt.Errorf("config invalidated after TLS handshake failed, error = %w", err) + c.cfgCache[instance] = cacheEntry{ + err: err, + done: e.done, + lastRefreshed: e.lastRefreshed, + } +} + +// NewConnSrc returns a chan which can be used to receive connections +// on the passed Listener. All requests sent to the returned chan will have the +// instance name provided here. The chan will be closed if the Listener returns +// an error. +func NewConnSrc(instance string, l net.Listener) <-chan Conn { + ch := make(chan Conn) + go func() { + for { + start := time.Now() + c, err := l.Accept() + if err != nil { + logging.Errorf("listener (%#v) had error: %v", l, err) + if nerr, ok := err.(net.Error); ok && nerr.Temporary() { + d := 10*time.Millisecond - time.Since(start) + if d > 0 { + time.Sleep(d) + } + continue + } + l.Close() + close(ch) + return + } + ch <- Conn{instance, c} + } + }() + return ch +} + +// InstanceVersion uses client cache to return instance version string. +// +// Deprecated: Use Client.InstanceVersionContext instead. +func (c *Client) InstanceVersion(instance string) (string, error) { + return c.InstanceVersionContext(context.Background(), instance) +} + +// InstanceVersionContext uses client cache to return instance version string. +func (c *Client) InstanceVersionContext(ctx context.Context, instance string) (string, error) { + _, _, version, err := c.cachedCfg(ctx, instance) + if err != nil { + return "", err + } + return version, nil +} + +// ParseInstanceConnectionName verifies that instances are in the expected format and include +// the necessary components. +func ParseInstanceConnectionName(instance string) (string, string, string, []string, error) { + args := strings.Split(instance, "=") + if len(args) > 2 { + return "", "", "", nil, fmt.Errorf("invalid instance argument: must be either form - `` or `=`; invalid arg was %q", instance) + } + // Parse the instance connection name - everything before the "=". + proj, region, name := util.SplitName(args[0]) + if proj == "" || region == "" || name == "" { + return "", "", "", nil, fmt.Errorf("invalid instance connection string: must be in the form `project:region:instance-name`; invalid name was %q", args[0]) + } + return proj, region, name, args, nil +} + +// GetInstances iterates through the client cache, returning a list of previously dialed +// instances. +func (c *Client) GetInstances() []string { + var insts []string + c.cacheL.Lock() + cfgCache := c.cfgCache + c.cacheL.Unlock() + for i := range cfgCache { + insts = append(insts, i) + } + return insts +} + +// AvailableConn returns false if MaxConnections has been reached, true otherwise. +// When MaxConnections is 0, there is no limit. +func (c *Client) AvailableConn() bool { + return c.MaxConnections == 0 || atomic.LoadUint64(&c.ConnectionsCounter) < c.MaxConnections +} + +// Shutdown waits up to a given amount of time for all active connections to +// close. Returns an error if there are still active connections after waiting +// for the whole length of the timeout. +func (c *Client) Shutdown(termTimeout time.Duration) error { + term, ticker := time.After(termTimeout), time.NewTicker(100*time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if atomic.LoadUint64(&c.ConnectionsCounter) > 0 { + continue + } + case <-term: + } + break + } + + active := atomic.LoadUint64(&c.ConnectionsCounter) + if active == 0 { + return nil + } + return fmt.Errorf("%d active connections still exist after waiting for %v", active, termTimeout) +} diff --git a/proxy/proxy/client_test.go b/proxy/proxy/client_test.go new file mode 100644 index 0000000000..123b5ae77c --- /dev/null +++ b/proxy/proxy/client_test.go @@ -0,0 +1,637 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package proxy + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http/httptest" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + "unsafe" +) + +const instance = "project:region:instance" + +var ( + sentinelError = errors.New("sentinel error") + forever = time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC) +) + +type fakeCerts struct { + sync.Mutex + called int +} + +type blockingCertSource struct { + values map[string]*fakeCerts + validUntil time.Time +} + +func (cs *blockingCertSource) Local(instance string) (tls.Certificate, error) { + v, ok := cs.values[instance] + if !ok { + return tls.Certificate{}, fmt.Errorf("test setup failure: unknown instance %q", instance) + } + v.Lock() + v.called++ + v.Unlock() + + // Returns a cert which is valid forever. + return tls.Certificate{ + Leaf: &x509.Certificate{ + NotAfter: cs.validUntil, + }, + }, nil +} + +func (cs *blockingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { + return &x509.Certificate{}, "fake address", "fake name", "fake version", nil +} + +func newCertSource(certs *fakeCerts, expiration time.Time) CertSource { + return &blockingCertSource{ + values: map[string]*fakeCerts{ + instance: certs, + }, + validUntil: expiration, + } +} + +func newClient(cs CertSource) *Client { + return &Client{ + Certs: cs, + Dialer: func(string, string) (net.Conn, error) { + return nil, sentinelError + }, + } +} + +func TestContextDialer(t *testing.T) { + cs := newCertSource(&fakeCerts{}, forever) + c := newClient(cs) + + c.ContextDialer = func(context.Context, string, string) (net.Conn, error) { + return nil, sentinelError + } + c.Dialer = func(string, string) (net.Conn, error) { + return nil, fmt.Errorf("this dialer should not be used when ContextDialer is set") + } + + if _, err := c.DialContext(context.Background(), instance); err != sentinelError { + t.Errorf("unexpected error: %v", err) + } +} + +func TestClientCache(t *testing.T) { + b := &fakeCerts{} + c := newClient(newCertSource(b, forever)) + + for i := 0; i < 5; i++ { + if _, err := c.Dial(instance); err != sentinelError { + t.Errorf("unexpected error: %v", err) + } + } + + b.Lock() + if b.called != 1 { + t.Errorf("called %d times, want called 1 time", b.called) + } + b.Unlock() +} + +func TestInvalidateConfigCache(t *testing.T) { + srv := httptest.NewTLSServer(nil) + defer srv.Close() + b := &fakeCerts{} + c := &Client{ + Certs: newCertSource(b, forever), + Dialer: func(string, string) (net.Conn, error) { + return net.Dial( + srv.Listener.Addr().Network(), + srv.Listener.Addr().String(), + ) + }, + } + c.cachedCfg(context.Background(), instance) + if needsRefresh(c.cfgCache[instance], DefaultRefreshCfgBuffer) { + t.Error("cached config expected to be valid") + } + _, err := c.Dial(instance) + if err == nil { + t.Errorf("c.Dial(%q) expected to fail with handshake error", instance) + } + if !needsRefresh(c.cfgCache[instance], DefaultRefreshCfgBuffer) { + t.Error("cached config expected to be invalidated after handshake error") + } +} + +func TestValidClient(t *testing.T) { + someErr := errors.New("error") + openCh := make(chan struct{}) + closedCh := make(chan struct{}) + close(closedCh) + + equalErrors := func(a, b []*InvalidError) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].instance != b[i].instance { + return false + } + if a[i].err != b[i].err { + return false + } + if a[i].hasTLS != b[i].hasTLS { + return false + } + } + return true + } + + testCases := []struct { + desc string + cache map[string]cacheEntry + want []*InvalidError + }{ + { + desc: "when the cache has only valid entries", + cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{cfg: &tls.Config{}, done: closedCh}}, + want: nil, + }, + { + desc: "when the cache has invalid TLS entries", + cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{done: closedCh}}, + want: []*InvalidError{&InvalidError{instance: "proj:region:inst", hasTLS: false}}, + }, + { + desc: "when the cache has errored entries", + cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{err: someErr, done: closedCh}}, + want: []*InvalidError{&InvalidError{instance: "proj:region:inst", hasTLS: false, err: someErr}}, + }, + { + desc: "when the cache has an entry with an in-progress refresh", + cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{err: someErr, done: openCh}}, + want: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client := &Client{cfgCache: tc.cache} + if got := client.InvalidInstances(); !equalErrors(got, tc.want) { + t.Errorf("want = %v, got = %v", tc.want, got) + } + }) + } +} + +func TestConcurrentRefresh(t *testing.T) { + b := &fakeCerts{} + c := newClient(newCertSource(b, forever)) + + ch := make(chan error) + b.Lock() + + const numDials = 20 + + for i := 0; i < numDials; i++ { + go func() { + _, err := c.Dial(instance) + ch <- err + }() + } + + b.Unlock() + + for i := 0; i < numDials; i++ { + if err := <-ch; err != sentinelError { + t.Errorf("unexpected error: %v", err) + } + } + b.Lock() + if b.called != 1 { + t.Errorf("called %d times, want called 1 time", b.called) + } + b.Unlock() +} + +func TestMaximumConnectionsCount(t *testing.T) { + certSource := &blockingCertSource{ + values: map[string]*fakeCerts{}, + validUntil: forever, + } + c := newClient(certSource) + + const maxConnections = 10 + c.MaxConnections = maxConnections + var dials uint64 + firstDialExited := make(chan struct{}) + c.Dialer = func(string, string) (net.Conn, error) { + atomic.AddUint64(&dials, 1) + // Wait until the first dial fails to ensure the max connections count + // is reached by a concurrent dialer + <-firstDialExited + return nil, sentinelError + } + + // Build certSource.values before creating goroutines to avoid concurrent map read and map write + const numConnections = maxConnections + 1 + instanceNames := make([]string, numConnections) + for i := 0; i < numConnections; i++ { + // Vary instance name to bypass config cache and avoid second call to Client.tryConnect() in Client.Dial() + instanceName := fmt.Sprintf("%s-%d", instance, i) + certSource.values[instanceName] = &fakeCerts{} + instanceNames[i] = instanceName + } + + var wg sync.WaitGroup + var firstDialOnce sync.Once + for _, instanceName := range instanceNames { + wg.Add(1) + go func(instanceName string) { + defer wg.Done() + + conn := Conn{ + Instance: instanceName, + Conn: &dummyConn{}, + } + c.handleConn(context.Background(), conn) + + firstDialOnce.Do(func() { close(firstDialExited) }) + }(instanceName) + } + + wg.Wait() + + switch { + case dials > maxConnections: + t.Errorf("client should have refused to dial new connection on %dth attempt when the maximum of %d connections was reached (%d dials)", numConnections, maxConnections, dials) + case dials == maxConnections: + t.Logf("client has correctly refused to dial new connection on %dth attempt when the maximum of %d connections was reached (%d dials)\n", numConnections, maxConnections, dials) + case dials < maxConnections: + t.Errorf("client should have dialed exactly the maximum of %d connections (%d connections, %d dials)", maxConnections, numConnections, dials) + } +} + +func TestShutdownTerminatesEarly(t *testing.T) { + cs := newCertSource(&fakeCerts{}, forever) + c := newClient(cs) + // Ensure the dialer returns no error. + c.Dialer = func(string, string) (net.Conn, error) { + return nil, nil + } + + shutdown := make(chan bool, 1) + go func() { + c.Shutdown(1) + shutdown <- true + }() + shutdownFinished := false + // In case the code is actually broken and the client doesn't shut down quickly, don't cause the test to hang until it times out. + select { + case <-time.After(100 * time.Millisecond): + case shutdownFinished = <-shutdown: + } + if !shutdownFinished { + t.Errorf("shutdown should have completed quickly because there are no active connections") + } +} + +func TestRefreshTimer(t *testing.T) { + timeToExpire := 2 * time.Second + certCreated := time.Now() + cs := newCertSource(&fakeCerts{}, certCreated.Add(timeToExpire)) + c := newClient(cs) + + c.RefreshCfgThrottle = 20 * time.Millisecond + c.RefreshCfgBuffer = time.Second + + // Call Dial to cache the cert. + if _, err := c.Dial(instance); err != sentinelError { + t.Fatalf("Dial(%s) failed: %v", instance, err) + } + c.cacheL.Lock() + cfg, ok := c.cfgCache[instance] + c.cacheL.Unlock() + if !ok { + t.Fatalf("expected instance to be cached") + } + + time.Sleep(timeToExpire - time.Since(certCreated)) + // Check if cert was refreshed in the background, without calling Dial again. + c.cacheL.Lock() + newCfg, ok := c.cfgCache[instance] + c.cacheL.Unlock() + if !ok { + t.Fatalf("expected instance to be cached") + } + if !newCfg.lastRefreshed.After(cfg.lastRefreshed) { + t.Error("expected cert to be refreshed.") + } +} + +func TestSyncAtomicAlignment(t *testing.T) { + // The sync/atomic pkg has a bug that requires the developer to guarantee + // 64-bit alignment when using 64-bit functions on 32-bit systems. + c := &Client{} + if a := unsafe.Offsetof(c.ConnectionsCounter); a%64 != 0 { + t.Errorf("Client.ConnectionsCounter is not aligned: want %v, got %v", 0, a) + } +} + +type invalidRemoteCertSource struct{} + +func (cs *invalidRemoteCertSource) Local(instance string) (tls.Certificate, error) { + return tls.Certificate{}, nil +} + +func (cs *invalidRemoteCertSource) Remote(instance string) (*x509.Certificate, string, string, string, error) { + return nil, "", "", "", sentinelError +} + +func TestRemoteCertError(t *testing.T) { + c := newClient(&invalidRemoteCertSource{}) + + _, err := c.DialContext(context.Background(), instance) + if err != sentinelError { + t.Errorf("expected sentinel error, got %v", err) + } + +} + +func TestParseInstanceConnectionName(t *testing.T) { + // SplitName has its own tests and is not specifically tested here. + table := []struct { + in string + wantErrorStr string + }{ + {"proj:region:my-db", ""}, + {"proj:region:my-db=options", ""}, + {"proj=region=my-db", "invalid instance argument: must be either form - `` or `=`; invalid arg was \"proj=region=my-db\""}, + {"projregionmy-db", "invalid instance connection string: must be in the form `project:region:instance-name`; invalid name was \"projregionmy-db\""}, + } + + for _, test := range table { + _, _, _, _, gotError := ParseInstanceConnectionName(test.in) + var gotErrorStr string + if gotError != nil { + gotErrorStr = gotError.Error() + } + if gotErrorStr != test.wantErrorStr { + t.Errorf("ParseInstanceConnectionName(%q): got \"%v\" for error, want \"%v\"", test.in, gotErrorStr, test.wantErrorStr) + } + } +} + +type localhostCertSource struct { +} + +func (c localhostCertSource) Local(instance string) (tls.Certificate, error) { + return tls.Certificate{ + Leaf: &x509.Certificate{ + NotAfter: forever, + }, + }, nil +} + +func (c localhostCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { + return &x509.Certificate{}, "localhost", "fake name", "fake version", nil +} + +var _ CertSource = &localhostCertSource{} + +func TestClientHandshakeCanceled(t *testing.T) { + errorIsDeadlineOrTimeout := func(err error) bool { + if errors.Is(err, context.Canceled) { + return true + } + if errors.Is(err, context.DeadlineExceeded) { + return true + } + if strings.Contains(err.Error(), "i/o timeout") { + // We should use os.ErrDeadlineExceeded exceeded here, + // but it is not present in Go versions below 1.15. + return true + } + return false + } + + withTestHarness := func(t *testing.T, f func(port int)) { + // serverShutdown is closed to free the server + // goroutine that is holding up the client request. + serverShutdown := make(chan struct{}) + + l, err := tls.Listen( + "tcp", + ":", + &tls.Config{ + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + // Make the client wait forever to handshake. + <-serverShutdown + return nil, errors.New("some error") + }, + }) + if err != nil { + t.Fatalf("tls.Listen: %v", err) + } + + port := l.Addr().(*net.TCPAddr).Port + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + for { + conn, err := l.Accept() + if err != nil { + // Below Go 1.16, we have to string match here. + // https://golang.org/doc/go1.16#net + if !strings.Contains(err.Error(), "use of closed network connection") { + t.Errorf("l.Accept: %v", err) + } + return + } + + _, _ = ioutil.ReadAll(conn) // Trigger the handshake. + _ = conn.Close() + } + }() + + f(port) + close(serverShutdown) // Free the server thread. + _ = l.Close() + wg.Wait() + } + + validateError := func(t *testing.T, err error) { + if err == nil { + t.Fatal("nil error unexpected") + } + if !errorIsDeadlineOrTimeout(err) { + t.Fatalf("unexpected error: %v", err) + } + } + + newClient := func(port int) *Client { + return &Client{ + Port: port, + Certs: &localhostCertSource{}, + } + } + + // Makes it to Handshake. + t.Run("with timeout", func(t *testing.T) { + withTestHarness(t, func(port int) { + c := newClient(port) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + _, err := c.DialContext(ctx, instance) + validateError(t, err) + }) + }) + + t.Run("when liveness check is called on invalidated config", func(t *testing.T) { + withTestHarness(t, func(port int) { + c := newClient(port) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + _, err := c.DialContext(ctx, instance) + if err == nil { + t.Fatal("expected DialContext to fail, got no error") + } + + invalid := c.InvalidInstances() + if gotLen := len(invalid); gotLen != 1 { + t.Fatalf("invalid instance want = 1, got = %v", gotLen) + } + got := invalid[0] + if got.err == nil { + t.Fatal("want invalid instance error, got nil") + } + }) + }) + + // Makes it to Handshake. + // Same as the above but the context doesn't have a deadline, + // it is canceled manually after a while. + t.Run("canceled after a while, no deadline", func(t *testing.T) { + withTestHarness(t, func(port int) { + c := newClient(port) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + time.AfterFunc(3*time.Second, cancel) + + _, err := c.DialContext(ctx, instance) + validateError(t, err) + }) + + }) + + // Doesn't make it to Handshake. + t.Run("with short timeout", func(t *testing.T) { + withTestHarness(t, func(port int) { + c := newClient(port) + + ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) + defer cancel() + + _, err := c.DialContext(ctx, instance) + validateError(t, err) + }) + }) + + // Doesn't make it to Handshake. + t.Run("canceled without timeout", func(t *testing.T) { + withTestHarness(t, func(port int) { + c := newClient(port) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := c.DialContext(ctx, instance) + validateError(t, err) + }) + }) +} + +func TestConnectingWithInvalidConfig(t *testing.T) { + c := &Client{} + + _, err := c.tryConnect(context.Background(), "", "myinstance", &tls.Config{}) + if err != ErrUnexpectedFailure { + t.Fatalf("wanted ErrUnexpectedFailure, got = %v", err) + } +} + +var ( + errLocal = errors.New("local failed") + errRemote = errors.New("remote failed") +) + +type failingCertSource struct{} + +func (cs failingCertSource) Local(instance string) (tls.Certificate, error) { + return tls.Certificate{}, errLocal +} + +func (cs failingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { + return nil, "", "", "", errRemote +} + +func TestInstanceVersionContext(t *testing.T) { + testCases := []struct { + certSource CertSource + wantErr error + wantVersion string + }{ + { + certSource: newCertSource(&fakeCerts{}, forever), + wantErr: nil, + wantVersion: "fake version", + }, + { + certSource: failingCertSource{}, + wantErr: errLocal, + wantVersion: "", + }, + } + for _, tc := range testCases { + c := newClient(tc.certSource) + v, err := c.InstanceVersionContext(context.Background(), instance) + if v != tc.wantVersion { + t.Fatalf("want version = %v, got version = %v", tc.wantVersion, v) + } + if err != tc.wantErr { + t.Fatalf("want = %v, got = %v", tc.wantErr, err) + } + } +} diff --git a/proxy/proxy/common.go b/proxy/proxy/common.go new file mode 100644 index 0000000000..c5b6c034be --- /dev/null +++ b/proxy/proxy/common.go @@ -0,0 +1,225 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package proxy implements client and server code for proxying an unsecure connection over SSL. +package proxy + +import ( + "bytes" + "errors" + "fmt" + "io" + "net" + "sync" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" +) + +// SQLScope is the Google Cloud Platform scope required for executing API +// calls to Cloud SQL. +const SQLScope = "https://www.googleapis.com/auth/sqlservice.admin" + +// myCopy is similar to io.Copy, but reports whether the returned error was due +// to a bad read or write. The returned error will never be nil +func myCopy(dst io.Writer, src io.Reader) (readErr bool, err error) { + buf := make([]byte, 4096) + for { + n, err := src.Read(buf) + if n > 0 { + if _, werr := dst.Write(buf[:n]); werr != nil { + if err == nil { + return false, werr + } + // Read and write error; just report read error (it happened first). + return true, err + } + } + if err != nil { + return true, err + } + } +} + +func copyError(readDesc, writeDesc string, readErr bool, err error) { + var desc string + if readErr { + desc = "Reading data from " + readDesc + } else { + desc = "Writing data to " + writeDesc + } + logging.Errorf("%v had error: %v", desc, err) +} + +func copyThenClose(remote, local io.ReadWriteCloser, remoteDesc, localDesc string) { + firstErr := make(chan error, 1) + + go func() { + readErr, err := myCopy(remote, local) + select { + case firstErr <- err: + if readErr && err == io.EOF { + logging.Verbosef("Client closed %v", localDesc) + } else { + copyError(localDesc, remoteDesc, readErr, err) + } + remote.Close() + local.Close() + default: + } + }() + + readErr, err := myCopy(local, remote) + select { + case firstErr <- err: + if readErr && err == io.EOF { + logging.Verbosef("Instance %v closed connection", remoteDesc) + } else { + copyError(remoteDesc, localDesc, readErr, err) + } + remote.Close() + local.Close() + default: + // In this case, the other goroutine exited first and already printed its + // error (and closed the things). + } +} + +// NewConnSet initializes a new ConnSet and returns it. +func NewConnSet() *ConnSet { + return &ConnSet{m: make(map[string][]net.Conn)} +} + +// A ConnSet tracks net.Conns associated with a provided ID. +// A nil ConnSet will be a no-op for all methods called on it. +type ConnSet struct { + sync.RWMutex + m map[string][]net.Conn +} + +// String returns a debug string for the ConnSet. +func (c *ConnSet) String() string { + if c == nil { + return "" + } + var b bytes.Buffer + + c.RLock() + for id, conns := range c.m { + fmt.Fprintf(&b, "ID %s:", id) + for i, c := range conns { + fmt.Fprintf(&b, "\n\t%d: %v", i, c) + } + } + c.RUnlock() + + return b.String() +} + +// Add saves the provided conn and associates it with the given string +// identifier. +func (c *ConnSet) Add(id string, conn net.Conn) { + if c == nil { + return + } + c.Lock() + c.m[id] = append(c.m[id], conn) + c.Unlock() +} + +// IDs returns a slice of all identifiers which still have active connections. +func (c *ConnSet) IDs() []string { + if c == nil { + return nil + } + ret := make([]string, 0, len(c.m)) + + c.RLock() + for k := range c.m { + ret = append(ret, k) + } + c.RUnlock() + + return ret +} + +// Conns returns all active connections associated with the provided ids. +func (c *ConnSet) Conns(ids ...string) []net.Conn { + if c == nil { + return nil + } + var ret []net.Conn + + c.RLock() + for _, id := range ids { + ret = append(ret, c.m[id]...) + } + c.RUnlock() + + return ret +} + +// Remove undoes an Add operation to have the set forget about a conn. Do not +// Remove an id/conn pair more than it has been Added. +func (c *ConnSet) Remove(id string, conn net.Conn) error { + if c == nil { + return nil + } + c.Lock() + defer c.Unlock() + + pos := -1 + conns := c.m[id] + for i, cc := range conns { + if cc == conn { + pos = i + break + } + } + + if pos == -1 { + return fmt.Errorf("couldn't find connection %v for id %s", conn, id) + } + + if len(conns) == 1 { + delete(c.m, id) + } else { + c.m[id] = append(conns[:pos], conns[pos+1:]...) + } + + return nil +} + +// Close closes every net.Conn contained in the set. +func (c *ConnSet) Close() error { + if c == nil { + return nil + } + var errs bytes.Buffer + + c.Lock() + for id, conns := range c.m { + for _, c := range conns { + if err := c.Close(); err != nil { + fmt.Fprintf(&errs, "%s close error: %v\n", id, err) + } + } + } + c.Unlock() + + if errs.Len() == 0 { + return nil + } + + return errors.New(errs.String()) +} diff --git a/proxy/proxy/common_test.go b/proxy/proxy/common_test.go new file mode 100644 index 0000000000..b20318e0b7 --- /dev/null +++ b/proxy/proxy/common_test.go @@ -0,0 +1,115 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file contains tests for common.go + +package proxy + +import ( + "net" + "reflect" + "testing" +) + +var c1, c2, c3 = &dummyConn{}, &dummyConn{}, &dummyConn{} + +type dummyConn struct{ net.Conn } + +func (c dummyConn) Close() error { + return nil +} + +func TestConnSetAdd(t *testing.T) { + s := NewConnSet() + + s.Add("a", c1) + aSlice := []string{"a"} + if !reflect.DeepEqual(s.IDs(), aSlice) { + t.Fatalf("got %v, want %v", s.IDs(), aSlice) + } + + s.Add("a", c2) + if !reflect.DeepEqual(s.IDs(), aSlice) { + t.Fatalf("got %v, want %v", s.IDs(), aSlice) + } + + s.Add("b", c3) + ids := s.IDs() + if len(ids) != 2 { + t.Fatalf("got %d ids, wanted 2", len(ids)) + } + ok := ids[0] == "a" && ids[1] == "b" || + ids[1] == "a" && ids[0] == "b" + + if !ok { + t.Fatalf(`got %v, want only "a" and "b"`, ids) + } +} + +func TestConnSetRemove(t *testing.T) { + s := NewConnSet() + + s.Add("a", c1) + s.Add("a", c2) + s.Add("b", c3) + + s.Remove("b", c3) + if got := s.Conns("b"); got != nil { + t.Fatalf("got %v, want nil", got) + } + + aSlice := []string{"a"} + if !reflect.DeepEqual(s.IDs(), aSlice) { + t.Fatalf("got %v, want %v", s.IDs(), aSlice) + } + + s.Remove("a", c1) + if !reflect.DeepEqual(s.IDs(), aSlice) { + t.Fatalf("got %v, want %v", s.IDs(), aSlice) + } + + s.Remove("a", c2) + if len(s.IDs()) != 0 { + t.Fatalf("got %v, want empty set", s.IDs()) + } +} + +func TestConns(t *testing.T) { + s := NewConnSet() + + s.Add("a", c1) + s.Add("a", c2) + s.Add("b", c3) + + got := s.Conns("b") + if !reflect.DeepEqual(got, []net.Conn{c3}) { + t.Fatalf("got %v, wanted only %v", got, c3) + } + + looking := map[net.Conn]bool{ + c1: true, + c2: true, + c3: true, + } + + for _, v := range s.Conns("a", "b") { + if _, ok := looking[v]; !ok { + t.Errorf("got unexpected conn %v", v) + } + delete(looking, v) + } + if len(looking) != 0 { + t.Fatalf("didn't find %v in list of Conns", looking) + } +} diff --git a/proxy/proxy/connect_tls_117.go b/proxy/proxy/connect_tls_117.go new file mode 100644 index 0000000000..a1d5802173 --- /dev/null +++ b/proxy/proxy/connect_tls_117.go @@ -0,0 +1,45 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.17 +// +build go1.17 + +package proxy + +import ( + "context" + "crypto/tls" + "net" +) + +// connectTLS returns a new TLS client side connection +// using conn as the underlying transport. +// +// The returned connection has already completed its TLS handshake. +func (c *Client) connectTLS( + ctx context.Context, + conn net.Conn, + instance string, + cfg *tls.Config, +) (net.Conn, error) { + ret := tls.Client(conn, cfg) + // HandshakeContext was introduced in Go 1.17, hence + // this file is conditionally compiled on only Go versions >= 1.17. + if err := ret.HandshakeContext(ctx); err != nil { + _ = ret.Close() + c.invalidateCfg(cfg, instance, err) + return nil, err + } + return ret, nil +} diff --git a/proxy/proxy/connect_tls_other.go b/proxy/proxy/connect_tls_other.go new file mode 100644 index 0000000000..574bcd5647 --- /dev/null +++ b/proxy/proxy/connect_tls_other.go @@ -0,0 +1,113 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !go1.17 +// +build !go1.17 + +package proxy + +import ( + "context" + "crypto/tls" + "net" + "sync" + "time" +) + +type cancelationWatcher struct { + done chan struct{} // closed when the caller requests shutdown by calling stop(). + wg sync.WaitGroup +} + +// newCancelationWatcher starts a goroutine that will monitor +// ctx for cancelation. If ctx is canceled, the I/O +// deadline on conn is set to some point in the past, canceling +// ongoing I/O and refusing new I/O. +// +// The caller must call stop() on the returned struct to +// release resources associated with this. +func newCancelationWatcher(ctx context.Context, conn net.Conn) *cancelationWatcher { + cw := &cancelationWatcher{ + done: make(chan struct{}), + } + // Monitor for context cancelation. + cw.wg.Add(1) + go func() { + defer cw.wg.Done() + + select { + case <-ctx.Done(): + // Set the deadline to some point in the past, but not + // the zero value. This will cancel ongoing requests + // and refuse future ones. + _ = conn.SetDeadline(time.Time{}.Add(1)) + case <-cw.done: + return + } + }() + return cw +} + +// stop shuts down this cancelationWatcher and releases +// the resources associated with it. +// +// Once stop has returned, the provided context is no longer +// watched for cancelation and the deadline on the +// provided net.Conn is no longer manipulated. +func (cw *cancelationWatcher) stop() { + close(cw.done) + cw.wg.Wait() +} + +// connectTLS returns a new TLS client side connection +// using conn as the underlying transport. +// +// The returned connection has already completed its TLS handshake. +func (c *Client) connectTLS( + ctx context.Context, + conn net.Conn, + instance string, + cfg *tls.Config, +) (net.Conn, error) { + // For the purposes of this Handshake, manipulate the I/O + // deadlines on this connection inline. We have to do this + // manual dance because we don't have HandshakeContext in this + // version of Go. + + defer func() { + // The connection didn't originally have a read deadline (we + // just created it). So no matter what happens here, restore + // the lack-of-deadline. + // + // In other words, only apply the deadline while dialing, + // not during subsequent usage. + _ = conn.SetDeadline(time.Time{}) + }() + + // If we have a context deadline, apply it. + if dl, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(dl) + } + + cw := newCancelationWatcher(ctx, conn) + defer cw.stop() // Always free the context watcher. + + ret := tls.Client(conn, cfg) + if err := ret.Handshake(); err != nil { + _ = ret.Close() + c.invalidateCfg(cfg, instance, err) + return nil, err + } + return ret, nil +} diff --git a/proxy/proxy/dial.go b/proxy/proxy/dial.go new file mode 100644 index 0000000000..d83614db5c --- /dev/null +++ b/proxy/proxy/dial.go @@ -0,0 +1,115 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package proxy + +import ( + "fmt" + "net" + "net/http" + "sync" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/certs" + "golang.org/x/net/context" + "golang.org/x/oauth2/google" +) + +// The port that CloudSQL expects the client to connect to. +const DefaultPort = 3307 + +var dialClient struct { + // This client is initialized in Init/InitWithClient/InitDefault + // and read in Dial. + c *Client + sync.Mutex +} + +// Dial returns a net.Conn connected to the Cloud SQL Instance specified. The +// format of 'instance' is "project-name:region:instance-name". +// +// If one of the Init functions hasn't been called yet, InitDefault is called. +// +// This is a network-level function; consider looking in the dialers +// subdirectory for more convenience functions related to actually logging into +// your database. +func DialContext(ctx context.Context, instance string) (net.Conn, error) { + dialClient.Lock() + c := dialClient.c + dialClient.Unlock() + if c == nil { + if err := InitDefault(ctx); err != nil { + return nil, fmt.Errorf("default proxy initialization failed; consider calling proxy.Init explicitly: %v", err) + } + // InitDefault initialized the client. + dialClient.Lock() + c = dialClient.c + dialClient.Unlock() + } + + return c.DialContext(ctx, instance) +} + +// Dial does the same as DialContext but using context.Background() as the context. +func Dial(instance string) (net.Conn, error) { + return DialContext(context.Background(), instance) +} + +// Dialer is a convenience type to model the standard 'Dial' function. +type Dialer func(net, addr string) (net.Conn, error) + +// Init must be called before Dial is called. This is a more flexible version +// of InitDefault, but allows you to set more fields. +// +// The http.Client is used to authenticate API requests. +// The connset parameter is optional. +// If the dialer is nil, net.Conn is used. +// Use InitWithClient to with a filled client if you want to provide a Context-Aware dialer +func Init(auth *http.Client, connset *ConnSet, dialer Dialer) { + dialClient.Lock() + dialClient.c = &Client{ + Port: DefaultPort, + Certs: certs.NewCertSource("", auth, true), + Conns: connset, + Dialer: dialer, + } + dialClient.Unlock() +} + +// InitClient is similar to Init, but allows you to specify the Client +// directly. + +// Deprecated: Use InitWithClient instead. +func InitClient(c Client) { + dialClient.Lock() + dialClient.c = &c + dialClient.Unlock() +} + +// InitWithClient specifies the Client directly. +func InitWithClient(c *Client) { + dialClient.Lock() + dialClient.c = c + dialClient.Unlock() +} + +// InitDefault attempts to initialize the Dial function using application +// default credentials. +func InitDefault(ctx context.Context) error { + cl, err := google.DefaultClient(ctx, "https://www.googleapis.com/auth/sqlservice.admin") + if err != nil { + return err + } + Init(cl, nil, nil) + return nil +} diff --git a/proxy/util/cloudsqlutil.go b/proxy/util/cloudsqlutil.go new file mode 100644 index 0000000000..d8d3bc7c88 --- /dev/null +++ b/proxy/util/cloudsqlutil.go @@ -0,0 +1,45 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package util contains utility functions for use throughout the Cloud SQL Auth proxy. +package util + +import "strings" + +// SplitName splits a fully qualified instance into its project, region, and +// instance name components. While we make the transition to regionalized +// metadata, the region is optional. +// +// Examples: +// "proj:region:my-db" -> ("proj", "region", "my-db") +// "google.com:project:region:instance" -> ("google.com:project", "region", "instance") +// "google.com:missing:part" -> ("google.com:missing", "", "part") +func SplitName(instance string) (project, region, name string) { + spl := strings.Split(instance, ":") + if len(spl) < 2 { + return "", "", instance + } + if dot := strings.Index(spl[0], "."); dot != -1 { + spl[1] = spl[0] + ":" + spl[1] + spl = spl[1:] + } + switch { + case len(spl) < 2: + return "", "", instance + case len(spl) == 2: + return spl[0], "", spl[1] + default: + return spl[0], spl[1], spl[2] + } +} diff --git a/proxy/util/cloudsqlutil_test.go b/proxy/util/cloudsqlutil_test.go new file mode 100644 index 0000000000..d614f33db8 --- /dev/null +++ b/proxy/util/cloudsqlutil_test.go @@ -0,0 +1,38 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import "testing" + +func TestSplitName(t *testing.T) { + table := []struct{ in, wantProj, wantRegion, wantInstance string }{ + {"proj:region:my-db", "proj", "region", "my-db"}, + {"google.com:project:region:instance", "google.com:project", "region", "instance"}, + {"google.com:missing:part", "google.com:missing", "", "part"}, + } + + for _, test := range table { + gotProj, gotRegion, gotInstance := SplitName(test.in) + if gotProj != test.wantProj { + t.Errorf("splitName(%q): got %v for project, want %v", test.in, gotProj, test.wantProj) + } + if gotRegion != test.wantRegion { + t.Errorf("splitName(%q): got %v for region, want %v", test.in, gotRegion, test.wantRegion) + } + if gotInstance != test.wantInstance { + t.Errorf("splitName(%q): got %v for instance, want %v", test.in, gotInstance, test.wantInstance) + } + } +} diff --git a/proxy/util/gcloudutil.go b/proxy/util/gcloudutil.go new file mode 100644 index 0000000000..a22b3ff9f0 --- /dev/null +++ b/proxy/util/gcloudutil.go @@ -0,0 +1,123 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "runtime" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" + "golang.org/x/oauth2" + exec "golang.org/x/sys/execabs" +) + +// GcloudConfigData represents the data returned by `gcloud config config-helper`. +type GcloudConfigData struct { + Configuration struct { + Properties struct { + Core struct { + Project string + Account string + } + } + } + Credential struct { + AccessToken string `json:"access_token"` + TokenExpiry time.Time `json:"token_expiry"` + } +} + +func (cfg *GcloudConfigData) oauthToken() *oauth2.Token { + return &oauth2.Token{ + AccessToken: cfg.Credential.AccessToken, + Expiry: cfg.Credential.TokenExpiry, + } +} + +type GcloudStatusCode int + +const ( + GcloudOk GcloudStatusCode = iota + GcloudNotFound + // generic execution failure error not specified above. + GcloudExecErr +) + +type GcloudError struct { + GcloudError error + Status GcloudStatusCode +} + +func (e *GcloudError) Error() string { + return e.GcloudError.Error() +} + +// GcloudConfig returns a GcloudConfigData object or an error of type *GcloudError. +func GcloudConfig() (*GcloudConfigData, error) { + gcloudCmd := "gcloud" + if runtime.GOOS == "windows" { + gcloudCmd = gcloudCmd + ".cmd" + } + + if _, err := exec.LookPath(gcloudCmd); err != nil { + return nil, &GcloudError{err, GcloudNotFound} + } + + buf, errbuf := new(bytes.Buffer), new(bytes.Buffer) + cmd := exec.Command(gcloudCmd, "--format", "json", "config", "config-helper", "--min-expiry", "1h") + cmd.Stdout = buf + cmd.Stderr = errbuf + + if err := cmd.Run(); err != nil { + err = fmt.Errorf("error reading config: %v; stderr was:\n%v", err, errbuf) + logging.Errorf("GcloudConfig: %v", err) + return nil, &GcloudError{err, GcloudExecErr} + } + + data := &GcloudConfigData{} + if err := json.Unmarshal(buf.Bytes(), data); err != nil { + logging.Errorf("Failed to unmarshal bytes from gcloud: %v", err) + logging.Errorf(" gcloud returned:\n%s", buf) + return nil, &GcloudError{err, GcloudExecErr} + } + + return data, nil +} + +// gcloudTokenSource implements oauth2.TokenSource via the `gcloud config config-helper` command. +type gcloudTokenSource struct { +} + +// Token helps gcloudTokenSource implement oauth2.TokenSource. +func (src *gcloudTokenSource) Token() (*oauth2.Token, error) { + cfg, err := GcloudConfig() + if err != nil { + return nil, err + } + return cfg.oauthToken(), nil +} + +func GcloudTokenSource(ctx context.Context) (oauth2.TokenSource, error) { + src := &gcloudTokenSource{} + tok, err := src.Token() + if err != nil { + return nil, err + } + return oauth2.ReuseTokenSource(tok, src), nil +} diff --git a/tests/alldb_test.go b/tests/alldb_test.go new file mode 100644 index 0000000000..5d90d4d486 --- /dev/null +++ b/tests/alldb_test.go @@ -0,0 +1,75 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// alldb_test.go contains end to end tests that require all environment variables to be defined. +package tests + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" +) + +// requireAllVars skips the given test if at least one environment variable is undefined. +func requireAllVars(t *testing.T) { + var allVars []string + allVars = append(allVars, *mysqlConnName, *mysqlUser, *mysqlPass, *mysqlDb) + allVars = append(allVars, *postgresConnName, *postgresUser, *postgresPass, *postgresDb) + allVars = append(allVars, *sqlserverConnName, *sqlserverUser, *sqlserverPass, *sqlserverDb) + + for _, envVar := range allVars { + if envVar == "" { + t.Skip("skipping test, all environment variable must be defined") + } + } +} + +// Test to verify that when a proxy client serves multiple instances that can all be successfully dialed, +// the health check readiness endpoint serves http.StatusOK. +func TestMultiInstanceDial(t *testing.T) { + if testing.Short() { + t.Skip("skipping Health Check integration tests") + } + requireAllVars(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Start the proxy. + args := []string{ + // This test doesn't care what the instance port is, so use "0" which + // means, let the runtime pick a random port. + fmt.Sprintf("-instances=%s=tcp:0,%s=tcp:0,%s=tcp:0", + *mysqlConnName, *postgresConnName, *sqlserverConnName), + "-use_http_health_check", + } + p, err := StartProxy(ctx, args...) + if err != nil { + t.Fatalf("unable to start proxy: %v", err) + } + defer p.Close() + output, err := p.WaitForServe(ctx) + if err != nil { + t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + } + + resp, err := http.Get("http://localhost:" + testPort + readinessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) + } +} diff --git a/tests/common_test.go b/tests/common_test.go new file mode 100644 index 0000000000..925817198d --- /dev/null +++ b/tests/common_test.go @@ -0,0 +1,193 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tests contains end to end tests meant to verify the Cloud SQL Auth proxy +// works as expected when executed as a binary. +// +// Required flags: +// -mysql_conn_name, -db_user, -db_pass +package tests + +import ( + "bufio" + "bytes" + "context" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path" + "runtime" + "strings" + "testing" +) + +var ( + binPath = "" +) + +func TestMain(m *testing.M) { + flag.Parse() + // compile the proxy as a binary + var err error + binPath, err = compileProxy() + if err != nil { + log.Fatalf("failed to compile proxy: %s", err) + } + // Run tests and cleanup + rtn := m.Run() + os.RemoveAll(binPath) + + os.Exit(rtn) +} + +// compileProxy compiles the binary into a temporary directory, and returns the path to the file or any error that occured. +func compileProxy() (string, error) { + // get path of the cmd pkg + _, f, _, ok := runtime.Caller(0) + if !ok { + return "", fmt.Errorf("failed to find cmd pkg") + } + projRoot := path.Dir(path.Dir(f)) // cd ../.. + pkgPath := path.Join(projRoot, "cmd", "cloud_sql_proxy") + // compile the proxy into a tmp directory + tmp, err := ioutil.TempDir("", "") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %s", err) + } + + b := path.Join(tmp, "cloud_sql_proxy") + + if runtime.GOOS == "windows" { + b += ".exe" + } + + cmd := exec.Command("go", "build", "-o", b, pkgPath) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("failed to run 'go build': %w \n %s", err, out) + } + return b, nil +} + +// proxyExec represents an execution of the Cloud SQL proxy. +type ProxyExec struct { + Out io.ReadCloser + + cmd *exec.Cmd + cancel context.CancelFunc + closers []io.Closer + done chan bool // closed once the cmd is completed + err error +} + +// StartProxy returns a proxyExec representing a running instance of the proxy. +func StartProxy(ctx context.Context, args ...string) (*ProxyExec, error) { + var err error + ctx, cancel := context.WithCancel(ctx) + p := ProxyExec{ + cmd: exec.CommandContext(ctx, binPath, args...), + cancel: cancel, + done: make(chan bool), + } + pr, pw, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("unable to open stdout pipe: %w", err) + } + defer pw.Close() + p.Out, p.cmd.Stdout, p.cmd.Stderr = pr, pw, pw + p.closers = append(p.closers, pr) + if err := p.cmd.Start(); err != nil { + defer p.Close() + return nil, fmt.Errorf("unable to start cmd: %w", err) + } + // when process is complete, mark as finished + go func() { + defer close(p.done) + p.err = p.cmd.Wait() + }() + return &p, nil +} + +// Stop sends the pskill signal to the proxy and returns. +func (p *ProxyExec) Kill() { + p.cancel() +} + +// Waits until the execution is completed and returns any error. +func (p *ProxyExec) Wait() error { + select { + case <-p.done: + return p.err + } +} + +// Stop sends the pskill signal to the proxy and returns. +func (p *ProxyExec) Done() bool { + select { + case <-p.done: + return true + default: + } + return false +} + +// Close releases any resources assotiated with the instance. +func (p *ProxyExec) Close() { + p.cancel() + for _, c := range p.closers { + c.Close() + } +} + +// WaitForServe waits until the proxy ready to serve traffic. Returns any output from the proxy +// while starting or any errors experienced before the proxy was ready to server. +func (p *ProxyExec) WaitForServe(ctx context.Context) (output string, err error) { + // Watch for the "Ready for new connections" to indicate the proxy is listening + buf, in, errCh := new(bytes.Buffer), bufio.NewReader(p.Out), make(chan error, 1) + go func() { + defer close(errCh) + for { + // if ctx is finished, stop processing + select { + case <-ctx.Done(): + return + default: + } + s, err := in.ReadString('\n') + if err != nil { + errCh <- err + return + } + buf.WriteString(s) + if strings.Contains(s, "Ready for new connections") { + errCh <- nil + return + } + } + }() + // Wait for either the background thread of the context to complete + select { + case <-ctx.Done(): + return buf.String(), fmt.Errorf("context done: %w", ctx.Err()) + case err := <-errCh: + if err != nil { + return buf.String(), fmt.Errorf("proxy start failed: %w", err) + } + } + return buf.String(), nil +} diff --git a/tests/connection_test.go b/tests/connection_test.go new file mode 100644 index 0000000000..cd2d4f3ede --- /dev/null +++ b/tests/connection_test.go @@ -0,0 +1,124 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// connection_test.go provides some helpers for basic connectivity tests to Cloud SQL instances. +package tests + +import ( + "context" + "database/sql" + "fmt" + "sync" + "testing" +) + +// proxyConnTest is a test helper to verify the proxy works with a basic connectivity test. +func proxyConnTest(t *testing.T, connName, driver, dsn string, port int, dir string) { + ctx := context.Background() + + var args []string + if dir != "" { // unix port + args = append(args, fmt.Sprintf("-dir=%s", dir), fmt.Sprintf("-instances=%s", connName)) + } else { // tcp socket + args = append(args, fmt.Sprintf("-instances=%s=tcp:%d", connName, port)) + } + + // Start the proxy + p, err := StartProxy(ctx, args...) + if err != nil { + t.Fatalf("unable to start proxy: %v", err) + } + defer p.Close() + output, err := p.WaitForServe(ctx) + if err != nil { + t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + } + + // Connect to the instance + db, err := sql.Open(driver, dsn) + if err != nil { + t.Fatalf("unable to connect to db: %s", err) + } + defer db.Close() + _, err = db.Exec("SELECT 1;") + if err != nil { + + t.Fatalf("unable to exec on db: %s", err) + } +} + +func proxyConnLimitTest(t *testing.T, connName, driver, dsn string, port int) { + ctx := context.Background() + + maxConn, totConn := 5, 10 + + // Start the proxy + p, err := StartProxy(ctx, fmt.Sprintf("-instances=%s=tcp:%d", connName, port), fmt.Sprintf("-max_connections=%d", maxConn)) + if err != nil { + t.Fatalf("unable to start proxy: %v", err) + } + defer p.Close() + output, err := p.WaitForServe(ctx) + if err != nil { + t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + } + + // Create connection pool + var stmt string + switch driver { + case "mysql": + stmt = "SELECT sleep(2);" + case "postgres": + stmt = "SELECT pg_sleep(2);" + case "sqlserver": + stmt = "WAITFOR DELAY '00:00:02'" + default: + t.Fatalf("unsupported driver: no sleep query found") + } + db, err := sql.Open(driver, dsn) + if err != nil { + t.Fatalf("unable to connect to db: %s", err) + } + db.SetMaxIdleConns(0) + defer db.Close() + + // Connect with up to totConn and count errors + var wg sync.WaitGroup + c := make(chan error, totConn) + for i := 0; i < totConn; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, err := db.ExecContext(ctx, stmt) + if err != nil { + c <- err + } + }() + } + wg.Wait() + close(c) + + var errs []error + for e := range c { + errs = append(errs, e) + } + want, got := totConn-maxConn, len(errs) + if want != got { + t.Errorf("wrong errCt - want: %d, got %d", want, got) + for _, e := range errs { + t.Errorf("%s\n", e) + } + t.Fail() + } +} diff --git a/tests/dialer_test.go b/tests/dialer_test.go new file mode 100644 index 0000000000..bea05d192b --- /dev/null +++ b/tests/dialer_test.go @@ -0,0 +1,146 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "context" + "database/sql" + "fmt" + "net" + "net/http" + "testing" + "time" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/certs" + "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/stdlib" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + "google.golang.org/api/sqladmin/v1" +) + +func TestClientHandlesSSLReset(t *testing.T) { + if testing.Short() { + t.Skip("skipping dialer integration tests") + } + newClient := func(c *http.Client) *proxy.Client { + return &proxy.Client{ + Port: 3307, + Certs: certs.NewCertSourceOpts(c, certs.RemoteOpts{ + UserAgent: "cloud_sql_proxy/test_build", + IPAddrTypeOpts: []string{"PUBLIC", "PRIVATE"}, + }), + Conns: proxy.NewConnSet(), + } + } + connectToDB := func(c *proxy.Client) (*sql.DB, error) { + var ( + dbUser = *postgresUser + dbPwd = *postgresPass + dbName = *postgresDb + ) + dsn := fmt.Sprintf("user=%s password=%s database=%s", dbUser, dbPwd, dbName) + config, err := pgx.ParseConfig(dsn) + if err != nil { + return nil, err + } + config.DialFunc = func(ctx context.Context, network, instance string) (net.Conn, error) { + return c.DialContext(ctx, *postgresConnName) + } + dbURI := stdlib.RegisterConnConfig(config) + return sql.Open("pgx", dbURI) + } + resetSSL := func(c *http.Client) error { + svc, err := sqladmin.NewService(context.Background(), option.WithHTTPClient(c)) + if err != nil { + return err + } + project, _, instance, _, _ := proxy.ParseInstanceConnectionName(*postgresConnName) + t.Log("Resetting SSL config.") + op, err := svc.Instances.ResetSslConfig(project, instance).Do() + if err != nil { + return err + } + for { + t.Log("Waiting for operation to complete.") + op, err = svc.Operations.Get(project, op.Name).Do() + if err != nil { + return err + } + if op.Status == "DONE" { + t.Log("reset SSL config operation complete") + break + } + time.Sleep(time.Second) + } + return nil + } + + // SETUP: create HTTP client and proxy client, then connect to database + src, err := google.DefaultTokenSource(context.Background(), proxy.SQLScope) + if err != nil { + t.Fatal(err) + } + client := oauth2.NewClient(context.Background(), src) + proxyClient := newClient(client) + + db, err := connectToDB(proxyClient) + if err != nil { + t.Fatalf("failed to connect to DB: %v", err) + } + + // Begin database transaction + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + + resetSSL(client) + + // Re-dial twice, once to invalidate config, once to establish connection + var attempts int + for { + t.Log("Re-dialing instance") + _, err = proxyClient.DialContext(context.Background(), *postgresConnName) + if err != nil { + t.Logf("Dial error: %v", err) + } + if err == nil { + break + } + attempts++ + if attempts > 1 { + t.Fatalf("could not dial: %v", err) + } + time.Sleep(time.Second) + } + + for i := 0; i < 5; i++ { + row, err := tx.Query("SELECT 1") + if err != nil { + t.Logf("Query after Reset SSL failed as expected after %v retries (error was %v)", i, err) + break + } + row.Close() + time.Sleep(time.Second) + } + + if err = db.Ping(); err != nil { + t.Fatalf("could not re-stablish a DB connection: %v", err) + } +} diff --git a/tests/healthcheck_test.go b/tests/healthcheck_test.go new file mode 100644 index 0000000000..2616b2c985 --- /dev/null +++ b/tests/healthcheck_test.go @@ -0,0 +1,56 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// healthcheck_test.go provides some helpers for end to end health check server tests. +package tests + +import ( + "context" + "fmt" + "net/http" + "testing" +) + +const ( + readinessPath = "/readiness" + testPort = "8090" +) + +// singleInstanceDial verifies that when a proxy client serves the given instance, the readiness +// endpoint serves http.StatusOK. +func singleInstanceDial(t *testing.T, connName string, port int) { + ctx := context.Background() + + var args []string + args = append(args, fmt.Sprintf("-instances=%s=tcp:%d", connName, port), "-use_http_health_check") + + // Start the proxy. + p, err := StartProxy(ctx, args...) + if err != nil { + t.Fatalf("unable to start proxy: %v", err) + } + defer p.Close() + output, err := p.WaitForServe(ctx) + if err != nil { + t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + } + + resp, err := http.Get("http://localhost:" + testPort + readinessPath) + if err != nil { + t.Fatalf("HTTP GET failed: %v", err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) + } +} diff --git a/tests/mysql_test.go b/tests/mysql_test.go new file mode 100644 index 0000000000..67330c92e1 --- /dev/null +++ b/tests/mysql_test.go @@ -0,0 +1,118 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mysql_test runs various tests against a MySQL flavored Cloud SQL instance. +package tests + +import ( + "flag" + "io/ioutil" + "log" + "os" + "path" + "runtime" + "testing" + + mysql "github.com/go-sql-driver/mysql" +) + +var ( + mysqlConnName = flag.String("mysql_conn_name", os.Getenv("MYSQL_CONNECTION_NAME"), "Cloud SQL MYSQL instance connection name, in the form of 'project:region:instance'.") + mysqlUser = flag.String("mysql_user", os.Getenv("MYSQL_USER"), "Name of database user.") + mysqlPass = flag.String("mysql_pass", os.Getenv("MYSQL_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + mysqlDb = flag.String("mysql_db", os.Getenv("MYSQL_DB"), "Name of the database to connect to.") + + mysqlPort = 3306 +) + +func requireMysqlVars(t *testing.T) { + switch "" { + case *mysqlConnName: + t.Fatal("'mysql_conn_name' not set") + case *mysqlUser: + t.Fatal("'mysql_user' not set") + case *mysqlPass: + t.Fatal("'mysql_pass' not set") + case *mysqlDb: + t.Fatal("'mysql_db' not set") + } +} + +func TestMysqlTcp(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMysqlVars(t) + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + DBName: *mysqlDb, + AllowNativePasswords: true, + } + proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort, "") +} + +func TestMysqlSocket(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + if runtime.GOOS == "windows" { + t.Skip("Skipped Unix socket test on Windows") + } + requireMysqlVars(t) + + dir, err := ioutil.TempDir("", "csql-proxy-tests") + if err != nil { + log.Fatalf("unable to create tmp dir: %s", err) + } + defer os.RemoveAll(dir) + + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + Net: "unix", + Addr: path.Join(dir, *mysqlConnName), + DBName: *mysqlDb, + AllowNativePasswords: true, + } + proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), 0, dir) +} + +func TestMysqlConnLimit(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMysqlVars(t) + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + DBName: *mysqlDb, + AllowNativePasswords: true, + } + proxyConnLimitTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort) +} + +// Test to verify that when a proxy client serves one mysql instance that can be +// dialed successfully, the health check readiness endpoint serves http.StatusOK. +func TestMysqlDial(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + switch "" { + case *mysqlConnName: + t.Fatal("'mysql_conn_name' not set") + } + + singleInstanceDial(t, *mysqlConnName, mysqlPort) +} diff --git a/tests/postgres_test.go b/tests/postgres_test.go new file mode 100644 index 0000000000..0cc3c28451 --- /dev/null +++ b/tests/postgres_test.go @@ -0,0 +1,165 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// postgres_test runs various tests against a Postgres flavored Cloud SQL instance. +package tests + +import ( + "context" + "database/sql" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "path" + "runtime" + "testing" + "time" + + _ "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/postgres" + _ "github.com/lib/pq" +) + +var ( + postgresConnName = flag.String("postgres_conn_name", os.Getenv("POSTGRES_CONNECTION_NAME"), "Cloud SQL Postgres instance connection name, in the form of 'project:region:instance'.") + postgresUser = flag.String("postgres_user", os.Getenv("POSTGRES_USER"), "Name of database user.") + postgresPass = flag.String("postgres_pass", os.Getenv("POSTGRES_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + postgresDb = flag.String("postgres_db", os.Getenv("POSTGRES_DB"), "Name of the database to connect to.") + + postgresIAMUser = flag.String("postgres_user_iam", os.Getenv("POSTGRES_USER_IAM"), "Name of database user configured with IAM DB Authentication.") + + postgresPort = 5432 +) + +func requirePostgresVars(t *testing.T) { + switch "" { + case *postgresConnName: + t.Fatal("'postgres_conn_name' not set") + case *postgresUser: + t.Fatal("'postgres_user' not set") + case *postgresPass: + t.Fatal("'postgres_pass' not set") + case *postgresDb: + t.Fatal("'postgres_db' not set") + } +} + +func TestPostgresTcp(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + + dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) + proxyConnTest(t, *postgresConnName, "postgres", dsn, postgresPort, "") +} + +func TestPostgresSocket(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + if runtime.GOOS == "windows" { + t.Skip("Skipped Unix socket test on Windows") + } + requirePostgresVars(t) + + dir, err := ioutil.TempDir("", "csql-proxy") + if err != nil { + log.Fatalf("unable to create tmp dir: %s", err) + } + defer os.RemoveAll(dir) + + dsn := fmt.Sprintf("user=%s password=%s database=%s host=%s", *postgresUser, *postgresPass, *postgresDb, path.Join(dir, *postgresConnName)) + proxyConnTest(t, *postgresConnName, "postgres", dsn, 0, dir) +} + +func TestPostgresConnLimit(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + + dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) + proxyConnLimitTest(t, *postgresConnName, "postgres", dsn, postgresPort) +} + +func TestPostgresIAMDBAuthn(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + if *postgresIAMUser == "" { + t.Fatal("'postgres_user_iam' not set") + } + + ctx := context.Background() + + // Start the proxy + p, err := StartProxy(ctx, fmt.Sprintf("-instances=%s=tcp:%d", *postgresConnName, 5432), "-enable_iam_login") + if err != nil { + t.Fatalf("unable to start proxy: %v", err) + } + defer p.Close() + output, err := p.WaitForServe(ctx) + if err != nil { + t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + } + + dsn := fmt.Sprintf("user=%s database=%s sslmode=disable", *postgresIAMUser, *postgresDb) + db, err := sql.Open("postgres", dsn) + if err != nil { + t.Fatalf("unable to connect to db: %s", err) + } + defer db.Close() + _, err = db.Exec("SELECT 1;") + if err != nil { + + t.Fatalf("unable to exec on db: %s", err) + } +} + +func TestPostgresHook(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", *postgresConnName, *postgresUser, *postgresPass, *postgresDb) + db, err := sql.Open("cloudsqlpostgres", dsn) + if err != nil { + t.Fatalf("connect failed: %s", err) + } + defer db.Close() + var now time.Time + err = db.QueryRowContext(ctx, "SELECT NOW()").Scan(&now) + if err != nil { + t.Fatalf("query failed: %s", err) + } +} + +// Test to verify that when a proxy client serves one postgres instance that can be +// dialed successfully, the health check readiness endpoint serves http.StatusOK. +func TestPostgresDial(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + switch "" { + case *postgresConnName: + t.Fatal("'postgres_conn_name' not set") + } + + singleInstanceDial(t, *postgresConnName, postgresPort) +} diff --git a/tests/sqlserver_test.go b/tests/sqlserver_test.go new file mode 100644 index 0000000000..93977e49ea --- /dev/null +++ b/tests/sqlserver_test.go @@ -0,0 +1,81 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// sqlserver_test runs various tests against a SqlServer flavored Cloud SQL instance. +package tests + +import ( + "flag" + "fmt" + "os" + "testing" + + _ "github.com/denisenkom/go-mssqldb" +) + +var ( + sqlserverConnName = flag.String("sqlserver_conn_name", os.Getenv("SQLSERVER_CONNECTION_NAME"), "Cloud SQL SqlServer instance connection name, in the form of 'project:region:instance'.") + sqlserverUser = flag.String("sqlserver_user", os.Getenv("SQLSERVER_USER"), "Name of database user.") + sqlserverPass = flag.String("sqlserver_pass", os.Getenv("SQLSERVER_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + sqlserverDb = flag.String("sqlserver_db", os.Getenv("SQLSERVER_DB"), "Name of the database to connect to.") + + sqlserverPort = 1433 +) + +func requireSqlserverVars(t *testing.T) { + switch "" { + case *sqlserverConnName: + t.Fatal("'sqlserver_conn_name' not set") + case *sqlserverUser: + t.Fatal("'sqlserver_user' not set") + case *sqlserverPass: + t.Fatal("'sqlserver_pass' not set") + case *sqlserverDb: + t.Fatal("'sqlserver_db' not set") + } +} + +func TestSqlServerTcp(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSqlserverVars(t) + + dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", *sqlserverUser, *sqlserverPass, *sqlserverDb) + proxyConnTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort, "") +} + +func TestSqlserverConnLimit(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSqlserverVars(t) + + dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", *sqlserverUser, *sqlserverPass, *sqlserverDb) + proxyConnLimitTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort) +} + +// Test to verify that when a proxy client serves one sqlserver instance that can be +// dialed successfully, the health check readiness endpoint serves http.StatusOK. +func TestSqlserverDial(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + switch "" { + case *sqlserverConnName: + t.Fatal("'sqlserver_conn_name' not set") + } + + singleInstanceDial(t, *sqlserverConnName, sqlserverPort) +} diff --git a/testsV2/common_test.go b/testsV2/common_test.go new file mode 100644 index 0000000000..f9ffa2b56c --- /dev/null +++ b/testsV2/common_test.go @@ -0,0 +1,147 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tests contains end to end tests meant to verify the Cloud SQL Auth proxy +// works as expected when executed as a binary. +// +// Required flags: +// -mysql_conn_name, -db_user, -db_pass +package tests + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd" +) + +// proxyExec represents an execution of the Cloud SQL proxy. +type proxyExec struct { + Out io.ReadCloser + + cmd *cmd.Command + cancel context.CancelFunc + closers []io.Closer + done chan bool // closed once the cmd is completed + err error +} + +// StartProxy returns a proxyExec representing a running instance of the proxy. +func StartProxy(ctx context.Context, args ...string) (*proxyExec, error) { + ctx, cancel := context.WithCancel(ctx) + cmd := cmd.NewCommand() + cmd.SetArgs(args) + + // Open a pipe for tracking the output from the cmd + pr, pw, err := os.Pipe() + if err != nil { + cancel() + return nil, fmt.Errorf("unable to open stdout pipe: %w", err) + } + // defer pw.Close() + cmd.SetOut(pw) + cmd.SetErr(pw) + + p := &proxyExec{ + Out: pr, + cmd: cmd, + cancel: cancel, + closers: []io.Closer{pr, pw}, + done: make(chan bool), + } + // Start the command in the background + go func() { + defer close(p.done) + defer cancel() + p.err = cmd.ExecuteContext(ctx) + }() + return p, nil +} + +// Stop sends the TERM signal to the proxy and returns. +func (p *proxyExec) Stop() { + p.cancel() +} + +// Waits until the execution is completed and returns any error. +func (p *proxyExec) Wait(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-p.done: + return p.err + } +} + +// Done returns true if the proxy has exited. +func (p *proxyExec) Done() bool { + select { + case <-p.done: + return true + default: + } + return false +} + +// Close releases any resources associated with the instance. +func (p *proxyExec) Close() { + p.cancel() + for _, c := range p.closers { + c.Close() + } +} + +// WaitForServe waits until the proxy ready to serve traffic. Returns any output from +// the proxy while starting or any errors experienced before the proxy was ready to +// server. +func (p *proxyExec) WaitForServe(ctx context.Context) (output string, err error) { + // Watch for the "Ready for new connections" to indicate the proxy is listening + buf, in, errCh := new(bytes.Buffer), bufio.NewReader(p.Out), make(chan error, 1) + go func() { + defer close(errCh) + for { + // if ctx is finished, stop processing + select { + case <-ctx.Done(): + return + default: + } + s, err := in.ReadString('\n') + if err != nil { + errCh <- err + return + } + buf.WriteString(s) + if strings.Contains(s, "ready for new connections") { + errCh <- nil + return + } + } + }() + // Wait for either the background thread of the context to complete + select { + case <-ctx.Done(): + return buf.String(), fmt.Errorf("context done: %w", ctx.Err()) + case err := <-errCh: + if err != nil { + return buf.String(), fmt.Errorf("proxy start failed: %w", err) + } + } + return buf.String(), nil +} diff --git a/testsV2/connection_test.go b/testsV2/connection_test.go new file mode 100644 index 0000000000..63e8a8bdb4 --- /dev/null +++ b/testsV2/connection_test.go @@ -0,0 +1,80 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "context" + "database/sql" + "os" + "testing" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/sqladmin/v1" +) + +const connTestTimeout = time.Minute + +// removeAuthEnvVar retrieves an OAuth2 token and a path to a service account key +// and then unsets GOOGLE_APPLICATION_CREDENTIALS. It returns a cleanup function +// that restores the original setup. +func removeAuthEnvVar(t *testing.T) (*oauth2.Token, string, func()) { + ts, err := google.DefaultTokenSource(context.Background(), sqladmin.SqlserviceAdminScope) + if err != nil { + t.Errorf("failed to resolve token source: %v", err) + } + tok, err := ts.Token() + if err != nil { + t.Errorf("failed to get token: %v", err) + } + path, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS") + if !ok { + t.Fatalf("GOOGLE_APPLICATION_CREDENTIALS was not set in the environment") + } + if err := os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS"); err != nil { + t.Fatalf("failed to unset GOOGLE_APPLICATION_CREDENTIALS") + } + return tok, path, func() { + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", path) + } +} + +// proxyConnTest is a test helper to verify the proxy works with a basic connectivity test. +func proxyConnTest(t *testing.T, args []string, driver, dsn string) { + ctx, cancel := context.WithTimeout(context.Background(), connTestTimeout) + defer cancel() + // Start the proxy + p, err := StartProxy(ctx, args...) + if err != nil { + t.Fatalf("unable to start proxy: %v", err) + } + defer p.Close() + output, err := p.WaitForServe(ctx) + if err != nil { + t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) + } + + // Connect to the instance + db, err := sql.Open(driver, dsn) + if err != nil { + t.Fatalf("unable to connect to db: %s", err) + } + defer db.Close() + _, err = db.Exec("SELECT 1;") + if err != nil { + t.Fatalf("unable to exec on db: %s", err) + } +} diff --git a/testsV2/mysql_test.go b/testsV2/mysql_test.go new file mode 100644 index 0000000000..2246b28611 --- /dev/null +++ b/testsV2/mysql_test.go @@ -0,0 +1,102 @@ +// Copyright 2021 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mysql_test runs various tests against a MySQL flavored Cloud SQL instance. +package tests + +import ( + "flag" + "os" + "testing" + + mysql "github.com/go-sql-driver/mysql" +) + +var ( + mysqlConnName = flag.String("mysql_conn_name", os.Getenv("MYSQL_CONNECTION_NAME"), "Cloud SQL MYSQL instance connection name, in the form of 'project:region:instance'.") + mysqlUser = flag.String("mysql_user", os.Getenv("MYSQL_USER"), "Name of database user.") + mysqlPass = flag.String("mysql_pass", os.Getenv("MYSQL_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + mysqlDB = flag.String("mysql_db", os.Getenv("MYSQL_DB"), "Name of the database to connect to.") +) + +func requireMySQLVars(t *testing.T) { + switch "" { + case *mysqlConnName: + t.Fatal("'mysql_conn_name' not set") + case *mysqlUser: + t.Fatal("'mysql_user' not set") + case *mysqlPass: + t.Fatal("'mysql_pass' not set") + case *mysqlDB: + t.Fatal("'mysql_db' not set") + } +} + +func TestMySQLTCP(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMySQLVars(t) + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + DBName: *mysqlDB, + AllowNativePasswords: true, + Addr: "127.0.0.1:3306", + Net: "tcp", + } + proxyConnTest(t, []string{*mysqlConnName}, "mysql", cfg.FormatDSN()) +} + +func TestMySQLAuthWithToken(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMySQLVars(t) + tok, _, cleanup := removeAuthEnvVar(t) + defer cleanup() + + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + DBName: *mysqlDB, + AllowNativePasswords: true, + Addr: "127.0.0.1:3306", + Net: "tcp", + } + proxyConnTest(t, + []string{"--token", tok.AccessToken, *mysqlConnName}, + "mysql", cfg.FormatDSN()) +} + +func TestMySQLAuthWithCredentialsFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping MySQL integration tests") + } + requireMySQLVars(t) + _, path, cleanup := removeAuthEnvVar(t) + defer cleanup() + + cfg := mysql.Config{ + User: *mysqlUser, + Passwd: *mysqlPass, + DBName: *mysqlDB, + AllowNativePasswords: true, + Addr: "127.0.0.1:3306", + Net: "tcp", + } + proxyConnTest(t, + []string{"--credentials-file", path, *mysqlConnName}, + "mysql", cfg.FormatDSN()) +} diff --git a/testsV2/other_test.go b/testsV2/other_test.go new file mode 100644 index 0000000000..de9e6021f6 --- /dev/null +++ b/testsV2/other_test.go @@ -0,0 +1,57 @@ +// Copyright 2022 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// other_test runs various tests that are database agnostic. +package tests + +import ( + "bufio" + "context" + "os" + "strings" + "testing" + "time" +) + +func TestVersion(t *testing.T) { + ctx := context.Background() + + data, err := os.ReadFile("../cmd/version.txt") + if err != nil { + t.Fatalf("failed to read version.txt: %v", err) + } + want := strings.TrimSpace(string(data)) + + // Start the proxy + p, err := StartProxy(ctx, "--version") + if err != nil { + t.Fatalf("proxy start failed: %v", err) + } + defer p.Close() + + // Assume the proxy should be able to print "version" relatively quickly + ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) + defer cancel() + err = p.Wait(ctx) + if err != nil { + t.Fatalf("proxy exited unexpectedly: %v", err) + } + output, err := bufio.NewReader(p.Out).ReadString('\n') + if err != nil { + t.Fatalf("failed to read output from proxy: %v", err) + } + if !strings.Contains(output, want) { + t.Errorf("proxy did not return correct version: want %q, got %q", want, output) + } +} diff --git a/testsV2/postgres_test.go b/testsV2/postgres_test.go new file mode 100644 index 0000000000..f28f17b5d9 --- /dev/null +++ b/testsV2/postgres_test.go @@ -0,0 +1,88 @@ +// Copyright 2021 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// postgres_test runs various tests against a Postgres flavored Cloud SQL instance. +package tests + +import ( + "flag" + "fmt" + "os" + "testing" + + _ "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/postgres" + _ "github.com/lib/pq" +) + +var ( + postgresConnName = flag.String("postgres_conn_name", os.Getenv("POSTGRES_CONNECTION_NAME"), "Cloud SQL Postgres instance connection name, in the form of 'project:region:instance'.") + postgresUser = flag.String("postgres_user", os.Getenv("POSTGRES_USER"), "Name of database user.") + postgresPass = flag.String("postgres_pass", os.Getenv("POSTGRES_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + postgresDB = flag.String("postgres_db", os.Getenv("POSTGRES_DB"), "Name of the database to connect to.") + + postgresIAMUser = flag.String("postgres_user_iam", os.Getenv("POSTGRES_USER_IAM"), "Name of database user configured with IAM DB Authentication.") +) + +func requirePostgresVars(t *testing.T) { + switch "" { + case *postgresConnName: + t.Fatal("'postgres_conn_name' not set") + case *postgresUser: + t.Fatal("'postgres_user' not set") + case *postgresPass: + t.Fatal("'postgres_pass' not set") + case *postgresDB: + t.Fatal("'postgres_db' not set") + } +} + +func TestPostgresTCP(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + + dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDB) + proxyConnTest(t, []string{*postgresConnName}, "postgres", dsn) +} + +func TestPostgresAuthWithToken(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + tok, _, cleanup := removeAuthEnvVar(t) + defer cleanup() + + dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", + *postgresUser, *postgresPass, *postgresDB) + proxyConnTest(t, + []string{"--token", tok.AccessToken, *postgresConnName}, + "postgres", dsn) +} + +func TestPostgresAuthWithCredentialsFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping Postgres integration tests") + } + requirePostgresVars(t) + _, path, cleanup := removeAuthEnvVar(t) + defer cleanup() + + dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", + *postgresUser, *postgresPass, *postgresDB) + proxyConnTest(t, + []string{"--credentials-file", path, *postgresConnName}, + "postgres", dsn) +} diff --git a/testsV2/sqlserver_test.go b/testsV2/sqlserver_test.go new file mode 100644 index 0000000000..3ba683391d --- /dev/null +++ b/testsV2/sqlserver_test.go @@ -0,0 +1,86 @@ +// Copyright 2021 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// sqlserver_test runs various tests against a SqlServer flavored Cloud SQL instance. +package tests + +import ( + "flag" + "fmt" + "os" + "testing" + + _ "github.com/denisenkom/go-mssqldb" +) + +var ( + sqlserverConnName = flag.String("sqlserver_conn_name", os.Getenv("SQLSERVER_CONNECTION_NAME"), "Cloud SQL SqlServer instance connection name, in the form of 'project:region:instance'.") + sqlserverUser = flag.String("sqlserver_user", os.Getenv("SQLSERVER_USER"), "Name of database user.") + sqlserverPass = flag.String("sqlserver_pass", os.Getenv("SQLSERVER_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") + sqlserverDB = flag.String("sqlserver_db", os.Getenv("SQLSERVER_DB"), "Name of the database to connect to.") +) + +func requireSQLServerVars(t *testing.T) { + switch "" { + case *sqlserverConnName: + t.Fatal("'sqlserver_conn_name' not set") + case *sqlserverUser: + t.Fatal("'sqlserver_user' not set") + case *sqlserverPass: + t.Fatal("'sqlserver_pass' not set") + case *sqlserverDB: + t.Fatal("'sqlserver_db' not set") + } +} + +func TestSQLServerTCP(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSQLServerVars(t) + + dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", + *sqlserverUser, *sqlserverPass, *sqlserverDB) + proxyConnTest(t, []string{*sqlserverConnName}, "sqlserver", dsn) +} + +func TestSQLServerAuthWithToken(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSQLServerVars(t) + tok, _, cleanup := removeAuthEnvVar(t) + defer cleanup() + + dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", + *sqlserverUser, *sqlserverPass, *sqlserverDB) + proxyConnTest(t, + []string{"--token", tok.AccessToken, *sqlserverConnName}, + "sqlserver", dsn) +} + +func TestSQLServerAuthWithCredentialsFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping SQL Server integration tests") + } + requireSQLServerVars(t) + _, path, cleanup := removeAuthEnvVar(t) + defer cleanup() + + dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", + *sqlserverUser, *sqlserverPass, *sqlserverDB) + proxyConnTest(t, + []string{"--credentials-file", path, *sqlserverConnName}, + "sqlserver", dsn) +} From 16d6ed01ca345a9280f0c82446089981447e5c14 Mon Sep 17 00:00:00 2001 From: Eno Compton Date: Tue, 12 Apr 2022 22:13:29 -0600 Subject: [PATCH 2/4] Remove all v1 proxy code --- CHANGELOG.md | 215 ----- CONTRIBUTORS | 40 - cmd/cloud_sql_proxy/cloud_sql_proxy.go | 734 ------------------ cmd/cloud_sql_proxy/cloud_sql_proxy_test.go | 33 - .../internal/healthcheck/healthcheck.go | 194 ----- .../internal/healthcheck/healthcheck_test.go | 251 ------ cmd/cloud_sql_proxy/proxy.go | 388 --------- cmd/cloud_sql_proxy/proxy_test.go | 214 ----- cmd/cloud_sql_proxy/version.txt | 1 - logging/logging.go | 106 --- proxy/README.md | 33 - proxy/certs/certs.go | 365 --------- proxy/certs/certs_test.go | 174 ----- proxy/dialers/mysql/hook.go | 94 --- proxy/dialers/mysql/hook_test.go | 47 -- proxy/dialers/postgres/hook.go | 61 -- proxy/dialers/postgres/hook_test.go | 38 - proxy/fuse/fuse.go | 378 --------- proxy/fuse/fuse_darwin.go | 43 - proxy/fuse/fuse_linux.go | 34 - proxy/fuse/fuse_linux_test.go | 47 -- proxy/fuse/fuse_openbsd.go | 32 - proxy/fuse/fuse_test.go | 247 ------ proxy/fuse/fuse_windows.go | 31 - proxy/limits/limits.go | 89 --- proxy/limits/limits_freebsd.go | 82 -- proxy/limits/limits_test.go | 136 ---- proxy/limits/limits_windows.go | 30 - proxy/proxy/client.go | 652 ---------------- proxy/proxy/client_test.go | 637 --------------- proxy/proxy/common.go | 225 ------ proxy/proxy/common_test.go | 115 --- proxy/proxy/connect_tls_117.go | 45 -- proxy/proxy/connect_tls_other.go | 113 --- proxy/proxy/dial.go | 115 --- proxy/util/cloudsqlutil.go | 45 -- proxy/util/cloudsqlutil_test.go | 38 - proxy/util/gcloudutil.go | 123 --- tests/alldb_test.go | 75 -- tests/common_test.go | 193 ----- tests/connection_test.go | 124 --- tests/dialer_test.go | 146 ---- tests/healthcheck_test.go | 56 -- tests/mysql_test.go | 118 --- tests/postgres_test.go | 165 ---- tests/sqlserver_test.go | 81 -- testsV2/mysql_test.go | 102 --- testsV2/postgres_test.go | 41 +- testsV2/sqlserver_test.go | 86 -- 49 files changed, 29 insertions(+), 7403 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTORS delete mode 100644 cmd/cloud_sql_proxy/cloud_sql_proxy.go delete mode 100644 cmd/cloud_sql_proxy/cloud_sql_proxy_test.go delete mode 100644 cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go delete mode 100644 cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go delete mode 100644 cmd/cloud_sql_proxy/proxy.go delete mode 100644 cmd/cloud_sql_proxy/proxy_test.go delete mode 100644 cmd/cloud_sql_proxy/version.txt delete mode 100644 logging/logging.go delete mode 100644 proxy/README.md delete mode 100644 proxy/certs/certs.go delete mode 100644 proxy/certs/certs_test.go delete mode 100644 proxy/dialers/mysql/hook.go delete mode 100644 proxy/dialers/mysql/hook_test.go delete mode 100644 proxy/dialers/postgres/hook.go delete mode 100644 proxy/dialers/postgres/hook_test.go delete mode 100644 proxy/fuse/fuse.go delete mode 100644 proxy/fuse/fuse_darwin.go delete mode 100644 proxy/fuse/fuse_linux.go delete mode 100644 proxy/fuse/fuse_linux_test.go delete mode 100644 proxy/fuse/fuse_openbsd.go delete mode 100644 proxy/fuse/fuse_test.go delete mode 100644 proxy/fuse/fuse_windows.go delete mode 100644 proxy/limits/limits.go delete mode 100644 proxy/limits/limits_freebsd.go delete mode 100644 proxy/limits/limits_test.go delete mode 100644 proxy/limits/limits_windows.go delete mode 100644 proxy/proxy/client.go delete mode 100644 proxy/proxy/client_test.go delete mode 100644 proxy/proxy/common.go delete mode 100644 proxy/proxy/common_test.go delete mode 100644 proxy/proxy/connect_tls_117.go delete mode 100644 proxy/proxy/connect_tls_other.go delete mode 100644 proxy/proxy/dial.go delete mode 100644 proxy/util/cloudsqlutil.go delete mode 100644 proxy/util/cloudsqlutil_test.go delete mode 100644 proxy/util/gcloudutil.go delete mode 100644 tests/alldb_test.go delete mode 100644 tests/common_test.go delete mode 100644 tests/connection_test.go delete mode 100644 tests/dialer_test.go delete mode 100644 tests/healthcheck_test.go delete mode 100644 tests/mysql_test.go delete mode 100644 tests/postgres_test.go delete mode 100644 tests/sqlserver_test.go delete mode 100644 testsV2/mysql_test.go delete mode 100644 testsV2/sqlserver_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 0068795d40..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,215 +0,0 @@ -# Changelog - -## [1.30.0](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.29.0...v1.30.0) (2022-04-04) - - -### Features - -* drop support and testing for Go 1.13, 1.14, 1.15 ([#1148](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1148)) ([158b0d5](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/158b0d57d46054be6a0d1600d5030b23be69dc9b)) - -## [1.29.0](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.28.1...v1.29.0) (2022-03-01) - - -### Features - -* add Go version support policy ([#1109](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1109)) ([ae6f4a1](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ae6f4a1a534df8a273c0ea96880154b90bc65e77)) - -### [1.28.1](https://github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.28.0...v1.28.1) (2022-01-31) - - -### Bug Fixes - -* invalidated config should retain error ([#1068](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1068)) ([49d3003](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/49d3003c018afdc0cde54340d5be808f9dcd5c84)) -* remove unnecessary token parsing ([#1074](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1074)) ([e138611](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/e1386118ad239e6c1ff16df6f2be1351a6432bb3)) -* return error from instance version ([#1069](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1069)) ([d9fc819](https://github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d9fc819a197bd75d0060bd46b8e06da6bdd6630c)) - -## [1.28.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.27.1...v1.28.0) (2022-01-04) - - -### Features - -* add support for ReadTime in Admin API requests ([#1040](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1040)) ([a7c8b5c](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/a7c8b5cf4d10c17bea405ce67ee642232b43fdec)) -* add support for specifying a quota project ([#1044](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1044)) ([dc66aca](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/dc66aca88190ae3f6d39f191489fdfb280146ed9)) -* allow multiple -instances flags ([#1046](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1046)) ([1972693](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/1972693b8ac65c912bb719dc23d4f578cb6ff9e2)), closes [#1030](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1030) - - -### Bug Fixes - -* increase rateLimit burst size to 2 ([#1048](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1048)) ([df6b6f9](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/df6b6f9ed8860d28f5e934db495257d288c42f2b)) - -### [1.27.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.27.0...v1.27.1) (2021-12-07) - - -### Bug Fixes - -* update dependencies to latest versions ([#1034](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/1034)) ([8954d24](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/8954d241a71b59d9bf82cb47469e6652d3f379e7)) - -## [1.27.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.26.0...v1.27.0) (2021-11-02) - - -### Features - -* switch to supported FUSE library ([#953](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/953)) ([10f2133](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/10f2133010f3bf7ef8a13b43e0bfa16bdca8cedb)) -* verify FUSE is installed on macOS / linux ([#959](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/959)) ([9ab868e](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/9ab868ef344b9a82c06f97928420f98a4d37c5ce)) - - -### Bug Fixes - -* fail fast on invalid config ([#999](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/999)) ([18a0960](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/18a096037d9ceb2ca71218984b65fe342fc2a778)) -* respect context deadline for TLS handshakes ([#987](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/987)) ([12ff12c](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/12ff12c9f87459dc40e2e6e4a2d08bebb0786ee7)), closes [#986](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/986) -* validate instance connections in liveness probe ([#995](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/995)) ([e5cc8d4](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/e5cc8d4f8676fed2013cc491578a1aaf7416ec3e)) - -## [1.26.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.25.0...v1.26.0) (2021-10-05) - - -### Features - -* improve reliability of refresh operations ([#883](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/883)) ([480992a](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/480992a7671abe9b76f940175f4ed17f5271d3f8)) - -## [1.25.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.24.0...v1.25.0) (2021-09-07) - - -### Features - -* add health checks to proxy ([#859](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/859)) ([ea62bdd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ea62bddaaf3aa7df79250d045ba2f5f3fe7edaea)) -* add instance dialing to health check ([#871](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/871)) ([eca3793](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/eca37935e7cd54efcd612c170e46f45c1d8e3556)) -* require TLS v1.3 at minimum ([#906](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/906)) ([cafa966](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/cafa966e50170ad94f12f067549ba3aedf8ecdca)) - - -### Bug Fixes - -* ensure proxy shuts down gracefully on SIGTERM ([#877](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/877)) ([9793555](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/97935551ac44cb7a92e2901def1938d604dfeecb)) -* validate instances in fuse mode ([#875](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/875)) ([96f8b65](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/96f8b655b09b711fd9adfcb486626b64d3b917f3)) - -## [1.24.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.23.1...v1.24.0) (2021-08-02) - - -### Features - -* Add option to delay key generation until first connect ([#841](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/841)) ([4999ffd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4999ffd0c3406e91874648630f9805b2d5f0ac50)) -* stop building darwin 386 binaries ([#846](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/846)) ([77d7c40](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/77d7c40ff79cf99a10d2dbae39b737625a08582f)), closes [#780](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/780) - - -### Bug Fixes - -* invalidate cached config on handshake error ([#817](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/817)) ([5d98f5c](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/5d98f5c40e0b58da479bf6897712d53e6846f613)) -* strip padding from access tokens if present ([#851](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/851)) ([1f195e5](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/1f195e500c1a8989dcf4d73c429620ddd5b20891)) -* structured_logs compatibility with Google Cloud Logging ([#861](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/861)) ([74a6ec7](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/74a6ec70b63f4f0488470164fa4da68a26779fb2)) - -### [1.23.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.23.0...v1.23.1) (2021-07-12) - - -### Bug Fixes - -* improve log message when refresh is throttled ([#830](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/830)) ([4ffee2a](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4ffee2a1950fd6fb6703647d178a436b566b8a80)) - -## [1.23.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.22.0...v1.23.0) (2021-06-01) - - -### Features - -* add deprecation warning for Darwin 386 ([#781](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/781)) ([cdc552b](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/cdc552b8da7abb3378d43c060acb019de7e12fcc)) - - -### Bug Fixes - -* change to static base container ([#791](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/791)) ([d66233e](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d66233e2a0aecb6e80a4f802b0dc6a5cd2fa9041)) - -## [1.22.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.21.0...v1.22.0) (2021-04-21) - - -### Features - -* Add support for systemd notify ([#719](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/719)) ([4305eff](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4305eff05f1d33da4251a7b512b723cb086e4ce5)) - - -### Bug Fixes - -* Allow combined use of structured logs and -log_debug_stdout ([#726](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/726)) ([45bda77](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/45bda776fc964a3464a1703035b4f2a719779bc6)) -* return early when cert refresh fails ([#748](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/748)) ([fd21f66](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/fd21f66f2d8dc3b8e787ab0b467db4d4b85921cb)) -* structured logging respects the -verbose flag ([#737](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/737)) ([f35422f](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/f35422f449a0c79f6b2225de21c26c2da04d3528)) - -## [1.21.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.20.2...v1.21.0) (2021-04-05) - - -### Features - -* add support for structured logs ([#650](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/650)) ([ca8993a](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ca8993a2110affa0b0cbbfdebf6f6bdd86004e9f)) - - -### Bug Fixes - -* improve cache to prevent multiple concurrent refreshes ([#674](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/674)) ([c5ffa69](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c5ffa69952eba713e7acc688841f9b448a180625)) -* lower refresh buffer and config throttle when IAM authn is enabled ([#680](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/680)) ([58acab3](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/58acab3b03375032501f17c85949db493af7a292)) -* prevent refreshCfg from scheduling multiple refreshes ([#666](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/666)) ([52db349](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/52db3492ac78a9a68218c2a12840c4016b1d0b99)) - -### [1.20.2](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.20.1...v1.20.2) (2021-03-05) - - -### Bug Fixes - -* ensure certificate expiration is correct ([#659](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/659)) ([2fd2504](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/2fd2504381405b0d5fe7cc81d3c55a15f949df99)) -* perform initial gcloud check and reuse token ([#657](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/657)) ([f3bf3f9](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/f3bf3f931621285875363fab5fe3563bc82a3d94)) - -### [1.20.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.20.0...v1.20.1) (2021-03-04) - - -### Bug Fixes - -* prevent untrusted gcloud exe's from running ([#649](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/649)) ([0f0ff49](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/0f0ff49a0fac990ba1ec05a6cbd4e666e3141c08)) -* use new oauth2 token with cert refresh ([#648](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/648)) ([6d5e455](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6d5e4558a63957714f6347c9768e671586c0a605)) -* verify TokenSource exists in TokenExpiration() ([#642](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/642)) ([d01d7eb](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d01d7eb78652cf83f713b5d47bb696378929e8a6)) - -## [1.20.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.19.2...v1.20.0) (2021-02-24) - - -### Features - -* add ARM releases ([#631](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/631)) ([d3fb7f6](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/d3fb7f6394f2c641f0ba7339ab29a1c02d82e396)) -* Added '-enable_iam_login' flag for IAM db authentication ([#583](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/583)) ([470f92d](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/470f92d29d7a32f7903a3cb6d49fb09363185866)) - - -### [1.19.2](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.19.1...v1.19.2) (2021-02-16) - - -### Bug Fixes - -* improve logging for file descriptor limits ([#609](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/609)) ([b42b681](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/b42b68134543fbee7da4fbb9a8d667fd9153bec2)), closes [#413](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/413) - -### [1.19.1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.19.0...v1.19.1) (2020-12-02) - - -### Bug Fixes - -* Ensure necessary fields are 64-bit aligned ([#550](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/550)) ([4575c8f](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4575c8f8cb496ac3069208e446c47fb6c6acb868)) - -## [1.19.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.18.0...v1.19.0) (2020-11-18) - - -### Features - -* Added DialContext to Client and proxy package ([#483](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/483)) ([c84aa50](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c84aa5079668e07e3d2dc8f254d30e1103a6ead3)) -* use regionalized instance ids to prevent global conflicts with sqladmin v1 ([#504](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/504)) ([6c45513](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6c455136a24b841dbfc015a1f8ed7505f9e77dec)) - - -### Bug Fixes - -* **containers:** Allow non-root users to mount fuse filesystems for alpine and buster images ([#540](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/540)) ([5b653f5](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/5b653f5df6d9c4c226e3c4f6036d5e7d4c43c699)) -* only allow fuse mode to unmount if an error occurs first ([#537](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/537)) ([6caef36](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6caef36968d23b931c824450e418e29ac6277191)) -* refreshCfg no longer caches error over valid cert ([#521](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/521)) ([4a6b3d8](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4a6b3d8c895e2634afd8cee2341db668f20b9a33)) - -## [1.18.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.17.0...v1.18.0) (2020-09-08) - - -### Features - -* **containers:** Add "-alpine" and "-buster" based images. ([#415](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/415)) ([ebcf294](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ebcf294b9ee028340695868fb6f4cc4bbe09d849)) -* **containers:** Add fuse to alpine and buster images ([#459](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/459)) ([0f28fcd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/0f28fcd008a5bb863ec2ca1402c31ae81d7dae5d)) - - -### Bug Fixes -* Print out any errors during SIGTERM-caused shutdown ([#389](https://github.com/GoogleCloudPlatform/cloudsql-proxy/pull/389)) -* Optimize `-term-timeout` wait ([#391](https://github.com/GoogleCloudPlatform/cloudsql-proxy/pull/391)) -* Add socket suffix for Postgres instances when running in `-fuse` mode ([#426](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/426)) ([20ffaec](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/20ffaec2f0f00a2516206a0453bd0d1c6e62770c)) -* **containers:** Specify nonroot user by uid to work with runAsNonRoot ([#402](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/402)) ([c5c0be1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c5c0be1b60bfc1c3fa862039619908a328066e5e)) -* Releases are now tagged using `vMAJOR.MINOR.PATCH` for correct compatibility with go-modules. Please note that this will effect container image tags (which were previously only `vMAJOR.MINOR`), since these tags correspond directly to the release on GitHub. diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 63a15e2c31..0000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,40 +0,0 @@ -# This is the official list of people who can contribute -# (and typically have contributed) code to the repository. -# The AUTHORS file lists the copyright holders; this file -# lists people. For example, Google employees are listed here -# but not in AUTHORS, because Google holds the copyright. -# -# The submission process automatically checks to make sure -# that people submitting code are listed in this file (by email address). -# -# Names should be added to this file only after verifying that -# the individual or the individual's organization has agreed to -# the appropriate Contributor License Agreement, found here: -# -# https://cla.developers.google.com/about/google-individual -# https://cla.developers.google.com/about/google-corporate -# -# The CLA can be filled out on the web: -# -# https://cla.developers.google.com/ -# -# When adding J Random Contributor's name to this file, -# either J's name or J's organization's name should be -# added to the AUTHORS file, depending on whether the -# individual or corporate CLA was used. - -# Names should be added to this file like so: -# Name -# -# An entry with two email addresses specifies that the -# first address should be used in the submit logs and -# that the second address should be recognized as the -# same person when interacting with Rietveld. - -# Please keep the list sorted. - -Ben Brown -Frank van Rest -Kevin Malachowski -Mykola Smith - diff --git a/cmd/cloud_sql_proxy/cloud_sql_proxy.go b/cmd/cloud_sql_proxy/cloud_sql_proxy.go deleted file mode 100644 index 61ce326ea1..0000000000 --- a/cmd/cloud_sql_proxy/cloud_sql_proxy.go +++ /dev/null @@ -1,734 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// cloudsql-proxy can be used as a proxy to Cloud SQL databases. It supports -// connecting to many instances and authenticating via different means. -// Specifically, a list of instances may be provided on the command line, in -// GCE metadata (for VMs), or provided during connection time via a -// FUSE-mounted directory. See flags for a more specific explanation. -package main - -import ( - _ "embed" - "errors" - "flag" - "fmt" - "io/ioutil" - "net/http" - "os" - "os/signal" - "path/filepath" - "strings" - "sync" - "syscall" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd/cloud_sql_proxy/internal/healthcheck" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/certs" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/fuse" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/limits" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/util" - - "cloud.google.com/go/compute/metadata" - "github.com/coreos/go-systemd/v22/daemon" - "golang.org/x/net/context" - "golang.org/x/oauth2" - goauth "golang.org/x/oauth2/google" - sqladmin "google.golang.org/api/sqladmin/v1beta4" -) - -var ( - version = flag.Bool("version", false, "Print the version of the proxy and exit") - verbose = flag.Bool("verbose", true, - `If false, verbose output such as information about when connections are -created/closed without error are suppressed`, - ) - quiet = flag.Bool("quiet", false, "Disable log messages") - logDebugStdout = flag.Bool("log_debug_stdout", false, "If true, log messages that are not errors will output to stdout instead of stderr") - structuredLogs = flag.Bool("structured_logs", false, "Configures all log messages to be emitted as JSON.") - - refreshCfgThrottle = flag.Duration("refresh_config_throttle", proxy.DefaultRefreshCfgThrottle, - `If set, this flag specifies the amount of forced sleep between successive -API calls in order to protect client API quota. Minimum allowed value is - `+minimumRefreshCfgThrottle.String(), - ) - checkRegion = flag.Bool("check_region", false, `If specified, the 'region' portion of the connection string is required for -Unix socket-based connections.`) - - // Settings for how to choose which instance to connect to. - dir = flag.String("dir", "", "Directory to use for placing Unix sockets representing database instances") - projects = flag.String("projects", "", - `Open sockets for each Cloud SQL Instance in the projects specified -(comma-separated list)`, - ) - instances stringListValue // -instances flag is defined in runProxy() - instanceSrc = flag.String("instances_metadata", "", `If provided, it is treated as a path to a metadata value which -is polled for a comma-separated list of instances to connect to. For example, -to use the instance metadata value named 'cloud-sql-instances' you would -provide 'instance/attributes/cloud-sql-instances'. Not compatible with -fuse`) - useFuse = flag.Bool("fuse", false, `Mount a directory at 'dir' using FUSE for accessing instances. Note that the -directory at 'dir' must be empty before this program is started.`) - fuseTmp = flag.String("fuse_tmp", defaultTmp, `Used as a temporary directory if -fuse is set. Note that files in this directory -can be removed automatically by this program.`) - - // Settings for limits - maxConnections = flag.Uint64("max_connections", 0, - `If provided, the maximum number of connections to establish before refusing -new connections. Defaults to 0 (no limit)`, - ) - fdRlimit = flag.Uint64("fd_rlimit", limits.ExpectedFDs, - `Sets the rlimit on the number of open file descriptors for the proxy to -the provided value. If set to zero, disables attempts to set the rlimit. -Defaults to a value which can support 4K connections to one instance`, - ) - termTimeout = flag.Duration("term_timeout", 0, - `When set, the proxy will wait for existing connections to close before -terminating. Any connections that haven't closed after the timeout will be -dropped`, - ) - - // Settings for authentication. - token = flag.String("token", "", "When set, the proxy uses this Bearer token for authorization.") - tokenFile = flag.String("credential_file", "", - `If provided, this json file will be used to retrieve Service Account -credentials. You may set the GOOGLE_APPLICATION_CREDENTIALS environment -variable for the same effect.`, - ) - ipAddressTypes = flag.String("ip_address_types", "PUBLIC,PRIVATE", - `Default to be 'PUBLIC,PRIVATE'. Options: a list of strings separated by -',', e.g. 'PUBLIC,PRIVATE' `, - ) - // Settings for IAM db proxy authentication - enableIAMLogin = flag.Bool("enable_iam_login", false, "Enables database user authentication using Cloud SQL's IAM DB Authentication (Postgres only).") - - skipInvalidInstanceConfigs = flag.Bool("skip_failed_instance_config", false, - `Setting this flag will allow you to prevent the proxy from terminating -when some instance configurations could not be parsed and/or are -unavailable.`, - ) - - // Setting to choose what API to connect to - host = flag.String("host", "", - `When set, the proxy uses this host as the base API path. Example: -https://sqladmin.googleapis.com`, - ) - quotaProject = flag.String("quota_project", "", - `Specifies the project to use for Cloud SQL Admin API quota tracking.`) - - // Settings for healthcheck - useHTTPHealthCheck = flag.Bool("use_http_health_check", false, "When set, creates an HTTP server that checks and communicates the health of the proxy client.") - healthCheckPort = flag.String("health_check_port", "8090", "When applicable, health checks take place on this port number. Defaults to 8090.") -) - -const ( - minimumRefreshCfgThrottle = time.Second - - port = 3307 -) - -func init() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, ` -The Cloud SQL Auth proxy allows simple, secure connectivity to Google Cloud SQL. It -is a long-running process that opens local sockets (either TCP or Unix sockets) -according to the parameters passed to it. A local application connects to a -Cloud SQL instance by using the corresponding socket. - - -Authorization: - * On Google Compute Engine, the default service account is used. - The Cloud SQL API must be enabled for the VM. - - * When the gcloud command-line tool is installed on the local machine, the - "active account" is used for authentication. Run 'gcloud auth list' to see - which accounts are installed on your local machine and - 'gcloud config list account' to view the active account. - - * To configure the proxy using a service account, pass the -credential_file - parameter or set the GOOGLE_APPLICATION_CREDENTIALS environment variable. - This will override gcloud or GCE (Google Compute Engine) credentials, - if they exist. - - * To configure the proxy using IAM authentication, pass the -enable_iam_login - flag. This will cause the proxy to use IAM account credentials for - database user authentication. - -General: - -quiet - Disable log messages (e.g. when new connections are established). - WARNING: this option disables ALL logging output (including connection - errors), which will likely make debugging difficult. The -quiet flag takes - precedence over the -verbose flag. - - -log_debug_stdout - When explicitly set to true, verbose and info log messages will be directed - to stdout as opposed to the default stderr. - - -verbose - When explicitly set to false, disable log messages that are not errors nor - first-time startup messages (e.g. when new connections are established). - - -structured_logs - When set to true, all log messages are written out as JSON. - - -term_timeout - How long to wait for connections to close after receiving a SIGTERM before - shutting down the proxy. Defaults to 0. If all connections close before the - duration, the proxy will shutdown early. - -Connection: - -instances - To connect to a specific list of instances, set the instances parameter - to a comma-separated list of instance connection strings. For example: - - -instances=my-project:my-region:my-instance - - For convenience, this flag may be specified multiple times. - - For connectivity over TCP, you must specify a tcp port as part of the - instance string. For example, the following example opens a loopback TCP - socket on port 3306, which will be proxied to connect to the instance - 'my-instance' in project 'my-project'. To listen on other interfaces than - localhost, a custom bind address (e.g., 0.0.0.0) may be provided. For - example: - - -instances=my-project:my-region:my-instance=tcp:3306 - or - -instances=my-project:my-region:my-instance=tcp:0.0.0.0:3306 - - When connecting over TCP, the -instances parameter is required. - - To set a custom socket name, you can specify it as part of the instance - string. The following example opens a unix socket in the directory - specified by -dir, which will be proxied to connect to the instance - 'my-instance' in project 'my-project': - - -instances=my-project:my-region:my-instance=unix:custom-socket-name - - Note: The directory specified by -dir must exist and the socket file path - (i.e., dir plus INSTANCE_CONNECTION_NAME) must be under your platform's - limit (typically 108 characters on many Unix systems, but varies by platform). - - To override the -dir parameter, specify an absolute path as shown in the - following example: - - -instances=my-project:my-region:my-instance=unix:/my/custom/sql-socket - - Supplying INSTANCES environment variable achieves the same effect. One can - use that to keep k8s manifest files constant across multiple environments - - -instances_metadata - When running on GCE (Google Compute Engine) you can avoid the need to - specify the list of instances on the command line by using the Metadata - server. This parameter specifies a path to a metadata value which is then - interpreted as a list of instances in the exact same way as the -instances - parameter. Updates to the metadata value will be observed and acted on by - the Proxy. - - -projects - To direct the proxy to allow connections to all instances in specific - projects, set the projects parameter: - - -projects=my-project - - -fuse - If your local environment has FUSE installed, you can specify the -fuse - flag to avoid the requirement to specify instances in advance. With FUSE, - any attempts to open a Unix socket in the directory specified by -dir - automatically creates that socket and connects to the corresponding - instance. - - -dir - When using Unix sockets (the default for systems which support them), the - Proxy places the sockets in the directory specified by the -dir parameter. - -Automatic instance discovery: - If the Google Cloud SQL is installed on the local machine and no instance - connection flags are specified, the proxy connects to all instances in the - gcloud tool's active project. Run 'gcloud config list project' to - display the active project. - - -Information for all flags: -`) - flag.VisitAll(func(f *flag.Flag) { - usage := strings.Replace(f.Usage, "\n", "\n ", -1) - fmt.Fprintf(os.Stderr, " -%s\n %s\n\n", f.Name, usage) - }) - } -} - -var defaultTmp = filepath.Join(os.TempDir(), "cloudsql-proxy-tmp") - -// versionString indiciates the version of the proxy currently in use. -//go:embed version.txt -var versionString string - -// metadataString indiciates additional build or distribution metadata. -var metadataString = "" - -// semanticVersion returns the version of the proxy in a semver format. -func semanticVersion() string { - v := strings.TrimSpace(versionString) - if metadataString != "" { - v += "+" + metadataString - } - return v -} - -// userAgentFromVersionString returns an appropriate user agent string for identifying this proxy process. -func userAgentFromVersionString() string { - return "cloud_sql_proxy/" + semanticVersion() -} - -const accountErrorSuffix = `Please create a new VM with Cloud SQL access (scope) enabled under "Identity and API access". Alternatively, create a new "service account key" and specify it using the -credential_file parameter` - -type stringListValue []string - -func (i *stringListValue) String() string { - return strings.Join(*i, ",") -} - -func (i *stringListValue) Set(s string) error { - *i = append(*i, stringList(s)...) - return nil -} - -func checkFlags(onGCE bool) error { - if !onGCE { - if *instanceSrc != "" { - return errors.New("-instances_metadata unsupported outside of Google Compute Engine") - } - return nil - } - - if *token != "" || *tokenFile != "" || os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") != "" { - return nil - } - - // Check if gcloud credentials are available and if so, skip checking the GCE VM service account scope. - _, err := util.GcloudConfig() - if err == nil { - return nil - } - - scopes, err := metadata.Scopes("default") - if err != nil { - if _, ok := err.(metadata.NotDefinedError); ok { - return errors.New("no service account found for this Compute Engine VM. " + accountErrorSuffix) - } - return fmt.Errorf("error checking scopes: %T %v | %+v", err, err, err) - } - - ok := false - for _, sc := range scopes { - if sc == proxy.SQLScope || sc == "https://www.googleapis.com/auth/cloud-platform" { - ok = true - break - } - } - if !ok { - return errors.New(`the default Compute Engine service account is not configured with sufficient permissions to access the Cloud SQL API from this VM. ` + accountErrorSuffix) - } - return nil -} - -func authenticatedClientFromPath(ctx context.Context, f string) (*http.Client, oauth2.TokenSource, error) { - all, err := ioutil.ReadFile(f) - if err != nil { - return nil, nil, fmt.Errorf("invalid json file %q: %v", f, err) - } - // First try and load this as a service account config, which allows us to see the service account email: - if cfg, err := goauth.JWTConfigFromJSON(all, proxy.SQLScope); err == nil { - logging.Infof("using credential file for authentication; email=%s", cfg.Email) - return cfg.Client(ctx), cfg.TokenSource(ctx), nil - } - - cred, err := goauth.CredentialsFromJSON(ctx, all, proxy.SQLScope) - if err != nil { - return nil, nil, fmt.Errorf("invalid json file %q: %v", f, err) - } - logging.Infof("using credential file for authentication; path=%q", f) - return oauth2.NewClient(ctx, cred.TokenSource), cred.TokenSource, nil -} - -func authenticatedClient(ctx context.Context) (*http.Client, oauth2.TokenSource, error) { - if *tokenFile != "" { - return authenticatedClientFromPath(ctx, *tokenFile) - } else if tok := *token; tok != "" { - src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: tok}) - return oauth2.NewClient(ctx, src), src, nil - } else if f := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); f != "" { - return authenticatedClientFromPath(ctx, f) - } - - // If flags or env don't specify an auth source, try either gcloud or application default - // credentials. - src, err := util.GcloudTokenSource(ctx) - if err != nil { - src, err = goauth.DefaultTokenSource(ctx, proxy.SQLScope) - } - if err != nil { - return nil, nil, err - } - - return oauth2.NewClient(ctx, src), src, nil -} - -// quotaProjectTransport is an http.RoundTripper that adds an X-Goog-User-Project -// header to all requests for quota and billing purposes. -// -// For details, see: -// https://cloud.google.com/apis/docs/system-parameters#definitions -type quotaProjectTransport struct { - base http.RoundTripper - project string -} - -var _ http.RoundTripper = quotaProjectTransport{} - -// RoundTrip adds a X-Goog-User-Project header to each request. -func (t quotaProjectTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if req.Header == nil { - req.Header = make(http.Header) - } - req.Header.Add("X-Goog-User-Project", t.project) - return t.base.RoundTrip(req) -} - -// configureQuotaProject configures an HTTP client to use the provided project -// for quota calculations for all requests. -func configureQuotaProject(c *http.Client, project string) { - // Copy the given client's tripper. Note that tripper can be nil, which is equivalent to - // http.DefaultTransport. (See https://golang.org/pkg/net/http/#Client) - base := c.Transport - if base == nil { - base = http.DefaultTransport - } - c.Transport = quotaProjectTransport{ - base: base, - project: project, - } -} - -func stringList(s string) []string { - spl := strings.Split(s, ",") - if len(spl) == 1 && spl[0] == "" { - return nil - } - return spl -} - -func listInstances(ctx context.Context, cl *http.Client, projects []string) ([]string, error) { - if len(projects) == 0 { - // No projects requested. - return nil, nil - } - - sql, err := sqladmin.New(cl) - if err != nil { - return nil, err - } - if *host != "" { - sql.BasePath = *host - } - - ch := make(chan string) - var wg sync.WaitGroup - wg.Add(len(projects)) - for _, proj := range projects { - proj := proj - go func() { - err := sql.Instances.List(proj).Pages(ctx, func(r *sqladmin.InstancesListResponse) error { - for _, in := range r.Items { - // The Proxy is only support on Second Gen - if in.BackendType == "SECOND_GEN" { - ch <- in.ConnectionName - } - } - return nil - }) - if err != nil { - logging.Errorf("Error listing instances in %v: %v", proj, err) - } - wg.Done() - }() - } - go func() { - wg.Wait() - close(ch) - }() - var ret []string - for x := range ch { - ret = append(ret, x) - } - if len(ret) == 0 { - return nil, fmt.Errorf("no Cloud SQL Instances found in these projects: %v", projects) - } - return ret, nil -} - -func gcloudProject() ([]string, error) { - cfg, err := util.GcloudConfig() - if err != nil { - return nil, err - } - if cfg.Configuration.Properties.Core.Project == "" { - return nil, fmt.Errorf("gcloud has no active project, you can set it by running `gcloud config set project `") - } - return []string{cfg.Configuration.Properties.Core.Project}, nil -} - -func runProxy() int { - flag.Var(&instances, "instances", - `Comma-separated list of fully qualified instances (project:region:name) -to connect to. If the name has the suffix '=tcp:port', a TCP server is opened -on the specified port on localhost to proxy to that instance. It is also possible -to listen on a custom address by providing a host, e.g., '=tcp:0.0.0.0:port'. If -no value is provided for 'tcp', one socket file per instance is opened in 'dir'. -For convenience, this flag may be specified multiple times. -You may use the INSTANCES environment variable for the same effect. Using both will -use the value from the flag, Not compatible with -fuse.`, - ) - - flag.Parse() - - if *version { - fmt.Println("Cloud SQL Auth proxy:", semanticVersion()) - return 0 - } - - if *logDebugStdout { - logging.LogDebugToStdout() - } - - if !*verbose { - logging.LogVerboseToNowhere() - } - - if *structuredLogs { - cleanup, err := logging.EnableStructuredLogs(*logDebugStdout, *verbose) - if err != nil { - logging.Errorf("failed to enable structured logs: %v", err) - return 1 - } - defer cleanup() - } - - if *quiet { - logging.Infof("Cloud SQL Auth proxy logging has been disabled by the -quiet flag. All messages (including errors) will be suppressed.") - logging.DisableLogging() - } - - // Split the input ipAddressTypes to the slice of string - ipAddrTypeOptsInput := strings.Split(*ipAddressTypes, ",") - - if *fdRlimit != 0 { - if err := limits.SetupFDLimits(*fdRlimit); err != nil { - logging.Infof("failed to setup file descriptor limits: %v", err) - } - } - - if *host != "" && !strings.HasSuffix(*host, "/") { - logging.Errorf("Flag host should always end with /") - flag.PrintDefaults() - return 0 - } - - // TODO: needs a better place for consolidation - // if instances is blank and env var INSTANCES is supplied use it - if envInstances := os.Getenv("INSTANCES"); len(instances) == 0 && envInstances != "" { - instances.Set(envInstances) - } - - projList := stringList(*projects) - // TODO: it'd be really great to consolidate flag verification in one place. - if len(instances) == 0 && *instanceSrc == "" && len(projList) == 0 && !*useFuse { - var err error - projList, err = gcloudProject() - if err == nil { - logging.Infof("Using gcloud's active project: %v", projList) - } else if gErr, ok := err.(*util.GcloudError); ok && gErr.Status == util.GcloudNotFound { - logging.Errorf("gcloud is not in the path and -instances and -projects are empty") - return 1 - } else { - logging.Errorf("unable to retrieve the active gcloud project and -instances and -projects are empty: %v", err) - return 1 - } - } - - onGCE := metadata.OnGCE() - if err := checkFlags(onGCE); err != nil { - logging.Errorf(err.Error()) - return 1 - } - - ctx, cancel := context.WithCancel(context.Background()) - client, tokSrc, err := authenticatedClient(ctx) - if err != nil { - logging.Errorf(err.Error()) - return 1 - } - - if *quotaProject != "" { - logging.Infof("Using the project %q for SQL Admin API quota", *quotaProject) - configureQuotaProject(client, *quotaProject) - } - - ins, err := listInstances(ctx, client, projList) - if err != nil { - logging.Errorf(err.Error()) - return 1 - } - instances = append(instances, ins...) - cfgs, err := CreateInstanceConfigs(*dir, *useFuse, instances, *instanceSrc, client, *skipInvalidInstanceConfigs) - if err != nil { - logging.Errorf(err.Error()) - return 1 - } - - // We only need to store connections in a ConnSet if FUSE is used; otherwise - // it is not efficient to do so. - var connset *proxy.ConnSet - if *useFuse { - connset = proxy.NewConnSet() - } - - // Create proxy client first; fuse uses its cache to resolve database version. - refreshCfgThrottle := *refreshCfgThrottle - if refreshCfgThrottle < minimumRefreshCfgThrottle { - refreshCfgThrottle = minimumRefreshCfgThrottle - } - refreshCfgBuffer := proxy.DefaultRefreshCfgBuffer - if *enableIAMLogin { - refreshCfgThrottle = proxy.IAMLoginRefreshThrottle - refreshCfgBuffer = proxy.IAMLoginRefreshCfgBuffer - } - proxyClient := &proxy.Client{ - Port: port, - MaxConnections: *maxConnections, - Certs: certs.NewCertSourceOpts(client, certs.RemoteOpts{ - APIBasePath: *host, - IgnoreRegion: !*checkRegion, - UserAgent: userAgentFromVersionString(), - IPAddrTypeOpts: ipAddrTypeOptsInput, - EnableIAMLogin: *enableIAMLogin, - TokenSource: tokSrc, - }), - Conns: connset, - RefreshCfgThrottle: refreshCfgThrottle, - RefreshCfgBuffer: refreshCfgBuffer, - } - - var hc *healthcheck.Server - if *useHTTPHealthCheck { - // Extract a list of all instances specified statically. List is empty when in fuse mode. - var insts []string - for _, cfg := range cfgs { - insts = append(insts, cfg.Instance) - } - hc, err = healthcheck.NewServer(proxyClient, *healthCheckPort, insts) - if err != nil { - logging.Errorf("[Health Check] Could not initialize health check server: %v", err) - return 1 - } - defer hc.Close(ctx) - } - - // Initialize a source of new connections to Cloud SQL instances. - var connSrc <-chan proxy.Conn - if *useFuse { - c, fuse, err := fuse.NewConnSrc(*dir, *fuseTmp, proxyClient, connset) - if err != nil { - logging.Errorf("Could not start fuse directory at %q: %v", *dir, err) - return 1 - } - connSrc = c - defer fuse.Close() - } else { - updates := make(chan string) - if *instanceSrc != "" { - go func() { - for { - err := metadata.Subscribe(*instanceSrc, func(v string, ok bool) error { - if ok { - updates <- v - } - return nil - }) - if err != nil { - logging.Errorf("Error on receiving new instances from metadata: %v", err) - } - time.Sleep(5 * time.Second) - } - }() - } - - c, err := WatchInstances(*dir, cfgs, updates, client) - if err != nil { - logging.Errorf(err.Error()) - return 1 - } - connSrc = c - } - - logging.Infof("Ready for new connections") - - if hc != nil { - hc.NotifyStarted() - } - - signals := make(chan os.Signal, 1) - signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) - - shutdown := make(chan int, 1) - go func() { - defer func() { cancel(); close(shutdown) }() - <-signals - logging.Infof("Received TERM signal. Waiting up to %s before terminating.", *termTimeout) - go func() { - if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil { - logging.Errorf("Failed to notify systemd of termination: %v", err) - } - }() - - err := proxyClient.Shutdown(*termTimeout) - if err != nil { - logging.Errorf("Error during SIGTERM shutdown: %v", err) - shutdown <- 2 - return - } - }() - - // If running under systemd with Type=notify, we'll send a message to the - // service manager that we are ready to handle connections now, and any other - // units that are waiting for us can start. - go func() { - if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { - logging.Errorf("Failed to notify systemd of readiness: %v", err) - } - }() - proxyClient.RunContext(ctx, connSrc) - if code, ok := <-shutdown; ok { - return code - } - return 0 -} - -func main() { - code := runProxy() - os.Exit(code) -} diff --git a/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go b/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go deleted file mode 100644 index 4319379bc8..0000000000 --- a/cmd/cloud_sql_proxy/cloud_sql_proxy_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2022 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "os" - "strings" - "testing" -) - -func TestVersionStripsNewline(t *testing.T) { - v, err := os.ReadFile("version.txt") - if err != nil { - t.Fatalf("failed to read verion.txt: %v", err) - } - want := strings.TrimSpace(string(v)) - - if got := semanticVersion(); got != want { - t.Fatalf("want = %q, got = %q", want, got) - } -} diff --git a/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go deleted file mode 100644 index 7b5ed0e708..0000000000 --- a/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2021 Google LLC All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package healthcheck tests and communicates the health of the Cloud SQL Auth proxy. -package healthcheck - -import ( - "context" - "errors" - "net" - "net/http" - "sync" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" -) - -const ( - startupPath = "/startup" - livenessPath = "/liveness" - readinessPath = "/readiness" -) - -// Server is a type used to implement health checks for the proxy. -type Server struct { - // started is used to indicate whether the proxy has finished starting up. - // If started is open, startup has not finished. If started is closed, - // startup is complete. - started chan struct{} - // once ensures that started can only be closed once. - once *sync.Once - // port designates the port number on which Server listens and serves. - port string - // srv is a pointer to the HTTP server used to communicate proxy health. - srv *http.Server - // instances is a list of all instances specified statically (e.g. as flags to the binary) - instances []string -} - -// NewServer initializes a Server and exposes HTTP endpoints used to -// communicate proxy health. -func NewServer(c *proxy.Client, port string, staticInst []string) (*Server, error) { - mux := http.NewServeMux() - - srv := &http.Server{ - Addr: ":" + port, - Handler: mux, - } - - hcServer := &Server{ - started: make(chan struct{}), - once: &sync.Once{}, - port: port, - srv: srv, - instances: staticInst, - } - - mux.HandleFunc(startupPath, func(w http.ResponseWriter, _ *http.Request) { - if !hcServer.proxyStarted() { - w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte("error")) - return - } - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - }) - - mux.HandleFunc(readinessPath, func(w http.ResponseWriter, _ *http.Request) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - if !isReady(ctx, c, hcServer) { - w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte("error")) - return - } - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - }) - - mux.HandleFunc(livenessPath, func(w http.ResponseWriter, _ *http.Request) { - if !isLive(c) { - w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte("error")) - return - } - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - }) - - ln, err := net.Listen("tcp", srv.Addr) - if err != nil { - return nil, err - } - - go func() { - if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { - logging.Errorf("[Health Check] Failed to serve: %v", err) - } - }() - - return hcServer, nil -} - -// Close gracefully shuts down the HTTP server belonging to the Server. -func (s *Server) Close(ctx context.Context) error { - return s.srv.Shutdown(ctx) -} - -// NotifyStarted tells the Server that the proxy has finished startup. -func (s *Server) NotifyStarted() { - s.once.Do(func() { close(s.started) }) -} - -// proxyStarted returns true if started is closed, false otherwise. -func (s *Server) proxyStarted() bool { - select { - case <-s.started: - return true - default: - return false - } -} - -// isLive returns true as long as the proxy Client has all valid connections. -func isLive(c *proxy.Client) bool { - invalid := c.InvalidInstances() - alive := len(invalid) == 0 - if !alive { - for _, err := range invalid { - logging.Errorf("[Health Check] Liveness failed: %v", err) - } - } - return alive -} - -// isReady will check the following criteria: -// 1. Finished starting up / been sent the 'Ready for Connections' log. -// 2. Not yet hit the MaxConnections limit, if set. -// 3. Able to dial all specified instances without error. -func isReady(ctx context.Context, c *proxy.Client, s *Server) bool { - // Not ready until we reach the 'Ready for Connections' log. - if !s.proxyStarted() { - logging.Errorf("[Health Check] Readiness failed because proxy has not finished starting up.") - return false - } - - // Not ready if the proxy is at the optional MaxConnections limit. - if !c.AvailableConn() { - logging.Errorf("[Health Check] Readiness failed because proxy has reached the maximum connections limit (%v).", c.MaxConnections) - return false - } - - // Not ready if one or more instances cannot be dialed. - instances := s.instances - if s.instances == nil { // Proxy is in fuse mode. - instances = c.GetInstances() - } - - canDial := true - var once sync.Once - var wg sync.WaitGroup - - for _, inst := range instances { - wg.Add(1) - go func(inst string) { - defer wg.Done() - conn, err := c.DialContext(ctx, inst) - if err != nil { - logging.Errorf("[Health Check] Readiness failed because proxy couldn't connect to %q: %v", inst, err) - once.Do(func() { canDial = false }) - return - } - - err = conn.Close() - if err != nil { - logging.Errorf("[Health Check] Readiness: error while closing connection: %v", err) - } - }(inst) - } - wg.Wait() - - return canDial -} diff --git a/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go b/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go deleted file mode 100644 index f6e0e2c9a2..0000000000 --- a/cmd/cloud_sql_proxy/internal/healthcheck/healthcheck_test.go +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2021 Google LLC All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package healthcheck_test - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "net" - "net/http" - "testing" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/cmd/cloud_sql_proxy/internal/healthcheck" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" -) - -const ( - startupPath = "/startup" - livenessPath = "/liveness" - readinessPath = "/readiness" - testPort = "8090" -) - -type fakeCertSource struct{} - -func (cs *fakeCertSource) Local(instance string) (tls.Certificate, error) { - return tls.Certificate{ - Leaf: &x509.Certificate{ - NotAfter: time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC), - }, - }, nil -} - -func (cs *fakeCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { - return &x509.Certificate{}, "fake address", "fake name", "fake version", nil -} - -type failingCertSource struct{} - -func (cs *failingCertSource) Local(instance string) (tls.Certificate, error) { - return tls.Certificate{}, errors.New("failed") -} - -func (cs *failingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { - return nil, "", "", "", errors.New("failed") -} - -// Test to verify that when the proxy client is up, the liveness endpoint writes http.StatusOK. -func TestLivenessPasses(t *testing.T) { - s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) - if err != nil { - t.Fatalf("Could not initialize health check: %v", err) - } - defer s.Close(context.Background()) - - resp, err := http.Get("http://localhost:" + testPort + livenessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) - } -} - -func TestLivenessFails(t *testing.T) { - c := &proxy.Client{ - Certs: &failingCertSource{}, - Dialer: func(string, string) (net.Conn, error) { - return nil, errors.New("error") - }, - } - // ensure cache has errored config - _, err := c.Dial("proj:region:instance") - if err == nil { - t.Fatalf("expected Dial to fail, but it succeeded") - } - - s, err := healthcheck.NewServer(c, testPort, []string{"proj:region:instance"}) - if err != nil { - t.Fatalf("Could not initialize health check: %v", err) - } - defer s.Close(context.Background()) - - resp, err := http.Get("http://localhost:" + testPort + livenessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - defer resp.Body.Close() - want := http.StatusServiceUnavailable - if got := resp.StatusCode; got != want { - t.Errorf("want %v, got %v", want, got) - } -} - -// Test to verify that when startup HAS finished (and MaxConnections limit not specified), -// the startup and readiness endpoints write http.StatusOK. -func TestStartupPass(t *testing.T) { - s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) - if err != nil { - t.Fatalf("Could not initialize health check: %v", err) - } - defer s.Close(context.Background()) - - // Simulate the proxy client completing startup. - s.NotifyStarted() - - resp, err := http.Get("http://localhost:" + testPort + startupPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("%v: want %v, got %v", startupPath, http.StatusOK, resp.StatusCode) - } - - resp, err = http.Get("http://localhost:" + testPort + readinessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("%v: want %v, got %v", readinessPath, http.StatusOK, resp.StatusCode) - } -} - -// Test to verify that when startup has NOT finished, the startup and readiness endpoints write -// http.StatusServiceUnavailable. -func TestStartupFail(t *testing.T) { - s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) - if err != nil { - t.Fatalf("Could not initialize health check: %v", err) - } - defer s.Close(context.Background()) - - resp, err := http.Get("http://localhost:" + testPort + startupPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusServiceUnavailable { - t.Errorf("%v: want %v, got %v", startupPath, http.StatusOK, resp.StatusCode) - } - - resp, err = http.Get("http://localhost:" + testPort + readinessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusServiceUnavailable { - t.Errorf("%v: want %v, got %v", readinessPath, http.StatusOK, resp.StatusCode) - } -} - -// Test to verify that when startup has finished, but MaxConnections has been reached, -// the readiness endpoint writes http.StatusServiceUnavailable. -func TestMaxConnectionsReached(t *testing.T) { - c := &proxy.Client{ - MaxConnections: 1, - } - s, err := healthcheck.NewServer(c, testPort, nil) - if err != nil { - t.Fatalf("Could not initialize health check: %v", err) - } - defer s.Close(context.Background()) - - s.NotifyStarted() - c.ConnectionsCounter = c.MaxConnections // Simulate reaching the limit for maximum number of connections - - resp, err := http.Get("http://localhost:" + testPort + readinessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusServiceUnavailable { - t.Errorf("want %v, got %v", http.StatusServiceUnavailable, resp.StatusCode) - } -} - -// Test to verify that when dialing instance(s) returns an error, the readiness endpoint -// writes http.StatusServiceUnavailable. -func TestDialFail(t *testing.T) { - tests := map[string]struct { - insts []string - }{ - "Single instance": {insts: []string{"project:region:instance"}}, - "Multiple instances": {insts: []string{"project:region:instance-1", "project:region:instance-2", "project:region:instance-3"}}, - } - - c := &proxy.Client{ - Certs: &fakeCertSource{}, - Dialer: func(string, string) (net.Conn, error) { - return nil, errors.New("error") - }, - } - - for name, test := range tests { - func() { - s, err := healthcheck.NewServer(c, testPort, test.insts) - if err != nil { - t.Fatalf("%v: Could not initialize health check: %v", name, err) - } - defer s.Close(context.Background()) - s.NotifyStarted() - - resp, err := http.Get("http://localhost:" + testPort + readinessPath) - if err != nil { - t.Fatalf("%v: HTTP GET failed: %v", name, err) - } - if resp.StatusCode != http.StatusServiceUnavailable { - t.Errorf("want %v, got %v", http.StatusServiceUnavailable, resp.StatusCode) - } - }() - } -} - -// Test to verify that after closing a healthcheck, its liveness endpoint serves -// an error. -func TestCloseHealthCheck(t *testing.T) { - s, err := healthcheck.NewServer(&proxy.Client{}, testPort, nil) - if err != nil { - t.Fatalf("Could not initialize health check: %v", err) - } - defer s.Close(context.Background()) - - resp, err := http.Get("http://localhost:" + testPort + livenessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) - } - - err = s.Close(context.Background()) - if err != nil { - t.Fatalf("Failed to close health check: %v", err) - } - - _, err = http.Get("http://localhost:" + testPort + livenessPath) - if err == nil { - t.Fatalf("HTTP GET did not return error after closing health check server.") - } -} diff --git a/cmd/cloud_sql_proxy/proxy.go b/cmd/cloud_sql_proxy/proxy.go deleted file mode 100644 index c9943ee2c2..0000000000 --- a/cmd/cloud_sql_proxy/proxy.go +++ /dev/null @@ -1,388 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -// This file contains code for supporting local sockets for the Cloud SQL Auth proxy. - -import ( - "bytes" - "errors" - "fmt" - "net" - "net/http" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/fuse" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" - sqladmin "google.golang.org/api/sqladmin/v1beta4" -) - -// WatchInstances handles the lifecycle of local sockets used for proxying -// local connections. Values received from the updates channel are -// interpretted as a comma-separated list of instances. The set of sockets in -// 'dir' is the union of 'instances' and the most recent list from 'updates'. -func WatchInstances(dir string, cfgs []instanceConfig, updates <-chan string, cl *http.Client) (<-chan proxy.Conn, error) { - ch := make(chan proxy.Conn, 1) - - // Instances specified statically (e.g. as flags to the binary) will always - // be available. They are ignored if also returned by the GCE metadata since - // the socket will already be open. - staticInstances := make(map[string]net.Listener, len(cfgs)) - for _, v := range cfgs { - l, err := listenInstance(ch, v) - if err != nil { - return nil, err - } - staticInstances[v.Instance] = l - } - - if updates != nil { - go watchInstancesLoop(dir, ch, updates, staticInstances, cl) - } - return ch, nil -} - -func watchInstancesLoop(dir string, dst chan<- proxy.Conn, updates <-chan string, static map[string]net.Listener, cl *http.Client) { - dynamicInstances := make(map[string]net.Listener) - for instances := range updates { - // All instances were legal when we started, so we pass false below to ensure we don't skip them - // later if they became unhealthy for some reason; this would be a serious enough problem. - list, err := parseInstanceConfigs(dir, strings.Split(instances, ","), cl, false) - if err != nil { - logging.Errorf("%v", err) - // If we do not have a valid list of instances, skip this update - continue - } - - stillOpen := make(map[string]net.Listener) - for _, cfg := range list { - instance := cfg.Instance - - // If the instance is specified in the static list don't do anything: - // it's already open and should stay open forever. - if _, ok := static[instance]; ok { - continue - } - - if l, ok := dynamicInstances[instance]; ok { - delete(dynamicInstances, instance) - stillOpen[instance] = l - continue - } - - l, err := listenInstance(dst, cfg) - if err != nil { - logging.Errorf("Couldn't open socket for %q: %v", instance, err) - continue - } - stillOpen[instance] = l - } - - // Any instance in dynamicInstances was not in the most recent metadata - // update. Clean up those instances' sockets by closing them; note that - // this does not affect any existing connections instance. - for instance, listener := range dynamicInstances { - logging.Infof("Closing socket for instance %v", instance) - listener.Close() - } - - dynamicInstances = stillOpen - } - - for _, v := range static { - if err := v.Close(); err != nil { - logging.Errorf("Error closing %q: %v", v.Addr(), err) - } - } - for _, v := range dynamicInstances { - if err := v.Close(); err != nil { - logging.Errorf("Error closing %q: %v", v.Addr(), err) - } - } -} - -func remove(path string) { - if err := os.Remove(path); err != nil && !os.IsNotExist(err) { - logging.Infof("Remove(%q) error: %v", path, err) - } -} - -// listenInstance starts listening on a new unix socket in dir to connect to the -// specified instance. New connections to this socket are sent to dst. -func listenInstance(dst chan<- proxy.Conn, cfg instanceConfig) (net.Listener, error) { - unix := cfg.Network == "unix" - if unix { - remove(cfg.Address) - } - l, err := net.Listen(cfg.Network, cfg.Address) - if err != nil { - return nil, err - } - if unix { - if err := os.Chmod(cfg.Address, 0777|os.ModeSocket); err != nil { - logging.Errorf("couldn't update permissions for socket file %q: %v; other users may not be unable to connect", cfg.Address, err) - } - } - - go func() { - for { - start := time.Now() - c, err := l.Accept() - if err != nil { - logging.Errorf("Error in accept for %q on %v: %v", cfg, cfg.Address, err) - if nerr, ok := err.(net.Error); ok && nerr.Temporary() { - d := 10*time.Millisecond - time.Since(start) - if d > 0 { - time.Sleep(d) - } - continue - } - l.Close() - return - } - logging.Verbosef("New connection for %q", cfg.Instance) - - switch clientConn := c.(type) { - case *net.TCPConn: - clientConn.SetKeepAlive(true) - clientConn.SetKeepAlivePeriod(1 * time.Minute) - - } - dst <- proxy.Conn{cfg.Instance, c} - } - }() - - logging.Infof("Listening on %s for %s", cfg.Address, cfg.Instance) - return l, nil -} - -type instanceConfig struct { - Instance string - Network, Address string -} - -// loopbackForNet maps a network (e.g. tcp6) to the loopback address for that -// network. It is updated during the initialization of validNets to include a -// valid loopback address for "tcp". -var loopbackForNet = map[string]string{ - "tcp4": "127.0.0.1", - "tcp6": "::1", -} - -// validNets tracks the networks that are valid for this platform and machine. -var validNets = func() map[string]bool { - m := map[string]bool{ - "unix": runtime.GOOS != "windows", - } - - anyTCP := false - for _, n := range []string{"tcp4", "tcp6"} { - host, ok := loopbackForNet[n] - if !ok { - // This is effectively a compile-time error. - panic(fmt.Sprintf("no loopback address found for %v", n)) - } - // Open any port to see if the net is valid. - x, err := net.Listen(n, net.JoinHostPort(host, "0")) - if err != nil { - // Error is too verbose to be useful. - continue - } - x.Close() - m[n] = true - - if !anyTCP { - anyTCP = true - // Set the loopback value for generic tcp if it hasn't already been - // set. (If both tcp4/tcp6 are supported the first one in the list - // (tcp4's 127.0.0.1) is used. - loopbackForNet["tcp"] = host - } - } - if anyTCP { - m["tcp"] = true - } - return m -}() - -func parseInstanceConfig(dir, instance string, cl *http.Client) (instanceConfig, error) { - var ret instanceConfig - proj, region, name, args, err := proxy.ParseInstanceConnectionName(instance) - if err != nil { - return instanceConfig{}, err - } - ret.Instance = args[0] - regionName := fmt.Sprintf("%s~%s", region, name) - if len(args) == 1 { - // Default to listening via unix socket in specified directory - ret.Network = "unix" - ret.Address = filepath.Join(dir, instance) - } else { - // Parse the instance options if present. - opts := strings.SplitN(args[1], ":", 2) - if len(opts) != 2 { - return instanceConfig{}, fmt.Errorf("invalid instance options: must be in the form `unix:/path/to/socket`, `tcp:port`, `tcp:host:port`; invalid option was %q", strings.Join(opts, ":")) - } - ret.Network = opts[0] - var err error - if ret.Network == "unix" { - if strings.HasPrefix(opts[1], "/") { - ret.Address = opts[1] // Root path. - } else { - ret.Address = filepath.Join(dir, opts[1]) - } - } else { - ret.Address, err = parseTCPOpts(opts[0], opts[1]) - } - if err != nil { - return instanceConfig{}, err - } - } - - // Use the SQL Admin API to verify compatibility with the instance. - sql, err := sqladmin.New(cl) - if err != nil { - return instanceConfig{}, err - } - if *host != "" { - sql.BasePath = *host - } - inst, err := sql.Connect.Get(proj, regionName).Do() - if err != nil { - return instanceConfig{}, err - } - if inst.BackendType == "FIRST_GEN" { - logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") - return instanceConfig{}, fmt.Errorf("%q is a first generation instance", instance) - } - // Postgres instances use a special suffix on the unix socket. - // See https://www.postgresql.org/docs/11/runtime-config-connection.html - if ret.Network == "unix" && strings.HasPrefix(strings.ToLower(inst.DatabaseVersion), "postgres") { - // Verify the directory exists. - if err := os.MkdirAll(ret.Address, 0755); err != nil { - return instanceConfig{}, err - } - ret.Address = filepath.Join(ret.Address, ".s.PGSQL.5432") - } - - if !validNets[ret.Network] { - return ret, fmt.Errorf("invalid %q: unsupported network: %v", instance, ret.Network) - } - return ret, nil -} - -// parseTCPOpts parses the instance options when specifying tcp port options. -func parseTCPOpts(ntwk, addrOpt string) (string, error) { - if strings.Contains(addrOpt, ":") { - return addrOpt, nil // User provided a host and port; use that. - } - // No "host" part of the address. Be safe and assume that they want a loopback address. - addr, ok := loopbackForNet[ntwk] - if !ok { - return "", fmt.Errorf("invalid %q:%q: unrecognized network %v", ntwk, addrOpt, ntwk) - } - return net.JoinHostPort(addr, addrOpt), nil -} - -// parseInstanceConfigs calls parseInstanceConfig for each instance in the -// provided slice, collecting errors along the way. There may be valid -// instanceConfigs returned even if there's an error. -func parseInstanceConfigs(dir string, instances []string, cl *http.Client, skipFailedInstanceConfigs bool) ([]instanceConfig, error) { - errs := new(bytes.Buffer) - var cfg []instanceConfig - for _, v := range instances { - if v == "" { - continue - } - if c, err := parseInstanceConfig(dir, v, cl); err != nil { - if skipFailedInstanceConfigs { - logging.Infof("There was a problem when parsing an instance configuration but ignoring due to the configuration. Error: %v", err) - } else { - fmt.Fprintf(errs, "\n\t%v", err) - } - - } else { - cfg = append(cfg, c) - } - } - - var err error - if errs.Len() > 0 { - err = fmt.Errorf("errors parsing config:%s", errs) - } - return cfg, err -} - -// CreateInstanceConfigs verifies that the parameters passed to it are valid -// for the proxy for the platform and system and then returns a slice of valid -// instanceConfig. It is possible for the instanceConfig to be empty if no valid -// configurations were specified, however `err` will be set. -func CreateInstanceConfigs(dir string, useFuse bool, instances []string, instancesSrc string, cl *http.Client, skipFailedInstanceConfigs bool) ([]instanceConfig, error) { - if useFuse && !fuse.Supported() { - return nil, errors.New("FUSE not supported on this system") - } - - cfgs, err := parseInstanceConfigs(dir, instances, cl, skipFailedInstanceConfigs) - if err != nil { - return nil, err - } - - if dir == "" { - // Reasons to set '-dir': - // - Using -fuse - // - Using the metadata to get a list of instances - // - Having an instance that uses a 'unix' network - if useFuse { - return nil, errors.New("must set -dir because -fuse was set") - } else if instancesSrc != "" { - return nil, errors.New("must set -dir because -instances_metadata was set") - } else { - for _, v := range cfgs { - if v.Network == "unix" { - return nil, fmt.Errorf("must set -dir: using a unix socket for %v", v.Instance) - } - } - } - // Otherwise it's safe to not set -dir - } - - if useFuse { - if len(instances) != 0 || instancesSrc != "" { - return nil, errors.New("-fuse is not compatible with -projects, -instances, or -instances_metadata") - } - return nil, nil - } - // FUSE disabled. - if len(instances) == 0 && instancesSrc == "" { - // Failure to specifying instance can be caused by following reasons. - // 1. not enough information is provided by flags - // 2. failed to invoke gcloud - var flags string - if fuse.Supported() { - flags = "-projects, -fuse, -instances or -instances_metadata" - } else { - flags = "-projects, -instances or -instances_metadata" - } - - errStr := fmt.Sprintf("no instance selected because none of %s is specified", flags) - return nil, errors.New(errStr) - } - return cfgs, nil -} diff --git a/cmd/cloud_sql_proxy/proxy_test.go b/cmd/cloud_sql_proxy/proxy_test.go deleted file mode 100644 index 54837c3f44..0000000000 --- a/cmd/cloud_sql_proxy/proxy_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "runtime" - "testing" -) - -type mockTripper struct { -} - -func (m *mockTripper) RoundTrip(r *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte("{}")))}, nil -} - -var mockClient = &http.Client{Transport: &mockTripper{}} - -func TestCreateInstanceConfigs(t *testing.T) { - for _, v := range []struct { - desc string - //inputs - dir string - useFuse bool - instances []string - instancesSrc string - - // We don't need to check the []instancesConfig return value, we already - // have a TestParseInstanceConfig. - wantErr bool - - skipFailedInstanceConfig bool - - supportedOnWindows bool - }{ - { - "setting -fuse and -dir", - "dir", true, nil, "", false, false, false, - }, { - "setting -fuse", - "", true, nil, "", true, false, false, - }, { - "setting -fuse, -dir, and -instances", - "dir", true, []string{"proj:reg:x"}, "", true, false, false, - }, { - "setting -fuse, -dir, and -instances_metadata", - "dir", true, nil, "md", true, false, false, - }, { - "setting -dir and -instances (unix socket)", - "dir", false, []string{"proj:reg:x"}, "", false, false, false, - }, { - // tests for the case where invalid configs can still exist, when skipped - "setting -dir and -instances (unix socket) w/ something invalid", - "dir", false, []string{"proj:reg:x", "INVALID_PROJECT_STRING"}, "", false, true, false, - }, { - "Seting -instance (unix socket)", - "", false, []string{"proj:reg:x"}, "", true, false, false, - }, { - "setting -instance (tcp socket)", - "", false, []string{"proj:reg:x=tcp:1234"}, "", false, false, true, - }, { - "setting -instance (tcp socket) and -instances_metadata", - "", false, []string{"proj:reg:x=tcp:1234"}, "md", true, false, true, - }, { - "setting -dir, -instance (tcp socket), and -instances_metadata", - "dir", false, []string{"proj:reg:x=tcp:1234"}, "md", false, false, true, - }, { - "setting -dir, -instance (unix socket), and -instances_metadata", - "dir", false, []string{"proj:reg:x"}, "md", false, false, false, - }, { - "setting -dir and -instances_metadata", - "dir", false, nil, "md", false, false, false, - }, { - "setting -instances_metadata", - "", false, nil, "md", true, false, true, - }, - } { - if runtime.GOOS == "windows" && !v.supportedOnWindows { - continue - } - if v.useFuse && testing.Short() { - t.Skip("skipping fuse tests in short mode.") - } - _, err := CreateInstanceConfigs(v.dir, v.useFuse, v.instances, v.instancesSrc, mockClient, v.skipFailedInstanceConfig) - if v.wantErr { - if err == nil { - t.Errorf("CreateInstanceConfigs passed when %s, wanted error", v.desc) - } - continue - } - if err != nil { - t.Errorf("CreateInstanceConfigs gave error when %s: %v", v.desc, err) - } - } -} - -func TestParseInstanceConfig(t *testing.T) { - // sentinel values - var ( - anyLoopbackAddress = "" - wantErr = instanceConfig{"", "", ""} - ) - - tcs := []struct { - // inputs - dir, instance string - - wantCfg instanceConfig - }{ - { - "/x", "domain.com:my-proj:my-reg:my-instance", - instanceConfig{"domain.com:my-proj:my-reg:my-instance", "unix", "/x/domain.com:my-proj:my-reg:my-instance"}, - }, { - "/x", "my-proj:my-reg:my-instance", - instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/my-proj:my-reg:my-instance"}, - }, { - "/x", "my-proj:my-reg:my-instance=unix:socket_name", - instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/socket_name"}, - }, { - "/x", "my-proj:my-reg:my-instance=unix:/my/custom/sql-socket", - instanceConfig{"my-proj:my-reg:my-instance", "unix", "/my/custom/sql-socket"}, - }, { - "/x", "my-proj:my-reg:my-instance=tcp:1234", - instanceConfig{"my-proj:my-reg:my-instance", "tcp", anyLoopbackAddress}, - }, { - "/x", "my-proj:my-reg:my-instance=tcp4:1234", - instanceConfig{"my-proj:my-reg:my-instance", "tcp4", "127.0.0.1:1234"}, - }, { - "/x", "my-proj:my-reg:my-instance=tcp6:1234", - instanceConfig{"my-proj:my-reg:my-instance", "tcp6", "[::1]:1234"}, - }, { - "/x", "my-proj:my-reg:my-instance=tcp:my-host:1111", - instanceConfig{"my-proj:my-reg:my-instance", "tcp", "my-host:1111"}, - }, { - "/x", "my-proj:my-reg:my-instance=", - wantErr, - }, { - "/x", "my-proj:my-reg:my-instance=cool network", - wantErr, - }, { - "/x", "my-proj:my-reg:my-instance=cool network:1234", - wantErr, - }, { - "/x", "my-proj:my-reg:my-instance=oh:so:many:colons", - wantErr, - }, - } - - for _, tc := range tcs { - t.Run(fmt.Sprintf("parseInstanceConfig(%q, %q)", tc.dir, tc.instance), func(t *testing.T) { - if os.Getenv("EXPECT_IPV4_AND_IPV6") != "true" { - // Skip ipv4 and ipv6 if they are not supported by the machine. - // (assumption is that validNets isn't buggy) - if tc.wantCfg.Network == "tcp4" || tc.wantCfg.Network == "tcp6" { - if !validNets[tc.wantCfg.Network] { - t.Skipf("%q net not supported, skipping", tc.wantCfg.Network) - } - } - // Skip unix sockets on Windows - if runtime.GOOS == "windows" && tc.wantCfg.Network == "unix" { - t.Skipf("%q net not supported on Windows, skipping", tc.wantCfg.Network) - } - } - - got, err := parseInstanceConfig(tc.dir, tc.instance, mockClient) - if tc.wantCfg == wantErr { - if err != nil { - return // pass. an error was expected and returned. - } - t.Fatalf("parseInstanceConfig(%s, %s) = %+v, wanted error", tc.dir, tc.instance, got) - } - if err != nil { - t.Fatalf("parseInstanceConfig(%s, %s) had unexpected error: %v", tc.dir, tc.instance, err) - } - - if tc.wantCfg.Address == anyLoopbackAddress { - host, _, err := net.SplitHostPort(got.Address) - if err != nil { - t.Fatalf("net.SplitHostPort(%v): %v", got.Address, err) - } - ip := net.ParseIP(host) - if !ip.IsLoopback() { - t.Fatalf("want loopback, got addr: %v", got.Address) - } - - // use a placeholder address, so the rest of the config can be compared - got.Address = "" - tc.wantCfg.Address = got.Address - } - - if got != tc.wantCfg { - t.Errorf("parseInstanceConfig(%s, %s) = %+v, want %+v", tc.dir, tc.instance, got, tc.wantCfg) - } - }) - } -} diff --git a/cmd/cloud_sql_proxy/version.txt b/cmd/cloud_sql_proxy/version.txt deleted file mode 100644 index a6c4ddd7b3..0000000000 --- a/cmd/cloud_sql_proxy/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.30.1-dev diff --git a/logging/logging.go b/logging/logging.go deleted file mode 100644 index ec6d20f8c9..0000000000 --- a/logging/logging.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package logging contains helpers to support log messages. If you are using -// the Cloud SQL Auth proxy as a Go library, you can override these variables to -// control where log messages end up. -package logging - -import ( - "log" - "os" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" -) - -// Verbosef is called to write verbose logs, such as when a new connection is -// established correctly. -var Verbosef = log.Printf - -// Infof is called to write informational logs, such as when startup has -var Infof = log.Printf - -// Errorf is called to write an error log, such as when a new connection fails. -var Errorf = log.Printf - -// LogDebugToStdout updates Verbosef and Info logging to use stdout instead of stderr. -func LogDebugToStdout() { - logger := log.New(os.Stdout, "", log.LstdFlags) - Verbosef = logger.Printf - Infof = logger.Printf -} - -func noop(string, ...interface{}) {} - -// LogVerboseToNowhere updates Verbosef so verbose log messages are discarded -func LogVerboseToNowhere() { - Verbosef = noop -} - -// DisableLogging sets all logging levels to no-op's. -func DisableLogging() { - Verbosef = noop - Infof = noop - Errorf = noop -} - -// EnableStructuredLogs replaces all logging functions with structured logging -// variants. -func EnableStructuredLogs(logDebugStdout, verbose bool) (func(), error) { - // Configuration of zap is based on its Advanced Configuration example. - // See: https://pkg.go.dev/go.uber.org/zap#example-package-AdvancedConfiguration - - // Define level-handling logic. - highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl >= zapcore.ErrorLevel - }) - lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl < zapcore.ErrorLevel - }) - - // Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In - // particular, *os.File types must be locked before use. - consoleErrors := zapcore.Lock(os.Stderr) - consoleDebugging := consoleErrors - if logDebugStdout { - consoleDebugging = zapcore.Lock(os.Stdout) - } - - config := zap.NewProductionEncoderConfig() - config.LevelKey = "severity" - config.MessageKey = "message" - config.TimeKey = "timestamp" - config.EncodeLevel = zapcore.CapitalLevelEncoder - config.EncodeTime = zapcore.ISO8601TimeEncoder - consoleEncoder := zapcore.NewJSONEncoder(config) - core := zapcore.NewTee( - zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), - zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), - ) - // By default, caller and stacktrace are not included, so add them here - logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) - - sugar := logger.Sugar() - Verbosef = sugar.Infof - if !verbose { - Verbosef = noop - } - Infof = sugar.Infof - Errorf = sugar.Errorf - - return func() { - logger.Sync() - }, nil -} diff --git a/proxy/README.md b/proxy/README.md deleted file mode 100644 index 1d0c8ea95f..0000000000 --- a/proxy/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Cloud SQL proxy dialer for Go - -You can also use the Cloud SQL proxy directly from a Go program. - -These packages are primarily used as implementation for the Cloud SQL proxy -command, and may be changed in backwards incompatible ways in the future. - -## Usage - -If your program is written in [Go](https://golang.org) you can use the Cloud SQL -Proxy as a library, avoiding the need to start the Proxy as a companion process. - -Alternatively, there are Cloud SQL Connectors for [Java][] and [Python][]. - - -### MySQL - -If you're using the MySQL [go-sql-driver][go-mysql] you can use helper -functions found in the [`proxy/dialers/mysql`][mysql-godoc] - -See [example usage](dialers/mysql/hook_test.go). - -### Postgres - -If you're using the Postgres [lib/pq](https://github.com/lib/pq), you can -use the `cloudsqlpostgres` driver from [here](proxy/dialers/postgres). - -See [example usage](dialers/postgres/hook_test.go). - -[Java]: https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory -[Python]: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector -[go-mysql]: https://github.com/go-sql-driver/mysql -[mysql-godoc]: https://pkg.go.dev/github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql diff --git a/proxy/certs/certs.go b/proxy/certs/certs.go deleted file mode 100644 index 892cf2964e..0000000000 --- a/proxy/certs/certs.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package certs implements a CertSource which speaks to the public Cloud SQL API endpoint. -package certs - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "math" - mrand "math/rand" - "net/http" - "strings" - "sync" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/util" - "golang.org/x/oauth2" - "google.golang.org/api/googleapi" - sqladmin "google.golang.org/api/sqladmin/v1beta4" -) - -const defaultUserAgent = "custom cloud_sql_proxy version >= 1.10" - -// NewCertSource returns a CertSource which can be used to authenticate using -// the provided client, which must not be nil. -// -// This function is deprecated; use NewCertSourceOpts instead. -func NewCertSource(host string, c *http.Client, checkRegion bool) *RemoteCertSource { - return NewCertSourceOpts(c, RemoteOpts{ - APIBasePath: host, - IgnoreRegion: !checkRegion, - UserAgent: defaultUserAgent, - }) -} - -// RemoteOpts are a collection of options for NewCertSourceOpts. All fields are -// optional. -type RemoteOpts struct { - // APIBasePath specifies the base path for the sqladmin API. If left blank, - // the default from the autogenerated sqladmin library is used (which is - // sufficient for nearly all users) - APIBasePath string - - // IgnoreRegion specifies whether a missing or mismatched region in the - // instance name should be ignored. In a future version this value will be - // forced to 'false' by the RemoteCertSource. - IgnoreRegion bool - - // A string for the RemoteCertSource to identify itself when contacting the - // sqladmin API. - UserAgent string - - // IP address type options - IPAddrTypeOpts []string - - // Enable IAM proxy db authentication - EnableIAMLogin bool - - // Token source for token information used in cert creation - TokenSource oauth2.TokenSource - - // DelayKeyGenerate, if true, causes the RSA key to be generated lazily - // on the first connection to a database. The default behavior is to generate - // the key when the CertSource is created. - DelayKeyGenerate bool -} - -// NewCertSourceOpts returns a CertSource configured with the provided Opts. -// The provided http.Client must not be nil. -// -// Use this function instead of NewCertSource; it has a more forward-compatible -// signature. -func NewCertSourceOpts(c *http.Client, opts RemoteOpts) *RemoteCertSource { - serv, err := sqladmin.New(c) - if err != nil { - panic(err) // Only will happen if the provided client is nil. - } - if opts.APIBasePath != "" { - serv.BasePath = opts.APIBasePath - } - ua := opts.UserAgent - if ua == "" { - ua = defaultUserAgent - } - serv.UserAgent = ua - - // Set default value to be "PUBLIC,PRIVATE" if not specified - if len(opts.IPAddrTypeOpts) == 0 { - opts.IPAddrTypeOpts = []string{"PUBLIC", "PRIVATE"} - } - - // Add "PUBLIC" as an alias for "PRIMARY" - for index, ipAddressType := range opts.IPAddrTypeOpts { - if strings.ToUpper(ipAddressType) == "PUBLIC" { - opts.IPAddrTypeOpts[index] = "PRIMARY" - } - } - - certSource := &RemoteCertSource{ - serv: serv, - checkRegion: !opts.IgnoreRegion, - IPAddrTypes: opts.IPAddrTypeOpts, - EnableIAMLogin: opts.EnableIAMLogin, - TokenSource: opts.TokenSource, - } - if !opts.DelayKeyGenerate { - // Generate the RSA key now, but don't block on it. - go certSource.generateKey() - } - - return certSource -} - -// RemoteCertSource implements a CertSource, using Cloud SQL APIs to -// return Local certificates for identifying oneself as a specific user -// to the remote instance and Remote certificates for confirming the -// remote database's identity. -type RemoteCertSource struct { - // keyOnce is used to create `key` lazily. - keyOnce sync.Once - // key is the private key used for certificates returned by Local. - key *rsa.PrivateKey - // serv is used to make authenticated API calls to Cloud SQL. - serv *sqladmin.Service - // If set, providing an incorrect region in their connection string will be - // treated as an error. This is to provide the same functionality that will - // occur when API calls require the region. - checkRegion bool - // a list of ip address types that users select - IPAddrTypes []string - // flag to enable IAM proxy db authentication - EnableIAMLogin bool - // token source for the token information used in cert creation - TokenSource oauth2.TokenSource -} - -// Constants for backoffAPIRetry. These cause the retry logic to scale the -// backoff delay from 200ms to around 3.5s. -const ( - baseBackoff = float64(200 * time.Millisecond) - backoffMult = 1.618 - backoffRetries = 5 -) - -func backoffAPIRetry(desc, instance string, do func(staleRead time.Time) error) error { - var ( - err error - t time.Time - ) - for i := 0; i < backoffRetries; i++ { - err = do(t) - gErr, ok := err.(*googleapi.Error) - switch { - case !ok: - // 'ok' will also be false if err is nil. - return err - case gErr.Code == 403 && len(gErr.Errors) > 0 && gErr.Errors[0].Reason == "insufficientPermissions": - // The case where the admin API has not yet been enabled. - return fmt.Errorf("ensure that the Cloud SQL API is enabled for your project (https://console.cloud.google.com/flows/enableapi?apiid=sqladmin). Error during %s %s: %v", desc, instance, err) - case gErr.Code == 404 || gErr.Code == 403: - return fmt.Errorf("ensure that the account has access to %q (and make sure there's no typo in that name). Error during %s %s: %v", instance, desc, instance, err) - case gErr.Code < 500: - // Only Server-level HTTP errors are immediately retryable. - return err - } - - // sleep = baseBackoff * backoffMult^(retries + randomFactor) - exp := float64(i+1) + mrand.Float64() - sleep := time.Duration(baseBackoff * math.Pow(backoffMult, exp)) - logging.Errorf("Error in %s %s: %v; retrying in %v", desc, instance, err, sleep) - time.Sleep(sleep) - // Create timestamp 30 seconds before now for stale read requests - t = time.Now().UTC().Add(-30 * time.Second) - } - return err -} - -func refreshToken(ts oauth2.TokenSource, tok *oauth2.Token) (*oauth2.Token, error) { - expiredToken := &oauth2.Token{ - AccessToken: tok.AccessToken, - TokenType: tok.TokenType, - RefreshToken: tok.RefreshToken, - Expiry: time.Time{}.Add(1), // Expired - } - return oauth2.ReuseTokenSource(expiredToken, ts).Token() -} - -// Local returns a certificate that may be used to establish a TLS -// connection to the specified instance. -func (s *RemoteCertSource) Local(instance string) (tls.Certificate, error) { - pkix, err := x509.MarshalPKIXPublicKey(s.generateKey().Public()) - if err != nil { - return tls.Certificate{}, err - } - - p, r, n := util.SplitName(instance) - regionName := fmt.Sprintf("%s~%s", r, n) - pubKey := string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"})) - generateEphemeralCertRequest := &sqladmin.GenerateEphemeralCertRequest{ - PublicKey: pubKey, - } - var tok *oauth2.Token - // If IAM login is enabled, add the OAuth2 token into the ephemeral - // certificate request. - if s.EnableIAMLogin { - var tokErr error - tok, tokErr = s.TokenSource.Token() - if tokErr != nil { - return tls.Certificate{}, tokErr - } - // Always refresh the token to ensure its expiration is far enough in - // the future. - tok, tokErr = refreshToken(s.TokenSource, tok) - if tokErr != nil { - return tls.Certificate{}, tokErr - } - generateEphemeralCertRequest.AccessToken = tok.AccessToken - } - req := s.serv.Connect.GenerateEphemeralCert(p, regionName, generateEphemeralCertRequest) - - var data *sqladmin.GenerateEphemeralCertResponse - err = backoffAPIRetry("generateEphemeral for", instance, func(staleRead time.Time) error { - if !staleRead.IsZero() { - generateEphemeralCertRequest.ReadTime = staleRead.Format(time.RFC3339) - } - data, err = req.Do() - return err - }) - if err != nil { - return tls.Certificate{}, err - } - - c, err := parseCert(data.EphemeralCert.Cert) - if err != nil { - return tls.Certificate{}, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err) - } - - if s.EnableIAMLogin { - // Adjust the certificate's expiration to be the earlier of tok.Expiry or c.NotAfter - if tok.Expiry.Before(c.NotAfter) { - c.NotAfter = tok.Expiry - } - } - return tls.Certificate{ - Certificate: [][]byte{c.Raw}, - PrivateKey: s.generateKey(), - Leaf: c, - }, nil -} - -func parseCert(pemCert string) (*x509.Certificate, error) { - bl, _ := pem.Decode([]byte(pemCert)) - if bl == nil { - return nil, errors.New("invalid PEM: " + pemCert) - } - return x509.ParseCertificate(bl.Bytes) -} - -// Return the RSA private key, which is lazily initialized. -func (s *RemoteCertSource) generateKey() *rsa.PrivateKey { - s.keyOnce.Do(func() { - start := time.Now() - pkey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) // very unexpected. - } - logging.Verbosef("Generated RSA key in %v", time.Since(start)) - s.key = pkey - }) - return s.key -} - -// Find the first matching IP address by user input IP address types -func (s *RemoteCertSource) findIPAddr(data *sqladmin.ConnectSettings, instance string) (ipAddrInUse string, err error) { - for _, eachIPAddrTypeByUser := range s.IPAddrTypes { - for _, eachIPAddrTypeOfInstance := range data.IpAddresses { - if strings.ToUpper(eachIPAddrTypeOfInstance.Type) == strings.ToUpper(eachIPAddrTypeByUser) { - ipAddrInUse = eachIPAddrTypeOfInstance.IpAddress - return ipAddrInUse, nil - } - } - } - - ipAddrTypesOfInstance := "" - for _, eachIPAddrTypeOfInstance := range data.IpAddresses { - ipAddrTypesOfInstance += fmt.Sprintf("(TYPE=%v, IP_ADDR=%v)", eachIPAddrTypeOfInstance.Type, eachIPAddrTypeOfInstance.IpAddress) - } - - ipAddrTypeOfUser := fmt.Sprintf("%v", s.IPAddrTypes) - - return "", fmt.Errorf("User input IP address type %v does not match the instance %v, the instance's IP addresses are %v ", ipAddrTypeOfUser, instance, ipAddrTypesOfInstance) -} - -// Remote returns the specified instance's CA certificate, address, and name. -func (s *RemoteCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { - p, region, n := util.SplitName(instance) - regionName := fmt.Sprintf("%s~%s", region, n) - req := s.serv.Connect.Get(p, regionName) - - var data *sqladmin.ConnectSettings - err = backoffAPIRetry("get instance", instance, func(staleRead time.Time) error { - if !staleRead.IsZero() { - req.ReadTime(staleRead.Format(time.RFC3339)) - } - data, err = req.Do() - return err - }) - if err != nil { - return nil, "", "", "", err - } - - // TODO(chowski): remove this when us-central is removed. - if data.Region == "us-central" { - data.Region = "us-central1" - } - if data.Region != region { - if region == "" { - err = fmt.Errorf("instance %v doesn't provide region", instance) - } else { - err = fmt.Errorf(`for connection string "%s": got region %q, want %q`, instance, region, data.Region) - } - if s.checkRegion { - return nil, "", "", "", err - } - logging.Errorf("%v", err) - logging.Errorf("WARNING: specifying the correct region in an instance string will become required in a future version!") - } - - if len(data.IpAddresses) == 0 { - return nil, "", "", "", fmt.Errorf("no IP address found for %v", instance) - } - if data.BackendType == "FIRST_GEN" { - logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") - return nil, "", "", "", fmt.Errorf("%q is a first generation instance", instance) - } - - // Find the first matching IP address by user input IP address types - ipAddrInUse := "" - ipAddrInUse, err = s.findIPAddr(data, instance) - if err != nil { - return nil, "", "", "", err - } - - c, err := parseCert(data.ServerCaCert.Cert) - - return c, ipAddrInUse, p + ":" + n, data.DatabaseVersion, err -} diff --git a/proxy/certs/certs_test.go b/proxy/certs/certs_test.go deleted file mode 100644 index 7f1f780647..0000000000 --- a/proxy/certs/certs_test.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package certs - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "google.golang.org/api/option" - sqladmin "google.golang.org/api/sqladmin/v1beta4" -) - -const fakeCert = `-----BEGIN CERTIFICATE----- -MIICgTCCAWmgAwIBAgIBADANBgkqhkiG9w0BAQsFADAAMCIYDzAwMDEwMTAxMDAw -MDAwWhgPMDAwMTAxMDEwMDAwMDBaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQCvN0H6/ecloIfNyRu8KKtVSIK0JaW1lB1C1/ZI9iZmihqiUrxeyKTb -9hWuMPJ3u9NfSn1Vlwuj0bw7/T8e3Ol5BImcGxYxWMefkqFtqnjCafo2wnIea/eQ -JFLt4wXYkeveHReUseGtaBzpCo4wYOiqgxyIrGiQ/rq4Xjr2hXuqTg4TTgxv+0Iv -nrJwn61pitGvLPjsl9quzSQ6CdM3tWfb6cwozF5uJatbxRCZDsp1qUBXX9/zYqmx -8regdRG95btNgXLCfNS0iX0jopl00vGwYRGGKjfPZ5AkpuxX9M4Ys3X7pOspaQMC -Zf4VjXdwOljqZxIOGhOBbrXQacSywTLjAgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUA -A4IBAQAXj/0iiU2AQGztlFstLVwQ9yz+7/pfqAr26DYu9hpI/QvrZsJWjwNUNlX+ -7gwhrwiJs7xsLZqnEr2qvj6at/MtxIEVgQd43sOsWW9de8R5WNQNzsCb+5npWcx7 -vtcKXD9jFFLDDCIYjAf9+6m/QrMJtIf++zBmjguShccjZzY+GQih78oWqNTYqRQs -//wOP15vFQ/gB4DcJ0UyO9icVgbJha66yzG7XABDEepha5uhpLhwFaONU8jMxW7A -fOx52xqIUu3m4M3Ci0ZIp22TeGVuJ/Dy1CPbDOshcb0dXTE+mU5T91SHKRF4jz77 -+9TQIXHGk7lJyVVhbed8xm/p727f ------END CERTIFICATE-----` - -func TestLocalCertSupportsStaleReads(t *testing.T) { - var ( - gotReadTimes []string - ok bool - ) - handleEphemeralCert := func(w http.ResponseWriter, r *http.Request) { - var actual sqladmin.GenerateEphemeralCertRequest - data, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Fatalf("failed to read request body: %v", err) - } - defer r.Body.Close() - if err = json.Unmarshal(data, &actual); err != nil { - t.Fatalf("failed to unmarshal request body: %v", err) - } - gotReadTimes = append(gotReadTimes, actual.ReadTime) - if !ok { - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintln(w, `{"message":"the first request fails"}`) - ok = true - return - } - // the second request succeeds - fmt.Fprintln(w, fmt.Sprintf(`{"ephemeralCert":{"cert": %q}}`, fakeCert)) - } - ts := httptest.NewServer(http.HandlerFunc(handleEphemeralCert)) - defer ts.Close() - - cs := NewCertSourceOpts(ts.Client(), RemoteOpts{}) - // replace SQL Admin API client with client backed by test server - var err error - cs.serv, err = sqladmin.NewService(context.Background(), - option.WithEndpoint(ts.URL), option.WithHTTPClient(ts.Client())) - if err != nil { - t.Fatalf("failed to replace SQL Admin client: %v", err) - } - - // Send request to generate a cert - _, err = cs.Local("my-proj:reg:my-inst") - if err != nil { - t.Fatal(err) - } - - // Verify read time is not present for first request - // and is 30 seconds before "now" for second request - if len(gotReadTimes) != 2 { - t.Fatalf("expected two results, got = %v", len(gotReadTimes)) - } - if gotReadTimes[0] != "" { - t.Fatalf("expected empty ReadTime for first request, got = %v", gotReadTimes[0]) - } - wantStaleness := 30 * time.Second - if !staleTimestamp(gotReadTimes[1], wantStaleness) { - t.Fatalf("expected timestamp at least %v old, got = %v (now = %v)", - wantStaleness, gotReadTimes[1], time.Now().UTC().Format(time.RFC3339)) - } -} - -func staleTimestamp(ts string, staleness time.Duration) bool { - t, err := time.Parse(time.RFC3339, ts) - if err != nil { - // ts was not in expected format, fail - return false - } - return t.Before(time.Now().Add(-staleness)) -} - -func TestRemoteCertSupportsStaleReads(t *testing.T) { - var ( - gotReadTimes []string - ok bool - ) - handleConnectSettings := func(w http.ResponseWriter, r *http.Request) { - rt := r.URL.Query()["readTime"] - // if the URL parameter isn't nil, record its value; otherwise add an - // empty string to indicate no query param was set - if rt != nil { - gotReadTimes = append(gotReadTimes, rt[0]) - } else { - gotReadTimes = append(gotReadTimes, "") - } - if !ok { - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintln(w, `{"message":"the first request fails"}`) - ok = true - return - } - fmt.Fprintln(w, fmt.Sprintf(`{ - "region":"us-central1", - "ipAddresses": [ - {"type":"PRIMARY", "ipAddress":"127.0.0.1"} - ], - "serverCaCert": {"cert": %q} - }`, fakeCert)) - } - ts := httptest.NewServer(http.HandlerFunc(handleConnectSettings)) - defer ts.Close() - - cs := NewCertSourceOpts(ts.Client(), RemoteOpts{}) - var err error - // replace SQL Admin API client with client backed by test server - cs.serv, err = sqladmin.NewService(context.Background(), - option.WithEndpoint(ts.URL), option.WithHTTPClient(ts.Client())) - if err != nil { - t.Fatalf("failed to replace SQL Admin client: %v", err) - } - - // Send request to retrieve instance metadata - _, _, _, _, err = cs.Remote("my-proj:us-central1:my-inst") - if err != nil { - t.Fatal(err) - } - - // Verify read time is not present for first request - // and is 30 seconds before "now" for second request - if len(gotReadTimes) != 2 { - t.Fatalf("expected two results, got = %v", len(gotReadTimes)) - } - if gotReadTimes[0] != "" { - t.Fatalf("expected empty ReadTime for first request, got = %v", gotReadTimes[0]) - } - wantStaleness := 30 * time.Second - if !staleTimestamp(gotReadTimes[1], wantStaleness) { - t.Fatalf("expected timestamp at least %v old, got = %v (now = %v)", - wantStaleness, gotReadTimes[1], time.Now().UTC().Format(time.RFC3339)) - } -} diff --git a/proxy/dialers/mysql/hook.go b/proxy/dialers/mysql/hook.go deleted file mode 100644 index 7bee55d5b7..0000000000 --- a/proxy/dialers/mysql/hook.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package mysql adds a 'cloudsql' network to use when you want to access a -// Cloud SQL Database via the mysql driver found at -// github.com/go-sql-driver/mysql. It also exposes helper functions for -// dialing. -package mysql - -import ( - "database/sql" - "errors" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" - "github.com/go-sql-driver/mysql" -) - -func init() { - mysql.RegisterDialContext("cloudsql", proxy.DialContext) -} - -// Dial logs into the specified Cloud SQL Instance using the given user and no -// password. To set more options, consider calling DialCfg instead. -// -// The provided instance should be in the form project-name:region:instance-name. -// -// The returned *sql.DB may be valid even if there's also an error returned -// (e.g. if there was a transient connection error). -func Dial(instance, user string) (*sql.DB, error) { - cfg := mysql.NewConfig() - cfg.User = user - cfg.Addr = instance - return DialCfg(cfg) -} - -// DialPassword is similar to Dial, but allows you to specify a password. -// -// Note that using a password with the proxy is not necessary as long as the -// user's hostname in the mysql.user table is 'cloudsqlproxy~'. For more -// information, see: -// https://cloud.google.com/sql/docs/sql-proxy#user -func DialPassword(instance, user, password string) (*sql.DB, error) { - cfg := mysql.NewConfig() - cfg.User = user - cfg.Passwd = password - cfg.Addr = instance - return DialCfg(cfg) -} - -// Cfg returns the effective *mysql.Config to represent connectivity to the -// provided instance via the given user and password. The config can be -// modified and passed to DialCfg to connect. If you don't modify the returned -// config before dialing, consider using Dial or DialPassword. -func Cfg(instance, user, password string) *mysql.Config { - cfg := mysql.NewConfig() - cfg.User = user - cfg.Passwd = password - cfg.Addr = instance - cfg.Net = "cloudsql" - return cfg -} - -// DialCfg opens up a SQL connection to a Cloud SQL Instance specified by the -// provided configuration. It is otherwise the same as Dial. -// -// The cfg.Addr should be the instance's connection string, in the format of: -// project-name:region:instance-name. -func DialCfg(cfg *mysql.Config) (*sql.DB, error) { - if cfg.TLSConfig != "" { - return nil, errors.New("do not specify TLS when using the Proxy") - } - - // Copy the config so that we can modify it without feeling bad. - c := *cfg - c.Net = "cloudsql" - dsn := c.FormatDSN() - - db, err := sql.Open("mysql", dsn) - if err == nil { - err = db.Ping() - } - return db, err -} diff --git a/proxy/dialers/mysql/hook_test.go b/proxy/dialers/mysql/hook_test.go deleted file mode 100644 index a2c8df3201..0000000000 --- a/proxy/dialers/mysql/hook_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package mysql_test - -import ( - "fmt" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/mysql" -) - -// ExampleCfg shows how to use Cloud SQL Auth proxy dialer if you must update some -// settings normally passed in the DSN such as the DBName or timeouts. -func ExampleCfg() { - cfg := mysql.Cfg("project:region:instance-name", "user", "") - cfg.DBName = "DB_1" - cfg.ParseTime = true - - const timeout = 10 * time.Second - cfg.Timeout = timeout - cfg.ReadTimeout = timeout - cfg.WriteTimeout = timeout - - db, err := mysql.DialCfg(cfg) - if err != nil { - panic("couldn't dial: " + err.Error()) - } - // Close db after this method exits since we don't need it for the - // connection pooling. - defer db.Close() - - var now time.Time - fmt.Println(db.QueryRow("SELECT NOW()").Scan(&now)) - fmt.Println(now) -} diff --git a/proxy/dialers/postgres/hook.go b/proxy/dialers/postgres/hook.go deleted file mode 100644 index 678bef0fe4..0000000000 --- a/proxy/dialers/postgres/hook.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2017 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package postgres adds a 'cloudsqlpostgres' driver to use when you want -// to access a Cloud SQL Database via the go database/sql library. -// It is a wrapper over the driver found at github.com/lib/pq. -// To use this driver, you can look at an example in -// postgres_test package in the hook_test.go file -package postgres - -import ( - "database/sql" - "database/sql/driver" - "fmt" - "net" - "regexp" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" - "github.com/lib/pq" -) - -func init() { - sql.Register("cloudsqlpostgres", &Driver{}) -} - -type Driver struct{} - -type dialer struct{} - -// instanceRegexp is used to parse the addr returned by lib/pq. -// lib/pq returns the format '[project:region:instance]:port' -var instanceRegexp = regexp.MustCompile(`^\[(.+)\]:[0-9]+$`) - -func (d dialer) Dial(ntw, addr string) (net.Conn, error) { - matches := instanceRegexp.FindStringSubmatch(addr) - if len(matches) != 2 { - return nil, fmt.Errorf("failed to parse addr: %q. It should conform to the regular expression %q", addr, instanceRegexp) - } - instance := matches[1] - return proxy.Dial(instance) -} - -func (d dialer) DialTimeout(ntw, addr string, timeout time.Duration) (net.Conn, error) { - return nil, fmt.Errorf("timeout is not currently supported for cloudsqlpostgres dialer") -} - -func (d *Driver) Open(name string) (driver.Conn, error) { - return pq.DialOpen(dialer{}, name) -} diff --git a/proxy/dialers/postgres/hook_test.go b/proxy/dialers/postgres/hook_test.go deleted file mode 100644 index cfead5bbcc..0000000000 --- a/proxy/dialers/postgres/hook_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2017 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// Package postgres_test contains an example on how to use cloudsqlpostgres dialer -package postgres_test - -import ( - "database/sql" - "fmt" - "log" - "time" - - _ "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/postgres" -) - -// Example shows how to use cloudsqlpostgres dialer -func Example() { - // Note that sslmode=disable is required it does not mean that the connection - // is unencrypted. All connections via the proxy are completely encrypted. - db, err := sql.Open("cloudsqlpostgres", "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable") - if err != nil { - log.Fatal(err) - } - defer db.Close() - var now time.Time - fmt.Println(db.QueryRow("SELECT NOW()").Scan(&now)) - fmt.Println(now) -} diff --git a/proxy/fuse/fuse.go b/proxy/fuse/fuse.go deleted file mode 100644 index ec3b8178c9..0000000000 --- a/proxy/fuse/fuse.go +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !windows && !openbsd -// +build !windows,!openbsd - -// Package fuse provides a connection source wherein the user does not need to -// specify which instance they are connecting to before they start the -// executable. Instead, simply attempting to access a file in the provided -// directory will transparently create a proxied connection to an instance -// which has that name. -// -// Specifically, given that NewConnSrc was called with the mounting directory -// as /cloudsql: -// -// 1) Execute `mysql -S /cloudsql/speckle:instance` -// 2) The 'mysql' executable looks up the file "speckle:instance" inside "/cloudsql" -// 3) This lookup is intercepted by the code in this package. A local unix socket -// located in a temporary directory is opened for listening and the lookup for -// "speckle:instance" returns to mysql saying that it is a symbolic link -// pointing to this new local socket. -// 4) mysql dials the local unix socket, creating a new connection to the -// specified instance. -package fuse - -import ( - "bytes" - "errors" - "fmt" - "io" - "net" - "os" - "path/filepath" - "strings" - "sync" - "syscall" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - "golang.org/x/net/context" -) - -// NewConnSrc returns a source of new connections based on Lookups in the -// provided mount directory. If there isn't a directory located at tmpdir one -// is created. The second return parameter can be used to shutdown and release -// any resources. As a result of this shutdown, or during any other fatal -// error, the returned chan will be closed. -// -// The connset parameter is optional. -func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { - if err := os.MkdirAll(tmpdir, 0777); err != nil { - return nil, nil, err - } - if connset == nil { - // Make a dummy one. - connset = proxy.NewConnSet() - } - conns := make(chan proxy.Conn, 1) - root := &fsRoot{ - tmpDir: tmpdir, - linkDir: mountdir, - dst: conns, - links: make(map[string]*symlink), - connset: connset, - client: client, - } - - srv, err := fs.Mount(mountdir, root, &fs.Options{ - MountOptions: fuse.MountOptions{AllowOther: true}, - }) - if err != nil { - return nil, nil, fmt.Errorf("FUSE mount failed: %q: %v", mountdir, err) - } - - closer := fuseCloser(func() error { - err := srv.Unmount() // Best effort unmount - if err != nil { - logging.Errorf("Unmount failed: %v", err) - } - return root.Close() - }) - return conns, closer, nil -} - -type fuseCloser func() error - -func (fc fuseCloser) Close() error { - return fc() -} - -// symlink implements a symbolic link, returning the underlying path when -// Readlink is called. -type symlink struct { - fs.Inode - path string -} - -var _ fs.NodeReadlinker = &symlink{} - -func (s *symlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) { - return []byte(s.path), fs.OK -} - -// fsRoot provides the in-memory file system that supports lazy connections to -// Cloud SQL instances. -type fsRoot struct { - fs.Inode - - // tmpDir defines a temporary directory where all the sockets are placed - // faciliating connections to Cloud SQL instances. - tmpDir string - // linkDir is the directory that holds symbolic links to the tmp dir for - // each Cloud SQL instance connection. After shutdown, this directory is - // cleaned out. - linkDir string - - client *proxy.Client - connset *proxy.ConnSet - - // sockLock protects fields in this struct related to sockets; specifically - // 'links' and 'closers'. - sockLock sync.Mutex - links map[string]*symlink - // closers includes a reference to all open Unix socket listeners. When - // fs.Close is called, all of these listeners are also closed. - closers []io.Closer - - sync.RWMutex - dst chan<- proxy.Conn -} - -var _ interface { - fs.InodeEmbedder - fs.NodeGetattrer - fs.NodeLookuper - fs.NodeReaddirer -} = &fsRoot{} - -func (r *fsRoot) newConn(instance string, c net.Conn) { - r.RLock() - // dst will be nil if Close has been called already. - if ch := r.dst; ch != nil { - ch <- proxy.Conn{Instance: instance, Conn: c} - } else { - logging.Errorf("Ignored new conn request to %q: system has been closed", instance) - } - r.RUnlock() -} - -// Close shuts down the fsRoot filesystem and closes all open Unix socket -// listeners. -func (r *fsRoot) Close() error { - r.Lock() - if r.dst != nil { - // Since newConn only sends on dst while holding a reader lock, holding the - // writer lock is sufficient to ensure there are no pending sends on the - // channel when it is closed. - close(r.dst) - // Setting it to nil prevents further sends. - r.dst = nil - } - r.Unlock() - - var errs bytes.Buffer - r.sockLock.Lock() - for _, c := range r.closers { - if err := c.Close(); err != nil { - fmt.Fprintln(&errs, err) - } - } - r.sockLock.Unlock() - - if errs.Len() == 0 { - return nil - } - logging.Errorf("Close %q: %v", r.linkDir, errs.String()) - return errors.New(errs.String()) -} - -// Getattr implements fs.NodeGetattrer and represents fsRoot as a directory. -func (r *fsRoot) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - *out = fuse.AttrOut{Attr: fuse.Attr{ - Mode: 0555 | fuse.S_IFDIR, - }} - return fs.OK -} - -// Lookup implements fs.NodeLookuper and handles all requests, either for the -// README, or for a new connection to a Cloud SQL instance. When receiving a -// request for a Cloud SQL instance, Lookup will return a symlink to a Unix -// socket that provides connectivity to a remote instance. -func (r *fsRoot) Lookup(ctx context.Context, instance string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - if instance == "README" { - return r.NewInode(ctx, &readme{}, fs.StableAttr{}), fs.OK - } - r.sockLock.Lock() - defer r.sockLock.Unlock() - - if _, _, _, _, err := proxy.ParseInstanceConnectionName(instance); err != nil { - return nil, syscall.ENOENT - } - - if ret, ok := r.links[instance]; ok { - return ret.EmbeddedInode(), fs.OK - } - - // path is the location of the Unix socket - path := filepath.Join(r.tmpDir, instance) - os.RemoveAll(path) // Best effort; the following will fail if this does. - // linkpath is the location the symlink points to - linkpath := path - - // Add a ".s.PGSQL.5432" suffix to path for Postgres instances - if r.client != nil { - version, err := r.client.InstanceVersionContext(ctx, instance) - if err != nil { - logging.Errorf("Failed to get Instance version for %s: %v", instance, err) - return nil, syscall.ENOENT - } - if strings.HasPrefix(strings.ToLower(version), "postgres") { - if err := os.MkdirAll(path, 0755); err != nil { - logging.Errorf("Failed to create path %s: %v", path, err) - return nil, syscall.EIO - } - path = filepath.Join(linkpath, ".s.PGSQL.5432") - } - } - // TODO: check path length -- if it exceeds the max supported socket length, - // return an error that helps the user understand what went wrong. - // Otherwise, we get a "bind: invalid argument" error. - - sock, err := net.Listen("unix", path) - if err != nil { - logging.Errorf("couldn't listen at %q: %v", path, err) - return nil, syscall.EEXIST - } - if err := os.Chmod(path, 0777|os.ModeSocket); err != nil { - logging.Errorf("couldn't update permissions for socket file %q: %v; other users may be unable to connect", path, err) - } - - go r.listenerLifecycle(sock, instance, path) - - ret := &symlink{path: linkpath} - inode := r.NewInode(ctx, ret, fs.StableAttr{Mode: 0777 | fuse.S_IFLNK}) - r.links[instance] = ret - // TODO(chowski): memory leak when listeners exit on their own via removeListener. - r.closers = append(r.closers, sock) - - return inode, fs.OK -} - -// removeListener marks that a Listener for an instance has exited and is no -// longer serving new connections. -func (r *fsRoot) removeListener(instance, path string) { - r.sockLock.Lock() - defer r.sockLock.Unlock() - v, ok := r.links[instance] - if ok && v.path == path { - delete(r.links, instance) - } else { - logging.Errorf("Removing a listener for %q at %q which was already replaced", instance, path) - } -} - -// listenerLifecycle calls l.Accept in a loop, and for each new connection -// r.newConn is called. After the Listener returns an error it is removed. -func (r *fsRoot) listenerLifecycle(l net.Listener, instance, path string) { - for { - start := time.Now() - c, err := l.Accept() - if err != nil { - logging.Errorf("error in Accept for %q: %v", instance, err) - if nerr, ok := err.(net.Error); ok && nerr.Temporary() { - d := 10*time.Millisecond - time.Since(start) - if d > 0 { - time.Sleep(d) - } - continue - } - break - } - r.newConn(instance, c) - } - r.removeListener(instance, path) - l.Close() - if err := os.Remove(path); err != nil { - logging.Errorf("couldn't remove %q: %v", path, err) - } -} - -// Readdir implements fs.NodeReaddirer and returns a list of files for each -// instance to which the proxy is actively connected. In addition, the list -// includes a README. -func (r *fsRoot) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - activeConns := r.connset.IDs() - entries := []fuse.DirEntry{ - {Name: "README", Mode: 0555 | fuse.S_IFREG}, - } - for _, conn := range activeConns { - entries = append(entries, fuse.DirEntry{ - Name: conn, - Mode: 0777 | syscall.S_IFSOCK, - }) - } - ds := fs.NewListDirStream(entries) - return ds, fs.OK -} - -// readme represents a static read-only text file. -type readme struct { - fs.Inode -} - -var _ interface { - fs.InodeEmbedder - fs.NodeGetattrer - fs.NodeReader - fs.NodeOpener -} = &readme{} - -const readmeText = ` -When programs attempt to open files in this directory, a remote connection to -the Cloud SQL instance of the same name will be established. - -That is, running: - - mysql -u root -S "/path/to/this/directory/project:region:instance-2" - -or- - psql "host=/path/to/this/directory/project:region:instance-2 dbname=mydb user=myuser" - -will open a new connection to the specified instance, given you have the correct -permissions. - -Listing the contents of this directory will show all instances with active -connections. -` - -// Getattr implements fs.NodeGetattrer and indicates that this file is a regular -// file. -func (*readme) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - *out = fuse.AttrOut{Attr: fuse.Attr{ - Mode: 0444 | syscall.S_IFREG, - Size: uint64(len(readmeText)), - }} - return fs.OK -} - -// Read implements fs.NodeReader and supports incremental reads. -func (*readme) Read(ctx context.Context, f fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { - end := int(off) + len(dest) - if end > len(readmeText) { - end = len(readmeText) - } - return fuse.ReadResultData([]byte(readmeText[off:end])), fs.OK -} - -// Open implements fs.NodeOpener and supports opening the README as a read-only -// file. -func (*readme) Open(ctx context.Context, mode uint32) (fs.FileHandle, uint32, syscall.Errno) { - df := nodefs.NewDataFile([]byte(readmeText)) - rf := nodefs.NewReadOnlyFile(df) - return rf, 0, fs.OK -} diff --git a/proxy/fuse/fuse_darwin.go b/proxy/fuse/fuse_darwin.go deleted file mode 100644 index 5adb84482e..0000000000 --- a/proxy/fuse/fuse_darwin.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fuse - -import ( - "os" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" -) - -const ( - macfusePath = "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse" - osxfusePath = "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse" -) - -// Supported checks if macfuse or osxfuse are installed on the host by looking -// for both in their known installation location. -func Supported() bool { - // This code follows the same strategy as hanwen/go-fuse. - // See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_darwin.go#L121-L124. - - // check for macfuse first (newer version of osxfuse) - if _, err := os.Stat(macfusePath); err != nil { - // if that fails, check for osxfuse next - if _, err := os.Stat(osxfusePath); err != nil { - logging.Errorf("Failed to find osxfuse or macfuse. Verify FUSE installation and try again (see https://osxfuse.github.io).") - return false - } - } - return true -} diff --git a/proxy/fuse/fuse_linux.go b/proxy/fuse/fuse_linux.go deleted file mode 100644 index 45f14b885c..0000000000 --- a/proxy/fuse/fuse_linux.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fuse - -import ( - "os/exec" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" -) - -// Supported returns true if the current system supports FUSE. -func Supported() bool { - // This code follows the same strategy found in hanwen/go-fuse. - // See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_linux.go#L184-L198. - if _, err := exec.LookPath("fusermount"); err != nil { - if _, err := exec.LookPath("/bin/fusermount"); err != nil { - logging.Errorf("Failed to find fusermount binary in PATH or /bin. Verify FUSE installation and try again.") - return false - } - } - return true -} diff --git a/proxy/fuse/fuse_linux_test.go b/proxy/fuse/fuse_linux_test.go deleted file mode 100644 index 928442a5ec..0000000000 --- a/proxy/fuse/fuse_linux_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build linux -// +build linux - -package fuse_test - -import ( - "os" - "testing" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/fuse" -) - -func TestFUSESupport(t *testing.T) { - if testing.Short() { - t.Skip("skipping fuse tests in short mode.") - } - - removePath := func() func() { - original := os.Getenv("PATH") - os.Unsetenv("PATH") - return func() { os.Setenv("PATH", original) } - } - if !fuse.Supported() { - t.Fatal("expected FUSE to be supported") - } - cleanup := removePath() - defer cleanup() - - if !fuse.Supported() { - t.Fatal("expected FUSE to be supported") - } - -} diff --git a/proxy/fuse/fuse_openbsd.go b/proxy/fuse/fuse_openbsd.go deleted file mode 100644 index 61908d4a64..0000000000 --- a/proxy/fuse/fuse_openbsd.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package fuse is a package stub for openbsd, which isn't supported by our -// fuse library. -package fuse - -import ( - "errors" - "io" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" -) - -func Supported() bool { - return false -} - -func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { - return nil, nil, errors.New("fuse not supported on openbsd") -} diff --git a/proxy/fuse/fuse_test.go b/proxy/fuse/fuse_test.go deleted file mode 100644 index 3bd3a21783..0000000000 --- a/proxy/fuse/fuse_test.go +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !windows -// +build !windows - -package fuse - -import ( - "bytes" - "io" - "io/ioutil" - "net" - "os" - "path/filepath" - "sync" - "syscall" - "testing" - "time" -) - -func randTmpDir(t interface { - Fatalf(format string, args ...interface{}) -}) string { - name, err := ioutil.TempDir("", "*") - if err != nil { - t.Fatalf("failed to create tmp dir: %v", err) - } - return name -} - -// tryFunc executes the provided function up to maxCount times, sleeping 100ms -// between attempts. -func tryFunc(f func() error, maxCount int) error { - var errCount int - for { - err := f() - if err == nil { - return nil - } - errCount++ - if errCount == maxCount { - return err - } - time.Sleep(100 * time.Millisecond) - } -} - -func TestFuseClose(t *testing.T) { - if testing.Short() { - t.Skip("skipping fuse tests in short mode.") - } - - dir := randTmpDir(t) - tmpdir := randTmpDir(t) - src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) - if err != nil { - t.Fatal(err) - } - - if err := tryFunc(fuse.Close, 10); err != nil { - t.Fatal(err) - } - if got, ok := <-src; ok { - t.Fatalf("got new connection %#v, expected closed source", got) - } -} - -// TestBadDir verifies that the fuse module does not create directories, only simple files. -func TestBadDir(t *testing.T) { - if testing.Short() { - t.Skip("skipping fuse tests in short mode.") - } - - dir := randTmpDir(t) - tmpdir := randTmpDir(t) - _, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := tryFunc(fuse.Close, 10); err != nil { - t.Fatal(err) - } - }() - - _, err = os.Stat(filepath.Join(dir, "proj:region:inst-1", "proj:region:inst-2")) - if err == nil { - t.Fatal("able to find a directory inside the mount point, expected only regular files") - } - if err := err.(*os.PathError); err.Err != syscall.ENOTDIR { - t.Fatalf("got %#v, want ENOTDIR (%v)", err.Err, syscall.ENOTDIR) - } -} - -func TestReadme(t *testing.T) { - if testing.Short() { - t.Skip("skipping fuse tests in short mode.") - } - - dir := randTmpDir(t) - tmpdir := randTmpDir(t) - _, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := tryFunc(fuse.Close, 10); err != nil { - t.Fatal(err) - } - }() - - data, err := ioutil.ReadFile(filepath.Join(dir, "README")) - if err != nil { - t.Fatal(err) - } - // We just care that the file exists. Print out the contents for - // informational purposes. - t.Log(string(data)) -} - -func TestSingleInstance(t *testing.T) { - if testing.Short() { - t.Skip("skipping fuse tests in short mode.") - } - - dir := randTmpDir(t) - tmpdir := randTmpDir(t) - src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := tryFunc(fuse.Close, 10); err != nil { - t.Fatal(err) - } - }() - - const want = "test:instance:string" - path := filepath.Join(dir, want) - - fi, err := os.Stat(path) - if err != nil { - t.Fatal(err) - } - - if fi.Mode()&os.ModeType != os.ModeSocket { - t.Fatalf("%q had mode %v (%X), expected a socket file", path, fi.Mode(), uint32(fi.Mode())) - } - - c, err := net.Dial("unix", path) - if err != nil { - t.Fatal(err) - } - defer c.Close() - - got, ok := <-src - if !ok { - t.Fatal("connection source was closed, expected a connection") - } else if got.Instance != want { - t.Fatalf("got %q, want %q", got.Instance, want) - } else if got.Conn == nil { - t.Fatal("got nil connection, wanted a connection") - } - - const sent = "test string" - go func() { - if _, err := c.Write([]byte(sent)); err != nil { - t.Error(err) - } - if err := c.Close(); err != nil { - t.Error(err) - } - }() - - gotData := new(bytes.Buffer) - if _, err := io.Copy(gotData, got.Conn); err != nil { - t.Fatal(err) - } else if gotData.String() != sent { - t.Fatalf("got %q, want %v", gotData.String(), sent) - } -} - -func BenchmarkNewConnection(b *testing.B) { - if testing.Short() { - b.Skip("skipping fuse tests in short mode.") - } - - dir := randTmpDir(b) - tmpdir := randTmpDir(b) - src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) - if err != nil { - b.Fatal(err) - } - - const want = "X" - incomingCount := 0 - var incoming sync.Mutex // Is unlocked when the following goroutine exits. - go func() { - incoming.Lock() - defer incoming.Unlock() - - for c := range src { - c.Conn.Write([]byte(want)) - c.Conn.Close() - incomingCount++ - } - }() - - const instance = "test:instance:string" - path := filepath.Join(dir, instance) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c, err := net.Dial("unix", path) - if err != nil { - b.Errorf("couldn't dial: %v", err) - } - - data, err := ioutil.ReadAll(c) - if err != nil { - b.Errorf("got read error: %v", err) - } else if got := string(data); got != want { - b.Errorf("read %q, want %q", string(data), want) - } - } - if err := fuse.Close(); err != nil { - b.Fatal(err) - } - - // Wait for the 'incoming' goroutine to finish. - incoming.Lock() - if incomingCount != b.N { - b.Fatalf("got %d connections, want %d", incomingCount, b.N) - } -} diff --git a/proxy/fuse/fuse_windows.go b/proxy/fuse/fuse_windows.go deleted file mode 100644 index 1b4666a7b2..0000000000 --- a/proxy/fuse/fuse_windows.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package fuse is a package stub for windows, which does not support FUSE. -package fuse - -import ( - "errors" - "io" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/proxy" -) - -func Supported() bool { - return false -} - -func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { - return nil, nil, errors.New("fuse not supported on windows") -} diff --git a/proxy/limits/limits.go b/proxy/limits/limits.go deleted file mode 100644 index 02b3bb913a..0000000000 --- a/proxy/limits/limits.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !windows && !freebsd -// +build !windows,!freebsd - -// Package limits provides routines to check and enforce certain resource -// limits on the Cloud SQL client proxy process. -package limits - -import ( - "fmt" - "syscall" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" -) - -var ( - // For overriding in unittests. - syscallGetrlimit = syscall.Getrlimit - syscallSetrlimit = syscall.Setrlimit -) - -// Each connection handled by the proxy requires two file descriptors, one -// for the local end of the connection and one for the remote. So, the proxy -// process should be able to open at least 8K file descriptors if it is to -// handle 4K connections to one instance. -const ExpectedFDs = 8500 - -// SetupFDLimits ensures that the process running the Cloud SQL proxy can have -// at least wantFDs number of open file descriptors. It returns an error if it -// cannot ensure the same. -func SetupFDLimits(wantFDs uint64) error { - rlim := &syscall.Rlimit{} - if err := syscallGetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { - return fmt.Errorf("failed to read rlimit for max file descriptors: %v", err) - } - - if rlim.Cur >= wantFDs { - logging.Verbosef("current FDs rlimit set to %d, wanted limit is %d. Nothing to do here.", rlim.Cur, wantFDs) - return nil - } - - // Linux man page: - // The soft limit is the value that the kernel enforces for the corre‐ - // sponding resource. The hard limit acts as a ceiling for the soft limit: - // an unprivileged process may set only its soft limit to a value in the - // range from 0 up to the hard limit, and (irreversibly) lower its hard - // limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE - // capability in the initial user namespace) may make arbitrary changes to - // either limit value. - if rlim.Max < wantFDs { - // When the hard limit is less than what is requested, let's just give it a - // shot, and if we fail, we fallback and try just setting the softlimit. - rlim2 := &syscall.Rlimit{} - rlim2.Max = wantFDs - rlim2.Cur = wantFDs - if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim2); err == nil { - logging.Verbosef("Rlimits for file descriptors set to {Current = %v, Max = %v}", rlim2.Cur, rlim2.Max) - return nil - } - } - - rlim.Cur = wantFDs - if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { - return fmt.Errorf( - `failed to set rlimit {Current = %v, Max = %v} for max file -descriptors. The hard limit on file descriptors (4096) is lower than the -requested rlimit. The proxy will only be able to handle ~2048 -connections. To hide this message, please request a limit within the available range.`, - rlim.Cur, - rlim.Max, - ) - } - - logging.Verbosef("Rlimits for file descriptors set to {Current = %v, Max = %v}", rlim.Cur, rlim.Max) - return nil -} diff --git a/proxy/limits/limits_freebsd.go b/proxy/limits/limits_freebsd.go deleted file mode 100644 index 485b6a7ae9..0000000000 --- a/proxy/limits/limits_freebsd.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build freebsd -// +build freebsd - -// Package limits provides routines to check and enforce certain resource -// limits on the Cloud SQL client proxy process. -package limits - -import ( - "fmt" - "syscall" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" -) - -var ( - // For overriding in unittests. - syscallGetrlimit = syscall.Getrlimit - syscallSetrlimit = syscall.Setrlimit -) - -// Each connection handled by the proxy requires two file descriptors, one -// for the local end of the connection and one for the remote. So, the proxy -// process should be able to open at least 8K file descriptors if it is to -// handle 4K connections to one instance. -const ExpectedFDs = 8500 - -// SetupFDLimits ensures that the process running the Cloud SQL proxy can have -// at least wantFDs number of open file descriptors. It returns an error if it -// cannot ensure the same. -func SetupFDLimits(wantFDs uint64) error { - rlim := &syscall.Rlimit{} - if err := syscallGetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { - return fmt.Errorf("failed to read rlimit for max file descriptors: %v", err) - } - - if uint64(rlim.Cur) >= wantFDs { - logging.Verbosef("current FDs rlimit set to %d, wanted limit is %d. Nothing to do here.", rlim.Cur, wantFDs) - return nil - } - - // Linux man page: - // The soft limit is the value that the kernel enforces for the corre‐ - // sponding resource. The hard limit acts as a ceiling for the soft limit: - // an unprivileged process may set only its soft limit to a value in the - // range from 0 up to the hard limit, and (irreversibly) lower its hard - // limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE - // capability in the initial user namespace) may make arbitrary changes to - // either limit value. - if uint64(rlim.Max) < wantFDs { - // When the hard limit is less than what is requested, let's just give it a - // shot, and if we fail, we fallback and try just setting the softlimit. - rlim2 := &syscall.Rlimit{} - rlim2.Max = int64(wantFDs) - rlim2.Cur = int64(wantFDs) - if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim2); err == nil { - logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim2) - return nil - } - } - - rlim.Cur = int64(wantFDs) - if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { - return fmt.Errorf("failed to set rlimit {%v} for max file descriptors: %v", rlim, err) - } - - logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim) - return nil -} diff --git a/proxy/limits/limits_test.go b/proxy/limits/limits_test.go deleted file mode 100644 index 1ae394a54b..0000000000 --- a/proxy/limits/limits_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !windows -// +build !windows - -package limits - -import ( - "errors" - "math" - "syscall" - "testing" -) - -type rlimitFunc func(int, *syscall.Rlimit) error - -func TestSetupFDLimits(t *testing.T) { - tests := []struct { - desc string - getFunc rlimitFunc - setFunc rlimitFunc - wantFDs uint64 - wantErr bool - }{ - { - desc: "Getrlimit fails", - getFunc: func(_ int, _ *syscall.Rlimit) error { - return errors.New("failed to read rlimit for max file descriptors") - }, - setFunc: func(_ int, _ *syscall.Rlimit) error { - panic("shouldn't be called") - }, - wantFDs: 0, - wantErr: true, - }, - { - desc: "Getrlimit max is less than wantFDs", - getFunc: func(_ int, rlim *syscall.Rlimit) error { - rlim.Cur = 512 - rlim.Max = 512 - return nil - }, - setFunc: func(_ int, rlim *syscall.Rlimit) error { - if rlim.Cur != 1024 || rlim.Max != 1024 { - return errors.New("setrlimit called with unexpected value") - } - return nil - }, - wantFDs: 1024, - wantErr: false, - }, - { - desc: "Getrlimit returns rlim_infinity", - getFunc: func(_ int, rlim *syscall.Rlimit) error { - rlim.Cur = math.MaxUint64 - rlim.Max = math.MaxUint64 - return nil - }, - setFunc: func(_ int, _ *syscall.Rlimit) error { - panic("shouldn't be called") - }, - wantFDs: 1024, - wantErr: false, - }, - { - desc: "Getrlimit cur is greater than wantFDs", - getFunc: func(_ int, rlim *syscall.Rlimit) error { - rlim.Cur = 512 - rlim.Max = 512 - return nil - }, - setFunc: func(_ int, _ *syscall.Rlimit) error { - panic("shouldn't be called") - }, - wantFDs: 256, - wantErr: false, - }, - { - desc: "Setrlimit fails", - getFunc: func(_ int, rlim *syscall.Rlimit) error { - rlim.Cur = 128 - rlim.Max = 512 - return nil - }, - setFunc: func(_ int, _ *syscall.Rlimit) error { - return errors.New("failed to set rlimit for max file descriptors") - }, - wantFDs: 256, - wantErr: true, - }, - { - desc: "Success", - getFunc: func(_ int, rlim *syscall.Rlimit) error { - rlim.Cur = 128 - rlim.Max = 512 - return nil - }, - setFunc: func(_ int, _ *syscall.Rlimit) error { - return nil - }, - wantFDs: 256, - wantErr: false, - }, - } - - for _, test := range tests { - oldGetFunc := syscallGetrlimit - syscallGetrlimit = test.getFunc - defer func() { - syscallGetrlimit = oldGetFunc - }() - - oldSetFunc := syscallSetrlimit - syscallSetrlimit = test.setFunc - defer func() { - syscallSetrlimit = oldSetFunc - }() - - gotErr := SetupFDLimits(test.wantFDs) - if (gotErr != nil) != test.wantErr { - t.Errorf("%s: limits.SetupFDLimits(%d) returned error %v, wantErr %v", test.desc, test.wantFDs, gotErr, test.wantErr) - } - } -} diff --git a/proxy/limits/limits_windows.go b/proxy/limits/limits_windows.go deleted file mode 100644 index 9bfab790b0..0000000000 --- a/proxy/limits/limits_windows.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package limits is a package stub for windows, and we currently don't support -// setting limits in windows. -package limits - -import "errors" - -// We don't support limit on the number of file handles in windows. -const ExpectedFDs = 0 - -func SetupFDLimits(wantFDs uint64) error { - if wantFDs != 0 { - return errors.New("setting limits on the number of file handles is not supported") - } - - return nil -} diff --git a/proxy/proxy/client.go b/proxy/proxy/client.go deleted file mode 100644 index 9c570fcc58..0000000000 --- a/proxy/proxy/client.go +++ /dev/null @@ -1,652 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package proxy - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "net" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/util" - "golang.org/x/net/proxy" - "golang.org/x/time/rate" -) - -const ( - // DefaultRefreshCfgThrottle is the time a refresh attempt must wait since - // the last attempt. - DefaultRefreshCfgThrottle = time.Minute - // IAMLoginRefreshThrottle is the time a refresh attempt must wait since the - // last attempt when using IAM login. - IAMLoginRefreshThrottle = 30 * time.Second - keepAlivePeriod = time.Minute - // DefaultRefreshCfgBuffer is the minimum amount of time for which a - // certificate must be valid to ensure the next refresh attempt has adequate - // time to complete. - DefaultRefreshCfgBuffer = 5 * time.Minute - // IAMLoginRefreshCfgBuffer is the minimum amount of time for which a - // certificate holding an Access Token must be valid. Because some token - // sources (e.g., ouath2.ComputeTokenSource) are refreshed with only ~60 - // seconds before expiration, this value must be smaller than the - // DefaultRefreshCfgBuffer. - IAMLoginRefreshCfgBuffer = 55 * time.Second -) - -var ( - // errNotCached is returned when the instance was not found in the Client's - // cache. It is an internal detail and is not actually ever returned to the - // user. - errNotCached = errors.New("instance was not found in cache") -) - -// Conn represents a connection from a client to a specific instance. -type Conn struct { - Instance string - Conn net.Conn -} - -// CertSource is how a Client obtains various certificates required for operation. -type CertSource interface { - // Local returns a certificate that can be used to authenticate with the - // provided instance. - Local(instance string) (tls.Certificate, error) - // Remote returns the instance's CA certificate, address, and name. - Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) -} - -// Client is a type to handle connecting to a Server. All fields are required -// unless otherwise specified. -type Client struct { - // ConnectionsCounter is used to enforce the optional maxConnections limit - ConnectionsCounter uint64 - - // MaxConnections is the maximum number of connections to establish - // before refusing new connections. 0 means no limit. - MaxConnections uint64 - - // Port designates which remote port should be used when connecting to - // instances. This value is defined by the server-side code, but for now it - // should always be 3307. - Port int - // Required; specifies how certificates are obtained. - Certs CertSource - // Optionally tracks connections through this client. If nil, connections - // are not tracked and will not be closed before method Run exits. - Conns *ConnSet - // ContextDialer should return a new connection to the provided address. - // It is called on each new connection to an instance. - // If left nil, Dialer will be tried first, and if that one is nil too then net.Dial will be used. - ContextDialer func(ctx context.Context, net, addr string) (net.Conn, error) - // Dialer should return a new connection to the provided address. It will be used only if ContextDialer is nil. - Dialer func(net, addr string) (net.Conn, error) - - // The cfgCache holds the most recent connection configuration keyed by - // instance. Relevant functions are refreshCfg and cachedCfg. It is - // protected by cacheL. - cfgCache map[string]cacheEntry - cacheL sync.RWMutex - // limiters holds a rate limiter keyed by instance. It is protected by - // cacheL. - limiters map[string]*rate.Limiter - - // refreshCfgL prevents multiple goroutines from contacting the Cloud SQL API at once. - refreshCfgL sync.Mutex - - // RefreshCfgThrottle is the amount of time to wait between configuration - // refreshes. If not set, it defaults to 1 minute. - // - // This is to prevent quota exhaustion in the case of client-side - // malfunction. - RefreshCfgThrottle time.Duration - - // RefreshCertBuffer is the amount of time before the configuration expires - // to attempt to refresh it. If not set, it defaults to 5 minutes. When IAM - // Login is enabled, this value should be set to IAMLoginRefreshCfgBuffer. - RefreshCfgBuffer time.Duration -} - -type cacheEntry struct { - lastRefreshed time.Time - // If err is not nil, the addr and cfg are not valid. - err error - addr string - version string - cfg *tls.Config - // done represents the status of any pending refresh operation related to this instance. - // If unset the op hasn't started, if open the op is still pending, and if closed the op has finished. - done chan struct{} -} - -// Run causes the client to start waiting for new connections to connSrc and -// proxy them to the destination instance. It blocks until connSrc is closed. -func (c *Client) Run(connSrc <-chan Conn) { - c.RunContext(context.Background(), connSrc) -} - -func (c *Client) run(ctx context.Context, connSrc <-chan Conn) { - for { - select { - case conn, ok := <-connSrc: - if !ok { - return - } - go c.handleConn(ctx, conn) - case <-ctx.Done(): - return - } - } -} - -// RunContext is like Run with an additional context.Context argument. -func (c *Client) RunContext(ctx context.Context, connSrc <-chan Conn) { - c.run(ctx, connSrc) - - if err := c.Conns.Close(); err != nil { - logging.Errorf("closing client had error: %v", err) - } -} - -func (c *Client) handleConn(ctx context.Context, conn Conn) { - active := atomic.AddUint64(&c.ConnectionsCounter, 1) - - // Deferred decrement of ConnectionsCounter upon connection closing - defer atomic.AddUint64(&c.ConnectionsCounter, ^uint64(0)) - - if c.MaxConnections > 0 && active > c.MaxConnections { - logging.Errorf("too many open connections (max %d)", c.MaxConnections) - conn.Conn.Close() - return - } - - server, err := c.DialContext(ctx, conn.Instance) - if err != nil { - logging.Errorf("couldn't connect to %q: %v", conn.Instance, err) - conn.Conn.Close() - return - } - - c.Conns.Add(conn.Instance, conn.Conn) - copyThenClose(server, conn.Conn, conn.Instance, "local connection on "+conn.Conn.LocalAddr().String()) - - if err := c.Conns.Remove(conn.Instance, conn.Conn); err != nil { - logging.Errorf("%s", err) - } -} - -// refreshCfg uses the CertSource inside the Client to find the instance's -// address as well as construct a new tls.Config to connect to the instance. -// This function should only be called from the scope of "cachedCfg", which -// controls the logic around throttling. -func (c *Client) refreshCfg(instance string) (addr string, cfg *tls.Config, version string, err error) { - c.refreshCfgL.Lock() - defer c.refreshCfgL.Unlock() - logging.Verbosef("refreshing ephemeral certificate for instance %s", instance) - - mycert, err := c.Certs.Local(instance) - if err != nil { - return "", nil, "", err - } - - scert, addr, name, version, err := c.Certs.Remote(instance) - if err != nil { - return "", nil, "", err - } - certs := x509.NewCertPool() - certs.AddCert(scert) - - cfg = &tls.Config{ - ServerName: name, - Certificates: []tls.Certificate{mycert}, - RootCAs: certs, - // We need to set InsecureSkipVerify to true due to - // https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194 - // https://tip.golang.org/doc/go1.11#crypto/x509 - // - // Since we have a secure channel to the Cloud SQL API which we use to retrieve the - // certificates, we instead need to implement our own VerifyPeerCertificate function - // that will verify that the certificate is OK. - InsecureSkipVerify: true, - VerifyPeerCertificate: genVerifyPeerCertificateFunc(name, certs), - MinVersion: tls.VersionTLS13, - } - - return fmt.Sprintf("%s:%d", addr, c.Port), cfg, version, nil -} - -// refreshCertAfter refreshes the epehemeral certificate of the instance after timeToRefresh. -func (c *Client) refreshCertAfter(instance string, timeToRefresh time.Duration) { - <-time.After(timeToRefresh) - logging.Verbosef("ephemeral certificate for instance %s will expire soon, refreshing now.", instance) - if _, _, _, err := c.cachedCfg(context.Background(), instance); err != nil { - logging.Errorf("failed to refresh the ephemeral certificate for %s before expiring: %v", instance, err) - } -} - -// genVerifyPeerCertificateFunc creates a VerifyPeerCertificate func that verifies that the peer -// certificate is in the cert pool. We need to define our own because of our sketchy non-standard -// CNs. -func genVerifyPeerCertificateFunc(instanceName string, pool *x509.CertPool) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - if len(rawCerts) == 0 { - return fmt.Errorf("no certificate to verify") - } - - cert, err := x509.ParseCertificate(rawCerts[0]) - if err != nil { - return fmt.Errorf("x509.ParseCertificate(rawCerts[0]) returned error: %v", err) - } - - opts := x509.VerifyOptions{Roots: pool} - if _, err = cert.Verify(opts); err != nil { - return err - } - - if cert.Subject.CommonName != instanceName { - return fmt.Errorf("certificate had CN %q, expected %q", cert.Subject.CommonName, instanceName) - } - return nil - } -} - -func isExpired(cfg *tls.Config) bool { - if cfg == nil { - return true - } - return time.Now().After(cfg.Certificates[0].Leaf.NotAfter) -} - -// startRefresh kicks off a refreshCfg asynchronously, that updates the cacheEntry and closes the returned channel once the refresh is completed. This function -// should only be called from the scope of "cachedCfg", which controls the logic around throttling refreshes. -func (c *Client) startRefresh(instance string, refreshCfgBuffer time.Duration) chan struct{} { - done := make(chan struct{}) - go func() { - defer close(done) - addr, cfg, ver, err := c.refreshCfg(instance) - - c.cacheL.Lock() - old := c.cfgCache[instance] - // if we failed to refresh cfg do not throw out potentially valid one - if err != nil && !isExpired(old.cfg) { - logging.Errorf("failed to refresh the ephemeral certificate for %s, returning previous cert instead: %v", instance, err) - addr, cfg, ver, err = old.addr, old.cfg, old.version, old.err - } - e := cacheEntry{ - lastRefreshed: time.Now(), - err: err, - addr: addr, - version: ver, - cfg: cfg, - done: done, - } - c.cfgCache[instance] = e - c.cacheL.Unlock() - - if !isValid(e) { - // Note: Future refreshes will not be scheduled unless another - // connection attempt is made. - logging.Errorf("failed to refresh the ephemeral certificate for %v: %v", instance, err) - return - } - - certExpiration := cfg.Certificates[0].Leaf.NotAfter - now := time.Now() - timeToRefresh := certExpiration.Sub(now) - refreshCfgBuffer - if timeToRefresh <= 0 { - // If a new certificate expires before our buffer has expired, we should wait a bit and schedule a new refresh to much closer to the expiration's date - // This situation probably only occurs when the oauth2 token isn't refreshed before the cert is, so by scheduling closer to the expiration we can hope the oauth2 token is newer. - timeToRefresh = certExpiration.Sub(now) - (5 * time.Second) - logging.Errorf("new ephemeral certificate expires sooner than expected (adjusting refresh time to compensate): current time: %v, certificate expires: %v", now, certExpiration) - } - logging.Infof("Scheduling refresh of ephemeral certificate in %s", timeToRefresh) - go c.refreshCertAfter(instance, timeToRefresh) - }() - return done -} - -// isValid returns true if the cacheEntry is still useable -func isValid(c cacheEntry) bool { - // the entry is only valid there wasn't an error retrieving it and it has a cfg - return c.err == nil && c.cfg != nil -} - -// InvalidError is an error from an instance connection that is invalid because -// its recent refresh attempt has failed, its TLS config is invalid, etc. -type InvalidError struct { - // instance is the instance connection name - instance string - // err is what makes the instance invalid - err error - // hasTLS reports whether the instance has a valid TLS config - hasTLS bool -} - -func (e *InvalidError) Error() string { - if e.hasTLS { - return e.instance + ": " + e.err.Error() - } - return e.instance + ": missing TLS config, " + e.err.Error() -} - -// InvalidInstances reports whether the existing connections have valid -// configuration. -func (c *Client) InvalidInstances() []*InvalidError { - c.cacheL.RLock() - defer c.cacheL.RUnlock() - - var invalid []*InvalidError - for instance, entry := range c.cfgCache { - var refreshInProgress bool - select { - case <-entry.done: - // refresh has already completed - default: - refreshInProgress = true - } - if !isValid(entry) && !refreshInProgress { - invalid = append(invalid, &InvalidError{ - instance: instance, - err: entry.err, - hasTLS: entry.cfg != nil, - }) - } - } - return invalid -} - -func needsRefresh(e cacheEntry, refreshCfgBuffer time.Duration) bool { - if e.done == nil { // no refresh started - return true - } - if !isValid(e) || e.cfg.Certificates[0].Leaf.NotAfter.Sub(time.Now()) <= refreshCfgBuffer { - // if the entry is invalid or close enough to expiring check - // use the entry's done channel to determine if a refresh has started yet - select { - case <-e.done: // last refresh completed, so it's time for a new one - return true - default: // new refresh already started, so we can wait on that - return false - } - } - return false -} - -func (c *Client) cachedCfg(ctx context.Context, instance string) (string, *tls.Config, string, error) { - c.cacheL.RLock() - - throttle := c.RefreshCfgThrottle - if throttle == 0 { - throttle = DefaultRefreshCfgThrottle - } - refreshCfgBuffer := c.RefreshCfgBuffer - if refreshCfgBuffer == 0 { - refreshCfgBuffer = DefaultRefreshCfgBuffer - } - - e := c.cfgCache[instance] - c.cacheL.RUnlock() - if needsRefresh(e, refreshCfgBuffer) { - // Reenter the critical section with intent to make changes - c.cacheL.Lock() - if c.cfgCache == nil { - c.cfgCache = make(map[string]cacheEntry) - } - if c.limiters == nil { - c.limiters = make(map[string]*rate.Limiter) - } - // the state may have changed between critical sections, so double check - e = c.cfgCache[instance] - limiter := c.limiters[instance] - if limiter == nil { - limiter = rate.NewLimiter(rate.Every(throttle), 2) - c.limiters[instance] = limiter - } - if needsRefresh(e, refreshCfgBuffer) { - if limiter.Allow() { - // start a new refresh and update the cachedEntry to reflect that - e.done = c.startRefresh(instance, refreshCfgBuffer) - e.lastRefreshed = time.Now() - c.cfgCache[instance] = e - } else { - // TODO: Investigate returning this as an error instead of just logging - logging.Infof("refresh operation throttled for %s: reusing config from last refresh (%s ago)", instance, time.Since(e.lastRefreshed)) - } - } - c.cacheL.Unlock() - } - - if !isValid(e) { - // if the previous result was invalid, wait for the next result to complete - select { - case <-ctx.Done(): - return "", nil, "", ctx.Err() - case <-e.done: - } - - c.cacheL.RLock() - // the state may have changed between critical sections, so double check - e = c.cfgCache[instance] - c.cacheL.RUnlock() - } - return e.addr, e.cfg, e.version, e.err -} - -// DialContext uses the configuration stored in the client to connect to an instance. -// If this func returns a nil error the connection is correctly authenticated -// to connect to the instance. -func (c *Client) DialContext(ctx context.Context, instance string) (net.Conn, error) { - addr, cfg, _, err := c.cachedCfg(ctx, instance) - if err != nil { - return nil, err - } - - // TODO: attempt an early refresh if an connect fails? - return c.tryConnect(ctx, addr, instance, cfg) -} - -// Dial does the same as DialContext but using context.Background() as the context. -func (c *Client) Dial(instance string) (net.Conn, error) { - return c.DialContext(context.Background(), instance) -} - -// ErrUnexpectedFailure indicates the internal refresh operation failed unexpectedly. -var ErrUnexpectedFailure = errors.New("ErrUnexpectedFailure") - -func (c *Client) tryConnect(ctx context.Context, addr, instance string, cfg *tls.Config) (net.Conn, error) { - // When multiple dial attempts start in quick succession, the internal - // refresh logic is sometimes subject to a race condition. If the first - // attempt fails on a handshake error, it will invalidate the cached config. - // In some cases, a second dial attempt will initiate a connection with an - // invalid config. This check fails fast in such cases. - if addr == "" { - return nil, ErrUnexpectedFailure - } - dial := c.selectDialer() - conn, err := dial(ctx, "tcp", addr) - if err != nil { - return nil, err - } - type setKeepAliver interface { - SetKeepAlive(keepalive bool) error - SetKeepAlivePeriod(d time.Duration) error - } - - if s, ok := conn.(setKeepAliver); ok { - if err := s.SetKeepAlive(true); err != nil { - logging.Verbosef("Couldn't set KeepAlive to true: %v", err) - } else if err := s.SetKeepAlivePeriod(keepAlivePeriod); err != nil { - logging.Verbosef("Couldn't set KeepAlivePeriod to %v", keepAlivePeriod) - } - } else { - logging.Verbosef("KeepAlive not supported: long-running tcp connections may be killed by the OS.") - } - - return c.connectTLS(ctx, conn, instance, cfg) -} - -func (c *Client) selectDialer() func(context.Context, string, string) (net.Conn, error) { - if c.ContextDialer != nil { - return c.ContextDialer - } - - if c.Dialer != nil { - return func(_ context.Context, net, addr string) (net.Conn, error) { - return c.Dialer(net, addr) - } - } - - dialer := proxy.FromEnvironment() - if ctxDialer, ok := dialer.(proxy.ContextDialer); ok { - // although proxy.FromEnvironment() returns a Dialer interface which only has a Dial method, - // it happens in fact that method often returns ContextDialers. - return ctxDialer.DialContext - } - - return func(_ context.Context, net, addr string) (net.Conn, error) { - return dialer.Dial(net, addr) - } -} - -func (c *Client) invalidateCfg(cfg *tls.Config, instance string, err error) { - c.cacheL.RLock() - e := c.cfgCache[instance] - c.cacheL.RUnlock() - if e.cfg != cfg { - return - } - c.cacheL.Lock() - defer c.cacheL.Unlock() - e = c.cfgCache[instance] - // the state may have changed between critical sections, so double check - if e.cfg != cfg { - return - } - err = fmt.Errorf("config invalidated after TLS handshake failed, error = %w", err) - c.cfgCache[instance] = cacheEntry{ - err: err, - done: e.done, - lastRefreshed: e.lastRefreshed, - } -} - -// NewConnSrc returns a chan which can be used to receive connections -// on the passed Listener. All requests sent to the returned chan will have the -// instance name provided here. The chan will be closed if the Listener returns -// an error. -func NewConnSrc(instance string, l net.Listener) <-chan Conn { - ch := make(chan Conn) - go func() { - for { - start := time.Now() - c, err := l.Accept() - if err != nil { - logging.Errorf("listener (%#v) had error: %v", l, err) - if nerr, ok := err.(net.Error); ok && nerr.Temporary() { - d := 10*time.Millisecond - time.Since(start) - if d > 0 { - time.Sleep(d) - } - continue - } - l.Close() - close(ch) - return - } - ch <- Conn{instance, c} - } - }() - return ch -} - -// InstanceVersion uses client cache to return instance version string. -// -// Deprecated: Use Client.InstanceVersionContext instead. -func (c *Client) InstanceVersion(instance string) (string, error) { - return c.InstanceVersionContext(context.Background(), instance) -} - -// InstanceVersionContext uses client cache to return instance version string. -func (c *Client) InstanceVersionContext(ctx context.Context, instance string) (string, error) { - _, _, version, err := c.cachedCfg(ctx, instance) - if err != nil { - return "", err - } - return version, nil -} - -// ParseInstanceConnectionName verifies that instances are in the expected format and include -// the necessary components. -func ParseInstanceConnectionName(instance string) (string, string, string, []string, error) { - args := strings.Split(instance, "=") - if len(args) > 2 { - return "", "", "", nil, fmt.Errorf("invalid instance argument: must be either form - `` or `=`; invalid arg was %q", instance) - } - // Parse the instance connection name - everything before the "=". - proj, region, name := util.SplitName(args[0]) - if proj == "" || region == "" || name == "" { - return "", "", "", nil, fmt.Errorf("invalid instance connection string: must be in the form `project:region:instance-name`; invalid name was %q", args[0]) - } - return proj, region, name, args, nil -} - -// GetInstances iterates through the client cache, returning a list of previously dialed -// instances. -func (c *Client) GetInstances() []string { - var insts []string - c.cacheL.Lock() - cfgCache := c.cfgCache - c.cacheL.Unlock() - for i := range cfgCache { - insts = append(insts, i) - } - return insts -} - -// AvailableConn returns false if MaxConnections has been reached, true otherwise. -// When MaxConnections is 0, there is no limit. -func (c *Client) AvailableConn() bool { - return c.MaxConnections == 0 || atomic.LoadUint64(&c.ConnectionsCounter) < c.MaxConnections -} - -// Shutdown waits up to a given amount of time for all active connections to -// close. Returns an error if there are still active connections after waiting -// for the whole length of the timeout. -func (c *Client) Shutdown(termTimeout time.Duration) error { - term, ticker := time.After(termTimeout), time.NewTicker(100*time.Millisecond) - defer ticker.Stop() - for { - select { - case <-ticker.C: - if atomic.LoadUint64(&c.ConnectionsCounter) > 0 { - continue - } - case <-term: - } - break - } - - active := atomic.LoadUint64(&c.ConnectionsCounter) - if active == 0 { - return nil - } - return fmt.Errorf("%d active connections still exist after waiting for %v", active, termTimeout) -} diff --git a/proxy/proxy/client_test.go b/proxy/proxy/client_test.go deleted file mode 100644 index 123b5ae77c..0000000000 --- a/proxy/proxy/client_test.go +++ /dev/null @@ -1,637 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package proxy - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io/ioutil" - "net" - "net/http/httptest" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - "unsafe" -) - -const instance = "project:region:instance" - -var ( - sentinelError = errors.New("sentinel error") - forever = time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC) -) - -type fakeCerts struct { - sync.Mutex - called int -} - -type blockingCertSource struct { - values map[string]*fakeCerts - validUntil time.Time -} - -func (cs *blockingCertSource) Local(instance string) (tls.Certificate, error) { - v, ok := cs.values[instance] - if !ok { - return tls.Certificate{}, fmt.Errorf("test setup failure: unknown instance %q", instance) - } - v.Lock() - v.called++ - v.Unlock() - - // Returns a cert which is valid forever. - return tls.Certificate{ - Leaf: &x509.Certificate{ - NotAfter: cs.validUntil, - }, - }, nil -} - -func (cs *blockingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { - return &x509.Certificate{}, "fake address", "fake name", "fake version", nil -} - -func newCertSource(certs *fakeCerts, expiration time.Time) CertSource { - return &blockingCertSource{ - values: map[string]*fakeCerts{ - instance: certs, - }, - validUntil: expiration, - } -} - -func newClient(cs CertSource) *Client { - return &Client{ - Certs: cs, - Dialer: func(string, string) (net.Conn, error) { - return nil, sentinelError - }, - } -} - -func TestContextDialer(t *testing.T) { - cs := newCertSource(&fakeCerts{}, forever) - c := newClient(cs) - - c.ContextDialer = func(context.Context, string, string) (net.Conn, error) { - return nil, sentinelError - } - c.Dialer = func(string, string) (net.Conn, error) { - return nil, fmt.Errorf("this dialer should not be used when ContextDialer is set") - } - - if _, err := c.DialContext(context.Background(), instance); err != sentinelError { - t.Errorf("unexpected error: %v", err) - } -} - -func TestClientCache(t *testing.T) { - b := &fakeCerts{} - c := newClient(newCertSource(b, forever)) - - for i := 0; i < 5; i++ { - if _, err := c.Dial(instance); err != sentinelError { - t.Errorf("unexpected error: %v", err) - } - } - - b.Lock() - if b.called != 1 { - t.Errorf("called %d times, want called 1 time", b.called) - } - b.Unlock() -} - -func TestInvalidateConfigCache(t *testing.T) { - srv := httptest.NewTLSServer(nil) - defer srv.Close() - b := &fakeCerts{} - c := &Client{ - Certs: newCertSource(b, forever), - Dialer: func(string, string) (net.Conn, error) { - return net.Dial( - srv.Listener.Addr().Network(), - srv.Listener.Addr().String(), - ) - }, - } - c.cachedCfg(context.Background(), instance) - if needsRefresh(c.cfgCache[instance], DefaultRefreshCfgBuffer) { - t.Error("cached config expected to be valid") - } - _, err := c.Dial(instance) - if err == nil { - t.Errorf("c.Dial(%q) expected to fail with handshake error", instance) - } - if !needsRefresh(c.cfgCache[instance], DefaultRefreshCfgBuffer) { - t.Error("cached config expected to be invalidated after handshake error") - } -} - -func TestValidClient(t *testing.T) { - someErr := errors.New("error") - openCh := make(chan struct{}) - closedCh := make(chan struct{}) - close(closedCh) - - equalErrors := func(a, b []*InvalidError) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i].instance != b[i].instance { - return false - } - if a[i].err != b[i].err { - return false - } - if a[i].hasTLS != b[i].hasTLS { - return false - } - } - return true - } - - testCases := []struct { - desc string - cache map[string]cacheEntry - want []*InvalidError - }{ - { - desc: "when the cache has only valid entries", - cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{cfg: &tls.Config{}, done: closedCh}}, - want: nil, - }, - { - desc: "when the cache has invalid TLS entries", - cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{done: closedCh}}, - want: []*InvalidError{&InvalidError{instance: "proj:region:inst", hasTLS: false}}, - }, - { - desc: "when the cache has errored entries", - cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{err: someErr, done: closedCh}}, - want: []*InvalidError{&InvalidError{instance: "proj:region:inst", hasTLS: false, err: someErr}}, - }, - { - desc: "when the cache has an entry with an in-progress refresh", - cache: map[string]cacheEntry{"proj:region:inst": cacheEntry{err: someErr, done: openCh}}, - want: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - client := &Client{cfgCache: tc.cache} - if got := client.InvalidInstances(); !equalErrors(got, tc.want) { - t.Errorf("want = %v, got = %v", tc.want, got) - } - }) - } -} - -func TestConcurrentRefresh(t *testing.T) { - b := &fakeCerts{} - c := newClient(newCertSource(b, forever)) - - ch := make(chan error) - b.Lock() - - const numDials = 20 - - for i := 0; i < numDials; i++ { - go func() { - _, err := c.Dial(instance) - ch <- err - }() - } - - b.Unlock() - - for i := 0; i < numDials; i++ { - if err := <-ch; err != sentinelError { - t.Errorf("unexpected error: %v", err) - } - } - b.Lock() - if b.called != 1 { - t.Errorf("called %d times, want called 1 time", b.called) - } - b.Unlock() -} - -func TestMaximumConnectionsCount(t *testing.T) { - certSource := &blockingCertSource{ - values: map[string]*fakeCerts{}, - validUntil: forever, - } - c := newClient(certSource) - - const maxConnections = 10 - c.MaxConnections = maxConnections - var dials uint64 - firstDialExited := make(chan struct{}) - c.Dialer = func(string, string) (net.Conn, error) { - atomic.AddUint64(&dials, 1) - // Wait until the first dial fails to ensure the max connections count - // is reached by a concurrent dialer - <-firstDialExited - return nil, sentinelError - } - - // Build certSource.values before creating goroutines to avoid concurrent map read and map write - const numConnections = maxConnections + 1 - instanceNames := make([]string, numConnections) - for i := 0; i < numConnections; i++ { - // Vary instance name to bypass config cache and avoid second call to Client.tryConnect() in Client.Dial() - instanceName := fmt.Sprintf("%s-%d", instance, i) - certSource.values[instanceName] = &fakeCerts{} - instanceNames[i] = instanceName - } - - var wg sync.WaitGroup - var firstDialOnce sync.Once - for _, instanceName := range instanceNames { - wg.Add(1) - go func(instanceName string) { - defer wg.Done() - - conn := Conn{ - Instance: instanceName, - Conn: &dummyConn{}, - } - c.handleConn(context.Background(), conn) - - firstDialOnce.Do(func() { close(firstDialExited) }) - }(instanceName) - } - - wg.Wait() - - switch { - case dials > maxConnections: - t.Errorf("client should have refused to dial new connection on %dth attempt when the maximum of %d connections was reached (%d dials)", numConnections, maxConnections, dials) - case dials == maxConnections: - t.Logf("client has correctly refused to dial new connection on %dth attempt when the maximum of %d connections was reached (%d dials)\n", numConnections, maxConnections, dials) - case dials < maxConnections: - t.Errorf("client should have dialed exactly the maximum of %d connections (%d connections, %d dials)", maxConnections, numConnections, dials) - } -} - -func TestShutdownTerminatesEarly(t *testing.T) { - cs := newCertSource(&fakeCerts{}, forever) - c := newClient(cs) - // Ensure the dialer returns no error. - c.Dialer = func(string, string) (net.Conn, error) { - return nil, nil - } - - shutdown := make(chan bool, 1) - go func() { - c.Shutdown(1) - shutdown <- true - }() - shutdownFinished := false - // In case the code is actually broken and the client doesn't shut down quickly, don't cause the test to hang until it times out. - select { - case <-time.After(100 * time.Millisecond): - case shutdownFinished = <-shutdown: - } - if !shutdownFinished { - t.Errorf("shutdown should have completed quickly because there are no active connections") - } -} - -func TestRefreshTimer(t *testing.T) { - timeToExpire := 2 * time.Second - certCreated := time.Now() - cs := newCertSource(&fakeCerts{}, certCreated.Add(timeToExpire)) - c := newClient(cs) - - c.RefreshCfgThrottle = 20 * time.Millisecond - c.RefreshCfgBuffer = time.Second - - // Call Dial to cache the cert. - if _, err := c.Dial(instance); err != sentinelError { - t.Fatalf("Dial(%s) failed: %v", instance, err) - } - c.cacheL.Lock() - cfg, ok := c.cfgCache[instance] - c.cacheL.Unlock() - if !ok { - t.Fatalf("expected instance to be cached") - } - - time.Sleep(timeToExpire - time.Since(certCreated)) - // Check if cert was refreshed in the background, without calling Dial again. - c.cacheL.Lock() - newCfg, ok := c.cfgCache[instance] - c.cacheL.Unlock() - if !ok { - t.Fatalf("expected instance to be cached") - } - if !newCfg.lastRefreshed.After(cfg.lastRefreshed) { - t.Error("expected cert to be refreshed.") - } -} - -func TestSyncAtomicAlignment(t *testing.T) { - // The sync/atomic pkg has a bug that requires the developer to guarantee - // 64-bit alignment when using 64-bit functions on 32-bit systems. - c := &Client{} - if a := unsafe.Offsetof(c.ConnectionsCounter); a%64 != 0 { - t.Errorf("Client.ConnectionsCounter is not aligned: want %v, got %v", 0, a) - } -} - -type invalidRemoteCertSource struct{} - -func (cs *invalidRemoteCertSource) Local(instance string) (tls.Certificate, error) { - return tls.Certificate{}, nil -} - -func (cs *invalidRemoteCertSource) Remote(instance string) (*x509.Certificate, string, string, string, error) { - return nil, "", "", "", sentinelError -} - -func TestRemoteCertError(t *testing.T) { - c := newClient(&invalidRemoteCertSource{}) - - _, err := c.DialContext(context.Background(), instance) - if err != sentinelError { - t.Errorf("expected sentinel error, got %v", err) - } - -} - -func TestParseInstanceConnectionName(t *testing.T) { - // SplitName has its own tests and is not specifically tested here. - table := []struct { - in string - wantErrorStr string - }{ - {"proj:region:my-db", ""}, - {"proj:region:my-db=options", ""}, - {"proj=region=my-db", "invalid instance argument: must be either form - `` or `=`; invalid arg was \"proj=region=my-db\""}, - {"projregionmy-db", "invalid instance connection string: must be in the form `project:region:instance-name`; invalid name was \"projregionmy-db\""}, - } - - for _, test := range table { - _, _, _, _, gotError := ParseInstanceConnectionName(test.in) - var gotErrorStr string - if gotError != nil { - gotErrorStr = gotError.Error() - } - if gotErrorStr != test.wantErrorStr { - t.Errorf("ParseInstanceConnectionName(%q): got \"%v\" for error, want \"%v\"", test.in, gotErrorStr, test.wantErrorStr) - } - } -} - -type localhostCertSource struct { -} - -func (c localhostCertSource) Local(instance string) (tls.Certificate, error) { - return tls.Certificate{ - Leaf: &x509.Certificate{ - NotAfter: forever, - }, - }, nil -} - -func (c localhostCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { - return &x509.Certificate{}, "localhost", "fake name", "fake version", nil -} - -var _ CertSource = &localhostCertSource{} - -func TestClientHandshakeCanceled(t *testing.T) { - errorIsDeadlineOrTimeout := func(err error) bool { - if errors.Is(err, context.Canceled) { - return true - } - if errors.Is(err, context.DeadlineExceeded) { - return true - } - if strings.Contains(err.Error(), "i/o timeout") { - // We should use os.ErrDeadlineExceeded exceeded here, - // but it is not present in Go versions below 1.15. - return true - } - return false - } - - withTestHarness := func(t *testing.T, f func(port int)) { - // serverShutdown is closed to free the server - // goroutine that is holding up the client request. - serverShutdown := make(chan struct{}) - - l, err := tls.Listen( - "tcp", - ":", - &tls.Config{ - GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - // Make the client wait forever to handshake. - <-serverShutdown - return nil, errors.New("some error") - }, - }) - if err != nil { - t.Fatalf("tls.Listen: %v", err) - } - - port := l.Addr().(*net.TCPAddr).Port - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - - for { - conn, err := l.Accept() - if err != nil { - // Below Go 1.16, we have to string match here. - // https://golang.org/doc/go1.16#net - if !strings.Contains(err.Error(), "use of closed network connection") { - t.Errorf("l.Accept: %v", err) - } - return - } - - _, _ = ioutil.ReadAll(conn) // Trigger the handshake. - _ = conn.Close() - } - }() - - f(port) - close(serverShutdown) // Free the server thread. - _ = l.Close() - wg.Wait() - } - - validateError := func(t *testing.T, err error) { - if err == nil { - t.Fatal("nil error unexpected") - } - if !errorIsDeadlineOrTimeout(err) { - t.Fatalf("unexpected error: %v", err) - } - } - - newClient := func(port int) *Client { - return &Client{ - Port: port, - Certs: &localhostCertSource{}, - } - } - - // Makes it to Handshake. - t.Run("with timeout", func(t *testing.T) { - withTestHarness(t, func(port int) { - c := newClient(port) - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - _, err := c.DialContext(ctx, instance) - validateError(t, err) - }) - }) - - t.Run("when liveness check is called on invalidated config", func(t *testing.T) { - withTestHarness(t, func(port int) { - c := newClient(port) - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - _, err := c.DialContext(ctx, instance) - if err == nil { - t.Fatal("expected DialContext to fail, got no error") - } - - invalid := c.InvalidInstances() - if gotLen := len(invalid); gotLen != 1 { - t.Fatalf("invalid instance want = 1, got = %v", gotLen) - } - got := invalid[0] - if got.err == nil { - t.Fatal("want invalid instance error, got nil") - } - }) - }) - - // Makes it to Handshake. - // Same as the above but the context doesn't have a deadline, - // it is canceled manually after a while. - t.Run("canceled after a while, no deadline", func(t *testing.T) { - withTestHarness(t, func(port int) { - c := newClient(port) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - time.AfterFunc(3*time.Second, cancel) - - _, err := c.DialContext(ctx, instance) - validateError(t, err) - }) - - }) - - // Doesn't make it to Handshake. - t.Run("with short timeout", func(t *testing.T) { - withTestHarness(t, func(port int) { - c := newClient(port) - - ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) - defer cancel() - - _, err := c.DialContext(ctx, instance) - validateError(t, err) - }) - }) - - // Doesn't make it to Handshake. - t.Run("canceled without timeout", func(t *testing.T) { - withTestHarness(t, func(port int) { - c := newClient(port) - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - _, err := c.DialContext(ctx, instance) - validateError(t, err) - }) - }) -} - -func TestConnectingWithInvalidConfig(t *testing.T) { - c := &Client{} - - _, err := c.tryConnect(context.Background(), "", "myinstance", &tls.Config{}) - if err != ErrUnexpectedFailure { - t.Fatalf("wanted ErrUnexpectedFailure, got = %v", err) - } -} - -var ( - errLocal = errors.New("local failed") - errRemote = errors.New("remote failed") -) - -type failingCertSource struct{} - -func (cs failingCertSource) Local(instance string) (tls.Certificate, error) { - return tls.Certificate{}, errLocal -} - -func (cs failingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { - return nil, "", "", "", errRemote -} - -func TestInstanceVersionContext(t *testing.T) { - testCases := []struct { - certSource CertSource - wantErr error - wantVersion string - }{ - { - certSource: newCertSource(&fakeCerts{}, forever), - wantErr: nil, - wantVersion: "fake version", - }, - { - certSource: failingCertSource{}, - wantErr: errLocal, - wantVersion: "", - }, - } - for _, tc := range testCases { - c := newClient(tc.certSource) - v, err := c.InstanceVersionContext(context.Background(), instance) - if v != tc.wantVersion { - t.Fatalf("want version = %v, got version = %v", tc.wantVersion, v) - } - if err != tc.wantErr { - t.Fatalf("want = %v, got = %v", tc.wantErr, err) - } - } -} diff --git a/proxy/proxy/common.go b/proxy/proxy/common.go deleted file mode 100644 index c5b6c034be..0000000000 --- a/proxy/proxy/common.go +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package proxy implements client and server code for proxying an unsecure connection over SSL. -package proxy - -import ( - "bytes" - "errors" - "fmt" - "io" - "net" - "sync" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" -) - -// SQLScope is the Google Cloud Platform scope required for executing API -// calls to Cloud SQL. -const SQLScope = "https://www.googleapis.com/auth/sqlservice.admin" - -// myCopy is similar to io.Copy, but reports whether the returned error was due -// to a bad read or write. The returned error will never be nil -func myCopy(dst io.Writer, src io.Reader) (readErr bool, err error) { - buf := make([]byte, 4096) - for { - n, err := src.Read(buf) - if n > 0 { - if _, werr := dst.Write(buf[:n]); werr != nil { - if err == nil { - return false, werr - } - // Read and write error; just report read error (it happened first). - return true, err - } - } - if err != nil { - return true, err - } - } -} - -func copyError(readDesc, writeDesc string, readErr bool, err error) { - var desc string - if readErr { - desc = "Reading data from " + readDesc - } else { - desc = "Writing data to " + writeDesc - } - logging.Errorf("%v had error: %v", desc, err) -} - -func copyThenClose(remote, local io.ReadWriteCloser, remoteDesc, localDesc string) { - firstErr := make(chan error, 1) - - go func() { - readErr, err := myCopy(remote, local) - select { - case firstErr <- err: - if readErr && err == io.EOF { - logging.Verbosef("Client closed %v", localDesc) - } else { - copyError(localDesc, remoteDesc, readErr, err) - } - remote.Close() - local.Close() - default: - } - }() - - readErr, err := myCopy(local, remote) - select { - case firstErr <- err: - if readErr && err == io.EOF { - logging.Verbosef("Instance %v closed connection", remoteDesc) - } else { - copyError(remoteDesc, localDesc, readErr, err) - } - remote.Close() - local.Close() - default: - // In this case, the other goroutine exited first and already printed its - // error (and closed the things). - } -} - -// NewConnSet initializes a new ConnSet and returns it. -func NewConnSet() *ConnSet { - return &ConnSet{m: make(map[string][]net.Conn)} -} - -// A ConnSet tracks net.Conns associated with a provided ID. -// A nil ConnSet will be a no-op for all methods called on it. -type ConnSet struct { - sync.RWMutex - m map[string][]net.Conn -} - -// String returns a debug string for the ConnSet. -func (c *ConnSet) String() string { - if c == nil { - return "" - } - var b bytes.Buffer - - c.RLock() - for id, conns := range c.m { - fmt.Fprintf(&b, "ID %s:", id) - for i, c := range conns { - fmt.Fprintf(&b, "\n\t%d: %v", i, c) - } - } - c.RUnlock() - - return b.String() -} - -// Add saves the provided conn and associates it with the given string -// identifier. -func (c *ConnSet) Add(id string, conn net.Conn) { - if c == nil { - return - } - c.Lock() - c.m[id] = append(c.m[id], conn) - c.Unlock() -} - -// IDs returns a slice of all identifiers which still have active connections. -func (c *ConnSet) IDs() []string { - if c == nil { - return nil - } - ret := make([]string, 0, len(c.m)) - - c.RLock() - for k := range c.m { - ret = append(ret, k) - } - c.RUnlock() - - return ret -} - -// Conns returns all active connections associated with the provided ids. -func (c *ConnSet) Conns(ids ...string) []net.Conn { - if c == nil { - return nil - } - var ret []net.Conn - - c.RLock() - for _, id := range ids { - ret = append(ret, c.m[id]...) - } - c.RUnlock() - - return ret -} - -// Remove undoes an Add operation to have the set forget about a conn. Do not -// Remove an id/conn pair more than it has been Added. -func (c *ConnSet) Remove(id string, conn net.Conn) error { - if c == nil { - return nil - } - c.Lock() - defer c.Unlock() - - pos := -1 - conns := c.m[id] - for i, cc := range conns { - if cc == conn { - pos = i - break - } - } - - if pos == -1 { - return fmt.Errorf("couldn't find connection %v for id %s", conn, id) - } - - if len(conns) == 1 { - delete(c.m, id) - } else { - c.m[id] = append(conns[:pos], conns[pos+1:]...) - } - - return nil -} - -// Close closes every net.Conn contained in the set. -func (c *ConnSet) Close() error { - if c == nil { - return nil - } - var errs bytes.Buffer - - c.Lock() - for id, conns := range c.m { - for _, c := range conns { - if err := c.Close(); err != nil { - fmt.Fprintf(&errs, "%s close error: %v\n", id, err) - } - } - } - c.Unlock() - - if errs.Len() == 0 { - return nil - } - - return errors.New(errs.String()) -} diff --git a/proxy/proxy/common_test.go b/proxy/proxy/common_test.go deleted file mode 100644 index b20318e0b7..0000000000 --- a/proxy/proxy/common_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// This file contains tests for common.go - -package proxy - -import ( - "net" - "reflect" - "testing" -) - -var c1, c2, c3 = &dummyConn{}, &dummyConn{}, &dummyConn{} - -type dummyConn struct{ net.Conn } - -func (c dummyConn) Close() error { - return nil -} - -func TestConnSetAdd(t *testing.T) { - s := NewConnSet() - - s.Add("a", c1) - aSlice := []string{"a"} - if !reflect.DeepEqual(s.IDs(), aSlice) { - t.Fatalf("got %v, want %v", s.IDs(), aSlice) - } - - s.Add("a", c2) - if !reflect.DeepEqual(s.IDs(), aSlice) { - t.Fatalf("got %v, want %v", s.IDs(), aSlice) - } - - s.Add("b", c3) - ids := s.IDs() - if len(ids) != 2 { - t.Fatalf("got %d ids, wanted 2", len(ids)) - } - ok := ids[0] == "a" && ids[1] == "b" || - ids[1] == "a" && ids[0] == "b" - - if !ok { - t.Fatalf(`got %v, want only "a" and "b"`, ids) - } -} - -func TestConnSetRemove(t *testing.T) { - s := NewConnSet() - - s.Add("a", c1) - s.Add("a", c2) - s.Add("b", c3) - - s.Remove("b", c3) - if got := s.Conns("b"); got != nil { - t.Fatalf("got %v, want nil", got) - } - - aSlice := []string{"a"} - if !reflect.DeepEqual(s.IDs(), aSlice) { - t.Fatalf("got %v, want %v", s.IDs(), aSlice) - } - - s.Remove("a", c1) - if !reflect.DeepEqual(s.IDs(), aSlice) { - t.Fatalf("got %v, want %v", s.IDs(), aSlice) - } - - s.Remove("a", c2) - if len(s.IDs()) != 0 { - t.Fatalf("got %v, want empty set", s.IDs()) - } -} - -func TestConns(t *testing.T) { - s := NewConnSet() - - s.Add("a", c1) - s.Add("a", c2) - s.Add("b", c3) - - got := s.Conns("b") - if !reflect.DeepEqual(got, []net.Conn{c3}) { - t.Fatalf("got %v, wanted only %v", got, c3) - } - - looking := map[net.Conn]bool{ - c1: true, - c2: true, - c3: true, - } - - for _, v := range s.Conns("a", "b") { - if _, ok := looking[v]; !ok { - t.Errorf("got unexpected conn %v", v) - } - delete(looking, v) - } - if len(looking) != 0 { - t.Fatalf("didn't find %v in list of Conns", looking) - } -} diff --git a/proxy/proxy/connect_tls_117.go b/proxy/proxy/connect_tls_117.go deleted file mode 100644 index a1d5802173..0000000000 --- a/proxy/proxy/connect_tls_117.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build go1.17 -// +build go1.17 - -package proxy - -import ( - "context" - "crypto/tls" - "net" -) - -// connectTLS returns a new TLS client side connection -// using conn as the underlying transport. -// -// The returned connection has already completed its TLS handshake. -func (c *Client) connectTLS( - ctx context.Context, - conn net.Conn, - instance string, - cfg *tls.Config, -) (net.Conn, error) { - ret := tls.Client(conn, cfg) - // HandshakeContext was introduced in Go 1.17, hence - // this file is conditionally compiled on only Go versions >= 1.17. - if err := ret.HandshakeContext(ctx); err != nil { - _ = ret.Close() - c.invalidateCfg(cfg, instance, err) - return nil, err - } - return ret, nil -} diff --git a/proxy/proxy/connect_tls_other.go b/proxy/proxy/connect_tls_other.go deleted file mode 100644 index 574bcd5647..0000000000 --- a/proxy/proxy/connect_tls_other.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !go1.17 -// +build !go1.17 - -package proxy - -import ( - "context" - "crypto/tls" - "net" - "sync" - "time" -) - -type cancelationWatcher struct { - done chan struct{} // closed when the caller requests shutdown by calling stop(). - wg sync.WaitGroup -} - -// newCancelationWatcher starts a goroutine that will monitor -// ctx for cancelation. If ctx is canceled, the I/O -// deadline on conn is set to some point in the past, canceling -// ongoing I/O and refusing new I/O. -// -// The caller must call stop() on the returned struct to -// release resources associated with this. -func newCancelationWatcher(ctx context.Context, conn net.Conn) *cancelationWatcher { - cw := &cancelationWatcher{ - done: make(chan struct{}), - } - // Monitor for context cancelation. - cw.wg.Add(1) - go func() { - defer cw.wg.Done() - - select { - case <-ctx.Done(): - // Set the deadline to some point in the past, but not - // the zero value. This will cancel ongoing requests - // and refuse future ones. - _ = conn.SetDeadline(time.Time{}.Add(1)) - case <-cw.done: - return - } - }() - return cw -} - -// stop shuts down this cancelationWatcher and releases -// the resources associated with it. -// -// Once stop has returned, the provided context is no longer -// watched for cancelation and the deadline on the -// provided net.Conn is no longer manipulated. -func (cw *cancelationWatcher) stop() { - close(cw.done) - cw.wg.Wait() -} - -// connectTLS returns a new TLS client side connection -// using conn as the underlying transport. -// -// The returned connection has already completed its TLS handshake. -func (c *Client) connectTLS( - ctx context.Context, - conn net.Conn, - instance string, - cfg *tls.Config, -) (net.Conn, error) { - // For the purposes of this Handshake, manipulate the I/O - // deadlines on this connection inline. We have to do this - // manual dance because we don't have HandshakeContext in this - // version of Go. - - defer func() { - // The connection didn't originally have a read deadline (we - // just created it). So no matter what happens here, restore - // the lack-of-deadline. - // - // In other words, only apply the deadline while dialing, - // not during subsequent usage. - _ = conn.SetDeadline(time.Time{}) - }() - - // If we have a context deadline, apply it. - if dl, ok := ctx.Deadline(); ok { - _ = conn.SetDeadline(dl) - } - - cw := newCancelationWatcher(ctx, conn) - defer cw.stop() // Always free the context watcher. - - ret := tls.Client(conn, cfg) - if err := ret.Handshake(); err != nil { - _ = ret.Close() - c.invalidateCfg(cfg, instance, err) - return nil, err - } - return ret, nil -} diff --git a/proxy/proxy/dial.go b/proxy/proxy/dial.go deleted file mode 100644 index d83614db5c..0000000000 --- a/proxy/proxy/dial.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package proxy - -import ( - "fmt" - "net" - "net/http" - "sync" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/certs" - "golang.org/x/net/context" - "golang.org/x/oauth2/google" -) - -// The port that CloudSQL expects the client to connect to. -const DefaultPort = 3307 - -var dialClient struct { - // This client is initialized in Init/InitWithClient/InitDefault - // and read in Dial. - c *Client - sync.Mutex -} - -// Dial returns a net.Conn connected to the Cloud SQL Instance specified. The -// format of 'instance' is "project-name:region:instance-name". -// -// If one of the Init functions hasn't been called yet, InitDefault is called. -// -// This is a network-level function; consider looking in the dialers -// subdirectory for more convenience functions related to actually logging into -// your database. -func DialContext(ctx context.Context, instance string) (net.Conn, error) { - dialClient.Lock() - c := dialClient.c - dialClient.Unlock() - if c == nil { - if err := InitDefault(ctx); err != nil { - return nil, fmt.Errorf("default proxy initialization failed; consider calling proxy.Init explicitly: %v", err) - } - // InitDefault initialized the client. - dialClient.Lock() - c = dialClient.c - dialClient.Unlock() - } - - return c.DialContext(ctx, instance) -} - -// Dial does the same as DialContext but using context.Background() as the context. -func Dial(instance string) (net.Conn, error) { - return DialContext(context.Background(), instance) -} - -// Dialer is a convenience type to model the standard 'Dial' function. -type Dialer func(net, addr string) (net.Conn, error) - -// Init must be called before Dial is called. This is a more flexible version -// of InitDefault, but allows you to set more fields. -// -// The http.Client is used to authenticate API requests. -// The connset parameter is optional. -// If the dialer is nil, net.Conn is used. -// Use InitWithClient to with a filled client if you want to provide a Context-Aware dialer -func Init(auth *http.Client, connset *ConnSet, dialer Dialer) { - dialClient.Lock() - dialClient.c = &Client{ - Port: DefaultPort, - Certs: certs.NewCertSource("", auth, true), - Conns: connset, - Dialer: dialer, - } - dialClient.Unlock() -} - -// InitClient is similar to Init, but allows you to specify the Client -// directly. - -// Deprecated: Use InitWithClient instead. -func InitClient(c Client) { - dialClient.Lock() - dialClient.c = &c - dialClient.Unlock() -} - -// InitWithClient specifies the Client directly. -func InitWithClient(c *Client) { - dialClient.Lock() - dialClient.c = c - dialClient.Unlock() -} - -// InitDefault attempts to initialize the Dial function using application -// default credentials. -func InitDefault(ctx context.Context) error { - cl, err := google.DefaultClient(ctx, "https://www.googleapis.com/auth/sqlservice.admin") - if err != nil { - return err - } - Init(cl, nil, nil) - return nil -} diff --git a/proxy/util/cloudsqlutil.go b/proxy/util/cloudsqlutil.go deleted file mode 100644 index d8d3bc7c88..0000000000 --- a/proxy/util/cloudsqlutil.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package util contains utility functions for use throughout the Cloud SQL Auth proxy. -package util - -import "strings" - -// SplitName splits a fully qualified instance into its project, region, and -// instance name components. While we make the transition to regionalized -// metadata, the region is optional. -// -// Examples: -// "proj:region:my-db" -> ("proj", "region", "my-db") -// "google.com:project:region:instance" -> ("google.com:project", "region", "instance") -// "google.com:missing:part" -> ("google.com:missing", "", "part") -func SplitName(instance string) (project, region, name string) { - spl := strings.Split(instance, ":") - if len(spl) < 2 { - return "", "", instance - } - if dot := strings.Index(spl[0], "."); dot != -1 { - spl[1] = spl[0] + ":" + spl[1] - spl = spl[1:] - } - switch { - case len(spl) < 2: - return "", "", instance - case len(spl) == 2: - return spl[0], "", spl[1] - default: - return spl[0], spl[1], spl[2] - } -} diff --git a/proxy/util/cloudsqlutil_test.go b/proxy/util/cloudsqlutil_test.go deleted file mode 100644 index d614f33db8..0000000000 --- a/proxy/util/cloudsqlutil_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package util - -import "testing" - -func TestSplitName(t *testing.T) { - table := []struct{ in, wantProj, wantRegion, wantInstance string }{ - {"proj:region:my-db", "proj", "region", "my-db"}, - {"google.com:project:region:instance", "google.com:project", "region", "instance"}, - {"google.com:missing:part", "google.com:missing", "", "part"}, - } - - for _, test := range table { - gotProj, gotRegion, gotInstance := SplitName(test.in) - if gotProj != test.wantProj { - t.Errorf("splitName(%q): got %v for project, want %v", test.in, gotProj, test.wantProj) - } - if gotRegion != test.wantRegion { - t.Errorf("splitName(%q): got %v for region, want %v", test.in, gotRegion, test.wantRegion) - } - if gotInstance != test.wantInstance { - t.Errorf("splitName(%q): got %v for instance, want %v", test.in, gotInstance, test.wantInstance) - } - } -} diff --git a/proxy/util/gcloudutil.go b/proxy/util/gcloudutil.go deleted file mode 100644 index a22b3ff9f0..0000000000 --- a/proxy/util/gcloudutil.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2018 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package util - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "runtime" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/logging" - "golang.org/x/oauth2" - exec "golang.org/x/sys/execabs" -) - -// GcloudConfigData represents the data returned by `gcloud config config-helper`. -type GcloudConfigData struct { - Configuration struct { - Properties struct { - Core struct { - Project string - Account string - } - } - } - Credential struct { - AccessToken string `json:"access_token"` - TokenExpiry time.Time `json:"token_expiry"` - } -} - -func (cfg *GcloudConfigData) oauthToken() *oauth2.Token { - return &oauth2.Token{ - AccessToken: cfg.Credential.AccessToken, - Expiry: cfg.Credential.TokenExpiry, - } -} - -type GcloudStatusCode int - -const ( - GcloudOk GcloudStatusCode = iota - GcloudNotFound - // generic execution failure error not specified above. - GcloudExecErr -) - -type GcloudError struct { - GcloudError error - Status GcloudStatusCode -} - -func (e *GcloudError) Error() string { - return e.GcloudError.Error() -} - -// GcloudConfig returns a GcloudConfigData object or an error of type *GcloudError. -func GcloudConfig() (*GcloudConfigData, error) { - gcloudCmd := "gcloud" - if runtime.GOOS == "windows" { - gcloudCmd = gcloudCmd + ".cmd" - } - - if _, err := exec.LookPath(gcloudCmd); err != nil { - return nil, &GcloudError{err, GcloudNotFound} - } - - buf, errbuf := new(bytes.Buffer), new(bytes.Buffer) - cmd := exec.Command(gcloudCmd, "--format", "json", "config", "config-helper", "--min-expiry", "1h") - cmd.Stdout = buf - cmd.Stderr = errbuf - - if err := cmd.Run(); err != nil { - err = fmt.Errorf("error reading config: %v; stderr was:\n%v", err, errbuf) - logging.Errorf("GcloudConfig: %v", err) - return nil, &GcloudError{err, GcloudExecErr} - } - - data := &GcloudConfigData{} - if err := json.Unmarshal(buf.Bytes(), data); err != nil { - logging.Errorf("Failed to unmarshal bytes from gcloud: %v", err) - logging.Errorf(" gcloud returned:\n%s", buf) - return nil, &GcloudError{err, GcloudExecErr} - } - - return data, nil -} - -// gcloudTokenSource implements oauth2.TokenSource via the `gcloud config config-helper` command. -type gcloudTokenSource struct { -} - -// Token helps gcloudTokenSource implement oauth2.TokenSource. -func (src *gcloudTokenSource) Token() (*oauth2.Token, error) { - cfg, err := GcloudConfig() - if err != nil { - return nil, err - } - return cfg.oauthToken(), nil -} - -func GcloudTokenSource(ctx context.Context) (oauth2.TokenSource, error) { - src := &gcloudTokenSource{} - tok, err := src.Token() - if err != nil { - return nil, err - } - return oauth2.ReuseTokenSource(tok, src), nil -} diff --git a/tests/alldb_test.go b/tests/alldb_test.go deleted file mode 100644 index 5d90d4d486..0000000000 --- a/tests/alldb_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2021 Google LLC All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// alldb_test.go contains end to end tests that require all environment variables to be defined. -package tests - -import ( - "context" - "fmt" - "net/http" - "testing" - "time" -) - -// requireAllVars skips the given test if at least one environment variable is undefined. -func requireAllVars(t *testing.T) { - var allVars []string - allVars = append(allVars, *mysqlConnName, *mysqlUser, *mysqlPass, *mysqlDb) - allVars = append(allVars, *postgresConnName, *postgresUser, *postgresPass, *postgresDb) - allVars = append(allVars, *sqlserverConnName, *sqlserverUser, *sqlserverPass, *sqlserverDb) - - for _, envVar := range allVars { - if envVar == "" { - t.Skip("skipping test, all environment variable must be defined") - } - } -} - -// Test to verify that when a proxy client serves multiple instances that can all be successfully dialed, -// the health check readiness endpoint serves http.StatusOK. -func TestMultiInstanceDial(t *testing.T) { - if testing.Short() { - t.Skip("skipping Health Check integration tests") - } - requireAllVars(t) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - // Start the proxy. - args := []string{ - // This test doesn't care what the instance port is, so use "0" which - // means, let the runtime pick a random port. - fmt.Sprintf("-instances=%s=tcp:0,%s=tcp:0,%s=tcp:0", - *mysqlConnName, *postgresConnName, *sqlserverConnName), - "-use_http_health_check", - } - p, err := StartProxy(ctx, args...) - if err != nil { - t.Fatalf("unable to start proxy: %v", err) - } - defer p.Close() - output, err := p.WaitForServe(ctx) - if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) - } - - resp, err := http.Get("http://localhost:" + testPort + readinessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) - } -} diff --git a/tests/common_test.go b/tests/common_test.go deleted file mode 100644 index 925817198d..0000000000 --- a/tests/common_test.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2015 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package tests contains end to end tests meant to verify the Cloud SQL Auth proxy -// works as expected when executed as a binary. -// -// Required flags: -// -mysql_conn_name, -db_user, -db_pass -package tests - -import ( - "bufio" - "bytes" - "context" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "os/exec" - "path" - "runtime" - "strings" - "testing" -) - -var ( - binPath = "" -) - -func TestMain(m *testing.M) { - flag.Parse() - // compile the proxy as a binary - var err error - binPath, err = compileProxy() - if err != nil { - log.Fatalf("failed to compile proxy: %s", err) - } - // Run tests and cleanup - rtn := m.Run() - os.RemoveAll(binPath) - - os.Exit(rtn) -} - -// compileProxy compiles the binary into a temporary directory, and returns the path to the file or any error that occured. -func compileProxy() (string, error) { - // get path of the cmd pkg - _, f, _, ok := runtime.Caller(0) - if !ok { - return "", fmt.Errorf("failed to find cmd pkg") - } - projRoot := path.Dir(path.Dir(f)) // cd ../.. - pkgPath := path.Join(projRoot, "cmd", "cloud_sql_proxy") - // compile the proxy into a tmp directory - tmp, err := ioutil.TempDir("", "") - if err != nil { - return "", fmt.Errorf("failed to create temp dir: %s", err) - } - - b := path.Join(tmp, "cloud_sql_proxy") - - if runtime.GOOS == "windows" { - b += ".exe" - } - - cmd := exec.Command("go", "build", "-o", b, pkgPath) - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("failed to run 'go build': %w \n %s", err, out) - } - return b, nil -} - -// proxyExec represents an execution of the Cloud SQL proxy. -type ProxyExec struct { - Out io.ReadCloser - - cmd *exec.Cmd - cancel context.CancelFunc - closers []io.Closer - done chan bool // closed once the cmd is completed - err error -} - -// StartProxy returns a proxyExec representing a running instance of the proxy. -func StartProxy(ctx context.Context, args ...string) (*ProxyExec, error) { - var err error - ctx, cancel := context.WithCancel(ctx) - p := ProxyExec{ - cmd: exec.CommandContext(ctx, binPath, args...), - cancel: cancel, - done: make(chan bool), - } - pr, pw, err := os.Pipe() - if err != nil { - return nil, fmt.Errorf("unable to open stdout pipe: %w", err) - } - defer pw.Close() - p.Out, p.cmd.Stdout, p.cmd.Stderr = pr, pw, pw - p.closers = append(p.closers, pr) - if err := p.cmd.Start(); err != nil { - defer p.Close() - return nil, fmt.Errorf("unable to start cmd: %w", err) - } - // when process is complete, mark as finished - go func() { - defer close(p.done) - p.err = p.cmd.Wait() - }() - return &p, nil -} - -// Stop sends the pskill signal to the proxy and returns. -func (p *ProxyExec) Kill() { - p.cancel() -} - -// Waits until the execution is completed and returns any error. -func (p *ProxyExec) Wait() error { - select { - case <-p.done: - return p.err - } -} - -// Stop sends the pskill signal to the proxy and returns. -func (p *ProxyExec) Done() bool { - select { - case <-p.done: - return true - default: - } - return false -} - -// Close releases any resources assotiated with the instance. -func (p *ProxyExec) Close() { - p.cancel() - for _, c := range p.closers { - c.Close() - } -} - -// WaitForServe waits until the proxy ready to serve traffic. Returns any output from the proxy -// while starting or any errors experienced before the proxy was ready to server. -func (p *ProxyExec) WaitForServe(ctx context.Context) (output string, err error) { - // Watch for the "Ready for new connections" to indicate the proxy is listening - buf, in, errCh := new(bytes.Buffer), bufio.NewReader(p.Out), make(chan error, 1) - go func() { - defer close(errCh) - for { - // if ctx is finished, stop processing - select { - case <-ctx.Done(): - return - default: - } - s, err := in.ReadString('\n') - if err != nil { - errCh <- err - return - } - buf.WriteString(s) - if strings.Contains(s, "Ready for new connections") { - errCh <- nil - return - } - } - }() - // Wait for either the background thread of the context to complete - select { - case <-ctx.Done(): - return buf.String(), fmt.Errorf("context done: %w", ctx.Err()) - case err := <-errCh: - if err != nil { - return buf.String(), fmt.Errorf("proxy start failed: %w", err) - } - } - return buf.String(), nil -} diff --git a/tests/connection_test.go b/tests/connection_test.go deleted file mode 100644 index cd2d4f3ede..0000000000 --- a/tests/connection_test.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// connection_test.go provides some helpers for basic connectivity tests to Cloud SQL instances. -package tests - -import ( - "context" - "database/sql" - "fmt" - "sync" - "testing" -) - -// proxyConnTest is a test helper to verify the proxy works with a basic connectivity test. -func proxyConnTest(t *testing.T, connName, driver, dsn string, port int, dir string) { - ctx := context.Background() - - var args []string - if dir != "" { // unix port - args = append(args, fmt.Sprintf("-dir=%s", dir), fmt.Sprintf("-instances=%s", connName)) - } else { // tcp socket - args = append(args, fmt.Sprintf("-instances=%s=tcp:%d", connName, port)) - } - - // Start the proxy - p, err := StartProxy(ctx, args...) - if err != nil { - t.Fatalf("unable to start proxy: %v", err) - } - defer p.Close() - output, err := p.WaitForServe(ctx) - if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) - } - - // Connect to the instance - db, err := sql.Open(driver, dsn) - if err != nil { - t.Fatalf("unable to connect to db: %s", err) - } - defer db.Close() - _, err = db.Exec("SELECT 1;") - if err != nil { - - t.Fatalf("unable to exec on db: %s", err) - } -} - -func proxyConnLimitTest(t *testing.T, connName, driver, dsn string, port int) { - ctx := context.Background() - - maxConn, totConn := 5, 10 - - // Start the proxy - p, err := StartProxy(ctx, fmt.Sprintf("-instances=%s=tcp:%d", connName, port), fmt.Sprintf("-max_connections=%d", maxConn)) - if err != nil { - t.Fatalf("unable to start proxy: %v", err) - } - defer p.Close() - output, err := p.WaitForServe(ctx) - if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) - } - - // Create connection pool - var stmt string - switch driver { - case "mysql": - stmt = "SELECT sleep(2);" - case "postgres": - stmt = "SELECT pg_sleep(2);" - case "sqlserver": - stmt = "WAITFOR DELAY '00:00:02'" - default: - t.Fatalf("unsupported driver: no sleep query found") - } - db, err := sql.Open(driver, dsn) - if err != nil { - t.Fatalf("unable to connect to db: %s", err) - } - db.SetMaxIdleConns(0) - defer db.Close() - - // Connect with up to totConn and count errors - var wg sync.WaitGroup - c := make(chan error, totConn) - for i := 0; i < totConn; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _, err := db.ExecContext(ctx, stmt) - if err != nil { - c <- err - } - }() - } - wg.Wait() - close(c) - - var errs []error - for e := range c { - errs = append(errs, e) - } - want, got := totConn-maxConn, len(errs) - if want != got { - t.Errorf("wrong errCt - want: %d, got %d", want, got) - for _, e := range errs { - t.Errorf("%s\n", e) - } - t.Fail() - } -} diff --git a/tests/dialer_test.go b/tests/dialer_test.go deleted file mode 100644 index bea05d192b..0000000000 --- a/tests/dialer_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2022 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tests - -import ( - "context" - "database/sql" - "fmt" - "net" - "net/http" - "testing" - "time" - - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/certs" - "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" - "github.com/jackc/pgx/v4" - "github.com/jackc/pgx/v4/stdlib" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - "google.golang.org/api/option" - "google.golang.org/api/sqladmin/v1" -) - -func TestClientHandlesSSLReset(t *testing.T) { - if testing.Short() { - t.Skip("skipping dialer integration tests") - } - newClient := func(c *http.Client) *proxy.Client { - return &proxy.Client{ - Port: 3307, - Certs: certs.NewCertSourceOpts(c, certs.RemoteOpts{ - UserAgent: "cloud_sql_proxy/test_build", - IPAddrTypeOpts: []string{"PUBLIC", "PRIVATE"}, - }), - Conns: proxy.NewConnSet(), - } - } - connectToDB := func(c *proxy.Client) (*sql.DB, error) { - var ( - dbUser = *postgresUser - dbPwd = *postgresPass - dbName = *postgresDb - ) - dsn := fmt.Sprintf("user=%s password=%s database=%s", dbUser, dbPwd, dbName) - config, err := pgx.ParseConfig(dsn) - if err != nil { - return nil, err - } - config.DialFunc = func(ctx context.Context, network, instance string) (net.Conn, error) { - return c.DialContext(ctx, *postgresConnName) - } - dbURI := stdlib.RegisterConnConfig(config) - return sql.Open("pgx", dbURI) - } - resetSSL := func(c *http.Client) error { - svc, err := sqladmin.NewService(context.Background(), option.WithHTTPClient(c)) - if err != nil { - return err - } - project, _, instance, _, _ := proxy.ParseInstanceConnectionName(*postgresConnName) - t.Log("Resetting SSL config.") - op, err := svc.Instances.ResetSslConfig(project, instance).Do() - if err != nil { - return err - } - for { - t.Log("Waiting for operation to complete.") - op, err = svc.Operations.Get(project, op.Name).Do() - if err != nil { - return err - } - if op.Status == "DONE" { - t.Log("reset SSL config operation complete") - break - } - time.Sleep(time.Second) - } - return nil - } - - // SETUP: create HTTP client and proxy client, then connect to database - src, err := google.DefaultTokenSource(context.Background(), proxy.SQLScope) - if err != nil { - t.Fatal(err) - } - client := oauth2.NewClient(context.Background(), src) - proxyClient := newClient(client) - - db, err := connectToDB(proxyClient) - if err != nil { - t.Fatalf("failed to connect to DB: %v", err) - } - - // Begin database transaction - tx, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer tx.Rollback() - - resetSSL(client) - - // Re-dial twice, once to invalidate config, once to establish connection - var attempts int - for { - t.Log("Re-dialing instance") - _, err = proxyClient.DialContext(context.Background(), *postgresConnName) - if err != nil { - t.Logf("Dial error: %v", err) - } - if err == nil { - break - } - attempts++ - if attempts > 1 { - t.Fatalf("could not dial: %v", err) - } - time.Sleep(time.Second) - } - - for i := 0; i < 5; i++ { - row, err := tx.Query("SELECT 1") - if err != nil { - t.Logf("Query after Reset SSL failed as expected after %v retries (error was %v)", i, err) - break - } - row.Close() - time.Sleep(time.Second) - } - - if err = db.Ping(); err != nil { - t.Fatalf("could not re-stablish a DB connection: %v", err) - } -} diff --git a/tests/healthcheck_test.go b/tests/healthcheck_test.go deleted file mode 100644 index 2616b2c985..0000000000 --- a/tests/healthcheck_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2021 Google LLC All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// healthcheck_test.go provides some helpers for end to end health check server tests. -package tests - -import ( - "context" - "fmt" - "net/http" - "testing" -) - -const ( - readinessPath = "/readiness" - testPort = "8090" -) - -// singleInstanceDial verifies that when a proxy client serves the given instance, the readiness -// endpoint serves http.StatusOK. -func singleInstanceDial(t *testing.T, connName string, port int) { - ctx := context.Background() - - var args []string - args = append(args, fmt.Sprintf("-instances=%s=tcp:%d", connName, port), "-use_http_health_check") - - // Start the proxy. - p, err := StartProxy(ctx, args...) - if err != nil { - t.Fatalf("unable to start proxy: %v", err) - } - defer p.Close() - output, err := p.WaitForServe(ctx) - if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) - } - - resp, err := http.Get("http://localhost:" + testPort + readinessPath) - if err != nil { - t.Fatalf("HTTP GET failed: %v", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("want %v, got %v", http.StatusOK, resp.StatusCode) - } -} diff --git a/tests/mysql_test.go b/tests/mysql_test.go deleted file mode 100644 index 67330c92e1..0000000000 --- a/tests/mysql_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mysql_test runs various tests against a MySQL flavored Cloud SQL instance. -package tests - -import ( - "flag" - "io/ioutil" - "log" - "os" - "path" - "runtime" - "testing" - - mysql "github.com/go-sql-driver/mysql" -) - -var ( - mysqlConnName = flag.String("mysql_conn_name", os.Getenv("MYSQL_CONNECTION_NAME"), "Cloud SQL MYSQL instance connection name, in the form of 'project:region:instance'.") - mysqlUser = flag.String("mysql_user", os.Getenv("MYSQL_USER"), "Name of database user.") - mysqlPass = flag.String("mysql_pass", os.Getenv("MYSQL_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") - mysqlDb = flag.String("mysql_db", os.Getenv("MYSQL_DB"), "Name of the database to connect to.") - - mysqlPort = 3306 -) - -func requireMysqlVars(t *testing.T) { - switch "" { - case *mysqlConnName: - t.Fatal("'mysql_conn_name' not set") - case *mysqlUser: - t.Fatal("'mysql_user' not set") - case *mysqlPass: - t.Fatal("'mysql_pass' not set") - case *mysqlDb: - t.Fatal("'mysql_db' not set") - } -} - -func TestMysqlTcp(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - requireMysqlVars(t) - cfg := mysql.Config{ - User: *mysqlUser, - Passwd: *mysqlPass, - DBName: *mysqlDb, - AllowNativePasswords: true, - } - proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort, "") -} - -func TestMysqlSocket(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - if runtime.GOOS == "windows" { - t.Skip("Skipped Unix socket test on Windows") - } - requireMysqlVars(t) - - dir, err := ioutil.TempDir("", "csql-proxy-tests") - if err != nil { - log.Fatalf("unable to create tmp dir: %s", err) - } - defer os.RemoveAll(dir) - - cfg := mysql.Config{ - User: *mysqlUser, - Passwd: *mysqlPass, - Net: "unix", - Addr: path.Join(dir, *mysqlConnName), - DBName: *mysqlDb, - AllowNativePasswords: true, - } - proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), 0, dir) -} - -func TestMysqlConnLimit(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - requireMysqlVars(t) - cfg := mysql.Config{ - User: *mysqlUser, - Passwd: *mysqlPass, - DBName: *mysqlDb, - AllowNativePasswords: true, - } - proxyConnLimitTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort) -} - -// Test to verify that when a proxy client serves one mysql instance that can be -// dialed successfully, the health check readiness endpoint serves http.StatusOK. -func TestMysqlDial(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - switch "" { - case *mysqlConnName: - t.Fatal("'mysql_conn_name' not set") - } - - singleInstanceDial(t, *mysqlConnName, mysqlPort) -} diff --git a/tests/postgres_test.go b/tests/postgres_test.go deleted file mode 100644 index 0cc3c28451..0000000000 --- a/tests/postgres_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// postgres_test runs various tests against a Postgres flavored Cloud SQL instance. -package tests - -import ( - "context" - "database/sql" - "flag" - "fmt" - "io/ioutil" - "log" - "os" - "path" - "runtime" - "testing" - "time" - - _ "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/postgres" - _ "github.com/lib/pq" -) - -var ( - postgresConnName = flag.String("postgres_conn_name", os.Getenv("POSTGRES_CONNECTION_NAME"), "Cloud SQL Postgres instance connection name, in the form of 'project:region:instance'.") - postgresUser = flag.String("postgres_user", os.Getenv("POSTGRES_USER"), "Name of database user.") - postgresPass = flag.String("postgres_pass", os.Getenv("POSTGRES_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") - postgresDb = flag.String("postgres_db", os.Getenv("POSTGRES_DB"), "Name of the database to connect to.") - - postgresIAMUser = flag.String("postgres_user_iam", os.Getenv("POSTGRES_USER_IAM"), "Name of database user configured with IAM DB Authentication.") - - postgresPort = 5432 -) - -func requirePostgresVars(t *testing.T) { - switch "" { - case *postgresConnName: - t.Fatal("'postgres_conn_name' not set") - case *postgresUser: - t.Fatal("'postgres_user' not set") - case *postgresPass: - t.Fatal("'postgres_pass' not set") - case *postgresDb: - t.Fatal("'postgres_db' not set") - } -} - -func TestPostgresTcp(t *testing.T) { - if testing.Short() { - t.Skip("skipping Postgres integration tests") - } - requirePostgresVars(t) - - dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) - proxyConnTest(t, *postgresConnName, "postgres", dsn, postgresPort, "") -} - -func TestPostgresSocket(t *testing.T) { - if testing.Short() { - t.Skip("skipping Postgres integration tests") - } - if runtime.GOOS == "windows" { - t.Skip("Skipped Unix socket test on Windows") - } - requirePostgresVars(t) - - dir, err := ioutil.TempDir("", "csql-proxy") - if err != nil { - log.Fatalf("unable to create tmp dir: %s", err) - } - defer os.RemoveAll(dir) - - dsn := fmt.Sprintf("user=%s password=%s database=%s host=%s", *postgresUser, *postgresPass, *postgresDb, path.Join(dir, *postgresConnName)) - proxyConnTest(t, *postgresConnName, "postgres", dsn, 0, dir) -} - -func TestPostgresConnLimit(t *testing.T) { - if testing.Short() { - t.Skip("skipping Postgres integration tests") - } - requirePostgresVars(t) - - dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) - proxyConnLimitTest(t, *postgresConnName, "postgres", dsn, postgresPort) -} - -func TestPostgresIAMDBAuthn(t *testing.T) { - if testing.Short() { - t.Skip("skipping Postgres integration tests") - } - requirePostgresVars(t) - if *postgresIAMUser == "" { - t.Fatal("'postgres_user_iam' not set") - } - - ctx := context.Background() - - // Start the proxy - p, err := StartProxy(ctx, fmt.Sprintf("-instances=%s=tcp:%d", *postgresConnName, 5432), "-enable_iam_login") - if err != nil { - t.Fatalf("unable to start proxy: %v", err) - } - defer p.Close() - output, err := p.WaitForServe(ctx) - if err != nil { - t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) - } - - dsn := fmt.Sprintf("user=%s database=%s sslmode=disable", *postgresIAMUser, *postgresDb) - db, err := sql.Open("postgres", dsn) - if err != nil { - t.Fatalf("unable to connect to db: %s", err) - } - defer db.Close() - _, err = db.Exec("SELECT 1;") - if err != nil { - - t.Fatalf("unable to exec on db: %s", err) - } -} - -func TestPostgresHook(t *testing.T) { - if testing.Short() { - t.Skip("skipping Postgres integration tests") - } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", *postgresConnName, *postgresUser, *postgresPass, *postgresDb) - db, err := sql.Open("cloudsqlpostgres", dsn) - if err != nil { - t.Fatalf("connect failed: %s", err) - } - defer db.Close() - var now time.Time - err = db.QueryRowContext(ctx, "SELECT NOW()").Scan(&now) - if err != nil { - t.Fatalf("query failed: %s", err) - } -} - -// Test to verify that when a proxy client serves one postgres instance that can be -// dialed successfully, the health check readiness endpoint serves http.StatusOK. -func TestPostgresDial(t *testing.T) { - if testing.Short() { - t.Skip("skipping Postgres integration tests") - } - switch "" { - case *postgresConnName: - t.Fatal("'postgres_conn_name' not set") - } - - singleInstanceDial(t, *postgresConnName, postgresPort) -} diff --git a/tests/sqlserver_test.go b/tests/sqlserver_test.go deleted file mode 100644 index 93977e49ea..0000000000 --- a/tests/sqlserver_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// sqlserver_test runs various tests against a SqlServer flavored Cloud SQL instance. -package tests - -import ( - "flag" - "fmt" - "os" - "testing" - - _ "github.com/denisenkom/go-mssqldb" -) - -var ( - sqlserverConnName = flag.String("sqlserver_conn_name", os.Getenv("SQLSERVER_CONNECTION_NAME"), "Cloud SQL SqlServer instance connection name, in the form of 'project:region:instance'.") - sqlserverUser = flag.String("sqlserver_user", os.Getenv("SQLSERVER_USER"), "Name of database user.") - sqlserverPass = flag.String("sqlserver_pass", os.Getenv("SQLSERVER_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") - sqlserverDb = flag.String("sqlserver_db", os.Getenv("SQLSERVER_DB"), "Name of the database to connect to.") - - sqlserverPort = 1433 -) - -func requireSqlserverVars(t *testing.T) { - switch "" { - case *sqlserverConnName: - t.Fatal("'sqlserver_conn_name' not set") - case *sqlserverUser: - t.Fatal("'sqlserver_user' not set") - case *sqlserverPass: - t.Fatal("'sqlserver_pass' not set") - case *sqlserverDb: - t.Fatal("'sqlserver_db' not set") - } -} - -func TestSqlServerTcp(t *testing.T) { - if testing.Short() { - t.Skip("skipping SQL Server integration tests") - } - requireSqlserverVars(t) - - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", *sqlserverUser, *sqlserverPass, *sqlserverDb) - proxyConnTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort, "") -} - -func TestSqlserverConnLimit(t *testing.T) { - if testing.Short() { - t.Skip("skipping SQL Server integration tests") - } - requireSqlserverVars(t) - - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", *sqlserverUser, *sqlserverPass, *sqlserverDb) - proxyConnLimitTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort) -} - -// Test to verify that when a proxy client serves one sqlserver instance that can be -// dialed successfully, the health check readiness endpoint serves http.StatusOK. -func TestSqlserverDial(t *testing.T) { - if testing.Short() { - t.Skip("skipping SQL Server integration tests") - } - switch "" { - case *sqlserverConnName: - t.Fatal("'sqlserver_conn_name' not set") - } - - singleInstanceDial(t, *sqlserverConnName, sqlserverPort) -} diff --git a/testsV2/mysql_test.go b/testsV2/mysql_test.go deleted file mode 100644 index 2246b28611..0000000000 --- a/testsV2/mysql_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021 Google LLC - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// https://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mysql_test runs various tests against a MySQL flavored Cloud SQL instance. -package tests - -import ( - "flag" - "os" - "testing" - - mysql "github.com/go-sql-driver/mysql" -) - -var ( - mysqlConnName = flag.String("mysql_conn_name", os.Getenv("MYSQL_CONNECTION_NAME"), "Cloud SQL MYSQL instance connection name, in the form of 'project:region:instance'.") - mysqlUser = flag.String("mysql_user", os.Getenv("MYSQL_USER"), "Name of database user.") - mysqlPass = flag.String("mysql_pass", os.Getenv("MYSQL_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") - mysqlDB = flag.String("mysql_db", os.Getenv("MYSQL_DB"), "Name of the database to connect to.") -) - -func requireMySQLVars(t *testing.T) { - switch "" { - case *mysqlConnName: - t.Fatal("'mysql_conn_name' not set") - case *mysqlUser: - t.Fatal("'mysql_user' not set") - case *mysqlPass: - t.Fatal("'mysql_pass' not set") - case *mysqlDB: - t.Fatal("'mysql_db' not set") - } -} - -func TestMySQLTCP(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - requireMySQLVars(t) - cfg := mysql.Config{ - User: *mysqlUser, - Passwd: *mysqlPass, - DBName: *mysqlDB, - AllowNativePasswords: true, - Addr: "127.0.0.1:3306", - Net: "tcp", - } - proxyConnTest(t, []string{*mysqlConnName}, "mysql", cfg.FormatDSN()) -} - -func TestMySQLAuthWithToken(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - requireMySQLVars(t) - tok, _, cleanup := removeAuthEnvVar(t) - defer cleanup() - - cfg := mysql.Config{ - User: *mysqlUser, - Passwd: *mysqlPass, - DBName: *mysqlDB, - AllowNativePasswords: true, - Addr: "127.0.0.1:3306", - Net: "tcp", - } - proxyConnTest(t, - []string{"--token", tok.AccessToken, *mysqlConnName}, - "mysql", cfg.FormatDSN()) -} - -func TestMySQLAuthWithCredentialsFile(t *testing.T) { - if testing.Short() { - t.Skip("skipping MySQL integration tests") - } - requireMySQLVars(t) - _, path, cleanup := removeAuthEnvVar(t) - defer cleanup() - - cfg := mysql.Config{ - User: *mysqlUser, - Passwd: *mysqlPass, - DBName: *mysqlDB, - AllowNativePasswords: true, - Addr: "127.0.0.1:3306", - Net: "tcp", - } - proxyConnTest(t, - []string{"--credentials-file", path, *mysqlConnName}, - "mysql", cfg.FormatDSN()) -} diff --git a/testsV2/postgres_test.go b/testsV2/postgres_test.go index f28f17b5d9..49c769b37a 100644 --- a/testsV2/postgres_test.go +++ b/testsV2/postgres_test.go @@ -21,8 +21,8 @@ import ( "os" "testing" - _ "github.com/GoogleCloudPlatform/cloudsql-proxy/v2/proxy/dialers/postgres" - _ "github.com/lib/pq" + "cloud.google.com/go/cloudsqlconn" + "cloud.google.com/go/cloudsqlconn/postgres/pgxv4" ) var ( @@ -53,8 +53,15 @@ func TestPostgresTCP(t *testing.T) { } requirePostgresVars(t) - dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDB) - proxyConnTest(t, []string{*postgresConnName}, "postgres", dsn) + cleanup, err := pgxv4.RegisterDriver("postgres1") + if err != nil { + t.Fatalf("failed to register driver: %v", err) + } + defer cleanup() + + dsn := fmt.Sprintf("host=%v user=%v password=%v database=%v sslmode=disable", + *postgresConnName, *postgresUser, *postgresPass, *postgresDB) + proxyConnTest(t, []string{*postgresConnName}, "postgres1", dsn) } func TestPostgresAuthWithToken(t *testing.T) { @@ -62,14 +69,19 @@ func TestPostgresAuthWithToken(t *testing.T) { t.Skip("skipping Postgres integration tests") } requirePostgresVars(t) - tok, _, cleanup := removeAuthEnvVar(t) + cleanup, err := pgxv4.RegisterDriver("postgres2", cloudsqlconn.WithIAMAuthN()) + if err != nil { + t.Fatalf("failed to register driver: %v", err) + } defer cleanup() + tok, _, cleanup2 := removeAuthEnvVar(t) + defer cleanup2() - dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", - *postgresUser, *postgresPass, *postgresDB) + dsn := fmt.Sprintf("host=%v user=%v password=%v database=%v sslmode=disable", + *postgresConnName, *postgresUser, *postgresPass, *postgresDB) proxyConnTest(t, []string{"--token", tok.AccessToken, *postgresConnName}, - "postgres", dsn) + "postgres2", dsn) } func TestPostgresAuthWithCredentialsFile(t *testing.T) { @@ -77,12 +89,17 @@ func TestPostgresAuthWithCredentialsFile(t *testing.T) { t.Skip("skipping Postgres integration tests") } requirePostgresVars(t) - _, path, cleanup := removeAuthEnvVar(t) + cleanup, err := pgxv4.RegisterDriver("postgres3", cloudsqlconn.WithIAMAuthN()) + if err != nil { + t.Fatalf("failed to register driver: %v", err) + } defer cleanup() + _, path, cleanup2 := removeAuthEnvVar(t) + defer cleanup2() - dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", - *postgresUser, *postgresPass, *postgresDB) + dsn := fmt.Sprintf("host=%v user=%v password=%v database=%v sslmode=disable", + *postgresConnName, *postgresUser, *postgresPass, *postgresDB) proxyConnTest(t, []string{"--credentials-file", path, *postgresConnName}, - "postgres", dsn) + "postgres3", dsn) } diff --git a/testsV2/sqlserver_test.go b/testsV2/sqlserver_test.go deleted file mode 100644 index 3ba683391d..0000000000 --- a/testsV2/sqlserver_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2021 Google LLC - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// https://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// sqlserver_test runs various tests against a SqlServer flavored Cloud SQL instance. -package tests - -import ( - "flag" - "fmt" - "os" - "testing" - - _ "github.com/denisenkom/go-mssqldb" -) - -var ( - sqlserverConnName = flag.String("sqlserver_conn_name", os.Getenv("SQLSERVER_CONNECTION_NAME"), "Cloud SQL SqlServer instance connection name, in the form of 'project:region:instance'.") - sqlserverUser = flag.String("sqlserver_user", os.Getenv("SQLSERVER_USER"), "Name of database user.") - sqlserverPass = flag.String("sqlserver_pass", os.Getenv("SQLSERVER_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") - sqlserverDB = flag.String("sqlserver_db", os.Getenv("SQLSERVER_DB"), "Name of the database to connect to.") -) - -func requireSQLServerVars(t *testing.T) { - switch "" { - case *sqlserverConnName: - t.Fatal("'sqlserver_conn_name' not set") - case *sqlserverUser: - t.Fatal("'sqlserver_user' not set") - case *sqlserverPass: - t.Fatal("'sqlserver_pass' not set") - case *sqlserverDB: - t.Fatal("'sqlserver_db' not set") - } -} - -func TestSQLServerTCP(t *testing.T) { - if testing.Short() { - t.Skip("skipping SQL Server integration tests") - } - requireSQLServerVars(t) - - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", - *sqlserverUser, *sqlserverPass, *sqlserverDB) - proxyConnTest(t, []string{*sqlserverConnName}, "sqlserver", dsn) -} - -func TestSQLServerAuthWithToken(t *testing.T) { - if testing.Short() { - t.Skip("skipping SQL Server integration tests") - } - requireSQLServerVars(t) - tok, _, cleanup := removeAuthEnvVar(t) - defer cleanup() - - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", - *sqlserverUser, *sqlserverPass, *sqlserverDB) - proxyConnTest(t, - []string{"--token", tok.AccessToken, *sqlserverConnName}, - "sqlserver", dsn) -} - -func TestSQLServerAuthWithCredentialsFile(t *testing.T) { - if testing.Short() { - t.Skip("skipping SQL Server integration tests") - } - requireSQLServerVars(t) - _, path, cleanup := removeAuthEnvVar(t) - defer cleanup() - - dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1?database=%s", - *sqlserverUser, *sqlserverPass, *sqlserverDB) - proxyConnTest(t, - []string{"--credentials-file", path, *sqlserverConnName}, - "sqlserver", dsn) -} From 6182346cb22fd5b0763f2ba3d82d8d00c32f2fc1 Mon Sep 17 00:00:00 2001 From: Eno Compton Date: Wed, 13 Apr 2022 11:33:25 -0600 Subject: [PATCH 3/4] Remove non-Go files --- .build/alpine.yaml | 29 -- .build/buster.yaml | 29 -- .build/default.yaml | 28 -- .build/gcs_upload.yaml | 98 ------- .github/ISSUE_TEMPLATE/bug-report.md | 50 ---- .github/ISSUE_TEMPLATE/config.yml | 8 - .github/ISSUE_TEMPLATE/documentation-issue.md | 32 -- .github/ISSUE_TEMPLATE/feature-request.md | 32 -- .github/ISSUE_TEMPLATE/question.md | 31 -- .github/PULL_REQUEST_TEMPLATE.md | 17 -- .github/blunderbuss.yml | 25 -- .github/header-checker-lint.yml | 22 -- .github/release-please.yml | 18 -- .github/renovate.json | 27 -- .github/workflows/coverage.yaml | 50 ---- .github/workflows/tests.yaml | 34 --- .kokoro/go116/linux/common.cfg | 35 --- .kokoro/go116/linux/continuous.cfg | 15 - .kokoro/go116/linux/periodic.cfg | 15 - .kokoro/go116/linux/presubmit.cfg | 15 - .kokoro/go116/windows/common.cfg | 20 -- .kokoro/go116/windows/continuous.cfg | 15 - .kokoro/go116/windows/periodic.cfg | 15 - .kokoro/go116/windows/presubmit.cfg | 15 - .kokoro/go117/linux/common.cfg | 35 --- .kokoro/go117/linux/continuous.cfg | 15 - .kokoro/go117/linux/periodic.cfg | 15 - .kokoro/go117/linux/presubmit.cfg | 15 - .kokoro/go118/linux/common.cfg | 35 --- .kokoro/go118/linux/continuous.cfg | 15 - .kokoro/go118/linux/periodic.cfg | 15 - .kokoro/go118/linux/presubmit.cfg | 15 - .kokoro/go118/macos/common.cfg | 21 -- .kokoro/go118/macos/continuous.cfg | 15 - .kokoro/go118/macos/periodic.cfg | 15 - .kokoro/go118/macos/presubmit.cfg | 15 - .kokoro/release_artifacts.sh | 56 ---- .kokoro/tag_latest.sh | 42 --- .kokoro/tests/run_tests.bat | 1 - .kokoro/tests/run_tests.sh | 34 --- .kokoro/tests/run_tests_macos.sh | 41 --- .kokoro/tests/run_tests_windows.sh | 34 --- .kokoro/trampoline.sh | 16 - examples/k8s-health-check/README.md | 70 ----- .../proxy_with_http_health_check.yaml | 135 --------- examples/k8s-service/README.md | 277 ------------------ examples/k8s-service/ca_csr.json | 16 - examples/k8s-service/deployment.yaml | 67 ----- .../k8s-service/pgbouncer_deployment.yaml | 90 ------ examples/k8s-service/pgbouncer_service.yaml | 25 -- examples/k8s-service/server_csr.json | 19 -- examples/k8s-sidecar/README.md | 258 ---------------- examples/k8s-sidecar/no_proxy_private_ip.yaml | 53 ---- examples/k8s-sidecar/proxy_with_sa_key.yaml | 98 ------- .../proxy_with_workload_identity.yaml | 92 ------ examples/k8s-sidecar/service_account.yaml | 20 -- 56 files changed, 2345 deletions(-) delete mode 100644 .build/alpine.yaml delete mode 100644 .build/buster.yaml delete mode 100644 .build/default.yaml delete mode 100644 .build/gcs_upload.yaml delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/documentation-issue.md delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.md delete mode 100644 .github/ISSUE_TEMPLATE/question.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/blunderbuss.yml delete mode 100644 .github/header-checker-lint.yml delete mode 100644 .github/release-please.yml delete mode 100644 .github/renovate.json delete mode 100644 .github/workflows/coverage.yaml delete mode 100644 .github/workflows/tests.yaml delete mode 100644 .kokoro/go116/linux/common.cfg delete mode 100644 .kokoro/go116/linux/continuous.cfg delete mode 100644 .kokoro/go116/linux/periodic.cfg delete mode 100644 .kokoro/go116/linux/presubmit.cfg delete mode 100644 .kokoro/go116/windows/common.cfg delete mode 100644 .kokoro/go116/windows/continuous.cfg delete mode 100644 .kokoro/go116/windows/periodic.cfg delete mode 100644 .kokoro/go116/windows/presubmit.cfg delete mode 100644 .kokoro/go117/linux/common.cfg delete mode 100644 .kokoro/go117/linux/continuous.cfg delete mode 100644 .kokoro/go117/linux/periodic.cfg delete mode 100644 .kokoro/go117/linux/presubmit.cfg delete mode 100644 .kokoro/go118/linux/common.cfg delete mode 100644 .kokoro/go118/linux/continuous.cfg delete mode 100644 .kokoro/go118/linux/periodic.cfg delete mode 100644 .kokoro/go118/linux/presubmit.cfg delete mode 100644 .kokoro/go118/macos/common.cfg delete mode 100644 .kokoro/go118/macos/continuous.cfg delete mode 100644 .kokoro/go118/macos/periodic.cfg delete mode 100644 .kokoro/go118/macos/presubmit.cfg delete mode 100755 .kokoro/release_artifacts.sh delete mode 100755 .kokoro/tag_latest.sh delete mode 100644 .kokoro/tests/run_tests.bat delete mode 100755 .kokoro/tests/run_tests.sh delete mode 100644 .kokoro/tests/run_tests_macos.sh delete mode 100755 .kokoro/tests/run_tests_windows.sh delete mode 100644 .kokoro/trampoline.sh delete mode 100644 examples/k8s-health-check/README.md delete mode 100644 examples/k8s-health-check/proxy_with_http_health_check.yaml delete mode 100644 examples/k8s-service/README.md delete mode 100644 examples/k8s-service/ca_csr.json delete mode 100644 examples/k8s-service/deployment.yaml delete mode 100644 examples/k8s-service/pgbouncer_deployment.yaml delete mode 100644 examples/k8s-service/pgbouncer_service.yaml delete mode 100644 examples/k8s-service/server_csr.json delete mode 100644 examples/k8s-sidecar/README.md delete mode 100644 examples/k8s-sidecar/no_proxy_private_ip.yaml delete mode 100644 examples/k8s-sidecar/proxy_with_sa_key.yaml delete mode 100644 examples/k8s-sidecar/proxy_with_workload_identity.yaml delete mode 100644 examples/k8s-sidecar/service_account.yaml diff --git a/.build/alpine.yaml b/.build/alpine.yaml deleted file mode 100644 index e170df50cd..0000000000 --- a/.build/alpine.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -steps: -- name: 'gcr.io/cloud-builders/docker' - args: - - 'build' - - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - '-f=Dockerfile.alpine' - - '.' -images: - - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' - - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' \ No newline at end of file diff --git a/.build/buster.yaml b/.build/buster.yaml deleted file mode 100644 index 194e766f40..0000000000 --- a/.build/buster.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -steps: -- name: 'gcr.io/cloud-builders/docker' - args: - - 'build' - - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - '-f=Dockerfile.buster' - - '.' -images: - - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' - - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' \ No newline at end of file diff --git a/.build/default.yaml b/.build/default.yaml deleted file mode 100644 index 80b832bf85..0000000000 --- a/.build/default.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -steps: -- name: 'gcr.io/cloud-builders/docker' - args: - - 'build' - - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - '.' -images: - - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' - - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' \ No newline at end of file diff --git a/.build/gcs_upload.yaml b/.build/gcs_upload.yaml deleted file mode 100644 index d99e047d21..0000000000 --- a/.build/gcs_upload.yaml +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -timeout: 900s -options: - env: - - "GOPATH=/workspace/GOPATH" - - "CGO_ENABLED=0" - -steps: - - id: linux.amd64 - name: "golang:1.17" - env: - - "GOOS=linux" - - "GOARCH=amd64" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' - - id: linux.386 - name: "golang:1.17" - env: - - "GOOS=linux" - - "GOARCH=386" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' - - id: linux.arm64 - name: "golang:1.17" - env: - - "GOOS=linux" - - "GOARCH=arm64" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' - - id: linux.arm - name: "golang:1.17" - env: - - "GOOS=linux" - - "GOARCH=arm" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' - - id: darwin.amd64 - name: "golang:1.17" - env: - - "GOOS=darwin" - - "GOARCH=amd64" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' - - id: darwin.arm64 - name: "golang:1.17" - env: - - "GOOS=darwin" - - "GOARCH=arm64" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' - - id: windows.amd64 - name: "golang:1.17" - env: - - "GOOS=windows" - - "GOARCH=amd64" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy_x64.exe ./cmd/cloud_sql_proxy' - - id: windows.386 - name: "golang:1.17" - env: - - "GOOS=windows" - - "GOARCH=386" - entrypoint: "bash" - args: - - "-c" - - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy_x86.exe ./cmd/cloud_sql_proxy' -artifacts: - objects: - location: "gs://cloudsql-proxy/v${_VERSION}/" - paths: - - "cloud_sql_proxy*" diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index a555a7142d..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: Bug Report -about: Report defective or unintentional behavior you've experienced. -title: "Brief summary of what bug or error was observed" -labels: 'type: bug' - ---- - - - -## Bug Description - -Please enter a detailed description of the bug, and any information about what -behavior you noticed and how it differs from what you expected. - -## Example code (or command) - -``` -// example -``` - -## Stacktrace -``` -Any relevant stacktrace here. Be sure to filter sensitive information. -``` - -## How to reproduce - - 1. ? - 2. ? - -## Environment - -1. OS type and version: -2. Cloud SQL Proxy version (`./cloud_sql_proxy -version`): diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 94e6589ed0..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: -- name: Cloud SQL Issue tracker - url: https://issuetracker.google.com/savedsearches/559773 - about: Please use the Cloud SQL Issue tracker for problems with Cloud SQL itself. -- name: StackOverflow - url: https://stackoverflow.com/questions/tagged/google-cloud-sql - about: Please use the `google-cloud-sql` tag for questions on StackOverflow. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation-issue.md b/.github/ISSUE_TEMPLATE/documentation-issue.md deleted file mode 100644 index 3692027d2d..0000000000 --- a/.github/ISSUE_TEMPLATE/documentation-issue.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Documentation Issue -about: Report wrong or missing information with the documentation in the repo. -title: "Brief summary of what is missing or incorrect" -labels: 'type: docs' - ---- - - -## Description -Provide a short description of what is missing or incorrect, as well as a link to the specific location of the information. - -## Solution -What would you prefer the documentation say? Why would this information be more accurate or helpful? - -## Additional Context -Please reference any other relevant issues, PRs, descriptions, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 6849df4250..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for new or improved behavior. -title: "Brief summary of the proposed feature" -labels: 'type: feature request' - ---- - - -## Feature Description -A clear and concise description of what feature you would like to see, and why it would be useful to have added. - -## Alternatives Considered -Are there any workaround or third party tools to replicate this behavior? Why would adding this feature be preferred over them? - -## Additional Context -Please reference any other issues, PRs, descriptions, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 14f5e6bacd..0000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Question -about: Questions on how something works or the best way to do something. -title: "Breif summary of your question" -labels: 'type: question' - ---- - - - -## Question -What's your question? Please provide as much relevant information as possible -to reduce turnaround time. - -## Additional Context -Please reference any other relevant issues, PRs, descriptions, or screenshots here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 9208f5ba3d..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ -## Change Description - -Please provide a detailed description on what changes your PR will have. - - -## Checklist - -- [ ] Make sure to open an issue as a - [bug/issue](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/new/choose) - before writing your code! That way we can discuss the change, evaluate - designs, and agree on the general idea. -- [ ] Ensure the tests and linter pass -- [ ] Appropriate documentation is updated (if necessary) - -## Relevant issues: - -- Fixes # \ No newline at end of file diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml deleted file mode 100644 index de680bfe1e..0000000000 --- a/.github/blunderbuss.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -assign_issues: -# - shubha-rajan - - enocom -# - jackwotherspoon -# - kurtisvg - -assign_prs: -# - shubha-rajan - - enocom -# - jackwotherspoon -# - kurtisvg diff --git a/.github/header-checker-lint.yml b/.github/header-checker-lint.yml deleted file mode 100644 index ece9919898..0000000000 --- a/.github/header-checker-lint.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -allowedCopyrightHolders: - - 'Google LLC' -allowedLicenses: - - 'Apache-2.0' -sourceFileExtensions: - - 'go' - - 'yaml' - - 'yml' diff --git a/.github/release-please.yml b/.github/release-please.yml deleted file mode 100644 index 09418236db..0000000000 --- a/.github/release-please.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -handleGHRelease: true -packageName: cloud-sql-proxy -releaseType: simple -versionFile: 'cmd/version.txt' diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 9964bed603..0000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": [ - "config:base", - ":semanticCommitTypeAll(chore)" - ], - "ignorePresets": [":semanticPrefixFixDepsChoreOthers"], - "prConcurrentLimit": 0, - "rebaseStalePrs": true, - "dependencyDashboard": true, - "semanticCommits": true, - "postUpdateOptions": [ - "gomodTidy" - ], - "ignoreDeps": [ - "golang.org/x/net" - ], - "timezone": "America/Los_Angeles", - "schedule": [ - "after 8am on Friday", - "before 12pm on Friday" - ], - "force": { - "constraints": { - "go": "1.16" - } - } -} diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml deleted file mode 100644 index f2528bce6b..0000000000 --- a/.github/workflows/coverage.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: code coverage -on: [pull_request] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: "1.17" - - - name: Checkout base branch - uses: actions/checkout@v3 - with: - ref: ${{ github.base_ref }} - - name: Calculate base code coverage - run: | - go test -short -coverprofile current_cover.out ./... || true - export CUR_COVER=$(go tool cover -func current_cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') - echo "CUR_COVER=$CUR_COVER" >> $GITHUB_ENV - - - name: Checkout PR branch - uses: actions/checkout@v3 - - name: Calculate PR code coverage - run: | - go test -short -coverprofile pr_cover.out ./... || true - export PR_COVER=$(go tool cover -func pr_cover.out | grep total | awk '{print substr($3, 1, length($3)-1)}') - echo "PR_COVER=$PR_COVER" >> $GITHUB_ENV - - - name: Verify code coverage. If your reading this and the step has failed, please add tests to cover your changes. - run: | - go tool cover -func pr_cover.out - if [ "${{ env.PR_COVER }}" -lt "${{ env.CUR_COVER }}" ]; then - exit 1; - fi diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 094de1a17f..0000000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: tests -on: [pull_request] - -jobs: - lint: - name: lint - runs-on: ubuntu-latest - steps: - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: '1.17' - - name: Install goimports - run: go get golang.org/x/tools/cmd/goimports - - name: Checkout code - uses: actions/checkout@v3 - - run: goimports -w . - - run: go mod tidy - - name: Verify no changes from goimports and go mod tidy. If you're reading this and the check has failed, run `goimports -w . && go mod tidy`. - run: git diff --exit-code diff --git a/.kokoro/go116/linux/common.cfg b/.kokoro/go116/linux/common.cfg deleted file mode 100644 index fb89301c09..0000000000 --- a/.kokoro/go116/linux/common.cfg +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto - -# Get secrets for tests. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" - -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/go116:latest" -} - -# Tell the trampoline which tests to run. -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" -} diff --git a/.kokoro/go116/linux/continuous.cfg b/.kokoro/go116/linux/continuous.cfg deleted file mode 100644 index da14d0b262..0000000000 --- a/.kokoro/go116/linux/continuous.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/linux/periodic.cfg b/.kokoro/go116/linux/periodic.cfg deleted file mode 100644 index da14d0b262..0000000000 --- a/.kokoro/go116/linux/periodic.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/linux/presubmit.cfg b/.kokoro/go116/linux/presubmit.cfg deleted file mode 100644 index da14d0b262..0000000000 --- a/.kokoro/go116/linux/presubmit.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/windows/common.cfg b/.kokoro/go116/windows/common.cfg deleted file mode 100644 index 1d832aea09..0000000000 --- a/.kokoro/go116/windows/common.cfg +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto - -# Get secrets for tests. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" -build_file: "cloud-sql-proxy/.kokoro/tests/run_tests.bat" - diff --git a/.kokoro/go116/windows/continuous.cfg b/.kokoro/go116/windows/continuous.cfg deleted file mode 100644 index 5306d19b51..0000000000 --- a/.kokoro/go116/windows/continuous.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/windows/periodic.cfg b/.kokoro/go116/windows/periodic.cfg deleted file mode 100644 index 5306d19b51..0000000000 --- a/.kokoro/go116/windows/periodic.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go116/windows/presubmit.cfg b/.kokoro/go116/windows/presubmit.cfg deleted file mode 100644 index 5306d19b51..0000000000 --- a/.kokoro/go116/windows/presubmit.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go117/linux/common.cfg b/.kokoro/go117/linux/common.cfg deleted file mode 100644 index eaa711dc96..0000000000 --- a/.kokoro/go117/linux/common.cfg +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto - -# Get secrets for tests. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" - -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/go117:latest" -} - -# Tell the trampoline which tests to run. -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" -} diff --git a/.kokoro/go117/linux/continuous.cfg b/.kokoro/go117/linux/continuous.cfg deleted file mode 100644 index 5306d19b51..0000000000 --- a/.kokoro/go117/linux/continuous.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go117/linux/periodic.cfg b/.kokoro/go117/linux/periodic.cfg deleted file mode 100644 index 5306d19b51..0000000000 --- a/.kokoro/go117/linux/periodic.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go117/linux/presubmit.cfg b/.kokoro/go117/linux/presubmit.cfg deleted file mode 100644 index 5306d19b51..0000000000 --- a/.kokoro/go117/linux/presubmit.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/linux/common.cfg b/.kokoro/go118/linux/common.cfg deleted file mode 100644 index f2140104ac..0000000000 --- a/.kokoro/go118/linux/common.cfg +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto - -# Get secrets for tests. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" - -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/go118:latest" -} - -# Tell the trampoline which tests to run. -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" -} diff --git a/.kokoro/go118/linux/continuous.cfg b/.kokoro/go118/linux/continuous.cfg deleted file mode 100644 index a1d3379d4b..0000000000 --- a/.kokoro/go118/linux/continuous.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/linux/periodic.cfg b/.kokoro/go118/linux/periodic.cfg deleted file mode 100644 index a1d3379d4b..0000000000 --- a/.kokoro/go118/linux/periodic.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/linux/presubmit.cfg b/.kokoro/go118/linux/presubmit.cfg deleted file mode 100644 index a1d3379d4b..0000000000 --- a/.kokoro/go118/linux/presubmit.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/macos/common.cfg b/.kokoro/go118/macos/common.cfg deleted file mode 100644 index 2c95194994..0000000000 --- a/.kokoro/go118/macos/common.cfg +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto - -# Get secrets for tests. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" - -# Use the trampoline script to run in docker. -build_file: "cloud-sql-proxy/.kokoro/tests/run_tests_macos.sh" diff --git a/.kokoro/go118/macos/continuous.cfg b/.kokoro/go118/macos/continuous.cfg deleted file mode 100644 index a1d3379d4b..0000000000 --- a/.kokoro/go118/macos/continuous.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/macos/periodic.cfg b/.kokoro/go118/macos/periodic.cfg deleted file mode 100644 index a1d3379d4b..0000000000 --- a/.kokoro/go118/macos/periodic.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/go118/macos/presubmit.cfg b/.kokoro/go118/macos/presubmit.cfg deleted file mode 100644 index a1d3379d4b..0000000000 --- a/.kokoro/go118/macos/presubmit.cfg +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2022 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Format: //devtools/kokoro/config/proto/build.proto diff --git a/.kokoro/release_artifacts.sh b/.kokoro/release_artifacts.sh deleted file mode 100755 index 8ea59a8418..0000000000 --- a/.kokoro/release_artifacts.sh +++ /dev/null @@ -1,56 +0,0 @@ -#! /bin/bash -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script distributes the artifacts for the Cloud SQL proxy to their different channels. - -set -e # exit immediatly if any step fails - -PROJ_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. >/dev/null 2>&1 && pwd )" -cd $PROJ_ROOT - -# get the current version -export VERSION=$(cat version.txt) -if [ -z "$VERSION" ]; then - echo "error: No version.txt found in $PROJ_ROOT" - exit 1 -fi - - -read -p "This will release new Cloud SQL proxy artifacts for \"$VERSION\", even if they already exist. Are you sure (y/Y)? " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]] -then - exit 1 -fi - -# Build and push the container images -gcloud builds submit --async --config .build/default.yaml --substitutions _VERSION=$VERSION -gcloud builds submit --async --config .build/buster.yaml --substitutions _VERSION=$VERSION -gcloud builds submit --async --config .build/alpine.yaml --substitutions _VERSION=$VERSION - -# Build the binarys and upload to GCS -gcloud builds submit --config .build/gcs_upload.yaml --substitutions _VERSION=$VERSION -# cleam up any artifacts.json left by previous builds -gsutil rm -f gs://cloudsql-proxy/v$VERSION/*.json 2> /dev/null || true - -# Generate sha256 hashes for authentication -echo -e "Add the following table to the release notes on GitHub: \n\n" -echo "| filename | sha256 hash |" -echo "|----------|-------------|" -for f in $(gsutil ls "gs://cloudsql-proxy/v$VERSION/cloud_sql_proxy*"); do - file=$(basename $f) - sha=$(gsutil cat $f | sha256sum --binary | head -c 64) - echo "| [$file](https://storage.googleapis.com/cloudsql-proxy/v$VERSION/$file) | $sha |" -done diff --git a/.kokoro/tag_latest.sh b/.kokoro/tag_latest.sh deleted file mode 100755 index 0edf7c4a25..0000000000 --- a/.kokoro/tag_latest.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# This script finds all container images with the provided version tag and adds -# the "latest" tag to them. -# -# For example: -# 1. Add a "latest" tag to the v1.23.1 images -# -# ./tag_latest 1.23.1 -# -# 2. Print out the gcloud commands without running them: -# -# ./tag_latest 1.23.1 -dry-run -# - -if [ "$1" = "" ] -then - echo "Usage: $0 [-dry-run]" - exit 1 -fi - -dry_run=false -if [ "$2" = "-dry-run" ] -then - dry_run=true -fi - -tag_latest() { - local new_version=$1 - for registry in "gcr.io" "us.gcr.io" "eu.gcr.io" "asia.gcr.io" - do - local base_image="$registry/cloudsql-docker/gce-proxy" - if [ "$dry_run" != true ] - then - gcloud container images add-tag "$base_image:$new_version" "$base_image:latest" - else - echo [DRY RUN] gcloud container images add-tag "$base_image:$new_version" "$base_image:latest" - fi - done -} - -tag_latest "$1" diff --git a/.kokoro/tests/run_tests.bat b/.kokoro/tests/run_tests.bat deleted file mode 100644 index b0252d8b23..0000000000 --- a/.kokoro/tests/run_tests.bat +++ /dev/null @@ -1 +0,0 @@ -"C:\Program Files\Git\bin\bash.exe" github/cloud-sql-proxy/.kokoro/tests/run_tests_windows.sh diff --git a/.kokoro/tests/run_tests.sh b/.kokoro/tests/run_tests.sh deleted file mode 100755 index 5525bc7c93..0000000000 --- a/.kokoro/tests/run_tests.sh +++ /dev/null @@ -1,34 +0,0 @@ -#! /bin/bash -# Copyright 2020 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# `-e` enables the script to automatically fail when a command fails -set -e - -export GO111MODULE=on - -# Kokoro setup -if [ -n "$KOKORO_GFILE_DIR" ]; then - # Move into project directory - cd github/cloud-sql-proxy - # install fuse project - apt-get -qq update && apt-get -qq install fuse -y - # source secrets - source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" - export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" -fi - -echo -e "******************** Running tests... ********************\n" -go test -race -v ./... -echo -e "******************** Tests complete. ********************\n" diff --git a/.kokoro/tests/run_tests_macos.sh b/.kokoro/tests/run_tests_macos.sh deleted file mode 100644 index d669c5f8eb..0000000000 --- a/.kokoro/tests/run_tests_macos.sh +++ /dev/null @@ -1,41 +0,0 @@ -#! /bin/bash -# Copyright 2020 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# `-e` enables the script to automatically fail when a command fails -set -e - -export GO111MODULE=on - -# kokoro setup -if [ -n "$KOKORO_GFILE_DIR" ]; then - # move into project directory - cd github/cloud-sql-proxy - # install fuse project - brew update > /dev/null - brew install --cask --quiet osxfuse - # install go version - brew install go@1.18 - echo -e "******************** Printing Go version... ********************\n" - echo `go version` - # source secrets - source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" - export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" -fi - -# On macOS, the default $TMPDIR is too long for suitable use due to the unix socket length limits -export TMPDIR="/tmp" -echo -e "******************** Running tests... ********************\n" -go test -race -v ./... -echo -e "******************** Tests complete. ********************\n" diff --git a/.kokoro/tests/run_tests_windows.sh b/.kokoro/tests/run_tests_windows.sh deleted file mode 100755 index 42ada2fc3a..0000000000 --- a/.kokoro/tests/run_tests_windows.sh +++ /dev/null @@ -1,34 +0,0 @@ -#! /bin/bash -# Copyright 2021 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# `-e` enables the script to automatically fail when a command fails -set -e - -export GO111MODULE=on -export PATH=/c/Go/bin:$PATH -export GOPATH=/c/Go - -# Kokoro setup -if [ -n "$KOKORO_GFILE_DIR" ]; then - # Move into project directory - cd github/cloud-sql-proxy - # source secrets - source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" - export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" -fi - -echo -e "******************** Running tests... ********************\n" -go test -v ./... -echo -e "******************** Tests complete. ********************\n" diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh deleted file mode 100644 index f0e0070c07..0000000000 --- a/.kokoro/trampoline.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# Copyright 2019 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" \ No newline at end of file diff --git a/examples/k8s-health-check/README.md b/examples/k8s-health-check/README.md deleted file mode 100644 index 2508f009f8..0000000000 --- a/examples/k8s-health-check/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Cloud SQL proxy health checks - -Kubernetes supports three types of health checks. -1. Startup probes determine whether a container is done starting up. As soon as this probe succeeds, Kubernetes switches over to using liveness and readiness probing. -2. Liveness probes determine whether a container is healthy. When this probe is unsuccessful, the container is restarted. -3. Readiness probes determine whether a container can serve new traffic. When this probe fails, Kubernetes will wait to send requests to the container. - -## Running Cloud SQL proxy with health checks in Kubernetes -1. Configure your Cloud SQL proxy container to include health check probes. - > [proxy_with_http_health_check.yaml](proxy_with_http_health_check.yaml#L77-L111) - ```yaml - # Recommended configurations for health check probes. - # Probe parameters can be adjusted to best fit the requirements of your application. - # For details, see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ - livenessProbe: - httpGet: - path: /liveness - port: 8090 - # Number of seconds after the container has started before the first probe is scheduled. Defaults to 0. - # Not necessary when the startup probe is in use. - initialDelaySeconds: 0 - # Frequency of the probe. Defaults to 10. - periodSeconds: 10 - # Number of seconds after which the probe times out. Defaults to 1. - timeoutSeconds: 5 - # Number of times the probe is allowed to fail before the transition from healthy to failure state. - # Defaults to 3. - failureThreshold: 1 - readinessProbe: - httpGet: - path: /readiness - port: 8090 - initialDelaySeconds: 0 - periodSeconds: 10 - timeoutSeconds: 5 - # Number of times the probe must report success to transition from failure to healthy state. - # Defaults to 1 for readiness probe. - successThreshold: 1 - failureThreshold: 1 - startupProbe: - httpGet: - path: /startup - port: 8090 - periodSeconds: 1 - timeoutSeconds: 5 - failureThreshold: 20 - ``` - -2. Add `-use_http_health_check` and `-health-check-port` (optional) to your proxy container configuration under `command: `. - > [proxy_with_http_health_check.yaml](proxy_with_http_health_check.yaml#L39-L55) - ```yaml - command: - - "/cloud_sql_proxy" - - # If connecting from a VPC-native GKE cluster, you can use the - # following flag to have the proxy connect over private IP - # - "-ip_address_types=PRIVATE" - - # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" - # Enables HTTP health checks. - - "-use_http_health_check" - # Specifies the health check server port. - # Defaults to 8090. - - "-health_check_port=" - # This flag specifies where the service account key can be found - - "-credential_file=/secrets/service_account.json" - ``` - diff --git a/examples/k8s-health-check/proxy_with_http_health_check.yaml b/examples/k8s-health-check/proxy_with_http_health_check.yaml deleted file mode 100644 index d8ff78ad50..0000000000 --- a/examples/k8s-health-check/proxy_with_http_health_check.yaml +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# You must configure probes in your deployment to use health checks in Kubernetes. -# This sample configuration for HTTP probes is adapted from proxy_with_workload_identity.yaml. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - containers: - - name: - # ... other container configuration - env: - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - - name: cloud-sql-proxy - # It is recommended to use the latest version of the Cloud SQL proxy - # Make sure to update on a regular schedule! - image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version - command: - - "/cloud_sql_proxy" - - # If connecting from a VPC-native GKE cluster, you can use the - # following flag to have the proxy connect over private IP - # - "-ip_address_types=PRIVATE" - - # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" - # Enables HTTP health checks. - - "-use_http_health_check" - # Specifies the health check server port. - # Defaults to 8090. - - "-health_check_port=" - # This flag specifies where the service account key can be found - - "-credential_file=/secrets/service_account.json" - securityContext: - # The default Cloud SQL proxy image runs as the - # "nonroot" user and group (uid: 65532) by default. - runAsNonRoot: true - volumeMounts: - - name: - mountPath: /secrets/ - readOnly: true - # Resource configuration depends on an application's requirements. You - # should adjust the following values based on what your application - # needs. For details, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - resources: - requests: - # The proxy's memory use scales linearly with the number of active - # connections. Fewer open connections will use less memory. Adjust - # this value based on your application's requirements. - memory: "2Gi" - # The proxy's CPU use scales linearly with the amount of IO between - # the database and the application. Adjust this value based on your - # application's requirements. - cpu: "1" - # Recommended configurations for health check probes. - # Probe parameters can be adjusted to best fit the requirements of your application. - # For details, see https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ - livenessProbe: - httpGet: - path: /liveness - port: 8090 - # Number of seconds after the container has started before the first probe is scheduled. Defaults to 0. - # Not necessary when the startup probe is in use. - initialDelaySeconds: 0 - # Frequency of the probe. - periodSeconds: 60 - # Number of seconds after which the probe times out. - timeoutSeconds: 30 - # Number of times the probe is allowed to fail before the transition - # from healthy to failure state. - # - # If periodSeconds = 60, 5 tries will result in five minutes of - # checks. The proxy starts to refresh a certificate five minutes - # before its expiration. If those five minutes lapse without a - # successful refresh, the liveness probe will fail and the pod will be - # restarted. - failureThreshold: 5 - readinessProbe: - httpGet: - path: /readiness - port: 8090 - initialDelaySeconds: 0 - periodSeconds: 10 - timeoutSeconds: 5 - # Number of times the probe must report success to transition from failure to healthy state. - # Defaults to 1 for readiness probe. - successThreshold: 1 - failureThreshold: 1 - startupProbe: - httpGet: - path: /startup - port: 8090 - periodSeconds: 1 - timeoutSeconds: 5 - failureThreshold: 20 - volumes: - - name: - secret: - secretName: diff --git a/examples/k8s-service/README.md b/examples/k8s-service/README.md deleted file mode 100644 index 525de08d6f..0000000000 --- a/examples/k8s-service/README.md +++ /dev/null @@ -1,277 +0,0 @@ -# Running the Cloud SQL Proxy as a Service - -This example demonstrates how to run the Cloud SQL Auth Proxy with PgBouncer on -Kubernetes as a service. It assumes you have already successfully completed all -the steps in [Using the Cloud SQL Auth Proxy on Kubernetes][sidecar]. - -In this example, you will deploy [PgBouncer][] with the Cloud SQL Auth Proxy as -a sidecar, in addition to configuring encryption between the application and -PgBouncer. - -## A Word of Warning - -Running PgBouncer with the Cloud SQL Auth Proxy may pose a significant -operational burden and should be undertaken with caution given the attendant -complexity. - -In general, we recommend [running the proxy as a sidecar][sidecar] to your -application because it is simple, there is less overhead, it is secure out of -the box, and there is less latency involved. - -However, the service pattern is useful when you are at very large scale, when -you clearly need a database connection pooler, and when you are running into SQL -Admin API quota problems. - -## Initial Setup - -Before we deploy PgBouncer with the Cloud SQL Auth Proxy, there are three -initial steps to take. - -### Generate Certificates for PgBouncer - -First, you will need to generate certificates to encrypt the connection between -the application and PgBouncer. We recommend using [CFSSL][] to handle -certificate generation. Note: this example uses self-signed certificates. In -some cases, using a certificate signed by a public certificate authority may be -preferred. Alternatively, Kubernetes includes [an API for issuing -certificates][k8s-tls]. See the documentation on -[certificates][certificate-docs] for more details. - -The certificate signing request is encoded as JSON in -[`ca_csr.json`](ca_csr.json) for the certificate authority and in -[`server_csr.json`](server_csr.json) for the "server," here PgBouncer. - -First, we initialize our certificate authority. - -``` shell -# This step produces ca-key.pem (the CA private key) -# and ca.pem (the CA certificate). -cfssl genkey -initca ca_csr.json | cfssljson -bare ca -``` - -Next, we generate a public and private key for the server. These will be what -we will use to encrypt traffic from the application to PgBouncer. - -``` shell -# This step produces server-key.pem (the server private key) -# and server.pem (the server certicate). -cfssl gencert -ca cert -ca-key key server_csr.json | cfssljson -bare server -``` - -### Save the certificates as secrets - -Second, with all the necessary certificates generated, we will save them as -secrets: - -``` shell -# First the CA cert -kubectl create secret tls --key="ca-key.pem" --cert="ca.pem" - -# Next the server cert -kubectl create secret tls --key="server-key.pem" \ - --cert="server.pem" -``` - -### Containerize PgBouncer - -Third, we will containerize PgBouncer. Some users may prefer to containerize -PgBouncer themselves. For this example, we will make use of an open source -container, [edoburu/pgbouncer][edoburu]. One nice benefit of `edoburu/pgbouncer` -is that it will generate all the PgBouncer configuration based on environment -variables passed to the container. - -## Deploy PgBouncer as a Service - -With PgBouncer containerized, we will now create a deployment with PgBouncer and -the proxy as a sidecar. - -First, we mount our CA certificate and server certificate and private key, -renaming the certificate secrets to `cert.pem` and server private key to -`key.pem`: - -> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L15-L29) - -``` yaml -volumes: -- name: cacert - secret: - secretName: - items: - - key: tls.crt - path: cert.pem -- name: servercert - secret: - secretName: - items: - - key: tls.crt - path: cert.pem - - key: tls.key - path: key.pem -``` - -Next, we specify volume mounts in our PgBouncer container where the secrets will -be stored: - -> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L31-L41) - -``` yaml -- name: pgbouncer - image: - ports: - - containerPort: 5432 - volumeMounts: - - name: cacert - mountPath: "/etc/ca" - readOnly: true - - name: servercert - mountPath: "/etc/server" - readOnly: true -``` - -Then we configure PgBouncer through environment variables. Note: we use 5431 for -`DB_PORT` to leave 5432 available. - -> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L42-L69) - -``` yaml -env: -- name: DB_HOST - value: "127.0.0.1" -- name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username -- name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: - key: password -- name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database -- name: DB_PORT - value: "5431" -- name: CLIENT_TLS_SSLMODE - value: "require" -- name: CLIENT_TLS_CA_FILE - value: "/etc/ca/cert.pem" -- name: CLIENT_TLS_KEY_FILE - value: "/etc/server/key.pem" -- name: CLIENT_TLS_CERT_FILE - value: "/etc/server/cert.pem" -``` - -For the PgBouncer deployment, we add the proxy as a sidecar, starting it on port -5431: - -> [`pgbouncer_deployment.yaml`](pgbouncer_deployment.yaml#L70-L76) - -``` yaml -- name: cloud-sql-proxy - image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version - command: - - "/cloud_sql_proxy" - - "-instances==tcp:5431" - securityContext: - runAsNonRoot: true -``` - -Next, we create a PgBouncer service, listening on port 5342: - -> [`pgbouncer_service.yaml`](pgbouncer_service.yaml#L1-L11) - -``` yaml -apiVersion: v1 -kind: Service -metadata: - name: -spec: - selector: - app: - ports: - - protocol: TCP - port: 5432 - targetPort: 5432 -``` - -With the PgBouncer service and deployment done, we are ready to point our -application at it. - -## Configure your application - -First, we configure a volume for the CA certificate, mapping the file name to -`cert.pem`. - -> [`deployment.yaml`](deployment.yaml#L1-L11) - -``` yaml -volumes: -- name: cacert - secret: - secretName: - items: - - key: tls.crt - path: cert.pem -``` - -Next, we mount the volume within the application container: - -> [`deployment.yaml`](deployment.yaml#L28-L31) - -``` yaml -volumeMounts: -- name: cacert - mountPath: "/etc/ca" - readOnly: true -``` - -Then, we configure environment variables for connecting to the database, this -time including a `CA_CERT`: - -> [`deployment.yaml`](deployment.yaml#L32-L53) - -``` yaml -env: -- name: DB_HOST - value: ".default.svc.cluster.local" # using the "default" namespace -- name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username -- name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password -- name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database -- name: DB_PORT - value: "5432" -- name: CA_CERT - value: "/etc/ca/cert.pem" -``` - -Note: now the `DB_HOST` value uses an internal DNS record pointing at the -PgBouncer service. - -Finally, when configuring a database connection string, the application must -provide the additional properties: - -1. `sslmode` must be set to at least `verify-ca` -1. `sslrootcert` must set to the environment variable `CA_CERT` - - -[certificate-docs]: https://kubernetes.io/docs/tasks/administer-cluster/certificates/ -[CFSSL]: https://github.com/cloudflare/cfssl -[edoburu]: https://hub.docker.com/r/edoburu/pgbouncer -[sidecar]: ../k8s-sidecar/README.md -[k8s-tls]: https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ -[PgBouncer]: https://www.pgbouncer.org - diff --git a/examples/k8s-service/ca_csr.json b/examples/k8s-service/ca_csr.json deleted file mode 100644 index 9bb69a34d6..0000000000 --- a/examples/k8s-service/ca_csr.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hosts": [], - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "Boulder", - "O": "My Cool Self-Signing Certificate Authority", - "OU": "WWW", - "ST": "Colorado" - } - ] -} diff --git a/examples/k8s-service/deployment.yaml b/examples/k8s-service/deployment.yaml deleted file mode 100644 index c09c1532f4..0000000000 --- a/examples/k8s-service/deployment.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: -spec: - replicas: 5 - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - serviceAccountName: - volumes: - - name: cacert - secret: - secretName: - items: - - key: tls.crt - path: cert.pem - containers: - - name: - image: - ports: - - containerPort: 8080 - volumeMounts: - - name: cacert - mountPath: "/etc/ca" - readOnly: true - env: - - name: DB_HOST - value: ".default.svc.cluster.local" # using the "default" namespace - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - - name: DB_PORT - value: "5432" - - name: CA_CERT - value: "/etc/ca/cert.pem" diff --git a/examples/k8s-service/pgbouncer_deployment.yaml b/examples/k8s-service/pgbouncer_deployment.yaml deleted file mode 100644 index 5490ea6004..0000000000 --- a/examples/k8s-service/pgbouncer_deployment.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - serviceAccountName: - volumes: - - name: cacert - secret: - secretName: - items: - - key: tls.crt - path: cert.pem - - name: servercert - secret: - secretName: - items: - - key: tls.crt - path: cert.pem - - key: tls.key - path: key.pem - containers: - - name: pgbouncer - image: - ports: - - containerPort: 5432 - volumeMounts: - - name: cacert - mountPath: "/etc/ca" - readOnly: true - - name: servercert - mountPath: "/etc/server" - readOnly: true - env: - - name: DB_HOST - value: "127.0.0.1" - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASSWORD - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - - name: DB_PORT - value: "5431" - - name: CLIENT_TLS_SSLMODE - value: "require" - - name: CLIENT_TLS_CA_FILE - value: "/etc/ca/cert.pem" - - name: CLIENT_TLS_KEY_FILE - value: "/etc/server/key.pem" - - name: CLIENT_TLS_CERT_FILE - value: "/etc/server/cert.pem" - - name: cloud-sql-proxy - image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure to use the latest version - command: - - "/cloud_sql_proxy" - - "-instances==tcp:5431" - securityContext: - runAsNonRoot: true diff --git a/examples/k8s-service/pgbouncer_service.yaml b/examples/k8s-service/pgbouncer_service.yaml deleted file mode 100644 index 429a1ccaab..0000000000 --- a/examples/k8s-service/pgbouncer_service.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Service -metadata: - name: -spec: - selector: - app: - ports: - - protocol: TCP - port: 5432 - targetPort: 5432 diff --git a/examples/k8s-service/server_csr.json b/examples/k8s-service/server_csr.json deleted file mode 100644 index 5f6a4735f9..0000000000 --- a/examples/k8s-service/server_csr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "hosts": [ - "pgbouncersvc.default.svc.cluster.local", - "localhost" - ], - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "Boulder", - "O": "My Cool Kubernetes Cluster", - "OU": "WWW", - "ST": "Colorado" - } - ] -} diff --git a/examples/k8s-sidecar/README.md b/examples/k8s-sidecar/README.md deleted file mode 100644 index faed917539..0000000000 --- a/examples/k8s-sidecar/README.md +++ /dev/null @@ -1,258 +0,0 @@ -# Using the Cloud SQL proxy on Kubernetes - -The Cloud SQL proxy is the recommended way to connect to Cloud SQL, even when -using private IP. This is because the proxy provides strong encryption and -authentication using IAM, which help keep your database secure. - -## Configure your application with Secrets - -In Kubernetes, [Secrets][ksa-secret] are a secure way to pass configuration -details to your application. Each Secret object can contain multiple key/value -pairs that can be pass to your application in multiple ways. When connecting to -a database, you can create a Secret with details such as your database name, -user, and password which can be injected into your application as env vars. - -1. Create a secret with information needed to access your database: - ```shell - kubectl create secret generic \ - --from-literal=username= \ - --from-literal=password= \ - --from-literal=database= - ``` -2. Next, configure your application's container to mount the secrets as env - vars: - > [proxy_with_workload_identity.yaml](proxy_with_workload_identity.yaml#L21-L36) - ```yaml - env: - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - ``` -3. Finally, configure your application to use these values. In the example -above, the values will be in the env vars `DB_USER`, `DB_PASS`, and `DB_NAME`. - -[ksa-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ - -## Setting up a service account - -The first step to running the Cloud SQL proxy in Kubernetes is creating a -service account to represent your application. It is recommended that you create -a service account unique to each application, instead of using the same service -account everywhere. This model is more secure since it allows your to limit -permissions on a per-application basis. - -The service account for your application needs to meet the following criteria: - -1. Belong to a project with the [Cloud SQL Admin API][admin-api] enabled -1. [Has been granted][grant-sa] the - [`Cloud SQL Client` IAM role (or equivalent)][csql-roles] - for the project containing the instance you want to connect to -1. If connecting using private IP, you must use a - [VPC-native GKE cluster][vpc-gke], in the same VPC as your Cloud SQL instance - -[admin-api]: https://console.cloud.google.com/flows/enableapi?apiid=sqladmin&redirect=https://console.cloud.google.com -[grant-sa]: https://cloud.google.com/iam/docs/granting-roles-to-service-accounts -[csql-roles]: https://cloud.google.com/iam/docs/understanding-roles#cloud-sql-roles -[vpc-gke]: https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips - -## Providing the service account to the proxy - -Next, you need to configure Kubernetes to provide the service account to the -Cloud SQL Auth proxy. There are two recommended ways to do this. - -### Workload Identity - -If you are using [Google Kubernetes Engine][gke], the preferred method is to -use GKE's [Workload Identity][workload-id] feature. This method allows you to -bind a [Kubernetes Service Account (KSA)][ksa] to a Google Service Account -(GSA). The GSA will then be accessible to applications using the matching KSA. - -1. [Enable Workload Identity for your cluster][enable-wi] -1. [Enable Workload Identity for your node pool][enable-wi-node-pool] -1. Create a KSA for your application `kubectl apply -f service-account.yaml`: - - > [service-account.yaml](service_account.yaml#L2-L5) - ```yaml - apiVersion: v1 - kind: ServiceAccount - metadata: - name: # TODO(developer): replace these values - ``` -1. Enable the IAM binding between your `` and ``: - ```sh - gcloud iam service-accounts add-iam-policy-binding \ - --role roles/iam.workloadIdentityUser \ - --member "serviceAccount:.svc.id.goog[/]" \ - @.iam.gserviceaccount.com - ``` -1. Add an annotation to `` to complete the binding: - ```sh - kubectl annotate serviceaccount \ - \ - iam.gke.io/gcp-service-account=@.iam.gserviceaccount.com - ``` -1. Finally, make sure to specify the service account for the k8s pod spec: - > [proxy_with_workload_identity.yaml](proxy_with_workload_identity.yaml#L2-L15) - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: - spec: - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - serviceAccountName: - ``` - -[gke]: https://cloud.google.com/kubernetes-engine -[workload-id]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity -[ksa]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ -[enable-wi]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable_on_existing_cluster -[enable-wi-node-pool]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#option_2_node_pool_modification - -### Service account key file - -Alternatively, if your can't use Workload Identity, the recommended pattern is -to mount a service account key file into the Cloud SQL proxy pod and use the -`-credential_file` flag. - -1. Create a credential file for your service account key: - ```sh - gcloud iam service-accounts keys create ~/key.json \ - --iam-account @project-id.iam.gserviceaccount.com - ``` -1. Turn your service account key into a k8s [Secret][k8s-secret]: - ```shell - kubectl create secret generic \ - --from-file=service_account.json=~/key.json - ``` -3. Mount the secret as a volume under the`spec:` for your k8s object: - > [proxy_with_sa_key.yaml](proxy_with_sa_key.yaml#L74-L77) - ```yaml - volumes: - - name: - secret: - secretName: - ``` - -4. Follow the instructions in the next section to access the volume from the - proxy's pod. - -[k8s-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ - -## Run the Cloud SQL proxy as a sidecar - -We recommend running the proxy in a "sidecar" pattern (as an additional -container sharing a pod with your application). We recommend this over running -as a separate service for several reasons: - -* Prevents your SQL traffic from being exposed locally - the proxy provides - encryption on outgoing connections, but you should limit exposure for - incoming connections -* Prevents a single point of failure - each application's access to - your database is independent from the others, making it more resilient. -* Limits access to the proxy, allowing you to use IAM permissions per - application rather than exposing the database to the entire cluster -* Allows you to scope resource requests more accurately - because the - proxy consumes resources linearly to usage, this pattern allows you to more - accurately scope and request resources to match your applications as it - scales - -1. Add the Cloud SQL proxy to the pod configuration under `containers`: - > [proxy_with_workload-identity.yaml](proxy_with_workload_identity.yaml#L39-L69) - ```yaml - - name: cloud-sql-proxy - # It is recommended to use the latest version of the Cloud SQL proxy - # Make sure to update on a regular schedule! - image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version - command: - - "/cloud_sql_proxy" - - # If connecting from a VPC-native GKE cluster, you can use the - # following flag to have the proxy connect over private IP - # - "-ip_address_types=PRIVATE" - - # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" - securityContext: - # The default Cloud SQL proxy image runs as the - # "nonroot" user and group (uid: 65532) by default. - runAsNonRoot: true - # Resource configuration depends on an application's requirements. You - # should adjust the following values based on what your application - # needs. For details, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - resources: - requests: - # The proxy's memory use scales linearly with the number of active - # connections. Fewer open connections will use less memory. Adjust - # this value based on your application's requirements. - memory: "2Gi" - # The proxy's CPU use scales linearly with the amount of IO between - # the database and the application. Adjust this value based on your - # application's requirements. - cpu: "1" - ``` - If you are using a service account key, specify your secret volume and add - the `-credential_file` flag to the command: - - > [proxy_with_sa_key.yaml](proxy_with_sa_key.yaml#L49-L58) - ```yaml - # This flag specifies where the service account key can be found - - "-credential_file=/secrets/service_account.json" - securityContext: - # The default Cloud SQL proxy image runs as the - # "nonroot" user and group (uid: 65532) by default. - runAsNonRoot: true - volumeMounts: - - name: - mountPath: /secrets/ - readOnly: true - ``` - -1. Finally, configure your application to connect via `127.0.0.1` on whichever - `` you specified in the command section. - - -## Connecting without the Cloud SQL proxy - -While not as secure, it is possible to connect from a VPC-native GKE cluster to -a Cloud SQL instance on the same VPC using private IP without the proxy. - -1. Create a secret with your instance's private IP address: - ```shell - kubectl create secret generic \ - --from-literal=db_host= - ``` - -2. Next make sure you add the secret to your application's container: - > [no_proxy_private_ip.yaml](no_proxy_private_ip.yaml#L34-L38) - ```yaml - - name: DB_HOST - valueFrom: - secretKeyRef: - name: - key: db_host - ``` - -3. Finally, configure your application to connect using the IP address from the - `DB_HOST` env var. You will need to use the correct port for your db-engine - (MySQL: `3306`, Postgres: `5432`, SQLServer: `1433`). diff --git a/examples/k8s-sidecar/no_proxy_private_ip.yaml b/examples/k8s-sidecar/no_proxy_private_ip.yaml deleted file mode 100644 index 3a18438b9f..0000000000 --- a/examples/k8s-sidecar/no_proxy_private_ip.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - containers: - - name: - # ... other container configuration - env: - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - # [START cloud_sql_proxy_secret_host] - - name: DB_HOST - valueFrom: - secretKeyRef: - name: - key: db_host - # [END cloud_sql_proxy_secret_host] diff --git a/examples/k8s-sidecar/proxy_with_sa_key.yaml b/examples/k8s-sidecar/proxy_with_sa_key.yaml deleted file mode 100644 index fd71ad05ca..0000000000 --- a/examples/k8s-sidecar/proxy_with_sa_key.yaml +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - containers: - - name: - # ... other container configuration - env: - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - - name: cloud-sql-proxy - # It is recommended to use the latest version of the Cloud SQL proxy - # Make sure to update on a regular schedule! - image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version - command: - - "/cloud_sql_proxy" - - # If connecting from a VPC-native GKE cluster, you can use the - # following flag to have the proxy connect over private IP - # - "-ip_address_types=PRIVATE" - - # By default, the proxy will write all logs to stderr. In some - # environments, anything printed to stderr is consider an error. To - # disable this behavior and write all logs to stdout (except errors - # which will still go to stderr), use: - - "-log_debug_stdout" - - # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" - - # [START cloud_sql_proxy_k8s_volume_mount] - # This flag specifies where the service account key can be found - - "-credential_file=/secrets/service_account.json" - securityContext: - # The default Cloud SQL proxy image runs as the - # "nonroot" user and group (uid: 65532) by default. - runAsNonRoot: true - volumeMounts: - - name: - mountPath: /secrets/ - readOnly: true - # [END cloud_sql_proxy_k8s_volume_mount] - # Resource configuration depends on an application's requirements. You - # should adjust the following values based on what your application - # needs. For details, see https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - resources: - requests: - # The proxy's memory use scales linearly with the number of active - # connections. Fewer open connections will use less memory. Adjust - # this value based on your application's requirements. - memory: "2Gi" - # The proxy's CPU use scales linearly with the amount of IO between - # the database and the application. Adjust this value based on your - # application's requirements. - cpu: "1" - # [START cloud_sql_proxy_k8s_volume_secret] - volumes: - - name: - secret: - secretName: - # [END cloud_sql_proxy_k8s_volume_secret] diff --git a/examples/k8s-sidecar/proxy_with_workload_identity.yaml b/examples/k8s-sidecar/proxy_with_workload_identity.yaml deleted file mode 100644 index b9eaa3ee96..0000000000 --- a/examples/k8s-sidecar/proxy_with_workload_identity.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START cloud_sql_proxy_k8s_sa] -apiVersion: apps/v1 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - template: - metadata: - labels: - app: - spec: - serviceAccountName: - # [END cloud_sql_proxy_k8s_sa] - # [START cloud_sql_proxy_k8s_secrets] - containers: - - name: - # ... other container configuration - env: - - name: DB_USER - valueFrom: - secretKeyRef: - name: - key: username - - name: DB_PASS - valueFrom: - secretKeyRef: - name: - key: password - - name: DB_NAME - valueFrom: - secretKeyRef: - name: - key: database - # [END cloud_sql_proxy_k8s_secrets] - # [START cloud_sql_proxy_k8s_container] - - name: cloud-sql-proxy - # It is recommended to use the latest version of the Cloud SQL proxy - # Make sure to update on a regular schedule! - image: gcr.io/cloudsql-docker/gce-proxy:1.28.0 # make sure the use the latest version - command: - - "/cloud_sql_proxy" - - # If connecting from a VPC-native GKE cluster, you can use the - # following flag to have the proxy connect over private IP - # - "-ip_address_types=PRIVATE" - - # By default, the proxy will write all logs to stderr. In some - # environments, anything printed to stderr is consider an error. To - # disable this behavior and write all logs to stdout (except errors - # which will still go to stderr), use: - - "-log_debug_stdout" - - # Replace DB_PORT with the port the proxy should listen on - # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 - - "-instances==tcp:" - securityContext: - # The default Cloud SQL proxy image runs as the - # "nonroot" user and group (uid: 65532) by default. - runAsNonRoot: true - # You should use resource requests/limits as a best practice to prevent - # pods from consuming too many resources and affecting the execution of - # other pods. You should adjust the following values based on what your - # application needs. For details, see - # https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - resources: - requests: - # The proxy's memory use scales linearly with the number of active - # connections. Fewer open connections will use less memory. Adjust - # this value based on your application's requirements. - memory: "2Gi" - # The proxy's CPU use scales linearly with the amount of IO between - # the database and the application. Adjust this value based on your - # application's requirements. - cpu: "1" - # [END cloud_sql_proxy_k8s_container] diff --git a/examples/k8s-sidecar/service_account.yaml b/examples/k8s-sidecar/service_account.yaml deleted file mode 100644 index d66893229b..0000000000 --- a/examples/k8s-sidecar/service_account.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START cloud_sql_proxy_k8s_sa_yml] -apiVersion: v1 -kind: ServiceAccount -metadata: - name: # TODO(developer): replace these values -# [END cloud_sql_proxy_k8s_sa_yml] \ No newline at end of file From ac462247e4c4b05b4c40e1562c92ba8a22d51ef0 Mon Sep 17 00:00:00 2001 From: Eno Compton Date: Wed, 13 Apr 2022 11:35:22 -0600 Subject: [PATCH 4/4] Remove Dockerfiles too --- Dockerfile | 28 ---------------------------- Dockerfile.alpine | 36 ------------------------------------ Dockerfile.buster | 34 ---------------------------------- 3 files changed, 98 deletions(-) delete mode 100644 Dockerfile delete mode 100644 Dockerfile.alpine delete mode 100644 Dockerfile.buster diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4e1652f0e6..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2019 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Use the latest stable golang 1.x to compile to a binary -FROM golang:1 as build - -WORKDIR /go/src/cloudsql-proxy -COPY . . - -RUN go get ./... -RUN CGO_ENABLED=0 go build -ldflags "-X main.metadataString=container" -o cloud_sql_proxy ./cmd/cloud_sql_proxy - -# Final Stage -FROM gcr.io/distroless/static:nonroot -COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy -# set the uid as an integer for compatibility with runAsNonRoot in Kubernetes -USER 65532 diff --git a/Dockerfile.alpine b/Dockerfile.alpine deleted file mode 100644 index bb742e24b7..0000000000 --- a/Dockerfile.alpine +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Use the latest stable golang 1.x to compile to a binary -FROM golang:1 as build - -WORKDIR /go/src/cloudsql-proxy -COPY . . - -RUN go get ./... -RUN go build -ldflags "-X main.metadataString=container.alpine" -o cloud_sql_proxy ./cmd/cloud_sql_proxy - -# Final stage -FROM alpine:3 -RUN apk add --no-cache \ - ca-certificates \ - libc6-compat -# Install fuse and allow enable non-root users to mount -RUN apk add --no-cache fuse && sed -i 's/^#user_allow_other$/user_allow_other/g' /etc/fuse.conf -# Add a non-root user matching the nonroot user from the main container -RUN addgroup -g 65532 -S nonroot && adduser -u 65532 -S nonroot -G nonroot -# Set the uid as an integer for compatibility with runAsNonRoot in Kubernetes -USER 65532 - -COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy diff --git a/Dockerfile.buster b/Dockerfile.buster deleted file mode 100644 index d24a0dab12..0000000000 --- a/Dockerfile.buster +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Use the latest stable golang 1.x to compile to a binary -FROM golang:1 as build - -WORKDIR /go/src/cloudsql-proxy -COPY . . - -RUN go get ./... -RUN go build -ldflags "-X main.metadataString=container.buster" -o cloud_sql_proxy ./cmd/cloud_sql_proxy - -# Final stage -FROM debian:buster -RUN apt-get update && apt-get install -y ca-certificates -# Install fuse and allow enable non-root users to mount -RUN apt-get update && apt-get install -y fuse && sed -i 's/^#user_allow_other$/user_allow_other/g' /etc/fuse.conf -# Add a non-root user matching the nonroot user from the main container -RUN groupadd -g 65532 -r nonroot && useradd -u 65532 -g 65532 -r nonroot -# Set the uid as an integer for compatibility with runAsNonRoot in Kubernetes -USER 65532 - -COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy